@rafters/shared 0.4.5 → 0.5.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 CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  DTCGColorTokenSchema,
25
25
  oklchToDTCG,
26
26
  parseJSDocIntelligence,
27
- parseFillSignature,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafters/shared",
3
- "version": "0.4.5",
3
+ "version": "0.5.1",
4
4
  "description": "Shared W3C DTCG schemas, base types, and utilities for Rafters design system",
5
5
  "homepage": "https://rafters.studio",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export * from './component-intelligence';
2
- export * from './fill-signature';
3
2
  export * from './import-pending';
4
3
  export * from './types';
5
4
  export * from './version';
package/src/types.ts CHANGED
@@ -476,6 +476,169 @@ export const ColorReferenceSchema = z.object({
476
476
 
477
477
  export type ColorReference = z.infer<typeof ColorReferenceSchema>;
478
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
+
479
642
  // Progression system labels stamped on tokens to record which math system
480
643
  // produced them. Used only by TokenSchema below; the math-utils package
481
644
  // carries the actual ratio definitions in DEFAULT_RATIOS. This array will be
@@ -517,7 +680,7 @@ export type OverrideKind = z.infer<typeof OverrideKindSchema>;
517
680
  export const TokenSchema = z.object({
518
681
  // Core token data
519
682
  name: z.string(),
520
- value: z.union([z.string(), ColorValueSchema, ColorReferenceSchema]), // String, ColorValue for families, or ColorReference for semantic
683
+ value: z.union([z.string(), ColorValueSchema, ColorReferenceSchema, FillValueSchema]), // String, ColorValue for families, ColorReference for semantic, FillValue for fills
521
684
  category: z.string(),
522
685
  namespace: z.string(),
523
686
  binding: BindingSchema.optional(), // Plugin binding for cascade-driven values
@@ -550,7 +713,7 @@ export const TokenSchema = z.object({
550
713
  userOverride: z
551
714
  .object({
552
715
  // Value before override (for undo)
553
- previousValue: z.union([z.string(), ColorValueSchema, ColorReferenceSchema]),
716
+ previousValue: z.union([z.string(), ColorValueSchema, ColorReferenceSchema, FillValueSchema]),
554
717
  // Why was this overridden
555
718
  reason: z.string(),
556
719
  // Additional context (e.g. "Q1 marketing campaign", "accessibility audit")
@@ -566,7 +729,9 @@ export const TokenSchema = z.object({
566
729
 
567
730
  // Computed value from generation rule (before any override)
568
731
  // Stored so agents can see what the system WOULD produce vs what human chose
569
- computedValue: z.union([z.string(), ColorValueSchema, ColorReferenceSchema]).optional(),
732
+ computedValue: z
733
+ .union([z.string(), ColorValueSchema, ColorReferenceSchema, FillValueSchema])
734
+ .optional(),
570
735
 
571
736
  // Dependency tracking for automatic regeneration
572
737
  dependsOn: z.array(z.string()).optional(), // Parent token(s) - empty = root token
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * Written by packages/cli/scripts/sync-version.ts from the CLI's own
5
5
  * package.json version at build time -- do not edit by hand.
6
6
  */
7
- export const RAFTERS_VERSION = '0.4.5';
7
+ export const RAFTERS_VERSION = '0.5.1';
@@ -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
- }