@dorsk/tsumikit 0.67.0 → 0.68.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,116 @@
1
+ export interface SortableListMove {
2
+ key: string;
3
+ from: number;
4
+ to: number;
5
+ }
6
+ export interface SortableListRowContext {
7
+ index: number;
8
+ grabbed: boolean;
9
+ selected: boolean;
10
+ }
11
+ /** Accessible strings. `pos` is 1-based. */
12
+ export interface SortableListMessages {
13
+ instructions: string;
14
+ reorder: (name: string) => string;
15
+ moveUp: (name: string) => string;
16
+ moveDown: (name: string) => string;
17
+ grabbed: (name: string, pos: number, total: number) => string;
18
+ moved: (name: string, pos: number, total: number) => string;
19
+ dropped: (name: string, pos: number, total: number) => string;
20
+ cancelled: (name: string, pos: number, total: number) => string;
21
+ }
22
+ export declare const SORTABLE_LIST_MESSAGES: SortableListMessages;
23
+ /**
24
+ * Reorderable list. The consumer owns the order: `onreorder` reports the new
25
+ * key list, the list shows it until `items` changes or `revertAfter` ms pass.
26
+ * Drag runs on Pointer Events (touch included); the grip is also a keyboard
27
+ * grab button and the arrows move one step.
28
+ */
29
+ import { type Snippet } from 'svelte';
30
+ import type { HTMLAttributes } from 'svelte/elements';
31
+ declare function $$render<T>(): {
32
+ props: Omit<HTMLAttributes<HTMLDivElement>, keyof {
33
+ items: T[];
34
+ key: (item: T) => string;
35
+ /** Row content; the kit renders the grip, ordinal and arrows around it. */
36
+ row: Snippet<[T, SortableListRowContext]>;
37
+ /** Interactive row extras (go, skip), kept outside the selectable body. */
38
+ trailing?: Snippet<[T, SortableListRowContext]>;
39
+ /** Fires on drop or an arrow press with the full new key order. */
40
+ onreorder?: (keys: string[], move: SortableListMove) => void;
41
+ /** Veto a target index for a key. */
42
+ canMove?: (key: string, to: number) => boolean;
43
+ /** A locked row has no grip and disabled arrows. */
44
+ locked?: (item: T) => boolean;
45
+ /** Accessible row name used in labels and announcements. Defaults to the key. */
46
+ name?: (item: T) => string;
47
+ arrows?: boolean;
48
+ numbered?: boolean;
49
+ selected?: string;
50
+ onselect?: (key: string) => void;
51
+ /** Render the row body as a select button. Defaults to on when `onselect` or `selected` is given. */
52
+ selectable?: boolean;
53
+ /** `row` drags from anywhere on the row; touch then needs a 250 ms long-press. */
54
+ handle?: "grip" | "row";
55
+ size?: "sm" | "md";
56
+ label: string;
57
+ messages?: Partial<SortableListMessages>;
58
+ as?: "ol" | "ul";
59
+ /** Ms the optimistic order is kept when `items` is not updated. */
60
+ revertAfter?: number;
61
+ class?: string;
62
+ style?: string;
63
+ }> & {
64
+ items: T[];
65
+ key: (item: T) => string;
66
+ /** Row content; the kit renders the grip, ordinal and arrows around it. */
67
+ row: Snippet<[T, SortableListRowContext]>;
68
+ /** Interactive row extras (go, skip), kept outside the selectable body. */
69
+ trailing?: Snippet<[T, SortableListRowContext]>;
70
+ /** Fires on drop or an arrow press with the full new key order. */
71
+ onreorder?: (keys: string[], move: SortableListMove) => void;
72
+ /** Veto a target index for a key. */
73
+ canMove?: (key: string, to: number) => boolean;
74
+ /** A locked row has no grip and disabled arrows. */
75
+ locked?: (item: T) => boolean;
76
+ /** Accessible row name used in labels and announcements. Defaults to the key. */
77
+ name?: (item: T) => string;
78
+ arrows?: boolean;
79
+ numbered?: boolean;
80
+ selected?: string;
81
+ onselect?: (key: string) => void;
82
+ /** Render the row body as a select button. Defaults to on when `onselect` or `selected` is given. */
83
+ selectable?: boolean;
84
+ /** `row` drags from anywhere on the row; touch then needs a 250 ms long-press. */
85
+ handle?: "grip" | "row";
86
+ size?: "sm" | "md";
87
+ label: string;
88
+ messages?: Partial<SortableListMessages>;
89
+ as?: "ol" | "ul";
90
+ /** Ms the optimistic order is kept when `items` is not updated. */
91
+ revertAfter?: number;
92
+ class?: string;
93
+ style?: string;
94
+ };
95
+ exports: {};
96
+ bindings: "selected";
97
+ slots: {};
98
+ events: {};
99
+ };
100
+ declare class __sveltets_Render<T> {
101
+ props(): ReturnType<typeof $$render<T>>['props'];
102
+ events(): ReturnType<typeof $$render<T>>['events'];
103
+ slots(): ReturnType<typeof $$render<T>>['slots'];
104
+ bindings(): "selected";
105
+ exports(): {};
106
+ }
107
+ interface $$IsomorphicComponent {
108
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
109
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
110
+ } & ReturnType<__sveltets_Render<T>['exports']>;
111
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
112
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
113
+ }
114
+ declare const SortableList: $$IsomorphicComponent;
115
+ type SortableList<T> = InstanceType<typeof SortableList<T>>;
116
+ export default SortableList;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Move the entry at `from` to `to`, returning a new array.
3
+ *
4
+ * @template T
5
+ * @param {T[]} list
6
+ * @param {number} from
7
+ * @param {number} to
8
+ * @returns {T[]}
9
+ */
10
+ export function moveItem<T>(list: T[], from: number, to: number): T[];
11
+ /**
12
+ * True when both key lists hold the same keys, in any order.
13
+ *
14
+ * @param {string[]} a
15
+ * @param {string[]} b
16
+ */
17
+ export function sameKeys(a: string[], b: string[]): boolean;
18
+ /**
19
+ * The nearest index from `from` in direction `dir` that `allowed` accepts,
20
+ * skipping vetoed slots. `undefined` when nothing in that direction is allowed.
21
+ *
22
+ * @param {number} from
23
+ * @param {number} count
24
+ * @param {1 | -1} dir
25
+ * @param {(to: number) => boolean} allowed
26
+ * @returns {number | undefined}
27
+ */
28
+ export function stepTarget(from: number, count: number, dir: 1 | -1, allowed: (to: number) => boolean): number | undefined;
29
+ /**
30
+ * The farthest index toward `edge` that `allowed` accepts.
31
+ *
32
+ * @param {number} from
33
+ * @param {number} count
34
+ * @param {'start' | 'end'} edge
35
+ * @param {(to: number) => boolean} allowed
36
+ * @returns {number | undefined}
37
+ */
38
+ export function edgeTarget(from: number, count: number, edge: "start" | "end", allowed: (to: number) => boolean): number | undefined;
39
+ /**
40
+ * Resolve a key pressed on a grabbed grip to a list action.
41
+ *
42
+ * @param {string} key
43
+ * @returns {'up' | 'down' | 'start' | 'end' | 'drop' | 'cancel' | undefined}
44
+ */
45
+ export function grabbedAction(key: string): "up" | "down" | "start" | "end" | "drop" | "cancel" | undefined;
46
+ /**
47
+ * Slot a dragged row lands in: the one whose top is nearest the dragged row's
48
+ * top. `heights` are the other rows, top to bottom; `top` is relative to the
49
+ * first slot.
50
+ *
51
+ * @param {number} top
52
+ * @param {number[]} heights
53
+ * @returns {number}
54
+ */
55
+ export function slotForTop(top: number, heights: number[]): number;
56
+ /**
57
+ * Auto-scroll velocity for a pointer at `y` inside a box spanning
58
+ * `top`..`bottom`: negative near the top edge, positive near the bottom,
59
+ * scaled by how deep into the `zone` the pointer is.
60
+ *
61
+ * @param {number} y
62
+ * @param {number} top
63
+ * @param {number} bottom
64
+ * @param {number} [zone]
65
+ * @param {number} [max] px per frame
66
+ * @returns {number}
67
+ */
68
+ export function edgeScroll(y: number, top: number, bottom: number, zone?: number, max?: number): number;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Move the entry at `from` to `to`, returning a new array.
3
+ *
4
+ * @template T
5
+ * @param {T[]} list
6
+ * @param {number} from
7
+ * @param {number} to
8
+ * @returns {T[]}
9
+ */
10
+ export function moveItem(list, from, to) {
11
+ const next = [...list];
12
+ if (from < 0 || from >= next.length) return next;
13
+ const clamped = Math.min(Math.max(to, 0), next.length - 1);
14
+ const [moved] = next.splice(from, 1);
15
+ next.splice(clamped, 0, moved);
16
+ return next;
17
+ }
18
+
19
+ /**
20
+ * True when both key lists hold the same keys, in any order.
21
+ *
22
+ * @param {string[]} a
23
+ * @param {string[]} b
24
+ */
25
+ export function sameKeys(a, b) {
26
+ if (a.length !== b.length) return false;
27
+ const set = new Set(a);
28
+ return b.every((k) => set.has(k));
29
+ }
30
+
31
+ /**
32
+ * The nearest index from `from` in direction `dir` that `allowed` accepts,
33
+ * skipping vetoed slots. `undefined` when nothing in that direction is allowed.
34
+ *
35
+ * @param {number} from
36
+ * @param {number} count
37
+ * @param {1 | -1} dir
38
+ * @param {(to: number) => boolean} allowed
39
+ * @returns {number | undefined}
40
+ */
41
+ export function stepTarget(from, count, dir, allowed) {
42
+ for (let to = from + dir; to >= 0 && to < count; to += dir) {
43
+ if (allowed(to)) return to;
44
+ }
45
+ return undefined;
46
+ }
47
+
48
+ /**
49
+ * The farthest index toward `edge` that `allowed` accepts.
50
+ *
51
+ * @param {number} from
52
+ * @param {number} count
53
+ * @param {'start' | 'end'} edge
54
+ * @param {(to: number) => boolean} allowed
55
+ * @returns {number | undefined}
56
+ */
57
+ export function edgeTarget(from, count, edge, allowed) {
58
+ if (edge === 'start') {
59
+ for (let to = 0; to < from; to += 1) if (allowed(to)) return to;
60
+ } else {
61
+ for (let to = count - 1; to > from; to -= 1) if (allowed(to)) return to;
62
+ }
63
+ return undefined;
64
+ }
65
+
66
+ /**
67
+ * Resolve a key pressed on a grabbed grip to a list action.
68
+ *
69
+ * @param {string} key
70
+ * @returns {'up' | 'down' | 'start' | 'end' | 'drop' | 'cancel' | undefined}
71
+ */
72
+ export function grabbedAction(key) {
73
+ switch (key) {
74
+ case 'ArrowUp':
75
+ return 'up';
76
+ case 'ArrowDown':
77
+ return 'down';
78
+ case 'Home':
79
+ return 'start';
80
+ case 'End':
81
+ return 'end';
82
+ case ' ':
83
+ case 'Enter':
84
+ return 'drop';
85
+ case 'Escape':
86
+ return 'cancel';
87
+ default:
88
+ return undefined;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Slot a dragged row lands in: the one whose top is nearest the dragged row's
94
+ * top. `heights` are the other rows, top to bottom; `top` is relative to the
95
+ * first slot.
96
+ *
97
+ * @param {number} top
98
+ * @param {number[]} heights
99
+ * @returns {number}
100
+ */
101
+ export function slotForTop(top, heights) {
102
+ let best = 0;
103
+ let bestDist = Math.abs(top);
104
+ let slotTop = 0;
105
+ for (let j = 0; j < heights.length; j += 1) {
106
+ slotTop += heights[j];
107
+ const dist = Math.abs(top - slotTop);
108
+ if (dist < bestDist) {
109
+ best = j + 1;
110
+ bestDist = dist;
111
+ }
112
+ }
113
+ return best;
114
+ }
115
+
116
+ /**
117
+ * Auto-scroll velocity for a pointer at `y` inside a box spanning
118
+ * `top`..`bottom`: negative near the top edge, positive near the bottom,
119
+ * scaled by how deep into the `zone` the pointer is.
120
+ *
121
+ * @param {number} y
122
+ * @param {number} top
123
+ * @param {number} bottom
124
+ * @param {number} [zone]
125
+ * @param {number} [max] px per frame
126
+ * @returns {number}
127
+ */
128
+ export function edgeScroll(y, top, bottom, zone = 40, max = 12) {
129
+ if (bottom - top <= zone * 2) return 0;
130
+ if (y < top + zone) return -Math.ceil(max * Math.min(1, (top + zone - y) / zone));
131
+ if (y > bottom - zone) return Math.ceil(max * Math.min(1, (y - (bottom - zone)) / zone));
132
+ return 0;
133
+ }
@@ -12,3 +12,41 @@ export type AnchorRect = Pick<DOMRect, 'top' | 'left' | 'bottom' | 'right' | 'wi
12
12
  export declare function place(trigger: HTMLElement, floating: HTMLElement, placement?: Placement, gap?: number): void;
13
13
  /** `place` against a viewport rect instead of an element. */
14
14
  export declare function placeAt(t: AnchorRect, floating: HTMLElement, placement?: Placement, gap?: number): void;
15
+ /** Viewport edges (px) that a floating layout must stay inside. */
16
+ export interface Bounds {
17
+ left: number;
18
+ top: number;
19
+ right: number;
20
+ bottom: number;
21
+ }
22
+ export interface Point {
23
+ x: number;
24
+ y: number;
25
+ }
26
+ export interface FanInput {
27
+ /** Centre of the trigger, in viewport px. */
28
+ center: Point;
29
+ count: number;
30
+ /** Degrees, 0 = right, counter-clockwise. */
31
+ from: number;
32
+ to: number;
33
+ /** Preferred distance from `center` to each petal centre. */
34
+ radius: number;
35
+ /** Petal diameter. */
36
+ petal: number;
37
+ /** Smallest radius the fan may shrink to before it is shifted instead. */
38
+ minRadius?: number;
39
+ bounds: Bounds;
40
+ }
41
+ export interface FanLayout {
42
+ radius: number;
43
+ /** Petal centres, in `count` order. */
44
+ points: Point[];
45
+ }
46
+ /** Angles (degrees) of `count` petals spread evenly from `from` to `to`. */
47
+ export declare function fanAngles(count: number, from: number, to: number): number[];
48
+ /**
49
+ * Lay `count` petals on an arc around `center`, shrinking the radius until every
50
+ * petal fits in `bounds`; if even `minRadius` overflows, the whole fan shifts in.
51
+ */
52
+ export declare function fanLayout({ center, count, from, to, radius, petal, minRadius, bounds, }: FanInput): FanLayout;
package/dist/floating.js CHANGED
@@ -57,3 +57,41 @@ export function placeAt(t, floating, placement = 'bottom-start', gap = 6) {
57
57
  floating.style.top = `${Math.round(top)}px`;
58
58
  floating.style.left = `${Math.round(left)}px`;
59
59
  }
60
+ /** Angles (degrees) of `count` petals spread evenly from `from` to `to`. */
61
+ export function fanAngles(count, from, to) {
62
+ if (count <= 1)
63
+ return count === 1 ? [(from + to) / 2] : [];
64
+ return Array.from({ length: count }, (_, i) => from + ((to - from) * i) / (count - 1));
65
+ }
66
+ /**
67
+ * Lay `count` petals on an arc around `center`, shrinking the radius until every
68
+ * petal fits in `bounds`; if even `minRadius` overflows, the whole fan shifts in.
69
+ */
70
+ export function fanLayout({ center, count, from, to, radius, petal, minRadius = 0, bounds, }) {
71
+ const half = petal / 2;
72
+ const lo = { x: bounds.left + half, y: bounds.top + half };
73
+ const hi = { x: bounds.right - half, y: bounds.bottom - half };
74
+ const units = fanAngles(count, from, to).map((deg) => {
75
+ const rad = (deg * Math.PI) / 180;
76
+ return { x: Math.cos(rad), y: -Math.sin(rad) };
77
+ });
78
+ const reach = (c, u, min, max) => u > 1e-9 ? (max - c) / u : u < -1e-9 ? (min - c) / u : Number.POSITIVE_INFINITY;
79
+ let r = radius;
80
+ for (const u of units) {
81
+ r = Math.min(r, reach(center.x, u.x, lo.x, hi.x), reach(center.y, u.y, lo.y, hi.y));
82
+ }
83
+ r = Math.max(r, Math.min(minRadius, radius));
84
+ const raw = units.map((u) => ({ x: center.x + u.x * r, y: center.y + u.y * r }));
85
+ const shift = (vals, min, max) => {
86
+ if (!vals.length)
87
+ return 0;
88
+ const a = Math.min(...vals);
89
+ const b = Math.max(...vals);
90
+ if (b - a > max - min)
91
+ return (min + max) / 2 - (a + b) / 2;
92
+ return a < min ? min - a : b > max ? max - b : 0;
93
+ };
94
+ const dx = shift(raw.map((p) => p.x), lo.x, hi.x);
95
+ const dy = shift(raw.map((p) => p.y), lo.y, hi.y);
96
+ return { radius: r, points: raw.map((p) => ({ x: p.x + dx, y: p.y + dy })) };
97
+ }
package/dist/index.d.ts CHANGED
@@ -79,10 +79,12 @@ export { default as OptionButton } from './components/molecules/OptionButton.sve
79
79
  export { default as Pagination } from './components/molecules/Pagination.svelte';
80
80
  export { default as Popover } from './components/molecules/Popover.svelte';
81
81
  export { default as Prose } from './components/molecules/Prose.svelte';
82
+ export { default as RadialMenu, type RadialArc, } from './components/molecules/RadialMenu.svelte';
82
83
  export { default as RadioGroup, type RadioOption, } from './components/molecules/RadioGroup.svelte';
83
84
  export { default as SectionHeader } from './components/molecules/SectionHeader.svelte';
84
85
  export { default as SegmentedControl, type SegmentOption, } from './components/molecules/SegmentedControl.svelte';
85
86
  export { default as SelectButton } from './components/molecules/SelectButton.svelte';
87
+ export { default as SortableList, SORTABLE_LIST_MESSAGES, type SortableListMessages, type SortableListMove, type SortableListRowContext, } from './components/molecules/SortableList.svelte';
86
88
  export { default as SplitButton } from './components/molecules/SplitButton.svelte';
87
89
  export { default as StatusBar } from './components/molecules/StatusBar.svelte';
88
90
  export { default as Tabs, type TabItem } from './components/molecules/Tabs.svelte';
package/dist/index.js CHANGED
@@ -87,10 +87,12 @@ export { default as OptionButton } from './components/molecules/OptionButton.sve
87
87
  export { default as Pagination } from './components/molecules/Pagination.svelte';
88
88
  export { default as Popover } from './components/molecules/Popover.svelte';
89
89
  export { default as Prose } from './components/molecules/Prose.svelte';
90
+ export { default as RadialMenu, } from './components/molecules/RadialMenu.svelte';
90
91
  export { default as RadioGroup, } from './components/molecules/RadioGroup.svelte';
91
92
  export { default as SectionHeader } from './components/molecules/SectionHeader.svelte';
92
93
  export { default as SegmentedControl, } from './components/molecules/SegmentedControl.svelte';
93
94
  export { default as SelectButton } from './components/molecules/SelectButton.svelte';
95
+ export { default as SortableList, SORTABLE_LIST_MESSAGES, } from './components/molecules/SortableList.svelte';
94
96
  export { default as SplitButton } from './components/molecules/SplitButton.svelte';
95
97
  export { default as StatusBar } from './components/molecules/StatusBar.svelte';
96
98
  export { default as Tabs } from './components/molecules/Tabs.svelte';
@@ -0,0 +1,26 @@
1
+ export declare const SLIDE_THRESHOLD = 12;
2
+ export interface SlideMove {
3
+ x: number;
4
+ y: number;
5
+ dx: number;
6
+ dy: number;
7
+ }
8
+ export interface SlideOptions {
9
+ /** Target index under the pointer, or -1. Not asked within `threshold` of the press. */
10
+ pick: (move: SlideMove) => number;
11
+ /** The armed target changed (-1: none). */
12
+ onarm: (index: number) => void;
13
+ threshold?: number;
14
+ }
15
+ export interface SlideSelect {
16
+ /** Start tracking; captures the pointer on `el` so moves keep arriving over a top-layer panel. */
17
+ down(e: PointerEvent, el?: Element | null): void;
18
+ move(e: PointerEvent): void;
19
+ /** Stop tracking; returns the armed index (or -1). */
20
+ up(): number;
21
+ cancel(): void;
22
+ readonly active: boolean;
23
+ /** True once the pointer travelled past the threshold since `down`. */
24
+ readonly slid: boolean;
25
+ }
26
+ export declare function slideSelect({ pick, onarm, threshold, }: SlideOptions): SlideSelect;
@@ -0,0 +1,53 @@
1
+ // Press-and-slide picking: press a trigger, drag over a target to arm it,
2
+ // release to fire it. The caller owns opening and maps a pointer to a target.
3
+ export const SLIDE_THRESHOLD = 12;
4
+ export function slideSelect({ pick, onarm, threshold = SLIDE_THRESHOLD, }) {
5
+ let origin = null;
6
+ let armed = -1;
7
+ let slid = false;
8
+ const arm = (i) => {
9
+ if (i === armed)
10
+ return;
11
+ armed = i;
12
+ onarm(i);
13
+ };
14
+ const stop = () => {
15
+ const i = armed;
16
+ origin = null;
17
+ arm(-1);
18
+ return i;
19
+ };
20
+ return {
21
+ down(e, el) {
22
+ origin = { x: e.clientX, y: e.clientY };
23
+ slid = false;
24
+ arm(-1);
25
+ try {
26
+ el?.setPointerCapture(e.pointerId);
27
+ }
28
+ catch { }
29
+ },
30
+ move(e) {
31
+ if (!origin)
32
+ return;
33
+ const dx = e.clientX - origin.x;
34
+ const dy = e.clientY - origin.y;
35
+ if (Math.hypot(dx, dy) < threshold) {
36
+ arm(-1);
37
+ return;
38
+ }
39
+ slid = true;
40
+ arm(pick({ x: e.clientX, y: e.clientY, dx, dy }));
41
+ },
42
+ up: () => (origin ? stop() : -1),
43
+ cancel() {
44
+ stop();
45
+ },
46
+ get active() {
47
+ return origin !== null;
48
+ },
49
+ get slid() {
50
+ return slid;
51
+ },
52
+ };
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.67.0",
3
+ "version": "0.68.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",