@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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/* @odla-ai/ui — navigation & layout chrome */
|
|
2
|
+
|
|
3
|
+
/* App shell: fixed sidebar + content column. */
|
|
4
|
+
.layout {
|
|
5
|
+
display: grid;
|
|
6
|
+
grid-template-columns: 260px 1fr;
|
|
7
|
+
min-height: 100vh;
|
|
8
|
+
}
|
|
9
|
+
.sidebar {
|
|
10
|
+
background: var(--ui-surface);
|
|
11
|
+
border-right: 1px solid var(--ui-border);
|
|
12
|
+
padding: var(--ui-space-4);
|
|
13
|
+
}
|
|
14
|
+
.main { padding: var(--ui-space-5) var(--ui-space-6); max-width: 1100px; min-width: 0; }
|
|
15
|
+
.head {
|
|
16
|
+
display: flex;
|
|
17
|
+
justify-content: space-between;
|
|
18
|
+
align-items: baseline;
|
|
19
|
+
gap: var(--ui-space-4);
|
|
20
|
+
}
|
|
21
|
+
.head h2 { margin: 0; font-weight: 700; }
|
|
22
|
+
.brand {
|
|
23
|
+
font-family: var(--ui-font-display);
|
|
24
|
+
font-weight: 700;
|
|
25
|
+
letter-spacing: -0.02em;
|
|
26
|
+
font-size: 18px;
|
|
27
|
+
margin-bottom: var(--ui-space-4);
|
|
28
|
+
}
|
|
29
|
+
.brand small { color: var(--ui-text-muted); font-weight: 500; }
|
|
30
|
+
|
|
31
|
+
/* Sidebar nav entry (button or link). */
|
|
32
|
+
.navitem {
|
|
33
|
+
display: block;
|
|
34
|
+
width: 100%;
|
|
35
|
+
text-align: left;
|
|
36
|
+
font: inherit;
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
background: var(--ui-surface);
|
|
39
|
+
color: var(--ui-text-muted);
|
|
40
|
+
border: 1px solid var(--ui-border);
|
|
41
|
+
border-radius: var(--ui-radius-md);
|
|
42
|
+
padding: 8px 10px;
|
|
43
|
+
margin-bottom: var(--ui-space-2);
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
text-decoration: none;
|
|
46
|
+
}
|
|
47
|
+
.navitem:hover { background: var(--ui-surface-2); color: var(--ui-text); }
|
|
48
|
+
.navitem.active {
|
|
49
|
+
background: var(--ui-accent-soft);
|
|
50
|
+
border-color: var(--ui-accent);
|
|
51
|
+
color: var(--ui-accent-strong);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Content containers. */
|
|
55
|
+
.wrap { width: 100%; max-width: 1080px; margin: 0 auto; padding: 0 var(--ui-space-4); }
|
|
56
|
+
.wrap.narrow { max-width: 640px; }
|
|
57
|
+
|
|
58
|
+
/* Utility row + muted text (ubiquitous in app chrome). */
|
|
59
|
+
.row { display: flex; gap: var(--ui-space-2); align-items: center; }
|
|
60
|
+
.muted { color: var(--ui-text-muted); }
|
|
61
|
+
|
|
62
|
+
/* Off-canvas drawer. Toggle .open on both drawer and overlay. */
|
|
63
|
+
.drawer {
|
|
64
|
+
position: fixed;
|
|
65
|
+
top: 0;
|
|
66
|
+
right: 0;
|
|
67
|
+
bottom: 0;
|
|
68
|
+
width: min(340px, 88vw);
|
|
69
|
+
background: var(--ui-surface);
|
|
70
|
+
border-left: 1px solid var(--ui-border);
|
|
71
|
+
padding: var(--ui-space-4);
|
|
72
|
+
overflow-y: auto;
|
|
73
|
+
transform: translateX(100%);
|
|
74
|
+
transition: transform 0.25s ease;
|
|
75
|
+
z-index: 1001;
|
|
76
|
+
}
|
|
77
|
+
.drawer.open { transform: translateX(0); }
|
|
78
|
+
.drawer-overlay {
|
|
79
|
+
position: fixed;
|
|
80
|
+
inset: 0;
|
|
81
|
+
background: rgba(0, 0, 0, 0.4);
|
|
82
|
+
opacity: 0;
|
|
83
|
+
pointer-events: none;
|
|
84
|
+
transition: opacity 0.25s ease;
|
|
85
|
+
z-index: 1000;
|
|
86
|
+
}
|
|
87
|
+
.drawer-overlay.open { opacity: 1; pointer-events: auto; }
|
|
88
|
+
|
|
89
|
+
/* Page footer strip. */
|
|
90
|
+
.footer {
|
|
91
|
+
border-top: 1px solid var(--ui-border);
|
|
92
|
+
color: var(--ui-text-muted);
|
|
93
|
+
font-size: var(--ui-text-sm);
|
|
94
|
+
padding: var(--ui-space-5) 0;
|
|
95
|
+
margin-top: var(--ui-space-7);
|
|
96
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* @odla-ai/ui — data tables
|
|
2
|
+
`.table` is the class equivalent of a themed bare <table>.
|
|
3
|
+
Wrap in `.table-wrap` for scroll + sticky header. */
|
|
4
|
+
|
|
5
|
+
.table {
|
|
6
|
+
width: 100%;
|
|
7
|
+
border-collapse: collapse;
|
|
8
|
+
font-size: var(--ui-text-md);
|
|
9
|
+
}
|
|
10
|
+
.table td,
|
|
11
|
+
.table th {
|
|
12
|
+
text-align: left;
|
|
13
|
+
padding: 7px 8px;
|
|
14
|
+
border-bottom: 1px solid var(--ui-border);
|
|
15
|
+
}
|
|
16
|
+
.table th { color: var(--ui-text-muted); font-weight: 600; }
|
|
17
|
+
.table td { font-variant-numeric: tabular-nums; }
|
|
18
|
+
|
|
19
|
+
/* Scroll container; header sticks against the sunken surface. */
|
|
20
|
+
.table-wrap { overflow: auto; max-height: 420px; }
|
|
21
|
+
.table-wrap .table thead th {
|
|
22
|
+
position: sticky;
|
|
23
|
+
top: 0;
|
|
24
|
+
background: var(--ui-surface-2);
|
|
25
|
+
z-index: 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Signed values. */
|
|
29
|
+
.table .pos { color: var(--ui-good); }
|
|
30
|
+
.table .neg { color: var(--ui-danger); }
|
|
31
|
+
|
|
32
|
+
/* Inline-editable cells (data browsers). */
|
|
33
|
+
.table td.editable { cursor: text; }
|
|
34
|
+
.table td.editable:hover { background: var(--ui-accent-soft); }
|
package/css/tokens.css
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/* ══════════════════════════════════════════════════════════════
|
|
2
|
+
@odla-ai/ui — token defaults
|
|
3
|
+
Every block is wrapped in :where() so it has ZERO specificity:
|
|
4
|
+
a theme's plain `:root { … }` definitions always win, regardless
|
|
5
|
+
of stylesheet load order. Without a theme, these neutral values
|
|
6
|
+
(grayscale + restrained blue) render sanely on their own.
|
|
7
|
+
Contract data lives in js/tokens.js; themes map their palettes
|
|
8
|
+
onto the required tier in themes/<name>/ui.css.
|
|
9
|
+
══════════════════════════════════════════════════════════════ */
|
|
10
|
+
|
|
11
|
+
:where(:root) {
|
|
12
|
+
/* ─── Required tier: neutral fallbacks (themes override all of these) ─── */
|
|
13
|
+
--ui-bg: #f7f7f5;
|
|
14
|
+
--ui-surface: #ffffff;
|
|
15
|
+
--ui-surface-2: #efefec;
|
|
16
|
+
--ui-text: #24272b;
|
|
17
|
+
--ui-text-muted: #64686e;
|
|
18
|
+
--ui-text-faint: #9b9fa6;
|
|
19
|
+
--ui-border: #dfe0dc;
|
|
20
|
+
--ui-border-strong: #b9bbb5;
|
|
21
|
+
--ui-accent: #3b5e8c;
|
|
22
|
+
--ui-accent-strong: #2e4a70;
|
|
23
|
+
--ui-accent-soft: rgba(59, 94, 140, 0.1);
|
|
24
|
+
--ui-on-accent: #ffffff;
|
|
25
|
+
--ui-good: #4a7c59;
|
|
26
|
+
--ui-good-soft: rgba(74, 124, 89, 0.12);
|
|
27
|
+
--ui-warn: #a07d2e;
|
|
28
|
+
--ui-warn-soft: rgba(160, 125, 46, 0.12);
|
|
29
|
+
--ui-danger: #a8433a;
|
|
30
|
+
--ui-danger-soft: rgba(168, 67, 58, 0.1);
|
|
31
|
+
--ui-code-bg: #efefec;
|
|
32
|
+
--ui-code-text: #33363b;
|
|
33
|
+
--ui-shadow: 0 1px 2px rgba(36, 39, 43, 0.04), 0 8px 24px rgba(36, 39, 43, 0.06);
|
|
34
|
+
--ui-font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
35
|
+
--ui-font-serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
|
|
36
|
+
--ui-font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
37
|
+
--ui-font-display: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
38
|
+
|
|
39
|
+
/* ─── Defaulted tier: universal scales ─── */
|
|
40
|
+
--ui-space-1: 4px;
|
|
41
|
+
--ui-space-2: 8px;
|
|
42
|
+
--ui-space-3: 12px;
|
|
43
|
+
--ui-space-4: 16px;
|
|
44
|
+
--ui-space-5: 24px;
|
|
45
|
+
--ui-space-6: 32px;
|
|
46
|
+
--ui-space-7: 48px;
|
|
47
|
+
--ui-space-8: 64px;
|
|
48
|
+
--ui-radius-sm: 4px;
|
|
49
|
+
--ui-radius-md: 7px;
|
|
50
|
+
--ui-radius-lg: 10px;
|
|
51
|
+
--ui-radius-pill: 999px;
|
|
52
|
+
--ui-text-xs: 11px;
|
|
53
|
+
--ui-text-sm: 12px;
|
|
54
|
+
--ui-text-md: 13px;
|
|
55
|
+
--ui-text-lg: 15px;
|
|
56
|
+
--ui-tracking-caps: 0.06em;
|
|
57
|
+
--ui-focus: 0 0 0 3px var(--ui-accent-soft);
|
|
58
|
+
|
|
59
|
+
/* ─── Chart roles (derived; themes may override for richer palettes) ─── */
|
|
60
|
+
--ui-chart-1: var(--ui-accent);
|
|
61
|
+
--ui-chart-2: var(--ui-good);
|
|
62
|
+
--ui-chart-3: var(--ui-warn);
|
|
63
|
+
--ui-chart-4: var(--ui-danger);
|
|
64
|
+
--ui-chart-5: color-mix(in srgb, var(--ui-accent) 45%, var(--ui-text));
|
|
65
|
+
--ui-chart-6: var(--ui-text-faint);
|
|
66
|
+
--ui-chart-grid: color-mix(in srgb, var(--ui-text) 8%, transparent);
|
|
67
|
+
--ui-chart-grid-zero: color-mix(in srgb, var(--ui-text) 22%, transparent);
|
|
68
|
+
--ui-chart-axis: var(--ui-text-muted);
|
|
69
|
+
--ui-chart-axis-faint: var(--ui-text-faint);
|
|
70
|
+
--ui-chart-band: color-mix(in srgb, var(--ui-accent) 10%, transparent);
|
|
71
|
+
--ui-chart-band-strong: color-mix(in srgb, var(--ui-accent) 22%, transparent);
|
|
72
|
+
--ui-chart-pos: var(--ui-good);
|
|
73
|
+
--ui-chart-neg: var(--ui-danger);
|
|
74
|
+
--ui-chart-tip-bg: color-mix(in srgb, var(--ui-text) 92%, transparent);
|
|
75
|
+
--ui-chart-tip-text: var(--ui-bg);
|
|
76
|
+
--ui-chart-dot-stroke: var(--ui-bg);
|
|
77
|
+
|
|
78
|
+
/* ─── Chat roles (derived) ─── */
|
|
79
|
+
--ui-chat-user-bg: var(--ui-accent-soft);
|
|
80
|
+
--ui-chat-user-text: var(--ui-text);
|
|
81
|
+
--ui-chat-assistant-bg: var(--ui-surface);
|
|
82
|
+
--ui-chat-thinking-bg: var(--ui-surface-2);
|
|
83
|
+
--ui-chat-thinking-text: var(--ui-text-muted);
|
|
84
|
+
--ui-chat-tool-accent: var(--ui-accent);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Neutral dark fallbacks — only the required tier changes; the derived
|
|
88
|
+
tiers track it automatically via var()/color-mix(). Themes that define
|
|
89
|
+
their own dark palettes override these the same zero-specificity way. */
|
|
90
|
+
:where([data-theme="dark"]) {
|
|
91
|
+
--ui-bg: #17181b;
|
|
92
|
+
--ui-surface: #1f2125;
|
|
93
|
+
--ui-surface-2: #26282d;
|
|
94
|
+
--ui-text: #e6e6e3;
|
|
95
|
+
--ui-text-muted: #a2a6ad;
|
|
96
|
+
--ui-text-faint: #6f737a;
|
|
97
|
+
--ui-border: #33363c;
|
|
98
|
+
--ui-border-strong: #4a4e56;
|
|
99
|
+
--ui-accent: #7ba1d4;
|
|
100
|
+
--ui-accent-strong: #97b7e2;
|
|
101
|
+
--ui-accent-soft: rgba(123, 161, 212, 0.14);
|
|
102
|
+
--ui-on-accent: #14161a;
|
|
103
|
+
--ui-good: #7fae8e;
|
|
104
|
+
--ui-good-soft: rgba(127, 174, 142, 0.14);
|
|
105
|
+
--ui-warn: #c5a75e;
|
|
106
|
+
--ui-warn-soft: rgba(197, 167, 94, 0.14);
|
|
107
|
+
--ui-danger: #cf7a72;
|
|
108
|
+
--ui-danger-soft: rgba(207, 122, 114, 0.14);
|
|
109
|
+
--ui-code-bg: #26282d;
|
|
110
|
+
--ui-code-text: #d4d5d2;
|
|
111
|
+
--ui-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 24px rgba(0, 0, 0, 0.35);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@media (prefers-color-scheme: dark) {
|
|
115
|
+
:where(:root:not([data-theme="light"])) {
|
|
116
|
+
--ui-bg: #17181b;
|
|
117
|
+
--ui-surface: #1f2125;
|
|
118
|
+
--ui-surface-2: #26282d;
|
|
119
|
+
--ui-text: #e6e6e3;
|
|
120
|
+
--ui-text-muted: #a2a6ad;
|
|
121
|
+
--ui-text-faint: #6f737a;
|
|
122
|
+
--ui-border: #33363c;
|
|
123
|
+
--ui-border-strong: #4a4e56;
|
|
124
|
+
--ui-accent: #7ba1d4;
|
|
125
|
+
--ui-accent-strong: #97b7e2;
|
|
126
|
+
--ui-accent-soft: rgba(123, 161, 212, 0.14);
|
|
127
|
+
--ui-on-accent: #14161a;
|
|
128
|
+
--ui-good: #7fae8e;
|
|
129
|
+
--ui-good-soft: rgba(127, 174, 142, 0.14);
|
|
130
|
+
--ui-warn: #c5a75e;
|
|
131
|
+
--ui-warn-soft: rgba(197, 167, 94, 0.14);
|
|
132
|
+
--ui-danger: #cf7a72;
|
|
133
|
+
--ui-danger-soft: rgba(207, 122, 114, 0.14);
|
|
134
|
+
--ui-code-bg: #26282d;
|
|
135
|
+
--ui-code-text: #d4d5d2;
|
|
136
|
+
--ui-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 24px rgba(0, 0, 0, 0.35);
|
|
137
|
+
}
|
|
138
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
|
|
5
|
+
interface ButtonProps extends ComponentPropsWithoutRef<"button"> {
|
|
6
|
+
variant?: ButtonVariant;
|
|
7
|
+
mini?: boolean;
|
|
8
|
+
}
|
|
9
|
+
declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLButtonElement>>;
|
|
10
|
+
|
|
11
|
+
interface FieldProps extends ComponentPropsWithoutRef<"div"> {
|
|
12
|
+
label?: ReactNode;
|
|
13
|
+
htmlFor?: string;
|
|
14
|
+
error?: ReactNode;
|
|
15
|
+
hint?: ReactNode;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
declare function Field({ label, htmlFor, error, hint, children, className, ...rest }: FieldProps): react.JSX.Element;
|
|
19
|
+
|
|
20
|
+
interface InputProps extends ComponentPropsWithoutRef<"input"> {
|
|
21
|
+
invalid?: boolean;
|
|
22
|
+
}
|
|
23
|
+
declare const Input: react.ForwardRefExoticComponent<InputProps & react.RefAttributes<HTMLInputElement>>;
|
|
24
|
+
|
|
25
|
+
interface SelectOption {
|
|
26
|
+
value: string;
|
|
27
|
+
label?: string;
|
|
28
|
+
}
|
|
29
|
+
interface SelectProps extends ComponentPropsWithoutRef<"select"> {
|
|
30
|
+
invalid?: boolean;
|
|
31
|
+
/** Shorthand for simple value/label option lists; children win if present. */
|
|
32
|
+
options?: SelectOption[];
|
|
33
|
+
}
|
|
34
|
+
declare const Select: react.ForwardRefExoticComponent<SelectProps & react.RefAttributes<HTMLSelectElement>>;
|
|
35
|
+
|
|
36
|
+
interface CheckboxProps extends ComponentPropsWithoutRef<"input"> {
|
|
37
|
+
label?: ReactNode;
|
|
38
|
+
}
|
|
39
|
+
declare const Checkbox: react.ForwardRefExoticComponent<CheckboxProps & react.RefAttributes<HTMLInputElement>>;
|
|
40
|
+
|
|
41
|
+
interface TextareaProps extends ComponentPropsWithoutRef<"textarea"> {
|
|
42
|
+
invalid?: boolean;
|
|
43
|
+
/** Body-font textarea for prose; default is mono (data/config editing). */
|
|
44
|
+
prose?: boolean;
|
|
45
|
+
}
|
|
46
|
+
declare const Textarea: react.ForwardRefExoticComponent<TextareaProps & react.RefAttributes<HTMLTextAreaElement>>;
|
|
47
|
+
|
|
48
|
+
declare function cx(...parts: Array<string | false | null | undefined>): string;
|
|
49
|
+
|
|
50
|
+
export { Button, type ButtonProps, type ButtonVariant, Checkbox, type CheckboxProps, Field, type FieldProps, Input, type InputProps, Select, type SelectOption, type SelectProps, Textarea, type TextareaProps, cx };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/button.tsx
|
|
5
|
+
|
|
6
|
+
// src/cx.ts
|
|
7
|
+
function cx(...parts) {
|
|
8
|
+
return parts.filter(Boolean).join(" ");
|
|
9
|
+
}
|
|
10
|
+
var Button = forwardRef(function Button2({ variant = "primary", mini, className, type = "button", ...rest }, ref) {
|
|
11
|
+
return /* @__PURE__ */ jsx(
|
|
12
|
+
"button",
|
|
13
|
+
{
|
|
14
|
+
ref,
|
|
15
|
+
type,
|
|
16
|
+
className: cx("btn", variant !== "primary" && variant, mini && "mini", className),
|
|
17
|
+
...rest
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
function Field({ label, htmlFor, error, hint, children, className, ...rest }) {
|
|
22
|
+
return /* @__PURE__ */ jsxs("div", { className: cx("field", className), ...rest, children: [
|
|
23
|
+
label != null && /* @__PURE__ */ jsx("label", { className: "field-label", htmlFor, children: label }),
|
|
24
|
+
children,
|
|
25
|
+
error != null && /* @__PURE__ */ jsx("p", { className: "field-error", children: error }),
|
|
26
|
+
error == null && hint != null && /* @__PURE__ */ jsx("p", { className: "field-hint", children: hint })
|
|
27
|
+
] });
|
|
28
|
+
}
|
|
29
|
+
var Input = forwardRef(function Input2({ invalid, className, ...rest }, ref) {
|
|
30
|
+
return /* @__PURE__ */ jsx(
|
|
31
|
+
"input",
|
|
32
|
+
{
|
|
33
|
+
ref,
|
|
34
|
+
className: cx("input", invalid && "invalid", className),
|
|
35
|
+
"aria-invalid": invalid || void 0,
|
|
36
|
+
...rest
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
var Select = forwardRef(function Select2({ invalid, options, className, children, ...rest }, ref) {
|
|
41
|
+
return /* @__PURE__ */ jsx(
|
|
42
|
+
"select",
|
|
43
|
+
{
|
|
44
|
+
ref,
|
|
45
|
+
className: cx("select", invalid && "invalid", className),
|
|
46
|
+
"aria-invalid": invalid || void 0,
|
|
47
|
+
...rest,
|
|
48
|
+
children: children ?? options?.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label ?? o.value }, o.value))
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
var Checkbox = forwardRef(function Checkbox2({ label, className, ...rest }, ref) {
|
|
53
|
+
return /* @__PURE__ */ jsxs("label", { className: cx("check", className), children: [
|
|
54
|
+
/* @__PURE__ */ jsx("input", { ref, type: "checkbox", ...rest }),
|
|
55
|
+
label != null && /* @__PURE__ */ jsx("span", { children: label })
|
|
56
|
+
] });
|
|
57
|
+
});
|
|
58
|
+
var Textarea = forwardRef(function Textarea2({ invalid, prose, className, ...rest }, ref) {
|
|
59
|
+
return /* @__PURE__ */ jsx(
|
|
60
|
+
"textarea",
|
|
61
|
+
{
|
|
62
|
+
ref,
|
|
63
|
+
className: cx("textarea", prose && "prose-input", invalid && "invalid", className),
|
|
64
|
+
"aria-invalid": invalid || void 0,
|
|
65
|
+
...rest
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export { Button, Checkbox, Field, Input, Select, Textarea, cx };
|
|
71
|
+
//# sourceMappingURL=index.js.map
|
|
72
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cx.ts","../src/button.tsx","../src/field.tsx","../src/input.tsx","../src/select.tsx","../src/checkbox.tsx","../src/textarea.tsx"],"names":["Button","jsx","forwardRef","Input","Select","Checkbox","jsxs","Textarea"],"mappings":"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","file":"index.js","sourcesContent":["export function cx(...parts: Array<string | false | null | undefined>): string {\n return parts.filter(Boolean).join(\" \");\n}\n","import { forwardRef, type ComponentPropsWithoutRef } from \"react\";\nimport { cx } from \"./cx.js\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"danger\";\n\nexport interface ButtonProps extends ComponentPropsWithoutRef<\"button\"> {\n variant?: ButtonVariant;\n mini?: boolean;\n}\n\n// `.btn` alone is the primary variant; modifiers chain as extra classes.\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n { variant = \"primary\", mini, className, type = \"button\", ...rest },\n ref\n) {\n return (\n <button\n ref={ref}\n type={type}\n className={cx(\"btn\", variant !== \"primary\" && variant, mini && \"mini\", className)}\n {...rest}\n />\n );\n});\n","import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cx } from \"./cx.js\";\n\nexport interface FieldProps extends ComponentPropsWithoutRef<\"div\"> {\n label?: ReactNode;\n htmlFor?: string;\n error?: ReactNode;\n hint?: ReactNode;\n children?: ReactNode;\n}\n\n// Label above the control, error/hint below. Pass htmlFor to link the label\n// to the wrapped control's id.\nexport function Field({ label, htmlFor, error, hint, children, className, ...rest }: FieldProps) {\n return (\n <div className={cx(\"field\", className)} {...rest}>\n {label != null && (\n <label className=\"field-label\" htmlFor={htmlFor}>\n {label}\n </label>\n )}\n {children}\n {error != null && <p className=\"field-error\">{error}</p>}\n {error == null && hint != null && <p className=\"field-hint\">{hint}</p>}\n </div>\n );\n}\n","import { forwardRef, type ComponentPropsWithoutRef } from \"react\";\nimport { cx } from \"./cx.js\";\n\nexport interface InputProps extends ComponentPropsWithoutRef<\"input\"> {\n invalid?: boolean;\n}\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n { invalid, className, ...rest },\n ref\n) {\n return (\n <input\n ref={ref}\n className={cx(\"input\", invalid && \"invalid\", className)}\n aria-invalid={invalid || undefined}\n {...rest}\n />\n );\n});\n","import { forwardRef, type ComponentPropsWithoutRef } from \"react\";\nimport { cx } from \"./cx.js\";\n\nexport interface SelectOption {\n value: string;\n label?: string;\n}\n\nexport interface SelectProps extends ComponentPropsWithoutRef<\"select\"> {\n invalid?: boolean;\n /** Shorthand for simple value/label option lists; children win if present. */\n options?: SelectOption[];\n}\n\nexport const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(\n { invalid, options, className, children, ...rest },\n ref\n) {\n return (\n <select\n ref={ref}\n className={cx(\"select\", invalid && \"invalid\", className)}\n aria-invalid={invalid || undefined}\n {...rest}\n >\n {children ??\n options?.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label ?? o.value}\n </option>\n ))}\n </select>\n );\n});\n","import { forwardRef, type ComponentPropsWithoutRef, type ReactNode } from \"react\";\nimport { cx } from \"./cx.js\";\n\nexport interface CheckboxProps extends ComponentPropsWithoutRef<\"input\"> {\n label?: ReactNode;\n}\n\n// A .check label wrapping the input, so the text is part of the hit target.\nexport const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(\n { label, className, ...rest },\n ref\n) {\n return (\n <label className={cx(\"check\", className)}>\n <input ref={ref} type=\"checkbox\" {...rest} />\n {label != null && <span>{label}</span>}\n </label>\n );\n});\n","import { forwardRef, type ComponentPropsWithoutRef } from \"react\";\nimport { cx } from \"./cx.js\";\n\nexport interface TextareaProps extends ComponentPropsWithoutRef<\"textarea\"> {\n invalid?: boolean;\n /** Body-font textarea for prose; default is mono (data/config editing). */\n prose?: boolean;\n}\n\nexport const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(\n { invalid, prose, className, ...rest },\n ref\n) {\n return (\n <textarea\n ref={ref}\n className={cx(\"textarea\", prose && \"prose-input\", invalid && \"invalid\", className)}\n aria-invalid={invalid || undefined}\n {...rest}\n />\n );\n});\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* Cormorant Garamond display serif — the salt/editorial stack.
|
|
2
|
+
The @font-face blocks re-source DIGITS ONLY (U+0030-0039) to Cormorant
|
|
3
|
+
Infant: Garamond's "1" is an unflagged stem that misreads as I or l.
|
|
4
|
+
Blog themes @import their own fonts; this file is for standalone apps. */
|
|
5
|
+
@import url("https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400;1,600&display=swap");
|
|
6
|
+
|
|
7
|
+
@font-face {
|
|
8
|
+
font-family: "Cormorant Garamond";
|
|
9
|
+
font-style: normal;
|
|
10
|
+
font-weight: 300;
|
|
11
|
+
font-display: swap;
|
|
12
|
+
src: url(https://fonts.gstatic.com/s/cormorantinfant/v22/HhyPU44g9vKiM1sORYSiWeAsLN997_cV2RkDTq8.woff2) format("woff2");
|
|
13
|
+
unicode-range: U+0030-0039;
|
|
14
|
+
}
|
|
15
|
+
@font-face {
|
|
16
|
+
font-family: "Cormorant Garamond";
|
|
17
|
+
font-style: normal;
|
|
18
|
+
font-weight: 400;
|
|
19
|
+
font-display: swap;
|
|
20
|
+
src: url(https://fonts.gstatic.com/s/cormorantinfant/v22/HhyPU44g9vKiM1sORYSiWeAsLN997_cV2RkDTq8.woff2) format("woff2");
|
|
21
|
+
unicode-range: U+0030-0039;
|
|
22
|
+
}
|
|
23
|
+
@font-face {
|
|
24
|
+
font-family: "Cormorant Garamond";
|
|
25
|
+
font-style: normal;
|
|
26
|
+
font-weight: 600;
|
|
27
|
+
font-display: swap;
|
|
28
|
+
src: url(https://fonts.gstatic.com/s/cormorantinfant/v22/HhyPU44g9vKiM1sORYSiWeAsLN997_cV2RkDTq8.woff2) format("woff2");
|
|
29
|
+
unicode-range: U+0030-0039;
|
|
30
|
+
}
|
|
31
|
+
@font-face {
|
|
32
|
+
font-family: "Cormorant Garamond";
|
|
33
|
+
font-style: normal;
|
|
34
|
+
font-weight: 700;
|
|
35
|
+
font-display: swap;
|
|
36
|
+
src: url(https://fonts.gstatic.com/s/cormorantinfant/v22/HhyPU44g9vKiM1sORYSiWeAsLN997_cV2RkDTq8.woff2) format("woff2");
|
|
37
|
+
unicode-range: U+0030-0039;
|
|
38
|
+
}
|
|
39
|
+
@font-face {
|
|
40
|
+
font-family: "Cormorant Garamond";
|
|
41
|
+
font-style: italic;
|
|
42
|
+
font-weight: 400;
|
|
43
|
+
font-display: swap;
|
|
44
|
+
src: url(https://fonts.gstatic.com/s/cormorantinfant/v22/HhyJU44g9vKiM1sORYSiWeAsLN997_Il2xMEbK0UPg.woff2) format("woff2");
|
|
45
|
+
unicode-range: U+0030-0039;
|
|
46
|
+
}
|
package/fonts/lora.css
ADDED
package/fonts/plex.css
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/* IBM Plex Sans + IBM Plex Mono — the paper theme's stack.
|
|
2
|
+
HTML consumers may prefer <link> tags (add preconnects to
|
|
3
|
+
fonts.googleapis.com / fonts.gstatic.com either way). */
|
|
4
|
+
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap");
|
package/fonts/system.css
ADDED
package/index.css
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* @odla-ai/ui — bundler barrel: token defaults + every component sheet.
|
|
2
|
+
Fonts and themes are deliberate consumer choices — load them separately:
|
|
3
|
+
fonts/<stack>.css → themes/<name>/styles.css + themes/<name>/ui.css → this file.
|
|
4
|
+
File-copy consumers (no bundler) use the pre-flattened odla-ui.css instead.
|
|
5
|
+
Keep this import list in sync with scripts/gen-css.mjs. */
|
|
6
|
+
@import "./css/tokens.css";
|
|
7
|
+
@import "./css/components/buttons.css";
|
|
8
|
+
@import "./css/components/forms.css";
|
|
9
|
+
@import "./css/components/cards.css";
|
|
10
|
+
@import "./css/components/table.css";
|
|
11
|
+
@import "./css/components/nav.css";
|
|
12
|
+
@import "./css/components/feedback.css";
|
|
13
|
+
@import "./css/components/chat.css";
|
|
14
|
+
@import "./css/components/chart.css";
|
package/js/canvas.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Canvas chart primitives — pure geometry + drawing, no palette globals.
|
|
2
|
+
// Colors come from readPalette() (js/palette.js); pass them in explicitly.
|
|
3
|
+
|
|
4
|
+
// Hi-DPI canvas setup sized to the parent element. Returns the 2d context
|
|
5
|
+
// plus CSS-pixel width/height, transform pre-scaled so all drawing happens
|
|
6
|
+
// in CSS pixels.
|
|
7
|
+
export function fitCanvas(cv, { aspect = 0.52, maxH = 440, minH = 240, pad = 16 } = {}) {
|
|
8
|
+
const rect = cv.parentElement.getBoundingClientRect();
|
|
9
|
+
const w = Math.max(280, rect.width - pad);
|
|
10
|
+
const h = Math.round(Math.min(maxH, Math.max(minH, w * aspect)));
|
|
11
|
+
const dpr = window.devicePixelRatio || 1;
|
|
12
|
+
cv.width = Math.round(w * dpr);
|
|
13
|
+
cv.height = Math.round(h * dpr);
|
|
14
|
+
cv.style.width = w + "px";
|
|
15
|
+
cv.style.height = h + "px";
|
|
16
|
+
const ctx = cv.getContext("2d");
|
|
17
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
18
|
+
ctx.clearRect(0, 0, w, h);
|
|
19
|
+
return { ctx, w, h };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// "Nice" axis tick values covering [min, max] with ~n steps.
|
|
23
|
+
export function niceTicks(min, max, n = 5) {
|
|
24
|
+
const span = max - min || 1;
|
|
25
|
+
const raw = span / n;
|
|
26
|
+
const mag = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
27
|
+
const norm = raw / mag;
|
|
28
|
+
const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
|
|
29
|
+
const out = [];
|
|
30
|
+
let t = Math.ceil(min / step) * step;
|
|
31
|
+
for (; t <= max + step / 1000; t += step) out.push(Math.abs(t) < step / 1000 ? 0 : t);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Rounded-rect path (radius clamped to the box). Caller fills/strokes.
|
|
36
|
+
export function roundRect(c, x, y, w, h, r) {
|
|
37
|
+
r = Math.min(r, w / 2, h / 2);
|
|
38
|
+
c.beginPath();
|
|
39
|
+
c.moveTo(x + r, y);
|
|
40
|
+
c.arcTo(x + w, y, x + w, y + h, r);
|
|
41
|
+
c.arcTo(x + w, y + h, x, y + h, r);
|
|
42
|
+
c.arcTo(x, y + h, x, y, r);
|
|
43
|
+
c.arcTo(x, y, x + w, y, r);
|
|
44
|
+
c.closePath();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Filled band between two series. xs/loYs/hiYs are pixel arrays of equal
|
|
48
|
+
// length (percentile ribbons, confidence bands).
|
|
49
|
+
export function bandFill(c, xs, loYs, hiYs, fill) {
|
|
50
|
+
c.fillStyle = fill;
|
|
51
|
+
c.beginPath();
|
|
52
|
+
for (let i = 0; i < xs.length; i++) {
|
|
53
|
+
i === 0 ? c.moveTo(xs[i], hiYs[i]) : c.lineTo(xs[i], hiYs[i]);
|
|
54
|
+
}
|
|
55
|
+
for (let i = xs.length - 1; i >= 0; i--) c.lineTo(xs[i], loYs[i]);
|
|
56
|
+
c.closePath();
|
|
57
|
+
c.fill();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Polyline path through pixel points. Caller sets strokeStyle and strokes.
|
|
61
|
+
export function linePath(c, xs, ys) {
|
|
62
|
+
c.beginPath();
|
|
63
|
+
for (let i = 0; i < xs.length; i++) {
|
|
64
|
+
i === 0 ? c.moveTo(xs[i], ys[i]) : c.lineTo(xs[i], ys[i]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Marker dot with a contrasting ring (stroke usually palette.dotStroke).
|
|
69
|
+
export function dot(c, x, y, color, { r = 3.5, stroke } = {}) {
|
|
70
|
+
c.fillStyle = color;
|
|
71
|
+
c.beginPath();
|
|
72
|
+
c.arc(x, y, r, 0, 7);
|
|
73
|
+
c.fill();
|
|
74
|
+
if (stroke) {
|
|
75
|
+
c.strokeStyle = stroke;
|
|
76
|
+
c.lineWidth = 1.5;
|
|
77
|
+
c.stroke();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Hover tooltip: dark rounded box, one line per entry, per-line colors,
|
|
82
|
+
// flips left of the cursor near rightBound.
|
|
83
|
+
export function drawTip(c, x, y, lines, colors, { rightBound = Infinity, tipBg = "rgba(20,20,20,0.92)", font = "11px monospace", pad = 8 } = {}) {
|
|
84
|
+
c.font = font;
|
|
85
|
+
let w = 0;
|
|
86
|
+
for (const l of lines) w = Math.max(w, c.measureText(l).width);
|
|
87
|
+
w += pad * 2;
|
|
88
|
+
const h = lines.length * 15 + 10;
|
|
89
|
+
let bx = x + 12;
|
|
90
|
+
if (bx + w > rightBound) bx = x - 12 - w;
|
|
91
|
+
c.fillStyle = tipBg;
|
|
92
|
+
roundRect(c, bx, y, w, h, 4);
|
|
93
|
+
c.fill();
|
|
94
|
+
c.textAlign = "left";
|
|
95
|
+
c.textBaseline = "top";
|
|
96
|
+
lines.forEach((l, i) => {
|
|
97
|
+
c.fillStyle = colors[i] || colors[colors.length - 1];
|
|
98
|
+
c.fillText(l, bx + pad, y + 6 + i * 15);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Coalesce a burst of events (slider drags, resizes) into one rAF render.
|
|
103
|
+
export function rafThrottle(fn) {
|
|
104
|
+
let pending = false;
|
|
105
|
+
return (...args) => {
|
|
106
|
+
if (pending) return;
|
|
107
|
+
pending = true;
|
|
108
|
+
requestAnimationFrame(() => {
|
|
109
|
+
pending = false;
|
|
110
|
+
fn(...args);
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
}
|
package/js/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// @odla-ai/ui vanilla barrel — buildless-importable ESM.
|
|
2
|
+
// CSS lives in index.css / odla-ui.css / themes/; React/Preact wrappers in
|
|
3
|
+
// the "@odla-ai/ui/components" subpath.
|
|
4
|
+
export {
|
|
5
|
+
UI_TOKENS,
|
|
6
|
+
REQUIRED_TOKENS,
|
|
7
|
+
DEFAULTED_TOKENS,
|
|
8
|
+
CHART_TOKENS,
|
|
9
|
+
CHAT_TOKENS,
|
|
10
|
+
THEMES,
|
|
11
|
+
BLOG_THEMES,
|
|
12
|
+
} from "./tokens.js";
|
|
13
|
+
export { THEME_NO_FLASH, initTheme, currentTheme, toggleTheme, bindThemeToggle } from "./theme.js";
|
|
14
|
+
export { cssVar, readPalette } from "./palette.js";
|
|
15
|
+
export {
|
|
16
|
+
fitCanvas,
|
|
17
|
+
niceTicks,
|
|
18
|
+
roundRect,
|
|
19
|
+
bandFill,
|
|
20
|
+
linePath,
|
|
21
|
+
dot,
|
|
22
|
+
drawTip,
|
|
23
|
+
rafThrottle,
|
|
24
|
+
} from "./canvas.js";
|
package/js/palette.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Chart/UI palette read from the --ui-* custom properties, so canvas
|
|
2
|
+
// rendering and the stylesheet can never drift (single source of truth).
|
|
3
|
+
// Re-read after toggleTheme() — computed values change with the mode.
|
|
4
|
+
|
|
5
|
+
export function cssVar(name, root = document.documentElement) {
|
|
6
|
+
return getComputedStyle(root).getPropertyValue(name).trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function readPalette(root = document.documentElement) {
|
|
10
|
+
const cs = getComputedStyle(root);
|
|
11
|
+
const g = (n) => cs.getPropertyValue(n).trim() || "#000";
|
|
12
|
+
return {
|
|
13
|
+
bg: g("--ui-bg"),
|
|
14
|
+
surface: g("--ui-surface"),
|
|
15
|
+
text: g("--ui-text"),
|
|
16
|
+
muted: g("--ui-text-muted"),
|
|
17
|
+
faint: g("--ui-text-faint"),
|
|
18
|
+
border: g("--ui-border"),
|
|
19
|
+
accent: g("--ui-accent"),
|
|
20
|
+
good: g("--ui-good"),
|
|
21
|
+
warn: g("--ui-warn"),
|
|
22
|
+
danger: g("--ui-danger"),
|
|
23
|
+
series: [1, 2, 3, 4, 5, 6].map((i) => g(`--ui-chart-${i}`)),
|
|
24
|
+
grid: g("--ui-chart-grid"),
|
|
25
|
+
gridZero: g("--ui-chart-grid-zero"),
|
|
26
|
+
axis: g("--ui-chart-axis"),
|
|
27
|
+
axisFaint: g("--ui-chart-axis-faint"),
|
|
28
|
+
band: g("--ui-chart-band"),
|
|
29
|
+
bandStrong: g("--ui-chart-band-strong"),
|
|
30
|
+
pos: g("--ui-chart-pos"),
|
|
31
|
+
neg: g("--ui-chart-neg"),
|
|
32
|
+
tipBg: g("--ui-chart-tip-bg"),
|
|
33
|
+
tipText: g("--ui-chart-tip-text"),
|
|
34
|
+
dotStroke: g("--ui-chart-dot-stroke"),
|
|
35
|
+
fontMono: g("--ui-font-mono") || "monospace",
|
|
36
|
+
};
|
|
37
|
+
}
|