@spaethtech/svelte-ui 0.6.1-dev.32.fbb6fa6 → 0.6.1-dev.34.cf577c5

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.
@@ -162,7 +162,9 @@ examples):
162
162
  `onShow(ctx)`/`onHide(ctx)` control focus — set `ctx.autoFocus` to a selector/element/`false`,
163
163
  `ctx.restoreFocus` likewise) · `ConfirmDialog` (title + message + Confirm/Cancel, built on Dialog)
164
164
  · `Popup` `Menu` · `tooltip` (a Svelte `use:` action) · `anchored` (positioning engine)
165
- - **Navigation:** `TabStrip` `SideBarMenu`
165
+ - **Navigation:** `TabStrip` (tabs; `size`/`variant`, `gap`/`pad` bordered spacer cells, `height` for a
166
+ fixed-height bar with bottom-aligned tabs, `activeSurface` to flow the active tab into the content
167
+ below) · `SideBarMenu`
166
168
  - **Feedback:** `Alert` `Banner` `Badge` `Toaster` + `toast`
167
169
  - **Layout:** `Card` `CardHeader` `CardBody` `CardFooter` · **`Grid`** (auto-placed equal-cell grid;
168
170
  `columns` + `gap`)
@@ -41,11 +41,20 @@
41
41
  * Not CSS `gap` — a real cell, so the base line stays continuous through it. Excludes the grow
42
42
  * filler and the leading/trailing cells (those are `pad`). */
43
43
  gap?: number;
44
- /** Strip padding, in `rem` (default `0`). A scalar pads both equally; `{ top, sides }` sets them
45
- * independently. **sides** = leading + trailing bordered spacer cells (before the first tab, after
46
- * the last) cells, not CSS padding, so the base line runs through them. **top** = a real
47
- * `padding-top` on the strip (no line there; there is no bottom pad — the base line lives there). */
48
- pad?: number | { top?: number; sides?: number };
44
+ /** Leading + trailing bordered spacer cells (before the first tab, after the last), in `rem`
45
+ * (default `0`). Cells, not CSS padding, so the base line runs through them. For vertical room,
46
+ * set `height` (the tabs bottom-align to the base line). */
47
+ pad?: number;
48
+ /** Total strip height, in `rem` (applied as `min-height`, so it never clips a taller tab). The tab
49
+ * row bottom-aligns to the base line and the extra space breathes ABOVE it. `size` still sets the
50
+ * tab height; `height` sets the whole bar. Unset → the strip hugs the tab row. */
51
+ height?: number;
52
+ /** CSS colour for the ACTIVE tab's fill AND its bottom border — so the base line disappears under
53
+ * it and the tab reads as one surface with the content below. Default `"transparent"` (reveals
54
+ * whatever surface the strip sits on — today's behaviour). Set a token/colour to force a specific
55
+ * surface, e.g. `activeSurface="var(--ui-color-background)"` when the strip sits in a container
56
+ * whose background differs from the content below. Any CSS colour value. */
57
+ activeSurface?: string;
49
58
  onselect?: (index: number) => void;
50
59
  right?: Snippet;
