Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
17a0c00
feat(parser): new style parser
tenphi May 26, 2025
e5d4739
feat(parser): new style parser * 2
tenphi May 26, 2025
c154597
feat(parser): new style parser * 3
tenphi May 26, 2025
ded64db
feat(parser): new style parser * 4
tenphi May 26, 2025
b93f994
feat(parser): new style parser * 5
tenphi May 27, 2025
d74a1f5
feat(parser): new style parser * 6
tenphi May 27, 2025
7022d19
fix(Slider): wrong style declaration
tenphi May 27, 2025
e926da1
fix(Slider): wrong styles
tenphi May 27, 2025
35576fe
fix(Slider): wrong ref
tenphi May 27, 2025
5e4291b
feat(parser): new style parser * 7
tenphi May 27, 2025
000e53f
feat(parser): new style parser * 8
tenphi May 27, 2025
f6faeaa
feat(parser): new style parser * 9
tenphi May 27, 2025
a8ea8dd
chore: update config
tenphi May 28, 2025
64d0ba3
chore: update config * 2
tenphi May 28, 2025
759ace5
chore: add test for parser
tenphi Jun 3, 2025
e8b6788
Update src/tasty/styles/border.ts
tenphi Jun 4, 2025
02c7fed
chore: styles util clean up
tenphi Jun 6, 2025
3cd9fd8
fix(border.style): type
tenphi Jun 6, 2025
346792e
fix(scrollnar.style): none keyword
tenphi Jun 6, 2025
5f8ef40
Merge remote-tracking branch 'origin' into new-style-parser
tenphi Jun 6, 2025
ae4079a
fix(Tooltip): drop-shadow declaration
tenphi Jun 6, 2025
5dfb93c
Merge remote-tracking branch 'origin' into new-style-parser
tenphi Aug 5, 2025
639e3b5
fix: new var syntax
tenphi Aug 5, 2025
cd64a07
chore: fix cursor rules
tenphi Aug 5, 2025
e0bf8f2
chore: update size-limit
tenphi Aug 5, 2025
91c7e62
chore: update node, pnpm, add changelog
tenphi Aug 5, 2025
e2b67aa
chore: update github actions to support new node and pnpm versions
tenphi Aug 5, 2025
1444831
chore: update deps
tenphi Aug 5, 2025
186f483
chore: update ci test command
tenphi Aug 5, 2025
a9ce99e
chore: add vercel config
tenphi Aug 5, 2025
d4dc30d
chore: update vercel config
tenphi Aug 5, 2025
8c8f1f5
chore: update vercel config
tenphi Aug 5, 2025
f882118
fix(ComboBox): remove modAttr helper
tenphi Aug 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .cursor/rules/parser.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
description: If the task requires information about how styles are parsed in `tasty`.
globs:
alwaysApply: false
---
The specification of the style parser is described in [parser.md](mdc:src/parser/parser.md)
This part of the styles handling only covers the parsing of string values. Though, boolean and number styles can be converted to string.
Style-2-state mapping and responsive values are handled separately in [styles.ts](mdc:src/tasty/utils/styles.ts) and [responsive.ts](mdc:src/tasty/utils/responsive.ts)
6 changes: 6 additions & 0 deletions .cursor/rules/tasty.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
description: If it requires the understanding of `tasty` helper API.
globs:
alwaysApply: false
---
The API of `tasty` helper is described in [tasty.md](mdc:tasty.md)
4 changes: 2 additions & 2 deletions .size-limit.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ module.exports = [
path: './dist/es/index.js',
webpack: true,
import: '{ Button }',
limit: '23 kB',
limit: '24 kB',
},
{
name: 'Tree shaking (just an Icon)',
path: './dist/es/index.js',
webpack: true,
import: '{ AiIcon }',
limit: '12 kB',
limit: '13 kB',
},
];
6 changes: 6 additions & 0 deletions src/components/fields/Slider/RangeSlider.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,9 @@ WithoutValue.args = {
label: 'Slider',
showValueLabel: false,
};

