@viniaraujo68/plinth 0.0.0-stage → 0.3.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/LICENSE +21 -0
- package/README.md +361 -2
- package/dist/anchoring.d.ts +61 -0
- package/dist/anchoring.js +151 -0
- package/dist/async-action.svelte.d.ts +37 -0
- package/dist/async-action.svelte.js +64 -0
- package/dist/attachments/index.d.ts +2 -0
- package/dist/attachments/index.js +2 -0
- package/dist/attachments/sortable.d.ts +6 -0
- package/dist/attachments/sortable.js +227 -0
- package/dist/attachments/tooltip.d.ts +37 -0
- package/dist/attachments/tooltip.js +166 -0
- package/dist/components/AsyncButton.svelte +49 -0
- package/dist/components/AsyncButton.svelte.d.ts +32 -0
- package/dist/components/Combobox.svelte +380 -0
- package/dist/components/Combobox.svelte.d.ts +74 -0
- package/dist/components/Copyable.svelte +116 -0
- package/dist/components/Copyable.svelte.d.ts +37 -0
- package/dist/components/DateRangePicker.svelte +266 -0
- package/dist/components/DateRangePicker.svelte.d.ts +66 -0
- package/dist/components/Dialog.svelte +102 -0
- package/dist/components/Dialog.svelte.d.ts +38 -0
- package/dist/components/DragHandle.svelte +26 -0
- package/dist/components/DragHandle.svelte.d.ts +7 -0
- package/dist/components/Dropdown.svelte +204 -0
- package/dist/components/Dropdown.svelte.d.ts +49 -0
- package/dist/components/ErrorDisplay.svelte +59 -0
- package/dist/components/ErrorDisplay.svelte.d.ts +24 -0
- package/dist/components/ErrorPage.svelte +119 -0
- package/dist/components/ErrorPage.svelte.d.ts +52 -0
- package/dist/components/JsonInput.svelte +106 -0
- package/dist/components/JsonInput.svelte.d.ts +40 -0
- package/dist/components/Listbox.svelte +257 -0
- package/dist/components/Listbox.svelte.d.ts +78 -0
- package/dist/components/LoadingButton.svelte +36 -0
- package/dist/components/LoadingButton.svelte.d.ts +22 -0
- package/dist/components/Logo.svelte +112 -0
- package/dist/components/Logo.svelte.d.ts +29 -0
- package/dist/components/Modal.svelte +187 -0
- package/dist/components/Modal.svelte.d.ts +62 -0
- package/dist/components/MultiSelect.svelte +256 -0
- package/dist/components/MultiSelect.svelte.d.ts +89 -0
- package/dist/components/OptionPanel.svelte +241 -0
- package/dist/components/OptionPanel.svelte.d.ts +51 -0
- package/dist/components/Paginator.svelte +205 -0
- package/dist/components/Paginator.svelte.d.ts +48 -0
- package/dist/components/RefreshButton.svelte +246 -0
- package/dist/components/RefreshButton.svelte.d.ts +46 -0
- package/dist/components/SegmentedControl.svelte +147 -0
- package/dist/components/SegmentedControl.svelte.d.ts +71 -0
- package/dist/components/Select.svelte +312 -0
- package/dist/components/Select.svelte.d.ts +68 -0
- package/dist/components/Skeleton.svelte +97 -0
- package/dist/components/Skeleton.svelte.d.ts +32 -0
- package/dist/components/Tooltip.svelte +60 -0
- package/dist/components/Tooltip.svelte.d.ts +58 -0
- package/dist/components/date-range.d.ts +101 -0
- package/dist/components/date-range.js +144 -0
- package/dist/components/index.d.ts +26 -0
- package/dist/components/index.js +38 -0
- package/dist/components/json-text.d.ts +22 -0
- package/dist/components/json-text.js +22 -0
- package/dist/components/pick.d.ts +18 -0
- package/dist/components/pick.js +18 -0
- package/dist/components/segmented.d.ts +7 -0
- package/dist/components/segmented.js +1 -0
- package/dist/components/select.d.ts +66 -0
- package/dist/components/select.js +90 -0
- package/dist/confirm/Confirmer.svelte +194 -0
- package/dist/confirm/Confirmer.svelte.d.ts +45 -0
- package/dist/confirm/challenge.d.ts +16 -0
- package/dist/confirm/challenge.js +16 -0
- package/dist/confirm/confirm-manager.svelte.d.ts +91 -0
- package/dist/confirm/confirm-manager.svelte.js +149 -0
- package/dist/confirm/index.d.ts +3 -0
- package/dist/confirm/index.js +3 -0
- package/dist/error.d.ts +25 -0
- package/dist/error.js +39 -0
- package/dist/formatters.d.ts +95 -0
- package/dist/formatters.js +146 -0
- package/dist/http.d.ts +68 -0
- package/dist/http.js +138 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +14 -0
- package/dist/routing.svelte.d.ts +169 -0
- package/dist/routing.svelte.js +208 -0
- package/dist/shell/AppShell.svelte +874 -0
- package/dist/shell/AppShell.svelte.d.ts +93 -0
- package/dist/shell/Breadcrumbs.svelte +78 -0
- package/dist/shell/Breadcrumbs.svelte.d.ts +25 -0
- package/dist/shell/crumbs.d.ts +23 -0
- package/dist/shell/crumbs.js +1 -0
- package/dist/shell/index.d.ts +3 -0
- package/dist/shell/index.js +6 -0
- package/dist/table/ColumnMenu.svelte +249 -0
- package/dist/table/ColumnMenu.svelte.d.ts +47 -0
- package/dist/table/DataTable.svelte +643 -0
- package/dist/table/DataTable.svelte.d.ts +153 -0
- package/dist/table/columns.d.ts +55 -0
- package/dist/table/columns.js +153 -0
- package/dist/table/index.d.ts +4 -0
- package/dist/table/index.js +8 -0
- package/dist/table/sort.d.ts +48 -0
- package/dist/table/sort.js +139 -0
- package/dist/table/types.d.ts +93 -0
- package/dist/table/types.js +1 -0
- package/dist/theme/ThemeController.svelte +54 -0
- package/dist/theme/ThemeController.svelte.d.ts +27 -0
- package/dist/theme/ThemeToggle.svelte +119 -0
- package/dist/theme/ThemeToggle.svelte.d.ts +52 -0
- package/dist/theme/css-color.d.ts +23 -0
- package/dist/theme/css-color.js +31 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.js +5 -0
- package/dist/theme/theme.css +410 -0
- package/dist/theme/theme.svelte.d.ts +54 -0
- package/dist/theme/theme.svelte.js +58 -0
- package/dist/toast/Toaster.svelte +257 -0
- package/dist/toast/Toaster.svelte.d.ts +35 -0
- package/dist/toast/index.d.ts +2 -0
- package/dist/toast/index.js +2 -0
- package/dist/toast/toast-manager.svelte.d.ts +99 -0
- package/dist/toast/toast-manager.svelte.js +176 -0
- package/dist/user/ScopedComponent.svelte +47 -0
- package/dist/user/ScopedComponent.svelte.d.ts +29 -0
- package/dist/user/anonymous.d.ts +21 -0
- package/dist/user/anonymous.js +26 -0
- package/dist/user/context.d.ts +55 -0
- package/dist/user/context.js +13 -0
- package/dist/user/index.d.ts +3 -0
- package/dist/user/index.js +6 -0
- package/package.json +133 -4
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
@import url("https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400..700&family=JetBrains+Mono:wght@100..800&display=swap");
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--font-sans: "Instrument Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
5
|
+
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
6
|
+
--default-font-family: var(--font-sans);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
:where(.font-mono, [data-mono], code, kbd, samp, pre) {
|
|
10
|
+
font-variant-ligatures: none;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* The daisyUI theme-controller checkbox holds the user's PREFERENCE rather than the resolved
|
|
14
|
+
theme (see ThemeController.svelte). Unchecked is "system": `color-scheme: light dark` hands
|
|
15
|
+
the choice to the OS, which is already correct on the very first paint and keeps tracking
|
|
16
|
+
live OS changes without a line of JS. Checked pins the scheme named by the checkbox `value`.
|
|
17
|
+
|
|
18
|
+
`[data-theme]` pins a scheme by attribute, and does two jobs: on the root it is the first-paint
|
|
19
|
+
seed a consumer stamps before hydration, from a cookie no checkbox has read yet; on any other
|
|
20
|
+
element it re-themes that subtree. Both are guarded by `:not(:has(input.theme-controller
|
|
21
|
+
:checked))`, and the guard is what keeps a live controller in charge. A selector list is folded
|
|
22
|
+
by Lightning CSS into `:is(...)`, whose specificity is the MAX over its arguments, so an
|
|
23
|
+
unguarded attribute branch would tie with the `:has()` branch of the other theme and source
|
|
24
|
+
order alone would decide — a stale seed on the root beating the checkbox the user just moved.
|
|
25
|
+
Guarded, the two branches are mutually exclusive: an element holding the attribute yields for
|
|
26
|
+
as long as a checked controller sits under it, which for the root is from the moment the app
|
|
27
|
+
mounts one, while a nested subtree keeps its attribute because the controller is not under it.
|
|
28
|
+
|
|
29
|
+
Because every color below is declared once through `light-dark()`, the inherited
|
|
30
|
+
`color-scheme` ends up being the only switch in the whole system. That also buys subtree
|
|
31
|
+
theming for free: `light-dark()` resolves at the point of USE, not of declaration, so putting
|
|
32
|
+
[data-theme] on any element re-themes everything under it by changing `color-scheme` alone. */
|
|
33
|
+
:root {
|
|
34
|
+
color-scheme: light dark;
|
|
35
|
+
}
|
|
36
|
+
:root:has(input.theme-controller[value="plinth-light"]:checked),
|
|
37
|
+
[data-theme="plinth-light"]:not(:has(input.theme-controller:checked)) {
|
|
38
|
+
color-scheme: light;
|
|
39
|
+
}
|
|
40
|
+
:root:has(input.theme-controller[value="plinth-dark"]:checked),
|
|
41
|
+
[data-theme="plinth-dark"]:not(:has(input.theme-controller:checked)) {
|
|
42
|
+
color-scheme: dark;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
:root {
|
|
46
|
+
--color-base-100: light-dark(#ffffff, #14161c);
|
|
47
|
+
--color-base-200: light-dark(#faf9f5, #0c0d11);
|
|
48
|
+
--color-base-300: light-dark(#f2f0ea, #1a1d25);
|
|
49
|
+
--color-base-content: light-dark(#1b1d22, #d9e1f6);
|
|
50
|
+
|
|
51
|
+
--color-primary: light-dark(#5d3ac4, #6f4bd4);
|
|
52
|
+
--color-primary-content: #ffffff;
|
|
53
|
+
--color-secondary: light-dark(#432a8f, #e7e1fa);
|
|
54
|
+
--color-secondary-content: light-dark(#ffffff, #432a8f);
|
|
55
|
+
--color-accent: light-dark(#e7e1fa, #242833);
|
|
56
|
+
--color-accent-content: light-dark(#432a8f, #b6a2ee);
|
|
57
|
+
--color-neutral: light-dark(#1b1d22, #e7e9ee);
|
|
58
|
+
--color-neutral-content: light-dark(#faf9f5, #0c0d11);
|
|
59
|
+
|
|
60
|
+
--color-info: light-dark(#2876c7, #5aa7f0);
|
|
61
|
+
--color-info-content: light-dark(#ffffff, #0c0d11);
|
|
62
|
+
--color-success: light-dark(#1c8a4c, #34b36a);
|
|
63
|
+
--color-success-content: light-dark(#ffffff, #0c0d11);
|
|
64
|
+
--color-warning: light-dark(#a67a0f, #e4b33a);
|
|
65
|
+
--color-warning-content: light-dark(#ffffff, #0c0d11);
|
|
66
|
+
--color-error: light-dark(#b64337, #e46b5b);
|
|
67
|
+
--color-error-content: light-dark(#ffffff, #0c0d11);
|
|
68
|
+
|
|
69
|
+
--radius-selector: 0.5rem;
|
|
70
|
+
--radius-field: 0.5rem;
|
|
71
|
+
--radius-box: 0.75rem;
|
|
72
|
+
--size-selector: 0.25rem;
|
|
73
|
+
--size-field: 0.25rem;
|
|
74
|
+
--border: 1px;
|
|
75
|
+
--depth: 0;
|
|
76
|
+
--noise: 0;
|
|
77
|
+
|
|
78
|
+
/* A skin that paints its own surface re-declares --color-base-* for its subtree, so reading
|
|
79
|
+
the page values back through those names would be a cycle. These aliases are the escape
|
|
80
|
+
hatch: they keep pointing at the page, whatever a nested surface has done locally. */
|
|
81
|
+
--theme-content: var(--color-base-content);
|
|
82
|
+
--theme-surface: var(--color-base-100);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* What follows re-skins daisyUI components on top of the tokens above. Wherever the selector is
|
|
86
|
+
wrapped in :where(...) it lands at zero specificity, so a Tailwind utility or a one-off
|
|
87
|
+
override at the call site still wins without needing !important. */
|
|
88
|
+
|
|
89
|
+
:where(.input, .select, .textarea) {
|
|
90
|
+
background-color: var(--color-base-100);
|
|
91
|
+
border-color: color-mix(in oklch, var(--color-base-content) 15%, transparent);
|
|
92
|
+
}
|
|
93
|
+
:where(.input, .select, .textarea):focus-within {
|
|
94
|
+
outline-color: color-mix(in oklch, var(--color-primary) 35%, transparent);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.btn {
|
|
98
|
+
font-weight: 500;
|
|
99
|
+
letter-spacing: 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* One default height for every unsized button. Square and circle are deliberately in scope:
|
|
103
|
+
left alone they keep their own height, and the moment an icon button sits next to a text
|
|
104
|
+
button in a join — or any align-items:stretch row — the whole row grows to the taller of the
|
|
105
|
+
two. --btn-p does nothing on square/circle, which zero their inline padding themselves. */
|
|
106
|
+
.btn:not(:where(.btn-xs, .btn-sm, .btn-md, .btn-lg, .btn-xl)) {
|
|
107
|
+
--size: 2.125rem;
|
|
108
|
+
--btn-p: 0.875rem;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* The unqualified `.btn` — no color modifier, no style modifier — is the neutral button of this
|
|
112
|
+
system: page surface, hairline border, no fill. */
|
|
113
|
+
.btn:not(
|
|
114
|
+
:where(
|
|
115
|
+
.btn-primary,
|
|
116
|
+
.btn-secondary,
|
|
117
|
+
.btn-accent,
|
|
118
|
+
.btn-neutral,
|
|
119
|
+
.btn-info,
|
|
120
|
+
.btn-success,
|
|
121
|
+
.btn-warning,
|
|
122
|
+
.btn-error,
|
|
123
|
+
.btn-ghost,
|
|
124
|
+
.btn-link,
|
|
125
|
+
.btn-outline,
|
|
126
|
+
.btn-soft,
|
|
127
|
+
.btn-dash
|
|
128
|
+
)
|
|
129
|
+
) {
|
|
130
|
+
--btn-bg: var(--color-base-100);
|
|
131
|
+
--btn-fg: var(--color-base-content);
|
|
132
|
+
--btn-border: color-mix(in oklch, var(--color-base-content) 15%, transparent);
|
|
133
|
+
}
|
|
134
|
+
.btn:not(
|
|
135
|
+
:where(
|
|
136
|
+
.btn-primary,
|
|
137
|
+
.btn-secondary,
|
|
138
|
+
.btn-accent,
|
|
139
|
+
.btn-neutral,
|
|
140
|
+
.btn-info,
|
|
141
|
+
.btn-success,
|
|
142
|
+
.btn-warning,
|
|
143
|
+
.btn-error,
|
|
144
|
+
.btn-ghost,
|
|
145
|
+
.btn-link,
|
|
146
|
+
.btn-outline,
|
|
147
|
+
.btn-soft,
|
|
148
|
+
.btn-dash
|
|
149
|
+
)
|
|
150
|
+
):is(:hover, :focus-visible) {
|
|
151
|
+
--btn-bg: light-dark(var(--color-base-200), var(--color-base-300));
|
|
152
|
+
--btn-border: color-mix(in oklch, var(--color-base-content) 15%, transparent);
|
|
153
|
+
}
|
|
154
|
+
/* aria-current is excluded so a ghost button acting as the selected nav entry keeps its fill. */
|
|
155
|
+
:where(.btn-ghost:not(:hover, :focus-visible, [aria-current="page"])) {
|
|
156
|
+
background-color: transparent;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* Table — the dense data surface. daisyUI's own size modifiers only move body-row text and
|
|
160
|
+
padding while headers stay pinned at .875rem, which desynchronises the two halves. Setting
|
|
161
|
+
the table's ROOT font-size per size instead re-anchors the whole scale around the 13px
|
|
162
|
+
default, and the em-based header typography then follows the body automatically. */
|
|
163
|
+
:where(.table) {
|
|
164
|
+
--table-line: color-mix(in oklch, var(--color-base-content) 15%, transparent);
|
|
165
|
+
--table-row-line: color-mix(in oklch, var(--color-base-content) 10%, transparent);
|
|
166
|
+
--table-ink-muted: color-mix(in oklch, var(--color-base-content) 55%, transparent);
|
|
167
|
+
|
|
168
|
+
border-collapse: separate;
|
|
169
|
+
border-spacing: 0;
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
border-radius: 0;
|
|
172
|
+
background-color: var(--color-base-100);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
:where(.table:not(.table-xs, .table-sm, .table-md, .table-lg, .table-xl)) {
|
|
176
|
+
font-size: 0.8125rem;
|
|
177
|
+
}
|
|
178
|
+
:where(.table:not(.table-xs, .table-sm, .table-md, .table-lg, .table-xl)) :where(th, td) {
|
|
179
|
+
padding-inline: calc(var(--spacing) * 3);
|
|
180
|
+
padding-block: calc(var(--spacing) * 2);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
:where(.table-xs) {
|
|
184
|
+
font-size: 0.6875rem;
|
|
185
|
+
}
|
|
186
|
+
:where(.table-sm) {
|
|
187
|
+
font-size: 0.75rem;
|
|
188
|
+
}
|
|
189
|
+
:where(.table-md) {
|
|
190
|
+
font-size: 0.8125rem;
|
|
191
|
+
}
|
|
192
|
+
:where(.table-lg) {
|
|
193
|
+
font-size: 0.875rem;
|
|
194
|
+
}
|
|
195
|
+
:where(.table-xl) {
|
|
196
|
+
font-size: 1rem;
|
|
197
|
+
}
|
|
198
|
+
/* daisyUI sets an absolute font-size on rows, which would defeat the root-anchored scale. */
|
|
199
|
+
:where(.table-xs, .table-sm, .table-md, .table-lg, .table-xl) :where(tr) {
|
|
200
|
+
font-size: inherit;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
:where(.table-xs) :where(th, td) {
|
|
204
|
+
padding-inline: calc(var(--spacing) * 2);
|
|
205
|
+
padding-block: var(--spacing);
|
|
206
|
+
}
|
|
207
|
+
:where(.table-sm) :where(th, td) {
|
|
208
|
+
padding-inline: calc(var(--spacing) * 2.5);
|
|
209
|
+
padding-block: calc(var(--spacing) * 1.5);
|
|
210
|
+
}
|
|
211
|
+
:where(.table-md) :where(th, td) {
|
|
212
|
+
padding-inline: calc(var(--spacing) * 3);
|
|
213
|
+
padding-block: calc(var(--spacing) * 2);
|
|
214
|
+
}
|
|
215
|
+
:where(.table-lg) :where(th, td) {
|
|
216
|
+
padding-inline: calc(var(--spacing) * 4);
|
|
217
|
+
padding-block: calc(var(--spacing) * 3);
|
|
218
|
+
}
|
|
219
|
+
:where(.table-xl) :where(th, td) {
|
|
220
|
+
padding-inline: calc(var(--spacing) * 5);
|
|
221
|
+
padding-block: calc(var(--spacing) * 4);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Column headers borrow the overline micro-label treatment used by stat titles. */
|
|
225
|
+
:where(.table :is(thead, tfoot)) {
|
|
226
|
+
font-size: 0.85em;
|
|
227
|
+
font-weight: 500;
|
|
228
|
+
text-transform: uppercase;
|
|
229
|
+
letter-spacing: 0.06em;
|
|
230
|
+
color: var(--table-ink-muted);
|
|
231
|
+
}
|
|
232
|
+
:where(.table thead th) {
|
|
233
|
+
background-color: var(--color-base-200);
|
|
234
|
+
white-space: normal;
|
|
235
|
+
text-wrap: wrap;
|
|
236
|
+
border-bottom: var(--border) solid var(--table-line);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
:where(.table tbody td) {
|
|
240
|
+
vertical-align: middle;
|
|
241
|
+
border-bottom: var(--border) solid var(--table-row-line);
|
|
242
|
+
}
|
|
243
|
+
:where(.table tbody tr:last-child td) {
|
|
244
|
+
border-bottom: 0;
|
|
245
|
+
}
|
|
246
|
+
:where(.table-zebra tbody tr:nth-child(even)) {
|
|
247
|
+
background-color: var(--color-base-300);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* Numbers line up only when the cell opts in, via font-mono or a data-mono marker. */
|
|
251
|
+
:where(.table .font-mono, .table [data-mono]) {
|
|
252
|
+
font-family: var(--font-mono);
|
|
253
|
+
font-variant-numeric: tabular-nums;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* Badge — two sanctioned treatments:
|
|
257
|
+
- plain `.badge` stays stock solid, and reads as a counter: sidebar counts, a count baked
|
|
258
|
+
into a button, an indicator over an icon. Nothing should be nested inside one, because a
|
|
259
|
+
solid badge does not re-export the context tokens a child would read.
|
|
260
|
+
- `.badge-soft` is the tinted chip used for status and entity labels, restyled by the formula
|
|
261
|
+
below. outline and ghost collapse into the same look on purpose, so a call site that
|
|
262
|
+
reaches for the wrong modifier still lands on the sanctioned chip. Tune the knobs here. */
|
|
263
|
+
:where(.badge) {
|
|
264
|
+
font-weight: 500;
|
|
265
|
+
line-height: 1;
|
|
266
|
+
white-space: nowrap;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
:where(.badge-soft, .badge-outline, .badge-ghost) {
|
|
270
|
+
--badge-hue: var(--badge-color, var(--theme-content));
|
|
271
|
+
--badge-tint: 12%;
|
|
272
|
+
--badge-line: 28%;
|
|
273
|
+
--badge-ink: var(--badge-hue);
|
|
274
|
+
|
|
275
|
+
color: var(--badge-ink);
|
|
276
|
+
background-color: color-mix(in oklch, var(--badge-hue) var(--badge-tint), transparent);
|
|
277
|
+
border-color: color-mix(in oklch, var(--badge-hue) var(--badge-line), transparent);
|
|
278
|
+
|
|
279
|
+
/* This rule paints a surface, so it owes the composition contract: re-declare the base tokens
|
|
280
|
+
that nested components read. A button or an icon dropped into a tinted badge then computes
|
|
281
|
+
its colors against the tint it actually sits on, not against the page. */
|
|
282
|
+
--color-base-content: var(--badge-ink);
|
|
283
|
+
--color-base-100: color-mix(in oklch, var(--badge-hue) var(--badge-tint), var(--theme-surface));
|
|
284
|
+
--color-base-200: color-mix(
|
|
285
|
+
in oklch,
|
|
286
|
+
var(--badge-hue) calc(min(var(--badge-tint) + 8%, 100%)),
|
|
287
|
+
var(--theme-surface)
|
|
288
|
+
);
|
|
289
|
+
--color-base-300: color-mix(
|
|
290
|
+
in oklch,
|
|
291
|
+
var(--badge-hue) calc(min(var(--badge-tint) + 14%, 100%)),
|
|
292
|
+
var(--theme-surface)
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* The same formula, run without a hue: `badge-soft` on its own, or paired with badge-neutral. */
|
|
297
|
+
:where(.badge-soft, .badge-outline, .badge-ghost):where(
|
|
298
|
+
.badge-neutral,
|
|
299
|
+
:not(
|
|
300
|
+
.badge-primary,
|
|
301
|
+
.badge-secondary,
|
|
302
|
+
.badge-accent,
|
|
303
|
+
.badge-neutral,
|
|
304
|
+
.badge-info,
|
|
305
|
+
.badge-success,
|
|
306
|
+
.badge-warning,
|
|
307
|
+
.badge-error
|
|
308
|
+
)
|
|
309
|
+
) {
|
|
310
|
+
--badge-hue: var(--theme-content);
|
|
311
|
+
--badge-tint: 5%;
|
|
312
|
+
--badge-line: 15%;
|
|
313
|
+
--badge-ink: color-mix(in oklch, var(--theme-content) 55%, transparent);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
:where(.card) {
|
|
317
|
+
border: 1px solid color-mix(in oklch, var(--color-base-content) 15%, transparent);
|
|
318
|
+
box-shadow:
|
|
319
|
+
0 1px 0 light-dark(rgb(0 0 0 / 0.02), rgb(255 255 255 / 0.02)),
|
|
320
|
+
0 1px 2px light-dark(rgb(20 20 20 / 0.04), rgb(0 0 0 / 0.3));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* Stat — the KPI card. The block is a bordered base-100 surface, the title is the overline
|
|
324
|
+
micro-label, and the value is deliberately calm: medium weight with slightly tightened
|
|
325
|
+
tracking, instead of the stock 800-weight display treatment. */
|
|
326
|
+
:where(.stats) {
|
|
327
|
+
background-color: var(--color-base-100);
|
|
328
|
+
border: var(--border) solid color-mix(in oklch, var(--color-base-content) 15%, transparent);
|
|
329
|
+
}
|
|
330
|
+
:where(.stat) {
|
|
331
|
+
padding-block: 0.875rem;
|
|
332
|
+
padding-inline: 1rem;
|
|
333
|
+
}
|
|
334
|
+
:where(.stat-title) {
|
|
335
|
+
font-size: 0.6875rem;
|
|
336
|
+
font-weight: 500;
|
|
337
|
+
text-transform: uppercase;
|
|
338
|
+
letter-spacing: 0.06em;
|
|
339
|
+
}
|
|
340
|
+
:where(.stat-value) {
|
|
341
|
+
font-size: 1.5rem;
|
|
342
|
+
font-weight: 500;
|
|
343
|
+
letter-spacing: -0.01em;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* Tabs — `tabs-border` is the one sanctioned variant: 13px medium type, muted when idle, and a
|
|
347
|
+
full-width 2px primary underline on the active tab. */
|
|
348
|
+
:where(.tab) {
|
|
349
|
+
font-size: 0.8125rem;
|
|
350
|
+
font-weight: 500;
|
|
351
|
+
}
|
|
352
|
+
:where(.tabs-border > .tab)::before {
|
|
353
|
+
width: 100%;
|
|
354
|
+
left: 0;
|
|
355
|
+
height: 2px;
|
|
356
|
+
}
|
|
357
|
+
:where(
|
|
358
|
+
.tabs-border
|
|
359
|
+
> .tab:is(
|
|
360
|
+
.tab-active,
|
|
361
|
+
[aria-selected="true"],
|
|
362
|
+
[aria-current="true"],
|
|
363
|
+
[aria-current="page"],
|
|
364
|
+
input:checked
|
|
365
|
+
)
|
|
366
|
+
)::before {
|
|
367
|
+
background-color: var(--color-primary);
|
|
368
|
+
border-top: 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
:where(.menu) {
|
|
372
|
+
--menu-active-bg: var(--color-primary);
|
|
373
|
+
--menu-active-fg: var(--color-primary-content);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* Every floating surface — dropdown, popover — gets one treatment: base-100, hairline border,
|
|
377
|
+
box radius, soft drop shadow. Content inside is therefore already on base-100, so the context
|
|
378
|
+
tokens need no adjustment. */
|
|
379
|
+
:where(.dropdown-content, .dropdown[popover]) {
|
|
380
|
+
background-color: var(--color-base-100);
|
|
381
|
+
border: var(--border) solid color-mix(in oklch, var(--color-base-content) 15%, transparent);
|
|
382
|
+
border-radius: var(--radius-box);
|
|
383
|
+
box-shadow: 0 8px 24px light-dark(rgb(0 0 0 / 0.12), rgb(0 0 0 / 0.28));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* Modal — the same panel, over a blurred backdrop. */
|
|
387
|
+
:where(.modal-box) {
|
|
388
|
+
border: var(--border) solid color-mix(in oklch, var(--color-base-content) 15%, transparent);
|
|
389
|
+
}
|
|
390
|
+
:where(.modal:is(.modal-open, [open], :target)) {
|
|
391
|
+
backdrop-filter: blur(4px);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* Tooltip — dropped into the same panel language instead of the stock inverted pill. */
|
|
395
|
+
:where(.tooltip) {
|
|
396
|
+
--tt-bg: var(--color-base-100);
|
|
397
|
+
}
|
|
398
|
+
:where(.tooltip > .tooltip-content),
|
|
399
|
+
:where(.tooltip[data-tip])::before {
|
|
400
|
+
color: var(--color-base-content);
|
|
401
|
+
border: var(--border) solid color-mix(in oklch, var(--color-base-content) 15%, transparent);
|
|
402
|
+
box-shadow: 0 8px 24px light-dark(rgb(0 0 0 / 0.12), rgb(0 0 0 / 0.28));
|
|
403
|
+
font-size: 0.8125rem;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* Kbd — mono and muted; also the treatment for short suffix labels sitting inside a field. */
|
|
407
|
+
:where(.kbd) {
|
|
408
|
+
font-family: var(--font-mono);
|
|
409
|
+
color: color-mix(in oklch, var(--color-base-content) 70%, transparent);
|
|
410
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Theme name the stylesheet pins to `color-scheme: light`. */
|
|
2
|
+
export declare const LIGHT_THEME = "plinth-light";
|
|
3
|
+
/** Theme name the stylesheet pins to `color-scheme: dark`. */
|
|
4
|
+
export declare const DARK_THEME = "plinth-dark";
|
|
5
|
+
/**
|
|
6
|
+
* Name of the cookie carrying the theme preference.
|
|
7
|
+
*
|
|
8
|
+
* A cookie rather than localStorage because it is the only client-side store a server can read:
|
|
9
|
+
* an SSR consumer picks it up in its root layout load and renders the controller already in the
|
|
10
|
+
* right state, which is what makes the first paint flash-free.
|
|
11
|
+
*/
|
|
12
|
+
export declare const THEME_COOKIE = "theme";
|
|
13
|
+
export type ThemePreference = "light" | "dark" | "system";
|
|
14
|
+
/**
|
|
15
|
+
* Narrow an untrusted string — a cookie, a query param, `data` handed down by a layout load — to
|
|
16
|
+
* a preference, falling back to "system". This is the seed for `ThemeContext` on whichever side
|
|
17
|
+
* happened to read the value.
|
|
18
|
+
*/
|
|
19
|
+
export declare const parseThemePreference: (value: string | undefined) => ThemePreference;
|
|
20
|
+
/**
|
|
21
|
+
* Read the stored preference from `document.cookie`, defaulting to "system".
|
|
22
|
+
*
|
|
23
|
+
* A client-rendered app seeds `ThemeContext` straight from this. An app that renders on the
|
|
24
|
+
* server reads the same cookie in its root layout load and seeds from `data` instead — same
|
|
25
|
+
* flow, different side of the wire.
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* setThemeContext(new ThemeContext(readThemePreference()));
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare const readThemePreference: () => ThemePreference;
|
|
32
|
+
/** Serialise a preference into a `Set-Cookie`-shaped string, for whoever is doing the writing. */
|
|
33
|
+
export declare const themeCookie: (preference: ThemePreference) => string;
|
|
34
|
+
/**
|
|
35
|
+
* The reactive theme preference. `preference` is what the user picked, and the only thing that
|
|
36
|
+
* gets persisted; `dark` is that preference resolved against the OS, kept current by a reactive
|
|
37
|
+
* media query.
|
|
38
|
+
*
|
|
39
|
+
* Painting never goes through `dark`. The daisyUI theme-controller checkbox rendered by
|
|
40
|
+
* `ThemeController` carries `preference` itself, and CSS resolves the "system" case through
|
|
41
|
+
* `color-scheme: light dark` — so the correct colors are on screen before any of this runs.
|
|
42
|
+
* `dark` exists for the JS that genuinely needs the resolved value, such as choosing a chart
|
|
43
|
+
* palette that CSS cannot express.
|
|
44
|
+
*
|
|
45
|
+
* `ThemeController` must be mounted exactly once, unconditionally, near the app root. Any other
|
|
46
|
+
* picker in the app just writes `preference` here.
|
|
47
|
+
*/
|
|
48
|
+
export declare class ThemeContext {
|
|
49
|
+
preference: ThemePreference;
|
|
50
|
+
private system;
|
|
51
|
+
dark: boolean;
|
|
52
|
+
constructor(initial: ThemePreference);
|
|
53
|
+
}
|
|
54
|
+
export declare const getThemeContext: () => ThemeContext, setThemeContext: (context: ThemeContext) => ThemeContext;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createContext } from "svelte";
|
|
2
|
+
import { MediaQuery } from "svelte/reactivity";
|
|
3
|
+
/** Theme name the stylesheet pins to `color-scheme: light`. */
|
|
4
|
+
export const LIGHT_THEME = "plinth-light";
|
|
5
|
+
/** Theme name the stylesheet pins to `color-scheme: dark`. */
|
|
6
|
+
export const DARK_THEME = "plinth-dark";
|
|
7
|
+
/**
|
|
8
|
+
* Name of the cookie carrying the theme preference.
|
|
9
|
+
*
|
|
10
|
+
* A cookie rather than localStorage because it is the only client-side store a server can read:
|
|
11
|
+
* an SSR consumer picks it up in its root layout load and renders the controller already in the
|
|
12
|
+
* right state, which is what makes the first paint flash-free.
|
|
13
|
+
*/
|
|
14
|
+
export const THEME_COOKIE = "theme";
|
|
15
|
+
const isPreference = (value) => value === "light" || value === "dark" || value === "system";
|
|
16
|
+
/**
|
|
17
|
+
* Narrow an untrusted string — a cookie, a query param, `data` handed down by a layout load — to
|
|
18
|
+
* a preference, falling back to "system". This is the seed for `ThemeContext` on whichever side
|
|
19
|
+
* happened to read the value.
|
|
20
|
+
*/
|
|
21
|
+
export const parseThemePreference = (value) => isPreference(value) ? value : "system";
|
|
22
|
+
/**
|
|
23
|
+
* Read the stored preference from `document.cookie`, defaulting to "system".
|
|
24
|
+
*
|
|
25
|
+
* A client-rendered app seeds `ThemeContext` straight from this. An app that renders on the
|
|
26
|
+
* server reads the same cookie in its root layout load and seeds from `data` instead — same
|
|
27
|
+
* flow, different side of the wire.
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* setThemeContext(new ThemeContext(readThemePreference()));
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const readThemePreference = () => parseThemePreference(new RegExp(`(?:^|;\\s*)${THEME_COOKIE}=(\\w+)`).exec(document.cookie)?.[1]);
|
|
34
|
+
/** Serialise a preference into a `Set-Cookie`-shaped string, for whoever is doing the writing. */
|
|
35
|
+
export const themeCookie = (preference) => `${THEME_COOKIE}=${preference}; path=/; max-age=31536000; samesite=lax`;
|
|
36
|
+
/**
|
|
37
|
+
* The reactive theme preference. `preference` is what the user picked, and the only thing that
|
|
38
|
+
* gets persisted; `dark` is that preference resolved against the OS, kept current by a reactive
|
|
39
|
+
* media query.
|
|
40
|
+
*
|
|
41
|
+
* Painting never goes through `dark`. The daisyUI theme-controller checkbox rendered by
|
|
42
|
+
* `ThemeController` carries `preference` itself, and CSS resolves the "system" case through
|
|
43
|
+
* `color-scheme: light dark` — so the correct colors are on screen before any of this runs.
|
|
44
|
+
* `dark` exists for the JS that genuinely needs the resolved value, such as choosing a chart
|
|
45
|
+
* palette that CSS cannot express.
|
|
46
|
+
*
|
|
47
|
+
* `ThemeController` must be mounted exactly once, unconditionally, near the app root. Any other
|
|
48
|
+
* picker in the app just writes `preference` here.
|
|
49
|
+
*/
|
|
50
|
+
export class ThemeContext {
|
|
51
|
+
preference = $state("system");
|
|
52
|
+
system = new MediaQuery("(prefers-color-scheme: dark)");
|
|
53
|
+
dark = $derived(this.preference === "system" ? this.system.current : this.preference === "dark");
|
|
54
|
+
constructor(initial) {
|
|
55
|
+
this.preference = initial;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export const [getThemeContext, setThemeContext] = createContext();
|