@timbal-ai/timbal-react 0.8.2 → 1.0.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.
@@ -13,14 +13,12 @@ import {
13
13
  monotoneAreaPath,
14
14
  monotoneLinePath,
15
15
  studioIntegrationCardClass,
16
- studioSearchChromeClass,
17
- studioSecondaryChromeClass,
18
16
  studioTopbarPillHeightClass,
19
17
  toNum
20
- } from "./chunk-5ZKLPWVN.esm.js";
18
+ } from "./chunk-SNLXVG7H.esm.js";
21
19
  import {
22
20
  PillSegmentedTabs
23
- } from "./chunk-OISVICYF.esm.js";
21
+ } from "./chunk-AYHOVAMI.esm.js";
24
22
  import {
25
23
  Button,
26
24
  Dialog,
@@ -31,10 +29,195 @@ import {
31
29
  TIMBAL_V2_SWITCH_THUMB,
32
30
  TIMBAL_V2_SWITCH_TRACK_OFF,
33
31
  TimbalV2Button,
34
- cn
35
- } from "./chunk-QVAUCVQA.esm.js";
32
+ cn,
33
+ controlClass
34
+ } from "./chunk-FOD67Z6G.esm.js";
35
+
36
+ // src/design/ui-vocabulary.ts
37
+ var SEMANTIC_COLOR_TOKENS = [
38
+ // shadcn-style base tokens
39
+ "background",
40
+ "foreground",
41
+ "card",
42
+ "card-foreground",
43
+ "popover",
44
+ "popover-foreground",
45
+ "primary",
46
+ "primary-foreground",
47
+ "secondary",
48
+ "secondary-foreground",
49
+ "muted",
50
+ "muted-foreground",
51
+ "accent",
52
+ "accent-foreground",
53
+ "destructive",
54
+ "destructive-foreground",
55
+ "border",
56
+ "input",
57
+ "ring",
58
+ // sidebar scope
59
+ "sidebar",
60
+ "sidebar-foreground",
61
+ "sidebar-primary",
62
+ "sidebar-primary-foreground",
63
+ "sidebar-accent",
64
+ "sidebar-accent-foreground",
65
+ "sidebar-border",
66
+ "sidebar-ring",
67
+ // timbal chrome extensions
68
+ "elevated-from",
69
+ "elevated-to",
70
+ "modal-from",
71
+ "modal-to",
72
+ "playground-from",
73
+ "playground-via",
74
+ "playground-to",
75
+ "composer-bg",
76
+ "composer-border",
77
+ "composer-border-focus",
78
+ "bubble-user",
79
+ "bubble-user-foreground",
80
+ "code-block-bg",
81
+ "code-header-bg"
82
+ ];
83
+ var RESERVED_GRADIENT_TOKENS = [
84
+ "primary-fill-from",
85
+ "primary-fill-to",
86
+ "primary-fill-hover-from",
87
+ "primary-fill-hover-to",
88
+ "primary-fill-active-from",
89
+ "primary-fill-active-to",
90
+ "secondary-fill-hover-from",
91
+ "secondary-fill-hover-to",
92
+ "secondary-fill-active-from",
93
+ "secondary-fill-active-to",
94
+ "destructive-fill-hover-from",
95
+ "destructive-fill-hover-to",
96
+ "destructive-fill-active-from",
97
+ "destructive-fill-active-to",
98
+ "ghost-fill-hover",
99
+ "ghost-fill-active",
100
+ "elevated-from",
101
+ "elevated-to",
102
+ "modal-from",
103
+ "modal-to",
104
+ "playground-from",
105
+ "playground-via",
106
+ "playground-to"
107
+ ];
108
+ var TAILWIND_PALETTE_COLORS = [
109
+ "slate",
110
+ "gray",
111
+ "zinc",
112
+ "neutral",
113
+ "stone",
114
+ "red",
115
+ "orange",
116
+ "amber",
117
+ "yellow",
118
+ "lime",
119
+ "green",
120
+ "emerald",
121
+ "teal",
122
+ "cyan",
123
+ "sky",
124
+ "blue",
125
+ "indigo",
126
+ "violet",
127
+ "purple",
128
+ "fuchsia",
129
+ "pink",
130
+ "rose"
131
+ ];
132
+ var COLOR_UTILITY_PREFIXES = [
133
+ "bg",
134
+ "text",
135
+ "border",
136
+ "ring",
137
+ "from",
138
+ "via",
139
+ "to",
140
+ "fill",
141
+ "stroke",
142
+ "decoration",
143
+ "outline",
144
+ "shadow",
145
+ "divide",
146
+ "accent",
147
+ "caret"
148
+ ];
149
+ var SLOP_BUDGETS = {
150
+ /** Max decorative/standalone icons rendered in a single generated file. */
151
+ maxIconsPerView: 6,
152
+ /** Max consecutive list rows separated by an explicit border/divider before
153
+ * it reads as a "ruled table" — prefer spacing or zebra instead. */
154
+ maxRowDividers: 2
155
+ };
156
+ var HOUSE_RULES = [
157
+ {
158
+ id: "semantic-color",
159
+ rule: "Color only through semantic tokens \u2014 never a raw palette color, hex, or oklch literal.",
160
+ why: "The theme generator owns every color; hardcoding breaks dark mode and rebranding.",
161
+ slop: `<span className="text-blue-600 bg-green-50">`,
162
+ good: `<span className="text-primary bg-muted">`
163
+ },
164
+ {
165
+ id: "no-decorative-icons",
166
+ rule: "Icons must earn their place (action, nav, or status). Never add an icon beside a label that already says the thing.",
167
+ why: "An icon on every tile/card is the #1 tell of generated slop.",
168
+ slop: `<StatTile label={<><BarChart2 /> Revenue</>} value="$95k" />`,
169
+ good: `<StatTile label="Revenue" value="$95k" />`
170
+ },
171
+ {
172
+ id: "neutral-trend",
173
+ rule: "Don't put a colored trend pill on every metric. Use a trend only when the delta is the point, and keep it muted.",
174
+ why: "Loud green/red pills everywhere are noise, not signal.",
175
+ slop: `<MetricTile trend="+8%" className="text-green-500" />`,
176
+ good: `<MetricTile label="Win rate" value="50%" />`
177
+ },
178
+ {
179
+ id: "values-normal-weight",
180
+ rule: "Metric values use normal font weight, not bold.",
181
+ why: "Giant bold numbers read as a template; normal weight reads as a product.",
182
+ slop: `<span className="text-3xl font-bold tabular-nums">$322k</span>`,
183
+ good: `<span className="text-2xl font-normal tabular-nums">$322k</span>`
184
+ },
185
+ {
186
+ id: "no-card-in-card",
187
+ rule: "Don't nest a bordered card inside another bordered card. Group with spacing or a Section instead.",
188
+ why: "Card-in-card doubles borders and shadows for no information gain."
189
+ },
190
+ {
191
+ id: "no-row-dividers",
192
+ rule: "Don't put a divider between every list row. Use spacing or zebra striping.",
193
+ why: "A rule under every row turns a clean list into a dense ledger."
194
+ },
195
+ {
196
+ id: "no-data-gradient",
197
+ rule: "Gradients are reserved for chrome (composer, elevated surface, playground). Never on a data card, tile, or table.",
198
+ why: "Gradient stat cards are the canonical 'AI dashboard' look."
199
+ },
200
+ {
201
+ id: "compose-from-blocks",
202
+ rule: "Build from premade blocks (MetricRow, MetricChartCard, DataTable, IntegrationCard). Drop to raw primitives only when no block fits.",
203
+ why: "Slop appears the moment generation falls below the curated block layer."
204
+ },
205
+ {
206
+ id: "use-kit-controls",
207
+ rule: "Use the kit's controls (SearchInput, Select, DropdownMenu, FieldInput, FieldSelect) \u2014 never hand-roll an input/trigger surface (`border-input rounded-* bg-\u2026`).",
208
+ why: "Hand-rolled controls drift from the shared control-surface skin and look foreign next to kit controls.",
209
+ slop: `<button className="rounded-lg border border-input bg-transparent px-3 h-9">`,
210
+ good: `<SelectTrigger><SelectValue /></SelectTrigger>`
211
+ }
212
+ ];
36
213
 
