@dorsk/tsumikit 0.49.0 → 0.50.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
@@ -166,7 +166,9 @@ threshold tones via `warnAt`/`dangerAt`, `corner` snippet), Artwork (lazy cover
166
166
  fallback, `aspect`, `status` overlay), Card (`tone` tints the surface for inline banners), Badge
167
167
  (`tone` semantic palette or `color` for any CSS colour, `size` xs/sm/md, `dot`,
168
168
  `icon`, `numeric`, `truncate`, `variant="text"`; all tints derive from
169
- `--badge-tone`), Dot (`ring` dark halo over artwork), Link (`tone`, `underline`
169
+ `--badge-tone`), Dot (`ring` dark halo over artwork), Divider (standalone
170
+ horizontal/vertical hairline, `tone`, `spacing`, optional centred label,
171
+ `decorative` to drop the `separator` role), Link (`tone`, `underline`
170
172
  always/hover/none, `align`), Icon (open registry — pass a `children` snippet for
171
173
  any custom SVG).
172
174
 
@@ -219,12 +221,17 @@ sidebar on desktop, overlay drawer on mobile, optionally resizable;
219
221
  the content column only, `stickySidebar` pins it to the viewport, and
220
222
  `sidebarPadding="none" | "sm" | "md"` sets the aside gutter — the header and its
221
223
  children are `min-width: 0` so a wide title/actions row can't widen the grid on
