@odla-ai/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +88 -0
  2. package/css/components/buttons.css +118 -0
  3. package/css/components/cards.css +66 -0
  4. package/css/components/chart.css +24 -0
  5. package/css/components/chat.css +167 -0
  6. package/css/components/feedback.css +77 -0
  7. package/css/components/forms.css +132 -0
  8. package/css/components/nav.css +96 -0
  9. package/css/components/table.css +34 -0
  10. package/css/tokens.css +138 -0
  11. package/dist/index.d.ts +50 -0
  12. package/dist/index.js +72 -0
  13. package/dist/index.js.map +1 -0
  14. package/fonts/editorial.css +46 -0
  15. package/fonts/fira-code.css +2 -0
  16. package/fonts/lora.css +2 -0
  17. package/fonts/plex.css +4 -0
  18. package/fonts/satoshi.css +2 -0
  19. package/fonts/system.css +3 -0
  20. package/index.css +14 -0
  21. package/js/canvas.js +113 -0
  22. package/js/index.js +24 -0
  23. package/js/palette.js +37 -0
  24. package/js/theme.js +51 -0
  25. package/js/tokens.js +104 -0
  26. package/llms.txt +201 -0
  27. package/odla-ui.css +863 -0
  28. package/package.json +73 -0
  29. package/themes/chalk/styles.css +720 -0
  30. package/themes/chalk/theme.json +6 -0
  31. package/themes/chalk/ui.css +51 -0
  32. package/themes/clay/styles.css +726 -0
  33. package/themes/clay/theme.json +6 -0
  34. package/themes/clay/ui.css +40 -0
  35. package/themes/juniper/styles.css +660 -0
  36. package/themes/juniper/theme.json +6 -0
  37. package/themes/juniper/ui.css +50 -0
  38. package/themes/paper/styles.css +129 -0
  39. package/themes/paper/theme.json +6 -0
  40. package/themes/paper/ui.css +30 -0
  41. package/themes/salt/styles.css +728 -0
  42. package/themes/salt/theme.json +6 -0
  43. package/themes/salt/ui.css +48 -0
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @odla-ai/ui
2
+
3
+ The odla design system: one semantic token contract (`--ui-*`), five themes,
4
+ class-scoped component CSS, canvas chart helpers, and optional React/Preact
5
+ form components. CSS-first — every odla surface can consume it, from
6
+ zero-build static sites to Vite React apps.
7
+
8
+ - **Tokens**: colors, text tiers, status, fonts, spacing, radius, focus,
9
+ chart and chat roles. Defaults ship at zero specificity (`:where()`), so
10
+ themes and app overrides always win.
11
+ - **Themes**: `paper` (the odla Studio's warm-paper dashboard look),
12
+ `juniper`, `salt`, `chalk`, `clay` (the blog themes — `@odla-ai/blog`
13
+ resolves them from this package). Each theme = `styles.css` (its own
14
+ palette) + `ui.css` (mapping onto the contract) + `theme.json`.
15
+ - **Components (CSS)**: buttons/segmented/tabs/pills, form fields with
16
+ validation states, cards/panels/badges, data tables, app-shell layout +
17
+ drawer, toasts/banners/dropzones, chat surfaces (message bubbles,
18
+ tool-call cards, thinking panels, composer — shaped by `@odla-ai/ai`'s
19
+ streaming contract), chart containers.
20
+ - **JS helpers** (buildless ESM): light/dark toggle with no-flash snippet,
21
+ a `--ui-*` palette reader for canvas, and chart primitives
22
+ (`fitCanvas`, `niceTicks`, `bandFill`, `drawTip`, …).
23
+ - **`/components`**: `Button`, `Field`, `Input`, `Select`, `Checkbox`,
24
+ `Textarea` — authored once against the `react` API, runs on React 18/19
25
+ natively and on Preact via `preact/compat` aliasing (Vite alias or a
26
+ browser import map for buildless islands).
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ npm install @odla-ai/ui
32
+ ```
33
+
34
+ ## 60-second start (Vite + React)
35
+
36
+ ```js
37
+ // main.tsx
38
+ import "@odla-ai/ui/fonts/plex.css";
39
+ import "@odla-ai/ui/themes/paper/styles.css";
40
+ import "@odla-ai/ui/themes/paper/ui.css";
41
+ import "@odla-ai/ui/index.css";
42
+ ```
43
+
44
+ ```tsx
45
+ import { Button, Field, Input } from "@odla-ai/ui/components";
46
+
47
+ <div className="card">
48
+ <h4>New note</h4>
49
+ <Field label="Title" htmlFor="t">
50
+ <Input id="t" placeholder="…" />
51
+ </Field>
52
+ <div className="row">
53
+ <Button>Save</Button>
54
+ <Button variant="secondary">Cancel</Button>
55
+ </div>
56
+ </div>
57
+ ```
58
+
59
+ Static sites copy `odla-ui.css` (pre-flattened, no imports) plus a theme's
60
+ `styles.css` + `ui.css` and use the same classes in plain HTML.
61
+
62
+ ## Themes
63
+
64
+ | Theme | Look | Fonts |
65
+ | --- | --- | --- |
66
+ | `paper` | Warm paper dashboard (the odla Studio) | IBM Plex Sans/Mono (`fonts/plex.css`) |
67
+ | `juniper` | Quiet long-form reading, violet accents | Lora + Gill Sans stack |
68
+ | `salt` | Warm editorial cream/moss/rust | Cormorant Garamond + Satoshi |
69
+ | `chalk` | Chalkboard on graph paper | Cormorant + Spectral + JetBrains Mono |
70
+ | `clay` | Warm light editorial, terracotta | System stacks only (zero font requests) |
71
+
72
+ Dark mode: every theme defines dark tokens on `[data-theme="dark"]` and the
73
+ `prefers-color-scheme` fallback (kept byte-identical — tested). Toggle with
74
+ `toggleTheme()` from the JS helpers.
75
+
76
+ The full contract — token tables, class inventory, recipes, invariants —
77
+ lives in [llms.txt](./llms.txt) (bundled for agents, good for humans too).
78
+
79
+ ## Development
80
+
81
+ ```bash
82
+ npm test -w @odla-ai/ui # builds dist, then node --test
83
+ npm run gen:css -w @odla-ai/ui # refresh odla-ui.css after editing css/
84
+ ```
85
+
86
+ Tests enforce: theme dark-block identity across all five themes, full
87
+ `--ui-*` coverage per theme, class-scoped selectors (no bare elements) in
88
+ component sheets, `odla-ui.css` freshness, and component render output.
@@ -0,0 +1,118 @@
1
+ /* @odla-ai/ui — buttons & controls
2
+ Class-scoped by design: no bare-element rules, so these sheets coexist
3
+ with themes (e.g. blog themes) that style bare elements themselves.
4
+ `.btn` alone renders the primary variant; modifiers chain (.btn.ghost). */
5
+
6
+ .btn {
7
+ font: inherit;
8
+ font-weight: 600;
9
+ background: var(--ui-accent);
10
+ color: var(--ui-on-accent);
11
+ border: 0;
12
+ padding: 7px 13px;
13
+ border-radius: var(--ui-radius-md);
14
+ cursor: pointer;
15
+ display: inline-flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ gap: 6px;
19
+ text-decoration: none;
20
+ }
21
+ .btn:hover { background: var(--ui-accent-strong); }
22
+ .btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }
23
+ .btn:disabled { opacity: 0.45; cursor: default; }
24
+
25
+ .btn.secondary {
26
+ background: var(--ui-surface);
27
+ color: var(--ui-text);
28
+ border: 1px solid var(--ui-border);
29
+ }
30
+ .btn.secondary:hover { background: var(--ui-surface-2); }
31
+
32
+ .btn.ghost {
33
+ background: transparent;
34
+ color: var(--ui-accent);
35
+ border: 0;
36
+ }
37
+ .btn.ghost:hover { background: var(--ui-accent-soft); }
38
+
39
+ .btn.danger { background: var(--ui-danger); color: var(--ui-on-accent); }
40
+ .btn.danger:hover { background: color-mix(in srgb, var(--ui-danger) 82%, var(--ui-text)); }
41
+
42
+ .btn.mini { padding: 3px 9px; font-size: var(--ui-text-sm); }
43
+
44
+ /* Segmented control: a strip of buttons, one active (.on). */
45
+ .seg {
46
+ display: inline-flex;
47
+ border: 1px solid var(--ui-border);
48
+ border-radius: var(--ui-radius-md);
49
+ background: var(--ui-surface);
50
+ overflow: hidden;
51
+ }
52
+ .seg button {
53
+ font: inherit;
54
+ font-size: var(--ui-text-md);
55
+ background: transparent;
56
+ color: var(--ui-text-muted);
57
+ border: 0;
58
+ padding: 6px 12px;
59
+ cursor: pointer;
60
+ }
61
+ .seg button + button { border-left: 1px solid var(--ui-border); }
62
+ .seg button:hover { color: var(--ui-text); }
63
+ .seg button.on,
64
+ .seg button[aria-pressed="true"] {
65
+ background: var(--ui-accent-soft);
66
+ color: var(--ui-accent-strong);
67
+ font-weight: 600;
68
+ }
69
+
70
+ /* Underlined tab bar; the active tab carries .on (or aria-selected). */
71
+ .tabs {
72
+ display: flex;
73
+ gap: var(--ui-space-4);
74
+ border-bottom: 1px solid var(--ui-border);
75
+ }
76
+ .tabs button {
77
+ font: inherit;
78
+ font-size: var(--ui-text-md);
79
+ background: transparent;
80
+ color: var(--ui-text-muted);
81
+ border: 0;
82
+ border-bottom: 2px solid transparent;
83
+ margin-bottom: -1px;
84
+ padding: 8px 2px;
85
+ cursor: pointer;
86
+ }
87
+ .tabs button:hover { color: var(--ui-text); }
88
+ .tabs button.on,
89
+ .tabs button[aria-selected="true"] {
90
+ color: var(--ui-text);
91
+ border-bottom-color: var(--ui-accent);
92
+ font-weight: 600;
93
+ }
94
+
95
+ /* Status/metadata pill: quiet, mono, rounded. */
96
+ .pill {
97
+ font-family: var(--ui-font-mono);
98
+ font-size: var(--ui-text-sm);
99
+ background: var(--ui-surface-2);
100
+ border: 1px solid var(--ui-border);
101
+ padding: 3px 9px;
102
+ border-radius: var(--ui-radius-pill);
103
+ color: var(--ui-text-muted);
104
+ }
105
+
106
+ /* Interactive chip (link chips, removable filters). */
107
+ .chip {
108
+ font-family: var(--ui-font-mono);
109
+ font-size: var(--ui-text-xs);
110
+ padding: 2px 7px;
111
+ margin: 1px 3px 1px 0;
112
+ border-radius: 6px;
113
+ background: var(--ui-accent-soft);
114
+ border: 1px solid var(--ui-border);
115
+ color: var(--ui-accent-strong);
116
+ cursor: pointer;
117
+ }
118
+ .chip:hover { border-color: var(--ui-accent); }
@@ -0,0 +1,66 @@
1
+ /* @odla-ai/ui — cards, panels, badges */
2
+
3
+ .card {
4
+ background: var(--ui-surface);
5
+ border: 1px solid var(--ui-border);
6
+ border-radius: var(--ui-radius-lg);
7
+ padding: 14px;
8
+ box-shadow: var(--ui-shadow);
9
+ }
10
+ .card h4 {
11
+ margin: 0 0 var(--ui-space-2);
12
+ color: var(--ui-text-muted);
13
+ font-weight: 600;
14
+ font-size: var(--ui-text-xs);
15
+ text-transform: uppercase;
16
+ letter-spacing: var(--ui-tracking-caps);
17
+ }
18
+
19
+ /* KPI card grid. */
20
+ .cards {
21
+ display: grid;
22
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
23
+ gap: var(--ui-space-3);
24
+ margin: var(--ui-space-4) 0;
25
+ }
26
+ .metric { font-size: 24px; font-weight: 700; font-variant-numeric: tabular-nums; }
27
+
28
+ .panel {
29
+ background: var(--ui-surface);
30
+ border: 1px solid var(--ui-border);
31
+ border-radius: var(--ui-radius-lg);
32
+ padding: var(--ui-space-4);
33
+ margin: var(--ui-space-4) 0;
34
+ box-shadow: var(--ui-shadow);
35
+ }
36
+ .panel h3 { margin: 0 0 var(--ui-space-3); font-weight: 700; }
37
+
38
+ /* Status badge: neutral by default, status variants chain. */
39
+ .badge {
40
+ display: inline-block;
41
+ font-family: var(--ui-font-mono);
42
+ font-size: var(--ui-text-xs);
43
+ font-weight: 600;
44
+ text-transform: uppercase;
45
+ letter-spacing: 0.04em;
46
+ padding: 2px 8px;
47
+ border-radius: var(--ui-radius-pill);
48
+ background: var(--ui-surface-2);
49
+ border: 1px solid var(--ui-border);
50
+ color: var(--ui-text-muted);
51
+ }
52
+ .badge.good { background: var(--ui-good-soft); border-color: var(--ui-good); color: var(--ui-good); }
53
+ .badge.warn { background: var(--ui-warn-soft); border-color: var(--ui-warn); color: var(--ui-warn); }
54
+ .badge.danger { background: var(--ui-danger-soft); border-color: var(--ui-danger); color: var(--ui-danger); }
55
+ .badge.accent { background: var(--ui-accent-soft); border-color: var(--ui-accent); color: var(--ui-accent-strong); }
56
+
57
+ /* Pass/fail verdict pill (permission inspectors, test results). */
58
+ .verdict {
59
+ font-family: var(--ui-font-mono);
60
+ font-size: var(--ui-text-sm);
61
+ font-weight: 600;
62
+ padding: 3px 10px;
63
+ border-radius: var(--ui-radius-pill);
64
+ }
65
+ .verdict-pass { background: var(--ui-good-soft); color: var(--ui-good); border: 1px solid var(--ui-good); }
66
+ .verdict-fail { background: var(--ui-danger-soft); color: var(--ui-danger); border: 1px solid var(--ui-danger); }
@@ -0,0 +1,24 @@
1
+ /* @odla-ai/ui — chart containers
2
+ Rendering happens on <canvas> via js/canvas.js; colors come from the
3
+ --ui-chart-* tokens read with js/palette.js readPalette(). */
4
+
5
+ .chart { position: relative; }
6
+ .chart canvas { display: block; width: 100%; }
7
+
8
+ .chart-legend {
9
+ display: flex;
10
+ flex-wrap: wrap;
11
+ gap: var(--ui-space-3);
12
+ font-family: var(--ui-font-mono);
13
+ font-size: var(--ui-text-xs);
14
+ color: var(--ui-text-muted);
15
+ margin-top: var(--ui-space-2);
16
+ }
17
+ .chart-key { display: inline-flex; align-items: center; gap: 5px; }
18
+ .chart-key i {
19
+ display: inline-block;
20
+ width: 10px;
21
+ height: 10px;
22
+ border-radius: var(--ui-radius-sm);
23
+ background: currentColor;
24
+ }
@@ -0,0 +1,167 @@
1
+ /* @odla-ai/ui — chat surfaces
2
+ Shaped by the @odla-ai/ai content-block contract: text, tool_use,
3
+ tool_result, thinking blocks; streaming deltas; usage; typed errors. */
4
+
5
+ .chat {
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: var(--ui-space-4);
9
+ max-width: 720px;
10
+ margin: 0 auto;
11
+ }
12
+
13
+ .chat-msg { display: flex; flex-direction: column; align-items: flex-start; }
14
+ .chat-msg.user { align-items: flex-end; }
15
+
16
+ .chat-bubble {
17
+ max-width: 88%;
18
+ padding: 10px 14px;
19
+ border-radius: var(--ui-radius-lg);
20
+ font-size: var(--ui-text-lg);
21
+ line-height: 1.55;
22
+ overflow-wrap: break-word;
23
+ }
24
+ .chat-msg.user .chat-bubble {
25
+ background: var(--ui-chat-user-bg);
26
+ color: var(--ui-chat-user-text);
27
+ }
28
+ .chat-msg.assistant .chat-bubble {
29
+ background: var(--ui-chat-assistant-bg);
30
+ border: 1px solid var(--ui-border);
31
+ }
32
+ .chat-bubble pre {
33
+ font-family: var(--ui-font-mono);
34
+ font-size: var(--ui-text-sm);
35
+ background: var(--ui-code-bg);
36
+ color: var(--ui-code-text);
37
+ border-radius: var(--ui-radius-sm);
38
+ padding: 10px 12px;
39
+ overflow-x: auto;
40
+ }
41
+
42
+ /* Collapsible reasoning panel (ThinkingBlock) — use a <details>. */
43
+ .chat-thinking {
44
+ align-self: stretch;
45
+ background: var(--ui-chat-thinking-bg);
46
+ color: var(--ui-chat-thinking-text);
47
+ border: 1px solid var(--ui-border);
48
+ border-radius: var(--ui-radius-md);
49
+ font-size: var(--ui-text-sm);
50
+ padding: 6px 10px;
51
+ }
52
+ .chat-thinking summary {
53
+ cursor: pointer;
54
+ font-family: var(--ui-font-mono);
55
+ font-size: var(--ui-text-xs);
56
+ text-transform: uppercase;
57
+ letter-spacing: var(--ui-tracking-caps);
58
+ }
59
+ .chat-thinking[open] summary { margin-bottom: 6px; }
60
+
61
+ /* Tool-call card (ToolUseBlock): name + streamed args. */
62
+ .chat-tool {
63
+ align-self: stretch;
64
+ border: 1px solid var(--ui-border);
65
+ border-left: 3px solid var(--ui-chat-tool-accent);
66
+ border-radius: var(--ui-radius-md);
67
+ background: var(--ui-surface);
68
+ padding: 8px 12px;
69
+ }
70
+ .chat-tool-name {
71
+ font-family: var(--ui-font-mono);
72
+ font-size: var(--ui-text-sm);
73
+ font-weight: 600;
74
+ color: var(--ui-chat-tool-accent);
75
+ }
76
+ .chat-tool-args {
77
+ font-family: var(--ui-font-mono);
78
+ font-size: var(--ui-text-sm);
79
+ background: var(--ui-code-bg);
80
+ color: var(--ui-code-text);
81
+ border-radius: var(--ui-radius-sm);
82
+ padding: 8px 10px;
83
+ margin: 6px 0 0;
84
+ overflow-x: auto;
85
+ white-space: pre-wrap;
86
+ }
87
+
88
+ /* Tool-result card (ToolResultBlock); .error for isError results. */
89
+ .chat-result {
90
+ align-self: stretch;
91
+ border: 1px solid var(--ui-border);
92
+ border-left: 3px solid var(--ui-good);
93
+ border-radius: var(--ui-radius-md);
94
+ background: var(--ui-surface-2);
95
+ padding: 8px 12px;
96
+ font-size: var(--ui-text-md);
97
+ }
98
+ .chat-result.error {
99
+ border-left-color: var(--ui-danger);
100
+ background: var(--ui-danger-soft);
101
+ color: var(--ui-danger);
102
+ }
103
+
104
+ /* Streaming caret appended during text/thinking deltas. */
105
+ .chat-cursor {
106
+ display: inline-block;
107
+ width: 0.55em;
108
+ height: 1.1em;
109
+ background: var(--ui-accent);
110
+ vertical-align: text-bottom;
111
+ animation: ui-blink 1s steps(1) infinite;
112
+ }
113
+ @keyframes ui-blink {
114
+ 50% { opacity: 0; }
115
+ }
116
+
117
+ /* Composer bar. */
118
+ .chat-composer {
119
+ position: sticky;
120
+ bottom: 0;
121
+ display: flex;
122
+ gap: var(--ui-space-2);
123
+ align-items: flex-end;
124
+ background: var(--ui-surface);
125
+ border-top: 1px solid var(--ui-border);
126
+ padding: var(--ui-space-3);
127
+ }
128
+ .chat-composer .textarea { flex: 1; min-height: 40px; max-height: 200px; }
129
+
130
+ /* Token/cost footer (OracleUsage). */
131
+ .chat-usage {
132
+ font-family: var(--ui-font-mono);
133
+ font-size: var(--ui-text-xs);
134
+ color: var(--ui-text-faint);
135
+ text-align: right;
136
+ }
137
+
138
+ /* Error/stop-reason notices (rate_limit, context_window, refusal, …). */
139
+ .chat-banner {
140
+ align-self: stretch;
141
+ padding: 8px 12px;
142
+ border-radius: var(--ui-radius-md);
143
+ border: 1px solid var(--ui-border);
144
+ background: var(--ui-surface-2);
145
+ font-size: var(--ui-text-md);
146
+ }
147
+ .chat-banner.warn { background: var(--ui-warn-soft); border-color: var(--ui-warn); color: var(--ui-warn); }
148
+ .chat-banner.error { background: var(--ui-danger-soft); border-color: var(--ui-danger); color: var(--ui-danger); }
149
+
150
+ /* Attachment chip (image/document blocks in history or composer). */
151
+ .chat-attachment {
152
+ display: inline-flex;
153
+ align-items: center;
154
+ gap: 6px;
155
+ font-family: var(--ui-font-mono);
156
+ font-size: var(--ui-text-xs);
157
+ border: 1px solid var(--ui-border);
158
+ border-radius: var(--ui-radius-sm);
159
+ background: var(--ui-surface-2);
160
+ color: var(--ui-text-muted);
161
+ padding: 3px 8px;
162
+ }
163
+ .chat-attachment img {
164
+ max-height: 48px;
165
+ border-radius: var(--ui-radius-sm);
166
+ display: block;
167
+ }
@@ -0,0 +1,77 @@
1
+ /* @odla-ai/ui — toasts, banners, dropzones, activity indicators */
2
+
3
+ .toast {
4
+ position: fixed;
5
+ bottom: 20px;
6
+ right: 20px;
7
+ background: var(--ui-text);
8
+ color: var(--ui-bg);
9
+ padding: 10px 14px;
10
+ border-radius: 8px;
11
+ font-size: var(--ui-text-md);
12
+ box-shadow: var(--ui-shadow);
13
+ z-index: 1100;
14
+ }
15
+ .toast.good { border-left: 3px solid var(--ui-good); }
16
+ .toast.warn { border-left: 3px solid var(--ui-warn); }
17
+ .toast.danger { border-left: 3px solid var(--ui-danger); }
18
+
19
+ /* Inline notice bar. */
20
+ .banner {
21
+ padding: 10px 14px;
22
+ border: 1px solid var(--ui-border);
23
+ border-radius: var(--ui-radius-md);
24
+ background: var(--ui-surface-2);
25
+ font-size: var(--ui-text-md);
26
+ margin: var(--ui-space-3) 0;
27
+ }
28
+ .banner.good { background: var(--ui-good-soft); border-color: var(--ui-good); color: var(--ui-good); }
29
+ .banner.warn { background: var(--ui-warn-soft); border-color: var(--ui-warn); color: var(--ui-warn); }
30
+ .banner.error { background: var(--ui-danger-soft); border-color: var(--ui-danger); color: var(--ui-danger); }
31
+
32
+ /* File-drop target; add .drag while a file hovers. */
33
+ .dropzone {
34
+ border: 1.5px dashed var(--ui-border);
35
+ border-radius: 8px;
36
+ padding: 18px;
37
+ text-align: center;
38
+ color: var(--ui-text-muted);
39
+ font-size: var(--ui-text-md);
40
+ background: var(--ui-surface-2);
41
+ margin-bottom: 14px;
42
+ }
43
+ .dropzone.drag {
44
+ border-color: var(--ui-accent);
45
+ background: var(--ui-accent-soft);
46
+ color: var(--ui-accent-strong);
47
+ }
48
+
49
+ /* Live-activity pulse dot. */
50
+ .live-dot {
51
+ display: inline-block;
52
+ width: 8px;
53
+ height: 8px;
54
+ border-radius: 50%;
55
+ background: var(--ui-good);
56
+ margin-right: 6px;
57
+ animation: ui-pulse 1.4s infinite;
58
+ }
59
+ @keyframes ui-pulse {
60
+ 0%, 100% { opacity: 1; }
61
+ 50% { opacity: 0.3; }
62
+ }
63
+
64
+ /* Border spinner. */
65
+ .spinner {
66
+ display: inline-block;
67
+ width: 16px;
68
+ height: 16px;
69
+ border: 2px solid var(--ui-border);
70
+ border-top-color: var(--ui-accent);
71
+ border-radius: 50%;
72
+ animation: ui-spin 0.8s linear infinite;
73
+ vertical-align: middle;
74
+ }
75
+ @keyframes ui-spin {
76
+ to { transform: rotate(360deg); }
77
+ }
@@ -0,0 +1,132 @@
1
+ /* @odla-ai/ui — form fields
2
+ .input/.select/.textarea are the class equivalents of themed bare
3
+ elements; .field wraps a label + control + error/hint. */
4
+
5
+ .input,
6
+ .select,
7
+ .textarea {
8
+ font: inherit;
9
+ font-size: var(--ui-text-md);
10
+ background: var(--ui-surface);
11
+ color: var(--ui-text);
12
+ border: 1px solid var(--ui-border);
13
+ border-radius: var(--ui-radius-md);
14
+ padding: 8px 10px;
15
+ width: 100%;
16
+ }
17
+ .input:focus,
18
+ .select:focus,
19
+ .textarea:focus {
20
+ outline: none;
21
+ border-color: var(--ui-accent);
22
+ box-shadow: var(--ui-focus);
23
+ }
24
+ .input::placeholder,
25
+ .textarea::placeholder { color: var(--ui-text-faint); }
26
+ .input:disabled,
27
+ .select:disabled,
28
+ .textarea:disabled { opacity: 0.55; cursor: default; }
29
+
30
+ .textarea {
31
+ font-family: var(--ui-font-mono);
32
+ resize: vertical;
33
+ min-height: 72px;
34
+ }
35
+ .textarea.prose-input { font-family: inherit; }
36
+
37
+ .input.invalid,
38
+ .select.invalid,
39
+ .textarea.invalid,
40
+ .input[aria-invalid="true"],
41
+ .select[aria-invalid="true"],
42
+ .textarea[aria-invalid="true"] {
43
+ border-color: var(--ui-danger);
44
+ }
45
+ .input.invalid:focus,
46
+ .select.invalid:focus,
47
+ .textarea.invalid:focus,
48
+ .input[aria-invalid="true"]:focus,
49
+ .select[aria-invalid="true"]:focus,
50
+ .textarea[aria-invalid="true"]:focus {
51
+ box-shadow: 0 0 0 3px var(--ui-danger-soft);
52
+ }
53
+
54
+ /* Field wrapper: label above control, error/hint below. */
55
+ .field { margin: 0 0 var(--ui-space-4); }
56
+ .field-label {
57
+ display: block;
58
+ font-size: var(--ui-text-xs);
59
+ text-transform: uppercase;
60
+ letter-spacing: var(--ui-tracking-caps);
61
+ color: var(--ui-text-muted);
62
+ margin: 0 0 4px;
63
+ }
64
+ .field-error { color: var(--ui-danger); font-size: var(--ui-text-md); margin: 4px 0 0; }
65
+ .field-hint { color: var(--ui-text-faint); font-size: var(--ui-text-sm); margin: 4px 0 0; }
66
+
67
+ /* Checkbox/radio with label. */
68
+ .check {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: var(--ui-space-2);
72
+ cursor: pointer;
73
+ font-size: var(--ui-text-md);
74
+ }
75
+ .check input {
76
+ accent-color: var(--ui-accent);
77
+ width: 15px;
78
+ height: 15px;
79
+ margin: 0;
80
+ flex: 0 0 auto;
81
+ }
82
+
83
+ /* Styled range slider. */
84
+ .range {
85
+ -webkit-appearance: none;
86
+ appearance: none;
87
+ width: 100%;
88
+ height: 4px;
89
+ border-radius: var(--ui-radius-pill);
90
+ background: var(--ui-border);
91
+ outline: none;
92
+ }
93
+ .range::-webkit-slider-thumb {
94
+ -webkit-appearance: none;
95
+ appearance: none;
96
+ width: 14px;
97
+ height: 14px;
98
+ border-radius: 50%;
99
+ background: var(--ui-accent);
100
+ border: 2px solid var(--ui-surface);
101
+ cursor: pointer;
102
+ }
103
+ .range::-moz-range-thumb {
104
+ width: 14px;
105
+ height: 14px;
106
+ border-radius: 50%;
107
+ background: var(--ui-accent);
108
+ border: 2px solid var(--ui-surface);
109
+ cursor: pointer;
110
+ }
111
+ .range:focus-visible { box-shadow: var(--ui-focus); }
112
+
113
+ /* Grouped form area (e.g. schema-aware add-row forms). */
114
+ .form-well {
115
+ padding: var(--ui-space-3);
116
+ background: var(--ui-surface-2);
117
+ border: 1px solid var(--ui-border);
118
+ border-radius: 8px;
119
+ margin-bottom: var(--ui-space-3);
120
+ }
121
+ .form-grid {
122
+ display: grid;
123
+ grid-template-columns: 180px 1fr;
124
+ gap: 8px 14px;
125
+ align-items: center;
126
+ }
127
+ .form-grid label {
128
+ font-family: var(--ui-font-mono);
129
+ font-size: var(--ui-text-sm);
130
+ color: var(--ui-text);
131
+ }
132
+ .form-grid .type { color: var(--ui-text-faint); font-size: var(--ui-text-xs); }