@dorsk/tsumikit 0.1.1 → 0.2.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 +4 -2
- package/dist/clipboard.d.ts +1 -0
- package/dist/clipboard.js +30 -0
- package/dist/components/atoms/Badge.svelte +65 -3
- package/dist/components/atoms/Badge.svelte.d.ts +4 -0
- package/dist/components/atoms/Button.svelte +80 -8
- package/dist/components/atoms/Button.svelte.d.ts +4 -0
- package/dist/components/atoms/Checkbox.svelte +7 -1
- package/dist/components/atoms/Checkbox.svelte.d.ts +2 -0
- package/dist/components/atoms/Dot.svelte +67 -0
- package/dist/components/atoms/Dot.svelte.d.ts +12 -0
- package/dist/components/atoms/Input.svelte +28 -2
- package/dist/components/atoms/Input.svelte.d.ts +5 -1
- package/dist/components/atoms/Select.svelte +9 -2
- package/dist/components/atoms/Select.svelte.d.ts +2 -0
- package/dist/components/atoms/Slider.svelte +5 -4
- package/dist/components/atoms/Switch.svelte +6 -1
- package/dist/components/atoms/Switch.svelte.d.ts +1 -0
- package/dist/components/atoms/Textarea.svelte +26 -1
- package/dist/components/atoms/Textarea.svelte.d.ts +4 -0
- package/dist/components/layouts/AppShell.svelte +15 -8
- package/dist/components/molecules/Accordion.svelte +6 -3
- package/dist/components/molecules/CopyButton.svelte +2 -26
- package/dist/components/molecules/FileButton.svelte +45 -3
- package/dist/components/molecules/IconButton.svelte +23 -3
- package/dist/components/molecules/IconButton.svelte.d.ts +2 -0
- package/dist/components/molecules/Modal.svelte +15 -4
- package/dist/components/molecules/Modal.svelte.d.ts +3 -0
- package/dist/components/molecules/OptionButton.svelte +28 -30
- package/dist/components/molecules/Popover.svelte +46 -25
- package/dist/components/molecules/Popover.svelte.d.ts +7 -2
- package/dist/components/molecules/SelectButton.svelte +20 -16
- package/dist/components/molecules/Tabs.svelte +26 -7
- package/dist/components/molecules/Tabs.svelte.d.ts +2 -0
- package/dist/components/molecules/Toggle.svelte +30 -15
- package/dist/components/molecules/Tooltip.svelte +41 -28
- package/dist/components/molecules/Tooltip.svelte.d.ts +1 -1
- package/dist/components/organisms/DataTable.svelte +85 -4
- package/dist/components/organisms/DataTable.svelte.d.ts +6 -0
- package/dist/floating.d.ts +10 -0
- package/dist/floating.js +56 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/styles/app.css +14 -234
- package/dist/styles/variables.css +1 -1
- package/package.json +1 -1
- package/dist/components/atoms/Chip.svelte +0 -53
- package/dist/components/atoms/Chip.svelte.d.ts +0 -11
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
id: string;
|
|
4
4
|
label: string;
|
|
5
5
|
icon?: import('../atoms/Icon.svelte').IconName;
|
|
6
|
+
/** Greyed out, not selectable, skipped by keyboard navigation. */
|
|
7
|
+
disabled?: boolean;
|
|
6
8
|
}
|
|
7
9
|
</script>
|
|
8
10
|
|
|
@@ -27,28 +29,40 @@
|
|
|
27
29
|
panel: Snippet<[string]>;
|
|
28
30
|
} = $props();
|
|
29
31
|
|
|
30
|
-
// Default to the first tab when no value is supplied.
|
|
32
|
+
// Default to the first selectable tab when no value is supplied.
|
|
31
33
|
$effect(() => {
|
|
32
|
-
if (value === undefined
|
|
34
|
+
if (value === undefined) {
|
|
35
|
+
const first = tabs.find((t) => !t.disabled);
|
|
36
|
+
if (first) value = first.id;
|
|
37
|
+
}
|
|
33
38
|
});
|
|
34
39
|
|
|
35
40
|
let listEl = $state<HTMLDivElement | null>(null);
|
|
36
41
|
const baseId = `tabs-${Math.random().toString(36).slice(2, 8)}`;
|
|
37
42
|
|
|
38
43
|
function select(id: string, focus = false) {
|
|
44
|
+
if (tabs.find((t) => t.id === id)?.disabled) return;
|
|
39
45
|
value = id;
|
|
40
46
|
if (focus) {
|
|
41
47
|
queueMicrotask(() => listEl?.querySelector<HTMLButtonElement>(`#${baseId}-tab-${id}`)?.focus());
|
|
42
48
|
}
|
|
43
49
|
}
|
|
50
|
+
// Step to the next non-disabled tab in a direction, wrapping around.
|
|
51
|
+
function step(from: number, dir: 1 | -1): number {
|
|
52
|
+
for (let n = 1; n <= tabs.length; n++) {
|
|
53
|
+
const j = (from + dir * n + tabs.length * n) % tabs.length;
|
|
54
|
+
if (!tabs[j].disabled) return j;
|
|
55
|
+
}
|
|
56
|
+
return from;
|
|
57
|
+
}
|
|
44
58
|
function onkeydown(e: KeyboardEvent) {
|
|
45
59
|
const i = tabs.findIndex((t) => t.id === value);
|
|
46
60
|
if (i < 0) return;
|
|
47
61
|
let next = i;
|
|
48
|
-
if (e.key === 'ArrowRight') next = (i
|
|
49
|
-
else if (e.key === 'ArrowLeft') next = (i -
|
|
50
|
-
else if (e.key === 'Home') next =
|
|
51
|
-
else if (e.key === 'End') next = tabs.length - 1;
|
|
62
|
+
if (e.key === 'ArrowRight') next = step(i, 1);
|
|
63
|
+
else if (e.key === 'ArrowLeft') next = step(i, -1);
|
|
64
|
+
else if (e.key === 'Home') next = tabs.findIndex((t) => !t.disabled);
|
|
65
|
+
else if (e.key === 'End') next = tabs.length - 1 - [...tabs].reverse().findIndex((t) => !t.disabled);
|
|
52
66
|
else return;
|
|
53
67
|
e.preventDefault();
|
|
54
68
|
select(tabs[next].id, true);
|
|
@@ -64,6 +78,7 @@
|
|
|
64
78
|
id="{baseId}-tab-{t.id}"
|
|
65
79
|
aria-selected={value === t.id}
|
|
66
80
|
aria-controls="{baseId}-panel"
|
|
81
|
+
disabled={t.disabled}
|
|
67
82
|
tabindex={value === t.id ? 0 : -1}
|
|
68
83
|
class="tab"
|
|
69
84
|
class:selected={value === t.id}
|
|
@@ -101,9 +116,13 @@
|
|
|
101
116
|
color 0.12s var(--ease),
|
|
102
117
|
border-color 0.12s var(--ease);
|
|
103
118
|
}
|
|
104
|
-
.tab:hover {
|
|
119
|
+
.tab:hover:not(:disabled) {
|
|
105
120
|
color: var(--text);
|
|
106
121
|
}
|
|
122
|
+
.tab:disabled {
|
|
123
|
+
opacity: 0.45;
|
|
124
|
+
cursor: not-allowed;
|
|
125
|
+
}
|
|
107
126
|
.tab.selected {
|
|
108
127
|
color: var(--accent);
|
|
109
128
|
border-bottom-color: var(--accent);
|
|
@@ -2,6 +2,8 @@ export interface TabItem {
|
|
|
2
2
|
id: string;
|
|
3
3
|
label: string;
|
|
4
4
|
icon?: import('../atoms/Icon.svelte').IconName;
|
|
5
|
+
/** Greyed out, not selectable, skipped by keyboard navigation. */
|
|
6
|
+
disabled?: boolean;
|
|
5
7
|
}
|
|
6
8
|
import type { Snippet } from 'svelte';
|
|
7
9
|
type $$ComponentProps = {
|
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
// message-type filters) without restyling the base. Used across the
|
|
6
6
|
// conversation toolbar (filters / formatting / behavior / mobile tabs).
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// `.btn.toggle` outranks the atom's :where()-scoped variant classes.
|
|
8
|
+
// A chip shares almost nothing visually with Button, so it owns its own
|
|
9
|
+
// <button> + scoped styles rather than specializing the Button atom.
|
|
11
10
|
import type { Snippet } from 'svelte';
|
|
12
11
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
13
|
-
import Button from '../atoms/Button.svelte';
|
|
14
12
|
|
|
15
13
|
let {
|
|
16
14
|
pressed = false,
|
|
17
15
|
pill = false,
|
|
18
16
|
struck = false,
|
|
17
|
+
type = 'button',
|
|
18
|
+
disabled = false,
|
|
19
19
|
class: klass = '',
|
|
20
20
|
children,
|
|
21
21
|
...rest
|
|
@@ -27,19 +27,25 @@
|
|
|
27
27
|
} = $props();
|
|
28
28
|
</script>
|
|
29
29
|
|
|
30
|
-
<
|
|
30
|
+
<button
|
|
31
31
|
{...rest}
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
{type}
|
|
33
|
+
{disabled}
|
|
34
|
+
class="toggle {klass}"
|
|
35
|
+
class:pill
|
|
36
|
+
class:struck
|
|
37
|
+
class:on={pressed}
|
|
34
38
|
aria-pressed={pressed}
|
|
35
39
|
>
|
|
36
40
|
{@render children?.()}
|
|
37
|
-
</
|
|
41
|
+
</button>
|
|
38
42
|
|
|
39
43
|
<style>
|
|
40
|
-
|
|
44
|
+
.toggle {
|
|
45
|
+
display: inline-flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
41
48
|
gap: 4px;
|
|
42
|
-
min-height: 0;
|
|
43
49
|
padding: 0.15rem var(--sp-2);
|
|
44
50
|
border-radius: var(--r-sm);
|
|
45
51
|
font-size: var(--fs-xs);
|
|
@@ -48,21 +54,30 @@
|
|
|
48
54
|
background: var(--bg-elevated-2);
|
|
49
55
|
color: var(--text-muted);
|
|
50
56
|
border: 1px solid var(--border);
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
user-select: none;
|
|
59
|
+
transition:
|
|
60
|
+
background 0.12s var(--ease),
|
|
61
|
+
border-color 0.12s var(--ease),
|
|
62
|
+
color 0.12s var(--ease);
|
|
51
63
|
}
|
|
52
|
-
|
|
64
|
+
.toggle:disabled {
|
|
65
|
+
opacity: 0.45;
|
|
66
|
+
cursor: not-allowed;
|
|
67
|
+
}
|
|
68
|
+
.toggle.pill {
|
|
53
69
|
border-radius: var(--r-pill);
|
|
54
70
|
}
|
|
55
|
-
|
|
71
|
+
.toggle:hover:not(:disabled) {
|
|
56
72
|
border-color: var(--border-strong);
|
|
57
|
-
background: var(--bg-elevated-2);
|
|
58
73
|
}
|
|
59
|
-
|
|
74
|
+
.toggle.on {
|
|
60
75
|
--tc: var(--toggle-accent, var(--accent));
|
|
61
76
|
color: var(--tc);
|
|
62
77
|
border-color: color-mix(in srgb, var(--tc) 55%, transparent);
|
|
63
78
|
background: color-mix(in srgb, var(--tc) 16%, transparent);
|
|
64
79
|
}
|
|
65
|
-
|
|
80
|
+
.toggle.struck {
|
|
66
81
|
text-decoration: line-through;
|
|
67
82
|
}
|
|
68
83
|
</style>
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
// Lightweight tooltip. Shows on hover (after a short delay) and on keyboard
|
|
3
3
|
// focus of the trigger; hides on blur / mouseleave / Escape. The trigger is
|
|
4
4
|
// any snippet; an action wires `aria-describedby` onto its first focusable
|
|
5
|
-
// element so screen readers announce the text.
|
|
6
|
-
//
|
|
5
|
+
// element so screen readers announce the text.
|
|
6
|
+
//
|
|
7
|
+
// The bubble renders in the browser top layer (Popover API, `manual` so it
|
|
8
|
+
// only opens/closes under our control) and is positioned by the shared
|
|
9
|
+
// `place()` helper — so, like Popover, it escapes ancestor overflow/transform
|
|
10
|
+
// clipping and flips/clamps to stay in the viewport. Dependency-free.
|
|
7
11
|
import type { Snippet } from 'svelte';
|
|
12
|
+
import { place } from '../../floating';
|
|
8
13
|
|
|
9
14
|
let {
|
|
10
15
|
text,
|
|
@@ -13,22 +18,35 @@
|
|
|
13
18
|
trigger
|
|
14
19
|
}: {
|
|
15
20
|
text: string;
|
|
16
|
-
placement?: 'top' | 'bottom';
|
|
21
|
+
placement?: 'top' | 'bottom' | 'left' | 'right';
|
|
17
22
|
delay?: number;
|
|
18
23
|
trigger: Snippet;
|
|
19
24
|
} = $props();
|
|
20
25
|
|
|
21
26
|
const id = `tip-${Math.random().toString(36).slice(2, 8)}`;
|
|
22
|
-
let
|
|
27
|
+
let wrapEl = $state<HTMLElement | null>(null);
|
|
28
|
+
let tipEl = $state<HTMLElement | null>(null);
|
|
23
29
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
24
30
|
|
|
31
|
+
function reposition() {
|
|
32
|
+
if (wrapEl && tipEl) place(wrapEl, tipEl, `${placement}-center`, 6);
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
function show() {
|
|
26
36
|
clearTimeout(timer);
|
|
27
|
-
timer = setTimeout(() =>
|
|
37
|
+
timer = setTimeout(() => {
|
|
38
|
+
if (!tipEl || tipEl.matches(':popover-open')) return; // re-entry guard
|
|
39
|
+
tipEl.showPopover(); // top layer — displayed before we measure it
|
|
40
|
+
reposition();
|
|
41
|
+
addEventListener('scroll', reposition, true);
|
|
42
|
+
addEventListener('resize', reposition);
|
|
43
|
+
}, delay);
|
|
28
44
|
}
|
|
29
45
|
function hide() {
|
|
30
46
|
clearTimeout(timer);
|
|
31
|
-
open
|
|
47
|
+
if (tipEl?.matches(':popover-open')) tipEl.hidePopover();
|
|
48
|
+
removeEventListener('scroll', reposition, true);
|
|
49
|
+
removeEventListener('resize', reposition);
|
|
32
50
|
}
|
|
33
51
|
|
|
34
52
|
const FOCUSABLE = 'a[href],button,input,select,textarea,[tabindex]';
|
|
@@ -59,23 +77,22 @@
|
|
|
59
77
|
}
|
|
60
78
|
</script>
|
|
61
79
|
|
|
62
|
-
<span class="tip-wrap" use:tooltip>
|
|
80
|
+
<span class="tip-wrap" bind:this={wrapEl} use:tooltip>
|
|
63
81
|
{@render trigger()}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
</span>
|
|
83
|
+
|
|
84
|
+
<span bind:this={tipEl} {id} role="tooltip" popover="manual" class="tip">
|
|
85
|
+
{text}
|
|
67
86
|
</span>
|
|
68
87
|
|
|
69
88
|
<style>
|
|
70
89
|
.tip-wrap {
|
|
71
|
-
position: relative;
|
|
72
90
|
display: inline-flex;
|
|
73
91
|
}
|
|
74
92
|
.tip {
|
|
75
|
-
position:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
z-index: var(--z-toast);
|
|
93
|
+
position: fixed;
|
|
94
|
+
margin: 0;
|
|
95
|
+
inset: auto; /* JS sets top/left; the popover lives in the top layer */
|
|
79
96
|
max-width: 16rem;
|
|
80
97
|
width: max-content;
|
|
81
98
|
padding: var(--sp-1) var(--sp-2);
|
|
@@ -87,20 +104,16 @@
|
|
|
87
104
|
font-size: var(--fs-xs);
|
|
88
105
|
line-height: 1.4;
|
|
89
106
|
white-space: normal;
|
|
90
|
-
pointer-events: none;
|
|
91
|
-
opacity: 0;
|
|
92
|
-
transition:
|
|
93
|
-
opacity 0.12s var(--ease),
|
|
94
|
-
transform 0.12s var(--ease);
|
|
95
|
-
}
|
|
96
|
-
.tip-top {
|
|
97
|
-
bottom: calc(100% + 6px);
|
|
107
|
+
pointer-events: none; /* non-interactive: never steals hover/clicks */
|
|
98
108
|
}
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
/* Fade/scale in when shown (skipped under reduced-motion via the global rule). */
|
|
110
|
+
.tip:popover-open {
|
|
111
|
+
animation: tip-in 0.12s var(--ease);
|
|
101
112
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
@keyframes tip-in {
|
|
114
|
+
from {
|
|
115
|
+
opacity: 0;
|
|
116
|
+
transform: scale(0.96);
|
|
117
|
+
}
|
|
105
118
|
}
|
|
106
119
|
</style>
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
align?: 'left' | 'center' | 'right';
|
|
8
8
|
/** Pull a display value from the row (defaults to row[key]). */
|
|
9
9
|
get?: (row: T) => unknown;
|
|
10
|
+
/** Header becomes a sort toggle. Sorts by the displayed value unless an
|
|
11
|
+
* `onsort` handler is supplied (then the caller controls ordering). */
|
|
12
|
+
sortable?: boolean;
|
|
10
13
|
}
|
|
11
14
|
</script>
|
|
12
15
|
|
|
@@ -24,6 +27,7 @@
|
|
|
24
27
|
rows,
|
|
25
28
|
rowKey,
|
|
26
29
|
onrowclick,
|
|
30
|
+
onsort,
|
|
27
31
|
cellSnippets = {},
|
|
28
32
|
empty = 'No data.',
|
|
29
33
|
stickyHeader = false
|
|
@@ -33,15 +37,47 @@
|
|
|
33
37
|
/** Stable key for each row (for keyed iteration). */
|
|
34
38
|
rowKey: (row: T) => string | number;
|
|
35
39
|
onrowclick?: (row: T) => void;
|
|
40
|
+
/** Supply to take over ordering (server-side / custom sort). When set, the
|
|
41
|
+
* table only reflects the indicator and emits; it does not reorder rows. */
|
|
42
|
+
onsort?: (key: string, dir: 'asc' | 'desc') => void;
|
|
36
43
|
cellSnippets?: Record<string, Snippet<[T]>>;
|
|
37
44
|
empty?: string;
|
|
38
45
|
stickyHeader?: boolean;
|
|
39
46
|
} = $props();
|
|
40
47
|
|
|
48
|
+
let sortKey = $state<string | null>(null);
|
|
49
|
+
let sortDir = $state<'asc' | 'desc'>('asc');
|
|
50
|
+
|
|
41
51
|
function display(col: Column<T>, row: T): unknown {
|
|
42
52
|
if (col.get) return col.get(row);
|
|
43
53
|
return (row as Record<string, unknown>)[col.key];
|
|
44
54
|
}
|
|
55
|
+
|
|
56
|
+
function toggleSort(col: Column<T>) {
|
|
57
|
+
if (!col.sortable) return;
|
|
58
|
+
if (sortKey === col.key) sortDir = sortDir === 'asc' ? 'desc' : 'asc';
|
|
59
|
+
else {
|
|
60
|
+
sortKey = col.key;
|
|
61
|
+
sortDir = 'asc';
|
|
62
|
+
}
|
|
63
|
+
onsort?.(col.key, sortDir);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Internal sort (skipped when the caller owns ordering via `onsort`).
|
|
67
|
+
const sortedRows = $derived.by(() => {
|
|
68
|
+
if (onsort || !sortKey) return rows;
|
|
69
|
+
const col = columns.find((c) => c.key === sortKey);
|
|
70
|
+
if (!col) return rows;
|
|
71
|
+
const dir = sortDir === 'asc' ? 1 : -1;
|
|
72
|
+
return [...rows].sort((a, b) => {
|
|
73
|
+
const av = display(col, a);
|
|
74
|
+
const bv = display(col, b);
|
|
75
|
+
if (av == null) return bv == null ? 0 : 1;
|
|
76
|
+
if (bv == null) return -1;
|
|
77
|
+
if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * dir;
|
|
78
|
+
return String(av).localeCompare(String(bv)) * dir;
|
|
79
|
+
});
|
|
80
|
+
});
|
|
45
81
|
</script>
|
|
46
82
|
|
|
47
83
|
<div class="dt-scroll">
|
|
@@ -49,19 +85,39 @@
|
|
|
49
85
|
<thead>
|
|
50
86
|
<tr>
|
|
51
87
|
{#each columns as col (col.key)}
|
|
52
|
-
<th
|
|
53
|
-
|
|
88
|
+
<th
|
|
89
|
+
scope="col"
|
|
90
|
+
style:width={col.width}
|
|
91
|
+
style:text-align={col.align ?? 'left'}
|
|
92
|
+
aria-sort={col.sortable
|
|
93
|
+
? sortKey === col.key
|
|
94
|
+
? sortDir === 'asc'
|
|
95
|
+
? 'ascending'
|
|
96
|
+
: 'descending'
|
|
97
|
+
: 'none'
|
|
98
|
+
: undefined}
|
|
99
|
+
>
|
|
100
|
+
{#if col.sortable}
|
|
101
|
+
<button type="button" class="dt-sort" onclick={() => toggleSort(col)}>
|
|
102
|
+
<span>{col.label}</span>
|
|
103
|
+
<span class="dt-arrow" class:active={sortKey === col.key}>
|
|
104
|
+
{sortKey === col.key ? (sortDir === 'asc' ? '↑' : '↓') : '↕'}
|
|
105
|
+
</span>
|
|
106
|
+
</button>
|
|
107
|
+
{:else}
|
|
108
|
+
{col.label}
|
|
109
|
+
{/if}
|
|
54
110
|
</th>
|
|
55
111
|
{/each}
|
|
56
112
|
</tr>
|
|
57
113
|
</thead>
|
|
58
114
|
<tbody>
|
|
59
|
-
{#if
|
|
115
|
+
{#if sortedRows.length === 0}
|
|
60
116
|
<tr>
|
|
61
117
|
<td class="dt-empty" colspan={columns.length}>{empty}</td>
|
|
62
118
|
</tr>
|
|
63
119
|
{:else}
|
|
64
|
-
{#each
|
|
120
|
+
{#each sortedRows as row (rowKey(row))}
|
|
65
121
|
<tr
|
|
66
122
|
class:clickable={!!onrowclick}
|
|
67
123
|
tabindex={onrowclick ? 0 : undefined}
|
|
@@ -125,6 +181,31 @@
|
|
|
125
181
|
top: 0;
|
|
126
182
|
z-index: 1;
|
|
127
183
|
}
|
|
184
|
+
/* Sort toggle: a bare button that inherits the th's type styling. */
|
|
185
|
+
.dt-sort {
|
|
186
|
+
display: inline-flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
gap: var(--sp-1);
|
|
189
|
+
border: 0;
|
|
190
|
+
background: none;
|
|
191
|
+
padding: 0;
|
|
192
|
+
font: inherit;
|
|
193
|
+
letter-spacing: inherit;
|
|
194
|
+
text-transform: inherit;
|
|
195
|
+
color: inherit;
|
|
196
|
+
cursor: pointer;
|
|
197
|
+
}
|
|
198
|
+
.dt-sort:hover {
|
|
199
|
+
color: var(--text);
|
|
200
|
+
}
|
|
201
|
+
.dt-arrow {
|
|
202
|
+
opacity: 0.4;
|
|
203
|
+
font-size: 0.9em;
|
|
204
|
+
}
|
|
205
|
+
.dt-arrow.active {
|
|
206
|
+
opacity: 1;
|
|
207
|
+
color: var(--accent);
|
|
208
|
+
}
|
|
128
209
|
tbody tr:last-child td {
|
|
129
210
|
border-bottom: none;
|
|
130
211
|
}
|
|
@@ -6,6 +6,9 @@ export interface Column<T> {
|
|
|
6
6
|
align?: 'left' | 'center' | 'right';
|
|
7
7
|
/** Pull a display value from the row (defaults to row[key]). */
|
|
8
8
|
get?: (row: T) => unknown;
|
|
9
|
+
/** Header becomes a sort toggle. Sorts by the displayed value unless an
|
|
10
|
+
* `onsort` handler is supplied (then the caller controls ordering). */
|
|
11
|
+
sortable?: boolean;
|
|
9
12
|
}
|
|
10
13
|
import type { Snippet } from 'svelte';
|
|
11
14
|
declare function $$render<T>(): {
|
|
@@ -15,6 +18,9 @@ declare function $$render<T>(): {
|
|
|
15
18
|
/** Stable key for each row (for keyed iteration). */
|
|
16
19
|
rowKey: (row: T) => string | number;
|
|
17
20
|
onrowclick?: (row: T) => void;
|
|
21
|
+
/** Supply to take over ordering (server-side / custom sort). When set, the
|
|
22
|
+
* table only reflects the indicator and emits; it does not reorder rows. */
|
|
23
|
+
onsort?: (key: string, dir: "asc" | "desc") => void;
|
|
18
24
|
cellSnippets?: Record<string, Snippet<[T]>>;
|
|
19
25
|
empty?: string;
|
|
20
26
|
stickyHeader?: boolean;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Side = 'top' | 'bottom' | 'left' | 'right';
|
|
2
|
+
export type Align = 'start' | 'center' | 'end';
|
|
3
|
+
/** `${side}-${align}`, e.g. `bottom-start` (Popover) or `left-center` (Tooltip). */
|
|
4
|
+
export type Placement = `${Side}-${Align}`;
|
|
5
|
+
/**
|
|
6
|
+
* Position `floating` next to `trigger` and write the result to its inline
|
|
7
|
+
* `top`/`left` (expects `floating` to be `position: fixed`). Call on open and on
|
|
8
|
+
* scroll/resize while open.
|
|
9
|
+
*/
|
|
10
|
+
export declare function place(trigger: HTMLElement, floating: HTMLElement, placement?: Placement, gap?: number): void;
|
package/dist/floating.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Shared placement for top-layer floating elements (Popover panel, Tooltip).
|
|
2
|
+
// Both render in the browser top layer (via the Popover API), so they escape
|
|
3
|
+
// ancestor `overflow`/`transform`/`contain` clipping; this just positions them
|
|
4
|
+
// against their trigger as `position: fixed` coordinates, flipping to the other
|
|
5
|
+
// side and clamping into the viewport so they never run off-screen.
|
|
6
|
+
const clamp = (v, lo, hi) => Math.max(lo, Math.min(v, hi));
|
|
7
|
+
// Position along the cross axis (start/center/end of the trigger's extent).
|
|
8
|
+
function alignPos(align, start, size, fSize) {
|
|
9
|
+
if (align === 'start')
|
|
10
|
+
return start;
|
|
11
|
+
if (align === 'end')
|
|
12
|
+
return start + size - fSize;
|
|
13
|
+
return start + size / 2 - fSize / 2;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Position `floating` next to `trigger` and write the result to its inline
|
|
17
|
+
* `top`/`left` (expects `floating` to be `position: fixed`). Call on open and on
|
|
18
|
+
* scroll/resize while open.
|
|
19
|
+
*/
|
|
20
|
+
export function place(trigger, floating, placement = 'bottom-start', gap = 6) {
|
|
21
|
+
const t = trigger.getBoundingClientRect();
|
|
22
|
+
const f = floating.getBoundingClientRect();
|
|
23
|
+
const vw = document.documentElement.clientWidth;
|
|
24
|
+
const vh = document.documentElement.clientHeight;
|
|
25
|
+
const [side, align] = placement.split('-');
|
|
26
|
+
let top;
|
|
27
|
+
let left;
|
|
28
|
+
if (side === 'top' || side === 'bottom') {
|
|
29
|
+
// Vertical side; align along x. Preferred side, flip if it overflows and the
|
|
30
|
+
// opposite side has room.
|
|
31
|
+
top = side === 'bottom' ? t.bottom + gap : t.top - f.height - gap;
|
|
32
|
+
if (side === 'bottom' && top + f.height > vh && t.top - f.height - gap >= 0) {
|
|
33
|
+
top = t.top - f.height - gap;
|
|
34
|
+
}
|
|
35
|
+
else if (side === 'top' && top < 0 && t.bottom + f.height + gap <= vh) {
|
|
36
|
+
top = t.bottom + gap;
|
|
37
|
+
}
|
|
38
|
+
left = alignPos(align, t.left, t.width, f.width);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Horizontal side; align along y.
|
|
42
|
+
left = side === 'right' ? t.right + gap : t.left - f.width - gap;
|
|
43
|
+
if (side === 'right' && left + f.width > vw && t.left - f.width - gap >= 0) {
|
|
44
|
+
left = t.left - f.width - gap;
|
|
45
|
+
}
|
|
46
|
+
else if (side === 'left' && left < 0 && t.right + f.width + gap <= vw) {
|
|
47
|
+
left = t.right + gap;
|
|
48
|
+
}
|
|
49
|
+
top = alignPos(align, t.top, t.height, f.height);
|
|
50
|
+
}
|
|
51
|
+
// Clamp both axes so it always stays on screen.
|
|
52
|
+
top = clamp(top, gap, vh - f.height - gap);
|
|
53
|
+
left = clamp(left, gap, vw - f.width - gap);
|
|
54
|
+
floating.style.top = `${Math.round(top)}px`;
|
|
55
|
+
floating.style.left = `${Math.round(left)}px`;
|
|
56
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { autoresize } from './autoresize';
|
|
2
|
+
export { copyToClipboard } from './clipboard';
|
|
2
3
|
export { default as Badge } from './components/atoms/Badge.svelte';
|
|
3
4
|
export { default as Button } from './components/atoms/Button.svelte';
|
|
4
5
|
export { default as Card } from './components/atoms/Card.svelte';
|
|
5
6
|
export { default as Checkbox } from './components/atoms/Checkbox.svelte';
|
|
6
|
-
export { default as
|
|
7
|
+
export { default as Dot } from './components/atoms/Dot.svelte';
|
|
7
8
|
export { default as Heading } from './components/atoms/Heading.svelte';
|
|
8
9
|
export type { IconName } from './components/atoms/Icon.svelte';
|
|
9
10
|
export { default as Icon } from './components/atoms/Icon.svelte';
|
package/dist/index.js
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
// import '@dorsk/tsumikit/styles/app.css';
|
|
6
6
|
// then use the components below.
|
|
7
7
|
export { autoresize } from './autoresize';
|
|
8
|
+
export { copyToClipboard } from './clipboard';
|
|
8
9
|
export { default as Badge } from './components/atoms/Badge.svelte';
|
|
9
10
|
export { default as Button } from './components/atoms/Button.svelte';
|
|
10
11
|
export { default as Card } from './components/atoms/Card.svelte';
|
|
11
12
|
export { default as Checkbox } from './components/atoms/Checkbox.svelte';
|
|
12
|
-
export { default as
|
|
13
|
+
export { default as Dot } from './components/atoms/Dot.svelte';
|
|
13
14
|
export { default as Heading } from './components/atoms/Heading.svelte';
|
|
14
15
|
export { default as Icon } from './components/atoms/Icon.svelte';
|
|
15
16
|
export { default as Input } from './components/atoms/Input.svelte';
|