222
- mobile), NavItem
224
+ mobile), NavBar (horizontal route bar — `<nav>` + `<a aria-current="page">`,
225
+ icon/emoji + label + count badge; `placement="bar"` pins it to the bottom edge
226
+ over `--z-nav` with `--safe-bottom` padding, `orientation` stacks or inlines the
227
+ glyph, `maxWidth` centres the row), NavItem
223
228
  (collapses to an icon rail when the sidebar is narrow; `icon` from the
224
229
  registry, `iconPath` / `iconChildren` for custom glyphs, `activeStyle="bar"`
225
230
  for the inset-bar active look), MasterDetail (list + detail columns that
226
231
  become two pages — one pane at a time with a sticky 44px back header — below
227
- their own `breakpoint`, so `selected` can be driven from the URL), Container, Stack
232
+ their own `breakpoint`, so `selected` can be driven from the URL; `gap` spaces
233
+ the columns and `divider="none"` drops the seam, both also settable as the
234
+ `--md-gap` / `--md-divider` custom properties), Container, Stack
228
235
  (vertical), Cluster (wrapping row; `stackAt="xs|sm|md|lg"` makes it its own
229
236
  query container and stacks children full-width below 18/30/40/48rem — phone
230
237
  action rows without a viewport query), AutoGrid (intrinsically responsive
@@ -0,0 +1,107 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ // Standalone hairline between two siblings. Horizontal by default; the
5
+ // vertical form stretches to the flex parent's cross size. With `children`
6
+ // the rule splits around a centred label ("or", "3 older messages") — a
7
+ // labelled divider is never decorative, so it keeps role="separator".
8
+ type Own = {
9
+ orientation?: 'horizontal' | 'vertical';
10
+ /** Margin along the axis (any CSS length). */
11
+ spacing?: string;
12
+ tone?: 'default' | 'strong' | 'faint';
13
+ /** Centred label turning the rule into rule + text. */
14
+ children?: Snippet;
15
+ /** Purely visual (aria-hidden) rather than a semantic separator. */
16
+ decorative?: boolean;
17
+ class?: string;
18
+ };
19
+ let {
20
+ orientation = 'horizontal',
21
+ spacing,
22
+ tone = 'default',
23
+ children,
24
+ decorative = false,
25
+ class: klass = '',
26
+ ...rest
27
+ }: Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own = $props();
28
+
29
+ const vertical = $derived(orientation === 'vertical');
30
+ const semantic = $derived(!decorative || !!children);
31
+ </script>
32
+
33
+ <div
34
+ class="divider {orientation} tone-{tone} {klass}"
35
+ class:labelled={!!children}
36
+ data-tsu="Divider"
37
+ role={semantic ? 'separator' : undefined}
38
+ aria-orientation={semantic ? orientation : undefined}
39
+ aria-hidden={semantic ? undefined : 'true'}
40
+ style:--divider-spacing={spacing}
41
+ {...rest}
42
+ >
43
+ {#if children}
44
+ <span class="divider-label">{@render children()}</span>
45
+ {/if}
46
+ </div>
47
+
48
+ <style>
49
+ .divider {
50
+ --divider-color: var(--border);
51
+ --divider-spacing: var(--sp-3);
52
+ flex: none;
53
+ border: 0;
54
+ }
55
+ .tone-strong {
56
+ --divider-color: var(--border-strong);
57
+ }
58
+ .tone-faint {
59
+ --divider-color: color-mix(in srgb, var(--border) 45%, transparent);
60
+ }
61
+ .horizontal {
62
+ width: 100%;
63
+ height: 1px;
64
+ background: var(--divider-color);
65
+ margin-block: var(--divider-spacing);
66
+ }
67
+ .vertical {
68
+ align-self: stretch;
69
+ width: 1px;
70
+ min-height: 1em;
71
+ background: var(--divider-color);
72
+ margin-inline: var(--divider-spacing);
73
+ }
74
+ .horizontal.labelled,
75
+ .vertical.labelled {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: var(--sp-2);
79
+ background: none;
80
+ }
81
+ .horizontal.labelled {
82
+ height: auto;
83
+ }
84
+ .vertical.labelled {
85
+ flex-direction: column;
86
+ width: auto;
87
+ }
88
+ .horizontal.labelled::before,
89
+ .horizontal.labelled::after {
90
+ content: '';
91
+ flex: 1;
92
+ height: 1px;
93
+ background: var(--divider-color);
94
+ }
95
+ .vertical.labelled::before,
96
+ .vertical.labelled::after {
97
+ content: '';
98
+ flex: 1;
99
+ width: 1px;
100
+ background: var(--divider-color);
101
+ }
102
+ .divider-label {
103
+ color: var(--text-muted);
104
+ font-size: var(--fs-sm);
105
+ white-space: nowrap;
106
+ }
107
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type Own = {
4
+ orientation?: 'horizontal' | 'vertical';
5
+ /** Margin along the axis (any CSS length). */
6
+ spacing?: string;
7
+ tone?: 'default' | 'strong' | 'faint';
8
+ /** Centred label turning the rule into rule + text. */
9
+ children?: Snippet;
10
+ /** Purely visual (aria-hidden) rather than a semantic separator. */
11
+ decorative?: boolean;
12
+ class?: string;
13
+ };
14
+ type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own;
15
+ declare const Divider: import("svelte").Component<$$ComponentProps, {}, "">;
16
+ type Divider = ReturnType<typeof Divider>;
17
+ export default Divider;
@@ -15,6 +15,8 @@
15
15
  empty,
16
16
  listWidth = '17rem',
17
17
  breakpoint = '48rem',
18
+ gap = '0',
19
+ divider = 'border',
18
20
  selected = false,
19
21
  onback,
20
22
  backLabel = 'Back',
@@ -32,6 +34,10 @@
32
34
  listWidth?: string;
33
35
  /** Own-width threshold (px/em/rem) below which the panes become two pages. */
34
36
  breakpoint?: string;
37
+ /** Space between the two columns (any CSS length). */
38
+ gap?: string;
39
+ /** Seam between the panes; `--md-divider` overrides it with any border shorthand. */
40
+ divider?: 'border' | 'none';
35
41
  selected?: boolean;
36
42
  onback?: () => void;
37
43
  backLabel?: string;
@@ -72,7 +78,9 @@
72
78
  class="md {klass}"
73
79
  class:mobile
74
80
  class:selected
75
- style="--md-list-w: {listWidth};{style ? ` ${style}` : ''}"
81
+ style="--md-list-w: {listWidth}; --md-gap: {gap}; --md-divider: {divider === 'none'
82
+ ? 'none'
83
+ : '1px solid var(--border)'};{style ? ` ${style}` : ''}"
76
84
  data-tsu="MasterDetail"
77
85
  {...rest}
78
86
  >
@@ -113,6 +121,7 @@
113
121
  .md {
114
122
  display: grid;
115
123
  grid-template-columns: var(--md-list-w) minmax(0, 1fr);
124
+ gap: var(--md-gap, 0);
116
125
  min-width: 0;
117
126
  min-height: 0;
118
127
  height: 100%;
@@ -127,7 +136,7 @@
127
136
  -webkit-overflow-scrolling: touch;
128
137
  }
129
138
  .md-list {
130
- border-right: 1px solid var(--border);
139
+ border-right: var(--md-divider, 1px solid var(--border));
131
140
  }
132
141
  .md-detail {
133
142
  display: flex;
@@ -183,6 +192,7 @@
183
192
 
184
193
  .md.mobile {
185
194
  grid-template-columns: minmax(0, 1fr);
195
+ gap: 0;
186
196
  }
187
197
  .md.mobile .md-list {
188
198
  border-right: 0;
@@ -9,6 +9,10 @@ type $$ComponentProps = {
9
9
  listWidth?: string;
10
10
  /** Own-width threshold (px/em/rem) below which the panes become two pages. */
11
11
  breakpoint?: string;
12
+ /** Space between the two columns (any CSS length). */
13
+ gap?: string;
14
+ /** Seam between the panes; `--md-divider` overrides it with any border shorthand. */
15
+ divider?: 'border' | 'none';
12
16
  selected?: boolean;
13
17
  onback?: () => void;
14
18
  backLabel?: string;
@@ -0,0 +1,170 @@
1
+ <script lang="ts">
2
+ // Horizontal route navigation: a `<nav>` landmark of `<a>` links, one marked
3
+ // `aria-current="page"`. `placement="bar"` pins it to the bottom edge over
4
+ // `--z-nav` and pads for the iOS home indicator with `--safe-bottom`;
5
+ // `placement="inline"` is a plain row for a header. `orientation` stacks the
6
+ // icon over the label (phone bar) or sits it beside (header).
7
+ import Icon from '../atoms/Icon.svelte';
8
+ import type { IconName } from '../atoms/Icon.svelte';
9
+
10
+ export interface NavBarItem {
11
+ href: string;
12
+ label: string;
13
+ icon?: IconName;
14
+ /** Rendered instead of `icon` when set. */
15
+ emoji?: string;
16
+ /** Count or short text pinned to the glyph. 0 / '' render nothing. */
17
+ badge?: number | string;
18
+ current?: boolean;
19
+ }
20
+
21
+ let {
22
+ items,
23
+ label,
24
+ placement = 'inline',
25
+ orientation = 'stacked',
26
+ maxWidth,
27
+ class: klass = '',
28
+ style: styleProp = '',
29
+ ...rest
30
+ }: {
31
+ items: NavBarItem[];
32
+ /** Accessible name of the `<nav>` landmark. */
33
+ label: string;
34
+ /** 'bar' = fixed to the bottom edge with safe-area padding. */
35
+ placement?: 'bar' | 'inline';
36
+ /** Icon over label vs icon beside label. */
37
+ orientation?: 'stacked' | 'inline';
38
+ /** Caps and centres the row of links. */
39
+ maxWidth?: string;
40
+ class?: string;
41
+ style?: string;
42
+ [key: string]: unknown;
43
+ } = $props();
44
+
45
+ const hasBadge = (badge: NavBarItem['badge']) =>
46
+ badge !== undefined && badge !== null && badge !== '' && badge !== 0;
47
+ </script>
48
+
49
+ <nav
50
+ data-tsu="NavBar"
51
+ aria-label={label}
52
+ class="navbar {klass}"
53
+ class:bar={placement === 'bar'}
54
+ class:stacked={orientation === 'stacked'}
55
+ style="{maxWidth ? `--navbar-max: ${maxWidth};` : ''}{styleProp}"
56
+ {...rest}
57
+ >
58
+ <div class="navbar-inner">
59
+ {#each items as item (item.href)}
60
+ <a
61
+ href={item.href}
62
+ class="navbar-link"
63
+ class:current={item.current}
64
+ aria-current={item.current ? 'page' : undefined}
65
+ >
66
+ <span class="navbar-glyph">
67
+ {#if item.emoji}
68
+ <span class="navbar-emoji" aria-hidden="true">{item.emoji}</span>
69
+ {:else if item.icon}
70
+ <Icon name={item.icon} size={20} />
71
+ {/if}
72
+ {#if hasBadge(item.badge)}
73
+ <span class="navbar-badge">{item.badge}</span>
74
+ {/if}
75
+ </span>
76
+ <span class="navbar-label">{item.label}</span>
77
+ </a>
78
+ {/each}
79
+ </div>
80
+ </nav>
81
+
82
+ <style>
83
+ .navbar {
84
+ --navbar-max: none;
85
+ min-width: 0;
86
+ }
87
+ .navbar-inner {
88
+ display: flex;
89
+ align-items: stretch;
90
+ justify-content: space-around;
91
+ gap: var(--sp-1);
92
+ min-width: 0;
93
+ max-width: var(--navbar-max);
94
+ margin-inline: auto;
95
+ min-height: var(--nav-h);
96
+ }
97
+ .bar {
98
+ position: fixed;
99
+ inset-inline: 0;
100
+ bottom: 0;
101
+ z-index: var(--z-nav);
102
+ padding-bottom: var(--safe-bottom);
103
+ padding-inline: max(var(--safe-left), var(--sp-1)) max(var(--safe-right), var(--sp-1));
104
+ background: color-mix(in srgb, var(--bg-elevated) 88%, transparent);
105
+ backdrop-filter: blur(12px);
106
+ border-top: 1px solid var(--border);
107
+ }
108
+ .navbar-link {
109
+ position: relative;
110
+ display: flex;
111
+ flex: 1 1 0;
112
+ min-width: 0;
113
+ flex-direction: row;
114
+ align-items: center;
115
+ justify-content: center;
116
+ gap: var(--sp-2);
117
+ padding: var(--sp-2);
118
+ border-radius: var(--r-md);
119
+ color: var(--text-muted);
120
+ font-size: var(--fs-sm);
121
+ font-weight: var(--fw-medium);
122
+ text-decoration: none;
123
+ transition:
124
+ background 0.12s var(--ease),
125
+ color 0.12s var(--ease);
126
+ }
127
+ .stacked .navbar-link {
128
+ flex-direction: column;
129
+ gap: 2px;
130
+ font-size: var(--fs-xs);
131
+ }
132
+ .navbar-link:hover {
133
+ background: var(--bg-elevated-2);
134
+ color: var(--text);
135
+ text-decoration: none;
136
+ }
137
+ .navbar-link.current {
138
+ color: var(--accent);
139
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
140
+ }
141
+ .navbar-glyph {
142
+ position: relative;
143
+ display: inline-flex;
144
+ flex: none;
145
+ align-items: center;
146
+ }
147
+ .navbar-emoji {
148
+ font-size: 1.15em;
149
+ line-height: 1;
150
+ }
151
+ .navbar-label {
152
+ min-width: 0;
153
+ overflow: hidden;
154
+ text-overflow: ellipsis;
155
+ white-space: nowrap;
156
+ }
157
+ .navbar-badge {
158
+ position: absolute;
159
+ top: -0.35rem;
160
+ left: calc(100% - 0.35rem);
161
+ padding-inline: 0.25rem;
162
+ min-width: 1rem;
163
+ border-radius: var(--r-pill);
164
+ background: var(--accent);
165
+ color: var(--text-on-accent);
166
+ font-size: var(--fs-xs);
167
+ line-height: 1rem;
168
+ text-align: center;
169
+ }
170
+ </style>
@@ -0,0 +1,28 @@
1
+ import type { IconName } from '../atoms/Icon.svelte';
2
+ export interface NavBarItem {
3
+ href: string;
4
+ label: string;
5
+ icon?: IconName;
6
+ /** Rendered instead of `icon` when set. */
7
+ emoji?: string;
8
+ /** Count or short text pinned to the glyph. 0 / '' render nothing. */
9
+ badge?: number | string;
10
+ current?: boolean;
11
+ }
12
+ type $$ComponentProps = {
13
+ items: NavBarItem[];
14
+ /** Accessible name of the `<nav>` landmark. */
15
+ label: string;
16
+ /** 'bar' = fixed to the bottom edge with safe-area padding. */
17
+ placement?: 'bar' | 'inline';
18
+ /** Icon over label vs icon beside label. */
19
+ orientation?: 'stacked' | 'inline';
20
+ /** Caps and centres the row of links. */
21
+ maxWidth?: string;
22
+ class?: string;
23
+ style?: string;
24
+ [key: string]: unknown;
25
+ };
26
+ declare const NavBar: import("svelte").Component<$$ComponentProps, {}, "">;
27
+ type NavBar = ReturnType<typeof NavBar>;
28
+ export default NavBar;
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { default as Badge } from './components/atoms/Badge.svelte';
6
6
  export { default as Button } from './components/atoms/Button.svelte';
7
7
  export { default as Card } from './components/atoms/Card.svelte';
8
8
  export { default as Checkbox } from './components/atoms/Checkbox.svelte';
9
+ export { default as Divider } from './components/atoms/Divider.svelte';
9
10
  export { default as Dot } from './components/atoms/Dot.svelte';
10
11
  export { default as Gauge, type GaugeTone } from './components/atoms/Gauge.svelte';
11
12
  export { default as Heading } from './components/atoms/Heading.svelte';
@@ -30,6 +31,7 @@ export { default as AutoGrid } from './components/layouts/AutoGrid.svelte';
30
31
  export { default as Cluster } from './components/layouts/Cluster.svelte';
31
32
  export { default as Container } from './components/layouts/Container.svelte';
32
33
  export { default as MasterDetail } from './components/layouts/MasterDetail.svelte';
34
+ export { default as NavBar, type NavBarItem } from './components/layouts/NavBar.svelte';
33
35
  export { default as NavItem } from './components/layouts/NavItem.svelte';
34
36
  export { default as NavSection } from './components/layouts/NavSection.svelte';
35
37
  export { default as ResizablePanel } from './components/layouts/ResizablePanel.svelte';
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ export { default as Badge } from './components/atoms/Badge.svelte';
12
12
  export { default as Button } from './components/atoms/Button.svelte';
13
13
  export { default as Card } from './components/atoms/Card.svelte';
14
14
  export { default as Checkbox } from './components/atoms/Checkbox.svelte';
15
+ export { default as Divider } from './components/atoms/Divider.svelte';
15
16
  export { default as Dot } from './components/atoms/Dot.svelte';
16
17
  export { default as Gauge } from './components/atoms/Gauge.svelte';
17
18
  export { default as Heading } from './components/atoms/Heading.svelte';
@@ -36,6 +37,7 @@ export { default as AutoGrid } from './components/layouts/AutoGrid.svelte';
36
37
  export { default as Cluster } from './components/layouts/Cluster.svelte';
37
38
  export { default as Container } from './components/layouts/Container.svelte';
38
39
  export { default as MasterDetail } from './components/layouts/MasterDetail.svelte';
40
+ export { default as NavBar } from './components/layouts/NavBar.svelte';
39
41
  export { default as NavItem } from './components/layouts/NavItem.svelte';
40
42
  export { default as NavSection } from './components/layouts/NavSection.svelte';
41
43
  export { default as ResizablePanel } from './components/layouts/ResizablePanel.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.49.0",
3
+ "version": "0.50.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",