@dorsk/tsumikit 0.50.0 → 0.51.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 CHANGED
@@ -154,6 +154,60 @@ for a light theme (`:root` defaults to dark, so dark themes may omit it).
154
154
  - Adding a built-in theme to the kit = one entry in `THEMES`
155
155
  (`stores/theme.svelte.ts`) + one block in `styles/themes.css`.
156
156
 
157
+ ### Styling a component from outside
158
+
159
+ A Svelte consumer's **scoped** CSS cannot reach a child component's markup — the
160
+ scoping attribute is only added to elements the consumer itself renders. `class`
161
+ passthrough therefore only helps for layout hooks (margin, flex, grid placement).
162
+ CSS custom properties *do* cross the component boundary, so they are the
163
+ sanctioned escape hatch for everything else.
164
+
165
+ - **Naming:** `--<prefix>-<axis>`, where `<prefix>` is the component's short
166
+ prefix (`btn`, `badge`, `txt`, `select`, `input`, `textarea`, `card`, `md`, …)
167
+ and `<axis>` is one of `bg`, `fg`, `border`, `size`, `radius`, plus a few
168
+ component-specific ones (`--btn-tone`, `--badge-max-width`, `--gauge-w`).
169
+ `Text` uses `txt` because `--text*` is already the theme's foreground token.
170
+ - **Setting them:** per instance via `style`, or on any ancestor for a subtree.
171
+
172
+ ```svelte
173
+ <Button style="--btn-bg: var(--c-violet); --btn-fg: #fff; --btn-radius: 0">Ship</Button>
174
+
175
+ <div class="danger-zone">
176
+ <Button>Delete</Button> <!-- inherits the block below -->
177
+ </div>
178
+
179
+ <style>
180
+ .danger-zone { --btn-border: var(--danger); --btn-fg: var(--danger); }
181
+ </style>
182
+ ```
183
+
184
+ - Every property is read as `var(--x, <default>)`, so leaving it unset renders
185
+ exactly as before.
186
+ - **`:global()` into kit internals is unsupported.** Class names like `.btn`,
187
+ `.badge`, `.select-wrap` or `.textarea` are private and change without a major
188
+ bump. If an axis you need is missing, open an issue — do not reach in.
189
+
190
+ | component | published properties |
191
+ | --- | --- |
192
+ | `Button` | `--btn-bg`, `--btn-fg`, `--btn-border`, `--btn-size`, `--btn-radius`, `--btn-tone`, `--btn-on`, `--btn-box` |
193
+ | `Badge` | `--badge-bg`, `--badge-fg`, `--badge-border`, `--badge-radius`, `--badge-tone`, `--badge-max-width` |
194
+ | `Text` | `--txt-fg`, `--txt-size` |
195
+ | `Select` | `--select-bg`, `--select-fg`, `--select-border`, `--select-size`, `--select-radius` |
196
+ | `Input` | `--input-bg`, `--input-fg`, `--input-border`, `--input-size`, `--input-radius` |
197
+ | `Textarea` | `--textarea-bg`, `--textarea-fg`, `--textarea-border`, `--textarea-size`, `--textarea-radius` |
198
+ | `Card` | `--card-pad`, `--card-gap`, `--card-border-style` |
199
+ | `Divider` | `--divider-color`, `--divider-spacing` |
200
+ | `Gauge` | `--gauge-w`, `--gauge-h`, `--gauge-fill` |
201
+ | `MasterDetail` | `--md-list-w`, `--md-gap`, `--md-divider` |
202
+ | `NavBar` | `--navbar-max` |
203
+ | `Callout` | `--callout-tone` |
204
+ | `Fieldset` | `--fieldset-pad`, `--fieldset-border` |
205
+ | `GitRef` | `--git-ref-tone`, `--git-ref-max-width` |
206
+ | `EmptyState` | `--empty-tone` |
207
+
208
+ `tests/css-custom-property-contract.test.js` reads this table and fails if a
209
+ listed property is not actually read by its component, so the docs cannot drift.
210
+
157
211
  ## Components
158
212
 
159
213
  **Atoms:** Text, Heading, Button, Input (`icon` inset leading glyph,
@@ -166,13 +166,13 @@
166
166
  align-items: center;
167
167
  gap: var(--sp-1);
168
168
  padding: 0.15rem var(--sp-2);
169
- border-radius: var(--r-pill);
169
+ border-radius: var(--badge-radius, var(--r-pill));
170
170
  font-size: var(--fs-xs);
171
171
  font-weight: var(--fw-medium);
172
172
  line-height: 1.4;
173
- background: var(--bg-elevated-2);
174
- color: var(--text-muted);
175
- border: 1px solid var(--border);
173
+ background: var(--badge-bg, var(--bg-elevated-2));
174
+ color: var(--badge-fg, var(--text-muted));
175
+ border: 1px solid var(--badge-border, var(--border));
176
176
  white-space: nowrap;
177
177
  max-width: var(--badge-max-width, 100%);
178
178
  }
