@dorsk/tsumikit 0.1.0 → 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.
Files changed (48) hide show
  1. package/README.md +4 -2
  2. package/dist/clipboard.d.ts +1 -0
  3. package/dist/clipboard.js +30 -0
  4. package/dist/components/atoms/Badge.svelte +65 -3
  5. package/dist/components/atoms/Badge.svelte.d.ts +4 -0
  6. package/dist/components/atoms/Button.svelte +80 -8
  7. package/dist/components/atoms/Button.svelte.d.ts +4 -0
  8. package/dist/components/atoms/Checkbox.svelte +7 -1
  9. package/dist/components/atoms/Checkbox.svelte.d.ts +2 -0
  10. package/dist/components/atoms/Dot.svelte +67 -0
  11. package/dist/components/atoms/Dot.svelte.d.ts +12 -0
  12. package/dist/components/atoms/Input.svelte +28 -2
  13. package/dist/components/atoms/Input.svelte.d.ts +5 -1
  14. package/dist/components/atoms/Select.svelte +9 -2
  15. package/dist/components/atoms/Select.svelte.d.ts +2 -0
  16. package/dist/components/atoms/Slider.svelte +5 -4
  17. package/dist/components/atoms/Switch.svelte +6 -1
  18. package/dist/components/atoms/Switch.svelte.d.ts +1 -0
  19. package/dist/components/atoms/Textarea.svelte +26 -1
  20. package/dist/components/atoms/Textarea.svelte.d.ts +4 -0
  21. package/dist/components/layouts/AppShell.svelte +15 -8
  22. package/dist/components/molecules/Accordion.svelte +6 -3
  23. package/dist/components/molecules/CopyButton.svelte +2 -26
  24. package/dist/components/molecules/FileButton.svelte +45 -3
  25. package/dist/components/molecules/IconButton.svelte +23 -3
  26. package/dist/components/molecules/IconButton.svelte.d.ts +2 -0
  27. package/dist/components/molecules/Modal.svelte +15 -4
  28. package/dist/components/molecules/Modal.svelte.d.ts +3 -0
  29. package/dist/components/molecules/OptionButton.svelte +30 -20
  30. package/dist/components/molecules/Popover.svelte +46 -25
  31. package/dist/components/molecules/Popover.svelte.d.ts +7 -2
  32. package/dist/components/molecules/SelectButton.svelte +20 -16
  33. package/dist/components/molecules/Tabs.svelte +26 -7
  34. package/dist/components/molecules/Tabs.svelte.d.ts +2 -0
  35. package/dist/components/molecules/Toggle.svelte +30 -15
  36. package/dist/components/molecules/Tooltip.svelte +41 -28
  37. package/dist/components/molecules/Tooltip.svelte.d.ts +1 -1
  38. package/dist/components/organisms/DataTable.svelte +85 -4
  39. package/dist/components/organisms/DataTable.svelte.d.ts +6 -0
  40. package/dist/floating.d.ts +10 -0
  41. package/dist/floating.js +56 -0
  42. package/dist/index.d.ts +2 -1
  43. package/dist/index.js +2 -1
  44. package/dist/styles/app.css +14 -234
  45. package/dist/styles/variables.css +1 -1
  46. package/package.json +4 -3
  47. package/dist/components/atoms/Chip.svelte +0 -53
  48. 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 && tabs.length) value = tabs[0].id;
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 + 1) % tabs.length;
49
- else if (e.key === 'ArrowLeft') next = (i - 1 + tabs.length) % tabs.length;
50
- else if (e.key === 'Home') next = 0;
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
- // Specializes the Button atom (ghost variant) for shared disabled/focus/
9
- // transition behaviour; the chip surface + "on" tint are overrides.
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
- <Button
30
+ <button
31
31
  {...rest}
32
- variant="ghost"
33
- class="toggle {pill ? 'pill' : ''} {struck ? 'struck' : ''} {pressed ? 'on' : ''} {klass}"
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
- </Button>
41
+ </button>
38
42
 
39
43
  <style>
40
- :global(.btn.toggle) {
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
- :global(.btn.toggle.pill) {
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
- :global(.btn.toggle:hover:not(:disabled)) {
71
+ .toggle:hover:not(:disabled) {
56
72
  border-color: var(--border-strong);
57
- background: var(--bg-elevated-2);
58
73
  }
59
- :global(.btn.toggle.on) {
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
- :global(.btn.toggle.struck) {
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. CSS-positioned above the
6
- // trigger (centered). Dependency-free.
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 open = $state(false);
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(() => (open = true), delay);
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 = false;
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
- <span {id} role="tooltip" class="tip tip-{placement}" class:open aria-hidden={!open}>
65
- {text}
66
- </span>
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: absolute;
76
- left: 50%;
77
- transform: translateX(-50%) scale(0.96);
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
- .tip-bottom {
100
- top: calc(100% + 6px);
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
- .tip.open {
103
- opacity: 1;
104
- transform: translateX(-50%) scale(1);
113
+ @keyframes tip-in {
114
+ from {
115
+ opacity: 0;
116
+ transform: scale(0.96);
117
+ }
105
118
  }
106
119
  </style>
@@ -1,7 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
3
  text: string;
4
- placement?: 'top' | 'bottom';
4
+ placement?: 'top' | 'bottom' | 'left' | 'right';
5
5
  delay?: number;
6
6
  trigger: Snippet;
7
7
  };
@@ -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 scope="col" style:width={col.width} style:text-align={col.align ?? 'left'}>
53
- {col.label}
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 rows.length === 0}
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 rows as row (rowKey(row))}
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;
@@ -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 Chip } from './components/atoms/Chip.svelte';
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 Chip } from './components/atoms/Chip.svelte';
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';