@keboola/design 3.1.1 → 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 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
+ ```