@treeui/mcp 0.6.0 → 0.8.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/dist/index.d.cts CHANGED
@@ -106,6 +106,13 @@ interface TreeuiTokenEntry {
106
106
  description?: string;
107
107
  /** Per-theme values for tokens that change with `[data-tree-theme]`. */
108
108
  themeValues?: Record<string, string>;
109
+ /**
110
+ * True for interaction states the library computes from the semantic layer
111
+ * (hover, press, selected, disabled). They are real, emitted variables an
112
+ * agent may read, but a product must not theme against them — and they rank
113
+ * below the semantic tokens in search so they cannot bury the answer.
114
+ */
115
+ derived?: boolean;
109
116
  }
110
117
  interface TreeuiCatalog {
111
118
  generatedAt: string;
package/dist/index.d.ts CHANGED
@@ -106,6 +106,13 @@ interface TreeuiTokenEntry {
106
106
  description?: string;
107
107
  /** Per-theme values for tokens that change with `[data-tree-theme]`. */
108
108
  themeValues?: Record<string, string>;
109
+ /**
110
+ * True for interaction states the library computes from the semantic layer
111
+ * (hover, press, selected, disabled). They are real, emitted variables an
112
+ * agent may read, but a product must not theme against them — and they rank
113
+ * below the semantic tokens in search so they cannot bury the answer.
114
+ */
115
+ derived?: boolean;
109
116
  }
110
117
  interface TreeuiCatalog {
111
118
  generatedAt: string;
package/dist/index.js CHANGED
@@ -11,14 +11,168 @@ import {
11
11
  searchComponents,
12
12
  searchRecipes,
13
13
  searchTokens
14
- } from "./chunk-ITEC3SEF.js";
14
+ } from "./chunk-CEDY3OJ4.js";
15
15
 
16
16
  // src/catalog-source.ts
17
17
  import fs from "fs";
18
18
  import path from "path";
19
19
  import { parseAllDocuments } from "yaml";
20
20
 
21
+ // ../tokens/src/primitives.ts
22
+ var treePrimitives = {
23
+ white: "#ffffff",
24
+ black: "#000000",
25
+ /**
26
+ * Neutral ramp. Carries both themes: 0-300 are light-mode surfaces and
27
+ * borders, 700-950 are dark-mode surfaces, and the 400-600 middle is where
28
+ * borders and muted text land in whichever theme needs that luminance.
29
+ */
30
+ gray: {
31
+ 0: "#ffffff",
32
+ 50: "#f6f8fa",
33
+ 100: "#eff2f5",
34
+ 200: "#d0d7de",
35
+ 300: "#afb8c1",
36
+ /** Clears 3:1 on every light surface, including the subtle one — control edges. */
37
+ 400: "#858c93",
38
+ /** Its dark-mode counterpart, 3:1 on every dark surface. */
39
+ 500: "#737c88",
40
+ 600: "#59636e",
41
+ 650: "#576270",
42
+ 700: "#444c56",
43
+ 800: "#2d333b",
44
+ 850: "#22272e",
45
+ 900: "#1c2128",
46
+ 950: "#0d1117",
47
+ /** Ink. Not gray-1000: it carries a blue cast, which is why it is named. */
48
+ ink: "#1f2328",
49
+ /** Muted ink for dark surfaces. */
50
+ inkDark: "#adbac7",
51
+ inkDarkMuted: "#95a3b2"
52
+ },
53
+ blue: {
54
+ 50: "#ddf4ff",
55
+ 300: "#6cb6ff",
56
+ 400: "#559cf5",
57
+ 450: "#6cabf7",
58
+ 500: "#0969da",
59
+ 600: "#0550ae",
60
+ 800: "#1a3453",
61
+ 900: "#0a1a2f"
62
+ },
63
+ /**
64
+ * Secondary accent ramp. Seeded from `#8957e5` and stepped until the mid tone
65
+ * reads as text on its own tint in both themes — see `DECISIONS.md`.
66
+ */
67
+ purple: {
68
+ 50: "#efeaf9",
69
+ 300: "#ba9df0",
70
+ 400: "#ad8bed",
71
+ 500: "#794dcb",
72
+ 600: "#633fa6",
73
+ 800: "#332e4a"
74
+ },
75
+ green: {
76
+ 400: "#59ac5c",
77
+ 500: "#1a7f37"
78
+ },
79
+ amber: {
80
+ 400: "#c69026",
81
+ 500: "#956400"
82
+ },
83
+ red: {
84
+ /** Landed on 4.50:1 against the dark subtle band; nudged for headroom. */
85
+ 400: "#f5766e",
86
+ 500: "#d1242f"
87
+ },
88
+ /**
89
+ * Categorical data-viz hues. Deliberately not a ramp — these are eight
90
+ * distinct hues chosen for separation under the three common CVD types, each
91
+ * held at >= 3:1 against its own theme's surface so a mark is visible without
92
+ * relying on the legend. Assign in order.
93
+ */
94
+ dataviz: {
95
+ light: {
96
+ 1: "#2a78d6",
97
+ // 2, 3 and 7 were #1baf7a / #eda100 / #e87ba4, measuring 2.82, 2.17 and
98
+ // 2.69 against the light surface — a mark you cannot see is not a
99
+ // category. Darkened along the same hue until each clears 3:1.
100
+ 2: "#12855c",
101
+ 3: "#8a6100",
102
+ 4: "#008300",
103
+ 5: "#4a3aa7",
104
+ 6: "#e34948",
105
+ 7: "#c4587f",
106
+ 8: "#eb6834"
107
+ },
108
+ dark: {
109
+ 1: "#3987e5",
110
+ 2: "#199e70",
111
+ 3: "#c98500",
112
+ 4: "#008300",
113
+ 5: "#9085e9",
114
+ 6: "#e66767",
115
+ 7: "#d55181",
116
+ 8: "#d95926"
117
+ }
118
+ }
119
+ };
120
+ var treeAlpha = {
121
+ /** Modal and drawer scrim. */
122
+ overlay: { light: 0.5, dark: 0.6 },
123
+ /** Focus ring, over the brand colour. */
124
+ focusRing: 0.32
125
+ };
126
+
127
+ // ../tokens/src/color.ts
128
+ var clampChannel = (value) => Math.min(255, Math.max(0, Math.round(value)));
129
+ var expandShortHex = (hex) => hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
130
+ var parseHex = (value) => {
131
+ const hex = expandShortHex(value.trim());
132
+ const match = /^#([0-9a-fA-F]{6})$/.exec(hex);
133
+ if (!match) {
134
+ throw new Error(`Invalid hex color: "${value}"`);
135
+ }
136
+ const int = Number.parseInt(match[1], 16);
137
+ return {
138
+ r: int >> 16 & 255,
139
+ g: int >> 8 & 255,
140
+ b: int & 255
141
+ };
142
+ };
143
+ var formatHex = ({ r, g, b }) => {
144
+ const toPart = (channel) => clampChannel(channel).toString(16).padStart(2, "0");
145
+ return `#${toPart(r)}${toPart(g)}${toPart(b)}`;
146
+ };
147
+ var mixColors = (a, b, weight) => {
148
+ const t = Math.min(1, Math.max(0, weight));
149
+ return {
150
+ r: a.r + (b.r - a.r) * t,
151
+ g: a.g + (b.g - a.g) * t,
152
+ b: a.b + (b.b - a.b) * t
153
+ };
154
+ };
155
+ var BLACK = { r: 0, g: 0, b: 0 };
156
+ var WHITE = { r: 255, g: 255, b: 255 };
157
+ var darken = (color, amount) => mixColors(color, BLACK, amount);
158
+ var lighten = (color, amount) => mixColors(color, WHITE, amount);
159
+ var channelLuminance = (value) => {
160
+ const channel = value / 255;
161
+ return channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
162
+ };
163
+ var relativeLuminance = ({ r, g, b }) => 0.2126 * channelLuminance(r) + 0.7152 * channelLuminance(g) + 0.0722 * channelLuminance(b);
164
+ var contrastRatio = (a, b) => {
165
+ const luminanceA = relativeLuminance(a);
166
+ const luminanceB = relativeLuminance(b);
167
+ const lighter = Math.max(luminanceA, luminanceB);
168
+ const darker = Math.min(luminanceA, luminanceB);
169
+ return (lighter + 0.05) / (darker + 0.05);
170
+ };
171
+ var bestContrast = (color, candidates) => contrastRatio(color, candidates[0]) >= contrastRatio(color, candidates[1]) ? candidates[0] : candidates[1];
172
+ var withAlpha = ({ r, g, b }, alpha2) => `rgba(${clampChannel(r)}, ${clampChannel(g)}, ${clampChannel(b)}, ${alpha2})`;
173
+
21
174
  // ../tokens/src/tokens.ts
175
+ var alpha = (color, amount) => withAlpha(parseHex(color), amount);
22
176
  var treeTokens = {
23
177
  font: {
24
178
  family: {
@@ -32,7 +186,11 @@ var treeTokens = {
32
186
  base: "0.9375rem",
33
187
  lg: "1.125rem",
34
188
  xl: "1.375rem",
35
- "2xl": "1.375rem",
189
+ // `2xl` used to repeat `xl` verbatim, which made every
190
+ // `clamp(xl … 2xl)` a constant and left a hole between 1.375 and 1.75rem.
191
+ // It now fills that hole; `xl` is unchanged, since it is the more used of
192
+ // the two. `tokens.test.ts` keeps the scale strictly increasing.
193
+ "2xl": "1.5rem",
36
194
  "3xl": "1.75rem",
37
195
  "4xl": "2.25rem",
38
196
  "5xl": "3rem"
@@ -52,6 +210,11 @@ var treeTokens = {
52
210
  base: "1.5",
53
211
  body: "1.6",
54
212
  relaxed: "1.65"
213
+ },
214
+ tracking: {
215
+ tight: "-0.02em",
216
+ normal: "0em",
217
+ wide: "0.08em"
55
218
  }
56
219
  },
57
220
  space: {
@@ -72,10 +235,23 @@ var treeTokens = {
72
235
  lg: "0.875rem",
73
236
  pill: "999px"
74
237
  },
238
+ // Elevation. The geometry is theme-independent and lives here; the *colour*
239
+ // comes from `--tree-color-shadow-rgb`, which the theme sets — a slate shadow
240
+ // was being emitted on `:root` and reused verbatim on the dark surface, where
241
+ // a near-black umbra is what actually reads.
75
242
  shadow: {
76
- xs: "0 1px 2px rgba(15, 23, 42, 0.06)",
77
- sm: "0 6px 18px rgba(15, 23, 42, 0.08)",
78
- md: "0 14px 34px rgba(15, 23, 42, 0.12)"
243
+ xs: "0 1px 2px rgba(var(--tree-color-shadow-rgb), 0.06)",
244
+ sm: "0 6px 18px rgba(var(--tree-color-shadow-rgb), 0.08)",
245
+ md: "0 14px 34px rgba(var(--tree-color-shadow-rgb), 0.12)",
246
+ // Marketing-scale elevation: a large, soft lift for a hero card or a banded
247
+ // section, where `md` reads flat. Same neutral shadow colour as the smaller
248
+ // steps, so the whole scale stays one family.
249
+ lg: "0 24px 48px -24px rgba(var(--tree-color-shadow-rgb), 0.22)",
250
+ xl: "0 40px 80px -32px rgba(var(--tree-color-shadow-rgb), 0.28)",
251
+ // Elevation tinted by whatever accent is in scope (see `--tree-color-accent-*`
252
+ // and the `accent` prop on TSection/THero/TPageSurface), so a surface's glow
253
+ // follows its accent instead of being written by hand per landing page.
254
+ accent: "0 24px 48px -32px color-mix(in srgb, var(--tree-color-accent-primary) 60%, transparent)"
79
255
  },
80
256
  // Brand gradient for high-emphasis surfaces (e.g. the primary CTA).
81
257
  // References the theme brand vars so it follows light/dark automatically;
@@ -89,6 +265,15 @@ var treeTokens = {
89
265
  strong: "1.5px"
90
266
  }
91
267
  },
268
+ /**
269
+ * Opacity for the parts of a disabled control that carry no text — an icon,
270
+ * a thumbnail, a toolbar. Anywhere a label sits on a surface, disabled is a
271
+ * colour instead (`--tree-color-state-disabled-*`), because opacity cannot be
272
+ * measured for contrast. This replaces eight different hand-picked values.
273
+ */
274
+ opacity: {
275
+ disabled: "0.5"
276
+ },
92
277
  motion: {
93
278
  duration: {
94
279
  fast: "100ms",
@@ -153,95 +338,233 @@ var treeThemes = {
153
338
  light: {
154
339
  color: {
155
340
  bg: {
156
- primary: "#f6f8fa",
157
- surface: "#ffffff",
158
- subtle: "#eff2f5"
341
+ primary: treePrimitives.gray[50],
342
+ surface: treePrimitives.gray[0],
343
+ subtle: treePrimitives.gray[100]
159
344
  },
160
345
  border: {
161
- default: "#d0d7de",
162
- strong: "#afb8c1"
346
+ default: treePrimitives.gray[200],
347
+ strong: treePrimitives.gray[300]
348
+ // `border.interactive` is deliberately absent: WCAG 1.4.11 asks for 3:1
349
+ // on the boundary that *identifies* a control, and `default` measures
350
+ // 1.45:1 here — fine for a card edge, not fine for the only thing
351
+ // telling a user where an input is. The library derives a passing value
352
+ // from `strong` (see states.ts); a product may set one, and it is
353
+ // validated the same way.
163
354
  },
164
355
  text: {
165
- primary: "#1f2328",
166
- muted: "#59636e",
167
- inverse: "#ffffff"
356
+ primary: treePrimitives.gray.ink,
357
+ muted: treePrimitives.gray[600],
358
+ inverse: treePrimitives.gray[0]
168
359
  },
360
+ /** Umbra as an `r, g, b` triple so the elevation scale can vary alpha. */
361
+ "shadow-rgb": "15, 23, 42",
169
362
  brand: {
170
- primary: "#0969da",
171
- hover: "#0550ae",
172
- soft: "#ddf4ff",
173
- contrast: "#ffffff"
363
+ primary: treePrimitives.blue[500],
364
+ hover: treePrimitives.blue[600],
365
+ soft: treePrimitives.blue[50],
366
+ contrast: treePrimitives.gray[0]
367
+ },
368
+ // Secondary brand accent — a closed axis, never a free colour. It is what
369
+ // `TTag tone="accent"` paints with, and the default value of the surface
370
+ // `accent` axis (TSection/THero/TPageSurface), which remaps these four
371
+ // variables when a product wants a status hue instead. Derived with
372
+ // `deriveBrandRamp` from #8957e5 so the accent stays legible as text on
373
+ // its own soft tint (measured 4.77:1) and on every background.
374
+ accent: {
375
+ primary: treePrimitives.purple[500],
376
+ hover: treePrimitives.purple[600],
377
+ soft: treePrimitives.purple[50],
378
+ contrast: treePrimitives.gray[0]
174
379
  },
175
380
  status: {
176
- success: "#1a7f37",
177
- warning: "#9a6700",
178
- error: "#d1242f",
179
- info: "#0969da"
381
+ success: treePrimitives.green[500],
382
+ warning: treePrimitives.amber[500],
383
+ error: treePrimitives.red[500],
384
+ info: treePrimitives.blue[500]
180
385
  },
181
386
  // Categorical data-viz palette. Fixed hue order (blue → orange), validated
182
387
  // for CVD separation and contrast against the light surface. Assign in order;
183
388
  // the components cycle after 8 as a fallback, but prefer folding a 9th+
184
389
  // category into "Other".
185
- chart: {
186
- 1: "#2a78d6",
187
- 2: "#1baf7a",
188
- 3: "#eda100",
189
- 4: "#008300",
190
- 5: "#4a3aa7",
191
- 6: "#e34948",
192
- 7: "#e87ba4",
193
- 8: "#eb6834"
194
- },
195
- overlay: "rgba(31, 35, 40, 0.5)",
196
- "focus-ring": "rgba(9, 105, 218, 0.32)"
390
+ chart: treePrimitives.dataviz.light,
391
+ overlay: alpha(treePrimitives.gray.ink, treeAlpha.overlay.light),
392
+ "focus-ring": alpha(treePrimitives.blue[500], treeAlpha.focusRing)
197
393
  }
198
394
  },
199
395
  dark: {
200
396
  color: {
201
397
  bg: {
202
- primary: "#1c2128",
203
- surface: "#22272e",
204
- subtle: "#2d333b"
398
+ primary: treePrimitives.gray[900],
399
+ surface: treePrimitives.gray[850],
400
+ subtle: treePrimitives.gray[800]
205
401
  },
206
402
  border: {
207
- default: "#444c56",
208
- strong: "#576270"
403
+ default: treePrimitives.gray[700],
404
+ strong: treePrimitives.gray[650]
209
405
  },
210
406
  text: {
211
- primary: "#adbac7",
212
- muted: "#95a3b2",
213
- inverse: "#1c2128"
407
+ primary: treePrimitives.gray.inkDark,
408
+ muted: treePrimitives.gray.inkDarkMuted,
409
+ inverse: treePrimitives.gray[900]
214
410
  },
411
+ /** Near-black umbra: a slate shadow is invisible on a dark surface. */
412
+ "shadow-rgb": "0, 0, 0",
215
413
  brand: {
216
- primary: "#539bf5",
217
- hover: "#6cabf7",
218
- soft: "#1a3453",
219
- contrast: "#0a1a2f"
414
+ primary: treePrimitives.blue[400],
415
+ hover: treePrimitives.blue[450],
416
+ soft: treePrimitives.blue[800],
417
+ contrast: treePrimitives.blue[900]
418
+ },
419
+ // Dark pair of the secondary accent (same #8957e5 seed, stepped for the
420
+ // dark surface). Measured 5.93:1 on bg-primary, 4.67:1 on bg-subtle and
421
+ // 4.72:1 on its own soft tint — AA on every background, and above what
422
+ // the dark brand pair itself clears. `accent.test.ts` holds that bar.
423
+ accent: {
424
+ primary: treePrimitives.purple[400],
425
+ hover: treePrimitives.purple[300],
426
+ soft: treePrimitives.purple[800],
427
+ contrast: treePrimitives.gray[900]
220
428
  },
221
429
  status: {
222
- success: "#57ab5a",
223
- warning: "#c69026",
224
- error: "#f47067",
225
- info: "#6cb6ff"
430
+ success: treePrimitives.green[400],
431
+ warning: treePrimitives.amber[400],
432
+ error: treePrimitives.red[400],
433
+ info: treePrimitives.blue[300]
226
434
  },
227
435
  // Same hue order stepped for the dark surface (not a separate palette) —
228
436
  // each step re-validated for the dark band and >= 3:1 on the dark surface.
229
- chart: {
230
- 1: "#3987e5",
231
- 2: "#199e70",
232
- 3: "#c98500",
233
- 4: "#008300",
234
- 5: "#9085e9",
235
- 6: "#e66767",
236
- 7: "#d55181",
237
- 8: "#d95926"
238
- },
239
- overlay: "rgba(13, 17, 23, 0.6)",
240
- "focus-ring": "rgba(83, 155, 245, 0.32)"
437
+ chart: treePrimitives.dataviz.dark,
438
+ overlay: alpha(treePrimitives.gray[950], treeAlpha.overlay.dark),
439
+ "focus-ring": alpha(treePrimitives.blue[400], treeAlpha.focusRing)
241
440
  }
242
441
  }
243
442
  };
244
443
 
444
+ // ../tokens/src/states.ts
445
+ var AA_UI = 3;
446
+ var AA_TEXT = 4.5;
447
+ var WHITE2 = { r: 255, g: 255, b: 255 };
448
+ var BLACK2 = { r: 0, g: 0, b: 0 };
449
+ var STEP = {
450
+ hover: 0.18,
451
+ press: 0.32,
452
+ /** Tint states move toward the solid colour instead of toward black/white. */
453
+ softHover: 0.14,
454
+ softPress: 0.26,
455
+ /** Neutral row/menu surfaces, as a fraction of the ink mixed into the surface. */
456
+ neutralHover: 0.06,
457
+ neutralPress: 0.12
458
+ };
459
+ var DEGENERATE = 1.12;
460
+ var step = (color, amount, mode) => {
461
+ const preferred = mode === "light" ? darken(color, amount) : lighten(color, amount);
462
+ if (contrastRatio(preferred, color) >= DEGENERATE) return preferred;
463
+ const flipped = mode === "light" ? lighten(color, amount) : darken(color, amount);
464
+ return contrastRatio(flipped, color) > contrastRatio(preferred, color) ? flipped : preferred;
465
+ };
466
+ var fadeToward = (from, to, against, minRatio) => {
467
+ let best = from;
468
+ for (let amount = 0.05; amount <= 0.9; amount += 0.05) {
469
+ const candidate = mixColors(from, to, amount);
470
+ if (contrastRatio(candidate, against) < minRatio) break;
471
+ best = candidate;
472
+ }
473
+ return best;
474
+ };
475
+ var quantize = (color) => parseHex(formatHex(color));
476
+ var untilLegible = (color, against, minRatio) => {
477
+ if (contrastRatio(color, against) >= minRatio) return color;
478
+ const goLighter = relativeLuminance(color) >= relativeLuminance(against);
479
+ let candidate = color;
480
+ for (let i = 0; i < 24; i += 1) {
481
+ candidate = goLighter ? lighten(candidate, 0.05) : darken(candidate, 0.05);
482
+ if (contrastRatio(candidate, against) >= minRatio) return candidate;
483
+ }
484
+ return bestContrast(against, [WHITE2, BLACK2]);
485
+ };
486
+ var STATUS_KEYS = ["success", "warning", "error", "info"];
487
+ var deriveStateColors = (color, mode) => {
488
+ const surface = parseHex(color.bg.surface);
489
+ const subtle = parseHex(color.bg.subtle);
490
+ const ink = parseHex(color.text.primary);
491
+ const muted = parseHex(color.text.muted);
492
+ const inverse = parseHex(color.text.inverse);
493
+ const familyStates = (primary, soft) => {
494
+ const base = parseHex(primary);
495
+ const tint = parseHex(soft);
496
+ const softHover = quantize(mixColors(tint, base, STEP.softHover));
497
+ const softPress = quantize(mixColors(tint, base, STEP.softPress));
498
+ return {
499
+ press: formatHex(step(base, STEP.press, mode)),
500
+ "soft-hover": formatHex(softHover),
501
+ "soft-press": formatHex(softPress),
502
+ "on-soft": formatHex(untilLegible(base, tint, AA_TEXT)),
503
+ "on-soft-hover": formatHex(untilLegible(base, softHover, AA_TEXT)),
504
+ "on-soft-press": formatHex(untilLegible(base, softPress, AA_TEXT))
505
+ };
506
+ };
507
+ const brand = familyStates(color.brand.primary, color.brand.soft);
508
+ const accent = familyStates(color.accent.primary, color.accent.soft);
509
+ const status = Object.fromEntries(
510
+ STATUS_KEYS.flatMap((key) => {
511
+ const base = parseHex(color.status[key]);
512
+ const tint = quantize(mixColors(surface, base, 0.14));
513
+ const softHover = quantize(mixColors(tint, base, STEP.softHover));
514
+ return [
515
+ [`${key}-hover`, formatHex(step(base, STEP.hover, mode))],
516
+ [`${key}-press`, formatHex(step(base, STEP.press, mode))],
517
+ [`${key}-soft`, formatHex(tint)],
518
+ [`${key}-soft-hover`, formatHex(softHover)],
519
+ /**
520
+ * The alert's edge. Mixing the neutral border toward the status by a
521
+ * fixed 36% — the recipe the stylesheet used inline — produced a
522
+ * 1.8:1 edge on the tint, i.e. an edge nobody could see. Derived from
523
+ * the status colour instead, softened as far as 3:1 allows.
524
+ */
525
+ [`${key}-border`, formatHex(quantize(fadeToward(base, tint, tint, AA_UI)))],
526
+ [`${key}-contrast`, formatHex(bestContrast(base, [WHITE2, inverse]))],
527
+ // The status colour as ink on its own tint — soft alerts and badges.
528
+ [`${key}-on-soft`, formatHex(untilLegible(base, tint, AA_TEXT))],
529
+ [`${key}-on-soft-hover`, formatHex(untilLegible(base, softHover, AA_TEXT))]
530
+ ];
531
+ })
532
+ );
533
+ const disabledBg = subtle;
534
+ const disabledFg = quantize(fadeToward(muted, disabledBg, disabledBg, AA_UI));
535
+ const disabledBorder = mixColors(parseHex(color.border.default), disabledBg, 0.45);
536
+ return {
537
+ border: {
538
+ /**
539
+ * Control boundaries. Explicit in the semantic layer when a product sets
540
+ * one; derived here from `border.strong` otherwise, pushed until it
541
+ * clears 3:1 on the *subtle* surface — the worst of the three, and the
542
+ * one inputs actually sit on inside a card.
543
+ */
544
+ interactive: color.border.interactive ?? formatHex(untilLegible(parseHex(color.border.strong), subtle, AA_UI))
545
+ },
546
+ brand,
547
+ accent,
548
+ status,
549
+ state: {
550
+ /** Neutral interactive surfaces: menu items, table rows, ghost buttons. */
551
+ "hover-bg": formatHex(quantize(mixColors(surface, ink, STEP.neutralHover))),
552
+ "press-bg": formatHex(quantize(mixColors(surface, ink, STEP.neutralPress))),
553
+ /**
554
+ * Selection reads as the brand tint. The ink is the brand's derived
555
+ * on-tint ink, not `brand.primary` — on the shipped dark tint those
556
+ * differ, and the raw brand measured 4.44:1 there.
557
+ */
558
+ "selected-bg": color.brand.soft,
559
+ "selected-fg": brand["on-soft"],
560
+ "selected-border": color.brand.primary,
561
+ "disabled-bg": formatHex(disabledBg),
562
+ "disabled-fg": formatHex(disabledFg),
563
+ "disabled-border": formatHex(disabledBorder)
564
+ }
565
+ };
566
+ };
567
+
245
568
  // src/token-source.ts
246
569
  var toCssVariable = (path2) => `--tree-${path2.join("-")}`;
247
570
  var flattenTokens = (record, path2 = []) => Object.entries(record).flatMap(([key, value]) => {
@@ -278,21 +601,29 @@ var buildTokenEntries = () => {
278
601
  );
279
602
  const themed = /* @__PURE__ */ new Map();
280
603
  for (const [themeName, theme] of Object.entries(treeThemes)) {
281
- for (const [path2, value] of flattenTokens(theme)) {
282
- const cssVar = toCssVariable(path2);
283
- const existing = themed.get(cssVar);
284
- if (existing) {
285
- existing.themeValues = { ...existing.themeValues, [themeName]: String(value) };
286
- continue;
604
+ const mode = themeName === "dark" ? "dark" : "light";
605
+ const derived = { color: deriveStateColors(theme.color, mode) };
606
+ for (const [source, entries] of [
607
+ ["semantic", flattenTokens(theme)],
608
+ ["derived", flattenTokens(derived)]
609
+ ]) {
610
+ for (const [path2, value] of entries) {
611
+ const cssVar = toCssVariable(path2);
612
+ const existing = themed.get(cssVar);
613
+ if (existing) {
614
+ existing.themeValues = { ...existing.themeValues, [themeName]: String(value) };
615
+ continue;
616
+ }
617
+ themed.set(cssVar, {
618
+ cssVar,
619
+ path: path2.join("."),
620
+ value: String(value),
621
+ category: path2[0],
622
+ derived: source === "derived",
623
+ description: source === "derived" ? `${describeToken(path2[0])} Derived by the library from the semantic layer \u2014 read it, do not override it.` : describeToken(path2[0]),
624
+ themeValues: { [themeName]: String(value) }
625
+ });
287
626
  }
288
- themed.set(cssVar, {
289
- cssVar,
290
- path: path2.join("."),
291
- value: String(value),
292
- category: path2[0],
293
- description: describeToken(path2[0]),
294
- themeValues: { [themeName]: String(value) }
295
- });
296
627
  }
297
628
  }
298
629
  for (const entry of themed.values()) {
@@ -542,15 +873,24 @@ var buildStoryIndex = (repoRoot) => {
542
873
  continue;
543
874
  }
544
875
  const componentName = componentMatch[1];
545
- const titleMatch = source.match(/title:\s*'([^']+)'/);
546
- const storyTitle = titleMatch?.[1];
876
+ const storyTitle = [...source.matchAll(/title:\s*'([^']+)'/g)].map((match) => match[1]).find((value) => /^(Components|Foundation|Patterns|Showcase)\/.+/.test(value));
877
+ if (!storyTitle) {
878
+ console.warn(
879
+ `[catalog] ${fileName} declares component: ${componentName} but no Storybook meta title \u2014 it will have no category or docs link.`
880
+ );
881
+ }
547
882
  const titleParts = storyTitle?.split("/") ?? [];
548
883
  const category = titleParts.length > 2 ? titleParts.slice(1, -1).join(" / ") : titleParts[1];
549
- index.set(componentName, {
884
+ const entry = {
550
885
  storyFile: path.relative(repoRoot, filePath),
551
886
  storyTitle,
552
887
  category
553
- });
888
+ };
889
+ index.set(componentName, entry);
890
+ const fileComponent = `T${fileName.replace(/\.stories\.ts$/, "")}`;
891
+ if (!index.has(fileComponent)) {
892
+ index.set(fileComponent, entry);
893
+ }
554
894
  }
555
895
  return index;
556
896
  };
package/package.json CHANGED
@@ -1,8 +1,27 @@
1
1
  {
2
2
  "name": "@treeui/mcp",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Model Context Protocol server and AI catalog for TreeUI.",
5
+ "license": "MIT",
6
+ "author": "TreeUI",
7
+ "homepage": "https://viserion77.github.io/treeui/",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Viserion77/treeui.git",
11
+ "directory": "packages/mcp"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/Viserion77/treeui/issues"
15
+ },
16
+ "keywords": [
17
+ "treeui",
18
+ "mcp",
19
+ "model-context-protocol",
20
+ "ai",
21
+ "design-system"
22
+ ],
5
23
  "type": "module",
24
+ "sideEffects": false,
6
25
  "main": "./dist/index.cjs",
7
26
  "module": "./dist/index.js",
8
27
  "types": "./dist/index.d.ts",