export const Vertical = Template.bind({});
Vertical.args = {
label: 'Slider',
orientation: 'vertical',
};
25 changes: 18 additions & 7 deletions src/components/fields/Slider/RangeSlider.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { forwardRef } from 'react';
import { forwardRef, useRef } from 'react';

import { Gradation } from './Gradation';
import { SliderBase, SliderBaseChildArguments } from './SliderBase';
import { SliderThumb } from './SliderThumb';
import { SliderTrack } from './SliderTrack';

import type { DOMRef } from '@react-types/shared';
import type { FocusableRef } from '@react-types/shared';
import type { RangeValue } from '../../../shared';
import type { CubeSliderBaseProps } from './types';

Expand All @@ -19,29 +19,40 @@ const INTL_MESSAGES = {
maximum: 'Maximum',
};

function RangeSlider(props: CubeRangeSliderProps, ref: DOMRef<HTMLDivElement>) {
function RangeSlider(
props: CubeRangeSliderProps,
ref: FocusableRef<HTMLDivElement>,
) {
let { isDisabled, styles, gradation, ...otherProps } = props;

// Create separate refs for each thumb to enable proper focus management
const minThumbInputRef = useRef<HTMLInputElement>(null);
const maxThumbInputRef = useRef<HTMLInputElement>(null);

return (
<SliderBase {...(otherProps as CubeSliderBaseProps<number[]>)}>
<SliderBase ref={ref} {...(otherProps as CubeSliderBaseProps<number[]>)}>
{({ trackRef, inputRef, state }: SliderBaseChildArguments) => {
return (
<>
<Gradation state={state} ranges={[0, 1]} values={gradation} />
<SliderTrack state={state} isDisabled={isDisabled} />
<SliderTrack
state={state}
isDisabled={isDisabled}
orientation={props.orientation}
/>
<SliderThumb
index={0}
aria-label={INTL_MESSAGES['minimum']}
state={state}
inputRef={inputRef}
inputRef={minThumbInputRef}
trackRef={trackRef}
isDisabled={isDisabled}
/>
<SliderThumb
index={1}
aria-label={INTL_MESSAGES['maximum']}
state={state}
inputRef={inputRef}
inputRef={maxThumbInputRef}
trackRef={trackRef}
isDisabled={isDisabled}
/>
Expand Down
4 changes: 3 additions & 1 deletion src/components/fields/Slider/SliderTrack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export function SliderTrack(props: SliderTrackProps) {
'--slider-range-start': `${selectedTrack[0] * 100}%`,
'--slider-range-end': `${selectedTrack[1] * 100}%`,
}
: {}
: {
'--slider-value': `${selectedTrack[0] * 100}%`,
}
}
/>
);
Expand Down
36 changes: 26 additions & 10 deletions src/components/fields/Slider/elements.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { tasty } from '../../../tasty';

export const SliderThumbElement = tasty({
qa: 'SliderThumb',
styles: {
top: '@slider-thumb-offset-top',
left: '@slider-thumb-offset-left',
Expand Down Expand Up @@ -28,6 +29,7 @@ export const SliderThumbElement = tasty({
});

export const SliderTrackContainerElement = tasty({
qa: 'SliderTrackContainer',
styles: {
top: {
'': '0',
Expand All @@ -51,21 +53,33 @@ export const SliderTrackContainerElement = tasty({

'&::before': {
content: '""',
display: {
'': 'none',
range: 'block',
},
display: 'block',
position: 'absolute',
top: 0,
bottom: 0,
inset: {
'': 'auto 0 0 0',
horizontal: '0 auto 0 0',
range: 'auto 0 @slider-range-start 0',
'range & horizontal': '0 auto 0 @slider-range-start',
},
fill: '#purple',
left: '@slider-range-start',
width: '(@slider-range-end - @slider-range-start)',
width: {
'': 'auto',
horizontal: '@slider-value',
range: 'auto',
'range & horizontal': '(@slider-range-end - @slider-range-start)',
},
height: {
'': '@slider-value',
horizontal: 'auto',
range: '(@slider-range-end - @slider-range-start)',
'range & horizontal': 'auto',
},
},
},
});

export const SliderGradationElement = tasty({
qa: 'SliderGradation',
styles: {
position: 'absolute',
top: '2x',
Expand All @@ -78,6 +92,7 @@ export const SliderGradationElement = tasty({
});

export const SliderGradeElement = tasty({
qa: 'SliderGrade',
styles: {
display: 'grid',
width: 'max 0',
Expand All @@ -88,6 +103,7 @@ export const SliderGradeElement = tasty({
});

export const SliderControlsElement = tasty({
qa: 'SliderControls',
styles: {
position: 'relative',
height: {
Expand All @@ -96,7 +112,7 @@ export const SliderControlsElement = tasty({
},
width: {
'': '2x',
horizontal: '100% - 2x',
horizontal: '(100% - 2x)',
},

'@slider-thumb-offset-top': {
Expand Down Expand Up @@ -130,7 +146,7 @@ export const SliderElement = tasty({
},

alignItems: 'center',
flexDirection: {
flow: {
'': 'column',
inputs: 'row',
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/Prefix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const Prefix = forwardRef(function Prefix(
styles={styles}
style={{
// @ts-ignore
'--prefix-gap': parseStyle(outerGap).value,
'--prefix-gap': parseStyle(outerGap).output,
}}
>
{children}
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/Suffix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const Suffix = forwardRef(function Suffix(
ref={ref}
styles={styles}
style={{
'--suffix-gap': parseStyle(outerGap).value,
'--suffix-gap': parseStyle(outerGap).output,
}}
>
{children}
Expand Down
174 changes: 174 additions & 0 deletions src/parser/classify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import {
COLOR_FUNCS,
RE_HEX,
RE_NUMBER,
RE_UNIT_NUM,
VALUE_KEYWORDS,
} from './const';
import { StyleParser } from './parser';
import { Bucket, ParserOptions, ProcessedStyle } from './types';

export function classify(
raw: string,
opts: ParserOptions,
recurse: (str: string) => ProcessedStyle,
): { bucket: Bucket; processed: string } {
const token = raw.trim();
if (!token) return { bucket: Bucket.Mod, processed: '' };

// Quoted string literals should be treated as value tokens (e.g., "" for content)
if (
(token.startsWith('"') && token.endsWith('"')) ||
(token.startsWith("'") && token.endsWith("'"))
) {
return { bucket: Bucket.Value, processed: token };
}

// 0. Direct var(--*-color) token
const varColorMatch = token.match(/^var\(--([a-z0-9-]+)-color\)$/);
if (varColorMatch) {
return { bucket: Bucket.Color, processed: token };
}

// 1. URL
if (token.startsWith('url(')) {
return { bucket: Bucket.Value, processed: token };
}

// 2. Custom property
if (token[0] === '@') {
const match = token.match(/^@\(([a-z0-9-_]+)\s*,\s*(.*)\)$/);
if (match) {
const [, name, fallback] = match;
const processedFallback = recurse(fallback).output;
return {
bucket: Bucket.Value,
processed: `var(--${name}, ${processedFallback})`,
};
}
const identMatch = token.match(/^@([a-z0-9-_]+)$/);
if (identMatch) {
const name = identMatch[1];
const processed = `var(--${name})`;
const bucketType = name.endsWith('-color') ? Bucket.Color : Bucket.Value;
return {
bucket: bucketType,
processed,
};
}
// invalid custom property → modifier
}

// 3. Hash colors (with optional alpha suffix e.g., #purple.5)
if (token[0] === '#' && token.length > 1) {
// alpha form: #name.alpha
const alphaMatch = token.match(/^#([a-z0-9-]+)\.([0-9]+)$/i);
if (alphaMatch) {
const [, base, rawAlpha] = alphaMatch;
let alpha: string;
if (rawAlpha === '0') alpha = '0';
else alpha = `.${rawAlpha}`;
return {
bucket: Bucket.Color,
processed: `rgb(var(--${base}-color-rgb) / ${alpha})`,
};
}

// hyphenated names like #dark-05 should keep full name

const name = token.slice(1);
// valid hex → treat as hex literal with fallback
if (RE_HEX.test(name)) {
return {
bucket: Bucket.Color,
processed: `var(--${name}-color, #${name})`,
};
}
// simple color name token → css variable lookup with rgb fallback
return { bucket: Bucket.Color, processed: `var(--${name}-color)` };
}

// 4 & 5. Functions
const openIdx = token.indexOf('(');
if (openIdx > 0 && token.endsWith(')')) {
const fname = token.slice(0, openIdx);
const inner = token.slice(openIdx + 1, -1); // without ()

if (COLOR_FUNCS.has(fname)) {
// Process inner to expand nested colors or units.
const argProcessed = recurse(inner).output.replace(/,\s+/g, ','); // color funcs expect no spaces after commas
return { bucket: Bucket.Color, processed: `${fname}(${argProcessed})` };
}

// user function (provided via opts)
if (opts.funcs && fname in opts.funcs) {
// split by top-level commas within inner
const tmp = new StyleParser(opts).process(inner); // fresh parser w/ same opts but no cache share issues
const argProcessed = opts.funcs[fname](tmp.groups);
return { bucket: Bucket.Value, processed: argProcessed };
}

// generic: process inner and rebuild
const argProcessed = recurse(inner).output;
return { bucket: Bucket.Value, processed: `${fname}(${argProcessed})` };
}

// 6. Auto-calc group
if (token[0] === '(' && token[token.length - 1] === ')') {
const inner = token.slice(1, -1);
const innerProcessed = recurse(inner).output;
return { bucket: Bucket.Value, processed: `calc(${innerProcessed})` };
}

// 7. Unit number
const um = token.match(RE_UNIT_NUM);
if (um) {
const unit = um[1];
const numericPart = parseFloat(token.slice(0, -unit.length));
const handler = opts.units && opts.units[unit];
if (handler) {
if (typeof handler === 'string') {
// Special-case the common `x` → gap mapping used by tests.
const base = unit === 'x' ? 'var(--gap)' : handler;
if (numericPart === 1) {
return { bucket: Bucket.Value, processed: base };
}
return {
bucket: Bucket.Value,
processed: `calc(${numericPart} * ${base})`,
};
} else {
const inner = handler(numericPart);
// Avoid double wrapping if handler already returns a calc(...)
return {
bucket: Bucket.Value,
processed: inner.startsWith('calc(') ? inner : `calc(${inner})`,
};
}
}
}

// 7b. Unknown numeric+unit → treat as literal value (e.g., 1fr)
if (/^[+-]?(?:\d*\.\d+|\d+)[a-z%]+$/.test(token)) {
return { bucket: Bucket.Value, processed: token };
}

// 7c. Plain unit-less numbers should be treated as value tokens so that
// code such as `scrollbar={10}` resolves correctly.
if (RE_NUMBER.test(token)) {
return { bucket: Bucket.Value, processed: token };
}

// 8. Literal value keywords
if (VALUE_KEYWORDS.has(token)) {
return { bucket: Bucket.Value, processed: token };
}

// 8b. Special keyword colors
if (token === 'transparent' || token === 'currentcolor') {
return { bucket: Bucket.Color, processed: token };
}

// 9. Fallback modifier
return { bucket: Bucket.Mod, processed: token };
}
Loading
Loading