@@ -188,9 +188,9 @@
188
188
  padding: 0.05rem var(--sp-2);
189
189
  }
190
190
  .toned {
191
- color: var(--badge-tone);
192
- border-color: color-mix(in srgb, var(--badge-tone) 40%, transparent);
193
- background: color-mix(in srgb, var(--badge-tone) 12%, transparent);
191
+ color: var(--badge-fg, var(--badge-tone));
192
+ border-color: var(--badge-border, color-mix(in srgb, var(--badge-tone) 40%, transparent));
193
+ background: var(--badge-bg, color-mix(in srgb, var(--badge-tone) 12%, transparent));
194
194
  }
195
195
  .text {
196
196
  padding: 0;
@@ -165,11 +165,11 @@
165
165
  justify-content: center;
166
166
  gap: var(--sp-2);
167
167
  padding: var(--sp-2) var(--sp-4);
168
- min-height: var(--control-height-default);
169
- border: 1px solid var(--border-strong);
170
- border-radius: var(--r-md);
171
- background: var(--surface);
172
- color: var(--text);
168
+ min-height: var(--btn-size, var(--control-height-default));
169
+ border: 1px solid var(--btn-border, var(--border-strong));
170
+ border-radius: var(--btn-radius, var(--r-md));
171
+ background: var(--btn-bg, var(--surface));
172
+ color: var(--btn-fg, var(--text));
173
173
  font-weight: var(--fw-medium);
174
174
  font-size: var(--fs-sm);
175
175
  line-height: 1;
@@ -188,9 +188,9 @@
188
188
  cursor: not-allowed;
189
189
  }
190
190
  .btn-primary {
191
- background: var(--accent);
192
- border-color: var(--accent);
193
- color: var(--text-on-accent);
191
+ background: var(--btn-bg, var(--accent));
192
+ border-color: var(--btn-border, var(--accent));
193
+ color: var(--btn-fg, var(--text-on-accent));
194
194
  font-weight: var(--fw-semibold);
195
195
  }
196
196
  .btn-primary:hover:not(:disabled) {
@@ -205,21 +205,21 @@
205
205
  border-color: var(--danger);
206
206
  }
207
207
  .btn-ghost {
208
- background: transparent;
209
- border-color: transparent;
208
+ background: var(--btn-bg, transparent);
209
+ border-color: var(--btn-border, transparent);
210
210
  }
211
211
  .btn-ghost:hover:not(:disabled) {
212
212
  background: var(--bg-elevated-2);
213
213
  border-color: transparent;
214
214
  }
215
215
  .btn-sm {
216
- height: var(--control-height-compact);
217
- min-height: var(--control-height-compact);
216
+ height: var(--btn-size, var(--control-height-compact));
217
+ min-height: var(--btn-size, var(--control-height-compact));
218
218
  padding: var(--sp-1) var(--sp-3);
219
219
  font-size: var(--fs-xs);
220
220
  }
221
221
  .btn-lg {
222
- min-height: var(--control-height-large);
222
+ min-height: var(--btn-size, var(--control-height-large));
223
223
  padding: var(--sp-3) var(--sp-5);
224
224
  font-size: var(--fs-base);
225
225
  }
@@ -298,13 +298,13 @@
298
298
  align-items: center;
299
299
  justify-content: center;
300
300
  gap: var(--sp-2);
301
- height: var(--control-height);
302
- min-height: var(--control-height);
301
+ height: var(--btn-size, var(--control-height));
302
+ min-height: var(--btn-size, var(--control-height));
303
303
  padding: 0 var(--sp-3);
304
- border: 1px solid var(--border-strong);
305
- border-radius: var(--r-md);
306
- background: var(--surface);
307
- color: var(--text);
304
+ border: 1px solid var(--btn-border, var(--border-strong));
305
+ border-radius: var(--btn-radius, var(--r-md));
306
+ background: var(--btn-bg, var(--surface));
307
+ color: var(--btn-fg, var(--text));
308
308
  font-weight: var(--fw-medium);
309
309
  font-size: var(--fs-sm);
310
310
  line-height: 1;
@@ -326,9 +326,9 @@
326
326
  the primary action with the neutral --surface; restore the accent fill when
327
327
  both are present. */
