@timbal-ai/timbal-react 0.8.1 → 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.
- package/CHANGELOG.md +40 -0
- package/README.md +69 -6
- package/dist/app.cjs +704 -56
- package/dist/app.d.cts +4 -3
- package/dist/app.d.ts +4 -3
- package/dist/app.esm.js +26 -4
- package/dist/{button-ClSgD6OF.d.cts → button-BoyX5pM_.d.cts} +1 -1
- package/dist/{button-ClSgD6OF.d.ts → button-BoyX5pM_.d.ts} +1 -1
- package/dist/{chart-artifact-DwfRtQWL.d.ts → chart-artifact-CBo9x8Ch.d.ts} +237 -13
- package/dist/{chart-artifact-DWkqIAK5.d.cts → chart-artifact-DOkwSTjQ.d.cts} +237 -13
- package/dist/chat.cjs +23 -1
- package/dist/chat.esm.js +3 -3
- package/dist/{chunk-OISVICYF.esm.js → chunk-AYHOVAMI.esm.js} +1 -1
- package/dist/{chunk-VWHHKAHN.esm.js → chunk-C6IXFM4T.esm.js} +4 -4
- package/dist/{chunk-QVAUCVQA.esm.js → chunk-FOD67Z6G.esm.js} +42 -0
- package/dist/{chunk-GBBLAM3G.esm.js → chunk-GLPOVYEA.esm.js} +776 -172
- package/dist/{chunk-CFU3YDTV.esm.js → chunk-RZ6QC6RG.esm.js} +5 -5
- package/dist/{chunk-5ZKLPWVN.esm.js → chunk-SNLXVG7H.esm.js} +1 -3
- package/dist/chunk-YEFBANNF.esm.js +3485 -0
- package/dist/index.cjs +3965 -84
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.esm.js +409 -9
- package/dist/studio.cjs +23 -1
- package/dist/studio.esm.js +5 -5
- package/dist/styles.css +194 -0
- package/dist/ui.cjs +3354 -89
- package/dist/ui.d.cts +402 -6
- package/dist/ui.d.ts +402 -6
- package/dist/ui.esm.js +384 -6
- package/package.json +3 -1
- package/dist/chunk-P4SN7M67.esm.js +0 -435
package/dist/app.cjs
CHANGED
|
@@ -40,6 +40,7 @@ __export(app_exports, {
|
|
|
40
40
|
Breadcrumbs: () => Breadcrumbs,
|
|
41
41
|
Button: () => Button,
|
|
42
42
|
CHART_PALETTE: () => CHART_PALETTE,
|
|
43
|
+
COLOR_UTILITY_PREFIXES: () => COLOR_UTILITY_PREFIXES,
|
|
43
44
|
ChartArtifactView: () => ChartArtifactView,
|
|
44
45
|
ChartPanel: () => ChartPanel,
|
|
45
46
|
ConnectionRow: () => ConnectionRow,
|
|
@@ -59,6 +60,7 @@ __export(app_exports, {
|
|
|
59
60
|
FilterBar: () => FilterBar,
|
|
60
61
|
FloatingUnsavedChangesBar: () => FloatingUnsavedChangesBar,
|
|
61
62
|
FormSection: () => FormSection,
|
|
63
|
+
HOUSE_RULES: () => HOUSE_RULES,
|
|
62
64
|
INTEGRATION_CATALOG_CARD_HEIGHT_CLASS: () => INTEGRATION_CATALOG_CARD_HEIGHT_CLASS,
|
|
63
65
|
InfoCard: () => InfoCard,
|
|
64
66
|
IntegrationCard: () => IntegrationCard,
|
|
@@ -70,7 +72,10 @@ __export(app_exports, {
|
|
|
70
72
|
Page: () => Page,
|
|
71
73
|
PageHeader: () => PageHeader,
|
|
72
74
|
PlanBadge: () => PlanBadge,
|
|
75
|
+
RESERVED_GRADIENT_TOKENS: () => RESERVED_GRADIENT_TOKENS,
|
|
73
76
|
ResourceCard: () => ResourceCard,
|
|
77
|
+
SEMANTIC_COLOR_TOKENS: () => SEMANTIC_COLOR_TOKENS,
|
|
78
|
+
SLOP_BUDGETS: () => SLOP_BUDGETS,
|
|
74
79
|
SearchInput: () => SearchInput,
|
|
75
80
|
Section: () => Section,
|
|
76
81
|
SettingsSection: () => SettingsSection,
|
|
@@ -81,11 +86,13 @@ __export(app_exports, {
|
|
|
81
86
|
StatusDot: () => StatusDot,
|
|
82
87
|
SubNav: () => SubNav,
|
|
83
88
|
SurfaceCard: () => SurfaceCard,
|
|
89
|
+
TAILWIND_PALETTE_COLORS: () => TAILWIND_PALETTE_COLORS,
|
|
84
90
|
THEME_AGENT_INSTRUCTIONS: () => THEME_AGENT_INSTRUCTIONS,
|
|
85
91
|
TIMBAL_THEME_PRESETS: () => TIMBAL_THEME_PRESETS,
|
|
86
92
|
ThemePresetGallery: () => ThemePresetGallery,
|
|
87
93
|
TimbalChat: () => TimbalChat,
|
|
88
94
|
TimbalThemeStyle: () => TimbalThemeStyle,
|
|
95
|
+
UI_REVIEW_AGENT_INSTRUCTIONS: () => UI_REVIEW_AGENT_INSTRUCTIONS,
|
|
89
96
|
appFilterBarClass: () => appFilterBarClass,
|
|
90
97
|
appPageColumnClass: () => appPageColumnClass,
|
|
91
98
|
appSearchInputClass: () => appSearchInputClass,
|
|
@@ -98,15 +105,203 @@ __export(app_exports, {
|
|
|
98
105
|
clearTimbalTheme: () => clearTimbalTheme,
|
|
99
106
|
connectionRowListClass: () => connectionRowListClass,
|
|
100
107
|
createTimbalTheme: () => createTimbalTheme,
|
|
108
|
+
ensureThemeFontLink: () => ensureThemeFontLink,
|
|
109
|
+
formatLintReport: () => formatLintReport,
|
|
101
110
|
getStoredThemePreset: () => getStoredThemePreset,
|
|
102
111
|
getThemePreset: () => getThemePreset,
|
|
112
|
+
lintGeneratedUi: () => lintGeneratedUi,
|
|
113
|
+
reviewGeneratedUi: () => reviewGeneratedUi,
|
|
103
114
|
themeToCss: () => themeToCss,
|
|
104
115
|
useAppCopilotContext: () => useAppCopilotContext,
|
|
105
116
|
useAppShellChat: () => useAppShellChat
|
|
106
117
|
});
|
|
107
118
|
module.exports = __toCommonJS(app_exports);
|
|
108
119
|
|
|
120
|
+
// src/design/ui-vocabulary.ts
|
|
121
|
+
var SEMANTIC_COLOR_TOKENS = [
|
|
122
|
+
// shadcn-style base tokens
|
|
123
|
+
"background",
|
|
124
|
+
"foreground",
|
|
125
|
+
"card",
|
|
126
|
+
"card-foreground",
|
|
127
|
+
"popover",
|
|
128
|
+
"popover-foreground",
|
|
129
|
+
"primary",
|
|
130
|
+
"primary-foreground",
|
|
131
|
+
"secondary",
|
|
132
|
+
"secondary-foreground",
|
|
133
|
+
"muted",
|
|
134
|
+
"muted-foreground",
|
|
135
|
+
"accent",
|
|
136
|
+
"accent-foreground",
|
|
137
|
+
"destructive",
|
|
138
|
+
"destructive-foreground",
|
|
139
|
+
"border",
|
|
140
|
+
"input",
|
|
141
|
+
"ring",
|
|
142
|
+
// sidebar scope
|
|
143
|
+
"sidebar",
|
|
144
|
+
"sidebar-foreground",
|
|
145
|
+
"sidebar-primary",
|
|
146
|
+
"sidebar-primary-foreground",
|
|
147
|
+
"sidebar-accent",
|
|
148
|
+
"sidebar-accent-foreground",
|
|
149
|
+
"sidebar-border",
|
|
150
|
+
"sidebar-ring",
|
|
151
|
+
// timbal chrome extensions
|
|
152
|
+
"elevated-from",
|
|
153
|
+
"elevated-to",
|
|
154
|
+
"modal-from",
|
|
155
|
+
"modal-to",
|
|
156
|
+
"playground-from",
|
|
157
|
+
"playground-via",
|
|
158
|
+
"playground-to",
|
|
159
|
+
"composer-bg",
|
|
160
|
+
"composer-border",
|
|
161
|
+
"composer-border-focus",
|
|
162
|
+
"bubble-user",
|
|
163
|
+
"bubble-user-foreground",
|
|
164
|
+
"code-block-bg",
|
|
165
|
+
"code-header-bg"
|
|
166
|
+
];
|
|
167
|
+
var RESERVED_GRADIENT_TOKENS = [
|
|
168
|
+
"primary-fill-from",
|
|
169
|
+
"primary-fill-to",
|
|
170
|
+
"primary-fill-hover-from",
|
|
171
|
+
"primary-fill-hover-to",
|
|
172
|
+
"primary-fill-active-from",
|
|
173
|
+
"primary-fill-active-to",
|
|
174
|
+
"secondary-fill-hover-from",
|
|
175
|
+
"secondary-fill-hover-to",
|
|
176
|
+
"secondary-fill-active-from",
|
|
177
|
+
"secondary-fill-active-to",
|
|
178
|
+
"destructive-fill-hover-from",
|
|
179
|
+
"destructive-fill-hover-to",
|
|
180
|
+
"destructive-fill-active-from",
|
|
181
|
+
"destructive-fill-active-to",
|
|
182
|
+
"ghost-fill-hover",
|
|
183
|
+
"ghost-fill-active",
|
|
184
|
+
"elevated-from",
|
|
185
|
+
"elevated-to",
|
|
186
|
+
"modal-from",
|
|
187
|
+
"modal-to",
|
|
188
|
+
"playground-from",
|
|
189
|
+
"playground-via",
|
|
190
|
+
"playground-to"
|
|
191
|
+
];
|
|
192
|
+
var TAILWIND_PALETTE_COLORS = [
|
|
193
|
+
"slate",
|
|
194
|
+
"gray",
|
|
195
|
+
"zinc",
|
|
196
|
+
"neutral",
|
|
197
|
+
"stone",
|
|
198
|
+
"red",
|
|
199
|
+
"orange",
|
|
200
|
+
"amber",
|
|
201
|
+
"yellow",
|
|
202
|
+
"lime",
|
|
203
|
+
"green",
|
|
204
|
+
"emerald",
|
|
205
|
+
"teal",
|
|
206
|
+
"cyan",
|
|
207
|
+
"sky",
|
|
208
|
+
"blue",
|
|
209
|
+
"indigo",
|
|
210
|
+
"violet",
|
|
211
|
+
"purple",
|
|
212
|
+
"fuchsia",
|
|
213
|
+
"pink",
|
|
214
|
+
"rose"
|
|
215
|
+
];
|
|
216
|
+
var COLOR_UTILITY_PREFIXES = [
|
|
217
|
+
"bg",
|
|
218
|
+
"text",
|
|
219
|
+
"border",
|
|
220
|
+
"ring",
|
|
221
|
+
"from",
|
|
222
|
+
"via",
|
|
223
|
+
"to",
|
|
224
|
+
"fill",
|
|
225
|
+
"stroke",
|
|
226
|
+
"decoration",
|
|
227
|
+
"outline",
|
|
228
|
+
"shadow",
|
|
229
|
+
"divide",
|
|
230
|
+
"accent",
|
|
231
|
+
"caret"
|
|
232
|
+
];
|
|
233
|
+
var SLOP_BUDGETS = {
|
|
234
|
+
/** Max decorative/standalone icons rendered in a single generated file. */
|
|
235
|
+
maxIconsPerView: 6,
|
|
236
|
+
/** Max consecutive list rows separated by an explicit border/divider before
|
|
237
|
+
* it reads as a "ruled table" — prefer spacing or zebra instead. */
|
|
238
|
+
maxRowDividers: 2
|
|
239
|
+
};
|
|
240
|
+
var HOUSE_RULES = [
|
|
241
|
+
{
|
|
242
|
+
id: "semantic-color",
|
|
243
|
+
rule: "Color only through semantic tokens \u2014 never a raw palette color, hex, or oklch literal.",
|
|
244
|
+
why: "The theme generator owns every color; hardcoding breaks dark mode and rebranding.",
|
|
245
|
+
slop: `<span className="text-blue-600 bg-green-50">`,
|
|
246
|
+
good: `<span className="text-primary bg-muted">`
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: "no-decorative-icons",
|
|
250
|
+
rule: "Icons must earn their place (action, nav, or status). Never add an icon beside a label that already says the thing.",
|
|
251
|
+
why: "An icon on every tile/card is the #1 tell of generated slop.",
|
|
252
|
+
slop: `<StatTile label={<><BarChart2 /> Revenue</>} value="$95k" />`,
|
|
253
|
+
good: `<StatTile label="Revenue" value="$95k" />`
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: "neutral-trend",
|
|
257
|
+
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.",
|
|
258
|
+
why: "Loud green/red pills everywhere are noise, not signal.",
|
|
259
|
+
slop: `<MetricTile trend="+8%" className="text-green-500" />`,
|
|
260
|
+
good: `<MetricTile label="Win rate" value="50%" />`
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: "values-normal-weight",
|
|
264
|
+
rule: "Metric values use normal font weight, not bold.",
|
|
265
|
+
why: "Giant bold numbers read as a template; normal weight reads as a product.",
|
|
266
|
+
slop: `<span className="text-3xl font-bold tabular-nums">$322k</span>`,
|
|
267
|
+
good: `<span className="text-2xl font-normal tabular-nums">$322k</span>`
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: "no-card-in-card",
|
|
271
|
+
rule: "Don't nest a bordered card inside another bordered card. Group with spacing or a Section instead.",
|
|
272
|
+
why: "Card-in-card doubles borders and shadows for no information gain."
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: "no-row-dividers",
|
|
276
|
+
rule: "Don't put a divider between every list row. Use spacing or zebra striping.",
|
|
277
|
+
why: "A rule under every row turns a clean list into a dense ledger."
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: "no-data-gradient",
|
|
281
|
+
rule: "Gradients are reserved for chrome (composer, elevated surface, playground). Never on a data card, tile, or table.",
|
|
282
|
+
why: "Gradient stat cards are the canonical 'AI dashboard' look."
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: "compose-from-blocks",
|
|
286
|
+
rule: "Build from premade blocks (MetricRow, MetricChartCard, DataTable, IntegrationCard). Drop to raw primitives only when no block fits.",
|
|
287
|
+
why: "Slop appears the moment generation falls below the curated block layer."
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: "use-kit-controls",
|
|
291
|
+
rule: "Use the kit's controls (SearchInput, Select, DropdownMenu, FieldInput, FieldSelect) \u2014 never hand-roll an input/trigger surface (`border-input rounded-* bg-\u2026`).",
|
|
292
|
+
why: "Hand-rolled controls drift from the shared control-surface skin and look foreign next to kit controls.",
|
|
293
|
+
slop: `<button className="rounded-lg border border-input bg-transparent px-3 h-9">`,
|
|
294
|
+
good: `<SelectTrigger><SelectValue /></SelectTrigger>`
|
|
295
|
+
}
|
|
296
|
+
];
|
|
297
|
+
|
|
109
298
|
// src/app/agent-instructions.ts
|
|
299
|
+
var ANTI_SLOP_CHECKLIST = HOUSE_RULES.map((r) => {
|
|
300
|
+
const pair = r.slop && r.good ? `
|
|
301
|
+
- slop: \`${r.slop}\`
|
|
302
|
+
- good: \`${r.good}\`` : "";
|
|
303
|
+
return `- **${r.id}** \u2014 ${r.rule} (${r.why})${pair}`;
|
|
304
|
+
}).join("\n");
|
|
110
305
|
var APP_KIT_AGENT_INSTRUCTIONS = `
|
|
111
306
|
## App kit (@timbal-ai/timbal-react/app)
|
|
112
307
|
|
|
@@ -154,7 +349,15 @@ Theming helpers (import from the package root or \`/app\`): \`createTimbalTheme\
|
|
|
154
349
|
| **Modals** | Use \`AppConfirmDialog\` for destructive/export confirmations. |
|
|
155
350
|
| **Metrics** | Overview KPIs \u2192 \`MetricRow\` or \`MetricChartCard\` (not four separate heavy cards). Values use **normal** font weight, not bold. |
|
|
156
351
|
| **Integrations** | Catalog \u2192 \`IntegrationCard\` grid; connected list \u2192 \`ConnectionRow\` inside \`ConnectionRowList\`. Footer CTAs: \`Button variant="secondary"\`. |
|
|
157
|
-
| **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). |
|
|
352
|
+
| **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). |
|
|
353
|
+
|
|
354
|
+
### Anti-slop checklist (required \u2014 output is linted against this)
|
|
355
|
+
|
|
356
|
+
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):
|
|
357
|
+
|
|
358
|
+
${ANTI_SLOP_CHECKLIST}
|
|
359
|
+
|
|
360
|
+
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.
|
|
158
361
|
|
|
159
362
|
### Accessibility (required)
|
|
160
363
|
|
|
@@ -180,7 +383,8 @@ Theming helpers (import from the package root or \`/app\`): \`createTimbalTheme\
|
|
|
180
383
|
| \`useAppShellChat\` | Custom open/close trigger when \`hideChatTrigger\` on shell. |
|
|
181
384
|
| \`Page\` | Page title, description, \`breadcrumbs\`, \`actions\`, children. |
|
|
182
385
|
| \`Section\` | Titled block inside a page. |
|
|
183
|
-
| \`SubNav\` |
|
|
386
|
+
| \`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. |
|
|
387
|
+
| **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\`. |
|
|
184
388
|
| \`Breadcrumbs\` | Trail: \`items: [{ label, href? }]\`. |
|
|
185
389
|
| \`Button\` | Actions \u2014 \`variant="secondary"\` for catalog/secondary CTAs; \`variant="default"\` for primary. |
|
|
186
390
|
| \`StatTile\` | Single KPI in its own card (grid of scattered stats). Prefer \`MetricRow\` for a unified overview strip. |
|
|
@@ -236,19 +440,36 @@ Theming helpers (import from the package root or \`/app\`): \`createTimbalTheme\
|
|
|
236
440
|
|
|
237
441
|
Studio chrome (\`StudioSidebar\`, \`ModeToggle\`, \u2026) lives in \`@timbal-ai/timbal-react/studio\` \u2014 optional, not required for every dashboard.
|
|
238
442
|
|
|
239
|
-
###
|
|
443
|
+
### Block recipes \u2014 compose these (don't clone wholesale)
|
|
444
|
+
|
|
445
|
+
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.
|
|
446
|
+
|
|
447
|
+
**Settings**
|
|
448
|
+
- **Project settings** \u2014 General / Usage / Danger sections; the floating save bar appears on first edit. Compose \`SettingsSection\` + \`FieldInput\`/\`FieldSwitch\` + \`FieldRow\` + \`InfoCard\` + \`DangerZone\` + \`FloatingUnsavedChangesBar\`.
|
|
449
|
+
- **Settings form** \u2014 compact stacked form for one concern (profile, billing). Compose \`FormSection\` + \`FieldInput\`/\`FieldSelect\`/\`FieldTextarea\`.
|
|
450
|
+
|
|
451
|
+
**Data & metrics**
|
|
452
|
+
- **Metrics row** \u2014 KPI strip in one elevated card. Compose \`MetricRow\` + \`MetricTile\`.
|
|
453
|
+
- **Analytics card** \u2014 selectable KPI tiles driving a shared chart. Compose \`MetricChartCard\` + \`LineAreaChart\`.
|
|
454
|
+
- **Charts panel** \u2014 embedded chart artifact. Compose \`ChartPanel\` + \`ChartArtifactView\`.
|
|
455
|
+
- **Table + filters** \u2014 \`FilterBar\` above a sortable \`DataTable\` (+ \`StatusBadge\` in cells).
|
|
240
456
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
457
|
+
**Collections**
|
|
458
|
+
- **Integrations grid** \u2014 connector catalog + connected list. Compose \`IntegrationCard\` + \`PlanBadge\` + \`ConnectionRowList\` (\`IntegrationsEmptyState\` when empty).
|
|
459
|
+
- **Resource gallery** \u2014 project / agent / dataset cards. Compose \`ResourceCard\` + \`StatusDot\` + \`Sparkline\`.
|
|
460
|
+
|
|
461
|
+
**Overlays & flows** (animate automatically)
|
|
462
|
+
- **Confirm & destructive** \u2014 confirm/cancel modal or destructive alert. Compose \`AppConfirmDialog\` (or \`AlertDialog\`) + \`Button\`; never hand-roll a \`Dialog\` for confirms.
|
|
463
|
+
- **Detail sheet** \u2014 slide-over edit panel without leaving the list. Compose \`Sheet\` + \`Field*\` + \`Button\` + \`Separator\`.
|
|
464
|
+
|
|
465
|
+
**States & auth**
|
|
466
|
+
- **Empty states** \u2014 no-data / no-results / first-run. Compose \`EmptyState\` + \`Card\` + \`Button\`.
|
|
467
|
+
- **Sign-in card** \u2014 centered auth entry. Compose \`Card\` + \`Input\` + \`Label\` + \`Button\`.
|
|
468
|
+
|
|
469
|
+
**Shells & theming**
|
|
470
|
+
- **Minimal shell** \u2014 \`AppShell\` + \`Page\` (no sidebar/chat).
|
|
471
|
+
- **Copilot overlay** \u2014 \`AppShell\` + floating \`AppChatPanel\`.
|
|
472
|
+
- **Theme presets** \u2014 \`ThemePresetGallery\` + \`applyTimbalTheme\` (never hand-author OKLCH).
|
|
252
473
|
|
|
253
474
|
### Typical compositions
|
|
254
475
|
|
|
@@ -259,6 +480,7 @@ Studio chrome (\`StudioSidebar\`, \`ModeToggle\`, \u2026) lives in \`@timbal-ai/
|
|
|
259
480
|
- **Integrations** \u2014 grid of \`IntegrationCard\`; \`ConnectionRowList\` for connected providers; \`IntegrationsEmptyState\` when empty.
|
|
260
481
|
- **Resource gallery** \u2014 grid of \`ResourceCard\`.
|
|
261
482
|
- **Copilot-assisted app** \u2014 \`AppCopilotProvider\` + \`AppShell\` with \`chat={<AppChatPanel workforceId="\u2026" />}\`.
|
|
483
|
+
- **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\`.
|
|
262
484
|
|
|
263
485
|
### Example imports
|
|
264
486
|
|
|
@@ -294,6 +516,211 @@ import {
|
|
|
294
516
|
- For rich in-chat widgets, use **artifacts** (\`ARTIFACT_AGENT_INSTRUCTIONS\`) \u2014 app kit is for the **host application shell**.
|
|
295
517
|
`.trim();
|
|
296
518
|
|
|
519
|
+
// src/design/ui-lint.ts
|
|
520
|
+
var PALETTE_GROUP = TAILWIND_PALETTE_COLORS.join("|");
|
|
521
|
+
var PREFIX_GROUP = COLOR_UTILITY_PREFIXES.join("|");
|
|
522
|
+
var RAW_COLOR_RE = new RegExp(
|
|
523
|
+
`(?:^|[\\s"'\`:])(?:[a-z-]+:)*(?:${PREFIX_GROUP})-(?:${PALETTE_GROUP})-\\d{2,3}(?:/\\d{1,3})?`,
|
|
524
|
+
"g"
|
|
525
|
+
);
|
|
526
|
+
var COLOR_LITERAL_RE = /#[0-9a-fA-F]{3,8}\b|\b(?:oklch|rgba?|hsla?)\s*\(/g;
|
|
527
|
+
var INLINE_STYLE_COLOR_RE = /style=\{\{[^}]*\b(?:color|background|backgroundColor|borderColor|fill|stroke)\b/;
|
|
528
|
+
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)/;
|
|
529
|
+
var GRADIENT_RE = /\bbg-(?:gradient|linear|radial|conic)-/;
|
|
530
|
+
var GRADIENT_DIRECTIONS = /* @__PURE__ */ new Set([
|
|
531
|
+
"t",
|
|
532
|
+
"tr",
|
|
533
|
+
"r",
|
|
534
|
+
"br",
|
|
535
|
+
"b",
|
|
536
|
+
"bl",
|
|
537
|
+
"l",
|
|
538
|
+
"tl"
|
|
539
|
+
]);
|
|
540
|
+
var ICON_IMPORT_RE = /from\s+["']lucide-react["']/;
|
|
541
|
+
var RAW_CONTROL_SURFACE_RE = /\bborder-input\b/;
|
|
542
|
+
var RESERVED_GRADIENT_SET = new Set(RESERVED_GRADIENT_TOKENS);
|
|
543
|
+
function stripVariants(util) {
|
|
544
|
+
return util.replace(/^(?:[a-z-]+:)*/, "");
|
|
545
|
+
}
|
|
546
|
+
function isCommentOrImport(line) {
|
|
547
|
+
const t = line.trim();
|
|
548
|
+
return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*") || t.startsWith("import ") || t.startsWith("export ");
|
|
549
|
+
}
|
|
550
|
+
function lintGeneratedUi(source, options = {}) {
|
|
551
|
+
const maxIcons = options.maxIconsPerView ?? SLOP_BUDGETS.maxIconsPerView;
|
|
552
|
+
const maxRowDividers = options.maxRowDividers ?? SLOP_BUDGETS.maxRowDividers;
|
|
553
|
+
const findings = [];
|
|
554
|
+
const lines = source.split("\n");
|
|
555
|
+
let usesLucide = false;
|
|
556
|
+
let iconUsageCount = 0;
|
|
557
|
+
let dividerRunCount = 0;
|
|
558
|
+
const lucideNames = /* @__PURE__ */ new Set();
|
|
559
|
+
for (let i = 0; i < lines.length; i++) {
|
|
560
|
+
const line = lines[i];
|
|
561
|
+
const lineNo = i + 1;
|
|
562
|
+
if (ICON_IMPORT_RE.test(line)) {
|
|
563
|
+
usesLucide = true;
|
|
564
|
+
const named = line.match(/\{([^}]*)\}/);
|
|
565
|
+
if (named) {
|
|
566
|
+
for (const raw of named[1].split(",")) {
|
|
567
|
+
const name = raw.trim().split(/\s+as\s+/)[0].trim();
|
|
568
|
+
if (name) lucideNames.add(name);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (isCommentOrImport(line)) continue;
|
|
574
|
+
const rawColors = line.match(RAW_COLOR_RE);
|
|
575
|
+
if (rawColors) {
|
|
576
|
+
for (const m of rawColors) {
|
|
577
|
+
findings.push({
|
|
578
|
+
rule: "raw-color",
|
|
579
|
+
severity: "error",
|
|
580
|
+
line: lineNo,
|
|
581
|
+
message: "Hardcoded palette color. Use a semantic token (text-primary, bg-muted, border-border, text-muted-foreground, \u2026) so dark mode and rebranding work.",
|
|
582
|
+
snippet: m.trim().replace(/^["'`:\s]+/, "")
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const literals = line.match(COLOR_LITERAL_RE);
|
|
587
|
+
if (literals) {
|
|
588
|
+
findings.push({
|
|
589
|
+
rule: "color-literal",
|
|
590
|
+
severity: "error",
|
|
591
|
+
line: lineNo,
|
|
592
|
+
message: "Hardcoded color literal. Colors must come from the theme generator (createTimbalTheme) and semantic tokens \u2014 never inline hex/oklch/rgb.",
|
|
593
|
+
snippet: line.trim().slice(0, 120)
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (INLINE_STYLE_COLOR_RE.test(line)) {
|
|
597
|
+
findings.push({
|
|
598
|
+
rule: "inline-style-color",
|
|
599
|
+
severity: "error",
|
|
600
|
+
line: lineNo,
|
|
601
|
+
message: "Inline style color. Move color to a semantic Tailwind token on className.",
|
|
602
|
+
snippet: line.trim().slice(0, 120)
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
if (RAW_CONTROL_SURFACE_RE.test(line)) {
|
|
606
|
+
findings.push({
|
|
607
|
+
rule: "raw-control-surface",
|
|
608
|
+
severity: "warn",
|
|
609
|
+
line: lineNo,
|
|
610
|
+
message: "Hand-rolled control surface (border-input). Use a kit control \u2014 SearchInput, Select, DropdownMenu, FieldInput, FieldSelect \u2014 so it matches every other control.",
|
|
611
|
+
snippet: line.trim().slice(0, 120)
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
if (BOLD_VALUE_RE.test(line)) {
|
|
615
|
+
findings.push({
|
|
616
|
+
rule: "bold-metric",
|
|
617
|
+
severity: "warn",
|
|
618
|
+
line: lineNo,
|
|
619
|
+
message: "Bold large value. House style: metric values use font-normal, not bold \u2014 bold giant numbers read as a template.",
|
|
620
|
+
snippet: line.trim().slice(0, 120)
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
if (GRADIENT_RE.test(line)) {
|
|
624
|
+
const fromTo = line.match(
|
|
625
|
+
new RegExp(`(?:from|via|to)-([a-z-]+)`, "g")
|
|
626
|
+
);
|
|
627
|
+
const colorStops = (fromTo ?? []).map((u) => stripVariants(u).replace(/^(?:from|via|to)-/, "")).filter((token) => !GRADIENT_DIRECTIONS.has(token));
|
|
628
|
+
const allReserved = colorStops.length > 0 && colorStops.every((token) => RESERVED_GRADIENT_SET.has(token));
|
|
629
|
+
if (!allReserved) {
|
|
630
|
+
findings.push({
|
|
631
|
+
rule: "data-gradient",
|
|
632
|
+
severity: "warn",
|
|
633
|
+
line: lineNo,
|
|
634
|
+
message: "Gradient outside chrome. Gradients are reserved for buttons / elevated / modal / playground \u2014 never a data card, tile, or table.",
|
|
635
|
+
snippet: line.trim().slice(0, 120)
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (/\b(?:border-t|border-b|divide-y)\b/.test(line)) {
|
|
640
|
+
dividerRunCount++;
|
|
641
|
+
if (dividerRunCount === maxRowDividers + 1) {
|
|
642
|
+
findings.push({
|
|
643
|
+
rule: "row-divider",
|
|
644
|
+
severity: "warn",
|
|
645
|
+
line: lineNo,
|
|
646
|
+
message: "Divider on every row. Prefer spacing (gap-*) or zebra striping over a rule under each list item.",
|
|
647
|
+
snippet: line.trim().slice(0, 120)
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
} else if (line.trim() !== "" && !line.includes("className")) {
|
|
651
|
+
if (!/^\s*[)>}/]/.test(line)) dividerRunCount = 0;
|
|
652
|
+
}
|
|
653
|
+
if (usesLucide && lucideNames.size > 0) {
|
|
654
|
+
for (const name of lucideNames) {
|
|
655
|
+
const usage = new RegExp(`<${name}\\b`, "g");
|
|
656
|
+
const hits = line.match(usage);
|
|
657
|
+
if (hits) iconUsageCount += hits.length;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (usesLucide && iconUsageCount > maxIcons) {
|
|
662
|
+
findings.push({
|
|
663
|
+
rule: "icon-spam",
|
|
664
|
+
severity: "warn",
|
|
665
|
+
line: 1,
|
|
666
|
+
message: `Too many icons (${iconUsageCount} > ${maxIcons}). Icons should mark actions/nav/status \u2014 not decorate every label, tile, and card.`,
|
|
667
|
+
snippet: `${iconUsageCount} lucide-react icon usages`
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
const effectiveErrors = findings.filter(
|
|
671
|
+
(f) => f.severity === "error" || options.strict && f.severity === "warn"
|
|
672
|
+
).length;
|
|
673
|
+
return {
|
|
674
|
+
findings,
|
|
675
|
+
errorCount: findings.filter((f) => f.severity === "error").length,
|
|
676
|
+
warnCount: findings.filter((f) => f.severity === "warn").length,
|
|
677
|
+
ok: effectiveErrors === 0
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function formatLintReport(findings) {
|
|
681
|
+
if (findings.length === 0) return "";
|
|
682
|
+
const lines = findings.slice().sort((a, b) => a.line - b.line).map((f) => {
|
|
683
|
+
const tag = f.severity === "error" ? "ERROR" : "warn ";
|
|
684
|
+
return ` ${tag} L${f.line} [${f.rule}] ${f.message}
|
|
685
|
+
\u2192 ${f.snippet}`;
|
|
686
|
+
});
|
|
687
|
+
const errs = findings.filter((f) => f.severity === "error").length;
|
|
688
|
+
const warns = findings.filter((f) => f.severity === "warn").length;
|
|
689
|
+
return `Anti-slop review: ${errs} error(s), ${warns} warning(s)
|
|
690
|
+
${lines.join("\n")}`;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/design/ui-review.ts
|
|
694
|
+
function reviewGeneratedUi(source, options = {}) {
|
|
695
|
+
const lint = lintGeneratedUi(source, options);
|
|
696
|
+
const report = formatLintReport(lint.findings);
|
|
697
|
+
if (lint.ok) {
|
|
698
|
+
return { lint, passed: true, report, revisionPrompt: null };
|
|
699
|
+
}
|
|
700
|
+
const revisionPrompt = [
|
|
701
|
+
"The generated UI failed the Timbal anti-slop review. Fix every issue below, then return the corrected code only.",
|
|
702
|
+
"",
|
|
703
|
+
report,
|
|
704
|
+
"",
|
|
705
|
+
"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."
|
|
706
|
+
].join("\n");
|
|
707
|
+
return { lint, passed: false, report, revisionPrompt };
|
|
708
|
+
}
|
|
709
|
+
var UI_REVIEW_AGENT_INSTRUCTIONS = `
|
|
710
|
+
## Self-review before returning UI (anti-slop)
|
|
711
|
+
|
|
712
|
+
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:
|
|
713
|
+
|
|
714
|
+
- **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 }}\`.
|
|
715
|
+
- **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.
|
|
716
|
+
- **Muted, sparse trends.** No colored up/down pill on every metric. Show a trend only when the change is the point.
|
|
717
|
+
- **Normal-weight values.** Metric numbers use \`font-normal\`, never \`font-bold\` at large sizes.
|
|
718
|
+
- **No card-in-card, no per-row dividers, no gradients on data surfaces.** Group with spacing/Sections; reserve gradients for chrome.
|
|
719
|
+
- **Compose from blocks.** Prefer \`MetricRow\` / \`MetricChartCard\` / \`DataTable\` / \`IntegrationCard\` over hand-assembled primitives.
|
|
720
|
+
|
|
721
|
+
If a check fails, fix it and re-read once more. Only return code that would pass clean.
|
|
722
|
+
`.trim();
|
|
723
|
+
|
|
297
724
|
// src/design/oklch.ts
|
|
298
725
|
var clamp = (n, min, max) => Math.min(max, Math.max(min, n));
|
|
299
726
|
var round = (n, digits) => {
|
|
@@ -431,6 +858,38 @@ function relativeLuminance(color) {
|
|
|
431
858
|
}
|
|
432
859
|
|
|
433
860
|
// src/design/theme.ts
|
|
861
|
+
var SHADOW_PRESETS = {
|
|
862
|
+
none: {
|
|
863
|
+
lightCard: "none",
|
|
864
|
+
lightElevated: "none",
|
|
865
|
+
darkCard: "none",
|
|
866
|
+
darkElevated: "none"
|
|
867
|
+
},
|
|
868
|
+
hairline: {
|
|
869
|
+
lightCard: "0 0 0 1px rgba(15, 23, 42, 0.06)",
|
|
870
|
+
lightElevated: "0 1px 2px rgba(15, 23, 42, 0.06)",
|
|
871
|
+
darkCard: "0 0 0 1px rgba(255, 255, 255, 0.06)",
|
|
872
|
+
darkElevated: "0 2px 8px rgba(0, 0, 0, 0.4)"
|
|
873
|
+
},
|
|
874
|
+
soft: {
|
|
875
|
+
lightCard: "0 1px 2px rgba(15, 23, 42, 0.04)",
|
|
876
|
+
lightElevated: "0 8px 30px rgba(15, 23, 42, 0.07)",
|
|
877
|
+
darkCard: "0 1px 2px rgba(0, 0, 0, 0.3)",
|
|
878
|
+
darkElevated: "0 10px 34px rgba(0, 0, 0, 0.45)"
|
|
879
|
+
},
|
|
880
|
+
medium: {
|
|
881
|
+
lightCard: "0 1px 2px -0.5px rgba(0, 0, 0, 0.05)",
|
|
882
|
+
lightElevated: "0 4px 24px rgba(0, 0, 0, 0.06)",
|
|
883
|
+
darkCard: "0 1px 3px rgba(0, 0, 0, 0.22)",
|
|
884
|
+
darkElevated: "0 4px 24px rgba(0, 0, 0, 0.35)"
|
|
885
|
+
},
|
|
886
|
+
strong: {
|
|
887
|
+
lightCard: "0 2px 6px rgba(15, 23, 42, 0.10)",
|
|
888
|
+
lightElevated: "0 16px 48px rgba(15, 23, 42, 0.16)",
|
|
889
|
+
darkCard: "0 2px 6px rgba(0, 0, 0, 0.4)",
|
|
890
|
+
darkElevated: "0 18px 50px rgba(0, 0, 0, 0.6)"
|
|
891
|
+
}
|
|
892
|
+
};
|
|
434
893
|
function primaryForMode(brand, mode) {
|
|
435
894
|
if (mode === "light") {
|
|
436
895
|
return { ...brand, l: Math.min(Math.max(brand.l, 0.42), 0.68) };
|
|
@@ -466,8 +925,26 @@ function createTimbalTheme(intent) {
|
|
|
466
925
|
const light = {};
|
|
467
926
|
const dark = {};
|
|
468
927
|
const root = {};
|
|
928
|
+
let fontFamily;
|
|
929
|
+
let fontImportUrl;
|
|
469
930
|
if (typeof intent.radius === "number") {
|
|
470
931
|
root["--radius"] = `${intent.radius}rem`;
|
|
932
|
+
root["--radius-2xl"] = `${Math.max(intent.radius + 0.25, 0)}rem`;
|
|
933
|
+
}
|
|
934
|
+
if (intent.typography) {
|
|
935
|
+
const { sans, display, mono, importUrl } = intent.typography;
|
|
936
|
+
root["--font-sans"] = sans;
|
|
937
|
+
if (display) root["--font-display"] = display;
|
|
938
|
+
if (mono) root["--font-mono"] = mono;
|
|
939
|
+
fontFamily = sans;
|
|
940
|
+
fontImportUrl = importUrl;
|
|
941
|
+
}
|
|
942
|
+
if (intent.shadow) {
|
|
943
|
+
const s = SHADOW_PRESETS[intent.shadow];
|
|
944
|
+
light["--shadow-card-value"] = s.lightCard;
|
|
945
|
+
light["--shadow-card-elevated-value"] = s.lightElevated;
|
|
946
|
+
dark["--shadow-card-value"] = s.darkCard;
|
|
947
|
+
dark["--shadow-card-elevated-value"] = s.darkElevated;
|
|
471
948
|
}
|
|
472
949
|
const primaryLight = primaryForMode(brand, "light");
|
|
473
950
|
const primaryDark = primaryForMode(brand, "dark");
|
|
@@ -565,7 +1042,7 @@ function createTimbalTheme(intent) {
|
|
|
565
1042
|
);
|
|
566
1043
|
}
|
|
567
1044
|
}
|
|
568
|
-
return { light, dark, root };
|
|
1045
|
+
return { light, dark, root, fontFamily, fontImportUrl };
|
|
569
1046
|
}
|
|
570
1047
|
function declarations(map, indent) {
|
|
571
1048
|
return Object.entries(map).map(([name, value]) => `${indent}${name}: ${value};`).join("\n");
|
|
@@ -588,6 +1065,11 @@ ${declarations(theme.dark, indent)}
|
|
|
588
1065
|
}`
|
|
589
1066
|
);
|
|
590
1067
|
}
|
|
1068
|
+
if (theme.fontFamily) {
|
|
1069
|
+
blocks.push(`${sel} {
|
|
1070
|
+
${indent}font-family: var(--font-sans);
|
|
1071
|
+
}`);
|
|
1072
|
+
}
|
|
591
1073
|
} else {
|
|
592
1074
|
if (Object.keys(lightVars).length) {
|
|
593
1075
|
blocks.push(`:root {
|
|
@@ -599,13 +1081,43 @@ ${declarations(lightVars, indent)}
|
|
|
599
1081
|
${declarations(theme.dark, indent)}
|
|
600
1082
|
}`);
|
|
601
1083
|
}
|
|
1084
|
+
if (theme.fontFamily) {
|
|
1085
|
+
blocks.push(`:root,
|
|
1086
|
+
body {
|
|
1087
|
+
${indent}font-family: var(--font-sans);
|
|
1088
|
+
}`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
const css = blocks.join("\n\n");
|
|
1092
|
+
if (options.includeFontImport && theme.fontImportUrl) {
|
|
1093
|
+
return `@import url("${theme.fontImportUrl}");
|
|
1094
|
+
|
|
1095
|
+
${css}`;
|
|
602
1096
|
}
|
|
603
|
-
return
|
|
1097
|
+
return css;
|
|
604
1098
|
}
|
|
605
1099
|
var RUNTIME_STYLE_ID = "timbal-theme-runtime";
|
|
1100
|
+
var FONT_LINK_ATTR = "data-timbal-theme-font";
|
|
1101
|
+
function ensureThemeFontLink(url) {
|
|
1102
|
+
if (typeof document === "undefined") return;
|
|
1103
|
+
const existing = document.head.querySelector(
|
|
1104
|
+
`link[${FONT_LINK_ATTR}]`
|
|
1105
|
+
);
|
|
1106
|
+
if (!url) {
|
|
1107
|
+
existing?.remove();
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
if (existing?.getAttribute("href") === url) return;
|
|
1111
|
+
const link = existing ?? document.createElement("link");
|
|
1112
|
+
link.rel = "stylesheet";
|
|
1113
|
+
link.href = url;
|
|
1114
|
+
link.setAttribute(FONT_LINK_ATTR, "");
|
|
1115
|
+
if (!existing) document.head.appendChild(link);
|
|
1116
|
+
}
|
|
606
1117
|
function applyTimbalTheme(theme) {
|
|
607
1118
|
if (typeof document === "undefined") return () => {
|
|
608
1119
|
};
|
|
1120
|
+
ensureThemeFontLink(theme.fontImportUrl);
|
|
609
1121
|
let el = document.getElementById(RUNTIME_STYLE_ID);
|
|
610
1122
|
if (!el) {
|
|
611
1123
|
el = document.createElement("style");
|
|
@@ -616,11 +1128,13 @@ function applyTimbalTheme(theme) {
|
|
|
616
1128
|
el.textContent = themeToCss(theme);
|
|
617
1129
|
return () => {
|
|
618
1130
|
el?.parentNode?.removeChild(el);
|
|
1131
|
+
ensureThemeFontLink(void 0);
|
|
619
1132
|
};
|
|
620
1133
|
}
|
|
621
1134
|
function clearTimbalTheme() {
|
|
622
1135
|
if (typeof document === "undefined") return;
|
|
623
1136
|
document.getElementById(RUNTIME_STYLE_ID)?.remove();
|
|
1137
|
+
ensureThemeFontLink(void 0);
|
|
624
1138
|
}
|
|
625
1139
|
function isDev() {
|
|
626
1140
|
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") {
|
|
@@ -671,48 +1185,122 @@ var STORAGE_KEYS = {
|
|
|
671
1185
|
|
|
672
1186
|
// src/design/theme-presets.ts
|
|
673
1187
|
var EMPTY_TOKENS = { light: {}, dark: {}, root: {} };
|
|
1188
|
+
var FONT_URL = {
|
|
1189
|
+
geist: "https://fonts.googleapis.com/css2?family=Geist:wght@400..600&display=swap",
|
|
1190
|
+
sora: "https://fonts.googleapis.com/css2?family=Sora:wght@400..600&display=swap",
|
|
1191
|
+
lexend: "https://fonts.googleapis.com/css2?family=Lexend:wght@400..600&display=swap",
|
|
1192
|
+
inter: "https://fonts.googleapis.com/css2?family=Inter:wght@400..600&display=swap",
|
|
1193
|
+
fraunces: "https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400..600&display=swap",
|
|
1194
|
+
jetbrains: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400..600&display=swap"
|
|
1195
|
+
};
|
|
1196
|
+
var STACK = {
|
|
1197
|
+
geist: '"Geist", ui-sans-serif, system-ui, sans-serif',
|
|
1198
|
+
sora: '"Sora", ui-sans-serif, system-ui, sans-serif',
|
|
1199
|
+
lexend: '"Lexend", ui-sans-serif, system-ui, sans-serif',
|
|
1200
|
+
inter: '"Inter", ui-sans-serif, system-ui, sans-serif',
|
|
1201
|
+
fraunces: '"Fraunces", ui-serif, Georgia, "Times New Roman", serif',
|
|
1202
|
+
jetbrains: '"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace'
|
|
1203
|
+
};
|
|
674
1204
|
var TIMBAL_THEME_PRESETS = [
|
|
675
1205
|
{
|
|
676
1206
|
id: "platform",
|
|
677
1207
|
label: "Platform",
|
|
678
|
-
description: "Shipped neutral monochrome \u2014 the Timbal Platform default. Calm, brand-agnostic.",
|
|
1208
|
+
description: "Shipped neutral monochrome \u2014 the Timbal Platform default. Calm, brand-agnostic, system font.",
|
|
679
1209
|
swatch: "oklch(0.205 0 0)",
|
|
1210
|
+
font: null,
|
|
680
1211
|
tokens: EMPTY_TOKENS
|
|
681
1212
|
},
|
|
682
1213
|
{
|
|
683
1214
|
id: "indigo",
|
|
684
1215
|
label: "Indigo",
|
|
685
|
-
description: "Cool, trustworthy blue-violet \u2014
|
|
1216
|
+
description: "Cool, trustworthy blue-violet, Geist type, generous radius, soft shadows \u2014 analytics & ops dashboards.",
|
|
686
1217
|
swatch: "#4f46e5",
|
|
687
|
-
|
|
1218
|
+
font: "Geist",
|
|
1219
|
+
tokens: createTimbalTheme({
|
|
1220
|
+
brand: "#4f46e5",
|
|
1221
|
+
radius: 0.875,
|
|
1222
|
+
shadow: "soft",
|
|
1223
|
+
typography: { sans: STACK.geist, importUrl: FONT_URL.geist }
|
|
1224
|
+
})
|
|
688
1225
|
},
|
|
689
1226
|
{
|
|
690
1227
|
id: "violet",
|
|
691
1228
|
label: "Violet",
|
|
692
|
-
description: "Vivid purple \u2014 expressive
|
|
1229
|
+
description: "Vivid purple, Sora type, rounded, soft shadows \u2014 expressive product / marketing surfaces.",
|
|
693
1230
|
swatch: "#7c3aed",
|
|
694
|
-
|
|
1231
|
+
font: "Sora",
|
|
1232
|
+
tokens: createTimbalTheme({
|
|
1233
|
+
brand: "#7c3aed",
|
|
1234
|
+
radius: 1,
|
|
1235
|
+
shadow: "soft",
|
|
1236
|
+
typography: { sans: STACK.sora, importUrl: FONT_URL.sora }
|
|
1237
|
+
})
|
|
695
1238
|
},
|
|
696
1239
|
{
|
|
697
1240
|
id: "forest",
|
|
698
1241
|
label: "Forest",
|
|
699
|
-
description: "Grounded green \u2014 finance, sustainability, status-positive apps.",
|
|
1242
|
+
description: "Grounded green, Lexend type, compact radius \u2014 finance, sustainability, status-positive apps.",
|
|
700
1243
|
swatch: "#16a34a",
|
|
701
|
-
|
|
1244
|
+
font: "Lexend",
|
|
1245
|
+
tokens: createTimbalTheme({
|
|
1246
|
+
brand: "#16a34a",
|
|
1247
|
+
radius: 0.625,
|
|
1248
|
+
shadow: "soft",
|
|
1249
|
+
typography: { sans: STACK.lexend, importUrl: FONT_URL.lexend }
|
|
1250
|
+
})
|
|
702
1251
|
},
|
|
703
1252
|
{
|
|
704
1253
|
id: "warm",
|
|
705
1254
|
label: "Warm",
|
|
706
|
-
description: "Energetic orange \u2014 consumer, creative, high-engagement tools.",
|
|
1255
|
+
description: "Energetic orange, Lexend type, friendly radius \u2014 consumer, creative, high-engagement tools.",
|
|
707
1256
|
swatch: "#ea580c",
|
|
708
|
-
|
|
1257
|
+
font: "Lexend",
|
|
1258
|
+
tokens: createTimbalTheme({
|
|
1259
|
+
brand: "#ea580c",
|
|
1260
|
+
radius: 0.875,
|
|
1261
|
+
shadow: "soft",
|
|
1262
|
+
typography: { sans: STACK.lexend, importUrl: FONT_URL.lexend }
|
|
1263
|
+
})
|
|
709
1264
|
},
|
|
710
1265
|
{
|
|
711
1266
|
id: "slate",
|
|
712
1267
|
label: "Slate",
|
|
713
|
-
description: "Muted
|
|
1268
|
+
description: "Muted enterprise gray-blue, Inter type, tight radius, hairline shadows, tinted neutrals.",
|
|
714
1269
|
swatch: "#475569",
|
|
715
|
-
|
|
1270
|
+
font: "Inter",
|
|
1271
|
+
tokens: createTimbalTheme({
|
|
1272
|
+
brand: "#475569",
|
|
1273
|
+
radius: 0.5,
|
|
1274
|
+
shadow: "hairline",
|
|
1275
|
+
tintNeutrals: true,
|
|
1276
|
+
typography: { sans: STACK.inter, importUrl: FONT_URL.inter }
|
|
1277
|
+
})
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
id: "folio",
|
|
1281
|
+
label: "Folio",
|
|
1282
|
+
description: "Editorial serif (Fraunces), near-sharp corners, hairline shadows \u2014 content / docs / reports.",
|
|
1283
|
+
swatch: "#9a3412",
|
|
1284
|
+
font: "Fraunces",
|
|
1285
|
+
tokens: createTimbalTheme({
|
|
1286
|
+
brand: "#9a3412",
|
|
1287
|
+
radius: 0.25,
|
|
1288
|
+
shadow: "hairline",
|
|
1289
|
+
typography: { sans: STACK.fraunces, importUrl: FONT_URL.fraunces }
|
|
1290
|
+
})
|
|
1291
|
+
},
|
|
1292
|
+
{
|
|
1293
|
+
id: "carbon",
|
|
1294
|
+
label: "Carbon",
|
|
1295
|
+
description: "Terminal monospace (JetBrains Mono), crisp corners, green accent \u2014 developer / infra tools.",
|
|
1296
|
+
swatch: "#15803d",
|
|
1297
|
+
font: "JetBrains Mono",
|
|
1298
|
+
tokens: createTimbalTheme({
|
|
1299
|
+
brand: "#15803d",
|
|
1300
|
+
radius: 0.375,
|
|
1301
|
+
shadow: "hairline",
|
|
1302
|
+
typography: { sans: STACK.jetbrains, importUrl: FONT_URL.jetbrains }
|
|
1303
|
+
})
|
|
716
1304
|
}
|
|
717
1305
|
];
|
|
718
1306
|
var PRESET_BY_ID = new Map(
|
|
@@ -753,18 +1341,29 @@ The package ships a complete light + dark token system (\`styles.css\`). Compone
|
|
|
753
1341
|
|
|
754
1342
|
**Never write \`oklch(...)\` / hex literals or hand-author paired \`:root\` + \`.dark\` blocks.** Express intent and let the package derive a complete, contrast-correct, paired palette.
|
|
755
1343
|
|
|
756
|
-
###
|
|
1344
|
+
### Generate a full personality (color + roundness + fonts + shadows)
|
|
757
1345
|
|
|
758
1346
|
\`\`\`ts
|
|
759
1347
|
import { createTimbalTheme, themeToCss } from "@timbal-ai/timbal-react";
|
|
760
1348
|
|
|
761
|
-
const theme = createTimbalTheme({
|
|
762
|
-
|
|
763
|
-
|
|
1349
|
+
const theme = createTimbalTheme({
|
|
1350
|
+
brand: "#4f46e5",
|
|
1351
|
+
radius: 0.875, // corner roundness in rem (sets --radius + --radius-2xl)
|
|
1352
|
+
shadow: "soft", // "none" | "hairline" | "soft" | "medium" | "strong"
|
|
1353
|
+
tintNeutrals: false, // tint background/border toward the brand hue
|
|
1354
|
+
accent: "#10b981", // optional secondary accent
|
|
1355
|
+
typography: { // optional \u2014 re-skins every component's font
|
|
1356
|
+
sans: '"Geist", ui-sans-serif, system-ui, sans-serif',
|
|
1357
|
+
importUrl: "https://fonts.googleapis.com/css2?family=Geist:wght@400..600&display=swap",
|
|
1358
|
+
// display?, mono? also supported
|
|
1359
|
+
},
|
|
1360
|
+
});
|
|
1361
|
+
const css = themeToCss(theme); // paired light + dark, guaranteed in sync
|
|
764
1362
|
\`\`\`
|
|
765
1363
|
|
|
766
|
-
-
|
|
767
|
-
-
|
|
1364
|
+
- \`createTimbalTheme\` derives \`--primary\`, its foreground, ring, the full button gradient, and a soft playground tint from \`brand\`. \`radius\` sets roundness, \`shadow\` sets card depth, \`typography\` sets fonts. You only supply intent \u2014 never raw OKLCH.
|
|
1365
|
+
- For a real company, look up the actual brand hex first (brandfetch / "<company> brand color hex").
|
|
1366
|
+
- **Web fonts must be loaded.** \`applyTimbalTheme\` / \`TimbalThemeStyle\` inject the \`<link>\` for \`typography.importUrl\` automatically. For build-time \`themeToCss\`, add the \`<link rel="stylesheet" href="\u2026">\` to your \`index.html\` yourself (or pass \`themeToCss(theme, { includeFontImport: true })\` when the result is a standalone stylesheet).
|
|
768
1367
|
|
|
769
1368
|
### Apply a theme
|
|
770
1369
|
|
|
@@ -781,14 +1380,18 @@ import { TIMBAL_THEME_PRESETS, applyThemePreset } from "@timbal-ai/timbal-react"
|
|
|
781
1380
|
// TIMBAL_THEME_PRESETS: { id, label, description, swatch, tokens }[]
|
|
782
1381
|
\`\`\`
|
|
783
1382
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
|
787
|
-
|
|
788
|
-
| \`
|
|
789
|
-
| \`
|
|
790
|
-
| \`
|
|
791
|
-
| \`
|
|
1383
|
+
Each preset is a **full personality** (color + radius + shadows + font), not just a color:
|
|
1384
|
+
|
|
1385
|
+
| Preset id | Personality |
|
|
1386
|
+
|-----------|-------------|
|
|
1387
|
+
| \`platform\` | Neutral monochrome, system font (the default \u2014 no brand) |
|
|
1388
|
+
| \`indigo\` | Blue-violet, Geist, generous radius, soft shadows \u2014 analytics / ops |
|
|
1389
|
+
| \`violet\` | Purple, Sora, rounded \u2014 product / marketing |
|
|
1390
|
+
| \`forest\` | Green, Lexend, compact \u2014 finance / sustainability |
|
|
1391
|
+
| \`warm\` | Orange, Lexend, friendly \u2014 consumer / creative |
|
|
1392
|
+
| \`slate\` | Enterprise gray-blue, Inter, tight radius, hairline shadows |
|
|
1393
|
+
| \`folio\` | Editorial serif (Fraunces), near-sharp corners \u2014 content / docs |
|
|
1394
|
+
| \`carbon\` | Terminal monospace (JetBrains Mono), green accent \u2014 dev / infra |
|
|
792
1395
|
|
|
793
1396
|
- To present options visually, render \`<ThemePresetGallery value={id} onSelect={setId} />\` \u2014 each swatch previews real components (Button + metric tile) scoped via \`data-timbal-theme\`, so the live app doesn't change until the user picks.
|
|
794
1397
|
- On selection, call \`applyThemePreset(id)\` (persists to \`localStorage\` and restores on reload).
|
|
@@ -812,14 +1415,17 @@ var TimbalThemeStyle = ({
|
|
|
812
1415
|
if (!tokens) return null;
|
|
813
1416
|
const css = themeToCss(tokens, scope ? { scope } : void 0);
|
|
814
1417
|
if (!css) return null;
|
|
815
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.
|
|
816
|
-
"
|
|
817
|
-
|
|
818
|
-
"
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1418
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1419
|
+
tokens.fontImportUrl ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("link", { rel: "stylesheet", href: tokens.fontImportUrl }) : null,
|
|
1420
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1421
|
+
"style",
|
|
1422
|
+
{
|
|
1423
|
+
"data-timbal-theme-style": scope ?? "root",
|
|
1424
|
+
nonce,
|
|
1425
|
+
dangerouslySetInnerHTML: { __html: css }
|
|
1426
|
+
}
|
|
1427
|
+
)
|
|
1428
|
+
] });
|
|
823
1429
|
};
|
|
824
1430
|
|
|
825
1431
|
// src/utils.ts
|
|
@@ -1403,6 +2009,10 @@ var ThemePresetGallery = ({
|
|
|
1403
2009
|
selected ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-xs font-medium text-primary", children: "Selected" }) : null
|
|
1404
2010
|
] }),
|
|
1405
2011
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-xs leading-snug text-muted-foreground", children: preset.description }),
|
|
2012
|
+
preset.font ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: [
|
|
2013
|
+
"Aa \xB7 ",
|
|
2014
|
+
preset.font
|
|
2015
|
+
] }) : null,
|
|
1406
2016
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-col gap-2 rounded-lg border border-border bg-background p-2", children: [
|
|
1407
2017
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
1408
2018
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Button, { size: "xs", className: "pointer-events-none", children: "Primary" }),
|
|
@@ -1431,6 +2041,38 @@ var ThemePresetGallery = ({
|
|
|
1431
2041
|
var import_react3 = require("motion/react");
|
|
1432
2042
|
var import_react4 = require("react");
|
|
1433
2043
|
|
|
2044
|
+
// src/design/control-surface.ts
|
|
2045
|
+
var CONTROL_SIZE = {
|
|
2046
|
+
sm: "h-9 px-3",
|
|
2047
|
+
default: "h-10 px-3"
|
|
2048
|
+
};
|
|
2049
|
+
var CONTROL_SHAPE = {
|
|
2050
|
+
field: "rounded-lg",
|
|
2051
|
+
pill: "rounded-full"
|
|
2052
|
+
};
|
|
2053
|
+
var controlSurfaceClass = cn(
|
|
2054
|
+
TIMBAL_V2_SECONDARY_CHROME,
|
|
2055
|
+
"text-sm text-foreground outline-none",
|
|
2056
|
+
"placeholder:text-muted-foreground/70",
|
|
2057
|
+
"focus-visible:ring-2 focus-visible:ring-foreground/10",
|
|
2058
|
+
"focus-within:ring-2 focus-within:ring-foreground/10",
|
|
2059
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
2060
|
+
"data-[placeholder]:text-muted-foreground"
|
|
2061
|
+
);
|
|
2062
|
+
function controlClass(options = {}, className) {
|
|
2063
|
+
const { size = "default", shape = "field" } = options;
|
|
2064
|
+
return cn(controlSurfaceClass, CONTROL_SIZE[size], CONTROL_SHAPE[shape], className);
|
|
2065
|
+
}
|
|
2066
|
+
var overlayAnimationClass = "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2";
|
|
2067
|
+
var overlaySurfaceClass = cn(
|
|
2068
|
+
"z-[80] border border-border bg-popover text-popover-foreground shadow-card",
|
|
2069
|
+
overlayAnimationClass
|
|
2070
|
+
);
|
|
2071
|
+
var overlayListPanelClass = cn(
|
|
2072
|
+
overlaySurfaceClass,
|
|
2073
|
+
"overflow-hidden rounded-lg p-0 outline-hidden"
|
|
2074
|
+
);
|
|
2075
|
+
|
|
1434
2076
|
// src/design/app-classes.ts
|
|
1435
2077
|
var appPageColumnClass = "mx-auto w-full max-w-6xl px-4 md:px-6";
|
|
1436
2078
|
var appShellTopbarInsetClass = "w-full px-4 md:px-6";
|
|
@@ -1464,18 +2106,13 @@ var appFilterBarClass = cn(
|
|
|
1464
2106
|
"flex flex-wrap items-center gap-2",
|
|
1465
2107
|
studioTopbarPillHeightClass
|
|
1466
2108
|
);
|
|
1467
|
-
var appSearchInputClass =
|
|
2109
|
+
var appSearchInputClass = controlClass({}, "inline-flex items-center gap-2");
|
|
1468
2110
|
var appBreadcrumbsClass = "flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground";
|
|
1469
2111
|
var appBreadcrumbLinkClass = "transition-colors hover:text-foreground";
|
|
1470
2112
|
var appFieldClass = "flex flex-col gap-1.5";
|
|
1471
2113
|
var appFieldLabelClass = "text-sm font-medium text-foreground";
|
|
1472
2114
|
var appFieldHintClass = "text-xs text-muted-foreground";
|
|
1473
|
-
var appInputClass =
|
|
1474
|
-
studioSecondaryChromeClass,
|
|
1475
|
-
"h-10 w-full rounded-lg px-3 text-sm text-foreground outline-none",
|
|
1476
|
-
"placeholder:text-muted-foreground/70",
|
|
1477
|
-
"focus-visible:ring-2 focus-visible:ring-foreground/10"
|
|
1478
|
-
);
|
|
2115
|
+
var appInputClass = controlClass({}, "w-full");
|
|
1479
2116
|
var appEmptyStateClass = cn(
|
|
1480
2117
|
appSurfaceCardClass,
|
|
1481
2118
|
"flex flex-col items-center justify-center gap-2 py-12 text-center"
|
|
@@ -7388,6 +8025,7 @@ function TimbalChat({
|
|
|
7388
8025
|
Breadcrumbs,
|
|
7389
8026
|
Button,
|
|
7390
8027
|
CHART_PALETTE,
|
|
8028
|
+
COLOR_UTILITY_PREFIXES,
|
|
7391
8029
|
ChartArtifactView,
|
|
7392
8030
|
ChartPanel,
|
|
7393
8031
|
ConnectionRow,
|
|
@@ -7407,6 +8045,7 @@ function TimbalChat({
|
|
|
7407
8045
|
FilterBar,
|
|
7408
8046
|
FloatingUnsavedChangesBar,
|
|
7409
8047
|
FormSection,
|
|
8048
|
+
HOUSE_RULES,
|
|
7410
8049
|
INTEGRATION_CATALOG_CARD_HEIGHT_CLASS,
|
|
7411
8050
|
InfoCard,
|
|
7412
8051
|
IntegrationCard,
|
|
@@ -7418,7 +8057,10 @@ function TimbalChat({
|
|
|
7418
8057
|
Page,
|
|
7419
8058
|
PageHeader,
|
|
7420
8059
|
PlanBadge,
|
|
8060
|
+
RESERVED_GRADIENT_TOKENS,
|
|
7421
8061
|
ResourceCard,
|
|
8062
|
+
SEMANTIC_COLOR_TOKENS,
|
|
8063
|
+
SLOP_BUDGETS,
|
|
7422
8064
|
SearchInput,
|
|
7423
8065
|
Section,
|
|
7424
8066
|
SettingsSection,
|
|
@@ -7429,11 +8071,13 @@ function TimbalChat({
|
|
|
7429
8071
|
StatusDot,
|
|
7430
8072
|
SubNav,
|
|
7431
8073
|
SurfaceCard,
|
|
8074
|
+
TAILWIND_PALETTE_COLORS,
|
|
7432
8075
|
THEME_AGENT_INSTRUCTIONS,
|
|
7433
8076
|
TIMBAL_THEME_PRESETS,
|
|
7434
8077
|
ThemePresetGallery,
|
|
7435
8078
|
TimbalChat,
|
|
7436
8079
|
TimbalThemeStyle,
|
|
8080
|
+
UI_REVIEW_AGENT_INSTRUCTIONS,
|
|
7437
8081
|
appFilterBarClass,
|
|
7438
8082
|
appPageColumnClass,
|
|
7439
8083
|
appSearchInputClass,
|
|
@@ -7446,8 +8090,12 @@ function TimbalChat({
|
|
|
7446
8090
|
clearTimbalTheme,
|
|
7447
8091
|
connectionRowListClass,
|
|
7448
8092
|
createTimbalTheme,
|
|
8093
|
+
ensureThemeFontLink,
|
|
8094
|
+
formatLintReport,
|
|
7449
8095
|
getStoredThemePreset,
|
|
7450
8096
|
getThemePreset,
|
|
8097
|
+
lintGeneratedUi,
|
|
8098
|
+
reviewGeneratedUi,
|
|
7451
8099
|
themeToCss,
|
|
7452
8100
|
useAppCopilotContext,
|
|
7453
8101
|
useAppShellChat
|