@rafters/shared 0.4.4 → 0.5.0
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 +9 -14
- package/package.json +1 -1
- package/src/index.ts +0 -1
- package/src/types.ts +235 -22
- package/src/version.ts +1 -1
- package/src/fill-signature.ts +0 -217
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
DTCGColorTokenSchema,
|
|
25
25
|
oklchToDTCG,
|
|
26
26
|
parseJSDocIntelligence,
|
|
27
|
-
|
|
27
|
+
FillValueSchema,
|
|
28
28
|
ImportPendingSchema,
|
|
29
29
|
RAFTERS_VERSION,
|
|
30
30
|
} from '@rafters/shared';
|
|
@@ -54,6 +54,14 @@ validate against.
|
|
|
54
54
|
- `ComponentIntelligenceSchema`, `ColorIntelligenceSchema`, `IntelligenceSchema`
|
|
55
55
|
and friends — the shapes the MCP and Studio read when they describe a
|
|
56
56
|
component or a color.
|
|
57
|
+
- `FillValueSchema` / `FillValue` — a fill recipe, the value of a token in the
|
|
58
|
+
`fill` namespace: background, gradient, mask, backdrop, blend, foreground
|
|
59
|
+
and border, with an optional `dark` block and `hover`, `active`, `focus`
|
|
60
|
+
and `disabled` state blocks. `foreground` is required. Every colour in a
|
|
61
|
+
recipe is a registry reference (`{ family, position }` or
|
|
62
|
+
`TokenReferenceSchema`'s `{ token }`), never a literal colour.
|
|
63
|
+
- `FILL_NAME_EXCLUDES` — the names a fill cannot take: Tailwind's own `bg-*`
|
|
64
|
+
utility names and keywords, without the `bg-` prefix.
|
|
57
65
|
- `COMPUTED` — a sentinel symbol. Set a token to it and the system reverts that
|
|
58
66
|
token to its computed value instead of a hardcoded override.
|
|
59
67
|
|
|
@@ -73,19 +81,6 @@ it back out.
|
|
|
73
81
|
for what's missing or malformed.
|
|
74
82
|
- `toDisplayName(name)` — turn a component id into its display name.
|
|
75
83
|
|
|
76
|
-
### Fill signatures (`fill-signature.ts`)
|
|
77
|
-
|
|
78
|
-
`fill=` is a compact color vocabulary, not a styling grammar. `fill="muted/50"`
|
|
79
|
-
is the muted color at 50% alpha; `fill="barbie-pink-to-ken-brown"` is a two-stop
|
|
80
|
-
gradient. `to`, `via`, and `from` are reserved segments.
|
|
81
|
-
|
|
82
|
-
- `parseFillSignature(input)` — parse a fill string into stops and alpha.
|
|
83
|
-
- `validateFillSignature(signature, hasWord)` — check every stop word against a
|
|
84
|
-
vocabulary predicate; returns the first unresolvable word, or `null`.
|
|
85
|
-
- `expandFillSignature(signature, context, hasWord?)` — expand a signature into
|
|
86
|
-
the classes it sets, for the given context (`surface` or `text`).
|
|
87
|
-
- `foregroundWordFor(...)` — pick the readable foreground word for a surface fill.
|
|
88
|
-
|
|
89
84
|
### Import pending (`import-pending.ts`)
|
|
90
85
|
|
|
91
86
|
The schema for tokens found during `rafters init` detection and left awaiting
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -70,16 +70,32 @@ export const DTCGTokenBaseSchema = z.object({
|
|
|
70
70
|
export type DTCGTokenBase = z.infer<typeof DTCGTokenBaseSchema>;
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* W3C DTCG Color Value (
|
|
73
|
+
* W3C DTCG Color Value (Design Tokens Color Module 2025.10, section 4.1)
|
|
74
|
+
* https://www.designtokens.org/TR/2025.10/color/
|
|
74
75
|
*/
|
|
75
76
|
export const DTCGColorValueSchema = z.object({
|
|
76
|
-
colorSpace: z.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
colorSpace: z.enum([
|
|
78
|
+
'srgb',
|
|
79
|
+
'srgb-linear',
|
|
80
|
+
'hsl',
|
|
81
|
+
'hwb',
|
|
82
|
+
'lab',
|
|
83
|
+
'lch',
|
|
84
|
+
'oklab',
|
|
85
|
+
'oklch',
|
|
86
|
+
'display-p3',
|
|
87
|
+
'a98-rgb',
|
|
88
|
+
'prophoto-rgb',
|
|
89
|
+
'rec2020',
|
|
90
|
+
'xyz-d65',
|
|
91
|
+
'xyz-d50',
|
|
81
92
|
]),
|
|
82
|
-
|
|
93
|
+
components: z.array(z.union([z.number(), z.literal('none')])),
|
|
94
|
+
alpha: z.number().min(0).max(1).optional(),
|
|
95
|
+
hex: z
|
|
96
|
+
.string()
|
|
97
|
+
.regex(/^#[0-9a-fA-F]{6}$/)
|
|
98
|
+
.optional(),
|
|
83
99
|
});
|
|
84
100
|
|
|
85
101
|
export type DTCGColorValue = z.infer<typeof DTCGColorValueSchema>;
|
|
@@ -126,26 +142,27 @@ export const DTCGFileSchema = z.record(z.string(), z.union([DTCGTokenBaseSchema,
|
|
|
126
142
|
export type DTCGFile = z.infer<typeof DTCGFileSchema>;
|
|
127
143
|
|
|
128
144
|
/**
|
|
129
|
-
* Helper: Convert OKLCH to DTCG Color Value
|
|
145
|
+
* Helper: Convert OKLCH to DTCG Color Value.
|
|
146
|
+
* `alpha` is present only when it is below 1 (Color Module 2025.10 defaults it to 1).
|
|
130
147
|
*/
|
|
131
148
|
export function oklchToDTCG(oklch: OKLCH): DTCGColorValue {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
alpha: oklch.alpha ?? 1,
|
|
136
|
-
};
|
|
149
|
+
const value: DTCGColorValue = { colorSpace: 'oklch', components: [oklch.l, oklch.c, oklch.h] };
|
|
150
|
+
if (oklch.alpha !== undefined && oklch.alpha < 1) value.alpha = oklch.alpha;
|
|
151
|
+
return value;
|
|
137
152
|
}
|
|
138
153
|
|
|
139
154
|
/**
|
|
140
|
-
* Helper: Convert DTCG Color Value to OKLCH
|
|
155
|
+
* Helper: Convert an oklch DTCG Color Value to OKLCH. A `none` component reads as 0.
|
|
141
156
|
*/
|
|
142
157
|
export function dtcgToOKLCH(dtcg: DTCGColorValue): OKLCH {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
158
|
+
if (dtcg.colorSpace !== 'oklch') {
|
|
159
|
+
throw new Error(`dtcgToOKLCH: colorSpace "${dtcg.colorSpace}" is not oklch`);
|
|
160
|
+
}
|
|
161
|
+
const component = (i: number): number => {
|
|
162
|
+
const c = dtcg.components[i];
|
|
163
|
+
return typeof c === 'number' ? c : 0;
|
|
148
164
|
};
|
|
165
|
+
return { l: component(0), c: component(1), h: component(2), alpha: dtcg.alpha ?? 1 };
|
|
149
166
|
}
|
|
150
167
|
|
|
151
168
|
/**
|
|
@@ -371,6 +388,25 @@ export type PerceptualWeight = z.infer<typeof PerceptualWeightSchema>;
|
|
|
371
388
|
export const SEMANTIC_STATUS_ROLES = ['destructive', 'success', 'warning', 'info'] as const;
|
|
372
389
|
export type SemanticStatusRole = (typeof SEMANTIC_STATUS_ROLES)[number];
|
|
373
390
|
|
|
391
|
+
// Family roles: the semantic tokens a designer assigns a palette to. Every
|
|
392
|
+
// other semantic colour belongs to one of these (its owner), and the CSS
|
|
393
|
+
// exporter emits one 50-950 scale per role so a role change moves everything
|
|
394
|
+
// it owns.
|
|
395
|
+
export const SEMANTIC_FAMILY_ROLES = [
|
|
396
|
+
'primary',
|
|
397
|
+
'secondary',
|
|
398
|
+
'tertiary',
|
|
399
|
+
'accent',
|
|
400
|
+
'highlight',
|
|
401
|
+
'neutral',
|
|
402
|
+
'muted',
|
|
403
|
+
'success',
|
|
404
|
+
'warning',
|
|
405
|
+
'destructive',
|
|
406
|
+
'info',
|
|
407
|
+
] as const;
|
|
408
|
+
export type SemanticFamilyRole = (typeof SEMANTIC_FAMILY_ROLES)[number];
|
|
409
|
+
|
|
374
410
|
// Semantic Color Suggestions Schema.
|
|
375
411
|
// 'danger' is a deprecated mirror of 'destructive', kept while apps/api (which
|
|
376
412
|
// asserts .danger) migrates to platform. Inputs with only the legacy key parse
|
|
@@ -416,6 +452,18 @@ export const ColorValueSchema = z.object({
|
|
|
416
452
|
|
|
417
453
|
// Unique token ID for quick color lookups (e.g., "color-0.500-0.120-240.0")
|
|
418
454
|
tokenId: z.string().optional(),
|
|
455
|
+
|
|
456
|
+
// The gamut each scale step sits in, index-aligned with `scale`. Generated
|
|
457
|
+
// steps are 'srgb' or 'p3'; only an imported seed pinned as typed can be 'out'.
|
|
458
|
+
gamut: z.array(z.enum(['srgb', 'p3', 'out'])).optional(),
|
|
459
|
+
// Present when the family was built from a different seed than requested
|
|
460
|
+
seedAdjusted: z
|
|
461
|
+
.object({
|
|
462
|
+
from: OKLCHSchema,
|
|
463
|
+
to: OKLCHSchema,
|
|
464
|
+
reason: z.enum(['lightness-bounds', 'gamut']),
|
|
465
|
+
})
|
|
466
|
+
.optional(),
|
|
419
467
|
});
|
|
420
468
|
|
|
421
469
|
export type ColorValue = z.infer<typeof ColorValueSchema>;
|
|
@@ -428,6 +476,169 @@ export const ColorReferenceSchema = z.object({
|
|
|
428
476
|
|
|
429
477
|
export type ColorReference = z.infer<typeof ColorReferenceSchema>;
|
|
430
478
|
|
|
479
|
+
// Token reference: names a role or semantic colour token ("card", "primary")
|
|
480
|
+
// and follows that token through the cascade and the mode (FR-RAF-FILL-001).
|
|
481
|
+
export const TokenReferenceSchema = z.object({ token: z.string().min(1) }).strict();
|
|
482
|
+
|
|
483
|
+
export type TokenReference = z.infer<typeof TokenReferenceSchema>;
|
|
484
|
+
|
|
485
|
+
// Fills (FR-RAF-FILL-001): a designer-authored recipe of stock Tailwind parts.
|
|
486
|
+
// Every colour is a registry reference, never a literal.
|
|
487
|
+
const FillColourSchema = z.union([ColorReferenceSchema.strict(), TokenReferenceSchema], {
|
|
488
|
+
error: 'must be a registry reference ({family, position} or {token}), not a literal colour',
|
|
489
|
+
});
|
|
490
|
+
const PercentSchema = z.number().min(0).max(100);
|
|
491
|
+
const FillStopSchema = z
|
|
492
|
+
.object({
|
|
493
|
+
color: FillColourSchema,
|
|
494
|
+
alpha: PercentSchema.optional(),
|
|
495
|
+
position: PercentSchema.optional(),
|
|
496
|
+
})
|
|
497
|
+
.strict();
|
|
498
|
+
const FillPartsSchema = z
|
|
499
|
+
.object({
|
|
500
|
+
background: z
|
|
501
|
+
.object({ color: FillColourSchema, alpha: PercentSchema.optional() })
|
|
502
|
+
.strict()
|
|
503
|
+
.optional(),
|
|
504
|
+
gradient: z
|
|
505
|
+
.object({
|
|
506
|
+
type: z.enum(['linear', 'radial', 'conic']),
|
|
507
|
+
direction: z
|
|
508
|
+
.enum(['to-t', 'to-tr', 'to-r', 'to-br', 'to-b', 'to-bl', 'to-l', 'to-tl'])
|
|
509
|
+
.optional(),
|
|
510
|
+
angle: z.number().optional(),
|
|
511
|
+
from: FillStopSchema.optional(),
|
|
512
|
+
via: FillStopSchema.optional(),
|
|
513
|
+
to: FillStopSchema.optional(),
|
|
514
|
+
interpolation: z
|
|
515
|
+
.enum(['srgb', 'hsl', 'oklab', 'oklch', 'longer', 'shorter', 'increasing', 'decreasing'])
|
|
516
|
+
.optional(),
|
|
517
|
+
})
|
|
518
|
+
.strict()
|
|
519
|
+
.optional(),
|
|
520
|
+
mask: z
|
|
521
|
+
.object({
|
|
522
|
+
type: z.enum(['linear', 'radial', 'conic', 't', 'r', 'b', 'l', 'x', 'y']),
|
|
523
|
+
from: PercentSchema.optional(),
|
|
524
|
+
to: PercentSchema.optional(),
|
|
525
|
+
})
|
|
526
|
+
.strict()
|
|
527
|
+
.optional(),
|
|
528
|
+
backdrop: z
|
|
529
|
+
.object({
|
|
530
|
+
blur: z.enum(['none', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl']).optional(),
|
|
531
|
+
brightness: z.number().min(0).optional(),
|
|
532
|
+
contrast: z.number().min(0).optional(),
|
|
533
|
+
saturate: z.number().min(0).optional(),
|
|
534
|
+
grayscale: PercentSchema.optional(),
|
|
535
|
+
opacity: PercentSchema.optional(),
|
|
536
|
+
})
|
|
537
|
+
.strict()
|
|
538
|
+
.optional(),
|
|
539
|
+
blend: z
|
|
540
|
+
.enum([
|
|
541
|
+
'normal',
|
|
542
|
+
'multiply',
|
|
543
|
+
'screen',
|
|
544
|
+
'overlay',
|
|
545
|
+
'darken',
|
|
546
|
+
'lighten',
|
|
547
|
+
'color-dodge',
|
|
548
|
+
'color-burn',
|
|
549
|
+
'hard-light',
|
|
550
|
+
'soft-light',
|
|
551
|
+
'difference',
|
|
552
|
+
'exclusion',
|
|
553
|
+
'hue',
|
|
554
|
+
'saturation',
|
|
555
|
+
'color',
|
|
556
|
+
'luminosity',
|
|
557
|
+
])
|
|
558
|
+
.optional(),
|
|
559
|
+
foreground: FillColourSchema.optional(),
|
|
560
|
+
border: FillColourSchema.optional(),
|
|
561
|
+
})
|
|
562
|
+
.strict();
|
|
563
|
+
const FillStateSchema = FillPartsSchema.extend({ dark: FillPartsSchema.optional() }).strict();
|
|
564
|
+
|
|
565
|
+
export const FillValueSchema = FillPartsSchema.extend({
|
|
566
|
+
foreground: FillColourSchema,
|
|
567
|
+
dark: FillPartsSchema.optional(),
|
|
568
|
+
hover: FillStateSchema.optional(),
|
|
569
|
+
active: FillStateSchema.optional(),
|
|
570
|
+
focus: FillStateSchema.optional(),
|
|
571
|
+
disabled: FillStateSchema.optional(),
|
|
572
|
+
}).strict();
|
|
573
|
+
|
|
574
|
+
export type FillValue = z.infer<typeof FillValueSchema>;
|
|
575
|
+
|
|
576
|
+
// Names a fill cannot take (FR-RAF-FILL-001): Tailwind's static bg-* utilities
|
|
577
|
+
// (45), the legacy ones it still emits (12) and the bg- keywords, each without
|
|
578
|
+
// its `bg-` prefix. Studio keeps the designer-facing list.
|
|
579
|
+
export const FILL_NAME_EXCLUDES: readonly string[] = [
|
|
580
|
+
'auto',
|
|
581
|
+
'blend-color',
|
|
582
|
+
'blend-color-burn',
|
|
583
|
+
'blend-color-dodge',
|
|
584
|
+
'blend-darken',
|
|
585
|
+
'blend-difference',
|
|
586
|
+
'blend-exclusion',
|
|
587
|
+
'blend-hard-light',
|
|
588
|
+
'blend-hue',
|
|
589
|
+
'blend-lighten',
|
|
590
|
+
'blend-luminosity',
|
|
591
|
+
'blend-multiply',
|
|
592
|
+
'blend-normal',
|
|
593
|
+
'blend-overlay',
|
|
594
|
+
'blend-saturation',
|
|
595
|
+
'blend-screen',
|
|
596
|
+
'blend-soft-light',
|
|
597
|
+
'bottom',
|
|
598
|
+
'bottom-left',
|
|
599
|
+
'bottom-right',
|
|
600
|
+
'center',
|
|
601
|
+
'clip-border',
|
|
602
|
+
'clip-content',
|
|
603
|
+
'clip-padding',
|
|
604
|
+
'clip-text',
|
|
605
|
+
'contain',
|
|
606
|
+
'cover',
|
|
607
|
+
'fixed',
|
|
608
|
+
'left',
|
|
609
|
+
'local',
|
|
610
|
+
'no-repeat',
|
|
611
|
+
'none',
|
|
612
|
+
'origin-border',
|
|
613
|
+
'origin-content',
|
|
614
|
+
'origin-padding',
|
|
615
|
+
'repeat',
|
|
616
|
+
'repeat-round',
|
|
617
|
+
'repeat-space',
|
|
618
|
+
'repeat-x',
|
|
619
|
+
'repeat-y',
|
|
620
|
+
'right',
|
|
621
|
+
'scroll',
|
|
622
|
+
'top',
|
|
623
|
+
'top-left',
|
|
624
|
+
'top-right',
|
|
625
|
+
'left-top',
|
|
626
|
+
'right-top',
|
|
627
|
+
'left-bottom',
|
|
628
|
+
'right-bottom',
|
|
629
|
+
'gradient-to-t',
|
|
630
|
+
'gradient-to-tr',
|
|
631
|
+
'gradient-to-r',
|
|
632
|
+
'gradient-to-br',
|
|
633
|
+
'gradient-to-b',
|
|
634
|
+
'gradient-to-bl',
|
|
635
|
+
'gradient-to-l',
|
|
636
|
+
'gradient-to-tl',
|
|
637
|
+
'inherit',
|
|
638
|
+
'current',
|
|
639
|
+
'transparent',
|
|
640
|
+
];
|
|
641
|
+
|
|
431
642
|
// Progression system labels stamped on tokens to record which math system
|
|
432
643
|
// produced them. Used only by TokenSchema below; the math-utils package
|
|
433
644
|
// carries the actual ratio definitions in DEFAULT_RATIOS. This array will be
|
|
@@ -469,7 +680,7 @@ export type OverrideKind = z.infer<typeof OverrideKindSchema>;
|
|
|
469
680
|
export const TokenSchema = z.object({
|
|
470
681
|
// Core token data
|
|
471
682
|
name: z.string(),
|
|
472
|
-
value: z.union([z.string(), ColorValueSchema, ColorReferenceSchema]), // String, ColorValue for families,
|
|
683
|
+
value: z.union([z.string(), ColorValueSchema, ColorReferenceSchema, FillValueSchema]), // String, ColorValue for families, ColorReference for semantic, FillValue for fills
|
|
473
684
|
category: z.string(),
|
|
474
685
|
namespace: z.string(),
|
|
475
686
|
binding: BindingSchema.optional(), // Plugin binding for cascade-driven values
|
|
@@ -502,7 +713,7 @@ export const TokenSchema = z.object({
|
|
|
502
713
|
userOverride: z
|
|
503
714
|
.object({
|
|
504
715
|
// Value before override (for undo)
|
|
505
|
-
previousValue: z.union([z.string(), ColorValueSchema, ColorReferenceSchema]),
|
|
716
|
+
previousValue: z.union([z.string(), ColorValueSchema, ColorReferenceSchema, FillValueSchema]),
|
|
506
717
|
// Why was this overridden
|
|
507
718
|
reason: z.string(),
|
|
508
719
|
// Additional context (e.g. "Q1 marketing campaign", "accessibility audit")
|
|
@@ -518,7 +729,9 @@ export const TokenSchema = z.object({
|
|
|
518
729
|
|
|
519
730
|
// Computed value from generation rule (before any override)
|
|
520
731
|
// Stored so agents can see what the system WOULD produce vs what human chose
|
|
521
|
-
computedValue: z
|
|
732
|
+
computedValue: z
|
|
733
|
+
.union([z.string(), ColorValueSchema, ColorReferenceSchema, FillValueSchema])
|
|
734
|
+
.optional(),
|
|
522
735
|
|
|
523
736
|
// Dependency tracking for automatic regeneration
|
|
524
737
|
dependsOn: z.array(z.string()).optional(), // Parent token(s) - empty = root token
|
package/src/version.ts
CHANGED
package/src/fill-signature.ts
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fill signature (#1637).
|
|
3
|
-
*
|
|
4
|
-
* A fill is a SIGNATURE: a compact form that sets namespaced classes and
|
|
5
|
-
* existing Tailwind utilities on things. Not a grammar, not an adjective
|
|
6
|
-
* system. The phrase is color vocabulary only:
|
|
7
|
-
*
|
|
8
|
-
* word solid fill="barbie-pink"
|
|
9
|
-
* word/alpha with opacity fill="muted/50" (Tailwind's slash, verbatim)
|
|
10
|
-
* word-to-word 2-stop gradient fill="barbie-pink-to-ken-brown"
|
|
11
|
-
*
|
|
12
|
-
* '-to-' is RESERVED in the color namer (generated names can never contain
|
|
13
|
-
* it), so splitting at the first '-to-' is deterministic without vocabulary.
|
|
14
|
-
* With a vocabulary present, every word must resolve or the signature is
|
|
15
|
-
* rejected naming the unresolvable word (the #1632 lesson: hyphens are
|
|
16
|
-
* overloaded in generated names; split only at reserved separators and
|
|
17
|
-
* validate every side).
|
|
18
|
-
*
|
|
19
|
-
* Everything else -- blur, blend, direction -- is already Tailwind's
|
|
20
|
-
* namespace; our tokens set the VALUES behind those words (what
|
|
21
|
-
* backdrop-blur-sm ends up as). Those words are spoken as plain utilities
|
|
22
|
-
* where class lists are legitimate. Not fill's job.
|
|
23
|
-
*
|
|
24
|
-
* Dark contract: falls out of which utility is emitted. Semantic words
|
|
25
|
-
* compile to utilities over flipping vars (bg-primary follows .dark);
|
|
26
|
-
* family words compile to literal scale utilities (never flip).
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
export interface FillStop {
|
|
30
|
-
/** A vocabulary word: semantic role, family, or family-position. */
|
|
31
|
-
word: string;
|
|
32
|
-
/** Tailwind slash-opacity, integer 0-100. Absent = opaque. */
|
|
33
|
-
alpha?: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface FillSignature {
|
|
37
|
-
/** One stop = solid; two stops = linear gradient (to-b). */
|
|
38
|
-
stops: [FillStop] | [FillStop, FillStop];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export type FillParseResult =
|
|
42
|
-
| { ok: true; signature: FillSignature }
|
|
43
|
-
| { ok: false; word: string; reason: string };
|
|
44
|
-
|
|
45
|
-
export type FillContext = 'surface' | 'text';
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Hyphen segments reserved across the system (#1637). The fill parser
|
|
49
|
-
* splits at '-to-'; 'via' and 'from' are Tailwind's other gradient
|
|
50
|
-
* connectors and are reserved alongside so they can join the signature
|
|
51
|
-
* without a namer migration. The color namer's bank invariant test
|
|
52
|
-
* imports this list -- one copy of the reservation.
|
|
53
|
-
*/
|
|
54
|
-
export const RESERVED_FILL_SEGMENTS = ['to', 'via', 'from'] as const;
|
|
55
|
-
|
|
56
|
-
const SEPARATOR = `-${RESERVED_FILL_SEGMENTS[0]}-`;
|
|
57
|
-
const ALPHA_PATTERN = /^(100|[1-9]?[0-9])$/;
|
|
58
|
-
const WORD_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
59
|
-
|
|
60
|
-
function parseStop(term: string): FillStop | { word: string; reason: string } {
|
|
61
|
-
const slash = term.indexOf('/');
|
|
62
|
-
const word = slash === -1 ? term : term.slice(0, slash);
|
|
63
|
-
if (!WORD_PATTERN.test(word)) {
|
|
64
|
-
return { word: word || term, reason: 'not a vocabulary word' };
|
|
65
|
-
}
|
|
66
|
-
if (slash === -1) return { word };
|
|
67
|
-
const alphaRaw = term.slice(slash + 1);
|
|
68
|
-
if (!ALPHA_PATTERN.test(alphaRaw)) {
|
|
69
|
-
return { word, reason: `invalid opacity "/${alphaRaw}" (integer 0-100)` };
|
|
70
|
-
}
|
|
71
|
-
return { word, alpha: Number(alphaRaw) };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Parse a fill signature. Syntax only -- vocabulary validation is the
|
|
76
|
-
* caller's job via `validateFillSignature` wherever a registry exists
|
|
77
|
-
* (build, tests, MCP). Runtime components expand optimistically; the
|
|
78
|
-
* build-time safelist pass is the strict gate.
|
|
79
|
-
*/
|
|
80
|
-
export function parseFillSignature(input: string): FillParseResult {
|
|
81
|
-
const trimmed = input.trim();
|
|
82
|
-
if (trimmed.length === 0) {
|
|
83
|
-
return { ok: false, word: input, reason: 'empty fill' };
|
|
84
|
-
}
|
|
85
|
-
if (trimmed.includes(' ')) {
|
|
86
|
-
return {
|
|
87
|
-
ok: false,
|
|
88
|
-
word: trimmed,
|
|
89
|
-
reason:
|
|
90
|
-
'fill is a single signature -- other utilities are plain Tailwind words in the class list',
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const split = trimmed.indexOf(SEPARATOR);
|
|
95
|
-
if (split === -1) {
|
|
96
|
-
const stop = parseStop(trimmed);
|
|
97
|
-
if ('reason' in stop) return { ok: false, ...stop };
|
|
98
|
-
return { ok: true, signature: { stops: [stop] } };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const left = trimmed.slice(0, split);
|
|
102
|
-
const right = trimmed.slice(split + SEPARATOR.length);
|
|
103
|
-
if (right.includes(SEPARATOR)) {
|
|
104
|
-
return {
|
|
105
|
-
ok: false,
|
|
106
|
-
word: trimmed,
|
|
107
|
-
reason: 'three-stop gradients are not supported (two stops only)',
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
const from = parseStop(left);
|
|
111
|
-
if ('reason' in from) return { ok: false, ...from };
|
|
112
|
-
const to = parseStop(right);
|
|
113
|
-
if ('reason' in to) return { ok: false, ...to };
|
|
114
|
-
return { ok: true, signature: { stops: [from, to] } };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Validate every word in a parsed signature against a vocabulary.
|
|
119
|
-
* Returns null when valid, else the first unresolvable word.
|
|
120
|
-
*/
|
|
121
|
-
export function validateFillSignature(
|
|
122
|
-
signature: FillSignature,
|
|
123
|
-
hasWord: (word: string) => boolean,
|
|
124
|
-
): string | null {
|
|
125
|
-
for (const stop of signature.stops) {
|
|
126
|
-
if (!hasWord(stop.word)) return stop.word;
|
|
127
|
-
}
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* The surface roles whose `{word}-foreground` pair is guaranteed by the
|
|
133
|
-
* frozen contract (shadcn drop-in names plus rafters role extensions --
|
|
134
|
-
* doctrine: contract names are frozen interface). Pairing is membership
|
|
135
|
-
* here, never spelling: family words (barbie-pink, blue-300) and unknown
|
|
136
|
-
* words never pair, so no phantom text-*-foreground class ever ships.
|
|
137
|
-
*/
|
|
138
|
-
const PAIRED_SURFACE_ROLES = new Set([
|
|
139
|
-
'background',
|
|
140
|
-
'card',
|
|
141
|
-
'panel',
|
|
142
|
-
'popover',
|
|
143
|
-
'surface',
|
|
144
|
-
'primary',
|
|
145
|
-
'secondary',
|
|
146
|
-
'muted',
|
|
147
|
-
'accent',
|
|
148
|
-
'destructive',
|
|
149
|
-
'success',
|
|
150
|
-
'warning',
|
|
151
|
-
'info',
|
|
152
|
-
'alert',
|
|
153
|
-
'highlight',
|
|
154
|
-
'selection',
|
|
155
|
-
'sidebar',
|
|
156
|
-
'nav',
|
|
157
|
-
'tooltip',
|
|
158
|
-
'overlay',
|
|
159
|
-
'table',
|
|
160
|
-
'table-header',
|
|
161
|
-
'code',
|
|
162
|
-
'badge',
|
|
163
|
-
'avatar',
|
|
164
|
-
'input',
|
|
165
|
-
]);
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* The paired foreground word for a surface word, or null when pairing
|
|
169
|
-
* does not apply. `background` pairs irregularly with `foreground`.
|
|
170
|
-
* With a vocabulary present (`hasWord`), any word whose `-foreground`
|
|
171
|
-
* pair resolves may pair; without one, only the frozen role set does.
|
|
172
|
-
*/
|
|
173
|
-
export function foregroundWordFor(
|
|
174
|
-
word: string,
|
|
175
|
-
hasWord?: (word: string) => boolean,
|
|
176
|
-
): string | null {
|
|
177
|
-
const pair = word === 'background' ? 'foreground' : `${word}-foreground`;
|
|
178
|
-
if (hasWord) return hasWord(pair) ? pair : null;
|
|
179
|
-
return PAIRED_SURFACE_ROLES.has(word) ? pair : null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function stopClass(prefix: string, stop: FillStop): string {
|
|
183
|
-
return stop.alpha === undefined
|
|
184
|
-
? `${prefix}-${stop.word}`
|
|
185
|
-
: `${prefix}-${stop.word}/${stop.alpha}`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Expand a signature to its class words. Surface context emits the
|
|
190
|
-
* background (and the paired foreground when one exists in the
|
|
191
|
-
* vocabulary); text context emits text color, or gradient text via
|
|
192
|
-
* bg-clip-text. Gradients emit Tailwind v4 utilities (bg-linear-to-b --
|
|
193
|
-
* bg-gradient-to-* is a deprecated v3 alias and is never emitted).
|
|
194
|
-
*/
|
|
195
|
-
export function expandFillSignature(
|
|
196
|
-
signature: FillSignature,
|
|
197
|
-
context: FillContext,
|
|
198
|
-
hasWord?: (word: string) => boolean,
|
|
199
|
-
): string {
|
|
200
|
-
const [first, second] = signature.stops;
|
|
201
|
-
|
|
202
|
-
if (!second) {
|
|
203
|
-
if (context === 'text') return stopClass('text', first);
|
|
204
|
-
const parts = [stopClass('bg', first)];
|
|
205
|
-
const fg = foregroundWordFor(first.word, hasWord);
|
|
206
|
-
if (fg) parts.push(`text-${fg}`);
|
|
207
|
-
return parts.join(' ');
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const gradient = ['bg-linear-to-b', stopClass('from', first), stopClass('to', second)];
|
|
211
|
-
if (context === 'text') {
|
|
212
|
-
return [...gradient, 'bg-clip-text', 'text-transparent'].join(' ');
|
|
213
|
-
}
|
|
214
|
-
const fg = foregroundWordFor(first.word, hasWord);
|
|
215
|
-
if (fg) gradient.push(`text-${fg}`);
|
|
216
|
-
return gradient.join(' ');
|
|
217
|
-
}
|