37
214
  // src/app/agent-instructions.ts
215
+ var ANTI_SLOP_CHECKLIST = HOUSE_RULES.map((r) => {
216
+ const pair = r.slop && r.good ? `
217
+ - slop: \`${r.slop}\`
218
+ - good: \`${r.good}\`` : "";
219
+ return `- **${r.id}** \u2014 ${r.rule} (${r.why})${pair}`;
220
+ }).join("\n");
38
221
  var APP_KIT_AGENT_INSTRUCTIONS = `
39
222
  ## App kit (@timbal-ai/timbal-react/app)
40
223
 
@@ -82,7 +265,15 @@ Theming helpers (import from the package root or \`/app\`): \`createTimbalTheme\
82
265
  | **Modals** | Use \`AppConfirmDialog\` for destructive/export confirmations. |
83
266
  | **Metrics** | Overview KPIs \u2192 \`MetricRow\` or \`MetricChartCard\` (not four separate heavy cards). Values use **normal** font weight, not bold. |
84
267
  | **Integrations** | Catalog \u2192 \`IntegrationCard\` grid; connected list \u2192 \`ConnectionRow\` inside \`ConnectionRowList\`. Footer CTAs: \`Button variant="secondary"\`. |
85
- | **Anti-slop** | No loud green/red trend pills on every tile; no \`bg-card\` flat grids when platform chrome exists; avoid recycling demo names ("Operations", mock workforce lists). |
268
+ | **Anti-slop** | Follow the **anti-slop checklist** below. No loud green/red trend pills on every tile; no \`bg-card\` flat grids when platform chrome exists; avoid recycling demo names ("Operations", mock workforce lists). |
269
+
270
+ ### Anti-slop checklist (required \u2014 output is linted against this)
271
+
272
+ Generated UIs are checked by \`lintGeneratedUi\` and rejected on any error. Self-review against these before returning code (icon budget: ${SLOP_BUDGETS.maxIconsPerView} per view; at most ${SLOP_BUDGETS.maxRowDividers} ruled rows before it reads as a ledger):
273
+
274
+ ${ANTI_SLOP_CHECKLIST}
275
+
276
+ The cause of slop is dropping **below** the curated block layer into raw primitives + free Tailwind. Stay on the blocks; reach for primitives only when no block fits, and even then keep colors on semantic tokens.
86
277
 
87
278
  ### Accessibility (required)
88
279
 
@@ -108,7 +299,8 @@ Theming helpers (import from the package root or \`/app\`): \`createTimbalTheme\
108
299
  | \`useAppShellChat\` | Custom open/close trigger when \`hideChatTrigger\` on shell. |
109
300
  | \`Page\` | Page title, description, \`breadcrumbs\`, \`actions\`, children. |
110
301
  | \`Section\` | Titled block inside a page. |
111
- | \`SubNav\` | In-page tabs: \`items\`, \`activeId\`, \`onChange\`. |
302
+ | \`SubNav\` | **Section switcher** (Overview / Reports pill bar): \`items\`, \`activeId\`, \`onChange\`. Never use Radix/shadcn \`Tabs\` \u2014 it is not in this package. Switch panels with state or the router. |
303
+ | **Menus** | **Select** = short list, no search. **Combobox** = searchable (same trigger as Select). **Command** only inside \`PopoverContent variant="list"\` or Combobox \u2014 never padded default Popover. See \`examples/app-kit/src/recipes/primitives-catalog.ts\`. |
112
304
  | \`Breadcrumbs\` | Trail: \`items: [{ label, href? }]\`. |
113
305
  | \`Button\` | Actions \u2014 \`variant="secondary"\` for catalog/secondary CTAs; \`variant="default"\` for primary. |
114
306
  | \`StatTile\` | Single KPI in its own card (grid of scattered stats). Prefer \`MetricRow\` for a unified overview strip. |
@@ -164,19 +356,36 @@ Theming helpers (import from the package root or \`/app\`): \`createTimbalTheme\
164
356
 
165
357
  Studio chrome (\`StudioSidebar\`, \`ModeToggle\`, \u2026) lives in \`@timbal-ai/timbal-react/studio\` \u2014 optional, not required for every dashboard.
166
358
 
167
- ### Recipe index (\`examples/app-kit/recipes/\`)
359
+ ### Block recipes \u2014 compose these (don't clone wholesale)
360
+
361
+ Ready-made **section patterns** assembled from the components above. Each is a composition to rebuild in your own domain with your data \u2014 **not** an importable component. Reach for a block before dropping to raw primitives.
362
+
363
+ **Settings**
364
+ - **Project settings** \u2014 General / Usage / Danger sections; the floating save bar appears on first edit. Compose \`SettingsSection\` + \`FieldInput\`/\`FieldSwitch\` + \`FieldRow\` + \`InfoCard\` + \`DangerZone\` + \`FloatingUnsavedChangesBar\`.
365
+ - **Settings form** \u2014 compact stacked form for one concern (profile, billing). Compose \`FormSection\` + \`FieldInput\`/\`FieldSelect\`/\`FieldTextarea\`.
366
+
367
+ **Data & metrics**
368
+ - **Metrics row** \u2014 KPI strip in one elevated card. Compose \`MetricRow\` + \`MetricTile\`.
369
+ - **Analytics card** \u2014 selectable KPI tiles driving a shared chart. Compose \`MetricChartCard\` + \`LineAreaChart\`.
370
+ - **Charts panel** \u2014 embedded chart artifact. Compose \`ChartPanel\` + \`ChartArtifactView\`.
371
+ - **Table + filters** \u2014 \`FilterBar\` above a sortable \`DataTable\` (+ \`StatusBadge\` in cells).
168
372
 
169
- | Recipe file | Components to study |
170
- |-------------|---------------------|
171
- | \`metrics-row.tsx\` | \`Page\`, \`MetricRow\` |
172
- | \`analytics-card.tsx\` | \`MetricChartCard\`, \`Button\` |
173
- | \`integrations-grid.tsx\` | \`IntegrationCard\`, \`ConnectionRowList\`, \`PlanBadge\` |
174
- | \`table-with-filters.tsx\` | \`FilterBar\`, \`DataTable\` |
175
- | \`settings-page.tsx\` | \`SettingsSection\`, \`DangerZone\`, \`FloatingUnsavedChangesBar\` |
176
- | \`resource-gallery.tsx\` | \`ResourceCard\`, \`StatusDot\`, \`Sparkline\` |
177
- | \`charts-panel.tsx\` | \`ChartPanel\`, \`ChartArtifact\` |
178
- | \`copilot-overlay.tsx\` | \`AppShell\`, \`AppChatPanel\` |
179
- | \`theme-presets.tsx\` | \`ThemePresetGallery\`, \`applyTimbalTheme\` |
373
+ **Collections**
374
+ - **Integrations grid** \u2014 connector catalog + connected list. Compose \`IntegrationCard\` + \`PlanBadge\` + \`ConnectionRowList\` (\`IntegrationsEmptyState\` when empty).
375
+ - **Resource gallery** \u2014 project / agent / dataset cards. Compose \`ResourceCard\` + \`StatusDot\` + \`Sparkline\`.
376
+
377
+ **Overlays & flows** (animate automatically)
378
+ - **Confirm & destructive** \u2014 confirm/cancel modal or destructive alert. Compose \`AppConfirmDialog\` (or \`AlertDialog\`) + \`Button\`; never hand-roll a \`Dialog\` for confirms.
379
+ - **Detail sheet** \u2014 slide-over edit panel without leaving the list. Compose \`Sheet\` + \`Field*\` + \`Button\` + \`Separator\`.
380
+
381
+ **States & auth**
382
+ - **Empty states** \u2014 no-data / no-results / first-run. Compose \`EmptyState\` + \`Card\` + \`Button\`.
383
+ - **Sign-in card** \u2014 centered auth entry. Compose \`Card\` + \`Input\` + \`Label\` + \`Button\`.
384
+
385
+ **Shells & theming**
386
+ - **Minimal shell** \u2014 \`AppShell\` + \`Page\` (no sidebar/chat).
387
+ - **Copilot overlay** \u2014 \`AppShell\` + floating \`AppChatPanel\`.
388
+ - **Theme presets** \u2014 \`ThemePresetGallery\` + \`applyTimbalTheme\` (never hand-author OKLCH).
180
389
 
181
390
  ### Typical compositions
182
391
 
@@ -187,6 +396,7 @@ Studio chrome (\`StudioSidebar\`, \`ModeToggle\`, \u2026) lives in \`@timbal-ai/
187
396
  - **Integrations** \u2014 grid of \`IntegrationCard\`; \`ConnectionRowList\` for connected providers; \`IntegrationsEmptyState\` when empty.
188
397
  - **Resource gallery** \u2014 grid of \`ResourceCard\`.
189
398
  - **Copilot-assisted app** \u2014 \`AppCopilotProvider\` + \`AppShell\` with \`chat={<AppChatPanel workforceId="\u2026" />}\`.
399
+ - **Motion is automatic** \u2014 Dialog, AlertDialog, Sheet, Popover, DropdownMenu, Select, Tooltip, Toast, and Accordion/Collapsible animate out of the box (fade/zoom/slide/height) via the engine inlined in \`styles.css\`. Do not add a separate animation library or hand-write \`@keyframes\`.
190
400
 
191
401
  ### Example imports
192
402
 
@@ -222,6 +432,211 @@ import {
222
432
  - For rich in-chat widgets, use **artifacts** (\`ARTIFACT_AGENT_INSTRUCTIONS\`) \u2014 app kit is for the **host application shell**.
223
433
  `.trim();
