@hearthlog/ui 0.0.0-stage → 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 (63) hide show
  1. package/LICENSE +235 -0
  2. package/package.json +37 -3
  3. package/src/assets/fonts/fira-code-latin-ext-ui.woff2 +0 -0
  4. package/src/assets/fonts/fira-code-latin-ui.woff2 +0 -0
  5. package/src/assets/fonts/inter-latin-ext-ui.woff2 +0 -0
  6. package/src/assets/fonts/inter-latin-ui.woff2 +0 -0
  7. package/src/components/ConfirmDialog.svelte +104 -0
  8. package/src/components/Drawer.svelte +135 -0
  9. package/src/components/EmptyState.svelte +33 -0
  10. package/src/components/JsonViewer.svelte +113 -0
  11. package/src/components/Modal.svelte +76 -0
  12. package/src/components/NotificationCenter.svelte +106 -0
  13. package/src/components/PageHeader.svelte +26 -0
  14. package/src/components/commands/CommandConfirmDialog.svelte +32 -0
  15. package/src/components/commands/CommandDrawer.svelte +31 -0
  16. package/src/components/commands/CommandFieldset.svelte +18 -0
  17. package/src/components/commands/CommandModal.svelte +25 -0
  18. package/src/components/commands/CommandOnly.svelte +26 -0
  19. package/src/components/data/DataPlaceholder.svelte +8 -0
  20. package/src/components/data/DataRegion.svelte +8 -0
  21. package/src/components/data/DataValue.svelte +11 -0
  22. package/src/components/data-table/DataTable.svelte +139 -0
  23. package/src/components/data-table/DataTableHeader.svelte +78 -0
  24. package/src/components/data-table/DataTablePagination.svelte +91 -0
  25. package/src/components/data-table/table.ts +30 -0
  26. package/src/components/data-table/types.ts +63 -0
  27. package/src/components/endless/DayPager.svelte +355 -0
  28. package/src/components/endless/EndlessList.svelte +498 -0
  29. package/src/components/endless/bucket-store.svelte.ts +81 -0
  30. package/src/components/json-viewer.ts +49 -0
  31. package/src/components/ui/Amount.svelte +17 -0
  32. package/src/components/ui/Badge.svelte +40 -0
  33. package/src/components/ui/Button.svelte +53 -0
  34. package/src/components/ui/Checkbox.svelte +29 -0
  35. package/src/components/ui/ChoiceChips.svelte +53 -0
  36. package/src/components/ui/CollectionToolbar.svelte +29 -0
  37. package/src/components/ui/DatePicker.svelte +375 -0
  38. package/src/components/ui/DateTimePicker.svelte +16 -0
  39. package/src/components/ui/Field.svelte +52 -0
  40. package/src/components/ui/FoldLine.svelte +38 -0
  41. package/src/components/ui/Input.svelte +21 -0
  42. package/src/components/ui/Label.svelte +18 -0
  43. package/src/components/ui/Metric.svelte +34 -0
  44. package/src/components/ui/MonthNavigator.svelte +194 -0
  45. package/src/components/ui/Panel.svelte +37 -0
  46. package/src/components/ui/RouteTabs.svelte +57 -0
  47. package/src/components/ui/Select.svelte +93 -0
  48. package/src/components/ui/Switch.svelte +41 -0
  49. package/src/components/ui/Textarea.svelte +19 -0
  50. package/src/components/ui/Tooltip.svelte +53 -0
  51. package/src/components/ui/tab-indicator.ts +84 -0
  52. package/src/state/commands.ts +22 -0
  53. package/src/state/notifications.svelte.ts +49 -0
  54. package/src/state/page-context.ts +9 -0
  55. package/src/state/readiness.ts +20 -0
  56. package/src/state/sheet-stack.svelte.ts +49 -0
  57. package/src/styles/base.css +243 -0
  58. package/src/styles/index.css +13 -0
  59. package/src/styles/recipes.css +174 -0
  60. package/src/utils/cn.ts +6 -0
  61. package/src/utils/endless-axis.ts +153 -0
  62. package/src/utils/number-format.ts +82 -0
  63. package/README.md +0 -3
