@tenphi/tasty 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -15
- package/dist/config.d.ts +8 -0
- package/dist/config.js.map +1 -1
- package/dist/hooks/useStyles.js +5 -5
- package/dist/hooks/useStyles.js.map +1 -1
- package/dist/injector/injector.js +38 -25
- package/dist/injector/injector.js.map +1 -1
- package/dist/injector/sheet-manager.d.ts +12 -4
- package/dist/injector/sheet-manager.js +23 -9
- package/dist/injector/sheet-manager.js.map +1 -1
- package/dist/injector/types.d.ts +9 -0
- package/dist/properties/index.js +80 -17
- package/dist/properties/index.js.map +1 -1
- package/dist/properties/property-type-resolver.d.ts +24 -0
- package/dist/properties/property-type-resolver.js +83 -0
- package/dist/properties/property-type-resolver.js.map +1 -0
- package/dist/styles/fill.js +6 -5
- package/dist/styles/fill.js.map +1 -1
- package/dist/utils/styles.js +161 -0
- package/dist/utils/styles.js.map +1 -1
- package/dist/zero/babel.d.ts +5 -0
- package/dist/zero/babel.js +13 -7
- package/dist/zero/babel.js.map +1 -1
- package/dist/zero/extractor.js +66 -1
- package/dist/zero/extractor.js.map +1 -1
- package/docs/configuration.md +211 -0
- package/docs/debug.md +505 -0
- package/docs/injector.md +528 -0
- package/docs/styles.md +567 -0
- package/docs/tasty-static.md +376 -0
- package/docs/usage.md +643 -0
- package/package.json +5 -4
package/docs/usage.md
ADDED
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
# Usage Guide
|
|
2
|
+
|
|
3
|
+
`tasty` is a powerful utility for creating styled React components with a declarative, design-system-integrated API. It combines the flexibility of CSS-in-JS with the consistency of a design system, enabling you to build maintainable, themeable components quickly.
|
|
4
|
+
|
|
5
|
+
For global configuration (tokens, recipes, custom units, handlers), see **[Configuration](configuration.md)**.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
### Creating Your First Component
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
import { tasty } from '@tenphi/tasty';
|
|
15
|
+
|
|
16
|
+
// Basic styled component
|
|
17
|
+
const Card = tasty({
|
|
18
|
+
as: 'div',
|
|
19
|
+
styles: {
|
|
20
|
+
padding: '4x',
|
|
21
|
+
fill: '#white',
|
|
22
|
+
border: true,
|
|
23
|
+
radius: true,
|
|
24
|
+
},
|
|
25
|
+
styleProps: ['padding', 'fill'], // Expose styles as props
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Usage
|
|
29
|
+
<Card>Hello World</Card>
|
|
30
|
+
<Card padding="6x" fill="#gray.05">Custom Card</Card>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Extending Existing Components
|
|
34
|
+
|
|
35
|
+
> **Best Practice:** Always prefer creating styled wrappers over using the `styles` prop directly.
|
|
36
|
+
|
|
37
|
+
```jsx
|
|
38
|
+
// Recommended
|
|
39
|
+
const PrimaryButton = tasty(Button, {
|
|
40
|
+
styles: {
|
|
41
|
+
fill: '#purple',
|
|
42
|
+
color: '#white',
|
|
43
|
+
padding: '2x 4x',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Avoid
|
|
48
|
+
<Button styles={{ fill: '#purple' }}>Click me</Button>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
#### Extending vs. Replacing State Maps
|
|
52
|
+
|
|
53
|
+
When a style property uses a state map, the merge behavior depends on whether the child provides a `''` (default) key:
|
|
54
|
+
|
|
55
|
+
- **No `''` key** — extend mode: parent states are preserved, child adds/overrides
|
|
56
|
+
- **Has `''` key** — replace mode: child defines everything from scratch
|
|
57
|
+
|
|
58
|
+
```jsx
|
|
59
|
+
// Parent has: fill: { '': '#white', hovered: '#blue', disabled: '#gray' }
|
|
60
|
+
|
|
61
|
+
// Extend — no '' key, parent states preserved
|
|
62
|
+
const MyButton = tasty(Button, {
|
|
63
|
+
styles: {
|
|
64
|
+
fill: {
|
|
65
|
+
'loading': '#yellow', // append new state
|
|
66
|
+
'disabled': '#gray.20', // override existing state in place
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Replace — has '' key, parent states dropped
|
|
72
|
+
const MyButton = tasty(Button, {
|
|
73
|
+
styles: {
|
|
74
|
+
fill: {
|
|
75
|
+
'': '#red',
|
|
76
|
+
'hovered': '#blue',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Use `'@inherit'` to pull a parent state value. In extend mode it repositions the state; in replace mode it cherry-picks it:
|
|
83
|
+
|
|
84
|
+
```jsx
|
|
85
|
+
// Extend mode: reposition disabled to end (highest CSS priority)
|
|
86
|
+
fill: {
|
|
87
|
+
'loading': '#yellow',
|
|
88
|
+
disabled: '@inherit',
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Replace mode: cherry-pick disabled from parent
|
|
92
|
+
fill: {
|
|
93
|
+
'': '#red',
|
|
94
|
+
disabled: '@inherit',
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Use `null` inside a state map to remove a state, or `false` to block it entirely (tombstone):
|
|
99
|
+
|
|
100
|
+
```jsx
|
|
101
|
+
fill: { pressed: null } // removes pressed from the result
|
|
102
|
+
fill: { disabled: false } // tombstone — no CSS for disabled, blocks recipe too
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### Resetting Properties with `null` and `false`
|
|
106
|
+
|
|
107
|
+
```jsx
|
|
108
|
+
const SimpleButton = tasty(Button, {
|
|
109
|
+
styles: {
|
|
110
|
+
fill: null, // discard parent's fill, let recipe fill in
|
|
111
|
+
border: false, // no border at all (tombstone — blocks recipe too)
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
| Value | Meaning | Recipe fills in? |
|
|
117
|
+
|-------|---------|-----------------|
|
|
118
|
+
| `undefined` | Not provided — parent preserved | N/A |
|
|
119
|
+
| `null` | Intentional unset — parent discarded | Yes |
|
|
120
|
+
| `false` | Tombstone — blocks everything | No |
|
|
121
|
+
|
|
122
|
+
### Essential Patterns
|
|
123
|
+
|
|
124
|
+
```jsx
|
|
125
|
+
// State-based styling
|
|
126
|
+
const InteractiveCard = tasty({
|
|
127
|
+
styles: {
|
|
128
|
+
fill: {
|
|
129
|
+
'': '#white',
|
|
130
|
+
'hovered': '#gray.05',
|
|
131
|
+
'pressed': '#gray.10',
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Using design tokens
|
|
137
|
+
const TokenCard = tasty({
|
|
138
|
+
styles: {
|
|
139
|
+
fill: '#surface', // Color token
|
|
140
|
+
color: '#text', // Color token
|
|
141
|
+
padding: '2x', // Custom unit (gap × 2)
|
|
142
|
+
radius: '1r', // Custom unit (border-radius)
|
|
143
|
+
border: '1bw solid #border', // Border width token
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Configuration
|
|
151
|
+
|
|
152
|
+
For tokens, recipes, custom units, style handlers, and other global settings, see **[Configuration](configuration.md)**.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Dictionary
|
|
157
|
+
|
|
158
|
+
### Style Mapping
|
|
159
|
+
|
|
160
|
+
Object where keys represent states and values are the styles to apply:
|
|
161
|
+
|
|
162
|
+
```jsx
|
|
163
|
+
fill: { '': '#white', hovered: '#gray.05', 'theme=danger': '#red' }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### State Key Types
|
|
167
|
+
|
|
168
|
+
| Syntax | Example | Generated CSS |
|
|
169
|
+
|--------|---------|---------------|
|
|
170
|
+
| Boolean modifier | `hovered` | `[data-hovered]` |
|
|
171
|
+
| Value modifier | `theme=danger` | `[data-theme="danger"]` |
|
|
172
|
+
| Pseudo-class | `:hover` | `:hover` |
|
|
173
|
+
| Class selector | `.active` | `.active` |
|
|
174
|
+
| Attribute selector | `[aria-expanded="true"]` | `[aria-expanded="true"]` |
|
|
175
|
+
| Combined | `hovered & .active` | `[data-hovered].active` |
|
|
176
|
+
|
|
177
|
+
### Sub-element
|
|
178
|
+
|
|
179
|
+
Element styled using a capitalized key. Identified by `data-element` attribute:
|
|
180
|
+
|
|
181
|
+
```jsx
|
|
182
|
+
styles: { Title: { preset: 'h3' } }
|
|
183
|
+
// Targets: <div data-element="Title">
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Color Token
|
|
187
|
+
|
|
188
|
+
Named color prefixed with `#` that maps to CSS custom properties. Supports opacity with `.N` suffix:
|
|
189
|
+
|
|
190
|
+
```jsx
|
|
191
|
+
fill: '#purple.5' // → var(--purple-color) with 50% opacity
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Modifier
|
|
195
|
+
|
|
196
|
+
State value via `mods` prop that generates `data-*` attributes:
|
|
197
|
+
|
|
198
|
+
```jsx
|
|
199
|
+
mods={{ hovered: true, theme: 'danger' }}
|
|
200
|
+
// → data-hovered="" data-theme="danger"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Core Concepts
|
|
206
|
+
|
|
207
|
+
### Component Creation
|
|
208
|
+
|
|
209
|
+
```jsx
|
|
210
|
+
// Create new element
|
|
211
|
+
const Box = tasty({
|
|
212
|
+
as: 'div',
|
|
213
|
+
styles: { /* styles */ },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Extend existing component
|
|
217
|
+
const StyledButton = tasty(Button, {
|
|
218
|
+
styles: { /* additional styles */ },
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Style Props
|
|
223
|
+
|
|
224
|
+
Use `styleProps` to expose style properties as direct component props:
|
|
225
|
+
|
|
226
|
+
```jsx
|
|
227
|
+
const FlexibleBox = tasty({
|
|
228
|
+
as: 'div',
|
|
229
|
+
styles: {
|
|
230
|
+
display: 'flex',
|
|
231
|
+
padding: '2x',
|
|
232
|
+
},
|
|
233
|
+
styleProps: ['gap', 'align', 'placeContent', 'fill'],
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
<FlexibleBox gap="2x" align="center" fill="#surface">
|
|
237
|
+
Content
|
|
238
|
+
</FlexibleBox>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Color Tokens & Opacity
|
|
242
|
+
|
|
243
|
+
```jsx
|
|
244
|
+
color: '#purple', // Full opacity
|
|
245
|
+
color: '#purple.5', // 50% opacity
|
|
246
|
+
color: '#purple.05', // 5% opacity
|
|
247
|
+
fill: '#current', // → currentcolor
|
|
248
|
+
fill: '#current.5', // → color-mix(in oklab, currentcolor 50%, transparent)
|
|
249
|
+
color: '(#primary, #secondary)', // Fallback syntax
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Built-in Units
|
|
253
|
+
|
|
254
|
+
| Unit | Description | Example | CSS Output |
|
|
255
|
+
|------|-------------|---------|------------|
|
|
256
|
+
| `x` | Gap multiplier | `2x` | `calc(var(--gap) * 2)` |
|
|
257
|
+
| `r` | Border radius | `1r` | `var(--radius)` |
|
|
258
|
+
| `cr` | Card border radius | `1cr` | `var(--card-radius)` |
|
|
259
|
+
| `bw` | Border width | `2bw` | `calc(var(--border-width) * 2)` |
|
|
260
|
+
| `ow` | Outline width | `1ow` | `var(--outline-width)` |
|
|
261
|
+
| `fs` | Font size | `1fs` | `var(--font-size)` |
|
|
262
|
+
| `lh` | Line height | `1lh` | `var(--line-height)` |
|
|
263
|
+
| `sf` | Stable fraction | `1sf` | `minmax(0, 1fr)` |
|
|
264
|
+
|
|
265
|
+
You can register additional custom units via [`configure()`](configuration.md#options).
|
|
266
|
+
|
|
267
|
+
### Predefined Tokens
|
|
268
|
+
|
|
269
|
+
Tokens defined via [`configure({ tokens })`](configuration.md#predefined-tokens) are replaced at parse time and baked into the generated CSS:
|
|
270
|
+
|
|
271
|
+
```jsx
|
|
272
|
+
const Card = tasty({
|
|
273
|
+
styles: {
|
|
274
|
+
padding: '$card-padding',
|
|
275
|
+
fill: '#surface',
|
|
276
|
+
border: '1bw solid #accent',
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Recipes
|
|
282
|
+
|
|
283
|
+
Apply predefined style bundles (defined via [`configure({ recipes })`](configuration.md#recipes)) using the `recipe` style property:
|
|
284
|
+
|
|
285
|
+
```jsx
|
|
286
|
+
const Card = tasty({
|
|
287
|
+
styles: {
|
|
288
|
+
recipe: 'card',
|
|
289
|
+
color: '#text',
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Compose multiple recipes
|
|
294
|
+
const ElevatedCard = tasty({
|
|
295
|
+
styles: {
|
|
296
|
+
recipe: 'card elevated',
|
|
297
|
+
color: '#text',
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Post-merge recipes (`/` separator):**
|
|
303
|
+
|
|
304
|
+
Recipes listed after `/` are applied *after* component styles using `mergeStyles`:
|
|
305
|
+
|
|
306
|
+
```jsx
|
|
307
|
+
const Input = tasty({
|
|
308
|
+
styles: {
|
|
309
|
+
recipe: 'reset input / input-autofill',
|
|
310
|
+
preset: 't3',
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Use `none` to skip base recipes and apply only post recipes:
|
|
316
|
+
|
|
317
|
+
```jsx
|
|
318
|
+
const Custom = tasty({
|
|
319
|
+
styles: {
|
|
320
|
+
recipe: 'none / disabled',
|
|
321
|
+
padding: '2x',
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Advanced States (`@` prefix)
|
|
327
|
+
|
|
328
|
+
| Prefix | Purpose | Example |
|
|
329
|
+
|--------|---------|---------|
|
|
330
|
+
| `@media` | Media queries | `@media(w < 768px)` |
|
|
331
|
+
| `@(...)` | Container queries | `@(panel, w >= 300px)` |
|
|
332
|
+
| `@supports` | Feature/selector support | `@supports(display: grid)` |
|
|
333
|
+
| `@root` | Root element states | `@root(theme=dark)` |
|
|
334
|
+
| `@parent` | Parent/ancestor element states | `@parent(hovered)` |
|
|
335
|
+
| `@own` | Sub-element's own state | `@own(hovered)` |
|
|
336
|
+
| `@starting` | Entry animation | `@starting` |
|
|
337
|
+
|
|
338
|
+
#### `@parent(...)` — Parent Element States
|
|
339
|
+
|
|
340
|
+
Style based on ancestor element attributes. Uses `:is([selector] *)` / `:not([selector] *)` for symmetric, composable parent checks. Boolean logic (`&`, `|`, `!`) is supported inside `@parent()`.
|
|
341
|
+
|
|
342
|
+
```jsx
|
|
343
|
+
const Highlight = tasty({
|
|
344
|
+
styles: {
|
|
345
|
+
fill: {
|
|
346
|
+
'': '#white',
|
|
347
|
+
'@parent(hovered)': '#gray.05', // Any ancestor has [data-hovered]
|
|
348
|
+
'@parent(theme=dark, >)': '#dark-02', // Direct parent has [data-theme="dark"]
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
| Syntax | CSS Output |
|
|
355
|
+
|--------|------------|
|
|
356
|
+
| `@parent(hovered)` | `:is([data-hovered] *)` |
|
|
357
|
+
| `!@parent(hovered)` | `:not([data-hovered] *)` |
|
|
358
|
+
| `@parent(hovered, >)` | `:is([data-hovered] > *)` (direct parent) |
|
|
359
|
+
| `@parent(.active)` | `:is(.active *)` |
|
|
360
|
+
| `@parent(hovered & focused)` | `:is([data-hovered][data-focused] *)` (same ancestor) |
|
|
361
|
+
| `@parent(hovered) & @parent(focused)` | `:is([data-hovered] *):is([data-focused] *)` (independent ancestors) |
|
|
362
|
+
| `@parent(hovered \| focused)` | `:is([data-hovered] *)`, `:is([data-focused] *)` (OR variants) |
|
|
363
|
+
|
|
364
|
+
For sub-elements, the parent check applies to the root element's ancestors:
|
|
365
|
+
|
|
366
|
+
```jsx
|
|
367
|
+
const Card = tasty({
|
|
368
|
+
styles: {
|
|
369
|
+
Label: {
|
|
370
|
+
color: {
|
|
371
|
+
'': '#text',
|
|
372
|
+
'@parent(hovered)': '#primary',
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
// → .t0.t0:is([data-hovered] *) [data-element="Label"]
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Style Properties
|
|
383
|
+
|
|
384
|
+
For a complete reference of all enhanced style properties — syntax, values, modifiers, and recommendations — see **[Style Properties Reference](styles.md)**.
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Advanced Features
|
|
389
|
+
|
|
390
|
+
### Keyframes
|
|
391
|
+
|
|
392
|
+
```jsx
|
|
393
|
+
const Pulse = tasty({
|
|
394
|
+
styles: {
|
|
395
|
+
animation: 'pulse 2s infinite',
|
|
396
|
+
'@keyframes': {
|
|
397
|
+
pulse: {
|
|
398
|
+
'0%, 100%': { transform: 'scale(1)' },
|
|
399
|
+
'50%': { transform: 'scale(1.05)' },
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Properties (`@property`)
|
|
407
|
+
|
|
408
|
+
CSS cannot transition or animate custom properties unless the browser knows their type. Tasty solves this automatically — when you assign a concrete value to a custom property, the type is inferred and a CSS `@property` rule is registered behind the scenes:
|
|
409
|
+
|
|
410
|
+
```jsx
|
|
411
|
+
const AnimatedGradient = tasty({
|
|
412
|
+
styles: {
|
|
413
|
+
'$gradient-angle': '0deg',
|
|
414
|
+
'#theme': 'okhsl(280 80% 50%)',
|
|
415
|
+
background: 'linear-gradient($gradient-angle, #theme, transparent)',
|
|
416
|
+
transition: '$$gradient-angle 0.3s, ##theme 0.3s',
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Here `$gradient-angle: '0deg'` is detected as `<angle>` and `#theme` as `<color>` (via the `#name` naming convention), so both transitions work without any manual `@property` declarations. Numeric types (`<number>`, `<length>`, `<percentage>`, `<angle>`, `<time>`) are inferred from values; `<color>` is inferred from `#name` tokens.
|
|
422
|
+
|
|
423
|
+
Use explicit `@properties` when you need non-default settings like `inherits: false`:
|
|
424
|
+
|
|
425
|
+
```jsx
|
|
426
|
+
'@properties': {
|
|
427
|
+
'$gradient-angle': { syntax: '<angle>', inherits: false, initialValue: '0deg' },
|
|
428
|
+
},
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Variants & Theming
|
|
432
|
+
|
|
433
|
+
```jsx
|
|
434
|
+
const Button = tasty({
|
|
435
|
+
styles: {
|
|
436
|
+
padding: '2x 4x',
|
|
437
|
+
border: true,
|
|
438
|
+
},
|
|
439
|
+
variants: {
|
|
440
|
+
default: { fill: '#blue', color: '#white' },
|
|
441
|
+
danger: { fill: '#red', color: '#white' },
|
|
442
|
+
outline: { fill: 'transparent', color: '#blue', border: '1bw solid #blue' },
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
<Button variant="danger">Delete</Button>
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
#### Extending Variants with Base State Maps
|
|
450
|
+
|
|
451
|
+
When base `styles` contain an extend-mode state map (an object **without** a `''` key), it is applied **after** the variant merge. This lets you add or override states across all variants without repeating yourself:
|
|
452
|
+
|
|
453
|
+
```jsx
|
|
454
|
+
const Badge = tasty({
|
|
455
|
+
styles: {
|
|
456
|
+
padding: '1x 2x',
|
|
457
|
+
// No '' key → extend mode: appended to every variant's border
|
|
458
|
+
border: {
|
|
459
|
+
'type=primary': '#clear',
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
variants: {
|
|
463
|
+
primary: {
|
|
464
|
+
border: { '': '#white.2', pressed: '#primary-text', disabled: '#clear' },
|
|
465
|
+
fill: { '': '#white #primary', hovered: '#white #primary-text' },
|
|
466
|
+
},
|
|
467
|
+
secondary: {
|
|
468
|
+
border: { '': '#primary.15', pressed: '#primary.3' },
|
|
469
|
+
fill: '#primary.10',
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Both variants get 'type=primary': '#clear' appended to their border map
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Properties that are **not** extend-mode (simple values, state maps with `''`, `null`, `false`, selectors, sub-elements) merge with variants as before — the variant can fully replace them.
|
|
478
|
+
|
|
479
|
+
### Sub-element Styling
|
|
480
|
+
|
|
481
|
+
Sub-elements are inner parts of a compound component, styled via capitalized keys in `styles` and identified by `data-element` attributes in the DOM.
|
|
482
|
+
|
|
483
|
+
> **Best Practice:** Use the `elements` prop to declare sub-element components. This gives you typed, reusable sub-components (`Card.Title`, `Card.Content`) instead of manually writing `data-element` attributes.
|
|
484
|
+
|
|
485
|
+
```jsx
|
|
486
|
+
const Card = tasty({
|
|
487
|
+
styles: {
|
|
488
|
+
padding: '4x',
|
|
489
|
+
Title: { preset: 'h3', color: '#primary' },
|
|
490
|
+
Content: { color: '#text' },
|
|
491
|
+
},
|
|
492
|
+
elements: {
|
|
493
|
+
Title: 'h3',
|
|
494
|
+
Content: 'div',
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Sub-components automatically get data-element attributes
|
|
499
|
+
<Card>
|
|
500
|
+
<Card.Title>Card Title</Card.Title>
|
|
501
|
+
<Card.Content>Card content</Card.Content>
|
|
502
|
+
</Card>
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Each entry in `elements` can be a tag name string or a config object:
|
|
506
|
+
|
|
507
|
+
```jsx
|
|
508
|
+
elements: {
|
|
509
|
+
Title: 'h3', // shorthand: tag name only
|
|
510
|
+
Icon: { as: 'span', qa: 'card-icon' }, // full form: tag + QA attribute
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
The sub-components produced by `elements` support `mods`, `tokens`, `isDisabled`, `isHidden`, and `isChecked` props — the same modifier interface as the root component.
|
|
515
|
+
|
|
516
|
+
If you don't need sub-components (e.g., the inner elements are already rendered by a third-party library), you can still style them by key alone — just omit `elements` and apply `data-element` manually:
|
|
517
|
+
|
|
518
|
+
```jsx
|
|
519
|
+
const Card = tasty({
|
|
520
|
+
styles: {
|
|
521
|
+
padding: '4x',
|
|
522
|
+
Title: { preset: 'h3', color: '#primary' },
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
<Card>
|
|
527
|
+
<div data-element="Title">Card Title</div>
|
|
528
|
+
</Card>
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
#### Selector Affix (`$`)
|
|
532
|
+
|
|
533
|
+
Control how a sub-element selector attaches to the root selector using the `$` property inside the sub-element's styles:
|
|
534
|
+
|
|
535
|
+
| Pattern | Result | Description |
|
|
536
|
+
|---------|--------|-------------|
|
|
537
|
+
| *(none)* | ` [el]` | Descendant (default) |
|
|
538
|
+
| `>` | `> [el]` | Direct child |
|
|
539
|
+
| `>Body>Row>` | `> [Body] > [Row] > [el]` | Chained elements |
|
|
540
|
+
| `::before` | `::before` | Root pseudo (no key) |
|
|
541
|
+
| `@::before` | `[el]::before` | Pseudo on the sub-element |
|
|
542
|
+
| `>@:hover` | `> [el]:hover` | Pseudo-class on the sub-element |
|
|
543
|
+
| `>@.active` | `> [el].active` | Class on the sub-element |
|
|
544
|
+
|
|
545
|
+
The `@` placeholder marks exactly where the `[data-element="..."]` selector is injected, allowing you to attach pseudo-classes, pseudo-elements, or class selectors directly to the sub-element instead of the root:
|
|
546
|
+
|
|
547
|
+
```jsx
|
|
548
|
+
const List = tasty({
|
|
549
|
+
styles: {
|
|
550
|
+
Item: {
|
|
551
|
+
$: '>@:last-child',
|
|
552
|
+
border: 'none',
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
// → .t0 > [data-element="Item"]:last-child { border: none }
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
## Hooks
|
|
562
|
+
|
|
563
|
+
### useStyles
|
|
564
|
+
|
|
565
|
+
```tsx
|
|
566
|
+
import { useStyles } from '@tenphi/tasty';
|
|
567
|
+
|
|
568
|
+
function MyComponent() {
|
|
569
|
+
const { className } = useStyles({
|
|
570
|
+
padding: '2x',
|
|
571
|
+
fill: '#surface',
|
|
572
|
+
radius: '1r',
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
return <div className={className}>Styled content</div>;
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### useGlobalStyles
|
|
580
|
+
|
|
581
|
+
```tsx
|
|
582
|
+
import { useGlobalStyles } from '@tenphi/tasty';
|
|
583
|
+
|
|
584
|
+
function ThemeStyles() {
|
|
585
|
+
useGlobalStyles('.card', {
|
|
586
|
+
padding: '4x',
|
|
587
|
+
fill: '#surface',
|
|
588
|
+
radius: '1r',
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### useRawCSS
|
|
596
|
+
|
|
597
|
+
```tsx
|
|
598
|
+
import { useRawCSS } from '@tenphi/tasty';
|
|
599
|
+
|
|
600
|
+
function GlobalReset() {
|
|
601
|
+
useRawCSS(`
|
|
602
|
+
body { margin: 0; padding: 0; }
|
|
603
|
+
`);
|
|
604
|
+
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### useMergeStyles
|
|
610
|
+
|
|
611
|
+
```tsx
|
|
612
|
+
import { useMergeStyles } from '@tenphi/tasty';
|
|
613
|
+
|
|
614
|
+
function MyTabs({ styles, tabListStyles, prefixStyles }) {
|
|
615
|
+
const mergedStyles = useMergeStyles(styles, {
|
|
616
|
+
TabList: tabListStyles,
|
|
617
|
+
Prefix: prefixStyles,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
return <TabsElement styles={mergedStyles} />;
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## Best Practices
|
|
627
|
+
|
|
628
|
+
### Do's
|
|
629
|
+
|
|
630
|
+
- Use styled wrappers instead of `styles` prop directly
|
|
631
|
+
- Use design tokens and custom units (`#text`, `2x`, `1r`)
|
|
632
|
+
- Use semantic transition names (`theme 0.3s`)
|
|
633
|
+
- Use `elements` prop to declare typed sub-components for compound components
|
|
634
|
+
- Use `styleProps` for component APIs
|
|
635
|
+
- Use `tokens` prop for dynamic values
|
|
636
|
+
|
|
637
|
+
### Don'ts
|
|
638
|
+
|
|
639
|
+
- Don't use `styles` prop directly on components
|
|
640
|
+
- Don't use raw CSS values when tokens exist
|
|
641
|
+
- Don't use CSS property names when Tasty alternatives exist — see [recommended props](styles.md#recommended-props)
|
|
642
|
+
- Don't change `styles` prop at runtime (use modifiers or tokens instead)
|
|
643
|
+
- Don't use `style` prop for custom styling (only for third-party library integration)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenphi/tasty",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "A design-system-integrated styling system and DSL for concise, state-aware UI styling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"files": [
|
|
49
49
|
"dist",
|
|
50
|
+
"docs",
|
|
50
51
|
"tasty.config.ts"
|
|
51
52
|
],
|
|
52
53
|
"sideEffects": false,
|
|
@@ -127,7 +128,7 @@
|
|
|
127
128
|
"name": "main (import *)",
|
|
128
129
|
"path": "dist/index.js",
|
|
129
130
|
"import": "*",
|
|
130
|
-
"limit": "
|
|
131
|
+
"limit": "47 kB"
|
|
131
132
|
},
|
|
132
133
|
{
|
|
133
134
|
"name": "core (import *)",
|
|
@@ -150,7 +151,7 @@
|
|
|
150
151
|
"path",
|
|
151
152
|
"crypto"
|
|
152
153
|
],
|
|
153
|
-
"limit": "
|
|
154
|
+
"limit": "27 kB"
|
|
154
155
|
},
|
|
155
156
|
{
|
|
156
157
|
"name": "babel-plugin",
|
|
@@ -161,7 +162,7 @@
|
|
|
161
162
|
"path",
|
|
162
163
|
"crypto"
|
|
163
164
|
],
|
|
164
|
-
"limit": "
|
|
165
|
+
"limit": "37 kB"
|
|
165
166
|
}
|
|
166
167
|
],
|
|
167
168
|
"scripts": {
|