51
60
  /** Optional aria-label for the strip when it renders as a navigation landmark
@@ -63,6 +72,8 @@
63
72
  borderless = false,
64
73
  gap = 0,
65
74
  pad = 0,
75
+ height,
76
+ activeSurface = "transparent",
66
77
  onselect,
67
78
  right,
68
79
  "aria-label": ariaLabel,
@@ -98,11 +109,6 @@
98
109
  const svgClass = $derived(responsiveClasses(size, svgMap));
99
110
  const gapClass = $derived(responsiveClasses(size, gapMap));
100
111
 
101
- // Normalize `pad` (scalar = both; `{ top, sides }` = independent). `sides` → leading/trailing spacer
102
- // cells; `top` → the strip's padding-top. In rem.
103
- const padTop = $derived(typeof pad === "number" ? pad : (pad.top ?? 0));
104
- const padSides = $derived(typeof pad === "number" ? pad : (pad.sides ?? 0));
105
-
106
112
  // Mode → label visibility. `expanded` shows the label span inline; `collapsed` hides it. Tabs
107
113
  // without an icon show the label regardless (falling back gracefully — nothing disappears).
108
114
  // Responsive shape supported: `{ base: 'collapsed', sm: 'expanded' }` collapses on mobile only.
@@ -231,13 +237,18 @@
231
237
  ].join(" "),
232
238
  );
233
239
 
234
- // Selected: transparent fill, so the tab shows whatever surface the strip sits on (page background,
235
- // a Card/panel, an inverse surface) and reads as one continuous surface with the content below it —
236
- // the folder-tab effect on ANY background, not just the page. Label + icon take `var(--ui-accent)`
237
- // (the variant colour, or plain text for neutral/ghost icons inherit via currentColor), the
238
- // clearest signal of the strip's variant. Bottom border transparent — the base line breaks under it.
239
- const selectedStyleClass =
240
- "[background-color:transparent] [color:var(--ui-accent)] [border-bottom-color:transparent]";
240
+ // Selected: label + icon take `var(--ui-accent)` (the variant colour, or plain text for
241
+ // neutral/ghost icons inherit via currentColor), the clearest signal of the strip's variant. The
242
+ // FILL + bottom-border colour are driven by `activeSurface` via inline `style` (below) a dynamic
243
+ // colour can't be a Tailwind class (the scanner never sees a runtime-built arbitrary value). Default
244
+ // `transparent`: the tab shows whatever surface the strip sits on and the base line breaks under it
245
+ // (the folder-tab effect on ANY background) — identical to before.
246
+ const selectedStyleClass = "[color:var(--ui-accent)]";
247
+ // Applied only to the active tab. `background-color` = the fill; `border-bottom-color` = the same, so
248
+ // the base-line segment under the active tab vanishes into that surface.
249
+ const selectedStyle = $derived(
250
+ `background-color:${activeSurface}; border-bottom-color:${activeSurface}`,
251
+ );
241
252
 
242
253
  // `ghost` is the "transparent, hover-tint only" grey variant (per `types/variants.ts`): unlike
243
254
  // `neutral`, its inactive tabs have NO resting fill — they sit on the bare surface and only tint on
@@ -277,6 +288,7 @@
277
288
  : isActive
278
289
  ? selectedStyleClass
279
290
  : `${unselectedStyleClass} ${lineBottom}`}
291
+ {@const tabStyle = isActive && !t.disabled ? selectedStyle : undefined}
280
292
  {@const tabTabIndex = t.disabled ? -1 : semanticMode === "tablist" ? (isRoving ? 0 : -1) : 0}
281
293
  {@const tabAriaCurrent =
282
294
  semanticMode === "navigation" && isActive ? ("page" as const) : undefined}
@@ -306,6 +318,7 @@
306
318
  focusedIndex = i;
307
319
  }}
308
320
  class={`${tabBase} ${stateClass}`}
321
+ style={tabStyle}
309
322
  >
310
323
  {#if t.icon}{@render t.icon()}{/if}
311
324
  <span class={`truncate ${t.icon ? labelClass : "inline-block"}`}>{t.label}</span>
@@ -329,6 +342,7 @@
329
342
  focusedIndex = i;
330
343
  }}
331
344
  class={`${tabBase} ${stateClass}`}
345
+ style={tabStyle}
332
346
  disabled={t.disabled}
333
347
  >
334
348
  {#if t.icon}{@render t.icon()}{/if}
@@ -346,12 +360,12 @@
346
360
  role={semanticMode === "tablist" ? "tablist" : "navigation"}
347
361
  aria-orientation={semanticMode === "tablist" ? "horizontal" : undefined}
348
362
  aria-label={semanticMode === "navigation" ? ariaLabel : undefined}
349
- class={`ui-accent flex items-stretch overflow-x-auto list-none m-0 p-0 ${cls}`}
350
- style={`${accentVar} padding-top:${padTop}rem; ${styleProp ?? ""}`}
363
+ class={`ui-accent flex items-end overflow-x-auto list-none m-0 p-0 ${cls}`}
364
+ style={`${accentVar}${height != null ? ` min-height:${height}rem;` : ""} ${styleProp ?? ""}`}
351
365
  {...restProps}
352
366
  >
353
367
  <!-- Leading pad cell (before the first left tab). -->
354
- {#if padSides > 0 && leftTabs.length > 0}{@render spacer(padSides)}{/if}
368
+ {#if pad > 0 && leftTabs.length > 0}{@render spacer(pad)}{/if}
355
369
 
356
370
  {#each leftTabs as { t, i }, idx (i)}
357
371
  {@render tabItem({ t, i })}
@@ -360,7 +374,7 @@
360
374
  {/each}
361
375
 
362
376
  <!-- Trailing pad cell when the left cluster is the last thing (no right tabs). -->
363
- {#if padSides > 0 && leftTabs.length > 0 && rightTabs.length === 0}{@render spacer(padSides)}{/if}
377
+ {#if pad > 0 && leftTabs.length > 0 && rightTabs.length === 0}{@render spacer(pad)}{/if}
364
378
 
365
379
  <!-- Grow filler (the "+1"): an empty list item that stretches to eat the free space and carry the
366
380
  base line across it. It sits BETWEEN the left and right clusters, so it lands last (only left
@@ -378,7 +392,7 @@
378
392
  {/if}
379
393
 
380
394
  <!-- Leading pad cell for a right-only strip (no left cluster). -->
381
- {#if padSides > 0 && rightTabs.length > 0 && leftTabs.length === 0}{@render spacer(padSides)}{/if}
395
+ {#if pad > 0 && rightTabs.length > 0 && leftTabs.length === 0}{@render spacer(pad)}{/if}
382
396
 
383
397
  {#each rightTabs as { t, i }, idx (i)}
384
398
  {@render tabItem({ t, i })}
@@ -386,5 +400,5 @@
386
400
  {/each}
387
401
 
388
402
  <!-- Trailing pad cell (after the last right tab). -->
389
- {#if padSides > 0 && rightTabs.length > 0}{@render spacer(padSides)}{/if}
403
+ {#if pad > 0 && rightTabs.length > 0}{@render spacer(pad)}{/if}
390
404
  </ul>
@@ -34,14 +34,20 @@ interface Props extends Omit<HTMLAttributes<HTMLUListElement>, "onselect"> {
34
34
  * Not CSS `gap` — a real cell, so the base line stays continuous through it. Excludes the grow
35
35
  * filler and the leading/trailing cells (those are `pad`). */
