@viniaraujo68/plinth 0.2.0 → 0.3.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 +2 -2
- package/dist/attachments/index.d.ts +1 -0
- package/dist/attachments/index.js +1 -0
- package/dist/attachments/sortable.d.ts +6 -0
- package/dist/attachments/sortable.js +227 -0
- package/dist/components/DragHandle.svelte +26 -0
- package/dist/components/DragHandle.svelte.d.ts +7 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -258,7 +258,7 @@ base URL, how auth is injected, and how the backend spells an error body.
|
|
|
258
258
|
| `@viniaraujo68/plinth` | `AsyncAction`, the error reporter, `LIBRARY_VERSION` |
|
|
259
259
|
| `@viniaraujo68/plinth/theme` | `ThemeContext`, `ThemeController`, `ThemeToggle`, the cookie helpers, `createCssColorReader` |
|
|
260
260
|
| `@viniaraujo68/plinth/theme.css` | The stylesheet the tokens are declared in |
|
|
261
|
-
| `@viniaraujo68/plinth/attachments` | The `tooltip` attachment, usable on any tag
|
|
261
|
+
| `@viniaraujo68/plinth/attachments` | The `tooltip` attachment, usable on any tag, and `sortable`, which reorders a list from its drag handles |
|
|
262
262
|
| `@viniaraujo68/plinth/components` | The primitives, the `DateRange` and option-matching helpers |
|
|
263
263
|
| `@viniaraujo68/plinth/confirm` | `confirm`, `Confirmer`, the challenge helpers |
|
|
264
264
|
| `@viniaraujo68/plinth/shell` | `AppShell`, `Breadcrumbs` |
|
|
@@ -270,7 +270,7 @@ base URL, how auth is injected, and how the backend spells an error body.
|
|
|
270
270
|
| `@viniaraujo68/plinth/formatters` | `createFormatters`, over `Intl` |
|
|
271
271
|
|
|
272
272
|
The components entry point ships `AsyncButton`, `Combobox`, `Copyable`, `DateRangePicker`,
|
|
273
|
-
`Dialog`, `Dropdown`, `ErrorDisplay`, `ErrorPage`, `JsonInput`, `Listbox`, `LoadingButton`, `Logo`,
|
|
273
|
+
`Dialog`, `DragHandle`, `Dropdown`, `ErrorDisplay`, `ErrorPage`, `JsonInput`, `Listbox`, `LoadingButton`, `Logo`,
|
|
274
274
|
`Modal`, `MultiSelect`, `Paginator`, `RefreshButton`, `SegmentedControl`, `Select`, `Skeleton` and
|
|
275
275
|
`Tooltip`. Every one of them has a page in the showcase.
|
|
276
276
|
It also exports the mark's outline as data — `PICK_PATH` and `PICK_CLIP_PATH` — for anything
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { tick } from "svelte";
|
|
2
|
+
const ITEM = "[data-sortable-item]";
|
|
3
|
+
const HANDLE = "[data-sortable-handle]";
|
|
4
|
+
const DRAGGING = "data-sortable-dragging";
|
|
5
|
+
const ANIMATION_ID = "plinth-sortable";
|
|
6
|
+
const SHIFT_MS = 180;
|
|
7
|
+
const SETTLE_MS = 160;
|
|
8
|
+
const EASING = "cubic-bezier(0.2, 0, 0, 1)";
|
|
9
|
+
const KEY_STEPS = {
|
|
10
|
+
ArrowLeft: -1,
|
|
11
|
+
ArrowUp: -1,
|
|
12
|
+
ArrowRight: 1,
|
|
13
|
+
ArrowDown: 1,
|
|
14
|
+
};
|
|
15
|
+
const destination = (key, from, last) => {
|
|
16
|
+
if (key === "Home")
|
|
17
|
+
return 0;
|
|
18
|
+
if (key === "End")
|
|
19
|
+
return last;
|
|
20
|
+
return key in KEY_STEPS ? from + KEY_STEPS[key] : null;
|
|
21
|
+
};
|
|
22
|
+
const layoutCenter = (element) => ({
|
|
23
|
+
x: element.offsetLeft + element.offsetWidth / 2,
|
|
24
|
+
y: element.offsetTop + element.offsetHeight / 2,
|
|
25
|
+
});
|
|
26
|
+
const draggedCenter = (current) => ({
|
|
27
|
+
x: current.startLeft + current.item.offsetWidth / 2 + current.x - current.grabX,
|
|
28
|
+
y: current.startTop + current.item.offsetHeight / 2 + current.y - current.grabY,
|
|
29
|
+
});
|
|
30
|
+
const distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
31
|
+
const reducedMotion = () => window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
32
|
+
const cancelShift = (element) => {
|
|
33
|
+
for (const animation of element.getAnimations())
|
|
34
|
+
if (animation.id === ANIMATION_ID)
|
|
35
|
+
animation.cancel();
|
|
36
|
+
};
|
|
37
|
+
const offsetOf = (element) => {
|
|
38
|
+
const [x = "0", y = "0"] = element.style.translate.split(" ");
|
|
39
|
+
return { x: Number.parseFloat(x) || 0, y: Number.parseFloat(y) || 0 };
|
|
40
|
+
};
|
|
41
|
+
export const sortable = (options) => (container) => {
|
|
42
|
+
let drag = null;
|
|
43
|
+
let shifting = false;
|
|
44
|
+
let frame = 0;
|
|
45
|
+
const items = () => [...container.querySelectorAll(ITEM)];
|
|
46
|
+
const handleOf = (event) => {
|
|
47
|
+
const handle = event.target?.closest(HANDLE) ?? null;
|
|
48
|
+
return handle && container.contains(handle) ? handle : null;
|
|
49
|
+
};
|
|
50
|
+
const shift = async (move, still) => {
|
|
51
|
+
const before = new Map(items().map((element) => [element, element.getBoundingClientRect()]));
|
|
52
|
+
move();
|
|
53
|
+
await tick();
|
|
54
|
+
if (reducedMotion())
|
|
55
|
+
return;
|
|
56
|
+
for (const element of items()) {
|
|
57
|
+
const previous = before.get(element);
|
|
58
|
+
if (element === still || !previous)
|
|
59
|
+
continue;
|
|
60
|
+
cancelShift(element);
|
|
61
|
+
const current = element.getBoundingClientRect();
|
|
62
|
+
const dx = previous.left - current.left;
|
|
63
|
+
const dy = previous.top - current.top;
|
|
64
|
+
if (!dx && !dy)
|
|
65
|
+
continue;
|
|
66
|
+
element.animate([{ translate: `${dx}px ${dy}px` }, { translate: "0px 0px" }], {
|
|
67
|
+
duration: SHIFT_MS,
|
|
68
|
+
easing: EASING,
|
|
69
|
+
id: ANIMATION_ID,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const follow = () => {
|
|
74
|
+
if (!drag)
|
|
75
|
+
return;
|
|
76
|
+
const { item } = drag;
|
|
77
|
+
const dx = drag.x - drag.grabX - (item.offsetLeft - drag.startLeft);
|
|
78
|
+
const dy = drag.y - drag.grabY - (item.offsetTop - drag.startTop);
|
|
79
|
+
item.style.translate = `${dx}px ${dy}px`;
|
|
80
|
+
};
|
|
81
|
+
const targetFor = (current) => {
|
|
82
|
+
const all = items();
|
|
83
|
+
const from = all.indexOf(current.item);
|
|
84
|
+
if (from < 0)
|
|
85
|
+
return null;
|
|
86
|
+
const center = draggedCenter(current);
|
|
87
|
+
let index = -1;
|
|
88
|
+
let nearest = Infinity;
|
|
89
|
+
for (const [position, element] of all.entries()) {
|
|
90
|
+
if (element === current.item)
|
|
91
|
+
continue;
|
|
92
|
+
const gap = distance(center, layoutCenter(element));
|
|
93
|
+
if (gap < nearest) {
|
|
94
|
+
nearest = gap;
|
|
95
|
+
index = position;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (index < 0)
|
|
99
|
+
return null;
|
|
100
|
+
const target = all[index];
|
|
101
|
+
const middle = layoutCenter(target);
|
|
102
|
+
const sameRow = Math.abs(target.offsetTop - current.item.offsetTop) < target.offsetHeight / 2;
|
|
103
|
+
const forward = index > from;
|
|
104
|
+
const passed = sameRow ? forward === center.x > middle.x : forward === center.y > middle.y;
|
|
105
|
+
return passed ? { from, to: index } : null;
|
|
106
|
+
};
|
|
107
|
+
const step = () => {
|
|
108
|
+
if (!drag || shifting)
|
|
109
|
+
return;
|
|
110
|
+
follow();
|
|
111
|
+
const target = targetFor(drag);
|
|
112
|
+
if (!target)
|
|
113
|
+
return;
|
|
114
|
+
shifting = true;
|
|
115
|
+
const { item } = drag;
|
|
116
|
+
void shift(() => {
|
|
117
|
+
options.onmove(target.from, target.to);
|
|
118
|
+
}, item).then(() => {
|
|
119
|
+
shifting = false;
|
|
120
|
+
follow();
|
|
121
|
+
frame = requestAnimationFrame(step);
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
const settle = (current) => {
|
|
125
|
+
const { item, restore } = current;
|
|
126
|
+
const offset = offsetOf(item);
|
|
127
|
+
item.style.translate = "";
|
|
128
|
+
item.removeAttribute(DRAGGING);
|
|
129
|
+
const done = () => {
|
|
130
|
+
item.style.position = restore.position;
|
|
131
|
+
item.style.zIndex = restore.zIndex;
|
|
132
|
+
item.style.willChange = restore.willChange;
|
|
133
|
+
};
|
|
134
|
+
if (reducedMotion() || (!offset.x && !offset.y)) {
|
|
135
|
+
done();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const animation = item.animate([{ translate: `${offset.x}px ${offset.y}px` }, { translate: "0px 0px" }], { duration: SETTLE_MS, easing: EASING });
|
|
139
|
+
void animation.finished.then(done, done);
|
|
140
|
+
};
|
|
141
|
+
const stop = () => {
|
|
142
|
+
cancelAnimationFrame(frame);
|
|
143
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
144
|
+
window.removeEventListener("pointerup", onPointerUp);
|
|
145
|
+
window.removeEventListener("pointercancel", onPointerUp);
|
|
146
|
+
if (!drag)
|
|
147
|
+
return;
|
|
148
|
+
const current = drag;
|
|
149
|
+
drag = null;
|
|
150
|
+
settle(current);
|
|
151
|
+
};
|
|
152
|
+
function onPointerMove(event) {
|
|
153
|
+
if (drag?.pointerId !== event.pointerId)
|
|
154
|
+
return;
|
|
155
|
+
drag.x = event.clientX;
|
|
156
|
+
drag.y = event.clientY;
|
|
157
|
+
step();
|
|
158
|
+
}
|
|
159
|
+
function onPointerUp(event) {
|
|
160
|
+
if (drag?.pointerId === event.pointerId)
|
|
161
|
+
stop();
|
|
162
|
+
}
|
|
163
|
+
const onPointerDown = (event) => {
|
|
164
|
+
if (options.disabled || event.button !== 0 || drag)
|
|
165
|
+
return;
|
|
166
|
+
const item = handleOf(event)?.closest(ITEM);
|
|
167
|
+
if (!item)
|
|
168
|
+
return;
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
cancelShift(item);
|
|
171
|
+
drag = {
|
|
172
|
+
item,
|
|
173
|
+
pointerId: event.pointerId,
|
|
174
|
+
grabX: event.clientX,
|
|
175
|
+
grabY: event.clientY,
|
|
176
|
+
startLeft: item.offsetLeft,
|
|
177
|
+
startTop: item.offsetTop,
|
|
178
|
+
x: event.clientX,
|
|
179
|
+
y: event.clientY,
|
|
180
|
+
restore: {
|
|
181
|
+
position: item.style.position,
|
|
182
|
+
zIndex: item.style.zIndex,
|
|
183
|
+
willChange: item.style.willChange,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
if (getComputedStyle(item).position === "static")
|
|
187
|
+
item.style.position = "relative";
|
|
188
|
+
item.style.zIndex = "10";
|
|
189
|
+
item.style.willChange = "translate";
|
|
190
|
+
item.setAttribute(DRAGGING, "");
|
|
191
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
192
|
+
window.addEventListener("pointerup", onPointerUp);
|
|
193
|
+
window.addEventListener("pointercancel", onPointerUp);
|
|
194
|
+
};
|
|
195
|
+
const onTouchMove = (event) => {
|
|
196
|
+
if (drag)
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
};
|
|
199
|
+
const onKeyDown = (event) => {
|
|
200
|
+
if (options.disabled || drag)
|
|
201
|
+
return;
|
|
202
|
+
const item = handleOf(event)?.closest(ITEM);
|
|
203
|
+
if (!item)
|
|
204
|
+
return;
|
|
205
|
+
const all = items();
|
|
206
|
+
const from = all.indexOf(item);
|
|
207
|
+
const last = all.length - 1;
|
|
208
|
+
const to = destination(event.key, from, last);
|
|
209
|
+
if (to === null || from < 0)
|
|
210
|
+
return;
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
if (to < 0 || to > last || to === from)
|
|
213
|
+
return;
|
|
214
|
+
void shift(() => {
|
|
215
|
+
options.onmove(from, to);
|
|
216
|
+
}).then(() => items()[to]?.querySelector(HANDLE)?.focus());
|
|
217
|
+
};
|
|
218
|
+
container.addEventListener("pointerdown", onPointerDown);
|
|
219
|
+
container.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
220
|
+
container.addEventListener("keydown", onKeyDown);
|
|
221
|
+
return () => {
|
|
222
|
+
stop();
|
|
223
|
+
container.removeEventListener("pointerdown", onPointerDown);
|
|
224
|
+
container.removeEventListener("touchmove", onTouchMove);
|
|
225
|
+
container.removeEventListener("keydown", onKeyDown);
|
|
226
|
+
};
|
|
227
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLButtonAttributes } from "svelte/elements";
|
|
3
|
+
|
|
4
|
+
interface Props extends Omit<HTMLButtonAttributes, "type" | "children"> {
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let { label, class: className = "", ...rest }: Props = $props();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<button
|
|
12
|
+
{...rest}
|
|
13
|
+
type="button"
|
|
14
|
+
aria-label={label}
|
|
15
|
+
data-sortable-handle
|
|
16
|
+
class="inline-grid shrink-0 cursor-grab touch-none place-items-center self-stretch px-1 text-base-content/45 select-none hover:text-base-content focus-visible:text-base-content focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary in-data-sortable-dragging:cursor-grabbing {className}"
|
|
17
|
+
>
|
|
18
|
+
<svg viewBox="0 0 10 15" class="h-[15px] w-2.5" fill="currentColor" aria-hidden="true">
|
|
19
|
+
<circle cx="2.5" cy="2.5" r="1.5" />
|
|
20
|
+
<circle cx="7.5" cy="2.5" r="1.5" />
|
|
21
|
+
<circle cx="2.5" cy="7.5" r="1.5" />
|
|
22
|
+
<circle cx="7.5" cy="7.5" r="1.5" />
|
|
23
|
+
<circle cx="2.5" cy="12.5" r="1.5" />
|
|
24
|
+
<circle cx="7.5" cy="12.5" r="1.5" />
|
|
25
|
+
</svg>
|
|
26
|
+
</button>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HTMLButtonAttributes } from "svelte/elements";
|
|
2
|
+
interface Props extends Omit<HTMLButtonAttributes, "type" | "children"> {
|
|
3
|
+
label: string;
|
|
4
|
+
}
|
|
5
|
+
declare const DragHandle: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type DragHandle = ReturnType<typeof DragHandle>;
|
|
7
|
+
export default DragHandle;
|
|
@@ -3,6 +3,7 @@ export { default as Combobox } from "./Combobox.svelte";
|
|
|
3
3
|
export { default as Copyable } from "./Copyable.svelte";
|
|
4
4
|
export { default as DateRangePicker } from "./DateRangePicker.svelte";
|
|
5
5
|
export { default as Dialog } from "./Dialog.svelte";
|
|
6
|
+
export { default as DragHandle } from "./DragHandle.svelte";
|
|
6
7
|
export { default as Dropdown } from "./Dropdown.svelte";
|
|
7
8
|
export { default as ErrorDisplay } from "./ErrorDisplay.svelte";
|
|
8
9
|
export { default as ErrorPage } from "./ErrorPage.svelte";
|
package/dist/components/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { default as Combobox } from "./Combobox.svelte";
|
|
|
3
3
|
export { default as Copyable } from "./Copyable.svelte";
|
|
4
4
|
export { default as DateRangePicker } from "./DateRangePicker.svelte";
|
|
5
5
|
export { default as Dialog } from "./Dialog.svelte";
|
|
6
|
+
export { default as DragHandle } from "./DragHandle.svelte";
|
|
6
7
|
export { default as Dropdown } from "./Dropdown.svelte";
|
|
7
8
|
export { default as ErrorDisplay } from "./ErrorDisplay.svelte";
|
|
8
9
|
export { default as ErrorPage } from "./ErrorPage.svelte";
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
* no portable way to import its own manifest, and a consumer that only wants to log a version
|
|
6
6
|
* should not have to bundle one. `version.spec.ts` fails when this drifts from the manifest.
|
|
7
7
|
*/
|
|
8
|
-
export declare const LIBRARY_VERSION = "0.
|
|
8
|
+
export declare const LIBRARY_VERSION = "0.3.0";
|
|
9
9
|
export { AsyncAction, type AsyncActionStatus, type AsyncFn } from "./async-action.svelte.js";
|
|
10
10
|
export { reportError, setErrorReporter, type ErrorReporter } from "./error.js";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* no portable way to import its own manifest, and a consumer that only wants to log a version
|
|
6
6
|
* should not have to bundle one. `version.spec.ts` fails when this drifts from the manifest.
|
|
7
7
|
*/
|
|
8
|
-
export const LIBRARY_VERSION = "0.
|
|
8
|
+
export const LIBRARY_VERSION = "0.3.0";
|
|
9
9
|
// The root entry carries only what is not tied to one area: the error channel an app installs
|
|
10
10
|
// once, and the async state machine the components are built on. Everything visual is reached
|
|
11
11
|
// through a subpath — `/theme`, `/attachments`, `/components`, `/toast` — so importing a button
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viniaraujo68/plinth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A personal Svelte 5 design system: theme, application shell, interface primitives, routing metadata, a fetch wrapper and locale-aware formatters.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|