@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.
- package/README.md +88 -0
- package/css/components/buttons.css +118 -0
- package/css/components/cards.css +66 -0
- package/css/components/chart.css +24 -0
- package/css/components/chat.css +167 -0
- package/css/components/feedback.css +77 -0
- package/css/components/forms.css +132 -0
- package/css/components/nav.css +96 -0
- package/css/components/table.css +34 -0
- package/css/tokens.css +138 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/fonts/editorial.css +46 -0
- package/fonts/fira-code.css +2 -0
- package/fonts/lora.css +2 -0
- package/fonts/plex.css +4 -0
- package/fonts/satoshi.css +2 -0
- package/fonts/system.css +3 -0
- package/index.css +14 -0
- package/js/canvas.js +113 -0
- package/js/index.js +24 -0
- package/js/palette.js +37 -0
- package/js/theme.js +51 -0
- package/js/tokens.js +104 -0
- package/llms.txt +201 -0
- package/odla-ui.css +863 -0
- package/package.json +73 -0
- package/themes/chalk/styles.css +720 -0
- package/themes/chalk/theme.json +6 -0
- package/themes/chalk/ui.css +51 -0
- package/themes/clay/styles.css +726 -0
- package/themes/clay/theme.json +6 -0
- package/themes/clay/ui.css +40 -0
- package/themes/juniper/styles.css +660 -0
- package/themes/juniper/theme.json +6 -0
- package/themes/juniper/ui.css +50 -0
- package/themes/paper/styles.css +129 -0
- package/themes/paper/theme.json +6 -0
- package/themes/paper/ui.css +30 -0
- package/themes/salt/styles.css +728 -0
- package/themes/salt/theme.json +6 -0
- package/themes/salt/ui.css +48 -0
package/js/theme.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Light/dark theme helpers. The convention (shared with @odla-ai/blog):
|
|
2
|
+
// the preference lives in localStorage under "theme" and is applied as a
|
|
3
|
+
// data-theme attribute on <html>; with no attribute, prefers-color-scheme
|
|
4
|
+
// decides. Stylesheets pair every [data-theme="dark"] block with an
|
|
5
|
+
// identical @media (prefers-color-scheme: dark) block.
|
|
6
|
+
|
|
7
|
+
// Inline this string in a <script> tag inside <head>, BEFORE the first
|
|
8
|
+
// paint, so a stored preference never flashes the wrong mode.
|
|
9
|
+
export const THEME_NO_FLASH = `(function(){try{var t=localStorage.getItem("theme");if(t)document.documentElement.setAttribute("data-theme",t)}catch(e){}})();`;
|
|
10
|
+
|
|
11
|
+
// Apply the stored preference at runtime (same effect as THEME_NO_FLASH,
|
|
12
|
+
// for late-loaded module code).
|
|
13
|
+
export function initTheme(storageKey = "theme") {
|
|
14
|
+
try {
|
|
15
|
+
const stored = localStorage.getItem(storageKey);
|
|
16
|
+
if (stored) document.documentElement.setAttribute("data-theme", stored);
|
|
17
|
+
} catch {
|
|
18
|
+
/* storage unavailable */
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The mode currently in effect ("light" | "dark"), attribute else system.
|
|
23
|
+
export function currentTheme() {
|
|
24
|
+
const attr = document.documentElement.getAttribute("data-theme");
|
|
25
|
+
if (attr === "dark" || attr === "light") return attr;
|
|
26
|
+
return matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Flip the mode, persist it, and return the new mode. Canvas callers must
|
|
30
|
+
// re-read their palette afterwards (see js/palette.js).
|
|
31
|
+
export function toggleTheme(storageKey = "theme") {
|
|
32
|
+
const next = currentTheme() === "dark" ? "light" : "dark";
|
|
33
|
+
document.documentElement.setAttribute("data-theme", next);
|
|
34
|
+
try {
|
|
35
|
+
localStorage.setItem(storageKey, next);
|
|
36
|
+
} catch {
|
|
37
|
+
/* storage unavailable */
|
|
38
|
+
}
|
|
39
|
+
return next;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Convenience: wire a click target (e.g. a .btn or .theme-toggle) to toggle.
|
|
43
|
+
// The callback receives the new mode — redraw canvases there.
|
|
44
|
+
export function bindThemeToggle(el, { storageKey = "theme", onChange } = {}) {
|
|
45
|
+
const handler = () => {
|
|
46
|
+
const mode = toggleTheme(storageKey);
|
|
47
|
+
if (onChange) onChange(mode);
|
|
48
|
+
};
|
|
49
|
+
el.addEventListener("click", handler);
|
|
50
|
+
return () => el.removeEventListener("click", handler);
|
|
51
|
+
}
|
package/js/tokens.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// The @odla-ai/ui token contract as data — the single source of truth shared
|
|
2
|
+
// by the tests, the palette reader, and the docs. Names here are CSS custom
|
|
3
|
+
// properties; see css/tokens.css for defaults and themes/*/ui.css for the
|
|
4
|
+
// per-theme values.
|
|
5
|
+
|
|
6
|
+
// Tier 1: every theme must define all of these (via themes/<name>/ui.css,
|
|
7
|
+
// usually as aliases onto the theme's own palette). Test-enforced.
|
|
8
|
+
export const REQUIRED_TOKENS = [
|
|
9
|
+
// Color roles
|
|
10
|
+
"--ui-bg",
|
|
11
|
+
"--ui-surface",
|
|
12
|
+
"--ui-surface-2",
|
|
13
|
+
"--ui-text",
|
|
14
|
+
"--ui-text-muted",
|
|
15
|
+
"--ui-text-faint",
|
|
16
|
+
"--ui-border",
|
|
17
|
+
"--ui-border-strong",
|
|
18
|
+
"--ui-accent",
|
|
19
|
+
"--ui-accent-strong",
|
|
20
|
+
"--ui-accent-soft",
|
|
21
|
+
"--ui-on-accent",
|
|
22
|
+
"--ui-good",
|
|
23
|
+
"--ui-good-soft",
|
|
24
|
+
"--ui-warn",
|
|
25
|
+
"--ui-warn-soft",
|
|
26
|
+
"--ui-danger",
|
|
27
|
+
"--ui-danger-soft",
|
|
28
|
+
"--ui-code-bg",
|
|
29
|
+
"--ui-code-text",
|
|
30
|
+
"--ui-shadow",
|
|
31
|
+
// Font slots
|
|
32
|
+
"--ui-font-sans",
|
|
33
|
+
"--ui-font-serif",
|
|
34
|
+
"--ui-font-mono",
|
|
35
|
+
"--ui-font-display",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Tier 2: defaulted in css/tokens.css inside :where() zero-specificity blocks
|
|
39
|
+
// (universal values, or derived from Tier 1 via var()/color-mix()). Themes
|
|
40
|
+
// override only when they want to.
|
|
41
|
+
export const DEFAULTED_TOKENS = [
|
|
42
|
+
"--ui-space-1",
|
|
43
|
+
"--ui-space-2",
|
|
44
|
+
"--ui-space-3",
|
|
45
|
+
"--ui-space-4",
|
|
46
|
+
"--ui-space-5",
|
|
47
|
+
"--ui-space-6",
|
|
48
|
+
"--ui-space-7",
|
|
49
|
+
"--ui-space-8",
|
|
50
|
+
"--ui-radius-sm",
|
|
51
|
+
"--ui-radius-md",
|
|
52
|
+
"--ui-radius-lg",
|
|
53
|
+
"--ui-radius-pill",
|
|
54
|
+
"--ui-text-xs",
|
|
55
|
+
"--ui-text-sm",
|
|
56
|
+
"--ui-text-md",
|
|
57
|
+
"--ui-text-lg",
|
|
58
|
+
"--ui-tracking-caps",
|
|
59
|
+
"--ui-focus",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// Chart roles: generic series/structure colors read into canvas via
|
|
63
|
+
// js/palette.js. Map your domain series onto --ui-chart-1..6.
|
|
64
|
+
export const CHART_TOKENS = [
|
|
65
|
+
"--ui-chart-1",
|
|
66
|
+
"--ui-chart-2",
|
|
67
|
+
"--ui-chart-3",
|
|
68
|
+
"--ui-chart-4",
|
|
69
|
+
"--ui-chart-5",
|
|
70
|
+
"--ui-chart-6",
|
|
71
|
+
"--ui-chart-grid",
|
|
72
|
+
"--ui-chart-grid-zero",
|
|
73
|
+
"--ui-chart-axis",
|
|
74
|
+
"--ui-chart-axis-faint",
|
|
75
|
+
"--ui-chart-band",
|
|
76
|
+
"--ui-chart-band-strong",
|
|
77
|
+
"--ui-chart-pos",
|
|
78
|
+
"--ui-chart-neg",
|
|
79
|
+
"--ui-chart-tip-bg",
|
|
80
|
+
"--ui-chart-tip-text",
|
|
81
|
+
"--ui-chart-dot-stroke",
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Chat surface roles, shaped by the @odla-ai/ai content-block contract.
|
|
85
|
+
export const CHAT_TOKENS = [
|
|
86
|
+
"--ui-chat-user-bg",
|
|
87
|
+
"--ui-chat-user-text",
|
|
88
|
+
"--ui-chat-assistant-bg",
|
|
89
|
+
"--ui-chat-thinking-bg",
|
|
90
|
+
"--ui-chat-thinking-text",
|
|
91
|
+
"--ui-chat-tool-accent",
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
export const UI_TOKENS = {
|
|
95
|
+
required: REQUIRED_TOKENS,
|
|
96
|
+
defaulted: DEFAULTED_TOKENS,
|
|
97
|
+
chart: CHART_TOKENS,
|
|
98
|
+
chat: CHAT_TOKENS,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Themes bundled with this package. paper satisfies only the ui contract;
|
|
102
|
+
// the other four also uphold @odla-ai/blog's theme selector contract.
|
|
103
|
+
export const THEMES = ["chalk", "clay", "juniper", "paper", "salt"];
|
|
104
|
+
export const BLOG_THEMES = ["chalk", "clay", "juniper", "salt"];
|
package/llms.txt
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# @odla-ai/ui — agent reference (llms.txt)
|
|
2
|
+
|
|
3
|
+
This file ships in the npm package so an agent building an odla-flavored app
|
|
4
|
+
can produce correct, attractive UI from `node_modules/@odla-ai/ui/llms.txt`
|
|
5
|
+
alone. Everything except `dist/` ships as source — read it when in doubt.
|
|
6
|
+
|
|
7
|
+
## Package anatomy
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
@odla-ai/ui/
|
|
11
|
+
├── index.css # bundler barrel: token defaults + all component CSS
|
|
12
|
+
├── odla-ui.css # the same, pre-flattened (no @imports) for file-copy/static use
|
|
13
|
+
├── css/tokens.css # --ui-* defaults in zero-specificity :where() blocks
|
|
14
|
+
├── css/components/*.css # buttons, forms, cards, table, nav, feedback, chat, chart
|
|
15
|
+
├── themes/<name>/ # paper, juniper, salt, chalk, clay
|
|
16
|
+
│ ├── theme.json # { name, label, description, fonts }
|
|
17
|
+
│ ├── styles.css # the theme's own tokens (+ page styling for blog themes)
|
|
18
|
+
│ └── ui.css # maps the theme's palette onto the --ui-* contract
|
|
19
|
+
├── fonts/*.css # remote-@import font stacks (plex, editorial, satoshi, lora, fira-code, system)
|
|
20
|
+
├── js/ # buildless ESM: tokens data, theme toggle, palette reader, canvas helpers
|
|
21
|
+
└── dist/ # compiled "@odla-ai/ui/components" (React/Preact form primitives)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## The token contract (--ui-*)
|
|
25
|
+
|
|
26
|
+
Component CSS references ONLY `--ui-*` custom properties. Machine-readable
|
|
27
|
+
source of truth: `js/tokens.js` (`UI_TOKENS.required/defaulted/chart/chat`).
|
|
28
|
+
|
|
29
|
+
Required tier — every theme defines all of these (test-enforced):
|
|
30
|
+
|
|
31
|
+
| Token | Role |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `--ui-bg` | page background |
|
|
34
|
+
| `--ui-surface` | raised surface: cards, panels, inputs |
|
|
35
|
+
| `--ui-surface-2` | sunken surface: wells, output panes, sticky table headers |
|
|
36
|
+
| `--ui-text` / `--ui-text-muted` / `--ui-text-faint` | text tiers |
|
|
37
|
+
| `--ui-border` / `--ui-border-strong` | hairline / emphasized borders |
|
|
38
|
+
| `--ui-accent` / `--ui-accent-strong` / `--ui-accent-soft` | brand accent, hover/pressed, translucent wash |
|
|
39
|
+
| `--ui-on-accent` | text on accent fills |
|
|
40
|
+
| `--ui-good(-soft)` / `--ui-warn(-soft)` / `--ui-danger(-soft)` | status + washes |
|
|
41
|
+
| `--ui-code-bg` / `--ui-code-text` | code surfaces |
|
|
42
|
+
| `--ui-shadow` | the theme's box-shadow (may be `none`) |
|
|
43
|
+
| `--ui-font-sans/serif/mono/display` | font slots (sans-only themes alias serif→sans) |
|
|
44
|
+
|
|
45
|
+
Defaulted tier (override per theme or per app only when needed):
|
|
46
|
+
`--ui-space-1..8` (4/8/12/16/24/32/48/64px), `--ui-radius-sm/md/lg/pill`
|
|
47
|
+
(4/7/10/999px), `--ui-text-xs/sm/md/lg` (11/12/13/15px), `--ui-tracking-caps`,
|
|
48
|
+
`--ui-focus` (focus ring shadow); chart roles `--ui-chart-1..6`, `-grid`,
|
|
49
|
+
`-grid-zero`, `-axis`, `-axis-faint`, `-band(-strong)`, `-pos`, `-neg`,
|
|
50
|
+
`-tip-bg`, `-tip-text`, `-dot-stroke`; chat roles `--ui-chat-user-bg/-text`,
|
|
51
|
+
`-assistant-bg`, `-thinking-bg/-text`, `-tool-accent`.
|
|
52
|
+
|
|
53
|
+
`css/tokens.css` wraps every default in `:where(:root)` (zero specificity),
|
|
54
|
+
so ANY plain `:root` definition — from a theme or your own stylesheet — wins
|
|
55
|
+
regardless of load order.
|
|
56
|
+
|
|
57
|
+
## Themes
|
|
58
|
+
|
|
59
|
+
Five bundled themes; metadata in each `theme.json`:
|
|
60
|
+
|
|
61
|
+
- **paper** — warm paper dashboard shell (IBM Plex; the odla Studio look).
|
|
62
|
+
Tokens + base only. NOT a blog theme (doesn't style blog selectors).
|
|
63
|
+
- **juniper / salt / chalk / clay** — full blog themes (moved here from
|
|
64
|
+
`@odla-ai/blog`, which resolves them from this package). Their `styles.css`
|
|
65
|
+
also styles the blog selector contract (`.site-header`, `.prose`,
|
|
66
|
+
`.post-item`, `.hljs-*`, …) — that contract belongs to the blog package.
|
|
67
|
+
|
|
68
|
+
Anatomy: `styles.css` carries the theme's OWN token names (e.g. salt's
|
|
69
|
+
`--accent`, paper's `--panel`); `ui.css` maps them onto `--ui-*`. **Always
|
|
70
|
+
load `ui.css` alongside `styles.css`** or component classes fall back to the
|
|
71
|
+
neutral defaults instead of the theme's palette.
|
|
72
|
+
|
|
73
|
+
Dark-mode invariant (tested; keep it when editing any theme): dark tokens are
|
|
74
|
+
duplicated on BOTH `[data-theme="dark"]` and
|
|
75
|
+
`@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) }`,
|
|
76
|
+
and the two blocks must stay IDENTICAL. In `ui.css`, prefer pure `var()`
|
|
77
|
+
aliases (they track dark automatically); any literal value must appear in
|
|
78
|
+
both dark blocks. `--ui-*-soft` washes are best derived:
|
|
79
|
+
`color-mix(in srgb, var(--ui-good) 12%, transparent)`.
|
|
80
|
+
|
|
81
|
+
## Component classes (all class-scoped — no bare-element rules)
|
|
82
|
+
|
|
83
|
+
Component CSS never styles bare `button`/`input`/`table`, so it can sit
|
|
84
|
+
beside theme stylesheets that do. Add the class to opt an element in.
|
|
85
|
+
|
|
86
|
+
- **Buttons** (`buttons.css`): `.btn` = primary; chain `.btn.secondary`,
|
|
87
|
+
`.btn.ghost`, `.btn.danger`, `.btn.mini`. Segmented control `.seg` (child
|
|
88
|
+
`button`, active `.on` or `aria-pressed`). Tab bar `.tabs` (active `.on` or
|
|
89
|
+
`aria-selected`). Quiet mono `.pill`; interactive `.chip`.
|
|
90
|
+
- **Forms** (`forms.css`): `.input`, `.select`, `.textarea` (mono by default;
|
|
91
|
+
add `.prose-input` for body font), invalid via `.invalid` or
|
|
92
|
+
`aria-invalid="true"`. `.field` wrapper with `.field-label`, `.field-error`,
|
|
93
|
+
`.field-hint`. `.check` (label wrapping a checkbox). `.range` slider.
|
|
94
|
+
`.form-well` + `.form-grid` for grouped label/control grids.
|
|
95
|
+
- **Cards** (`cards.css`): `.card` (child `h4` = caps heading), `.cards` KPI
|
|
96
|
+
grid, `.metric`, `.panel` (child `h3`), `.badge` (+`.good/.warn/.danger/.accent`),
|
|
97
|
+
`.verdict` + `.verdict-pass`/`.verdict-fail`.
|
|
98
|
+
- **Table** (`table.css`): `.table` (+`.pos`/`.neg` cells, `td.editable`);
|
|
99
|
+
wrap in `.table-wrap` for scroll + sticky header.
|
|
100
|
+
- **Nav/layout** (`nav.css`): `.layout` + `.sidebar` + `.main` + `.head` app
|
|
101
|
+
shell, `.brand`, `.navitem` (+`.active`), `.wrap` (+`.narrow`), `.row`,
|
|
102
|
+
`.muted`, `.drawer`/`.drawer-overlay` (+`.open`), `.footer`.
|
|
103
|
+
- **Feedback** (`feedback.css`): `.toast` (+status), `.banner`
|
|
104
|
+
(+`.good/.warn/.error`), `.dropzone` (+`.drag`), `.live-dot`, `.spinner`.
|
|
105
|
+
- **Chat** (`chat.css`, shaped by @odla-ai/ai's content blocks): `.chat`,
|
|
106
|
+
`.chat-msg.user/.assistant` > `.chat-bubble`, `.chat-thinking` (a styled
|
|
107
|
+
`<details>` for thinking blocks), `.chat-tool` + `.chat-tool-name` +
|
|
108
|
+
`.chat-tool-args` (tool_use), `.chat-result` (+`.error`) (tool_result),
|
|
109
|
+
`.chat-cursor` (streaming caret), `.chat-composer`, `.chat-usage`,
|
|
110
|
+
`.chat-banner` (+`.warn/.error`) for stop reasons/typed errors,
|
|
111
|
+
`.chat-attachment`.
|
|
112
|
+
- **Chart** (`chart.css`): `.chart` container, `.chart-legend`/`.chart-key`;
|
|
113
|
+
actual rendering is canvas via the JS helpers below.
|
|
114
|
+
|
|
115
|
+
## Consuming the CSS
|
|
116
|
+
|
|
117
|
+
Load order: **fonts → theme styles.css + ui.css → index.css** (forgiving in
|
|
118
|
+
practice thanks to `:where()`, but keep this order).
|
|
119
|
+
|
|
120
|
+
Static HTML:
|
|
121
|
+
```html
|
|
122
|
+
<link rel="stylesheet" href="/assets/fonts/plex.css"><!-- or copy the files -->
|
|
123
|
+
<link rel="stylesheet" href="/assets/themes/paper/styles.css">
|
|
124
|
+
<link rel="stylesheet" href="/assets/themes/paper/ui.css">
|
|
125
|
+
<link rel="stylesheet" href="/assets/odla-ui.css">
|
|
126
|
+
```
|
|
127
|
+
Copy `odla-ui.css` (pre-flattened) for single-file use; `index.css` uses
|
|
128
|
+
relative `@import`s and only works where a bundler (or the package layout)
|
|
129
|
+
resolves them.
|
|
130
|
+
|
|
131
|
+
Vite/bundler app:
|
|
132
|
+
```js
|
|
133
|
+
import "@odla-ai/ui/fonts/plex.css";
|
|
134
|
+
import "@odla-ai/ui/themes/paper/styles.css";
|
|
135
|
+
import "@odla-ai/ui/themes/paper/ui.css";
|
|
136
|
+
import "@odla-ai/ui/index.css";
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Blog sites (`@odla-ai/blog` ≥0.0.3) get all of this automatically: the build
|
|
140
|
+
emits `assets/odla-ui.css` and appends the theme's `ui.css` to
|
|
141
|
+
`assets/styles.css` — just use the classes in your pages/templates.
|
|
142
|
+
|
|
143
|
+
## JS helpers (buildless ESM — `import { … } from "@odla-ai/ui"`)
|
|
144
|
+
|
|
145
|
+
- Theme: `THEME_NO_FLASH` (inline this string in a `<head>` script tag),
|
|
146
|
+
`initTheme()`, `currentTheme()`, `toggleTheme()` → `"light"|"dark"`,
|
|
147
|
+
`bindThemeToggle(el, { onChange })`. Convention: localStorage `"theme"` →
|
|
148
|
+
`data-theme` attribute on `<html>`.
|
|
149
|
+
- Palette: `cssVar(name)`, `readPalette()` → `{ bg, surface, text, muted,
|
|
150
|
+
faint, border, accent, good, warn, danger, series[6], grid, gridZero, axis,
|
|
151
|
+
axisFaint, band, bandStrong, pos, neg, tipBg, tipText, dotStroke, fontMono }`
|
|
152
|
+
read from the `--ui-*` computed values. **Re-read after `toggleTheme()`**
|
|
153
|
+
and redraw canvases — computed colors change with the mode.
|
|
154
|
+
- Canvas: `fitCanvas(cv, {aspect,maxH,minH,pad})` → `{ctx,w,h}` (hi-DPI,
|
|
155
|
+
draws in CSS pixels), `niceTicks(min,max,n)`, `roundRect(ctx,x,y,w,h,r)`,
|
|
156
|
+
`bandFill(ctx,xs,loYs,hiYs,fill)`, `linePath(ctx,xs,ys)`,
|
|
157
|
+
`dot(ctx,x,y,color,{r,stroke})`, `drawTip(ctx,x,y,lines,colors,
|
|
158
|
+
{rightBound,tipBg,font,pad})`, `rafThrottle(fn)`.
|
|
159
|
+
|
|
160
|
+
## React/Preact components (`@odla-ai/ui/components`)
|
|
161
|
+
|
|
162
|
+
Form primitives emitting the classes above: `Button` (variant:
|
|
163
|
+
primary|secondary|ghost|danger, mini), `Field` (label/htmlFor/error/hint),
|
|
164
|
+
`Input`/`Textarea` (invalid; Textarea prose), `Select` (options or children),
|
|
165
|
+
`Checkbox` (label), plus the `cx` class joiner.
|
|
166
|
+
|
|
167
|
+
Authored against the `react` API using ONLY the preact/compat-safe subset
|
|
168
|
+
(forwardRef + plain JSX; no `use()`, no actions, no React-19-only APIs).
|
|
169
|
+
Keep that constraint when adding components.
|
|
170
|
+
|
|
171
|
+
- React 18/19: import directly (react is an optional peer).
|
|
172
|
+
- Preact (bundled): Vite `resolve.alias` → `react: "preact/compat"`,
|
|
173
|
+
`react/jsx-runtime: "preact/jsx-runtime"`... alias `react-dom` too if other
|
|
174
|
+
libs need it.
|
|
175
|
+
- Preact (buildless island on a static page):
|
|
176
|
+
```html
|
|
177
|
+
<script type="importmap">
|
|
178
|
+
{ "imports": {
|
|
179
|
+
"react": "https://esm.sh/preact@10/compat",
|
|
180
|
+
"react/jsx-runtime": "https://esm.sh/preact@10/jsx-runtime",
|
|
181
|
+
"@odla-ai/ui/components": "https://esm.sh/@odla-ai/ui@0.1/components"
|
|
182
|
+
} }
|
|
183
|
+
</script>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Gotchas
|
|
187
|
+
|
|
188
|
+
- `themes/<name>/ui.css` must load with `styles.css` — without it, component
|
|
189
|
+
classes render in neutral gray/blue, not the theme.
|
|
190
|
+
- `.btn` with no modifier IS the primary button; there is no `.btn-primary`.
|
|
191
|
+
- Editing any theme: keep the two dark blocks byte-identical (tests fail
|
|
192
|
+
otherwise), in `styles.css` AND `ui.css`.
|
|
193
|
+
- Editing `css/`: run `npm run gen:css` to refresh `odla-ui.css` (a test
|
|
194
|
+
fails on drift).
|
|
195
|
+
- `--ui-chart-*` are GENERIC roles — map your domain series onto
|
|
196
|
+
`--ui-chart-1..6`; don't invent per-domain token names in app code.
|
|
197
|
+
- The blog selector contract (`.site-header`, `.prose`, …) belongs to
|
|
198
|
+
`@odla-ai/blog`; ui component CSS must never style those selectors, and
|
|
199
|
+
`paper` must not be offered as a blog theme.
|
|
200
|
+
- Publishing: `llms.txt` is hand-maintained, committed, and listed in
|
|
201
|
+
`files`; `prepublishOnly` builds dist and runs the tests.
|