@keboola/design 3.2.0 → 3.2.3
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/AGENTS.md +427 -0
- package/COMPONENT-API.md +377 -0
- package/README.md +12 -3
- package/dist/index.cjs +25 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -2
- package/dist/index.d.ts +26 -2
- package/dist/index.js +25 -2
- package/dist/index.js.map +1 -1
- package/dist/styles.css +3 -1
- package/dist/testing.cjs +23 -0
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +23 -0
- package/dist/testing.js.map +1 -1
- package/package.json +10 -6
- package/src/styles.css +22 -1
package/AGENTS.md
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# @keboola/design
|
|
2
|
+
|
|
3
|
+
Consumer usage contract for the Keboola design system — shadcn-aligned React primitives (Radix underneath), Keboola-branded blocks, Lucide icons, and TanStack-Form bindings.
|
|
4
|
+
|
|
5
|
+
This file ships **inside the npm tarball** and is version-pinned to the package you installed — it is the source of truth for **which** component to use and how to wire theming. For the exact **prop signature** of the most-used components, see the generated [`COMPONENT-API.md`](./COMPONENT-API.md) (also shipped in the tarball).
|
|
6
|
+
|
|
7
|
+
Working **on** the package (commands, layer boundary, token surface, the FA→Lucide table, enforcement)? Those live in `MAINTAINING.md` in the repo — internal, not shipped with this package.
|
|
8
|
+
|
|
9
|
+
## Import surface
|
|
10
|
+
|
|
11
|
+
- **Always** import from the package root: `import { Button } from '@keboola/design'`.
|
|
12
|
+
- App entry points **must** import styles. Two paths, pick by build setup:
|
|
13
|
+
- `import '@keboola/design/styles'` — Tailwind-extending consumers. Your PostCSS+Tailwind processes `@tailwind` directives plus design's `dist/` in your content scan. Avoids duplicate preflights/utilities. **Default for any Keboola app.**
|
|
14
|
+
- `import '@keboola/design/styles/compiled'` — drop-in pre-compiled CSS (~176 KB minified). No Tailwind setup required. Use only when you genuinely cannot run Tailwind.
|
|
15
|
+
- Icons: `import { ChevronDown } from '@keboola/design/icons'`.
|
|
16
|
+
|
|
17
|
+
## Decision tree
|
|
18
|
+
|
|
19
|
+
This file answers **which** component to use; [`COMPONENT-API.md`](./COMPONENT-API.md) carries the exact prop signatures.
|
|
20
|
+
|
|
21
|
+
### Buttons
|
|
22
|
+
|
|
23
|
+
| Use | When |
|
|
24
|
+
| -------------------- | ------------------------------------------------------------------------------------ |
|
|
25
|
+
| `Button` | Primary, secondary, danger, outline, or invisible actions with optional label + icon |
|
|
26
|
+
| `Button size="icon"` | Icon-only button — no separate `IconButton` |
|
|
27
|
+
| `ButtonInline` | Inline text-link-style action inside body copy |
|
|
28
|
+
| `SplitButton` | Primary action + secondary dropdown of related actions |
|
|
29
|
+
| `Link` | Navigational anchor that opens a URL (defaults `target="_blank"`) |
|
|
30
|
+
| `ButtonGroup` | Two or more related buttons rendered as a segmented row |
|
|
31
|
+
|
|
32
|
+
**Never** use raw `<button>` or `<a>` — always through `Button`, `ButtonInline`, or `Link`.
|
|
33
|
+
|
|
34
|
+
### Overlays
|
|
35
|
+
|
|
36
|
+
| Use | When |
|
|
37
|
+
| -------------- | ------------------------------------------------------------------------ |
|
|
38
|
+
| `Modal` | Blocking dialog (confirmation, form, detail view) |
|
|
39
|
+
| `Sheet` | Slide-in panel for contextual editing without leaving the page |
|
|
40
|
+
| `Popconfirm` | Lightweight inline confirmation anchored to the trigger (delete actions) |
|
|
41
|
+
| `Popover` | Non-modal floating panel for arbitrary rich content |
|
|
42
|
+
| `HoverCard` | Read-only preview panel on hover; no interactive controls |
|
|
43
|
+
| `DropdownMenu` | Contextual list of actions or navigation items on click |
|
|
44
|
+
|
|
45
|
+
**Never** use `Popover` for destructive confirmations (use `Popconfirm`) or hover-only previews (use `HoverCard`).
|
|
46
|
+
|
|
47
|
+
### Inputs
|
|
48
|
+
|
|
49
|
+
| Use | When |
|
|
50
|
+
| ----------------------- | ------------------------------------------------------------------ |
|
|
51
|
+
| `TextInput` | Bare text input with no surrounding label / help — rare on its own |
|
|
52
|
+
| `FormGroup.TextInput` | Text field with help text + error state from `FormGroup state` |
|
|
53
|
+
| `useAppForm` | Full form layouts with validation (see Forms below) |
|
|
54
|
+
| `useAppForm` connectors | Picking between `TextField` / `NumberField` / `SelectField` / … |
|
|
55
|
+
| `InputOtp` | Fixed-length verification code (MFA, email confirm, sudo step-up) |
|
|
56
|
+
| `FileUpload` | Argo-only drag-and-drop file picker; caller owns the file array |
|
|
57
|
+
|
|
58
|
+
**Never** build custom label + input combinations — compose `Label` + `FormGroup.TextInput` + `FormGroup.Help` inside `FormGroup`.
|
|
59
|
+
|
|
60
|
+
### Status surfaces
|
|
61
|
+
|
|
62
|
+
| Use | When |
|
|
63
|
+
| ----------------------------- | ----------------------------------------------------- |
|
|
64
|
+
| `toast()` / `useNotification` | Transient auto-dismissing feedback for background ops |
|
|
65
|
+
| `Toaster` | Mount once at app root |
|
|
66
|
+
| `Alert` | Persistent contextual message in page layout |
|
|
67
|
+
| `FormGroup state="error"` | Field-level validation errors inline below input |
|
|
68
|
+
|
|
69
|
+
**Never** use `Alert` for ephemeral success (use `toast()`) or `toast()` for blocking errors (use `Alert`).
|
|
70
|
+
|
|
71
|
+
### Loading
|
|
72
|
+
|
|
73
|
+
| Use | When |
|
|
74
|
+
| ------------------ | ----------------------------------------------------------------------------------------------- |
|
|
75
|
+
| `Skeleton` | Layout-aware placeholder shown in the slot the real content will occupy (initial load, refetch) |
|
|
76
|
+
| `SkeletonList` | Pre-composed list shell (icon + row, repeated) for Table / Card-list pages |
|
|
77
|
+
| `Button isLoading` | In-flight action on a known button — swaps the icon for a spinner and disables the button |
|
|
78
|
+
| `DotsLoader` | Indeterminate background work without a layout slot (small inline progress signal) |
|
|
79
|
+
|
|
80
|
+
**Never** build a new "Loader" / "BoxLoader" / "ActionLoader" wrapper component. Compose `Skeleton` directly into the slot the real content will occupy.
|
|
81
|
+
|
|
82
|
+
### Tooltips
|
|
83
|
+
|
|
84
|
+
| Use | When |
|
|
85
|
+
| ----------- | ----------------------------------------------------- |
|
|
86
|
+
| `Tooltip` | Short plain-text label (icon-button aria description) |
|
|
87
|
+
| `HoverCard` | Rich preview content (multi-line, structured) |
|
|
88
|
+
|
|
89
|
+
**Never** put interactive elements inside a `Tooltip` — use `HoverCard` or `Popover`.
|
|
90
|
+
|
|
91
|
+
### Data display
|
|
92
|
+
|
|
93
|
+
| Use | When |
|
|
94
|
+
| ----------- | ------------------------------------------------------------------ |
|
|
95
|
+
| `Table` | Tabular data with columns, sortable headers, row actions |
|
|
96
|
+
| `Card` list | Small heterogeneous collections needing their own visual container |
|
|
97
|
+
|
|
98
|
+
**Never** reach for raw `<ul>/<li>` for entities — use `Table` for columnar data.
|
|
99
|
+
|
|
100
|
+
### Layout
|
|
101
|
+
|
|
102
|
+
**No** `Stack` / `Flex` / `Box` component exists, by design — shadcn doesn't ship them, and adding wrappers creates a third way to do layout. Use Tailwind utility classes directly.
|
|
103
|
+
|
|
104
|
+
| Use | When |
|
|
105
|
+
| ---------------- | ------------------------------------------------------------------------ |
|
|
106
|
+
| Tailwind classes | All ad-hoc layout: `tw-flex tw-flex-col tw-gap-4`, `tw-grid …`, `tw-p-6` |
|
|
107
|
+
| `AppShell` | App-root shell that follows the active brand (sidebar vs top-nav) |
|
|
108
|
+
| `PageLayout` | Top-level page shape: header, tabs, toolbar, content, footer |
|
|
109
|
+
| `PageToolbar` | Index-page toolbar: search + filters / sort / actions / view + chips row |
|
|
110
|
+
| `Card` | Semantic content surface with header / content / footer slots |
|
|
111
|
+
| `Sidebar` | App-shell left navigation rail (host inside `AppShell.Sidebar`) |
|
|
112
|
+
|
|
113
|
+
Vertical rhythm: prefer `tw-gap-*` on a flex column parent over `tw-mt-*` on children — avoids margin-collapse. Page-level max width: use `PageLayout`'s `maxWidth` prop (`narrow` / `default` / `wide` / `full`), not `tw-max-w-[…]` literals.
|
|
114
|
+
|
|
115
|
+
### Navigation / flow
|
|
116
|
+
|
|
117
|
+
| Use | When |
|
|
118
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
119
|
+
| `Stepper` | Linear progress through an ordered, finite sequence (multi-step form / wizard / pipeline). Argo-only. Set `onStepClick` for navigable flows |
|
|
120
|
+
| `Tabs` | Switching between sibling views of the same context |
|
|
121
|
+
| `Timeline` | Unordered chronological log of events |
|
|
122
|
+
| `Progress` | Single percentage value of an in-flight task |
|
|
123
|
+
|
|
124
|
+
**Never** use `Stepper` for branching navigation (use `Tabs`) or an unordered status list (use `Timeline`).
|
|
125
|
+
|
|
126
|
+
### Separators
|
|
127
|
+
|
|
128
|
+
| Use | When |
|
|
129
|
+
| -------------------------- | --------------------------------------------------------------------- |
|
|
130
|
+
| `Separator` | Plain horizontal or vertical line between sections |
|
|
131
|
+
| `<Separator label="OR" />` | Centered text divider between two options (sign-in flows, form forks) |
|
|
132
|
+
|
|
133
|
+
The `label` prop implies `orientation="horizontal"` — labels are not rendered vertically.
|
|
134
|
+
|
|
135
|
+
### Date/time, color, multi-select, code
|
|
136
|
+
|
|
137
|
+
| Use | When |
|
|
138
|
+
| ----------------- | ------------------------------------------------------------- |
|
|
139
|
+
| `DatePicker` | Single calendar date |
|
|
140
|
+
| `DateRangePicker` | Selecting a date range |
|
|
141
|
+
| `TimePicker` | Time-of-day |
|
|
142
|
+
| `Calendar` | Raw calendar grid (embed in custom range pickers) |
|
|
143
|
+
| `ColorInput` | Design-token-driven brand color picker |
|
|
144
|
+
| `Combobox` | Single selection from a searchable list |
|
|
145
|
+
| `MultiCombobox` | Multiple selections from a searchable list |
|
|
146
|
+
| `Select` | Single selection from a short enum (≤ ~10 options, no search) |
|
|
147
|
+
| `Tagger` | Free-form chip/tag entry |
|
|
148
|
+
| `CodeEditor` | Generic code editing (JSON, YAML, shell) |
|
|
149
|
+
| `SqlEditor` | SQL editing (syntax + autocomplete) |
|
|
150
|
+
|
|
151
|
+
`Select` is wrong above ~10 items — use `Combobox`. `Combobox` is wrong for free-form values — use `Tagger`. `CodeEditor`/`SqlEditor` load the full CodeMirror bundle; reserve for real code editing.
|
|
152
|
+
|
|
153
|
+
## Composition recipes
|
|
154
|
+
|
|
155
|
+
The platform ships **no** `EntityList` / `ModalForm` / `ConfirmDialog` wrapper — these are compositions, not components. Copy the recipe; see [`COMPONENT-API.md`](./COMPONENT-API.md) for the exact props.
|
|
156
|
+
|
|
157
|
+
**Modal form** — `Modal` + `useAppForm`. The submit button lives in the footer and targets the form by `id`:
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
import { Button, Modal, useAppForm } from '@keboola/design';
|
|
161
|
+
|
|
162
|
+
const FORM_ID = 'new-bucket-form';
|
|
163
|
+
|
|
164
|
+
const form = useAppForm({
|
|
165
|
+
defaultValues: { name: '', description: '' },
|
|
166
|
+
onSubmit: async ({ value }) => {
|
|
167
|
+
/* … */
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
<Modal open={open} onOpenChange={setOpen}>
|
|
172
|
+
<Modal.Content size="medium">
|
|
173
|
+
<Modal.Header title="New bucket" />
|
|
174
|
+
<Modal.Body>
|
|
175
|
+
<form.AppForm>
|
|
176
|
+
<form.FormBody id={FORM_ID}>
|
|
177
|
+
<form.AppField name="name">
|
|
178
|
+
{(field) => <field.TextField label="Name" required />}
|
|
179
|
+
</form.AppField>
|
|
180
|
+
</form.FormBody>
|
|
181
|
+
</form.AppForm>
|
|
182
|
+
</Modal.Body>
|
|
183
|
+
<Modal.Footer>
|
|
184
|
+
<Modal.Close asChild>
|
|
185
|
+
<Button type="button" variant="secondary">
|
|
186
|
+
Cancel
|
|
187
|
+
</Button>
|
|
188
|
+
</Modal.Close>
|
|
189
|
+
<form.AppForm>
|
|
190
|
+
<form.SubmitButton form={FORM_ID}>Create</form.SubmitButton>
|
|
191
|
+
</form.AppForm>
|
|
192
|
+
</Modal.Footer>
|
|
193
|
+
</Modal.Content>
|
|
194
|
+
</Modal>;
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Confirm dialog** — `Modal size="small"` + `Modal.Icon` + a danger action with `isLoading` while the async confirm runs:
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
<Modal open={open} onOpenChange={setOpen}>
|
|
201
|
+
<Modal.Content size="small">
|
|
202
|
+
<Modal.Icon color="red" icon={<AlertTriangle />} />
|
|
203
|
+
<Modal.Header title="Delete bucket?" description="This action cannot be undone." />
|
|
204
|
+
<Modal.Footer>
|
|
205
|
+
<ButtonGroup variant="block">
|
|
206
|
+
<Modal.Close asChild>
|
|
207
|
+
<Button type="button" variant="secondary" disabled={isDeleting}>
|
|
208
|
+
Cancel
|
|
209
|
+
</Button>
|
|
210
|
+
</Modal.Close>
|
|
211
|
+
<Button type="button" variant="danger" isLoading={isDeleting} onClick={handleConfirm}>
|
|
212
|
+
Delete bucket
|
|
213
|
+
</Button>
|
|
214
|
+
</ButtonGroup>
|
|
215
|
+
</Modal.Footer>
|
|
216
|
+
</Modal.Content>
|
|
217
|
+
</Modal>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Card with empty state** — branch on the collection length inside `Card.Content`:
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
<Card bordered padding="none">
|
|
224
|
+
<Card.Header className="tw-flex tw-items-center tw-justify-between tw-p-4">
|
|
225
|
+
<Card.Title>Tokens</Card.Title>
|
|
226
|
+
<Button size="small">
|
|
227
|
+
<Plus className="tw-size-4" /> New token
|
|
228
|
+
</Button>
|
|
229
|
+
</Card.Header>
|
|
230
|
+
<Card.Content className="tw-p-0">
|
|
231
|
+
{tokens.length === 0 ? (
|
|
232
|
+
<EmptyState variant="compact">
|
|
233
|
+
<EmptyState.Title>No tokens yet</EmptyState.Title>
|
|
234
|
+
<EmptyState.Actions>
|
|
235
|
+
<Button size="small">Create token</Button>
|
|
236
|
+
</EmptyState.Actions>
|
|
237
|
+
</EmptyState>
|
|
238
|
+
) : (
|
|
239
|
+
<Table>{/* rows */}</Table>
|
|
240
|
+
)}
|
|
241
|
+
</Card.Content>
|
|
242
|
+
</Card>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Entity list** — `SearchInput` + `Table` + `Pagination` + `EmptyState`. Keep query/page in local state, filter then slice:
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
<div className="tw-flex tw-flex-col tw-gap-4">
|
|
249
|
+
<SearchInput
|
|
250
|
+
allowClear
|
|
251
|
+
placeholder="Search…"
|
|
252
|
+
value={query}
|
|
253
|
+
onValueChange={setQuery}
|
|
254
|
+
className="tw-w-72"
|
|
255
|
+
/>
|
|
256
|
+
{filtered.length === 0 ? (
|
|
257
|
+
<EmptyState>
|
|
258
|
+
<EmptyState.Icon>
|
|
259
|
+
<Search />
|
|
260
|
+
</EmptyState.Icon>
|
|
261
|
+
<EmptyState.Title>No results match your search</EmptyState.Title>
|
|
262
|
+
</EmptyState>
|
|
263
|
+
) : (
|
|
264
|
+
<>
|
|
265
|
+
<Table bordered>{/* pageRows */}</Table>
|
|
266
|
+
<Pagination>{/* Pagination.Previous / .Button active={…} / .Next */}</Pagination>
|
|
267
|
+
</>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Data card with skeleton** — render `Skeleton` / `SkeletonList` in the exact slot the real content will occupy:
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
<Card bordered>
|
|
276
|
+
<Card.Header>
|
|
277
|
+
<Card.Title>{isLoading ? <Skeleton height="md" className="tw-w-32" /> : 'Storage'}</Card.Title>
|
|
278
|
+
</Card.Header>
|
|
279
|
+
<Card.Content>{isLoading ? <SkeletonList groups={2} rowHeight="sm" /> : <Stats />}</Card.Content>
|
|
280
|
+
</Card>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Alert with action** — pass the action node to `Alert`'s `actions` prop; the variant stays categorical (see brand-neutrality contract):
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
<Alert
|
|
287
|
+
variant="warning"
|
|
288
|
+
title="Trial ends in 3 days"
|
|
289
|
+
actions={
|
|
290
|
+
<Button size="small" variant="outline" onClick={onExtend}>
|
|
291
|
+
Extend
|
|
292
|
+
</Button>
|
|
293
|
+
}
|
|
294
|
+
>
|
|
295
|
+
Add a payment method to keep your project active.
|
|
296
|
+
</Alert>
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Disabled-button tooltip** — a disabled `Button` fires no events, so wrap it in a `span` that carries the trigger and `pointer-events-none` the button:
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
<Tooltip tooltip={canSave ? null : reason} placement="top" triggerAsChild>
|
|
303
|
+
<span className={canSave ? undefined : 'tw-inline-block tw-cursor-not-allowed'}>
|
|
304
|
+
<Button disabled={!canSave} className={canSave ? undefined : 'tw-pointer-events-none'}>
|
|
305
|
+
Save
|
|
306
|
+
</Button>
|
|
307
|
+
</span>
|
|
308
|
+
</Tooltip>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Info tooltip** — an icon-only `Button variant="inline"` next to a `Label`, with the hint on a `Tooltip`:
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
<div className="tw-flex tw-items-center tw-gap-1">
|
|
315
|
+
<Label htmlFor={id}>{label}</Label>
|
|
316
|
+
<Tooltip tooltip={hint} placement="right" triggerAsChild>
|
|
317
|
+
<Button
|
|
318
|
+
variant="inline"
|
|
319
|
+
size="icon-3xs"
|
|
320
|
+
aria-label={`More info about ${label}`}
|
|
321
|
+
className="tw-text-muted-foreground"
|
|
322
|
+
>
|
|
323
|
+
<Info className="tw-size-3" />
|
|
324
|
+
</Button>
|
|
325
|
+
</Tooltip>
|
|
326
|
+
</div>
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Page header with tabs** — `PageLayout.Header` hosting `PageHeader.Index`, with `Tabs` passed to `headerNav`:
|
|
330
|
+
|
|
331
|
+
```tsx
|
|
332
|
+
<PageLayout>
|
|
333
|
+
<PageLayout.Header>
|
|
334
|
+
<PageHeader.Index
|
|
335
|
+
title="Storage"
|
|
336
|
+
metadata="3 buckets and 39 tables"
|
|
337
|
+
headerNav={
|
|
338
|
+
<Tabs triggers={TABS} defaultValue={activeTab} onValueChange={setActiveTab}>
|
|
339
|
+
{/* TabsContent */}
|
|
340
|
+
</Tabs>
|
|
341
|
+
}
|
|
342
|
+
/>
|
|
343
|
+
</PageLayout.Header>
|
|
344
|
+
</PageLayout>
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Creatable combobox** — `Combobox` with `allowCreate` accepts values not in `items`:
|
|
348
|
+
|
|
349
|
+
```tsx
|
|
350
|
+
<Combobox
|
|
351
|
+
items={TAGS}
|
|
352
|
+
value={tag}
|
|
353
|
+
onValueChange={setTag}
|
|
354
|
+
allowCreate
|
|
355
|
+
allowClear
|
|
356
|
+
placeholder="Pick or create a tag"
|
|
357
|
+
/>
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Icons
|
|
361
|
+
|
|
362
|
+
Canonical icon surface is `@keboola/design/icons` — a wildcard re-export of [lucide-react](https://lucide.dev) plus Keboola-custom inline-SVG icons. Follow **shadcn convention**: import named icons directly, no wrapper component. Every Lucide icon is available without any package edit.
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
import { ChevronDown, Keboola, Loader2 } from '@keboola/design/icons';
|
|
366
|
+
|
|
367
|
+
<button>Toggle <ChevronDown className="tw-size-4" /></button>
|
|
368
|
+
<Loader2 className="tw-size-4 tw-animate-spin" />
|
|
369
|
+
<Keboola className="tw-size-6" />
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
- **One import surface**: Lucide icons and Keboola-custom icons (e.g. `Keboola`) both export from `@keboola/design/icons` and share the `LucideIcon` type, so they're interchangeable in any API typed `icon: LucideIcon` (Button, etc.).
|
|
373
|
+
- **Sizing**: prefixed Tailwind `tw-size-*` utility — `tw-size-4` = 16px (default for inline icons), `tw-size-5` = 20px, `tw-size-6` = 24px.
|
|
374
|
+
- **Spinner animation**: explicit via `tw-animate-spin`.
|
|
375
|
+
|
|
376
|
+
**FontAwesome is fully removed.** Every `icon` prop takes a Lucide `ReactNode`. (Maintainers: the canonical FA→Lucide mapping table lives in `MAINTAINING.md`.)
|
|
377
|
+
|
|
378
|
+
## Brand-neutrality contract (Layer 0)
|
|
379
|
+
|
|
380
|
+
Layer-0 components split into two categories. The contract applies to chrome only.
|
|
381
|
+
|
|
382
|
+
**Chrome — follows the brand.** Surface, structure, navigation: Card body, Modal frame, Input border, Button outline, focus rings, Sidebar, generic text. Re-skins when a customer brand is applied. **Rule:** use semantic tokens (`tw-bg-background`, `tw-text-foreground`, `tw-border-border`, `tw-bg-muted`, `tw-text-muted-foreground`, `tw-bg-accent`, `tw-bg-destructive`, …). Customers override these via `@keboola/brand-registry`.
|
|
383
|
+
|
|
384
|
+
**Categorical — stays fixed across brands.** Badge `color`, Alert `variant`, ModalIcon status, decorative tag palette. Colors encode meaning; the prop API (`color="green"`, `variant="error"`) is a semantic contract with the consumer. **Rule:** keep using primitive scales (`primary-500`, `error-500`, etc.). A customer's "error" alert stays red regardless of brand — that is correct.
|
|
385
|
+
|
|
386
|
+
**Heuristic.** If a component exposes a `variant` / `color` / `status` prop encoding meaning, it is categorical. Otherwise it is chrome.
|
|
387
|
+
|
|
388
|
+
Layer-1 blocks (`src/blocks/`) are allowed to be Keboola-opinionated — the contract only applies to Layer-0 primitives.
|
|
389
|
+
|
|
390
|
+
## Forms
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
import { useAppForm, withForm, setServerFieldErrors } from '@keboola/design';
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
`useAppForm` is a TanStack Form `createFormHook` binding with typed field connectors (`field.TextField`, `field.NumberField`, …). The schema library (`zod`, `valibot`, …) lives in your app — the block accepts any [Standard Schema](https://standardschema.dev/)-compatible validator. See the Modal-form recipe above for the composite case, and the per-connector signatures in [`COMPONENT-API.md`](./COMPONENT-API.md).
|
|
397
|
+
|
|
398
|
+
## Theming (brand + colorScheme)
|
|
399
|
+
|
|
400
|
+
Wrap your app once in `<ThemeProvider>` from `@keboola/design`. It reads a `Brand` from `@keboola/brand-registry`, sets `data-brand` on `<html>`, and injects the matching `--color-*` / `--radius-*` CSS variables.
|
|
401
|
+
|
|
402
|
+
```tsx
|
|
403
|
+
import { ThemeProvider } from '@keboola/design';
|
|
404
|
+
|
|
405
|
+
<ThemeProvider brand={brand} colorScheme="system">
|
|
406
|
+
<App />
|
|
407
|
+
</ThemeProvider>;
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
- **`brand`** — the active brand decides the chrome. **argo styling is brand-driven**: any non-`keboola-legacy` brand renders argo chrome automatically (`renderArgo = !isLegacy`), so a default `keboola` / `example-customer` brand needs no per-component flags. The `keboola-legacy` brand renders the pre-modernization chrome. With no `brand` prop, `ThemeProvider` defaults to the canonical `keboola` brand.
|
|
411
|
+
- **`colorScheme`** — `"light"` (default) / `"dark"` / `"system"`. Dark mode is set **here, on the root provider** — not per component. `"system"` follows the OS preference.
|
|
412
|
+
|
|
413
|
+
The per-call-site `argo` / `variant="argo"` prop is a **transitional escape hatch** (force argo on one component regardless of brand), **not** the primary switch — prefer driving the whole tree from the brand. Two escape-hatch forms exist:
|
|
414
|
+
|
|
415
|
+
- **Boolean prop:** `<Button argo>`, `<Alert argo>` — swaps the entire variant family.
|
|
416
|
+
- **String variant:** `<Select variant="argo">`, `<MultiCombobox variant="argo">`, `<DropdownMenu variant="argo">` — for components whose `variant` union already encodes `'argo'`.
|
|
417
|
+
|
|
418
|
+
Define a custom brand with `@keboola/brand-registry`'s `brandSchema` (Zod) and pass it to `ThemeProvider`; or override the `--color-*` / `--radius-*` CSS variables directly on `:root`.
|
|
419
|
+
|
|
420
|
+
## Common Tailwind patterns
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
not-disabled:hover:tw-bg-accent
|
|
424
|
+
disabled:tw-opacity-40 tw-cursor-not-allowed
|
|
425
|
+
focus-visible:tw-ring-2 tw-ring-primary
|
|
426
|
+
[&_svg]:tw-text-muted-foreground
|
|
427
|
+
```
|