328
328
  .btn-control.btn-primary {
329
- background: var(--accent);
330
- border-color: var(--accent);
331
- color: var(--text-on-accent);
329
+ background: var(--btn-bg, var(--accent));
330
+ border-color: var(--btn-border, var(--accent));
331
+ color: var(--btn-fg, var(--text-on-accent));
332
332
  }
333
333
  .btn-control.btn-primary:hover:not(:disabled) {
334
334
  border-color: var(--accent);
@@ -389,7 +389,7 @@
389
389
  align-self: flex-start;
390
390
  }
391
391
  .btn-pill {
392
- border-radius: var(--r-pill);
392
+ border-radius: var(--btn-radius, var(--r-pill));
393
393
  }
394
394
  /* Link variant: no box, inherits the surrounding text size. Placed after the
395
395
  size rules so their min-height doesn't reapply; a tone wins over --link. */
@@ -419,23 +419,23 @@
419
419
  }
420
420
  /* `square`: side = the height contract in force (size tier or control). */
421
421
  .btn-square {
422
- width: var(--control-height-default);
423
- min-width: var(--control-height-default);
422
+ width: var(--btn-size, var(--control-height-default));
423
+ min-width: var(--btn-size, var(--control-height-default));
424
424
  padding: 0;
425
425
  flex: none;
426
426
  }
427
427
  .btn-square.btn-sm {
428
- width: var(--control-height-compact);
429
- min-width: var(--control-height-compact);
428
+ width: var(--btn-size, var(--control-height-compact));
429
+ min-width: var(--btn-size, var(--control-height-compact));
430
430
  }
431
431
  .btn-square.btn-lg {
432
- width: var(--control-height-large);
433
- min-width: var(--control-height-large);
434
- height: var(--control-height-large);
432
+ width: var(--btn-size, var(--control-height-large));
433
+ min-width: var(--btn-size, var(--control-height-large));
434
+ height: var(--btn-size, var(--control-height-large));
435
435
  }
436
436
  .btn-square.btn-control {
437
- width: var(--control-height);
438
- min-width: var(--control-height);
437
+ width: var(--btn-size, var(--control-height));
438
+ min-width: var(--btn-size, var(--control-height));
439
439
  }
440
440
  /* `box`: explicit square scale, wins over icon/chip/square/size sizing. */
441
441
  .btn-box {
@@ -130,13 +130,13 @@
130
130
  <style>
131
131
  .input {
132
132
  width: 100%;
133
- min-height: var(--control-height-default);
133
+ min-height: var(--input-size, var(--control-height-default));
134
134
  padding: var(--sp-2) var(--sp-3);
135
135
  line-height: var(--lh-tight);
136
- background: var(--bg);
137
- border: 1px solid var(--border-strong);
138
- border-radius: var(--r-md);
139
- color: var(--text);
136
+ background: var(--input-bg, var(--bg));
137
+ border: 1px solid var(--input-border, var(--border-strong));
138
+ border-radius: var(--input-radius, var(--r-md));
139
+ color: var(--input-fg, var(--text));
140
140
  transition: border-color 0.12s var(--ease);
141
141
  }
142
142
  .input:focus {
@@ -148,12 +148,12 @@
148
148
  outline-offset: var(--focus-ring-offset);
149
149
  }
150
150
  .input-sm {
151
- min-height: var(--control-height-compact);
151
+ min-height: var(--input-size, var(--control-height-compact));
152
152
  padding: var(--sp-1) var(--sp-2);
153
153
  font-size: var(--fs-sm);
154
154
  }
155
155
  .input-lg {
156
- min-height: var(--control-height-large);
156
+ min-height: var(--input-size, var(--control-height-large));
157
157
  padding: var(--sp-3) var(--sp-4);
158
158
  font-size: var(--fs-base);
159
159
  }
@@ -166,7 +166,7 @@
166
166
  flex: none;
167
167
  }
168
168
  .input-pill {
169
- border-radius: var(--r-pill);
169
+ border-radius: var(--input-radius, var(--r-pill));
170
170
  padding-inline: var(--sp-4);
171
171
  }
172
172
  .input[aria-invalid='true'] {
@@ -205,13 +205,13 @@
205
205
  }
206
206
  .select {
207
207
  width: 100%;
208
- min-height: var(--control-height-default);
208
+ min-height: var(--select-size, var(--control-height-default));
209
209
  padding: var(--sp-2) var(--sp-3);
210
210
  line-height: var(--lh-tight);
211
- background: var(--bg);
212
- border: 1px solid var(--border-strong);
213
- border-radius: var(--r-md);
214
- color: var(--text);
211
+ background: var(--select-bg, var(--bg));
212
+ border: 1px solid var(--select-border, var(--border-strong));
213
+ border-radius: var(--select-radius, var(--r-md));
214
+ color: var(--select-fg, var(--text));
215
215
  transition: border-color 0.12s var(--ease);
216
216
  }
