@urbicon-ui/design-content 6.1.8
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/README.md +54 -0
- package/content/auth/components/account-settings/llm.txt +33 -0
- package/content/auth/components/forgot-password-page/llm.txt +25 -0
- package/content/auth/components/invitation-manager/llm.txt +28 -0
- package/content/auth/components/login-page/llm.txt +34 -0
- package/content/auth/components/notification-badge/llm.txt +21 -0
- package/content/auth/components/notification-center/llm.txt +33 -0
- package/content/auth/components/notification-listener/llm.txt +24 -0
- package/content/auth/components/passkey-manager/llm.txt +27 -0
- package/content/auth/components/push-permission-prompt/llm.txt +32 -0
- package/content/auth/components/register-page/llm.txt +35 -0
- package/content/auth/components/reset-password-page/llm.txt +26 -0
- package/content/auth/components/session-manager/llm.txt +32 -0
- package/content/auth/components/two-factor-manager/llm.txt +40 -0
- package/content/auth/components/verify-email-page/llm.txt +25 -0
- package/content/blocks/components/area-chart/llm.txt +46 -0
- package/content/blocks/components/bar-chart/llm.txt +44 -0
- package/content/blocks/components/calendar/llm.txt +105 -0
- package/content/blocks/components/chart-frame/llm.txt +38 -0
- package/content/blocks/components/command-palette/llm.txt +60 -0
- package/content/blocks/components/composition-bar/llm.txt +69 -0
- package/content/blocks/components/currency-input/llm.txt +65 -0
- package/content/blocks/components/date-picker/llm.txt +90 -0
- package/content/blocks/components/donut-chart/llm.txt +45 -0
- package/content/blocks/components/empty-state/llm.txt +43 -0
- package/content/blocks/components/file-upload/llm.txt +76 -0
- package/content/blocks/components/guide/llm.txt +49 -0
- package/content/blocks/components/guide-article/llm.txt +30 -0
- package/content/blocks/components/guide-beacon/llm.txt +38 -0
- package/content/blocks/components/guide-hint/llm.txt +41 -0
- package/content/blocks/components/guide-marker/llm.txt +36 -0
- package/content/blocks/components/guide-mention/llm.txt +31 -0
- package/content/blocks/components/guide-panel/llm.txt +42 -0
- package/content/blocks/components/guide-provider/llm.txt +31 -0
- package/content/blocks/components/line-chart/llm.txt +45 -0
- package/content/blocks/components/locale-switcher/llm.txt +44 -0
- package/content/blocks/components/planner/llm.txt +68 -0
- package/content/blocks/components/sankey/llm.txt +72 -0
- package/content/blocks/components/sidebar-layout/llm.txt +87 -0
- package/content/blocks/components/sparkline/llm.txt +33 -0
- package/content/blocks/components/theme-switcher/llm.txt +40 -0
- package/content/blocks/primitives/accordion/llm.txt +57 -0
- package/content/blocks/primitives/alert/llm.txt +54 -0
- package/content/blocks/primitives/avatar/llm.txt +61 -0
- package/content/blocks/primitives/badge/llm.txt +60 -0
- package/content/blocks/primitives/breadcrumb/llm.txt +47 -0
- package/content/blocks/primitives/button/llm.txt +80 -0
- package/content/blocks/primitives/button-group/llm.txt +65 -0
- package/content/blocks/primitives/card/llm.txt +68 -0
- package/content/blocks/primitives/checkbox/llm.txt +61 -0
- package/content/blocks/primitives/collapsible/llm.txt +66 -0
- package/content/blocks/primitives/combobox/llm.txt +86 -0
- package/content/blocks/primitives/confirm-dialog/llm.txt +47 -0
- package/content/blocks/primitives/dialog/llm.txt +59 -0
- package/content/blocks/primitives/drawer/llm.txt +54 -0
- package/content/blocks/primitives/form-field/llm.txt +43 -0
- package/content/blocks/primitives/input/llm.txt +73 -0
- package/content/blocks/primitives/menu/llm.txt +81 -0
- package/content/blocks/primitives/pagination/llm.txt +68 -0
- package/content/blocks/primitives/popover/llm.txt +72 -0
- package/content/blocks/primitives/progress/llm.txt +55 -0
- package/content/blocks/primitives/radio-group/llm.txt +53 -0
- package/content/blocks/primitives/segment-group/llm.txt +51 -0
- package/content/blocks/primitives/select/llm.txt +130 -0
- package/content/blocks/primitives/separator/llm.txt +45 -0
- package/content/blocks/primitives/sidebar/llm.txt +79 -0
- package/content/blocks/primitives/skeleton/llm.txt +54 -0
- package/content/blocks/primitives/slider/llm.txt +82 -0
- package/content/blocks/primitives/spinner/llm.txt +46 -0
- package/content/blocks/primitives/stepper/llm.txt +60 -0
- package/content/blocks/primitives/tab/llm.txt +72 -0
- package/content/blocks/primitives/textarea/llm.txt +61 -0
- package/content/blocks/primitives/toast/llm.txt +45 -0
- package/content/blocks/primitives/toggle/llm.txt +62 -0
- package/content/blocks/primitives/toolbar/llm.txt +60 -0
- package/content/blocks/primitives/tooltip/llm.txt +54 -0
- package/content/component-catalog.json +5010 -0
- package/content/design-system/patterns/dashboard.md +55 -0
- package/content/design-system/patterns/form-page.md +69 -0
- package/content/design-system/patterns/onboarding-guide.md +50 -0
- package/content/design-system/patterns/planning-board.md +46 -0
- package/content/design-system/patterns/settings-page.md +48 -0
- package/content/design-system/patterns/tab-navigation.md +136 -0
- package/content/design-system/principles.md +260 -0
- package/content/docs/components/api-reference/llm.txt +32 -0
- package/content/docs/components/code-example/llm.txt +44 -0
- package/content/docs/components/code-panel/llm.txt +26 -0
- package/content/docs/components/docs-layout/llm.txt +61 -0
- package/content/docs/components/info-card/llm.txt +31 -0
- package/content/docs/components/playground-configurator/llm.txt +49 -0
- package/content/docs/components/section/llm.txt +46 -0
- package/content/docs/components/table-of-contents/llm.txt +48 -0
- package/content/docs/components/types-reference/llm.txt +38 -0
- package/content/guides/llms-full-template.md +1019 -0
- package/content/icons.json +4834 -0
- package/content/meta.json +5 -0
- package/content/table/table/llm.txt +110 -0
- package/content/verbs/adopt.md +33 -0
- package/content/verbs/audit.md +29 -0
- package/content/verbs/compose.md +38 -0
- package/content/verbs/critique.md +27 -0
- package/content/verbs/fix.md +29 -0
- package/content/verbs/migrate.md +30 -0
- package/content/verbs/onboard.md +33 -0
- package/content/verbs/polish.md +25 -0
- package/content/verbs/redesign.md +29 -0
- package/content/verbs/retheme.md +29 -0
- package/package.json +45 -0
- package/src/content-loader.test.ts +78 -0
- package/src/content-loader.ts +97 -0
- package/src/index.ts +23 -0
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
# Urbicon UI – Full API Reference for LLMs
|
|
2
|
+
|
|
3
|
+
> This file is optimized for LLM consumption. It contains everything needed to correctly generate code using Urbicon UI.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Urbicon UI packages are published to a private registry. Create a `bunfig.toml` in your project root:
|
|
8
|
+
|
|
9
|
+
```toml
|
|
10
|
+
# bunfig.toml
|
|
11
|
+
[install.scopes]
|
|
12
|
+
"@urbicon-ui" = { url = "https://npm.urbicon.de/" }
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then install the packages:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun add @urbicon-ui/blocks @urbicon-ui/i18n
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
CSS setup (in your app's root layout or entry CSS). Your app owns the Tailwind
|
|
22
|
+
import and it MUST come first; the Urbicon UI CSS that follows depends on it and
|
|
23
|
+
overrides its defaults:
|
|
24
|
+
```css
|
|
25
|
+
@import 'tailwindcss';
|
|
26
|
+
@import '@urbicon-ui/blocks/style/index.css'; /* tokens + @source directives */
|
|
27
|
+
@import '@urbicon-ui/table/style/index.css'; /* only if you use @urbicon-ui/table */
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Import the single `style/index.css` — **not** the `foundation`/`semantic`/`interaction`
|
|
31
|
+
subfiles. `index.css` ships the design tokens, global classes (`.sr-only`, mint styles),
|
|
32
|
+
and the Tailwind `@source` directives that make Tailwind scan the component classes inside
|
|
33
|
+
`node_modules`. You do **not** need to add any manual `@source` directives; importing the
|
|
34
|
+
subfiles (which omit those directives) is the usual cause of responsive utilities like
|
|
35
|
+
`lg:hidden` going missing in production.
|
|
36
|
+
|
|
37
|
+
## Import Pattern
|
|
38
|
+
|
|
39
|
+
ALWAYS import from the package root:
|
|
40
|
+
```svelte
|
|
41
|
+
<script>
|
|
42
|
+
import { Button, Input, Card, Dialog, Badge } from '@urbicon-ui/blocks';
|
|
43
|
+
</script>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
NEVER import from internal paths like `@urbicon-ui/blocks/primitives/Button`.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Shared API Grammar
|
|
51
|
+
|
|
52
|
+
Every component follows a predictable API. Learn once, apply everywhere.
|
|
53
|
+
|
|
54
|
+
### `intent` – Semantic Color
|
|
55
|
+
|
|
56
|
+
Values: `primary` | `secondary` | `success` | `warning` | `danger` | `neutral`
|
|
57
|
+
Default: `neutral` (action elements), `primary` (form/decorative elements)
|
|
58
|
+
|
|
59
|
+
### `variant` – Visual Weight
|
|
60
|
+
|
|
61
|
+
Values: `filled` | `outlined` | `ghost` | `text` (component-dependent)
|
|
62
|
+
- `filled`: Solid background, highest emphasis
|
|
63
|
+
- `outlined`: Border only, transparent background
|
|
64
|
+
- `ghost`: No border, no background
|
|
65
|
+
- `text`: Minimal, text-only (Button only)
|
|
66
|
+
|
|
67
|
+
### `size` – Dimensions
|
|
68
|
+
|
|
69
|
+
Standard scale: `xs` | `sm` | `md` | `lg` | `xl`
|
|
70
|
+
Default: `md` for all components.
|
|
71
|
+
|
|
72
|
+
| Size | Height | Font |
|
|
73
|
+
|------|--------|----------|
|
|
74
|
+
| xs | h-6 | text-xs |
|
|
75
|
+
| sm | h-8 | text-sm |
|
|
76
|
+
| md | h-10 | text-base|
|
|
77
|
+
| lg | h-12 | text-lg |
|
|
78
|
+
| xl | h-14 | text-xl |
|
|
79
|
+
|
|
80
|
+
### `unstyled` – Strip All Default Styles
|
|
81
|
+
|
|
82
|
+
```svelte
|
|
83
|
+
<Button unstyled class="my-custom-classes">Custom</Button>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `slotClasses` – Override Specific Slots
|
|
87
|
+
|
|
88
|
+
Each component documents its slots. Override without losing other styles:
|
|
89
|
+
```svelte
|
|
90
|
+
<Card slotClasses={{ header: 'bg-primary-subtle', footer: 'border-t-2' }}>
|
|
91
|
+
...
|
|
92
|
+
</Card>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### `mint` – Micro-Interactions (opt-in)
|
|
96
|
+
|
|
97
|
+
Requires `registerDefaultMints()` call at app startup.
|
|
98
|
+
Values: `'scale'` | `'ripple'` | `'translate'` | `'glow'` | `'none'` | array
|
|
99
|
+
|
|
100
|
+
```svelte
|
|
101
|
+
<Button mint="scale">Hover me</Button>
|
|
102
|
+
<Card mint={['scale', 'glow']}>Interactive</Card>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### `preset` – Apply a Named Project Style
|
|
106
|
+
|
|
107
|
+
Values: any preset name registered via `<BlocksProvider presets={{...}}>` (project-specific).
|
|
108
|
+
|
|
109
|
+
Presets are the correct escape hatch when you need a look **outside the semantic intent palette**
|
|
110
|
+
(`primary | secondary | success | warning | danger | neutral`) — e.g. a dark translucent overlay
|
|
111
|
+
button on top of an image, a brand-colored button, or a "glass" surface. They keep hover/active/
|
|
112
|
+
dark-mode logic coherent AND make the custom look reusable across the project.
|
|
113
|
+
|
|
114
|
+
```svelte
|
|
115
|
+
<Button preset="overlay">Reinholen</Button>
|
|
116
|
+
<Card preset="glass">...</Card>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Registration happens once at the app root — see `Customization` → `Level 2: Presets` below.
|
|
120
|
+
|
|
121
|
+
**DO NOT** reach for `class="bg-…! hover:bg-…! active:bg-…!"` when the intent palette doesn't fit.
|
|
122
|
+
That pattern defeats the component's hover/active/dark-mode cascade and leaks visual decisions
|
|
123
|
+
into every call site. Register a preset instead.
|
|
124
|
+
|
|
125
|
+
### `class` – Additional CSS Classes
|
|
126
|
+
|
|
127
|
+
Merged with variant classes. Always available on every component.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Component Families
|
|
132
|
+
|
|
133
|
+
Every primitive belongs to exactly one of six families. The family decides ARIA role, tier-system membership, and border-token source. Pick the right family up-front to avoid categorical bugs (button that looks like an input, menu that doubles as a listbox, avatar that mutates when commit-radii flatten).
|
|
134
|
+
|
|
135
|
+
### Family table
|
|
136
|
+
|
|
137
|
+
| Family | Members | ARIA role | Tier default | Border source |
|
|
138
|
+
|---|---|---|---|---|
|
|
139
|
+
| Action | Button, ButtonGroup, Menu, Toolbar, Toggle | `button`, `menu`, `menuitem`, `toolbar`, `switch` | `commit` (tier-aware) | Intent (`border-neutral` etc.) |
|
|
140
|
+
| Form | Input, Select, Combobox, Textarea, Checkbox, RadioGroup, Slider, FormField | `textbox`, `listbox`, `combobox`, `checkbox`, `radio`, `slider` | `modify` (tier-aware) | Surface (`border-border-subtle`) |
|
|
141
|
+
| Navigation | Breadcrumb, Pagination, SegmentGroup, Stepper, Tab | `navigation`, `tablist`, `tab` | per-component (tier-aware) | mixed |
|
|
142
|
+
| Container | Card, Alert, Accordion, Collapsible, Dialog, Drawer, Popover, Tooltip, Sidebar, Separator, ConfirmDialog | `dialog`, `tooltip`, `region`, `aside` | `contain` (tier-aware) | Surface or Hairline |
|
|
143
|
+
| Feedback / Ambient | Toast, Spinner, Progress, Skeleton, Badge | `status`, `alert`, `progressbar` | **not tier-aware** (Badge is the documented edge case) | Intent (status-tinted) or none |
|
|
144
|
+
| Identity | Avatar | `img` or `button` | **not tier-aware** — own shape axis (`circle` / `rounded` / `square`) | none |
|
|
145
|
+
|
|
146
|
+
### Action — interactive triggers
|
|
147
|
+
|
|
148
|
+
- ARIA: items dispatch `onSelect` / `onclick`; never hold a value.
|
|
149
|
+
- Border: must read as interactive even in `intent="neutral"` — uses Intent tokens (~neutral-500 in light).
|
|
150
|
+
- Industry analogue: Radix DropdownMenu, Headless UI Menu.
|
|
151
|
+
- Pick `Menu` for one-off action lists; `Button` for single triggers; `ButtonGroup` for grouped triggers; `Toolbar` for free-form bars; `Toggle` for bistable switches.
|
|
152
|
+
|
|
153
|
+
### Form — value holders
|
|
154
|
+
|
|
155
|
+
- ARIA: control owns a value, emits `onValueChange` / `bind:value`.
|
|
156
|
+
- Border: must read as a container, not a button — uses Surface tokens (~neutral-200 in light).
|
|
157
|
+
- Industry analogue: Radix Select / Combobox, Headless UI Listbox / Combobox.
|
|
158
|
+
- Pick `Select` for value pickers; `Combobox` for searchable Select; `Select multiple` for multi-select (NOT `Menu multiple`); `Menu` (Action family) for one-off action lists.
|
|
159
|
+
|
|
160
|
+
### Navigation — section / route selection
|
|
161
|
+
|
|
162
|
+
- ARIA: `<nav aria-label>`, `role="tablist"` + `role="tab"`, `aria-current` for breadcrumbs / pagination.
|
|
163
|
+
- Border: per-component; SegmentGroup indicator uses Intent (active item reads as action-like).
|
|
164
|
+
- Pick `Tab` for sectioned content; `SegmentGroup` for inline pickers (holds value, unlike ButtonGroup); `Stepper` for linear progress; `Breadcrumb` for route context; `Pagination` for list paging.
|
|
165
|
+
|
|
166
|
+
### Container — content surfaces
|
|
167
|
+
|
|
168
|
+
- ARIA: `<dialog>`, `role="tooltip"`, `<aside>`, `<details>` / `aria-expanded`.
|
|
169
|
+
- Border: never Intent in default state — Surface or Hairline. If a container border reads as a button, family mismatch.
|
|
170
|
+
- Pick `Card` / `Alert` for in-page surfaces; `Dialog` / `Drawer` / `ConfirmDialog` for modal overlays; `Popover` for anchored floating; `Tooltip` for hover-described inline targets; `Sidebar` for persistent app-shell; `Accordion` / `Collapsible` for disclosure.
|
|
171
|
+
|
|
172
|
+
### Feedback / Ambient — status communication
|
|
173
|
+
|
|
174
|
+
- ARIA: `role="alert"`, `role="status"`, `role="progressbar"`. Spinner inherits `aria-busy` from host.
|
|
175
|
+
- Tier: NOT tier-aware. Geometry is per-component (e.g. Spinner is always `rounded-full`, Toast is always `rounded-contain`). A wrapping `<Toolbar tier="modify">` does NOT flatten a Toast.
|
|
176
|
+
- Badge exception: Badge DOES expose a `tier` prop (`commit` default, `modify` opt-in for inline Toolbar strips) — but the family rule remains: Feedback geometry is per-component, not per-context.
|
|
177
|
+
- Pick `Toast` for system notifications; `Alert` for in-page banners; `Spinner` / `Progress` / `Skeleton` for loading states; `Badge` for status tags / counters (see Badge Patterns section for the 5 use cases).
|
|
178
|
+
|
|
179
|
+
### Identity — Avatar only
|
|
180
|
+
|
|
181
|
+
- Avatar lives outside the tier system. Its `variant` axis (`circle` / `rounded` / `square`) is identity-shape, not layout-tier. A brand that flattens `--radius-commit` (squared pill buttons) keeps circular avatars.
|
|
182
|
+
- Avatar uses no border in its default render; `ring` is the only border-adjacent affordance.
|
|
183
|
+
|
|
184
|
+
### Cross-family disambiguation
|
|
185
|
+
|
|
186
|
+
- `Menu` vs `Select` — Menu for one-off actions (Action), Select for value pickers (Form). Different ARIA, different border family.
|
|
187
|
+
- `ButtonGroup` vs `SegmentGroup` — ButtonGroup dispatches actions, SegmentGroup holds a value. If you `bind:value` on a ButtonGroup, switch to SegmentGroup.
|
|
188
|
+
- `Sidebar` vs `Drawer` — Sidebar for persistent layout (`<aside>`, no backdrop by default), Drawer for transient modal (`<dialog>`, always backdrop + focus-trap).
|
|
189
|
+
- `Popover` vs `Tooltip` — Popover hosts a focus-trapped panel for click-interactions, Tooltip is non-focusable for hover-descriptions.
|
|
190
|
+
- `Alert` vs `Toast` — Alert is in-page (`role="alert"`), Toast is system-level + stacking.
|
|
191
|
+
|
|
192
|
+
### Tier-aware components (read context from `<TierContext>`)
|
|
193
|
+
|
|
194
|
+
Seven primitives expose a `tier` prop AND inherit from a wrapping context when unset:
|
|
195
|
+
|
|
196
|
+
| Component | Default tier | Family |
|
|
197
|
+
|---|---|---|
|
|
198
|
+
| Button | `commit` | Action |
|
|
199
|
+
| Toggle | `commit` | Action |
|
|
200
|
+
| SegmentGroup | `commit` | Navigation |
|
|
201
|
+
| Stepper | `commit` | Navigation |
|
|
202
|
+
| RadioGroup | `commit` | Form |
|
|
203
|
+
| Checkbox | `modify` | Form |
|
|
204
|
+
| Tab | `modify` | Navigation |
|
|
205
|
+
|
|
206
|
+
All other primitives use a fixed tier per family (see family table above) — Container components are `contain`, Form components are `modify`, Action components are `commit`, Feedback / Identity components are not tier-aware at all.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Components
|
|
211
|
+
|
|
212
|
+
{{COMPONENTS}}
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Design Token System
|
|
217
|
+
|
|
218
|
+
This is the COMPLETE list of available semantic tokens. Use ONLY these — do not invent token names.
|
|
219
|
+
|
|
220
|
+
### Surface Tokens (backgrounds)
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
bg-surface-base /* page background */
|
|
224
|
+
bg-surface-quiet /* softly tinted in-page zone (Lighter default) */
|
|
225
|
+
bg-surface-subtle /* visible tinted zone */
|
|
226
|
+
bg-surface-elevated /* floating surfaces (paired with shadow) */
|
|
227
|
+
bg-surface-overlay /* modals, popovers */
|
|
228
|
+
bg-surface-interactive /* interactive backgrounds */
|
|
229
|
+
bg-surface-hover /* hover state */
|
|
230
|
+
bg-surface-active /* active/pressed state */
|
|
231
|
+
bg-surface-disabled /* disabled elements */
|
|
232
|
+
bg-surface-selected /* selected items (uses primary-50) */
|
|
233
|
+
bg-surface-inverted /* inverted surfaces (tooltips) */
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Text Tokens
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
text-text-primary /* main text */
|
|
240
|
+
text-text-secondary /* supporting text */
|
|
241
|
+
text-text-tertiary /* muted text, metadata */
|
|
242
|
+
text-text-quaternary /* most subtle text */
|
|
243
|
+
text-text-disabled /* disabled text */
|
|
244
|
+
text-text-inverted /* text on inverted surfaces */
|
|
245
|
+
text-text-on-primary /* text on intent-colored backgrounds */
|
|
246
|
+
text-text-on-dark /* text on dark surfaces */
|
|
247
|
+
text-text-on-surface /* text on any surface (auto-contrast) */
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Border Tokens
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
border-border-subtle /* gentle grouping */
|
|
254
|
+
border-border-default /* standard borders */
|
|
255
|
+
border-border-emphasis /* emphasized borders */
|
|
256
|
+
border-border-strong /* high-contrast borders */
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Intent Tokens (available for ALL intents: primary, secondary, neutral, success, warning, danger)
|
|
260
|
+
|
|
261
|
+
Each intent has 5 variants. Example with `success`:
|
|
262
|
+
```
|
|
263
|
+
bg-success /* base intent color */
|
|
264
|
+
bg-success-hover /* hover state */
|
|
265
|
+
bg-success-active /* pressed state */
|
|
266
|
+
bg-success-subtle /* soft background (e.g. success-50) */
|
|
267
|
+
bg-success-emphasis /* strong/dark variant */
|
|
268
|
+
text-success /* intent-colored text */
|
|
269
|
+
text-success-hover /* hover text */
|
|
270
|
+
text-success-subtle /* subtle intent text */
|
|
271
|
+
border-success /* intent-colored border */
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Same pattern for: `primary-*`, `secondary-*`, `neutral-*`, `warning-*`, `danger-*`
|
|
275
|
+
|
|
276
|
+
### Feedback Tokens (status messages)
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
bg-feedback-info text-feedback-info /* maps to primary */
|
|
280
|
+
bg-feedback-info-subtle /* soft info background */
|
|
281
|
+
bg-feedback-success text-feedback-success /* maps to success */
|
|
282
|
+
bg-feedback-success-subtle /* soft success background */
|
|
283
|
+
bg-feedback-warning text-feedback-warning /* maps to warning */
|
|
284
|
+
bg-feedback-warning-subtle /* soft warning background */
|
|
285
|
+
bg-feedback-error text-feedback-error /* maps to danger */
|
|
286
|
+
bg-feedback-error-subtle /* soft error background */
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### DON'T invent token names — common mistakes:
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
/* WRONG — these don't exist */
|
|
293
|
+
text-status-danger bg-status-warning border-l-status-*
|
|
294
|
+
text-feedback-success-fg text-feedback-danger-fg
|
|
295
|
+
|
|
296
|
+
/* CORRECT equivalents */
|
|
297
|
+
text-danger bg-warning border-l-danger
|
|
298
|
+
text-feedback-success text-feedback-error
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### DON'T use primitive colors with dark: overrides:
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
/* WRONG */
|
|
305
|
+
bg-white dark:bg-neutral-900
|
|
306
|
+
|
|
307
|
+
/* CORRECT */
|
|
308
|
+
bg-surface-base
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Shadow Tokens
|
|
312
|
+
|
|
313
|
+
```
|
|
314
|
+
shadow-[var(--blocks-shadow-xs)] /* minimal */
|
|
315
|
+
shadow-[var(--blocks-shadow-sm)] /* buttons */
|
|
316
|
+
shadow-[var(--blocks-shadow-md)] /* hover states */
|
|
317
|
+
shadow-[var(--blocks-shadow-lg)] /* menus, popovers */
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Z-Index Tokens
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
z-[var(--z-dropdown)] /* 9999 */
|
|
324
|
+
z-[var(--z-overlay)] /* 1300 */
|
|
325
|
+
z-[var(--z-modal)] /* 1400 */
|
|
326
|
+
z-[var(--z-popover)] /* 1500 */
|
|
327
|
+
z-[var(--z-tooltip)] /* 1800 */
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Duration Tokens
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
duration-[var(--blocks-duration-instant)] /* 75ms */
|
|
334
|
+
duration-[var(--blocks-duration-fast)] /* 150ms */
|
|
335
|
+
duration-[var(--blocks-duration-normal)] /* 250ms */
|
|
336
|
+
duration-[var(--blocks-duration-slow)] /* 350ms */
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Easing Tokens
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
var(--blocks-ease-confident) /* standard transitions */
|
|
343
|
+
var(--blocks-ease-springy) /* bouncy animations */
|
|
344
|
+
var(--blocks-ease-smooth) /* gentle animations */
|
|
345
|
+
var(--blocks-ease-snappy) /* quick, decisive */
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Border Radius Scale
|
|
349
|
+
|
|
350
|
+
```
|
|
351
|
+
rounded-xs /* 0.125rem */ rounded-sm /* 0.25rem */
|
|
352
|
+
rounded-md /* 0.375rem */ rounded-lg /* 0.5rem */
|
|
353
|
+
rounded-xl /* 0.75rem */ rounded-2xl /* 1rem */
|
|
354
|
+
rounded-3xl /* 1.5rem */ rounded-4xl /* 2rem */
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Design Quality
|
|
360
|
+
|
|
361
|
+
These guidelines help you create interfaces with genuine visual identity. They are framed as
|
|
362
|
+
"AVOID → INSTEAD" patterns because knowing what NOT to do is more effective than abstract principles.
|
|
363
|
+
|
|
364
|
+
### Vary Visual Weight
|
|
365
|
+
|
|
366
|
+
Don't give every element the same visual importance. Vary `variant`, `padding`, and grid span to reflect content hierarchy.
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
/* AVOID — uniform grid, all cards identical */
|
|
370
|
+
<div class="grid grid-cols-4 gap-4">
|
|
371
|
+
<Card variant="elevated" padding="md">Metric A</Card>
|
|
372
|
+
<Card variant="elevated" padding="md">Metric B</Card>
|
|
373
|
+
<Card variant="elevated" padding="md">Metric C</Card>
|
|
374
|
+
<Card variant="elevated" padding="md">Metric D</Card>
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
/* INSTEAD — weight reflects importance */
|
|
378
|
+
<div class="grid grid-cols-4 gap-4">
|
|
379
|
+
<Card variant="elevated" padding="lg" class="col-span-2">Hero metric</Card>
|
|
380
|
+
<Card variant="outlined" padding="md">Secondary</Card>
|
|
381
|
+
<Card variant="outlined" padding="md">Secondary</Card>
|
|
382
|
+
</div>
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Color = Meaning, Not Decoration
|
|
386
|
+
|
|
387
|
+
Neutral surfaces should dominate (80–90%). Use `intent` and semantic color tokens ONLY for
|
|
388
|
+
their semantic meaning — status, severity, actions — never as visual flair.
|
|
389
|
+
|
|
390
|
+
```
|
|
391
|
+
/* AVOID — color without purpose */
|
|
392
|
+
<Card class="bg-primary-subtle"> <!-- why primary? -->
|
|
393
|
+
<Badge intent="primary">Active</Badge> <!-- "Active" isn't a primary action -->
|
|
394
|
+
|
|
395
|
+
/* INSTEAD — color communicates state */
|
|
396
|
+
<Card variant="elevated"> <!-- neutral default -->
|
|
397
|
+
<Badge intent="success" variant="soft">Healthy</Badge> <!-- green = healthy -->
|
|
398
|
+
<Badge intent="danger" variant="filled">Critical</Badge> <!-- red = critical -->
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Spacing Signals Relationships
|
|
402
|
+
|
|
403
|
+
Don't use the same gap everywhere. Tight spacing groups related items; generous spacing
|
|
404
|
+
separates distinct sections.
|
|
405
|
+
|
|
406
|
+
```
|
|
407
|
+
/* AVOID — uniform spacing */
|
|
408
|
+
<div class="space-y-4">
|
|
409
|
+
<section>...</section> <!-- same gap between sections -->
|
|
410
|
+
<section>...</section>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
/* INSTEAD — spacing hierarchy */
|
|
414
|
+
<div class="space-y-10"> <!-- generous between sections -->
|
|
415
|
+
<section class="space-y-3">...</section> <!-- tight within sections -->
|
|
416
|
+
<section class="space-y-3">...</section>
|
|
417
|
+
</div>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Commit to a Shape Language
|
|
421
|
+
|
|
422
|
+
Choose a border-radius philosophy and apply it consistently. Override component defaults
|
|
423
|
+
with `class` or `slotClasses` when your design requires it.
|
|
424
|
+
|
|
425
|
+
| Strategy | Radius | Personality |
|
|
426
|
+
|----------|--------|-------------|
|
|
427
|
+
| Sharp | `rounded-sm` / `rounded` | Technical precision, data-dense |
|
|
428
|
+
| Soft | `rounded-lg` / `rounded-xl` | Professional, approachable |
|
|
429
|
+
| Round | `rounded-2xl` / `rounded-3xl` | Friendly, modern |
|
|
430
|
+
|
|
431
|
+
Use larger radii for hero/prominent elements, smaller radii for compact/data-dense elements.
|
|
432
|
+
Don't rely on component defaults alone — make a deliberate choice:
|
|
433
|
+
|
|
434
|
+
```svelte
|
|
435
|
+
<!-- Override Card radius for a softer design -->
|
|
436
|
+
<Card class="rounded-2xl" padding="lg">...</Card>
|
|
437
|
+
|
|
438
|
+
<!-- Or set globally via BlocksProvider -->
|
|
439
|
+
<BlocksProvider defaults={{
|
|
440
|
+
Card: { slotClasses: { base: 'rounded-2xl' } }
|
|
441
|
+
}}>
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Data-Driven Styling
|
|
445
|
+
|
|
446
|
+
Let the data shape the presentation. Different states, severities, or categories should look
|
|
447
|
+
visually distinct — not just carry a different label.
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
/* AVOID — identical rows regardless of content */
|
|
451
|
+
{#each alerts as alert}
|
|
452
|
+
<div class="py-3 border-b border-border-subtle">
|
|
453
|
+
<Badge>{alert.severity}</Badge> {alert.message}
|
|
454
|
+
</div>
|
|
455
|
+
{/each}
|
|
456
|
+
|
|
457
|
+
/* INSTEAD — severity drives visual weight */
|
|
458
|
+
{#each alerts as alert}
|
|
459
|
+
{@const critical = alert.severity === 'critical'}
|
|
460
|
+
<div class="border-b border-border-subtle {critical ? 'py-4 font-medium' : 'py-2.5'}">
|
|
461
|
+
<Badge intent={severityIntent(alert.severity)}
|
|
462
|
+
variant={critical ? 'filled' : 'soft'}>
|
|
463
|
+
{alert.severity}
|
|
464
|
+
</Badge>
|
|
465
|
+
<span class={critical ? 'text-text-primary' : 'text-text-secondary'}>
|
|
466
|
+
{alert.message}
|
|
467
|
+
</span>
|
|
468
|
+
</div>
|
|
469
|
+
{/each}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Don't Copy, Compose
|
|
473
|
+
|
|
474
|
+
Recipes and examples show ONE possible interpretation. Use them for API understanding,
|
|
475
|
+
not as visual templates. Your implementation should:
|
|
476
|
+
|
|
477
|
+
- Vary Card `variant` across the page based on content importance (not all `elevated`)
|
|
478
|
+
- Use different density (padding, gap, text size) for different page sections
|
|
479
|
+
- Make one element per section clearly dominant — if everything is emphasized, nothing is
|
|
480
|
+
- Use `text-text-secondary` and `text-text-tertiary` generously to push supporting content back
|
|
481
|
+
- Choose your OWN spacing rhythm, radius strategy, and color distribution
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
## Customization
|
|
486
|
+
|
|
487
|
+
Four levels of customization, from simple color swaps to fully custom designs.
|
|
488
|
+
**Pick the lowest level that solves your problem** — lower levels preserve more of the
|
|
489
|
+
design system's behavior (dark mode, hover/active cascade, focus rings).
|
|
490
|
+
|
|
491
|
+
### Level 1: CSS Token Themes
|
|
492
|
+
|
|
493
|
+
Import a theme CSS file after base styles to override the color palette:
|
|
494
|
+
```css
|
|
495
|
+
@import '@urbicon-ui/blocks/style/index.css';
|
|
496
|
+
@import '@urbicon-ui/blocks/style/themes/ocean.css';
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Built-in themes: `ocean.css`, `forest.css`, `sunset.css`, `rose.css`, `neutral.css`.
|
|
500
|
+
|
|
501
|
+
Create custom themes with a `@theme` block:
|
|
502
|
+
```css
|
|
503
|
+
@theme {
|
|
504
|
+
--color-primary-50: oklch(0.95 0.03 YOUR_HUE);
|
|
505
|
+
--color-primary-500: oklch(0.58 0.13 YOUR_HUE);
|
|
506
|
+
--color-primary-900: oklch(0.26 0.06 YOUR_HUE);
|
|
507
|
+
/* ... full 50–950 scale */
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Level 2: Presets – Named Project-Defined Styles
|
|
512
|
+
|
|
513
|
+
**Use this when the built-in `intent` palette doesn't fit.** Presets are named, reusable
|
|
514
|
+
`slotClasses` bundles you register once at the app root, then apply per component via
|
|
515
|
+
a `preset="…"` prop. They are the **preferred alternative** to ad-hoc `class="bg-…!"`
|
|
516
|
+
overrides at call sites.
|
|
517
|
+
|
|
518
|
+
Register once at the app root:
|
|
519
|
+
```svelte
|
|
520
|
+
<script>
|
|
521
|
+
import { BlocksProvider } from '@urbicon-ui/blocks';
|
|
522
|
+
</script>
|
|
523
|
+
|
|
524
|
+
<BlocksProvider
|
|
525
|
+
presets={{
|
|
526
|
+
Button: {
|
|
527
|
+
overlay: {
|
|
528
|
+
slotClasses: {
|
|
529
|
+
base: 'bg-black/20 hover:bg-black/30 active:bg-black/40 text-white border-transparent'
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
brand: {
|
|
533
|
+
slotClasses: {
|
|
534
|
+
base: 'bg-[#FF5A1F] hover:bg-[#E04C15] active:bg-[#C53F0D] text-white border-transparent'
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
Card: {
|
|
539
|
+
glass: {
|
|
540
|
+
slotClasses: {
|
|
541
|
+
base: 'bg-white/10 backdrop-blur-xl border-white/20'
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}}
|
|
546
|
+
>
|
|
547
|
+
<slot />
|
|
548
|
+
</BlocksProvider>
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Then use across the project:
|
|
552
|
+
```svelte
|
|
553
|
+
<Button preset="overlay">Reinholen</Button>
|
|
554
|
+
<Button preset="brand">Jetzt kaufen</Button>
|
|
555
|
+
<Card preset="glass">Heads-up display</Card>
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**Why presets over inline `class` overrides?**
|
|
559
|
+
|
|
560
|
+
```svelte
|
|
561
|
+
<!-- ❌ AVOID: Defeats hover/active cascade, leaks decisions into every call site,
|
|
562
|
+
requires `!` to out-specify tv() defaults, inconsistent across the project. -->
|
|
563
|
+
<Button intent="primary" class="bg-black/20! hover:bg-black/30! active:bg-black/40!">
|
|
564
|
+
Reinholen
|
|
565
|
+
</Button>
|
|
566
|
+
|
|
567
|
+
<!-- ✅ PREFER: Intent-less, cohesive cascade, reusable across the codebase. -->
|
|
568
|
+
<Button preset="overlay">Reinholen</Button>
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
When defining a preset, specify **all interactive states explicitly** (`hover:`, `active:`,
|
|
572
|
+
and `focus-visible:` where applicable). The preset's `slotClasses.base` is merged *after*
|
|
573
|
+
the `tv()` defaults, so conflicting Tailwind utilities are resolved by `tailwind-merge`
|
|
574
|
+
(last value wins) — no `!` needed.
|
|
575
|
+
|
|
576
|
+
An unknown preset name emits a dev-only console warning, so typos are discoverable.
|
|
577
|
+
|
|
578
|
+
### Level 3: BlocksProvider – Global Component Defaults
|
|
579
|
+
|
|
580
|
+
Use `defaults` (distinct from `presets`) to set **project-wide baseline styles** that
|
|
581
|
+
apply to *every* instance of a component — e.g. "all Buttons should be rounded-full in this app":
|
|
582
|
+
|
|
583
|
+
```svelte
|
|
584
|
+
<BlocksProvider
|
|
585
|
+
defaults={{
|
|
586
|
+
Button: { slotClasses: { base: 'rounded-full font-bold uppercase' } },
|
|
587
|
+
Card: { slotClasses: { base: 'rounded-3xl shadow-2xl' } },
|
|
588
|
+
Input: { slotClasses: { base: 'rounded-full' } }
|
|
589
|
+
}}
|
|
590
|
+
>
|
|
591
|
+
<slot />
|
|
592
|
+
</BlocksProvider>
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
`defaults` vs. `presets` — when to use which:
|
|
596
|
+
- **`defaults`**: blanket project style applied to every instance (no opt-in required)
|
|
597
|
+
- **`presets`**: named alternative look, opt-in via `preset="…"` prop at the call site
|
|
598
|
+
|
|
599
|
+
Merge priority (lowest → highest):
|
|
600
|
+
1. `tv()` variant styles (library default)
|
|
601
|
+
2. `BlocksProvider defaults.slotClasses` (global baseline)
|
|
602
|
+
3. `BlocksProvider presets[Component][name].slotClasses` (when `preset` prop is set)
|
|
603
|
+
4. Instance `slotClasses` prop
|
|
604
|
+
5. Instance `class` prop
|
|
605
|
+
|
|
606
|
+
### Level 4: Global Unstyled Mode
|
|
607
|
+
|
|
608
|
+
Strip all default styles from every component, then apply your own:
|
|
609
|
+
```svelte
|
|
610
|
+
<BlocksProvider unstyled defaults={{
|
|
611
|
+
Button: { slotClasses: { base: 'inline-flex items-center border-2 px-6 py-3 font-mono' } }
|
|
612
|
+
}}>
|
|
613
|
+
<slot />
|
|
614
|
+
</BlocksProvider>
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
## Style Patterns
|
|
620
|
+
|
|
621
|
+
Urbicon UI components are designed to be radically reskinned. Do NOT default to the standard
|
|
622
|
+
look from examples — choose a visual style that fits the project's identity. Every component
|
|
623
|
+
supports `unstyled` (strips all defaults) and `slotClasses` (targeted sub-element overrides).
|
|
624
|
+
|
|
625
|
+
### Slot Reference
|
|
626
|
+
|
|
627
|
+
| Component | Slots |
|
|
628
|
+
|-----------|-------|
|
|
629
|
+
| Button | `base`, `content`, `spinner` |
|
|
630
|
+
| Card | `base`, `header`, `content`, `footer` |
|
|
631
|
+
| Input | `wrapper`, `container`, `base`, `label`, `message`, `iconContainer` |
|
|
632
|
+
| Badge | `base`, `content`, `removeButton`, `removeIcon` |
|
|
633
|
+
| Dialog | `base`, `overlay`, `header`, `body`, `footer` |
|
|
634
|
+
| Alert | `base`, `icon`, `content`, `title`, `description`, `actions` |
|
|
635
|
+
| Tooltip | `base`, `arrow` |
|
|
636
|
+
|
|
637
|
+
### Pattern: Glassmorphism
|
|
638
|
+
|
|
639
|
+
```svelte
|
|
640
|
+
<BlocksProvider defaults={{
|
|
641
|
+
Card: {
|
|
642
|
+
slotClasses: {
|
|
643
|
+
base: 'bg-white/10 backdrop-blur-xl border border-white/20 shadow-2xl rounded-2xl'
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
Button: {
|
|
647
|
+
slotClasses: {
|
|
648
|
+
base: 'bg-white/15 backdrop-blur-md border border-white/25 hover:bg-white/25 rounded-xl text-white shadow-lg'
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
Input: {
|
|
652
|
+
slotClasses: {
|
|
653
|
+
base: 'bg-white/10 backdrop-blur-sm border-white/20 text-white placeholder:text-white/50 rounded-xl',
|
|
654
|
+
label: 'text-white/80'
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}}>
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Combine with a gradient or image background on the page container.
|
|
661
|
+
|
|
662
|
+
### Pattern: Brutalist / Raw
|
|
663
|
+
|
|
664
|
+
```svelte
|
|
665
|
+
<BlocksProvider unstyled defaults={{
|
|
666
|
+
Card: {
|
|
667
|
+
slotClasses: {
|
|
668
|
+
base: 'border-4 border-black bg-white p-0 shadow-[8px_8px_0_black]'
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
Button: {
|
|
672
|
+
slotClasses: {
|
|
673
|
+
base: 'border-3 border-black bg-yellow-300 px-6 py-3 font-black uppercase tracking-widest hover:bg-black hover:text-yellow-300 active:translate-x-1 active:translate-y-1 active:shadow-none shadow-[4px_4px_0_black]'
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
Input: {
|
|
677
|
+
slotClasses: {
|
|
678
|
+
base: 'border-3 border-black bg-white font-mono text-lg',
|
|
679
|
+
label: 'font-black uppercase tracking-widest text-xs'
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
Badge: {
|
|
683
|
+
slotClasses: {
|
|
684
|
+
base: 'border-2 border-black bg-lime-400 font-black uppercase text-black rotate-[-2deg]'
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}}>
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### Pattern: Terminal / Hacker
|
|
691
|
+
|
|
692
|
+
```svelte
|
|
693
|
+
<BlocksProvider unstyled defaults={{
|
|
694
|
+
Card: {
|
|
695
|
+
slotClasses: {
|
|
696
|
+
base: 'border border-green-500/30 bg-black/90 font-mono text-green-400 rounded-none'
|
|
697
|
+
}
|
|
698
|
+
},
|
|
699
|
+
Button: {
|
|
700
|
+
slotClasses: {
|
|
701
|
+
base: 'border border-green-500/50 bg-green-500/10 font-mono text-green-400 rounded-none hover:bg-green-500/20 hover:text-green-300 px-4 py-2 uppercase tracking-wider text-xs'
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
Input: {
|
|
705
|
+
slotClasses: {
|
|
706
|
+
base: 'border border-green-500/30 bg-black font-mono text-green-400 rounded-none caret-green-400 placeholder:text-green-800',
|
|
707
|
+
label: 'font-mono text-green-600 uppercase text-[10px] tracking-[0.2em]'
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
Badge: {
|
|
711
|
+
slotClasses: {
|
|
712
|
+
base: 'border border-green-500/40 bg-green-500/10 font-mono text-green-400 text-[10px] uppercase rounded-none'
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}}>
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### Pattern: Soft / Organic
|
|
719
|
+
|
|
720
|
+
```svelte
|
|
721
|
+
<BlocksProvider defaults={{
|
|
722
|
+
Card: {
|
|
723
|
+
slotClasses: {
|
|
724
|
+
base: 'rounded-[2rem] bg-amber-50 border-0 shadow-sm'
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
Button: {
|
|
728
|
+
slotClasses: {
|
|
729
|
+
base: 'rounded-full bg-stone-800 text-amber-50 hover:bg-stone-700 px-8 font-light tracking-wide border-0 shadow-none'
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
Input: {
|
|
733
|
+
slotClasses: {
|
|
734
|
+
base: 'rounded-2xl border-stone-200 bg-stone-50 font-light',
|
|
735
|
+
label: 'font-light text-stone-500'
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}}>
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### Override Interaction Tokens
|
|
742
|
+
|
|
743
|
+
Change the feel of all components at once:
|
|
744
|
+
```css
|
|
745
|
+
:root {
|
|
746
|
+
--blocks-ease-confident: cubic-bezier(0.22, 1, 0.36, 1);
|
|
747
|
+
--blocks-duration-normal: 300ms;
|
|
748
|
+
--blocks-scale-hover: 1.00; /* disable scale on hover */
|
|
749
|
+
--blocks-scale-press: 0.98;
|
|
750
|
+
--blocks-focus-ring-width: 3px;
|
|
751
|
+
--blocks-focus-ring-color: oklch(0.7 0.15 var(--primary-hue, 240));
|
|
752
|
+
}
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### Per-Instance Override
|
|
756
|
+
|
|
757
|
+
For one-off customizations without changing the global style:
|
|
758
|
+
```svelte
|
|
759
|
+
<Card unstyled slotClasses={{ base: 'my-custom-card-class', header: 'my-header' }}>
|
|
760
|
+
...
|
|
761
|
+
</Card>
|
|
762
|
+
|
|
763
|
+
<Button class="rounded-full px-12 tracking-widest uppercase">
|
|
764
|
+
Custom Button
|
|
765
|
+
</Button>
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
`class` merges with defaults; `unstyled` + `slotClasses` replaces them entirely.
|
|
769
|
+
|
|
770
|
+
---
|
|
771
|
+
|
|
772
|
+
## Callback Naming Convention
|
|
773
|
+
|
|
774
|
+
Native DOM events: lowercase (`onclick`, `onfocus`)
|
|
775
|
+
Custom state callbacks: `on` + PascalCase (`onCheckedChange`, `onValueChange`, `onPageChange`)
|
|
776
|
+
|
|
777
|
+
Always pass the NEW STATE VALUE, not the raw event:
|
|
778
|
+
```svelte
|
|
779
|
+
<Checkbox onCheckedChange={(checked) => ...} />
|
|
780
|
+
<Menu onValueChange={(value) => ...} />
|
|
781
|
+
<Pagination onPageChange={(page) => ...} />
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
---
|
|
785
|
+
|
|
786
|
+
## i18n Integration
|
|
787
|
+
|
|
788
|
+
```svelte
|
|
789
|
+
<script>
|
|
790
|
+
import { T, LocaleSwitcher } from '@urbicon-ui/blocks';
|
|
791
|
+
</script>
|
|
792
|
+
|
|
793
|
+
<T key="blocks.button.loading" />
|
|
794
|
+
<LocaleSwitcher />
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
Supported locales: `en`, `de`. Components auto-translate their internal text.
|
|
798
|
+
|
|
799
|
+
---
|
|
800
|
+
|
|
801
|
+
## Common Patterns
|
|
802
|
+
|
|
803
|
+
### Form with validation
|
|
804
|
+
```svelte
|
|
805
|
+
<form onsubmit={handleSubmit}>
|
|
806
|
+
<Input label="Email" type="email" bind:value={email} error={errors.email} required />
|
|
807
|
+
<Input label="Password" type="password" bind:value={password} error={errors.password} required />
|
|
808
|
+
<Checkbox label="Remember me" bind:checked={remember} />
|
|
809
|
+
<Button intent="primary" type="submit" loading={submitting}>Sign In</Button>
|
|
810
|
+
</form>
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
### Confirmation dialog
|
|
814
|
+
```svelte
|
|
815
|
+
<Dialog bind:open={showConfirm} title="Delete Item?" intent="danger" size="sm">
|
|
816
|
+
<p>This action cannot be undone.</p>
|
|
817
|
+
{#snippet footer()}
|
|
818
|
+
<Button variant="ghost" onclick={() => showConfirm = false}>Cancel</Button>
|
|
819
|
+
<Button intent="danger" onclick={handleDelete} loading={deleting}>Delete</Button>
|
|
820
|
+
{/snippet}
|
|
821
|
+
</Dialog>
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
### Data display card
|
|
825
|
+
```svelte
|
|
826
|
+
<Card variant="outlined" padding="lg">
|
|
827
|
+
{#snippet header()}
|
|
828
|
+
<div class="flex items-center justify-between">
|
|
829
|
+
<h3 class="text-text-primary font-semibold">Revenue</h3>
|
|
830
|
+
<Badge intent="success" variant="soft">+12%</Badge>
|
|
831
|
+
</div>
|
|
832
|
+
{/snippet}
|
|
833
|
+
<p class="text-3xl font-bold text-text-primary">$48,200</p>
|
|
834
|
+
<p class="text-text-tertiary text-sm">vs. $43,000 last month</p>
|
|
835
|
+
</Card>
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
### Navigation tabs
|
|
839
|
+
```svelte
|
|
840
|
+
<Tab
|
|
841
|
+
tabs={[
|
|
842
|
+
{ label: 'Overview', value: 'overview' },
|
|
843
|
+
{ label: 'Analytics', value: 'analytics' },
|
|
844
|
+
{ label: 'Settings', value: 'settings', disabled: true }
|
|
845
|
+
]}
|
|
846
|
+
bind:value={activeTab}
|
|
847
|
+
variant="pills"
|
|
848
|
+
/>
|
|
849
|
+
|
|
850
|
+
{#if activeTab === 'overview'}
|
|
851
|
+
<OverviewPanel />
|
|
852
|
+
{:else if activeTab === 'analytics'}
|
|
853
|
+
<AnalyticsPanel />
|
|
854
|
+
{/if}
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
### Menu with custom items
|
|
858
|
+
```svelte
|
|
859
|
+
<Menu
|
|
860
|
+
items={users}
|
|
861
|
+
getItemLabel={(u) => u.name}
|
|
862
|
+
getItemValue={(u) => u.id}
|
|
863
|
+
placeholder="Assign to..."
|
|
864
|
+
bind:value={assignee}
|
|
865
|
+
>
|
|
866
|
+
{#snippet customItem(user, isSelected, toggle)}
|
|
867
|
+
<div class="flex items-center gap-2 p-2" onclick={toggle}>
|
|
868
|
+
<Avatar name={user.name} size="xs" />
|
|
869
|
+
<span>{user.name}</span>
|
|
870
|
+
</div>
|
|
871
|
+
{/snippet}
|
|
872
|
+
</Menu>
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
### Loading skeleton
|
|
876
|
+
```svelte
|
|
877
|
+
{#if loading}
|
|
878
|
+
<div class="flex items-center gap-3">
|
|
879
|
+
<Skeleton variant="circular" size="md" />
|
|
880
|
+
<div class="flex-1 flex flex-col gap-2">
|
|
881
|
+
<Skeleton variant="text" size="sm" />
|
|
882
|
+
<Skeleton variant="text" size="xs" class="w-2/3" />
|
|
883
|
+
</div>
|
|
884
|
+
</div>
|
|
885
|
+
{:else}
|
|
886
|
+
<UserCard {user} />
|
|
887
|
+
{/if}
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
### Breadcrumb navigation
|
|
891
|
+
```svelte
|
|
892
|
+
<Breadcrumb items={[
|
|
893
|
+
{ label: 'Dashboard', href: '/dashboard' },
|
|
894
|
+
{ label: 'Settings', href: '/dashboard/settings' },
|
|
895
|
+
{ label: 'Profile' }
|
|
896
|
+
]} size="sm" />
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
---
|
|
900
|
+
|
|
901
|
+
## Auth Setup
|
|
902
|
+
|
|
903
|
+
`@urbicon-ui/auth` is a zero-runtime-dependency auth system for SvelteKit (JWT sessions,
|
|
904
|
+
refresh-token rotation, passkeys, notifications). Server code imports from
|
|
905
|
+
`@urbicon-ui/auth/server`; client stores and components from `@urbicon-ui/auth`.
|
|
906
|
+
`createAuthHandle` is **mandatory** — it hydrates `locals.user`, guards routes, applies
|
|
907
|
+
the response security headers, and enforces CSRF; the handler factories alone do none of
|
|
908
|
+
that. Setup is staged: start with the dev quickstart, then layer on production hardening.
|
|
909
|
+
|
|
910
|
+
### Stage 1 — Dev quickstart (no database, no mail server)
|
|
911
|
+
|
|
912
|
+
In-memory adapter + console email transport. State is wiped on restart — **dev only**.
|
|
913
|
+
|
|
914
|
+
```typescript
|
|
915
|
+
// src/lib/server/auth-setup.ts
|
|
916
|
+
import { createAuthDeps } from '@urbicon-ui/auth/server';
|
|
917
|
+
import { createInMemoryRepos } from '@urbicon-ui/auth/server/adapters/in-memory';
|
|
918
|
+
import { createConsoleEmailTransport } from '@urbicon-ui/auth/server/email/console';
|
|
919
|
+
|
|
920
|
+
export const authDeps = createAuthDeps({
|
|
921
|
+
config: {
|
|
922
|
+
jwt: { secret: 'dev-secret-change-me', cookieSecure: false }, // cookieSecure:false = http dev
|
|
923
|
+
appUrl: 'http://localhost:5173', // required — builds email verification/reset links
|
|
924
|
+
routes: { afterLogin: '/', loginPage: '/auth/login' }
|
|
925
|
+
},
|
|
926
|
+
repos: createInMemoryRepos(),
|
|
927
|
+
email: createConsoleEmailTransport() // dev only — prints emails to the terminal
|
|
928
|
+
});
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
```typescript
|
|
932
|
+
// src/hooks.server.ts
|
|
933
|
+
import { createAuthHandle } from '@urbicon-ui/auth/server';
|
|
934
|
+
import { authDeps } from '$lib/server/auth-setup';
|
|
935
|
+
export const handle = createAuthHandle({ config: authDeps.config, repos: authDeps.repos });
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
**Note — Origin/CSRF is enforced only for requests routed _through_ the handle.** If you later
|
|
939
|
+
add a cross-origin, form-encoded endpoint (an OAuth 2.1 token endpoint, a webhook) _around_
|
|
940
|
+
`createAuthHandle`, SvelteKit's own built-in `csrf.checkOrigin` — which runs in the request
|
|
941
|
+
kernel before any hook, in production builds only — rejects it with `403 "Cross-site POST form
|
|
942
|
+
submissions are forbidden"` (most visibly the OAuth token endpoint: RFC 6749 mandates
|
|
943
|
+
`application/x-www-form-urlencoded` and its callers send no `Origin` header). Set
|
|
944
|
+
`kit.csrf: { trustedOrigins: ['*'] }` in `svelte.config.js` to disable the kernel check and let
|
|
945
|
+
this package's stricter `validateCsrf` be the single Origin gate — safe only when every
|
|
946
|
+
cookie-authenticated mutating route flows through `createAuthHandle` and the bypassed routes are
|
|
947
|
+
cookieless (bearer/PKCE/API-key). JSON endpoints (e.g. MCP JSON-RPC) are unaffected.
|
|
948
|
+
|
|
949
|
+
```typescript
|
|
950
|
+
// src/routes/api/auth/login/+server.ts — one file per handler
|
|
951
|
+
import { createLoginHandler } from '@urbicon-ui/auth/server';
|
|
952
|
+
import { authDeps } from '$lib/server/auth-setup';
|
|
953
|
+
export const { POST } = createLoginHandler(authDeps);
|
|
954
|
+
// Repeat with createLogoutHandler, createRegisterHandler, createForgotPasswordHandler,
|
|
955
|
+
// createResetPasswordHandler, createVerifyEmailHandler, createMeHandler.
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
```svelte
|
|
959
|
+
<!-- src/routes/auth/login/+page.svelte -->
|
|
960
|
+
<script>
|
|
961
|
+
import { LoginPage } from '@urbicon-ui/auth';
|
|
962
|
+
import { en } from '@urbicon-ui/auth/i18n/en';
|
|
963
|
+
import { goto } from '$app/navigation';
|
|
964
|
+
</script>
|
|
965
|
+
|
|
966
|
+
<LoginPage t={en} onSuccess={() => goto('/')} />
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
`createAuthDeps` applies secure brute-force defaults automatically (login rate-limit
|
|
970
|
+
5 / 15 min + lockout 5 / 15 min), so even the quickstart isn't an open door.
|
|
971
|
+
`cookieSecure: false` marks the config as dev and suppresses the production hardening
|
|
972
|
+
warnings (and HSTS). Registration is invitation-gated — seed one first:
|
|
973
|
+
`authDeps.repos.invitation.create({ email, role: 'USER', invitedById: 'seed' })`.
|
|
974
|
+
|
|
975
|
+
### Stage 2 — Production
|
|
976
|
+
|
|
977
|
+
Swap the two dev pieces (in-memory → Prisma, console → a real transport) and enable the
|
|
978
|
+
additive hardening layers. The hook and route stubs are unchanged.
|
|
979
|
+
|
|
980
|
+
```typescript
|
|
981
|
+
// src/lib/server/auth-setup.ts
|
|
982
|
+
import { createAuthDeps } from '@urbicon-ui/auth/server';
|
|
983
|
+
import { createPrismaRepos } from '@urbicon-ui/auth/server/adapters/prisma';
|
|
984
|
+
import { createLettermintTransport } from '@urbicon-ui/auth/server/email/lettermint';
|
|
985
|
+
import { env } from '$env/dynamic/private';
|
|
986
|
+
import { prisma } from './prisma';
|
|
987
|
+
|
|
988
|
+
type AppRole = 'ADMIN' | 'USER';
|
|
989
|
+
|
|
990
|
+
export const authDeps = createAuthDeps<AppRole>({
|
|
991
|
+
config: {
|
|
992
|
+
jwt: { secret: env.JWT_SECRET }, // cookieSecure defaults true → HTTPS + auto HSTS
|
|
993
|
+
appUrl: env.PUBLIC_APP_URL, // trusted base for email links — never request.url
|
|
994
|
+
csrf: { doubleSubmit: true }, // token layer on top of the always-on Origin check
|
|
995
|
+
refreshToken: { accessTokenTtl: '15m', refreshTokenTtl: '30d' }, // rotating refresh
|
|
996
|
+
rateLimit: { login: { windowMs: 900_000, max: 5 } },
|
|
997
|
+
lockout: { maxAttempts: 5, durationMinutes: 15 },
|
|
998
|
+
routes: { afterLogin: '/', loginPage: '/auth/login' }
|
|
999
|
+
},
|
|
1000
|
+
repos: createPrismaRepos<AppRole>(prisma), // pulls in the refreshToken adapter
|
|
1001
|
+
email: createLettermintTransport({ apiKey: env.LETTERMINT_KEY })
|
|
1002
|
+
});
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
Add a `refresh` route stub (`createRefreshHandler`) once rotation is on. With
|
|
1006
|
+
`csrf.doubleSubmit` enabled, the bundled stores/components send the CSRF header
|
|
1007
|
+
automatically; custom client fetches use `csrfFetch` from `@urbicon-ui/auth`. Copy the
|
|
1008
|
+
Prisma models from the package's `prisma/auth-schema.prisma`. Before going live: enforce
|
|
1009
|
+
HTTPS, use persistent stores for challenges / refresh tokens / rate limits when running
|
|
1010
|
+
more than one instance, tune `securityHeaders.csp` to your app, and keep `JWT_SECRET` in
|
|
1011
|
+
a secret store with a `keyId` + `previousSecrets` rotation runbook.
|
|
1012
|
+
|
|
1013
|
+
### Stage 3 — Advanced
|
|
1014
|
+
|
|
1015
|
+
- **Custom persistence adapter** (Drizzle, Kysely, raw SQL): implement the repository interface and prove it upholds the atomic claim/scope contract with `describeRepositoryConformance` from `@urbicon-ui/auth/server/adapters/conformance`.
|
|
1016
|
+
- **JWT key rotation**: set `jwt.keyId` + `jwt.previousSecrets` to roll the signing secret without invalidating live sessions.
|
|
1017
|
+
- **Passkeys (WebAuthn)**: wire `createPasskeyRegistrationOptionsHandler`, `createPasskeyRegistrationVerifyHandler`, `createPasskeyAuthenticationOptionsHandler`, `createPasskeyAuthenticationVerifyHandler` with a `webauthn: WebAuthnConfig` (persistent `challengeStore` at >1 instance; `requireUserVerification: true` to enforce UV), then add `<PasskeyManager>` and `<LoginPage mode="both" passkeyApiPath="/api/auth/passkey">`.
|
|
1018
|
+
- **Notifications + Web Push**: register events with `createNotificationRegistry` server-side; listen with `<NotificationListener>` + `<NotificationCenter>` client-side. `notification.url` is untrusted at navigation time — validate/allow-list it before `goto()`. Mark-read/delete route handlers must scope `userId` from `locals.user`, never the request body (IDOR).
|
|
1019
|
+
- **Federated identity / SSO**: on the roadmap (`createFederatedAuthHandle()`).
|