@dorsk/tsumikit 0.35.0 → 0.36.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.
@@ -0,0 +1,134 @@
1
+ <script lang="ts">
2
+ // Working-directory chip that abbreviates fish-style as its slot narrows:
3
+ // ancestors shrink to their first letter left → right, then the leaf alone,
4
+ // then the leaf ellipsised down to `minLeaf` chars. Candidate generation
5
+ // lives in `$lib/working-dir`; this component only measures and picks.
6
+ import Badge from '../atoms/Badge.svelte';
7
+ import Icon, { type IconName } from '../atoms/Icon.svelte';
8
+ import { copyToClipboard } from '../../clipboard';
9
+ import { normalizeWorkingDir, workingDirCandidates } from '../../working-dir';
10
+
11
+ let {
12
+ path,
13
+ minLeaf = 18,
14
+ mono = true,
15
+ full = false,
16
+ icon = 'folder',
17
+ title,
18
+ copy = false,
19
+ copiedLabel = 'Copied',
20
+ oncopy,
21
+ class: klass = '',
22
+ style = ''
23
+ }: {
24
+ path: string;
25
+ /** Char floor the chip reserves so the (truncated) leaf is always legible. */
26
+ minLeaf?: number;
27
+ mono?: boolean;
28
+ /** Skip the fit algorithm: render the whole path and size to content. */
29
+ full?: boolean;
30
+ icon?: IconName;
31
+ /** Defaults to the full path. */
32
+ title?: string;
33
+ /** Render as a button that copies the full path on click. */
34
+ copy?: boolean;
35
+ copiedLabel?: string;
36
+ oncopy?: (ok: boolean) => void;
37
+ class?: string;
38
+ style?: string;
39
+ } = $props();
40
+
41
+ const normalized = $derived(normalizeWorkingDir(path));
42
+ const candidates = $derived(workingDirCandidates(path, { minLeaf }));
43
+
44
+ let avail = $state(Infinity);
45
+ let chPx = $state(8);
46
+ let chrome = $state(0);
47
+ let rail: HTMLSpanElement | undefined = $state();
48
+ let txt: HTMLSpanElement | undefined = $state();
49
+ let probe: HTMLSpanElement | undefined = $state();
50
+ let status = $state('');
51
+ let timer: ReturnType<typeof setTimeout> | undefined;
52
+
53
+ $effect(() => {
54
+ if (!rail || full) return;
55
+ const measure = () => {
56
+ avail = rail!.clientWidth;
57
+ if (probe) chPx = probe.getBoundingClientRect().width / 10 || chPx;
58
+ const badgeEl = txt?.parentElement;
59
+ if (badgeEl && txt) chrome = Math.max(0, badgeEl.offsetWidth - txt.offsetWidth);
60
+ };
61
+ measure();
62
+ const ro = new ResizeObserver(measure);
63
+ ro.observe(rail);
64
+ return () => ro.disconnect();
65
+ });
66
+
67
+ const shown = $derived.by(() => {
68
+ if (full) return normalized;
69
+ const budget = Math.max(0, avail - chrome - 1);
70
+ return candidates.find((c) => c.length * chPx <= budget) ?? candidates[candidates.length - 1];
71
+ });
72
+
73
+ async function onclick(e: MouseEvent) {
74
+ e.stopPropagation();
75
+ const ok = await copyToClipboard(normalized);
76
+ oncopy?.(ok);
77
+ status = ok ? copiedLabel : 'Copy failed';
78
+ clearTimeout(timer);
79
+ timer = setTimeout(() => {
80
+ status = '';
81
+ }, 1500);
82
+ }
83
+ </script>
84
+
85
+ <span
86
+ data-tsu="WorkingDir"
87
+ class="rail {klass}"
88
+ class:full
89
+ bind:this={rail}
90
+ {style}
91
+ >
92
+ <Badge
93
+ as={copy ? 'button' : 'span'}
94
+ {mono}
95
+ style="min-width:0;max-width:100%;overflow:hidden;text-align:left"
96
+ title={title ?? normalized}
97
+ onpointerdown={copy ? (e: PointerEvent) => e.stopPropagation() : undefined}
98
+ onclick={copy ? onclick : undefined}
99
+ >
100
+ <Icon name={icon} />
101
+ <span class="text" bind:this={txt}>{shown}</span>
102
+ {#if !full}
103
+ <span class="probe" bind:this={probe} aria-hidden="true">{'0'.repeat(10)}</span>
104
+ {/if}
105
+ </Badge>
106
+ {#if copy}
107
+ <span class="sr-only" role="status" aria-live="polite">{status}</span>
108
+ {/if}
109
+ </span>
110
+
111
+ <style>
112
+ .rail {
113
+ display: flex;
114
+ align-items: center;
115
+ flex: 1 1 0;
116
+ min-width: 0;
117
+ overflow: hidden;
118
+ }
119
+ .rail.full {
120
+ flex: none;
121
+ max-width: 100%;
122
+ }
123
+ .text {
124
+ overflow: hidden;
125
+ white-space: nowrap;
126
+ text-overflow: ellipsis;
127
+ }
128
+ .probe {
129
+ position: absolute;
130
+ visibility: hidden;
131
+ white-space: pre;
132
+ pointer-events: none;
133
+ }
134
+ </style>
@@ -0,0 +1,21 @@
1
+ import { type IconName } from '../atoms/Icon.svelte';
2
+ type $$ComponentProps = {
3
+ path: string;
4
+ /** Char floor the chip reserves so the (truncated) leaf is always legible. */
5
+ minLeaf?: number;
6
+ mono?: boolean;
7
+ /** Skip the fit algorithm: render the whole path and size to content. */
8
+ full?: boolean;
9
+ icon?: IconName;
10
+ /** Defaults to the full path. */
11
+ title?: string;
12
+ /** Render as a button that copies the full path on click. */
13
+ copy?: boolean;
14
+ copiedLabel?: string;
15
+ oncopy?: (ok: boolean) => void;
16
+ class?: string;
17
+ style?: string;
18
+ };
19
+ declare const WorkingDir: import("svelte").Component<$$ComponentProps, {}, "">;
20
+ type WorkingDir = ReturnType<typeof WorkingDir>;
21
+ export default WorkingDir;
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ 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
9
  export { default as Dot } from './components/atoms/Dot.svelte';
10
+ export { default as Gauge, type GaugeTone } from './components/atoms/Gauge.svelte';
10
11
  export { default as Heading } from './components/atoms/Heading.svelte';
11
12
  export type { IconName } from './components/atoms/Icon.svelte';
12
13
  export { default as Icon } from './components/atoms/Icon.svelte';
@@ -27,6 +28,7 @@ export { default as AppShell } from './components/layouts/AppShell.svelte';
27
28
  export { default as AutoGrid } from './components/layouts/AutoGrid.svelte';
28
29
  export { default as Cluster } from './components/layouts/Cluster.svelte';
29
30
  export { default as Container } from './components/layouts/Container.svelte';
31
+ export { default as MasterDetail } from './components/layouts/MasterDetail.svelte';
30
32
  export { default as NavItem } from './components/layouts/NavItem.svelte';
31
33
  export { default as NavSection } from './components/layouts/NavSection.svelte';
32
34
  export { default as ResizablePanel } from './components/layouts/ResizablePanel.svelte';
@@ -35,15 +37,18 @@ export { default as Stack } from './components/layouts/Stack.svelte';
35
37
  export { type AccordionItem, default as Accordion, } from './components/molecules/Accordion.svelte';
36
38
  export { type BreadcrumbItem, default as Breadcrumb, } from './components/molecules/Breadcrumb.svelte';
37
39
  export { default as Callout } from './components/molecules/Callout.svelte';
40
+ export { default as CapBar } from './components/molecules/CapBar.svelte';
38
41
  export { default as CodeBlock } from './components/molecules/CodeBlock.svelte';
39
42
  export { default as ConfirmModal } from './components/molecules/ConfirmModal.svelte';
40
43
  export { default as CopyButton } from './components/molecules/CopyButton.svelte';
41
44
  export { default as Dropzone } from './components/molecules/Dropzone.svelte';
42
45
  export { default as EmptyState } from './components/molecules/EmptyState.svelte';
43
46
  export { default as Field } from './components/molecules/Field.svelte';
47
+ export { default as Fieldset } from './components/molecules/Fieldset.svelte';
44
48
  export { default as FileButton } from './components/molecules/FileButton.svelte';
45
49
  export { default as FilterInput, type FilterInputContext, } from './components/molecules/FilterInput.svelte';
46
50
  export { default as FontScalePicker } from './components/molecules/FontScalePicker.svelte';
51
+ export { type DiffStats, default as GitRef, type PrState, type PullRequest, } from './components/molecules/GitRef.svelte';
47
52
  export { default as IconButton } from './components/molecules/IconButton.svelte';
48
53
  export { default as KeyValue, type KeyValueRow, type KeyValueTone, } from './components/molecules/KeyValue.svelte';
49
54
  export { default as LoadMore } from './components/molecules/LoadMore.svelte';
@@ -64,6 +69,7 @@ export { default as Toaster } from './components/molecules/Toaster.svelte';
64
69
  export { default as Toggle } from './components/molecules/Toggle.svelte';
65
70
  export { default as Tooltip } from './components/molecules/Tooltip.svelte';
66
71
  export { default as Truncate } from './components/molecules/Truncate.svelte';
72
+ export { default as WorkingDir } from './components/molecules/WorkingDir.svelte';
67
73
  export { type Column, default as DataTable, type RowTone, } from './components/organisms/DataTable.svelte';
68
74
  export { default as FilterSearchBar } from './components/organisms/FilterSearchBar.svelte';
69
75
  export { FIELD_KEY, type FieldContext, getFieldContext, setFieldContext, warnUnlabelled, } from './field-context';
@@ -74,3 +80,4 @@ export { type Toast, type ToastAction, type ToastOptions, type ToastTone, type T
74
80
  export { formatTimestamp, localTimeZone, relativeTime, type TimeInput, type TimestampMode, } from './timestamp';
75
81
  export { canonicalTone, type Tone } from './tone';
76
82
  export { type TruncateMode, type TruncateOptions, truncate } from './truncate';
83
+ export { normalizeWorkingDir, type WorkingDirOptions, workingDirCandidates } from './working-dir';
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ 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
15
  export { default as Dot } from './components/atoms/Dot.svelte';
16
+ export { default as Gauge } from './components/atoms/Gauge.svelte';
16
17
  export { default as Heading } from './components/atoms/Heading.svelte';
17
18
  export { default as Icon } from './components/atoms/Icon.svelte';
18
19
  export { default as Input } from './components/atoms/Input.svelte';
@@ -34,6 +35,7 @@ export { default as AppShell } from './components/layouts/AppShell.svelte';
34
35
  export { default as AutoGrid } from './components/layouts/AutoGrid.svelte';
35
36
  export { default as Cluster } from './components/layouts/Cluster.svelte';
36
37
  export { default as Container } from './components/layouts/Container.svelte';
38
+ export { default as MasterDetail } from './components/layouts/MasterDetail.svelte';
37
39
  export { default as NavItem } from './components/layouts/NavItem.svelte';
38
40
  export { default as NavSection } from './components/layouts/NavSection.svelte';
39
41
  export { default as ResizablePanel } from './components/layouts/ResizablePanel.svelte';
@@ -42,6 +44,7 @@ export { default as Stack } from './components/layouts/Stack.svelte';
42
44
  export { default as Accordion, } from './components/molecules/Accordion.svelte';
43
45
  export { default as Breadcrumb, } from './components/molecules/Breadcrumb.svelte';
44
46
  export { default as Callout } from './components/molecules/Callout.svelte';
47
+ export { default as CapBar } from './components/molecules/CapBar.svelte';
45
48
  export { default as CodeBlock } from './components/molecules/CodeBlock.svelte';
46
49
  export { default as ConfirmModal } from './components/molecules/ConfirmModal.svelte';
47
50
  export { default as CopyButton } from './components/molecules/CopyButton.svelte';
@@ -49,9 +52,11 @@ export { default as Dropzone } from './components/molecules/Dropzone.svelte';
49
52
  export { default as EmptyState } from './components/molecules/EmptyState.svelte';
50
53
  // ---- molecules ----
51
54
  export { default as Field } from './components/molecules/Field.svelte';
55
+ export { default as Fieldset } from './components/molecules/Fieldset.svelte';
52
56
  export { default as FileButton } from './components/molecules/FileButton.svelte';
53
57
  export { default as FilterInput, } from './components/molecules/FilterInput.svelte';
54
58
  export { default as FontScalePicker } from './components/molecules/FontScalePicker.svelte';
59
+ export { default as GitRef, } from './components/molecules/GitRef.svelte';
55
60
  export { default as IconButton } from './components/molecules/IconButton.svelte';
56
61
  export { default as KeyValue, } from './components/molecules/KeyValue.svelte';
57
62
  export { default as LoadMore } from './components/molecules/LoadMore.svelte';
@@ -74,6 +79,7 @@ export { default as Toaster } from './components/molecules/Toaster.svelte';
74
79
  export { default as Toggle } from './components/molecules/Toggle.svelte';
75
80
  export { default as Tooltip } from './components/molecules/Tooltip.svelte';
76
81
  export { default as Truncate } from './components/molecules/Truncate.svelte';
82
+ export { default as WorkingDir } from './components/molecules/WorkingDir.svelte';
77
83
  // ---- organisms ----
78
84
  export { default as DataTable, } from './components/organisms/DataTable.svelte';
79
85
  export { default as FilterSearchBar } from './components/organisms/FilterSearchBar.svelte';
@@ -87,3 +93,4 @@ export { toasts, } from './stores/toast.svelte';
87
93
  export { formatTimestamp, localTimeZone, relativeTime, } from './timestamp';
88
94
  export { canonicalTone } from './tone';
89
95
  export { truncate } from './truncate';
96
+ export { normalizeWorkingDir, workingDirCandidates } from './working-dir';
@@ -0,0 +1,12 @@
1
+ export interface WorkingDirOptions {
2
+ /** Visible chars the truncated leaf keeps before the ellipsis, at minimum. Default 18. */
3
+ minLeaf?: number;
4
+ }
5
+ /** Trailing slashes dropped; an empty or root-only path becomes `/`. */
6
+ export declare function normalizeWorkingDir(path: string): string;
7
+ /**
8
+ * Candidate renderings of `path`:
9
+ * full path → ancestors abbreviated to their first letter one at a time,
10
+ * left to right → leaf only → leaf end-truncated down to `minLeaf` chars.
11
+ */
12
+ export declare function workingDirCandidates(path: string, options?: WorkingDirOptions): string[];
@@ -0,0 +1,38 @@
1
+ // Fish-style path abbreviation candidates, richest → poorest. The WorkingDir
2
+ // molecule picks the first entry that fits its measured width; the last entry
3
+ // (the leaf clipped to `minLeaf` chars) is the hard floor.
4
+ import { truncate } from './truncate';
5
+ /** Trailing slashes dropped; an empty or root-only path becomes `/`. */
6
+ export function normalizeWorkingDir(path) {
7
+ return path.replace(/\/+$/, '') || '/';
8
+ }
9
+ /**
10
+ * Candidate renderings of `path`:
11
+ * full path → ancestors abbreviated to their first letter one at a time,
12
+ * left to right → leaf only → leaf end-truncated down to `minLeaf` chars.
13
+ */
14
+ export function workingDirCandidates(path, options = {}) {
15
+ const { minLeaf = 18 } = options;
16
+ const trimmed = path.replace(/\/+$/, '');
17
+ const absolute = trimmed.startsWith('/');
18
+ const segs = trimmed.split('/').filter(Boolean);
19
+ if (segs.length === 0)
20
+ return [trimmed || '/'];
21
+ const leaf = segs[segs.length - 1];
22
+ const parents = segs.slice(0, -1);
23
+ const out = [];
24
+ const push = (c) => {
25
+ if (out[out.length - 1] !== c)
26
+ out.push(c);
27
+ };
28
+ for (let n = 0; n <= parents.length; n++) {
29
+ const shown = parents.map((p, i) => (i < n ? [...p][0] : p));
30
+ const joined = [...shown, leaf].join('/');
31
+ push(absolute ? `/${joined}` : joined);
32
+ }
33
+ push(leaf);
34
+ for (let keep = [...leaf].length - 1; keep >= minLeaf; keep--) {
35
+ push(truncate(leaf, { max: keep + 1, mode: 'end' }));
36
+ }
37
+ return out;
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.35.0",
3
+ "version": "0.36.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",
@@ -57,20 +57,28 @@
57
57
  "format": "biome check --write .",
58
58
  "package": "svelte-kit sync && svelte-package && publint",
59
59
  "package:watch": "svelte-kit sync && svelte-package --watch",
60
- "prepublishOnly": "npm run package"
60
+ "prepublishOnly": "npm run package",
61
+ "design:bundle": "node .design-sync/build-bundle.mjs",
62
+ "design:verify": "node .design-sync/verify-bundle.mjs"
61
63
  },
62
64
  "peerDependencies": {
63
65
  "svelte": "^5.0.0"
64
66
  },
65
67
  "devDependencies": {
66
68
  "@biomejs/biome": "^2.2.0",
69
+ "@dorsk/kakehashi": "^0.1.1",
67
70
  "@sveltejs/adapter-static": "^3.0.8",
68
71
  "@sveltejs/kit": "^2.22.0",
69
72
  "@sveltejs/package": "^2.3.0",
70
73
  "@sveltejs/vite-plugin-svelte": "^6.0.0",
71
74
  "@types/node": "^25.9.3",
75
+ "esbuild": "^0.28.2",
76
+ "esbuild-svelte": "^0.9.5",
77
+ "jsdom": "^30.0.1",
72
78
  "lefthook": "^1.7.0",
73
79
  "publint": "^0.2.0",
80
+ "react": "^19.2.8",
81
+ "react-dom": "^19.2.8",
74
82
  "svelte": "^5.0.0",
75
83
  "svelte-check": "^4.0.0",
76
84
  "typescript": "^5.0.0",