@marianmeres/stuic 3.172.0 → 3.173.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/AGENTS.md +2 -2
- package/API.md +56 -2
- package/README.md +14 -2
- package/dist/actions/resizable-width.svelte.d.ts +14 -28
- package/dist/actions/resizable-width.svelte.js +16 -171
- package/dist/attachments/index.d.ts +1 -0
- package/dist/attachments/index.js +1 -0
- package/dist/attachments/resizable.d.ts +113 -0
- package/dist/attachments/resizable.fixture.svelte +53 -0
- package/dist/attachments/resizable.fixture.svelte.d.ts +10 -0
- package/dist/attachments/resizable.js +295 -0
- package/dist/components/RangeSlider/README.md +291 -0
- package/dist/components/RangeSlider/RangeSlider.svelte +763 -0
- package/dist/components/RangeSlider/RangeSlider.svelte.d.ts +130 -0
- package/dist/components/RangeSlider/i18n-sk.d.ts +17 -0
- package/dist/components/RangeSlider/i18n-sk.js +19 -0
- package/dist/components/RangeSlider/i18n.d.ts +33 -0
- package/dist/components/RangeSlider/i18n.js +41 -0
- package/dist/components/RangeSlider/index.css +430 -0
- package/dist/components/RangeSlider/index.d.ts +3 -0
- package/dist/components/RangeSlider/index.js +3 -0
- package/dist/components/Rating/README.md +206 -0
- package/dist/components/Rating/Rating.svelte +355 -0
- package/dist/components/Rating/Rating.svelte.d.ts +82 -0
- package/dist/components/Rating/i18n-sk.d.ts +17 -0
- package/dist/components/Rating/i18n-sk.js +21 -0
- package/dist/components/Rating/i18n.d.ts +34 -0
- package/dist/components/Rating/i18n.js +42 -0
- package/dist/components/Rating/index.css +170 -0
- package/dist/components/Rating/index.d.ts +3 -0
- package/dist/components/Rating/index.js +3 -0
- package/dist/components/SplitPane/README.md +169 -0
- package/dist/components/SplitPane/SplitPane.svelte +202 -0
- package/dist/components/SplitPane/SplitPane.svelte.d.ts +67 -0
- package/dist/components/SplitPane/i18n-sk.d.ts +17 -0
- package/dist/components/SplitPane/i18n-sk.js +18 -0
- package/dist/components/SplitPane/i18n.d.ts +31 -0
- package/dist/components/SplitPane/i18n.js +39 -0
- package/dist/components/SplitPane/index.css +153 -0
- package/dist/components/SplitPane/index.d.ts +3 -0
- package/dist/components/SplitPane/index.js +3 -0
- package/dist/components/WithSidePanel/README.md +19 -16
- package/dist/icons/index.d.ts +2 -0
- package/dist/icons/index.js +3 -0
- package/dist/index.css +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/docs/{maybe-todo.md → _archive/maybe-todo.md} +18 -7
- package/docs/architecture.md +1 -1
- package/docs/domains/actions.md +18 -18
- package/docs/domains/attachments.md +72 -9
- package/docs/domains/components.md +175 -32
- package/package.json +5 -5
package/AGENTS.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
```
|
|
25
25
|
src/lib/
|
|
26
|
-
├── components/ #
|
|
26
|
+
├── components/ # 78 component directories
|
|
27
27
|
├── actions/ # 16 Svelte actions (use: directives)
|
|
28
28
|
├── attachments/ # Svelte attachments ({@attach} — preferred for new DOM helpers)
|
|
29
29
|
├── utils/ # 55 utility modules (48 on the barrel)
|
|
@@ -131,7 +131,7 @@ Global tokens that control cross-component visual properties. Defined in `src/li
|
|
|
131
131
|
|
|
132
132
|
### Domain Docs
|
|
133
133
|
|
|
134
|
-
- [Components](./docs/domains/components.md) —
|
|
134
|
+
- [Components](./docs/domains/components.md) — 78 component directories, Props pattern, snippets
|
|
135
135
|
- [Theming](./docs/domains/theming.md) — CSS tokens, dark mode, themes
|
|
136
136
|
- [CSS presets](./docs/domains/css-presets.md) — ratio-locked frame (letterbox), safe-area, scrollbar
|
|
137
137
|
- [Actions](./docs/domains/actions.md) — 16 Svelte directives
|
package/API.md
CHANGED
|
@@ -114,6 +114,25 @@ Expandable sections with exclusive or multi-open modes.
|
|
|
114
114
|
|
|
115
115
|
Responsive two-column layout with collapsible side panel, resizable width, and mobile-aware slide transitions.
|
|
116
116
|
|
|
117
|
+
#### `SplitPane`
|
|
118
|
+
|
|
119
|
+
Two panes with a draggable, keyboard-operable separator (ARIA window splitter) — `horizontal` (side by side, a width) or `vertical` (stacked, a height); nestable. Built on the `resizable` attachment.
|
|
120
|
+
|
|
121
|
+
| Prop | Type | Default | Description |
|
|
122
|
+
| --------------- | ---------------------------- | -------------- | ------------------------------------------------------------------------- |
|
|
123
|
+
| `start` / `end` | `Snippet` | — | The two panes' content |
|
|
124
|
+
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Resize a width (side by side) or a height (stacked) |
|
|
125
|
+
| `primary` | `"start" \| "end"` | `"start"` | Which pane carries the size; the other one fills the rest |
|
|
126
|
+
| `size` | `number` | `50` | Bindable size of the primary pane in `units`; a stored size wins on mount |
|
|
127
|
+
| `units` | `"%" \| "px"` | `"%"` | Of the container, or px |
|
|
128
|
+
| `min` / `max` | `number` | `0` | Bounds in `units` (`0` = none) |
|
|
129
|
+
| `key` | `string \| number` | — | Persist the size (`storage`: `"session"` default, or `"local"`) |
|
|
130
|
+
| `disabled` | `boolean` | `false` | Inert separator, layout kept |
|
|
131
|
+
| `label` / `t` | `string` / `TranslateFn` | `"Resize"` | Accessible name of the separator (Slovak catalog bundled) |
|
|
132
|
+
| `onResize` | `(info) => void` | — | Every applied size |
|
|
133
|
+
|
|
134
|
+
Imperative `reset()` restores the mount size. Class slots `startClass` / `endClass` / `separatorClass`.
|
|
135
|
+
|
|
117
136
|
#### `Collapsible`
|
|
118
137
|
|
|
119
138
|
Expandable/collapsible content sections.
|
|
@@ -1606,10 +1625,12 @@ Visual feedback (class toggle) on drag-over.
|
|
|
1606
1625
|
|
|
1607
1626
|
### `resizableWidth`
|
|
1608
1627
|
|
|
1609
|
-
Make an element's width draggable.
|
|
1628
|
+
Make an element's width draggable. A thin wrapper over the `resizable` attachment (`axis: "x"`, see [Attachments](#attachments)) — the handle is a keyboard-operable `role="separator"`, `min` / `max` clamp, `key` persists.
|
|
1610
1629
|
|
|
1611
1630
|
```svelte
|
|
1612
|
-
<div use:resizableWidth={() => ({
|
|
1631
|
+
<div use:resizableWidth={() => ({ initial: 300, min: 200, max: 600, key: "sidebar" })}>
|
|
1632
|
+
Resizable
|
|
1633
|
+
</div>
|
|
1613
1634
|
```
|
|
1614
1635
|
|
|
1615
1636
|
### `trim`
|
|
@@ -1802,6 +1823,37 @@ When a step has `selector`, the tour uses `document.querySelector(selector)` to
|
|
|
1802
1823
|
|
|
1803
1824
|
---
|
|
1804
1825
|
|
|
1826
|
+
## Attachments
|
|
1827
|
+
|
|
1828
|
+
Svelte `{@attach}` helpers — the preferred form for new DOM behavior (reactive, composable, forwardable through components). Imported from the package root like everything else.
|
|
1829
|
+
|
|
1830
|
+
### `autoHeight`
|
|
1831
|
+
|
|
1832
|
+
Animates the host's height to its single child's natural height (pair it with a CSS `height` transition).
|
|
1833
|
+
|
|
1834
|
+
```svelte
|
|
1835
|
+
<div class="viewport" {@attach autoHeight}>
|
|
1836
|
+
<div>…variable-height content…</div>
|
|
1837
|
+
</div>
|
|
1838
|
+
```
|
|
1839
|
+
|
|
1840
|
+
### `longPress`
|
|
1841
|
+
|
|
1842
|
+
Factory: calls `onLongPress` when a touch / pen pointer stays down for `duration` ms (default 500) without moving beyond `moveTolerance` px.
|
|
1843
|
+
|
|
1844
|
+
```svelte
|
|
1845
|
+
<div {@attach longPress({ onLongPress: (e) => openAt(e.clientX, e.clientY) })}>…</div>
|
|
1846
|
+
```
|
|
1847
|
+
|
|
1848
|
+
### `resizable`
|
|
1849
|
+
|
|
1850
|
+
Factory: drag-resizable width (`axis: "x"`) or height (`axis: "y"`) with an ARIA window-splitter handle (arrows / Home / End / Enter), `min` / `max`, `px` or `%` of the parent, persisted under `key`; `handle` drives an element of your own, `onInit` hands out `set()` / `reset()`.
|
|
1851
|
+
|
|
1852
|
+
```svelte
|
|
1853
|
+
<aside {@attach resizable({ initial: 300, min: 200, max: 600, key: "sidebar" })}>…</aside>
|
|
1854
|
+
<div {@attach resizable({ axis: "y", units: "%", initial: 40 })}>…</div>
|
|
1855
|
+
```
|
|
1856
|
+
|
|
1805
1857
|
## Utilities
|
|
1806
1858
|
|
|
1807
1859
|
Import from `@marianmeres/stuic`.
|
|
@@ -2327,6 +2379,8 @@ Each component defines customization tokens. Override globally in `:root {}` or
|
|
|
2327
2379
|
| Button | `--stuic-button-*` | `bg`, `text`, `border`, `ring-color`, `radius`, `padding-x-{size}` |
|
|
2328
2380
|
| Switch | `--stuic-switch-*` | `accent` |
|
|
2329
2381
|
| Slider | `--stuic-slider-*` | `track`, `fill`, `thumb`, `tick`, `tick-on-fill`, `thickness`, `length`, `radius`, `fill-radius` |
|
|
2382
|
+
| RangeSlider | `--stuic-range-slider-*` | `track`, `fill`, `thumb`, `tick`, `tick-on-fill`, `thickness`, `length`, `radius`, `fill-radius`, `ring-width`, `ring-color` |
|
|
2383
|
+
| SplitPane | `--stuic-split-pane-*` | `separator-color`, `separator-color-hover`, `separator-thickness`, `separator-hit-area`, `grip-color`, `grip-color-hover`, `grip-border-color`, `grip-length`, `grip-thickness`, `grip-radius`, `ring-color`, `transition` |
|
|
2330
2384
|
| Input | `--stuic-input-*` | `accent`, `accent-error` |
|
|
2331
2385
|
| Progress | `--stuic-progress-*` | `bg`, `accent` |
|
|
2332
2386
|
| ListItemButton | `--stuic-list-item-button-*` | `bg`, `text`, `border`, `bg-hover`, `text-hover` |
|
package/README.md
CHANGED
|
@@ -167,7 +167,7 @@ See [API.md](API.md) for the full list of exported theme types (`ThemeSchema`, `
|
|
|
167
167
|
|
|
168
168
|
### Layout & Overlays
|
|
169
169
|
|
|
170
|
-
AppShell, Accordion, Backdrop, Modal, ModalDialog, Drawer, Collapsible, Header, SlidingPanels, Nav, WithSidePanel
|
|
170
|
+
AppShell, Accordion, Backdrop, Modal, ModalDialog, Drawer, Collapsible, Header, SlidingPanels, Nav, WithSidePanel, SplitPane
|
|
171
171
|
|
|
172
172
|
### Forms & Inputs
|
|
173
173
|
|
|
@@ -175,7 +175,7 @@ FieldInput, FieldMoney, FieldDate, FieldDateRange, Calendar, FieldTextarea, Fiel
|
|
|
175
175
|
|
|
176
176
|
### Buttons & Controls
|
|
177
177
|
|
|
178
|
-
Button, ButtonGroupRadio, Switch, Slider, TwCheck, ListItemButton, X
|
|
178
|
+
Button, ButtonGroupRadio, Switch, Slider, RangeSlider, TwCheck, ListItemButton, X
|
|
179
179
|
|
|
180
180
|
### Feedback & Notifications
|
|
181
181
|
|
|
@@ -206,6 +206,18 @@ Cart, Checkout (CheckoutProgress, CheckoutOrderSummary, CheckoutCartReview, Chec
|
|
|
206
206
|
|
|
207
207
|
`autogrow` · `validate` · `focusTrap` · `autoscroll` · `dimBehind` · `fileDropzone` · `highlightDragover` · `resizableWidth` · `spotlight` · `trim` · `typeahead` · `onSubmitValidityCheck` · `popover` · `tooltip` · `createTour` / `tourStep` (onboarding)
|
|
208
208
|
|
|
209
|
+
## Attachments
|
|
210
|
+
|
|
211
|
+
Svelte `{@attach}` helpers — preferred over new actions (reactive, composable, forwardable).
|
|
212
|
+
|
|
213
|
+
```svelte
|
|
214
|
+
<div {@attach autoHeight}>…</div>
|
|
215
|
+
<aside {@attach resizable({ initial: 300, min: 200, max: 600, key: "sidebar" })}>…</aside>
|
|
216
|
+
<div {@attach longPress({ onLongPress: (e) => openAt(e.clientX, e.clientY) })}>…</div>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`autoHeight` · `longPress` · `resizable`
|
|
220
|
+
|
|
209
221
|
## PWA safe-area insets
|
|
210
222
|
|
|
211
223
|
When a stuic app is installed and launched standalone (iOS Home Screen, Android/desktop PWA), the web view fills the entire screen, so edge-anchored chrome can render under the status bar / notch / home indicator. stuic ships an **opt-in** safe-area layer that is **inert in a normal browser tab** (`env()` → `0`) and only engages under `@media (display-mode: standalone), (display-mode: fullscreen)`.
|
|
@@ -1,42 +1,28 @@
|
|
|
1
|
+
import { type ResizableOptions } from "../attachments/resizable.js";
|
|
1
2
|
/**
|
|
2
|
-
* Options for the
|
|
3
|
+
* Options for the {@link resizableWidth} action — the `x`-axis
|
|
4
|
+
* {@link ResizableOptions} of the `resizable` attachment with the legacy
|
|
5
|
+
* `onResize` payload shape (`width` instead of `size`).
|
|
3
6
|
*/
|
|
4
|
-
export interface ResizableWidthOptions {
|
|
5
|
-
enabled?: boolean;
|
|
6
|
-
initial?: number;
|
|
7
|
-
min?: number;
|
|
8
|
-
max?: number;
|
|
9
|
-
units?: "px" | "%";
|
|
10
|
-
/** Reverses handle position (left instead of right) and drag direction */
|
|
11
|
-
reverse?: boolean;
|
|
12
|
-
key?: string | number | null | undefined;
|
|
13
|
-
storage?: "local" | "session";
|
|
14
|
-
handleClass?: string;
|
|
15
|
-
handleDragClass?: string;
|
|
7
|
+
export interface ResizableWidthOptions extends Omit<ResizableOptions, "axis" | "onResize"> {
|
|
16
8
|
onResize?: (info: {
|
|
17
9
|
width: number;
|
|
18
10
|
units: "px" | "%";
|
|
19
11
|
container: number;
|
|
20
12
|
}) => void;
|
|
21
|
-
debug?: (...args: unknown[]) => void;
|
|
22
13
|
}
|
|
23
14
|
/**
|
|
24
|
-
* A Svelte action that makes an element's width resizable via drag handle
|
|
15
|
+
* A Svelte action that makes an element's width resizable via a drag handle on its
|
|
16
|
+
* right (or, with `reverse`, left) edge.
|
|
25
17
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* - Drag handle with visual feedback
|
|
32
|
-
* - Min/max width constraints
|
|
33
|
-
* - Support for px or % units
|
|
34
|
-
* - Optional storage persistence with custom key
|
|
35
|
-
* - Double-click handle to reset to initial width
|
|
36
|
-
* - Touch device support
|
|
18
|
+
* Kept for back-compat: it is a thin wrapper over the `resizable` attachment
|
|
19
|
+
* (`axis: "x"`), so it has everything the attachment has — the handle is a focusable
|
|
20
|
+
* `role="separator"` with arrow / Home / End / Enter keyboard resizing, Pointer Events
|
|
21
|
+
* cover mouse, touch and pen, `min` / `max` clamp, `key` persists, double-click resets.
|
|
22
|
+
* For new code (and for heights) use `{@attach resizable(...)}` directly.
|
|
37
23
|
*
|
|
38
24
|
* @param el - The element to make resizable
|
|
39
|
-
* @param fn - Function returning configuration options
|
|
25
|
+
* @param fn - Function returning configuration options (re-runs the action on reactive change)
|
|
40
26
|
*
|
|
41
27
|
* @remarks
|
|
42
28
|
* The `units` option should not be changed dynamically after initialization.
|
|
@@ -63,4 +49,4 @@ export interface ResizableWidthOptions {
|
|
|
63
49
|
* </div>
|
|
64
50
|
* ```
|
|
65
51
|
*/
|
|
66
|
-
export declare function resizableWidth(el:
|
|
52
|
+
export declare function resizableWidth(el: HTMLElement, fn?: () => ResizableWidthOptions): void;
|
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { twMerge } from "../utils/tw-merge.js";
|
|
1
|
+
import { resizable } from "../attachments/resizable.js";
|
|
3
2
|
/**
|
|
4
|
-
* A Svelte action that makes an element's width resizable via drag handle
|
|
3
|
+
* A Svelte action that makes an element's width resizable via a drag handle on its
|
|
4
|
+
* right (or, with `reverse`, left) edge.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - Drag handle with visual feedback
|
|
12
|
-
* - Min/max width constraints
|
|
13
|
-
* - Support for px or % units
|
|
14
|
-
* - Optional storage persistence with custom key
|
|
15
|
-
* - Double-click handle to reset to initial width
|
|
16
|
-
* - Touch device support
|
|
6
|
+
* Kept for back-compat: it is a thin wrapper over the `resizable` attachment
|
|
7
|
+
* (`axis: "x"`), so it has everything the attachment has — the handle is a focusable
|
|
8
|
+
* `role="separator"` with arrow / Home / End / Enter keyboard resizing, Pointer Events
|
|
9
|
+
* cover mouse, touch and pen, `min` / `max` clamp, `key` persists, double-click resets.
|
|
10
|
+
* For new code (and for heights) use `{@attach resizable(...)}` directly.
|
|
17
11
|
*
|
|
18
12
|
* @param el - The element to make resizable
|
|
19
|
-
* @param fn - Function returning configuration options
|
|
13
|
+
* @param fn - Function returning configuration options (re-runs the action on reactive change)
|
|
20
14
|
*
|
|
21
15
|
* @remarks
|
|
22
16
|
* The `units` option should not be changed dynamically after initialization.
|
|
@@ -44,162 +38,13 @@ import { twMerge } from "../utils/tw-merge.js";
|
|
|
44
38
|
* ```
|
|
45
39
|
*/
|
|
46
40
|
export function resizableWidth(el, fn) {
|
|
47
|
-
const HANDLE_CLS_BASE = [
|
|
48
|
-
"group",
|
|
49
|
-
"absolute top-0 bottom-0",
|
|
50
|
-
"w-[1px]",
|
|
51
|
-
"bg-black/20 hover:bg-black/30",
|
|
52
|
-
"dark:bg-white/10 dark:hover:bg-white/20",
|
|
53
|
-
"transition-colors duration-200",
|
|
54
|
-
"touch-none cursor-ew-resize",
|
|
55
|
-
].join(" ");
|
|
56
|
-
const DEFAULT_DRAG_HANDLE_CLS = [
|
|
57
|
-
"absolute h-[20px] w-[9px]",
|
|
58
|
-
"-translate-x-[4px] top-1/2 -translate-y-1/2",
|
|
59
|
-
"rounded border border-black/20 dark:border-white/20",
|
|
60
|
-
"bg-gray-300 group-hover:bg-gray-400",
|
|
61
|
-
"dark:bg-gray-600 dark:group-hover:bg-gray-500",
|
|
62
|
-
"transition-colors duration-200",
|
|
63
|
-
"touch-none cursor-ew-resize",
|
|
64
|
-
].join(" ");
|
|
65
|
-
function create_handle(el, handleClass, handleDragClass, reverse) {
|
|
66
|
-
const handle = document.createElement("div");
|
|
67
|
-
handle.setAttribute("data-handle", "true");
|
|
68
|
-
const dragHandle = document.createElement("div");
|
|
69
|
-
dragHandle.classList.add(...twMerge(DEFAULT_DRAG_HANDLE_CLS, handleDragClass).split(" "));
|
|
70
|
-
handle.appendChild(dragHandle);
|
|
71
|
-
el.appendChild(handle);
|
|
72
|
-
//
|
|
73
|
-
const positionCls = reverse ? "left-0" : "right-0";
|
|
74
|
-
handle.classList.add(...twMerge(HANDLE_CLS_BASE, positionCls, handleClass).split(" "));
|
|
75
|
-
return handle;
|
|
76
|
-
}
|
|
77
41
|
$effect(() => {
|
|
78
|
-
const {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
let isResizing = false;
|
|
87
|
-
let startX = 0;
|
|
88
|
-
let startWidth = 0;
|
|
89
|
-
let containerW = undefined;
|
|
90
|
-
//
|
|
91
|
-
const handle = create_handle(el, handleClass, handleDragClass, reverse);
|
|
92
|
-
const container = el.parentElement;
|
|
93
|
-
// do we have a storage? if so, adjust the initial value...
|
|
94
|
-
const initialBackup = initial;
|
|
95
|
-
const _storage = get_storage(storage, key, initial);
|
|
96
|
-
if (_storage)
|
|
97
|
-
initial = _storage.current ?? initial;
|
|
98
|
-
// handlers/workers/helpers //////////////////////////////////////////////////////
|
|
99
|
-
function set_width(pxOrPercent) {
|
|
100
|
-
if (pxOrPercent) {
|
|
101
|
-
_debug(`set_width(${pxOrPercent})`);
|
|
102
|
-
set_width_px(units === "%" ? container.offsetWidth * (pxOrPercent / 100) : pxOrPercent);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
function set_width_px(widthPx) {
|
|
106
|
-
_debug(`set_width_px(${widthPx})`);
|
|
107
|
-
containerW ??= container.offsetWidth;
|
|
108
|
-
const clamp = (value) => {
|
|
109
|
-
const _initial = value;
|
|
110
|
-
if (min)
|
|
111
|
-
value = Math.max(min, value);
|
|
112
|
-
if (max)
|
|
113
|
-
value = Math.min(max, value);
|
|
114
|
-
if (_initial !== value)
|
|
115
|
-
_debug("clamped", value, units);
|
|
116
|
-
return value;
|
|
117
|
-
};
|
|
118
|
-
let width;
|
|
119
|
-
if (units === "%") {
|
|
120
|
-
const widthPercent = Math.min(100, (widthPx / containerW) * 100); // convert to % (with 100 max)
|
|
121
|
-
width = clamp(widthPercent);
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
width = clamp(widthPx);
|
|
125
|
-
}
|
|
126
|
-
el.style.width = `${width}${units}`;
|
|
127
|
-
_debug("new width", width, units);
|
|
128
|
-
const info = { width, units, container: containerW };
|
|
129
|
-
onResize?.(info);
|
|
130
|
-
// maybe save to storage
|
|
131
|
-
if (_storage)
|
|
132
|
-
_storage.current = width;
|
|
133
|
-
return info;
|
|
134
|
-
}
|
|
135
|
-
function resize_start(e) {
|
|
136
|
-
e.preventDefault(); // prevent scrolling on touch devices
|
|
137
|
-
isResizing = true;
|
|
138
|
-
//
|
|
139
|
-
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
|
|
140
|
-
startX = clientX;
|
|
141
|
-
startWidth = parseInt(getComputedStyle(el).width, 10);
|
|
142
|
-
containerW = container.offsetWidth;
|
|
143
|
-
//
|
|
144
|
-
document.body.style.cursor = "ew-resize";
|
|
145
|
-
document.body.style.userSelect = "none";
|
|
146
|
-
}
|
|
147
|
-
function resize(e) {
|
|
148
|
-
if (!isResizing)
|
|
149
|
-
return;
|
|
150
|
-
e.preventDefault(); // prevent scrolling on touch devices
|
|
151
|
-
//
|
|
152
|
-
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
|
|
153
|
-
const deltaX = clientX - startX;
|
|
154
|
-
const width = reverse ? startWidth - deltaX : startWidth + deltaX;
|
|
155
|
-
set_width_px(width);
|
|
156
|
-
}
|
|
157
|
-
function resize_stop() {
|
|
158
|
-
if (isResizing) {
|
|
159
|
-
isResizing = false;
|
|
160
|
-
containerW = undefined;
|
|
161
|
-
//
|
|
162
|
-
document.body.style.cursor = "";
|
|
163
|
-
document.body.style.userSelect = "";
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
function on_dblclick() {
|
|
167
|
-
set_width(initialBackup);
|
|
168
|
-
}
|
|
169
|
-
// initial styles ////////////////////////////////////////////////////////////////
|
|
170
|
-
el.style.position = "relative"; // so the handle will work
|
|
171
|
-
set_width(initial);
|
|
172
|
-
// listeners /////////////////////////////////////////////////////////////////////
|
|
173
|
-
// handle
|
|
174
|
-
handle.addEventListener("dblclick", on_dblclick);
|
|
175
|
-
handle.addEventListener("selectstart", (e) => e.preventDefault()); // prevent text selection during resize
|
|
176
|
-
// mouse
|
|
177
|
-
handle.addEventListener("mousedown", resize_start);
|
|
178
|
-
document.addEventListener("mousemove", resize);
|
|
179
|
-
document.addEventListener("mouseup", resize_stop);
|
|
180
|
-
// touch
|
|
181
|
-
handle.addEventListener("touchstart", resize_start, { passive: false });
|
|
182
|
-
document.addEventListener("touchmove", resize, { passive: false });
|
|
183
|
-
document.addEventListener("touchend", resize_stop);
|
|
184
|
-
document.addEventListener("touchcancel", resize_stop);
|
|
185
|
-
// cleanup ///////////////////////////////////////////////////////////////////////
|
|
186
|
-
return () => {
|
|
187
|
-
// mouse
|
|
188
|
-
document.removeEventListener("mousemove", resize);
|
|
189
|
-
document.removeEventListener("mouseup", resize_stop);
|
|
190
|
-
// touch
|
|
191
|
-
document.removeEventListener("touchmove", resize);
|
|
192
|
-
document.removeEventListener("touchend", resize_stop);
|
|
193
|
-
document.removeEventListener("touchcancel", resize_stop);
|
|
194
|
-
// will also remove it's own event listeners
|
|
195
|
-
handle.remove();
|
|
196
|
-
};
|
|
42
|
+
const { onResize, ...rest } = fn?.() || {};
|
|
43
|
+
return resizable({
|
|
44
|
+
...rest,
|
|
45
|
+
axis: "x",
|
|
46
|
+
onResize: onResize &&
|
|
47
|
+
(({ size, units, container }) => onResize({ width: size, units, container })),
|
|
48
|
+
})(el);
|
|
197
49
|
});
|
|
198
50
|
}
|
|
199
|
-
// helpers ///////////////////////////////////////////////////////////////////////////////
|
|
200
|
-
function get_storage(type, key, initialValue) {
|
|
201
|
-
if (key) {
|
|
202
|
-
return (type === "session" ? sessionStorageState : localStorageState)(`resizable-width-${key}`, initialValue);
|
|
203
|
-
}
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Attachment } from "svelte/attachments";
|
|
2
|
+
/** Which box dimension is resized: `x` = width, `y` = height. */
|
|
3
|
+
export type ResizableAxis = "x" | "y";
|
|
4
|
+
/** `px`, or `%` of the parent element's size along the axis. */
|
|
5
|
+
export type ResizableUnits = "px" | "%";
|
|
6
|
+
/** Payload of {@link ResizableOptions.onResize}. */
|
|
7
|
+
export interface ResizableInfo {
|
|
8
|
+
/** The applied (clamped) size, in `units` */
|
|
9
|
+
size: number;
|
|
10
|
+
units: ResizableUnits;
|
|
11
|
+
axis: ResizableAxis;
|
|
12
|
+
/** The parent element's size along the axis in px, as measured for this resize */
|
|
13
|
+
container: number;
|
|
14
|
+
}
|
|
15
|
+
/** Imperative handle, delivered through {@link ResizableOptions.onInit}. */
|
|
16
|
+
export interface ResizableApi {
|
|
17
|
+
/** The last applied size, in `units` */
|
|
18
|
+
readonly current: number;
|
|
19
|
+
/** Applies a size (in `units`): clamped to `min` / `max`, persisted, announced, reported via `onResize` */
|
|
20
|
+
set(size: number): void;
|
|
21
|
+
/** Restores `resetTo` (defaults to `initial`) — what double-click / Enter on the handle do */
|
|
22
|
+
reset(): void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Options for the {@link resizable} attachment.
|
|
26
|
+
*/
|
|
27
|
+
export interface ResizableOptions {
|
|
28
|
+
/** Master switch. When `false` nothing is set up: no handle, no size applied. Default `true`. */
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* `x` resizes the width (the handle sits on a vertical edge), `y` resizes the height
|
|
32
|
+
* (horizontal edge). Default `x`.
|
|
33
|
+
*/
|
|
34
|
+
axis?: ResizableAxis;
|
|
35
|
+
/**
|
|
36
|
+
* Size applied on setup when nothing is stored under `key`. `0` (default) leaves the
|
|
37
|
+
* element's own CSS size alone until the first resize.
|
|
38
|
+
*/
|
|
39
|
+
initial?: number;
|
|
40
|
+
/** Lower bound in `units`. `0` = none. */
|
|
41
|
+
min?: number;
|
|
42
|
+
/** Upper bound in `units`. `0` = none (`%` is still capped at 100). */
|
|
43
|
+
max?: number;
|
|
44
|
+
/** `px` (default) or `%` of the parent element. Not meant to change after setup. */
|
|
45
|
+
units?: ResizableUnits;
|
|
46
|
+
/**
|
|
47
|
+
* Puts the handle on the start edge (left / top) and inverts the drag direction — for
|
|
48
|
+
* an element sitting at the end (right / bottom) of its container.
|
|
49
|
+
*/
|
|
50
|
+
reverse?: boolean;
|
|
51
|
+
/** Persist the size under this key (as `resizable-width-<key>` / `resizable-height-<key>`). */
|
|
52
|
+
key?: string | number | null | undefined;
|
|
53
|
+
/** Where `key` persists to. Default `session`. */
|
|
54
|
+
storage?: "local" | "session";
|
|
55
|
+
/** Keyboard step in `units`; Shift multiplies it by 10. Default `10` for px, `1` for %. */
|
|
56
|
+
step?: number;
|
|
57
|
+
/** Accessible name of the handle (it is a `separator`). Default `"Resize"`. */
|
|
58
|
+
label?: string;
|
|
59
|
+
/** The size restored by double-click / Enter / `api.reset()`. Defaults to `initial`. */
|
|
60
|
+
resetTo?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Use this element as the handle instead of creating one inside the resized element.
|
|
63
|
+
* It receives the separator semantics, the keyboard and the pointer handling (and
|
|
64
|
+
* `touch-action: none`), but no visual styling — that stays with the consumer.
|
|
65
|
+
*/
|
|
66
|
+
handle?: HTMLElement;
|
|
67
|
+
/** Extra classes of the created handle (the full-edge strip). Ignored with `handle`. */
|
|
68
|
+
handleClass?: string;
|
|
69
|
+
/** Extra classes of the created handle's grip. Ignored with `handle`. */
|
|
70
|
+
handleDragClass?: string;
|
|
71
|
+
/** Fires whenever a size is applied: on setup, while dragging, per key press, on reset and `api.set()` */
|
|
72
|
+
onResize?: (info: ResizableInfo) => void;
|
|
73
|
+
/** Receives the imperative api, once per setup */
|
|
74
|
+
onInit?: (api: ResizableApi) => void;
|
|
75
|
+
debug?: (...args: unknown[]) => void;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Makes an element's width (`axis: "x"`) or height (`axis: "y"`) resizable by dragging
|
|
79
|
+
* its edge. The handle is a keyboard-operable ARIA window splitter (`role="separator"`,
|
|
80
|
+
* focusable, `aria-valuenow` in `units`): arrow keys along the axis move it by `step`
|
|
81
|
+
* (×10 with Shift), Home / End go to `min` / `max`, Enter (like double-click) resets.
|
|
82
|
+
* Pointer Events with capture cover mouse, touch and pen in one code path.
|
|
83
|
+
*
|
|
84
|
+
* By default a thin handle strip with a grip is appended inside the element on the
|
|
85
|
+
* resized edge (the element becomes `position: relative`); pass `handle` to drive an
|
|
86
|
+
* element of your own (a sibling separator, say) instead. The size can be persisted
|
|
87
|
+
* under `key`. `min` / `max` clamp every path (drag, keys, `api.set()`).
|
|
88
|
+
*
|
|
89
|
+
* A factory: call it with the options (or a function returning them — reactive reads
|
|
90
|
+
* inside re-run the attachment) and attach the result.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```svelte
|
|
94
|
+
* <script>
|
|
95
|
+
* import { resizable } from "@marianmeres/stuic";
|
|
96
|
+
* </script>
|
|
97
|
+
*
|
|
98
|
+
* <!-- side by side: the first flex child gets a draggable width -->
|
|
99
|
+
* <div class="flex">
|
|
100
|
+
* <aside {@attach resizable({ initial: 300, min: 200, max: 600, key: "sidebar" })}>…</aside>
|
|
101
|
+
* <main class="flex-1">…</main>
|
|
102
|
+
* </div>
|
|
103
|
+
*
|
|
104
|
+
* <!-- stacked: a draggable height, in % of the (definite-height) parent -->
|
|
105
|
+
* <div class="flex flex-col h-96">
|
|
106
|
+
* <div {@attach resizable({ axis: "y", units: "%", initial: 40, max: 80 })}>…</div>
|
|
107
|
+
* <div class="flex-1">…</div>
|
|
108
|
+
* </div>
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* @param options - {@link ResizableOptions}, or a function returning them
|
|
112
|
+
*/
|
|
113
|
+
export declare function resizable(options?: ResizableOptions | (() => ResizableOptions)): Attachment<HTMLElement>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
TEST-ONLY fixture for the `resizable` attachment (and the `resizableWidth` action
|
|
3
|
+
wrapper). A 400x300 flex container, the resized element first, a flexing sibling
|
|
4
|
+
after it; `external` adds a sibling to serve as the provided handle; `unmount`
|
|
5
|
+
tears the whole thing down so cleanup can be observed on the detached nodes.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import { resizable, type ResizableOptions } from "./resizable.js";
|
|
9
|
+
import {
|
|
10
|
+
resizableWidth,
|
|
11
|
+
type ResizableWidthOptions,
|
|
12
|
+
} from "../actions/resizable-width.svelte.js";
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
options = {} as ResizableOptions,
|
|
16
|
+
action = undefined as ResizableWidthOptions | undefined,
|
|
17
|
+
external = false,
|
|
18
|
+
vertical = false,
|
|
19
|
+
} = $props();
|
|
20
|
+
|
|
21
|
+
let handleEl = $state<HTMLDivElement>();
|
|
22
|
+
let mounted = $state(true);
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<div
|
|
26
|
+
data-testid="container"
|
|
27
|
+
style="width:400px;height:300px;display:flex;flex-direction:{vertical
|
|
28
|
+
? 'column'
|
|
29
|
+
: 'row'}"
|
|
30
|
+
>
|
|
31
|
+
{#if mounted}
|
|
32
|
+
{#if action}
|
|
33
|
+
<div data-testid="el" style="width:100px" use:resizableWidth={() => action}>el</div>
|
|
34
|
+
{:else}
|
|
35
|
+
<div
|
|
36
|
+
data-testid="el"
|
|
37
|
+
style={vertical ? "height:100px" : "width:100px"}
|
|
38
|
+
{@attach external
|
|
39
|
+
? handleEl && resizable({ ...options, handle: handleEl })
|
|
40
|
+
: resizable(options)}
|
|
41
|
+
>
|
|
42
|
+
el
|
|
43
|
+
</div>
|
|
44
|
+
{/if}
|
|
45
|
+
{#if external}
|
|
46
|
+
<div data-testid="handle" data-foo="bar" bind:this={handleEl}></div>
|
|
47
|
+
{/if}
|
|
48
|
+
{/if}
|
|
49
|
+
<div style="flex:1">rest</div>
|
|
50
|
+
</div>
|
|
51
|
+
<button type="button" data-testid="unmount" onclick={() => (mounted = false)}
|
|
52
|
+
>unmount</button
|
|
53
|
+
>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ResizableOptions } from "./resizable.js";
|
|
2
|
+
import { type ResizableWidthOptions } from "../actions/resizable-width.svelte.js";
|
|
3
|
+
declare const Resizable: import("svelte").Component<{
|
|
4
|
+
options?: ResizableOptions;
|
|
5
|
+
action?: ResizableWidthOptions | undefined;
|
|
6
|
+
external?: boolean;
|
|
7
|
+
vertical?: boolean;
|
|
8
|
+
}, {}, "">;
|
|
9
|
+
type Resizable = ReturnType<typeof Resizable>;
|
|
10
|
+
export default Resizable;
|