224
434
 
435
+ // src/design/ui-lint.ts
436
+ var PALETTE_GROUP = TAILWIND_PALETTE_COLORS.join("|");
437
+ var PREFIX_GROUP = COLOR_UTILITY_PREFIXES.join("|");
438
+ var RAW_COLOR_RE = new RegExp(
439
+ `(?:^|[\\s"'\`:])(?:[a-z-]+:)*(?:${PREFIX_GROUP})-(?:${PALETTE_GROUP})-\\d{2,3}(?:/\\d{1,3})?`,
440
+ "g"
441
+ );
442
+ var COLOR_LITERAL_RE = /#[0-9a-fA-F]{3,8}\b|\b(?:oklch|rgba?|hsla?)\s*\(/g;
443
+ var INLINE_STYLE_COLOR_RE = /style=\{\{[^}]*\b(?:color|background|backgroundColor|borderColor|fill|stroke)\b/;
444
+ var BOLD_VALUE_RE = /text-(?:xl|2xl|3xl|4xl|5xl|6xl)[^"'`]*\bfont-(?:bold|extrabold|black|semibold)|font-(?:bold|extrabold|black|semibold)[^"'`]*text-(?:xl|2xl|3xl|4xl|5xl|6xl)/;
445
+ var GRADIENT_RE = /\bbg-(?:gradient|linear|radial|conic)-/;
446
+ var GRADIENT_DIRECTIONS = /* @__PURE__ */ new Set([
447
+ "t",
448
+ "tr",
449
+ "r",
450
+ "br",
451
+ "b",
452
+ "bl",
453
+ "l",
454
+ "tl"
455
+ ]);
456
+ var ICON_IMPORT_RE = /from\s+["']lucide-react["']/;
457
+ var RAW_CONTROL_SURFACE_RE = /\bborder-input\b/;
458
+ var RESERVED_GRADIENT_SET = new Set(RESERVED_GRADIENT_TOKENS);
459
+ function stripVariants(util) {
460
+ return util.replace(/^(?:[a-z-]+:)*/, "");
461
+ }
462
+ function isCommentOrImport(line) {
463
+ const t = line.trim();
464
+ return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*") || t.startsWith("import ") || t.startsWith("export ");
465
+ }
466
+ function lintGeneratedUi(source, options = {}) {
467
+ const maxIcons = options.maxIconsPerView ?? SLOP_BUDGETS.maxIconsPerView;
468
+ const maxRowDividers = options.maxRowDividers ?? SLOP_BUDGETS.maxRowDividers;
469
+ const findings = [];
470
+ const lines = source.split("\n");
471
+ let usesLucide = false;
472
+ let iconUsageCount = 0;
473
+ let dividerRunCount = 0;
474
+ const lucideNames = /* @__PURE__ */ new Set();
475
+ for (let i = 0; i < lines.length; i++) {
476
+ const line = lines[i];
477
+ const lineNo = i + 1;
478
+ if (ICON_IMPORT_RE.test(line)) {
479
+ usesLucide = true;
480
+ const named = line.match(/\{([^}]*)\}/);
481
+ if (named) {
482
+ for (const raw of named[1].split(",")) {
483
+ const name = raw.trim().split(/\s+as\s+/)[0].trim();
484
+ if (name) lucideNames.add(name);
485
+ }
486
+ }
487
+ continue;
488
+ }
489
+ if (isCommentOrImport(line)) continue;
490
+ const rawColors = line.match(RAW_COLOR_RE);
491
+ if (rawColors) {
492
+ for (const m of rawColors) {
493
+ findings.push({
494
+ rule: "raw-color",
495
+ severity: "error",
496
+ line: lineNo,
497
+ message: "Hardcoded palette color. Use a semantic token (text-primary, bg-muted, border-border, text-muted-foreground, \u2026) so dark mode and rebranding work.",
498
+ snippet: m.trim().replace(/^["'`:\s]+/, "")
499
+ });
500
+ }
501
+ }
502
+ const literals = line.match(COLOR_LITERAL_RE);
503
+ if (literals) {
504
+ findings.push({
505
+ rule: "color-literal",
506
+ severity: "error",
507
+ line: lineNo,
508
+ message: "Hardcoded color literal. Colors must come from the theme generator (createTimbalTheme) and semantic tokens \u2014 never inline hex/oklch/rgb.",
509
+ snippet: line.trim().slice(0, 120)
510
+ });
511
+ }
512
+ if (INLINE_STYLE_COLOR_RE.test(line)) {
513
+ findings.push({
514
+ rule: "inline-style-color",
515
+ severity: "error",
516
+ line: lineNo,
517
+ message: "Inline style color. Move color to a semantic Tailwind token on className.",
518
+ snippet: line.trim().slice(0, 120)
519
+ });
520
+ }
521
+ if (RAW_CONTROL_SURFACE_RE.test(line)) {
522
+ findings.push({
523
+ rule: "raw-control-surface",
524
+ severity: "warn",
525
+ line: lineNo,
526
+ message: "Hand-rolled control surface (border-input). Use a kit control \u2014 SearchInput, Select, DropdownMenu, FieldInput, FieldSelect \u2014 so it matches every other control.",
527
+ snippet: line.trim().slice(0, 120)
528
+ });
529
+ }
530
+ if (BOLD_VALUE_RE.test(line)) {
531
+ findings.push({
532
+ rule: "bold-metric",
533
+ severity: "warn",
534
+ line: lineNo,
535
+ message: "Bold large value. House style: metric values use font-normal, not bold \u2014 bold giant numbers read as a template.",
536
+ snippet: line.trim().slice(0, 120)
537
+ });
538
+ }
539
+ if (GRADIENT_RE.test(line)) {
540
+ const fromTo = line.match(
541
+ new RegExp(`(?:from|via|to)-([a-z-]+)`, "g")
542
+ );
543
+ const colorStops = (fromTo ?? []).map((u) => stripVariants(u).replace(/^(?:from|via|to)-/, "")).filter((token) => !GRADIENT_DIRECTIONS.has(token));
544
+ const allReserved = colorStops.length > 0 && colorStops.every((token) => RESERVED_GRADIENT_SET.has(token));
545
+ if (!allReserved) {
546
+ findings.push({
547
+ rule: "data-gradient",
548
+ severity: "warn",
549
+ line: lineNo,
550
+ message: "Gradient outside chrome. Gradients are reserved for buttons / elevated / modal / playground \u2014 never a data card, tile, or table.",
551
+ snippet: line.trim().slice(0, 120)
552
+ });
553
+ }
554
+ }
555
+ if (/\b(?:border-t|border-b|divide-y)\b/.test(line)) {
556
+ dividerRunCount++;
557
+ if (dividerRunCount === maxRowDividers + 1) {
558
+ findings.push({
559
+ rule: "row-divider",
560
+ severity: "warn",
561
+ line: lineNo,
562
+ message: "Divider on every row. Prefer spacing (gap-*) or zebra striping over a rule under each list item.",
563
+ snippet: line.trim().slice(0, 120)
564
+ });
565
+ }
566
+ } else if (line.trim() !== "" && !line.includes("className")) {
567
+ if (!/^\s*[)>}/]/.test(line)) dividerRunCount = 0;
568
+ }
569
+ if (usesLucide && lucideNames.size > 0) {
570
+ for (const name of lucideNames) {
571
+ const usage = new RegExp(`<${name}\\b`, "g");
572
+ const hits = line.match(usage);
573
+ if (hits) iconUsageCount += hits.length;
574
+ }
575
+ }
576
+ }
577
+ if (usesLucide && iconUsageCount > maxIcons) {
578
+ findings.push({
579
+ rule: "icon-spam",
580
+ severity: "warn",
581
+ line: 1,
582
+ message: `Too many icons (${iconUsageCount} > ${maxIcons}). Icons should mark actions/nav/status \u2014 not decorate every label, tile, and card.`,
583
+ snippet: `${iconUsageCount} lucide-react icon usages`
584
+ });
585
+ }
586
+ const effectiveErrors = findings.filter(
587
+ (f) => f.severity === "error" || options.strict && f.severity === "warn"
588
+ ).length;
589
+ return {
590
+ findings,
591
+ errorCount: findings.filter((f) => f.severity === "error").length,
592
+ warnCount: findings.filter((f) => f.severity === "warn").length,
593
+ ok: effectiveErrors === 0
594
+ };
595
+ }
596
+ function formatLintReport(findings) {
597
+ if (findings.length === 0) return "";
598
+ const lines = findings.slice().sort((a, b) => a.line - b.line).map((f) => {
599
+ const tag = f.severity === "error" ? "ERROR" : "warn ";
600
+ return ` ${tag} L${f.line} [${f.rule}] ${f.message}
601
+ \u2192 ${f.snippet}`;
602
+ });
603
+ const errs = findings.filter((f) => f.severity === "error").length;
604
+ const warns = findings.filter((f) => f.severity === "warn").length;
605
+ return `Anti-slop review: ${errs} error(s), ${warns} warning(s)
606
+ ${lines.join("\n")}`;
607
+ }
608
+
609
+ // src/design/ui-review.ts
610
+ function reviewGeneratedUi(source, options = {}) {
611
+ const lint = lintGeneratedUi(source, options);
612
+ const report = formatLintReport(lint.findings);
613
+ if (lint.ok) {
614
+ return { lint, passed: true, report, revisionPrompt: null };
615
+ }
616
+ const revisionPrompt = [
617
+ "The generated UI failed the Timbal anti-slop review. Fix every issue below, then return the corrected code only.",
618
+ "",
619
+ report,
620
+ "",
621
+ "Rules: colors come only from semantic tokens (text-primary, bg-muted, border-border, text-muted-foreground, \u2026) \u2014 never palette colors, hex, or oklch. Icons mark actions/nav/status, not decoration. Metric values use font-normal. No gradients on data surfaces. No divider under every row. Do not change anything that already passed."
622
+ ].join("\n");
623
+ return { lint, passed: false, report, revisionPrompt };
624
+ }
625
+ var UI_REVIEW_AGENT_INSTRUCTIONS = `
626
+ ## Self-review before returning UI (anti-slop)
627
+
628
+ Before you output any generated UI code, silently re-read it and fix anything that matches the slop checklist \u2014 this is the same rubric an automated linter applies, so output that fails it will be rejected and sent back:
629
+
630
+ - **No hardcoded colors.** Every color is a semantic token (\`text-primary\`, \`bg-muted\`, \`border-border\`, \`text-muted-foreground\`, \`bg-destructive\`, \u2026). No \`text-blue-600\`, no \`#hex\`, no \`oklch(...)\`, no \`style={{ color }}\`.
631
+ - **No decorative icons.** An icon must mark an action, nav target, or status. Remove icons that sit beside a label that already says the thing. Aim for very few icons per view.
632
+ - **Muted, sparse trends.** No colored up/down pill on every metric. Show a trend only when the change is the point.
633
+ - **Normal-weight values.** Metric numbers use \`font-normal\`, never \`font-bold\` at large sizes.
634
+ - **No card-in-card, no per-row dividers, no gradients on data surfaces.** Group with spacing/Sections; reserve gradients for chrome.
635
+ - **Compose from blocks.** Prefer \`MetricRow\` / \`MetricChartCard\` / \`DataTable\` / \`IntegrationCard\` over hand-assembled primitives.
636
+
637
+ If a check fails, fix it and re-read once more. Only return code that would pass clean.
638
+ `.trim();
639
+
225
640
  // src/design/oklch.ts
226
641
  var clamp = (n, min, max) => Math.min(max, Math.max(min, n));
227
642
  var round = (n, digits) => {
@@ -1113,18 +1528,13 @@ var appFilterBarClass = cn(
1113
1528
  "flex flex-wrap items-center gap-2",
1114
1529
  studioTopbarPillHeightClass
1115
1530
  );
1116
- var appSearchInputClass = cn(studioSearchChromeClass, "text-sm");
1531
+ var appSearchInputClass = controlClass({}, "inline-flex items-center gap-2");
1117
1532
  var appBreadcrumbsClass = "flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground";
1118
1533
  var appBreadcrumbLinkClass = "transition-colors hover:text-foreground";
1119
1534
  var appFieldClass = "flex flex-col gap-1.5";
1120
1535
  var appFieldLabelClass = "text-sm font-medium text-foreground";
1121
1536
  var appFieldHintClass = "text-xs text-muted-foreground";
1122
- var appInputClass = cn(
1123
- studioSecondaryChromeClass,
1124
- "h-10 w-full rounded-lg px-3 text-sm text-foreground outline-none",
1125
- "placeholder:text-muted-foreground/70",
1126
- "focus-visible:ring-2 focus-visible:ring-foreground/10"
1127
- );
1537
+ var appInputClass = controlClass({}, "w-full");
1128
1538
  var appEmptyStateClass = cn(
1129
1539
  appSurfaceCardClass,
1130
1540
  "flex flex-col items-center justify-center gap-2 py-12 text-center"
@@ -2984,7 +3394,17 @@ var Sparkline = ({
2984
3394
  };
2985
3395
 
2986
3396
  export {
3397
+ SEMANTIC_COLOR_TOKENS,
3398
+ RESERVED_GRADIENT_TOKENS,
3399
+ TAILWIND_PALETTE_COLORS,
3400
+ COLOR_UTILITY_PREFIXES,
3401
+ SLOP_BUDGETS,
3402
+ HOUSE_RULES,
2987
3403
  APP_KIT_AGENT_INSTRUCTIONS,
3404
+ lintGeneratedUi,
3405
+ formatLintReport,
3406
+ reviewGeneratedUi,
3407
+ UI_REVIEW_AGENT_INSTRUCTIONS,
2988
3408
  createTimbalTheme,
2989
3409
  themeToCss,
2990
3410
  ensureThemeFontLink,
@@ -1,10 +1,10 @@
1
1
  import {
2
- studioSecondaryChromeClass,
3
2
  studioTopbarPillHeightClass
4
- } from "./chunk-5ZKLPWVN.esm.js";
3
+ } from "./chunk-SNLXVG7H.esm.js";
5
4
  import {
6
- cn
7
- } from "./chunk-QVAUCVQA.esm.js";
5
+ cn,
6
+ controlSurfaceClass
7
+ } from "./chunk-FOD67Z6G.esm.js";
8
8
 
9
9
  // src/chat/workforce-selector.tsx
10
10
  import { ChevronDownIcon } from "lucide-react";
@@ -24,8 +24,8 @@ var WorkforceSelector = ({
24
24
  {
25
25
  className: cn(
26
26
  "aui-workforce-selector relative inline-flex items-center",
27
+ controlSurfaceClass,
27
28
  studioTopbarPillHeightClass,
28
- studioSecondaryChromeClass,
29
29
  "rounded-full",
30
30
  className
31
31
  ),
@@ -18,7 +18,7 @@ import {
18
18
  TooltipProvider,
19
19
  TooltipTrigger,
20
20
  cn
21
- } from "./chunk-QVAUCVQA.esm.js";
21
+ } from "./chunk-FOD67Z6G.esm.js";
22
22
 
23
23
  // src/chat/tooltip-icon-button.tsx
24
24
  import { forwardRef } from "react";
@@ -3907,8 +3907,6 @@ export {
3907
3907
  studioTopbarPillHeightClass,
3908
3908
  studioTopbarIconPillClass,
3909
3909
  studioPlaygroundGradientClass,
3910
- studioSecondaryChromeClass,
3911
- studioSearchChromeClass,
3912
3910
  studioIntegrationCardClass,
3913
3911
  studioSidebarPanelClass,
3914
3912
  studioSidebarNavItemClass,