217
217
  /* Default variant: drop the OS chevron, reserve room for our own. */
@@ -226,7 +226,7 @@
226
226
  }
227
227
  /* Compact inline form for dense headers/toolbars. */
228
228
  .select.select-lg {
229
- min-height: var(--control-height-large);
229
+ min-height: var(--select-size, var(--control-height-large));
230
230
  padding: var(--sp-3) var(--sp-4);
231
231
  font-size: var(--fs-base);
232
232
  }
@@ -240,15 +240,15 @@
240
240
  /* Toolbar contract: size="sm" shares the compact control height so it lines up
241
241
  with Button size="sm", Popover size="sm" and SegmentedControl size="sm". */
242
242
  .select.select-sm {
243
- height: var(--control-height-compact);
243
+ height: var(--select-size, var(--control-height-compact));
244
244
  }
245
245
  .select.w-auto {
246
246
  width: auto;
247
247
  }
248
248
  .select.embedded {
249
- background: var(--bg-elevated-2);
249
+ background: var(--select-bg, var(--bg-elevated-2));
250
250
  border: none;
251
- border-radius: var(--r-sm);
251
+ border-radius: var(--select-radius, var(--r-sm));
252
252
  }
253
253
  .select:focus {
254
254
  outline: none;
@@ -281,7 +281,7 @@
281
281
  gap: var(--sp-2);
282
282
  padding: 0 var(--sp-3);
283
283
  padding-right: calc(var(--sp-3) + 1.25rem);
284
- color: var(--text);
284
+ color: var(--select-fg, var(--text));
285
285
  pointer-events: none;
286
286
  overflow: hidden;
287
287
  }
@@ -95,29 +95,31 @@
95
95
  /* Base: inherit everything — a bare <Text> renders like a plain span. */
96
96
  .text {
97
97
  margin: 0;
98
+ color: var(--txt-fg, inherit);
99
+ font-size: var(--txt-size, inherit);
98
100
  }
99
101
  /* Variants (presets) — listed before tone/weight/size so those override. */
100
102
  .v-body {
101
- font-size: var(--fs-base);
103
+ font-size: var(--txt-size, var(--fs-base));
102
104
  line-height: var(--lh-normal);
103
- color: var(--text);
105
+ color: var(--txt-fg, var(--text));
104
106
  }
105
107
  .v-label {
106
- font-size: var(--fs-sm);
108
+ font-size: var(--txt-size, var(--fs-sm));
107
109
  font-weight: var(--fw-medium);
108
- color: var(--text-muted);
110
+ color: var(--txt-fg, var(--text-muted));
109
111
  }
110
112
  .v-caption {
111
- font-size: var(--fs-xs);
112
- color: var(--text-faint);
113
+ font-size: var(--txt-size, var(--fs-xs));
114
+ color: var(--txt-fg, var(--text-faint));
113
115
  }
114
116
  .v-code {
115
117
  font-family: var(--font-mono);
116
- font-size: 0.92em;
118
+ font-size: var(--txt-size, 0.92em);
117
119
  }
118
120
  .v-eyebrow {
119
- font-size: var(--fs-xs);
120
- color: var(--text-muted);
121
+ font-size: var(--txt-size, var(--fs-xs));
122
+ color: var(--txt-fg, var(--text-muted));
121
123
  font-weight: var(--fw-medium);
122
124
  text-transform: uppercase;
123
125
  letter-spacing: 0.04em;
@@ -127,28 +129,28 @@
127
129
  font-family: var(--font-mono);
128
130
  }
129
131
  .tone-default {
130
- color: var(--text);
132
+ color: var(--txt-fg, var(--text));
131
133
  }
132
134
  .tone-muted {
133
- color: var(--text-muted);
135
+ color: var(--txt-fg, var(--text-muted));
134
136
  }
135
137
  .tone-faint {
136
- color: var(--text-faint);
138
+ color: var(--txt-fg, var(--text-faint));
137
139
  }
138
140
  .tone-success {
139
- color: var(--ok);
141
+ color: var(--txt-fg, var(--ok));
140
142
  }
141
143
  .tone-warn {
142
- color: var(--warn);
144
+ color: var(--txt-fg, var(--warn));
143
145
  }
144
146
  .tone-danger {
145
- color: var(--danger);
147
+ color: var(--txt-fg, var(--danger));
146
148
  }
147
149
  .tone-info {
148
- color: var(--info);
150
+ color: var(--txt-fg, var(--info));
149
151
  }
150
152
  .tone-accent {
151
- color: var(--accent);
153
+ color: var(--txt-fg, var(--accent));
152
154
  }
153
155
  /* Weight — overrides variant weight. */
154
156
  .fw-normal {
@@ -165,46 +167,46 @@
165
167
  }
166
168
  /* Size — overrides variant size (listed last so it wins). */
167
169
  .fs-xs {
168
- font-size: var(--fs-xs);
170
+ font-size: var(--txt-size, var(--fs-xs));
169
171
  }
170
172
  .fs-sm {
171
- font-size: var(--fs-sm);
173
+ font-size: var(--txt-size, var(--fs-sm));
172
174
  }
173
175
  .fs-base {
174
- font-size: var(--fs-base);
176
+ font-size: var(--txt-size, var(--fs-base));
175
177
  }
176
178
  .fs-md {
177
- font-size: var(--fs-md);
179
+ font-size: var(--txt-size, var(--fs-md));
178
180
  }
179
181
  .fs-lg {
180
- font-size: var(--fs-lg);
182
+ font-size: var(--txt-size, var(--fs-lg));
181
183
  }
182
184
  .fs-xl {
183
- font-size: var(--fs-xl);
185
+ font-size: var(--txt-size, var(--fs-xl));
184
186
  }
185
187
  .fs-2xl {
186
- font-size: var(--fs-2xl);
188
+ font-size: var(--txt-size, var(--fs-2xl));
187
189
  }
188
190
  .noscale.fs-xs {
189
- font-size: 12px;
191
+ font-size: var(--txt-size, 12px);
190
192
  }
191
193
  .noscale.fs-sm {
192
- font-size: 13px;
194
+ font-size: var(--txt-size, 13px);
193
195
  }
194
196
  .noscale.fs-base {
195
- font-size: 15px;
197
+ font-size: var(--txt-size, 15px);
196
198
  }
197
199
  .noscale.fs-md {
198
- font-size: 16px;
200
+ font-size: var(--txt-size, 16px);
199
201
  }
200
202
  .noscale.fs-lg {
201
- font-size: 18px;
203
+ font-size: var(--txt-size, 18px);
202
204
  }
203
205
  .noscale.fs-xl {
204
- font-size: 22px;
206
+ font-size: var(--txt-size, 22px);
205
207
  }
206
208
  .noscale.fs-2xl {
207
- font-size: 28px;
209
+ font-size: var(--txt-size, 28px);
208
210
  }
209
211
  .italic {
210
212
  font-style: italic;
@@ -223,16 +223,16 @@
223
223
  --sp-3 top+bottom would overshoot it and a `rows={1}` textarea would
224
224
  render taller than the buttons it sits beside. */
225
225
  padding: var(--sp-2) var(--sp-3);
226
- background: var(--bg);
227
- border: 1px solid var(--border-strong);
228
- border-radius: var(--r-md);
229
- color: var(--text);
226
+ background: var(--textarea-bg, var(--bg));
227
+ border: 1px solid var(--textarea-border, var(--border-strong));
228
+ border-radius: var(--textarea-radius, var(--r-md));
229
+ color: var(--textarea-fg, var(--text));
230
230
  transition: border-color 0.12s var(--ease);
231
231
  /* Custom handle replaces the native grip; never show the native one. */
232
232
  resize: none;
233
233
  /* Match the single-row height of Button/Input so a `rows={1}` textarea
234
234
  lines up with them; the native `rows` attribute grows it from here. */
235
- min-height: 2.5rem;
235
+ min-height: var(--textarea-size, 2.5rem);
236
236
  line-height: var(--lh-tight);
237
237
  font-family: inherit;
238
238
  }
@@ -250,12 +250,12 @@
250
250
  .textarea-sm {
251
251
  padding: var(--sp-1) var(--sp-2);
252
252
  font-size: var(--fs-sm);
253
- min-height: 2rem;
253
+ min-height: var(--textarea-size, 2rem);
254
254
  }
255
255
  .textarea-lg {
256
256
  padding: var(--sp-3) var(--sp-4);
257
257
  font-size: var(--fs-base);
258
- min-height: var(--control-height-large);
258
+ min-height: var(--textarea-size, var(--control-height-large));
259
259
  }
260
260
  .textarea[aria-invalid='true'],
261
261
  .textarea[aria-invalid='true']:focus {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "Minimal, dependency-free Svelte 5 + pure-CSS UI kit. Token-driven atoms, molecules & layouts with theming out of the box.",
5
5
  "type": "module",
6
6
  "license": "MIT",