36
36
  gap?: number;
37
- /** Strip padding, in `rem` (default `0`). A scalar pads both equally; `{ top, sides }` sets them
38
- * independently. **sides** = leading + trailing bordered spacer cells (before the first tab, after
39
- * the last) cells, not CSS padding, so the base line runs through them. **top** = a real
40
- * `padding-top` on the strip (no line there; there is no bottom pad — the base line lives there). */
41
- pad?: number | {
42
- top?: number;
43
- sides?: number;
44
- };
37
+ /** Leading + trailing bordered spacer cells (before the first tab, after the last), in `rem`
38
+ * (default `0`). Cells, not CSS padding, so the base line runs through them. For vertical room,
39
+ * set `height` (the tabs bottom-align to the base line). */
40
+ pad?: number;
41
+ /** Total strip height, in `rem` (applied as `min-height`, so it never clips a taller tab). The tab
42
+ * row bottom-aligns to the base line and the extra space breathes ABOVE it. `size` still sets the
43
+ * tab height; `height` sets the whole bar. Unset → the strip hugs the tab row. */
44
+ height?: number;
45
+ /** CSS colour for the ACTIVE tab's fill AND its bottom border — so the base line disappears under
46
+ * it and the tab reads as one surface with the content below. Default `"transparent"` (reveals
47
+ * whatever surface the strip sits on — today's behaviour). Set a token/colour to force a specific
48
+ * surface, e.g. `activeSurface="var(--ui-color-background)"` when the strip sits in a container
49
+ * whose background differs from the content below. Any CSS colour value. */
50
+ activeSurface?: string;
45
51
  onselect?: (index: number) => void;
46
52
  right?: Snippet;
47
53
  /** Optional aria-label for the strip when it renders as a navigation landmark
@@ -197,6 +197,22 @@ Thin wrappers over `Input`, each preconfigured with an MDI icon (rendered into I
197
197
  - **Location**: `src/lib/components/NumberInput.svelte`
198
198
  - **Features**: number formatting and validation
199
199
 
200
+ ## Navigation
201
+
202
+ ### TabStrip
203
+
204
+ A horizontal tab bar (renders as a `tablist` of buttons, or a `navigation` of links when any tab has
205
+ `href`). Tabs split into left / right clusters; a continuous base line runs under the strip and breaks
206
+ under the active tab. Fully demoed at `/tabstrip`.
207
+
208
+ - **Location**: `src/lib/components/TabStrip/TabStrip.svelte`
209
+ - **Axes**: `size`, `variant` (both `Responsive`)
210
+ - **Props**: `tabs` (`TabDefinition[]`), `activeTabIndex` (bindable), `mode` (`'expanded'` |
211
+ `'collapsed'`, responsive), `borderless`, `gap` (rem — bordered cells between tabs), `pad` (rem —
212
+ leading/trailing side cells), **`height`** (rem — total bar height; tabs bottom-align, space above),
213
+ **`activeSurface`** (CSS colour for the active tab's fill + bottom border; default `"transparent"`),
214
+ `right` (Snippet), `aria-label`.
215
+
200
216
  ## Data Components
201
217
 
202
218
  These pair with the headless **`@spaethtech/svelte-ui/data`** layer (query-language parser/AST, `createGrid`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.6.1-dev.32.fbb6fa6",
3
+ "version": "0.6.1-dev.34.cf577c5",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"