@@ -0,0 +1,49 @@
1
+ export type NotificationType = "success" | "error" | "info" | "progress";
2
+
3
+ export interface NotificationAction {
4
+ label: string;
5
+ run: () => void | Promise<void>;
6
+ }
7
+
8
+ export interface AppNotification {
9
+ id: string;
10
+ type: NotificationType;
11
+ message: string;
12
+ action?: NotificationAction;
13
+ duration: number;
14
+ }
15
+
16
+ export const notificationState = $state<{ items: AppNotification[] }>({
17
+ items: [],
18
+ });
19
+
20
+ function push(
21
+ type: NotificationType,
22
+ message: string,
23
+ action?: NotificationAction,
24
+ duration = action ? 8000 : 4500,
25
+ ): string {
26
+ const id = crypto.randomUUID();
27
+ notificationState.items.push({ id, type, message, action, duration });
28
+ return id;
29
+ }
30
+
31
+ export function dismissNotification(id: string): void {
32
+ notificationState.items = notificationState.items.filter(
33
+ (item) => item.id !== id,
34
+ );
35
+ }
36
+
37
+ export const notify = {
38
+ success: (message: string, action?: NotificationAction) =>
39
+ push("success", message, action),
40
+ error: (message: string, action?: NotificationAction) =>
41
+ push("error", message, action, 7000),
42
+ info: (message: string, action?: NotificationAction) =>
43
+ push("info", message, action),
44
+ /**
45
+ * A sticky notification for an async task. The caller keeps the returned
46
+ * id and dismisses it with `dismissNotification` when the task settles.
47
+ */
48
+ progress: (message: string) => push("progress", message, undefined, 0),
49
+ };
@@ -0,0 +1,9 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const pageResourceContext = Symbol('page-resource');
3
+ type PageContext = { loading: boolean; error?: unknown; measurement?: { id: string } };
4
+ export function providePageResource(resource: () => PageContext): void {
5
+ setContext(pageResourceContext, resource);
6
+ }
7
+ export function currentPageResource(): (() => PageContext) | undefined {
8
+ return getContext(pageResourceContext);
9
+ }
@@ -0,0 +1,20 @@
1
+ import { get, writable } from 'svelte/store';
2
+
3
+ /** Read readiness is separate from both the mounted shell and network sync. */
4
+ export const workspaceReadiness = writable({ ready: false, key: '', error: null as unknown });
5
+
6
+ export function beginWorkspaceTransition(): void {
7
+ workspaceReadiness.update((state) => ({ ...state, ready: false, error: null }));
8
+ }
9
+
10
+ export function completeWorkspaceTransition(key: string): void {
11
+ workspaceReadiness.set({ ready: true, key, error: null });
12
+ }
13
+
14
+ export function failWorkspaceTransition(error: unknown): void {
15
+ workspaceReadiness.update((state) => ({ ...state, ready: false, error }));
16
+ }
17
+
18
+ export function workspaceIsReady(): boolean {
19
+ return get(workspaceReadiness).ready;
20
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The sheets open on screen, oldest first. On phones a sheet opened from another
3
+ * sheet takes its place: the ones below hide, the top one shows a back arrow
4
+ * that closes it (the sheet below comes back as it was left), and its × closes
5
+ * them all. `Drawer` and `Modal` register here while they are open.
6
+ */
7
+ import { untrack } from 'svelte';
8
+
9
+ interface OpenSheet {
10
+ id: number;
11
+ close: () => void;
12
+ }
13
+
14
+ class SheetStack {
15
+ entries = $state<OpenSheet[]>([]);
16
+ #next = 0;
17
+
18
+ open(close: () => void): number {
19
+ const id = ++this.#next;
20
+ // Called from the sheet's effect: writing must not make that effect depend on the stack.
21
+ untrack(() => { this.entries = [...this.entries, { id, close }]; });
22
+ return id;
23
+ }
24
+
25
+ remove(id: number): void {
26
+ untrack(() => { this.entries = this.entries.filter((entry) => entry.id !== id); });
27
+ }
28
+
29
+ indexOf(id: number | null): number {
30
+ return id === null ? -1 : this.entries.findIndex((entry) => entry.id === id);
31
+ }
32
+
33
+ /** Whether another sheet sits on top of this one. */
34
+ covered(id: number | null): boolean {
35
+ const index = this.indexOf(id);
36
+ return index >= 0 && index < this.entries.length - 1;
37
+ }
38
+
39
+ /** Whether this sheet was opened over another one. */
40
+ stacked(id: number | null): boolean {
41
+ return this.indexOf(id) > 0;
42
+ }
43
+
44
+ closeAll(): void {
45
+ for (const entry of [...this.entries].reverse()) entry.close();
46
+ }
47
+ }
48
+
49
+ export const sheetStack = new SheetStack();
@@ -0,0 +1,243 @@
1
+ /* Keep the first rendered font for this navigation if a local font arrives late. */
2
+ @font-face {
3
+ font-family: 'Inter Variable';
4
+ font-style: normal;
5
+ font-display: optional;
6
+ font-weight: 100 900;
7
+ src: url('../assets/fonts/inter-latin-ui.woff2') format('woff2-variations');
8
+ unicode-range: U+0020-007E, U+00A0, U+00B0, U+00B5, U+00B7, U+00C1, U+00C4, U+00C9, U+00CD, U+00D3, U+00D4, U+00D7, U+00DA, U+00DD, U+00E1, U+00E4, U+00E9, U+00ED, U+00F3, U+00F4, U+00FA, U+00FD, U+2013-2014, U+2018-201D, U+2022, U+2026, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215;
9
+ }
10
+ @font-face {
11
+ font-family: 'Inter Variable';
12
+ font-style: normal;
13
+ font-display: optional;
14
+ font-weight: 100 900;
15
+ src: url('../assets/fonts/inter-latin-ext-ui.woff2') format('woff2-variations');
16
+ unicode-range: U+010C-010D, U+010E-0111, U+011A-011B, U+0139-013A, U+013D-013E, U+0147-0148, U+0154-0159, U+0160-0165, U+016E-0171, U+0179-017E;
17
+ }
18
+ @font-face {
19
+ font-family: 'Fira Code Variable';
20
+ font-style: normal;
21
+ font-display: optional;
22
+ font-weight: 300 700;
23
+ src: url('../assets/fonts/fira-code-latin-ui.woff2') format('woff2-variations');
24
+ unicode-range: U+0020-007E, U+00A0, U+00B0, U+00B5, U+00B7, U+00C1, U+00C4, U+00C9, U+00CD, U+00D3, U+00D4, U+00D7, U+00DA, U+00DD, U+00E1, U+00E4, U+00E9, U+00ED, U+00F3, U+00F4, U+00FA, U+00FD, U+2013-2014, U+2018-201D, U+2022, U+2026, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215;
25
+ }
26
+ @font-face {
27
+ font-family: 'Fira Code Variable';
28
+ font-style: normal;
29
+ font-display: optional;
30
+ font-weight: 300 700;
31
+ src: url('../assets/fonts/fira-code-latin-ext-ui.woff2') format('woff2-variations');
32
+ unicode-range: U+010C-010D, U+010E-0111, U+011A-011B, U+0139-013A, U+013D-013E, U+0147-0148, U+0154-0159, U+0160-0165, U+016E-0171, U+0179-017E;
33
+ }
34
+ @theme {
35
+ --font-sans: var(--stor-font-sans);
36
+ --font-mono: var(--stor-font-mono);
37
+ /* Push desktop-plus breakpoint so multi-column only appears on very wide screens */
38
+ --breakpoint-2xl: 1920px;
39
+ /* Four radii: 8 small tiles, 12 controls and inner blocks, 20 panels, full pills.
40
+ md and 2xl fold into that set instead of adding 6px and 16px corners. */
41
+ --radius-md: 0.5rem;
42
+ --radius-2xl: 1.25rem;
43
+ }
44
+ :root {
45
+ /* Stacking scale. Shell chrome stays at z-10 to z-55; everything that floats above a page uses these. */
46
+ --stor-layer-sticky: 10;
47
+ --stor-layer-sheet: 70;
48
+ --stor-layer-sheet-content: 71;
49
+ --stor-layer-menu: 90;
50
+ --stor-layer-menu-sub: 91;
51
+ --stor-layer-dialog: 100;
52
+ --stor-layer-dialog-content: 101;
53
+ --stor-layer-toast: 110;
54
+ --stor-layer-tooltip: 120;
55
+ --stor-font-serif: ui-serif, "New York", "Iowan Old Style", Charter, Georgia, "Times New Roman", serif;
56
+ color-scheme: light;
57
+ font-family: var(--stor-font-sans);
58
+ background: var(--stor-bg);
59
+ /* Shared shell geometry; page actions and content clearance use the same stack. */
60
+ --stor-mobile-header-height: calc(3.25rem + env(safe-area-inset-top));
61
+ --stor-mobile-nav-height: 4rem;
62
+ --stor-mobile-bottom-inset: max(12px, env(safe-area-inset-bottom));
63
+ --stor-mobile-action-height: 2.75rem;
64
+ --stor-mobile-stack-gap: 10px;
65
+ --stor-section-gap: 16px;
66
+ /* Padding at least the 20px panel radius keeps content clear of the curve. */
67
+ --stor-panel-padding: 20px;
68
+ --stor-panel-radius: 20px;
69
+ --stor-sidebar-width: 224px;
70
+ /* Page gutters, panel padding and gaps; phones tighten them below. */
71
+ --stor-sidebar-rail-width: 5rem;
72
+ /* One short panel slide keeps the form legible as soon as it opens. */
73
+ --stor-drawer-shift: 1.5rem;
74
+ --stor-drawer-enter-motion: 190ms cubic-bezier(0.2, 0.8, 0.2, 1);
75
+ --stor-drawer-enter-fade: 150ms ease-out;
76
+ --stor-drawer-exit-motion: 150ms ease-in;
77
+ --stor-drawer-exit-fade: 120ms ease-in;
78
+ --stor-overlay-enter-fade: 180ms ease-out;
79
+ --stor-overlay-exit-fade: 150ms ease-in;
80
+ /* The selected tab pill slides between triggers. */
81
+ --stor-tab-motion: 280ms cubic-bezier(0.22, 1, 0.36, 1);
82
+ }
83
+ html[data-theme="dark"] {
84
+ color-scheme: dark;
85
+ }
86
+ html {
87
+ overflow: hidden;
88
+ background: var(--stor-bg);
89
+ }
90
+ body {
91
+ margin: 0;
92
+ font-size: 14px;
93
+ font-variant-numeric: tabular-nums;
94
+ min-width: 320px;
95
+ background: var(--stor-bg);
96
+ color: var(--stor-ink);
97
+ }
98
+ body,
99
+ button,
100
+ input,
101
+ select,
102
+ textarea {
103
+ transition:
104
+ background-color 160ms ease,
105
+ border-color 160ms ease,
106
+ color 160ms ease;
107
+ }
108
+ button,
109
+ a,
110
+ input,
111
+ select,
112
+ textarea {
113
+ outline-color: var(--stor-focus);
114
+ outline-offset: 2px;
115
+ }
116
+ button:focus-visible,
117
+ a:focus-visible,
118
+ input:focus-visible,
119
+ select:focus-visible,
120
+ textarea:focus-visible {
121
+ outline: 2px solid var(--stor-focus);
122
+ }
123
+ ::selection {
124
+ background: var(--stor-accent-soft);
125
+ color: var(--stor-accent-strong);
126
+ }
127
+ .mobile-scroll {
128
+ scrollbar-width: none;
129
+ }
130
+ .mobile-scroll::-webkit-scrollbar {
131
+ display: none;
132
+ }
133
+ input, select, textarea { accent-color: var(--stor-action); }
134
+ [class~="bg-[var(--stor-action)]"] { color: var(--stor-action-text); }
135
+ [class~="data-[selected]:bg-[var(--stor-action)]"][data-selected],
136
+ [class~="data-[state=checked]:bg-[var(--stor-action)]"][data-state="checked"] {
137
+ color: var(--stor-action-text);
138
+ }
139
+ @media (prefers-reduced-motion: reduce) {
140
+ *, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0s !important; animation-duration: 0s !important; }
141
+ }
142
+ /*
143
+ * Compatibility layer for existing Tailwind color utilities. Shared semantic
144
+ * tokens remain the source of truth while route markup is migrated naturally.
145
+ */
146
+ html[data-theme="dark"] [class~="bg-white"] { background-color: var(--stor-surface) !important; }
147
+ html[data-theme="dark"] [class~="bg-white/95"] { background-color: color-mix(in srgb, var(--stor-surface), transparent 5%) !important; }
148
+ html[data-theme="dark"] [class~="bg-white/90"] { background-color: color-mix(in srgb, var(--stor-surface), transparent 10%) !important; }
149
+ html[data-theme="dark"] [class~="bg-white/80"] { background-color: color-mix(in srgb, var(--stor-surface), transparent 20%) !important; }
150
+ html[data-theme="dark"] [class~="bg-white/70"] { background-color: color-mix(in srgb, var(--stor-surface), transparent 30%) !important; }
151
+ html[data-theme="dark"] [class~="bg-white/60"] { background-color: color-mix(in srgb, var(--stor-surface), transparent 40%) !important; }
152
+ html[data-theme="dark"] [class~="bg-white/50"] { background-color: color-mix(in srgb, var(--stor-surface), transparent 50%) !important; }
153
+ html[data-theme="dark"] [class~="bg-stone-50"],
154
+ html[data-theme="dark"] [class~="bg-stone-100"],
155
+ html[data-theme="dark"] [class~="bg-gray-50"],
156
+ html[data-theme="dark"] [class~="bg-gray-100"] { background-color: var(--stor-surface-muted) !important; }
157
+ html[data-theme="dark"] [class~="bg-stone-200"],
158
+ html[data-theme="dark"] [class~="bg-gray-200"] { background-color: var(--stor-border) !important; }
159
+ html[data-theme="dark"] [class~="text-stone-950"],
160
+ html[data-theme="dark"] [class~="text-stone-900"],
161
+ html[data-theme="dark"] [class~="text-stone-800"],
162
+ html[data-theme="dark"] [class~="text-gray-950"],
163
+ html[data-theme="dark"] [class~="text-gray-900"],
164
+ html[data-theme="dark"] [class~="text-gray-800"] { color: var(--stor-ink) !important; }
165
+ html[data-theme="dark"] [class~="text-stone-700"],
166
+ html[data-theme="dark"] [class~="text-stone-600"],
167
+ html[data-theme="dark"] [class~="text-stone-500"],
168
+ html[data-theme="dark"] [class~="text-gray-700"],
169
+ html[data-theme="dark"] [class~="text-gray-600"],
170
+ html[data-theme="dark"] [class~="text-gray-500"] { color: var(--stor-muted-text) !important; }
171
+ html[data-theme="dark"] [class~="border-stone-100"],
172
+ html[data-theme="dark"] [class~="border-stone-200"],
173
+ html[data-theme="dark"] [class~="border-gray-100"],
174
+ html[data-theme="dark"] [class~="border-gray-200"] { border-color: var(--stor-border) !important; }
175
+ html[data-theme="dark"] [class~="border-stone-300"],
176
+ html[data-theme="dark"] [class~="border-gray-300"] { border-color: var(--stor-field-border) !important; }
177
+ html[data-theme="dark"] [class~="ring-stone-200"],
178
+ html[data-theme="dark"] [class~="ring-gray-200"] { --tw-ring-color: var(--stor-border) !important; }
179
+ html[data-theme="dark"] [class~="hover:bg-white"]:hover,
180
+ html[data-theme="dark"] [class~="focus:bg-white"]:focus,
181
+ html[data-theme="dark"] [class~="focus-within:bg-white"]:focus-within,
182
+ html[data-theme="dark"] [class~="data-[state=active]:bg-white"][data-state="active"] {
183
+ background-color: var(--stor-surface-hover) !important;
184
+ }
185
+ html[data-theme="dark"] [class~="hover:text-stone-950"]:hover,
186
+ html[data-theme="dark"] [class~="hover:text-stone-900"]:hover,
187
+ html[data-theme="dark"] [class~="hover:text-stone-800"]:hover {
188
+ color: var(--stor-ink) !important;
189
+ }
190
+ html[data-theme="dark"] [class~="bg-red-50"],
191
+ html[data-theme="dark"] [class~="bg-red-50/80"],
192
+ html[data-theme="dark"] [class~="bg-red-50/35"],
193
+ html[data-theme="dark"] [class~="bg-red-100"],
194
+ html[data-theme="dark"] [class~="bg-red-100/70"] { background-color: var(--stor-negative-soft) !important; }
195
+ html[data-theme="dark"] [class~="text-red-600"],
196
+ html[data-theme="dark"] [class~="text-red-700"],
197
+ html[data-theme="dark"] [class~="text-red-800"],
198
+ html[data-theme="dark"] [class~="text-red-900"] { color: var(--stor-negative) !important; }
199
+ html[data-theme="dark"] [class~="border-red-100"],
200
+ html[data-theme="dark"] [class~="border-red-200"],
201
+ html[data-theme="dark"] [class~="border-red-300"] { border-color: color-mix(in srgb, var(--stor-negative), transparent 35%) !important; }
202
+ html[data-theme="dark"] [class~="bg-amber-50"],
203
+ html[data-theme="dark"] [class~="bg-amber-50/80"],
204
+ html[data-theme="dark"] [class~="bg-amber-100"] { background-color: var(--stor-warning-soft) !important; }
205
+ html[data-theme="dark"] [class~="text-amber-600"],
206
+ html[data-theme="dark"] [class~="text-amber-700"],
207
+ html[data-theme="dark"] [class~="text-amber-800"],
208
+ html[data-theme="dark"] [class~="text-amber-900"],
209
+ html[data-theme="dark"] [class~="text-amber-950"] { color: var(--stor-warning) !important; }
210
+ html[data-theme="dark"] [class~="border-amber-100"],
211
+ html[data-theme="dark"] [class~="border-amber-200"],
212
+ html[data-theme="dark"] [class~="border-amber-300"] { border-color: color-mix(in srgb, var(--stor-warning), transparent 35%) !important; }
213
+ html[data-theme="dark"] [class~="bg-emerald-50"],
214
+ html[data-theme="dark"] [class~="bg-emerald-100"],
215
+ html[data-theme="dark"] [class~="bg-green-50"],
216
+ html[data-theme="dark"] [class~="bg-green-100"] { background-color: var(--stor-positive-soft) !important; }
217
+ html[data-theme="dark"] [class~="text-emerald-600"],
218
+ html[data-theme="dark"] [class~="text-emerald-700"],
219
+ html[data-theme="dark"] [class~="text-emerald-800"],
220
+ html[data-theme="dark"] [class~="text-emerald-900"],
221
+ html[data-theme="dark"] [class~="text-green-600"],
222
+ html[data-theme="dark"] [class~="text-green-700"],
223
+ html[data-theme="dark"] [class~="text-green-800"] { color: var(--stor-positive) !important; }
224
+ html[data-theme="dark"] [class~="border-emerald-200"] { border-color: color-mix(in srgb, var(--stor-positive), transparent 35%) !important; }
225
+ html[data-theme="dark"] [class~="bg-sky-50"],
226
+ html[data-theme="dark"] [class~="bg-sky-50/60"],
227
+ html[data-theme="dark"] [class~="bg-sky-100"],
228
+ html[data-theme="dark"] [class~="bg-blue-50"],
229
+ html[data-theme="dark"] [class~="bg-blue-100"] { background-color: var(--stor-info-soft) !important; }
230
+ html[data-theme="dark"] [class~="text-sky-700"],
231
+ html[data-theme="dark"] [class~="text-sky-800"],
232
+ html[data-theme="dark"] [class~="text-sky-950"],
233
+ html[data-theme="dark"] [class~="text-blue-600"],
234
+ html[data-theme="dark"] [class~="text-blue-700"],
235
+ html[data-theme="dark"] [class~="text-blue-800"] { color: var(--stor-info) !important; }
236
+ html[data-theme="dark"] [class~="border-sky-200"] { border-color: var(--stor-info-border) !important; }
237
+ @media print {
238
+ *, *::before, *::after {
239
+ color-scheme: light !important;
240
+ box-shadow: none !important;
241
+ }
242
+ }
243
+ /* Borderless surfaces: ordinary information stays on the canvas. */
@@ -0,0 +1,13 @@
1
+ /*
2
+ * The design system's stylesheet: fonts, design tokens, base styles, the dark-mode remap of literal
3
+ * palette classes, and the generic recipes (panel, page, metric, menu, sheet, tabs, drawer...).
4
+ * Import it once, after Tailwind:
5
+ *
6
+ * @import 'tailwindcss' source('./');
7
+ * @import '@hearthlog/ui/styles/index.css';
8
+ */
9
+ @import './base.css';
10
+ @import './recipes.css';
11
+
12
+ /* The package's own components use Tailwind classes; scan them wherever the host's source root is. */
13
+ @source '../';
@@ -0,0 +1,174 @@
1
+ @layer components {
2
+ .stor-page { display: flex; min-width: 0; flex-direction: column; gap: var(--stor-section-gap); }
3
+ .stor-panel, .stor-today-card { border: 0; border-radius: var(--stor-panel-radius); background: var(--stor-surface); box-shadow: var(--stor-elevation-1); }
4
+ .stor-panel { overflow: hidden; }
5
+ .stor-panel-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: var(--stor-panel-padding); background: transparent; }
6
+ .stor-panel-body { padding: var(--stor-panel-padding); }
7
+ .stor-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
8
+ .stor-metric { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
9
+ .stor-metric-label { font-size: 13px; font-weight: 500; color: var(--stor-muted-text); }
10
+ .stor-metric-value { font-size: 28px; line-height: 1.2; font-weight: 600; letter-spacing: -.025em; color: var(--stor-ink); overflow-wrap: anywhere; }
11
+ .stor-metric-support { font-size: 12px; line-height: 16px; color: var(--stor-muted-text); }
12
+ /* A currency or unit after a large figure: smaller and quieter than the number. */
13
+ .stor-unit { font-size: .6em; font-weight: 600; letter-spacing: 0; color: var(--stor-muted-text); }
14
+ /* Elevation recipes. A component picks a level, never its own shadow (see docs/design-concepts/depth-2026-10-04). */
15
+ .stor-well { background: var(--stor-surface-well); box-shadow: var(--stor-well-shadow); }
16
+ .stor-menu { background: var(--stor-surface-overlay); box-shadow: var(--stor-elevation-3); border-radius: 16px; }
17
+ .stor-sheet { background: var(--stor-surface-sheet); box-shadow: var(--stor-elevation-4); }
18
+ /* Buttons: the primary and destructive faces carry a soft top highlight; pressing sinks them. */
19
+ [data-stor-button-variant="default"], [data-stor-button-variant="destructive"] {
20
+ background-image: linear-gradient(180deg, rgb(255 255 255 / .14), rgb(255 255 255 / 0));
21
+ box-shadow: 0 0 0 1px rgb(0 0 0 / .1), 0 1px 2px rgb(0 0 0 / .16), 0 4px 10px -4px rgb(0 0 0 / .22), inset 0 1px 0 rgb(255 255 255 / .22);
22
+ }
23
+ [data-stor-button-variant="default"]:active, [data-stor-button-variant="destructive"]:active { box-shadow: 0 0 0 1px rgb(0 0 0 / .12), inset 0 1px 2px rgb(0 0 0 / .24); }
24
+ [data-stor-button-variant="secondary"]:active { box-shadow: var(--stor-elevation-pressed); }
25
+ .stor-page-header { min-width: 0; padding-bottom: 4px; }
26
+ .stor-page-header h1, .stor-health-header h1 { font-size: 24px; line-height: 1.2; }
27
+ /* Section pages let the navigation name the place: the title is read by
28
+ assistive tech and shown as a compact label on mobile only. */
29
+ .stor-page-title { margin-bottom: 12px; }
30
+ .stor-page-label { margin-bottom: 8px; font-size: 18px; font-weight: 600; letter-spacing: -.01em; color: var(--stor-ink); }
31
+ /* Serif titles: page and overlay titles only. Controls, data, and body copy stay in Inter.
32
+ A system serif (New York on Apple devices) keeps first paint free of an extra font. */
33
+ .stor-page-title, .stor-page-label, .stor-serif-title { font-family: var(--stor-font-serif); font-weight: 600; letter-spacing: -.012em; font-optical-sizing: auto; }
34
+ .stor-page-header-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; }
35
+ /* Tabs: one capsule track with a raised active pill. Page-level links use
36
+ .stor-primary-tabs; compact in-panel rows use .stor-section-tabs. */
37
+ .stor-page-tabs, .stor-primary-tabs, .stor-section-tabs { position: relative; gap: 2px; border-radius: 9999px; background: var(--stor-surface-well); padding: 3px; box-shadow: var(--stor-well-shadow); }
38
+ .stor-page-tab { display: inline-flex; min-height: 44px; align-items: center; justify-content: center; flex-shrink: 0; gap: 8px; border-radius: 9999px; padding: 8px 16px; color: var(--stor-muted-text); font-size: 13px; font-weight: 600; white-space: nowrap; transition: color 160ms ease, background-color 160ms ease, box-shadow 160ms ease; }
39
+ .stor-page-tab:hover { color: var(--stor-ink); background: var(--stor-surface-hover); }
40
+ .stor-page-tab:focus-visible { outline: 2px solid var(--stor-focus); outline-offset: 2px; }
41
+ .stor-page-tab:is([data-state="active"], [aria-selected="true"], [aria-current="page"], [aria-current="true"]) { color: var(--stor-accent-strong); }
42
+ /* The measured pill replaces the selected tab's own background so it can slide. */
43
+ .stor-tabs-sliding .stor-page-tab { position: relative; z-index: 1; }
44
+ .stor-tabs-sliding .stor-page-tab:is([data-state="active"], [aria-selected="true"], [aria-current="page"], [aria-current="true"]) { background: transparent; box-shadow: none; }
45
+ .stor-tab-indicator { position: absolute; top: 3px; bottom: 3px; left: 0; width: var(--stor-tab-w, 0px); border-radius: 9999px; opacity: 0; pointer-events: none; transform: translateX(var(--stor-tab-x, 0px)); }
46
+ .stor-tabs-sliding .stor-tab-indicator { opacity: 1; }
47
+ .stor-tabs-ready .stor-tab-indicator { transition: transform var(--stor-tab-motion), width var(--stor-tab-motion), opacity 160ms ease; }
48
+ .stor-primary-tabs { width: fit-content; max-width: 100%; }
49
+ .stor-primary-tabs .stor-page-tab { font-size: 14px; padding-inline: 18px; }
50
+ /* Five sections fit a phone's row without scrolling the last one out of sight. */
51
+ @media (max-width: 639px) { .stor-primary-tabs .stor-page-tab { padding-inline: 12px; } }
52
+ .stor-section-tabs { width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); }
53
+ .stor-section-tabs .stor-page-tab { min-width: 0; padding: 8px 12px; }
54
+ .stor-tab-count { display: inline-flex; min-width: 24px; align-items: center; justify-content: center; border-radius: 999px; background: color-mix(in srgb, var(--stor-border) 50%, transparent); padding: 2px 6px; color: var(--stor-muted-text); font-size: 11px; font-variant-numeric: tabular-nums; line-height: 1.25; transition: background-color 160ms ease, color 160ms ease; }
55
+ .stor-page-tab:is([data-state="active"], [aria-selected="true"], [aria-current="page"], [aria-current="true"]) .stor-tab-count { background: var(--stor-accent-soft); color: var(--stor-accent-strong); }
56
+ @media (min-width: 640px) {
57
+ .stor-page-tab { min-height: 38px; padding-block: 7px; }
58
+ .stor-section-tabs { width: fit-content; grid-template-columns: repeat(2, minmax(140px, 1fr)); }
59
+ }
60
+ .stor-segmented-control { gap: 2px; border-radius: 9999px; background: var(--stor-surface-well); padding: 3px; box-shadow: var(--stor-well-shadow); }
61
+ .stor-segmented-option { min-height: 36px; border-radius: 9999px; padding-inline: 12px; color: var(--stor-muted-text); font-size: 12px; font-weight: 600; }
62
+ .stor-segmented-option[aria-pressed="true"], .stor-segmented-option[data-state="active"] { color: var(--stor-accent-strong); }
63
+ /* The selected pill: navigation, page tabs, segmented options and the sliding indicators. */
64
+ .stor-nav-selected, .stor-nav-indicator, .stor-tab-indicator,
65
+ .stor-page-tab:is([data-state="active"], [aria-selected="true"], [aria-current="page"], [aria-current="true"]),
66
+ .stor-segmented-option:is([aria-pressed="true"], [data-state="active"]) { background: var(--stor-surface-raised); box-shadow: var(--stor-elevation-2); }
67
+ .stor-nav-sliding .stor-nav-selected,
68
+ .stor-tabs-sliding .stor-page-tab:is([data-state="active"], [aria-selected="true"], [aria-current="page"], [aria-current="true"]) { background: transparent; box-shadow: none; }
69
+ }
70
+
71
+ .stor-drawer {
72
+ height: 100dvh; max-width: none; background: var(--stor-surface-sheet); box-shadow: var(--stor-elevation-4);
73
+ transition:
74
+ transform var(--stor-drawer-enter-motion),
75
+ opacity var(--stor-drawer-enter-fade);
76
+ }
77
+
78
+ .stor-drawer-overlay {
79
+ transition: opacity var(--stor-overlay-enter-fade);
80
+ }
81
+
82
+ @media (max-width: 639px) {
83
+ .stor-drawer-footer > div > button[data-stor-button-variant="default"]:last-child { flex: 1; }
84
+ }
85
+
86
+ /* First paint of a mounted drawer slides it in from just off the edge. */
87
+
88
+ @starting-style {
89
+ .stor-drawer {
90
+ opacity: 0;
91
+ transform: translateX(var(--stor-drawer-shift));
92
+ }
93
+ .stor-drawer-overlay {
94
+ opacity: 0;
95
+ }
96
+ }
97
+
98
+ .stor-drawer[data-ending-style] {
99
+ opacity: 0;
100
+ transform: translateX(var(--stor-drawer-shift));
101
+ transition:
102
+ transform var(--stor-drawer-exit-motion),
103
+ opacity var(--stor-drawer-exit-fade);
104
+ }
105
+
106
+ .stor-drawer-overlay[data-ending-style] {
107
+ opacity: 0;
108
+ transition: opacity var(--stor-overlay-exit-fade);
109
+ }
110
+
111
+ .stor-drawer-grabber { display: none; }
112
+
113
+ /* Phones: a drawer is a sheet from the bottom, sized to its content, with the page dimmed above it.
114
+ It rides on the keyboard (--stor-keyboard, set while open), and closes by its × or the dimmed page. */
115
+
116
+ @media (max-width: 767px) {
117
+ .stor-drawer {
118
+ top: auto; bottom: var(--stor-keyboard, 0px); height: auto;
119
+ max-height: calc(100dvh - var(--stor-keyboard, 0px) - env(safe-area-inset-top) - 2.5rem);
120
+ padding-top: 0; border-radius: 24px 24px 0 0;
121
+ transition: transform var(--stor-drawer-enter-motion), opacity var(--stor-drawer-enter-fade), bottom 120ms ease-out;
122
+ }
123
+ .stor-drawer[data-ending-style] { transform: translateY(2rem); }
124
+ .stor-drawer-grabber { display: block; width: 38px; height: 5px; flex: none; margin: 8px auto 0; border-radius: 3px; background: var(--stor-border); }
125
+ .stor-drawer-header { padding-top: 6px; padding-bottom: 8px; }
126
+ .stor-drawer-body { flex: 0 1 auto; padding-top: 4px; }
127
+ .stor-drawer-footer { border-top: 0; padding-top: 4px; }
128
+ [data-drawer-cancel] { display: none; }
129
+ }
130
+
131
+ /* A sheet opened over another takes its place; the one below waits, hidden, for Back. */
132
+
133
+ [data-sheet-covered] { visibility: hidden; }
134
+
135
+ @media (max-width: 767px) {
136
+ @starting-style { .stor-drawer { transform: translateY(2rem); } }
137
+ }
138
+
139
+ @media (max-width: 1023px) {
140
+ body:has([data-stor-drawer]) .stor-notifications { z-index: 60; }
141
+ }
142
+
143
+ @media (max-width: 767px) {
144
+ .stor-page-label, .stor-sidebar-owned-tabs { display: none; }
145
+ }
146
+
147
+ @media (min-width: 1024px) {
148
+ /* Two-level sidebar: the expanded sidebar lists sub-pages and names the place, so page tab rows and labels step aside. */
149
+ html:not([data-sidebar-collapsed="true"]) .stor-sidebar-owned-tabs { display: none; }
150
+ html:not([data-sidebar-collapsed="true"]) .stor-page-label { display: none; }
151
+ .stor-page-header h1, .stor-health-header h1 { font-size: 28px; }
152
+ .stor-metric-value { font-size: 32px; }
153
+ .stor-drawer { top: 16px; right: 16px; height: calc(100dvh - 32px); max-width: 28rem; border-radius: 24px; overflow: hidden; padding-top: 0; }
154
+ .stor-drawer[data-size="lg"] { max-width: 36rem; }
155
+ body:has([data-stor-drawer]) .stor-notifications { right: calc(28rem + 32px); max-width: calc(100vw - 28rem - 48px); }
156
+ body:has([data-stor-drawer][data-size="lg"]) .stor-notifications { right: calc(36rem + 32px); max-width: calc(100vw - 36rem - 48px); }
157
+ }
158
+
159
+ @media (max-width: 639px) {
160
+ .stor-page-label { margin-bottom: 6px; }
161
+ }
162
+
163
+ .stor-refined .stor-segmented-option { min-height: 44px; }
164
+
165
+ .stor-performance-chart .stor-panel-header { flex-wrap: wrap; align-items: flex-start; }
166
+
167
+ /* Lists that select by press and hold: rows have no checkbox until selecting starts. */
168
+
169
+ @media (max-width: 767px) {
170
+ [data-selecting="false"] [data-row-check] { display: none; }
171
+ [data-selecting="false"] [data-row-id] { --check: 0px; padding-left: 0.375rem; }
172
+ [data-selecting="true"] [data-row-id] { -webkit-user-select: none; user-select: none; }
173
+ [data-row-id] { -webkit-touch-callout: none; }
174
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }