@delightstack/components 0.1.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/LICENSE +21 -0
- package/README.md +136 -0
- package/SKILL.md +149 -0
- package/bin/agents.js +63 -0
- package/dist/actions/Alert.svelte +202 -0
- package/dist/actions/Alert.svelte.d.ts +36 -0
- package/dist/actions/Alert.svelte.d.ts.map +1 -0
- package/dist/actions/Button.svelte +1450 -0
- package/dist/actions/Button.svelte.d.ts +56 -0
- package/dist/actions/Button.svelte.d.ts.map +1 -0
- package/dist/actions/ButtonGroup.svelte +111 -0
- package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
- package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
- package/dist/actions/CommandPalette.svelte +939 -0
- package/dist/actions/CommandPalette.svelte.d.ts +37 -0
- package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
- package/dist/actions/ContextMenu.svelte +138 -0
- package/dist/actions/ContextMenu.svelte.d.ts +54 -0
- package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
- package/dist/actions/Modal.svelte +474 -0
- package/dist/actions/Modal.svelte.d.ts +28 -0
- package/dist/actions/Modal.svelte.d.ts.map +1 -0
- package/dist/actions/Popover.svelte +1214 -0
- package/dist/actions/Popover.svelte.d.ts +31 -0
- package/dist/actions/Popover.svelte.d.ts.map +1 -0
- package/dist/actions/Portal.svelte +80 -0
- package/dist/actions/Portal.svelte.d.ts +17 -0
- package/dist/actions/Portal.svelte.d.ts.map +1 -0
- package/dist/actions/ThemeToggle.svelte +345 -0
- package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
- package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
- package/dist/actions/index.d.ts +13 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +10 -0
- package/dist/actions/scrollbar.d.ts +48 -0
- package/dist/actions/scrollbar.d.ts.map +1 -0
- package/dist/actions/scrollbar.js +404 -0
- package/dist/display/Accordion.svelte +586 -0
- package/dist/display/Accordion.svelte.d.ts +41 -0
- package/dist/display/Accordion.svelte.d.ts.map +1 -0
- package/dist/display/Avatar.svelte +527 -0
- package/dist/display/Avatar.svelte.d.ts +22 -0
- package/dist/display/Avatar.svelte.d.ts.map +1 -0
- package/dist/display/AvatarGroup.svelte +298 -0
- package/dist/display/AvatarGroup.svelte.d.ts +31 -0
- package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
- package/dist/display/Calendar.svelte +1366 -0
- package/dist/display/Calendar.svelte.d.ts +58 -0
- package/dist/display/Calendar.svelte.d.ts.map +1 -0
- package/dist/display/Chart.svelte +1426 -0
- package/dist/display/Chart.svelte.d.ts +35 -0
- package/dist/display/Chart.svelte.d.ts.map +1 -0
- package/dist/display/Code.svelte +780 -0
- package/dist/display/Code.svelte.d.ts +19 -0
- package/dist/display/Code.svelte.d.ts.map +1 -0
- package/dist/display/Comparison.svelte +686 -0
- package/dist/display/Comparison.svelte.d.ts +22 -0
- package/dist/display/Comparison.svelte.d.ts.map +1 -0
- package/dist/display/Counter.svelte +285 -0
- package/dist/display/Counter.svelte.d.ts +21 -0
- package/dist/display/Counter.svelte.d.ts.map +1 -0
- package/dist/display/Expand.svelte +48 -0
- package/dist/display/Expand.svelte.d.ts +9 -0
- package/dist/display/Expand.svelte.d.ts.map +1 -0
- package/dist/display/List.svelte +294 -0
- package/dist/display/List.svelte.d.ts +40 -0
- package/dist/display/List.svelte.d.ts.map +1 -0
- package/dist/display/ListContextReset.svelte +19 -0
- package/dist/display/ListContextReset.svelte.d.ts +7 -0
- package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
- package/dist/display/ListItem.svelte +834 -0
- package/dist/display/ListItem.svelte.d.ts +22 -0
- package/dist/display/ListItem.svelte.d.ts.map +1 -0
- package/dist/display/QR.svelte +1193 -0
- package/dist/display/QR.svelte.d.ts +23 -0
- package/dist/display/QR.svelte.d.ts.map +1 -0
- package/dist/display/SplitPane.svelte +744 -0
- package/dist/display/SplitPane.svelte.d.ts +25 -0
- package/dist/display/SplitPane.svelte.d.ts.map +1 -0
- package/dist/display/Stat.svelte +439 -0
- package/dist/display/Stat.svelte.d.ts +24 -0
- package/dist/display/Stat.svelte.d.ts.map +1 -0
- package/dist/display/Table.svelte +4654 -0
- package/dist/display/Table.svelte.d.ts +249 -0
- package/dist/display/Table.svelte.d.ts.map +1 -0
- package/dist/display/TableCellEditor.svelte +935 -0
- package/dist/display/TableCellEditor.svelte.d.ts +58 -0
- package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
- package/dist/display/Timeline.svelte +1258 -0
- package/dist/display/Timeline.svelte.d.ts +43 -0
- package/dist/display/Timeline.svelte.d.ts.map +1 -0
- package/dist/display/Tree.svelte +1740 -0
- package/dist/display/Tree.svelte.d.ts +74 -0
- package/dist/display/Tree.svelte.d.ts.map +1 -0
- package/dist/display/Typewriter.svelte +338 -0
- package/dist/display/Typewriter.svelte.d.ts +22 -0
- package/dist/display/Typewriter.svelte.d.ts.map +1 -0
- package/dist/display/index.d.ts +24 -0
- package/dist/display/index.d.ts.map +1 -0
- package/dist/display/index.js +18 -0
- package/dist/feedback/Callout.svelte +529 -0
- package/dist/feedback/Callout.svelte.d.ts +24 -0
- package/dist/feedback/Callout.svelte.d.ts.map +1 -0
- package/dist/feedback/Confetti.svelte +631 -0
- package/dist/feedback/Confetti.svelte.d.ts +90 -0
- package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
- package/dist/feedback/Progress.svelte +382 -0
- package/dist/feedback/Progress.svelte.d.ts +25 -0
- package/dist/feedback/Progress.svelte.d.ts.map +1 -0
- package/dist/feedback/Toast.svelte +967 -0
- package/dist/feedback/Toast.svelte.d.ts +54 -0
- package/dist/feedback/Toast.svelte.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +7 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/index.js +4 -0
- package/dist/form/Checkbox.svelte +449 -0
- package/dist/form/Checkbox.svelte.d.ts +27 -0
- package/dist/form/Checkbox.svelte.d.ts.map +1 -0
- package/dist/form/Fieldset.svelte +410 -0
- package/dist/form/Fieldset.svelte.d.ts +22 -0
- package/dist/form/Fieldset.svelte.d.ts.map +1 -0
- package/dist/form/FileUpload.svelte +934 -0
- package/dist/form/FileUpload.svelte.d.ts +41 -0
- package/dist/form/FileUpload.svelte.d.ts.map +1 -0
- package/dist/form/Form.svelte +530 -0
- package/dist/form/Form.svelte.d.ts +120 -0
- package/dist/form/Form.svelte.d.ts.map +1 -0
- package/dist/form/Input.svelte +2858 -0
- package/dist/form/Input.svelte.d.ts +66 -0
- package/dist/form/Input.svelte.d.ts.map +1 -0
- package/dist/form/Radio.svelte +507 -0
- package/dist/form/Radio.svelte.d.ts +39 -0
- package/dist/form/Radio.svelte.d.ts.map +1 -0
- package/dist/form/Range.svelte +912 -0
- package/dist/form/Range.svelte.d.ts +33 -0
- package/dist/form/Range.svelte.d.ts.map +1 -0
- package/dist/form/Rating.svelte +429 -0
- package/dist/form/Rating.svelte.d.ts +28 -0
- package/dist/form/Rating.svelte.d.ts.map +1 -0
- package/dist/form/Select.svelte +1933 -0
- package/dist/form/Select.svelte.d.ts +54 -0
- package/dist/form/Select.svelte.d.ts.map +1 -0
- package/dist/form/Toggle.svelte +645 -0
- package/dist/form/Toggle.svelte.d.ts +50 -0
- package/dist/form/Toggle.svelte.d.ts.map +1 -0
- package/dist/form/index.d.ts +15 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +10 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/layout/README.md +172 -0
- package/dist/media/Carousel.svelte +2424 -0
- package/dist/media/Carousel.svelte.d.ts +47 -0
- package/dist/media/Carousel.svelte.d.ts.map +1 -0
- package/dist/media/Gallery.svelte +2881 -0
- package/dist/media/Gallery.svelte.d.ts +82 -0
- package/dist/media/Gallery.svelte.d.ts.map +1 -0
- package/dist/media/Image.svelte +389 -0
- package/dist/media/Image.svelte.d.ts +33 -0
- package/dist/media/Image.svelte.d.ts.map +1 -0
- package/dist/media/PDF.svelte +1793 -0
- package/dist/media/PDF.svelte.d.ts +44 -0
- package/dist/media/PDF.svelte.d.ts.map +1 -0
- package/dist/media/Panorama.svelte +1391 -0
- package/dist/media/Panorama.svelte.d.ts +47 -0
- package/dist/media/Panorama.svelte.d.ts.map +1 -0
- package/dist/media/Video.svelte +2501 -0
- package/dist/media/Video.svelte.d.ts +58 -0
- package/dist/media/Video.svelte.d.ts.map +1 -0
- package/dist/media/carousel.d.ts +211 -0
- package/dist/media/carousel.d.ts.map +1 -0
- package/dist/media/carousel.js +408 -0
- package/dist/media/index.d.ts +11 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +5 -0
- package/dist/navigation/BottomSheet.svelte +636 -0
- package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
- package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
- package/dist/navigation/Breadcrumbs.svelte +611 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
- package/dist/navigation/Pagination.svelte +641 -0
- package/dist/navigation/Pagination.svelte.d.ts +27 -0
- package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
- package/dist/navigation/Steps.svelte +965 -0
- package/dist/navigation/Steps.svelte.d.ts +43 -0
- package/dist/navigation/Steps.svelte.d.ts.map +1 -0
- package/dist/navigation/Tabs.svelte +698 -0
- package/dist/navigation/Tabs.svelte.d.ts +41 -0
- package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
- package/dist/navigation/index.d.ts +8 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +5 -0
- package/package.json +139 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
code: string;
|
|
3
|
+
language?: string;
|
|
4
|
+
filename?: string;
|
|
5
|
+
show_line_numbers?: boolean;
|
|
6
|
+
show_copy?: boolean;
|
|
7
|
+
start_line?: number;
|
|
8
|
+
highlight_lines?: number[];
|
|
9
|
+
diff?: boolean;
|
|
10
|
+
wrap?: boolean;
|
|
11
|
+
max_height?: string;
|
|
12
|
+
skeleton?: boolean;
|
|
13
|
+
id?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
};
|
|
16
|
+
declare const Code: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
17
|
+
type Code = ReturnType<typeof Code>;
|
|
18
|
+
export default Code;
|
|
19
|
+
//# sourceMappingURL=Code.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Code.svelte.d.ts","sourceRoot":"","sources":["../../src/display/Code.svelte.ts"],"names":[],"mappings":"AAUC,KAAK,gBAAgB,GAAI;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAmYH,QAAA,MAAM,IAAI,sDAAwC,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
|
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
const propId = $props.id();
|
|
3
|
+
let {
|
|
4
|
+
/** The URL of the "before" image */
|
|
5
|
+
before,
|
|
6
|
+
|
|
7
|
+
/** The URL of the "after" image */
|
|
8
|
+
after,
|
|
9
|
+
|
|
10
|
+
/** Alt text for the before image @default 'Before' */
|
|
11
|
+
before_alt = 'Before',
|
|
12
|
+
|
|
13
|
+
/** Alt text for the after image @default 'After' */
|
|
14
|
+
after_alt = 'After',
|
|
15
|
+
|
|
16
|
+
/** The divider position from 0 to 100 (percentage) */
|
|
17
|
+
position = $bindable(50),
|
|
18
|
+
|
|
19
|
+
/** Whether the comparison should be vertical instead of horizontal */
|
|
20
|
+
vertical = false,
|
|
21
|
+
|
|
22
|
+
/** Whether to show "Before" and "After" labels */
|
|
23
|
+
show_labels = true,
|
|
24
|
+
|
|
25
|
+
/** The text for the before label @default 'Before' */
|
|
26
|
+
label_before = 'Before',
|
|
27
|
+
|
|
28
|
+
/** The text for the after label @default 'After' */
|
|
29
|
+
label_after = 'After',
|
|
30
|
+
|
|
31
|
+
/** Whether to show a skeleton loading state */
|
|
32
|
+
skeleton = false,
|
|
33
|
+
|
|
34
|
+
/** Snap points the divider magnetically locks to (percentage values 0-100) */
|
|
35
|
+
snaps = [] as number[],
|
|
36
|
+
|
|
37
|
+
/** The ID of the component @defaults to a random ID */
|
|
38
|
+
id = propId,
|
|
39
|
+
|
|
40
|
+
/** Specifies a custom class name for the container element */
|
|
41
|
+
class: class_name = '',
|
|
42
|
+
|
|
43
|
+
/** Called when the position changes */
|
|
44
|
+
onchange = undefined as ((detail: { position: number }) => void) | undefined,
|
|
45
|
+
}: {
|
|
46
|
+
before: string;
|
|
47
|
+
after: string;
|
|
48
|
+
before_alt?: string;
|
|
49
|
+
after_alt?: string;
|
|
50
|
+
position?: number;
|
|
51
|
+
vertical?: boolean;
|
|
52
|
+
show_labels?: boolean;
|
|
53
|
+
label_before?: string;
|
|
54
|
+
label_after?: string;
|
|
55
|
+
skeleton?: boolean;
|
|
56
|
+
snaps?: number[];
|
|
57
|
+
id?: string;
|
|
58
|
+
class?: string;
|
|
59
|
+
onchange?: (detail: { position: number }) => void;
|
|
60
|
+
} = $props();
|
|
61
|
+
|
|
62
|
+
let container: HTMLElement | undefined = $state(undefined);
|
|
63
|
+
let dragging = $state(false);
|
|
64
|
+
/** Whether the pointer moved during the current press, to distinguish a drag
|
|
65
|
+
* (already positioned live) from a click that should position on release. */
|
|
66
|
+
let pointer_moved = false;
|
|
67
|
+
let overshoot_px = $state(0);
|
|
68
|
+
let last_pointer_coord = 0;
|
|
69
|
+
let force_overflow = $state(false);
|
|
70
|
+
let overflow_timer: ReturnType<typeof setTimeout> | undefined;
|
|
71
|
+
let snapped_to: number | null = null;
|
|
72
|
+
|
|
73
|
+
/** Capture radius (%) — how close the divider must come before a snap grabs
|
|
74
|
+
* it. Small so you can rest near a snap point without being pulled in. */
|
|
75
|
+
const SNAP_RADIUS = 4;
|
|
76
|
+
/** Hold/well radius (%) — once snapped, how far the divider may travel before
|
|
77
|
+
* it breaks free. Kept large relative to SNAP_RADIUS so the magnet feels
|
|
78
|
+
* strong and sticky with a satisfying release spring (hysteresis). Capped
|
|
79
|
+
* per-direction by {@link snapReach} so neighbouring wells never overlap. */
|
|
80
|
+
const SNAP_ESCAPE = 10;
|
|
81
|
+
|
|
82
|
+
/** How far the handle may stray from `snap` toward `dir` (+1 / -1) before it
|
|
83
|
+
* hands off, and the radius of that snap's gravity well in that direction.
|
|
84
|
+
* Capped at the midpoint to the nearest neighbour so adjacent wells meet
|
|
85
|
+
* cleanly at the midpoint instead of overlapping — an overlap lets the next
|
|
86
|
+
* snap grab the handle partway into its well, teleporting the divider. */
|
|
87
|
+
function snapReach(snap: number, dir: number): number {
|
|
88
|
+
let reach = SNAP_ESCAPE;
|
|
89
|
+
for (const other of snaps) {
|
|
90
|
+
if (Math.sign(other - snap) === dir) {
|
|
91
|
+
reach = Math.min(reach, Math.abs(other - snap) / 2);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return reach;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const clampedPosition = $derived(Math.min(100, Math.max(0, position)));
|
|
98
|
+
|
|
99
|
+
/** Clip-path for the "after" image based on orientation and position (includes overshoot) */
|
|
100
|
+
const afterClipPath = $derived(
|
|
101
|
+
vertical
|
|
102
|
+
? `inset(calc(${clampedPosition}% + ${overshoot_px}px) 0 0 0)`
|
|
103
|
+
: `inset(0 0 0 calc(${clampedPosition}% + ${overshoot_px}px))`,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
/** CSS for the divider position */
|
|
107
|
+
const dividerStyle = $derived(
|
|
108
|
+
vertical
|
|
109
|
+
? `top: ${clampedPosition}%; left: 0; right: 0;`
|
|
110
|
+
: `left: ${clampedPosition}%; top: 0; bottom: 0;`,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const needs_visible_overflow = $derived(dragging || force_overflow);
|
|
114
|
+
|
|
115
|
+
function snapPosition(raw: number): number {
|
|
116
|
+
if (snaps.length === 0) return raw;
|
|
117
|
+
|
|
118
|
+
// Hysteresis: once locked to a snap, stay locked to *that* snap until the
|
|
119
|
+
// pointer moves past its (direction-aware) reach. Using snapReach instead
|
|
120
|
+
// of a flat SNAP_ESCAPE keeps the hold zone from overlapping the next
|
|
121
|
+
// snap's capture zone, so escaping one snap hands off to a free zone (or a
|
|
122
|
+
// clean snap-in) rather than teleporting straight into the neighbour.
|
|
123
|
+
if (snapped_to !== null) {
|
|
124
|
+
if (
|
|
125
|
+
Math.abs(raw - snapped_to) <= snapReach(snapped_to, Math.sign(raw - snapped_to))
|
|
126
|
+
) {
|
|
127
|
+
return snapped_to;
|
|
128
|
+
}
|
|
129
|
+
snapped_to = null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Not locked to anything: capture the nearest snap within SNAP_RADIUS
|
|
133
|
+
// (never reaching past the handoff midpoint for tightly-spaced snaps).
|
|
134
|
+
let nearest_snap = -1;
|
|
135
|
+
let min_dist = Infinity;
|
|
136
|
+
|
|
137
|
+
for (const snap of snaps) {
|
|
138
|
+
const dist = Math.abs(raw - snap);
|
|
139
|
+
const capture = Math.min(SNAP_RADIUS, snapReach(snap, Math.sign(raw - snap)));
|
|
140
|
+
if (dist < min_dist && dist <= capture) {
|
|
141
|
+
min_dist = dist;
|
|
142
|
+
nearest_snap = snap;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (nearest_snap >= 0) {
|
|
147
|
+
snapped_to = nearest_snap;
|
|
148
|
+
return nearest_snap;
|
|
149
|
+
}
|
|
150
|
+
return raw;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function updatePosition(clientX: number, clientY: number) {
|
|
154
|
+
if (!container) return;
|
|
155
|
+
const rect = container.getBoundingClientRect();
|
|
156
|
+
let raw: number;
|
|
157
|
+
if (vertical) {
|
|
158
|
+
raw = ((clientY - rect.top) / rect.height) * 100;
|
|
159
|
+
} else {
|
|
160
|
+
raw = ((clientX - rect.left) / rect.width) * 100;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let newPosition: number;
|
|
164
|
+
if (raw < 0) {
|
|
165
|
+
newPosition = 0;
|
|
166
|
+
} else if (raw > 100) {
|
|
167
|
+
newPosition = 100;
|
|
168
|
+
} else {
|
|
169
|
+
newPosition = Math.round(raw * 100) / 100;
|
|
170
|
+
newPosition = snapPosition(newPosition);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (newPosition !== position) {
|
|
174
|
+
position = newPosition;
|
|
175
|
+
onchange?.({ position });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function updateOvershoot() {
|
|
180
|
+
if (!container) return;
|
|
181
|
+
const rect = container.getBoundingClientRect();
|
|
182
|
+
const dimension = vertical ? rect.height : rect.width;
|
|
183
|
+
const raw_pct = vertical
|
|
184
|
+
? (last_pointer_coord - rect.top) / rect.height
|
|
185
|
+
: (last_pointer_coord - rect.left) / rect.width;
|
|
186
|
+
|
|
187
|
+
if (raw_pct < 0 || raw_pct > 1) {
|
|
188
|
+
// Edge rubber band (tanh bounded)
|
|
189
|
+
const overflow_px = (raw_pct < 0 ? raw_pct : raw_pct - 1) * dimension;
|
|
190
|
+
const max_shift = 24;
|
|
191
|
+
overshoot_px = max_shift * Math.tanh(overflow_px / 100);
|
|
192
|
+
} else if (snapped_to !== null) {
|
|
193
|
+
// Magnetic snap gravity — the handle clings to the snap and only
|
|
194
|
+
// reluctantly follows the pointer until the escape boundary, giving
|
|
195
|
+
// an obvious "gravity well" feel before it pops free. The well radius
|
|
196
|
+
// must equal the escape reach (snapReach): at t === 1 the eased curve
|
|
197
|
+
// returns the full radius, so the divider sits exactly under the
|
|
198
|
+
// pointer at the boundary — making the handoff seamless instead of a
|
|
199
|
+
// jump when the snap releases the handle.
|
|
200
|
+
const snapped_pct = snapped_to / 100;
|
|
201
|
+
const pull_px = (raw_pct - snapped_pct) * dimension;
|
|
202
|
+
const well_radius_px =
|
|
203
|
+
(snapReach(snapped_to, Math.sign(raw_pct - snapped_pct)) / 100) * dimension;
|
|
204
|
+
|
|
205
|
+
if (well_radius_px < 1) {
|
|
206
|
+
overshoot_px = 0;
|
|
207
|
+
} else {
|
|
208
|
+
const t = Math.min(1, Math.abs(pull_px) / well_radius_px);
|
|
209
|
+
const gravity = 0.16;
|
|
210
|
+
const eased = gravity * t + (1 - gravity) * t * t;
|
|
211
|
+
overshoot_px = Math.sign(pull_px) * eased * well_radius_px;
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
overshoot_px = 0;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function handlePointerDown(e: PointerEvent) {
|
|
219
|
+
if (skeleton) return;
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
dragging = true;
|
|
222
|
+
pointer_moved = false;
|
|
223
|
+
last_pointer_coord = vertical ? e.clientY : e.clientX;
|
|
224
|
+
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
225
|
+
updatePosition(e.clientX, e.clientY);
|
|
226
|
+
updateOvershoot();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function handlePointerMove(e: PointerEvent) {
|
|
230
|
+
if (!dragging) return;
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
pointer_moved = true;
|
|
233
|
+
updatePosition(e.clientX, e.clientY);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function handlePointerUp(e: PointerEvent) {
|
|
237
|
+
if (!dragging) return;
|
|
238
|
+
dragging = false;
|
|
239
|
+
snapped_to = null;
|
|
240
|
+
if (Math.abs(overshoot_px) > 0.5) {
|
|
241
|
+
force_overflow = true;
|
|
242
|
+
clearTimeout(overflow_timer);
|
|
243
|
+
overflow_timer = setTimeout(() => {
|
|
244
|
+
force_overflow = false;
|
|
245
|
+
}, 400);
|
|
246
|
+
}
|
|
247
|
+
overshoot_px = 0;
|
|
248
|
+
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function handleContainerClick(e: MouseEvent) {
|
|
252
|
+
if (skeleton) return;
|
|
253
|
+
// The browser fires a click after every press+release. When that release
|
|
254
|
+
// concluded a drag, the divider is already positioned (and may have just
|
|
255
|
+
// snapped) — re-running updatePosition here would reset position to the
|
|
256
|
+
// drop point with snap state cleared, leaving the release spring to settle
|
|
257
|
+
// somewhere other than the snap it animated toward. Only handle genuine
|
|
258
|
+
// clicks (no drag movement); presses are already positioned in pointerdown.
|
|
259
|
+
if (pointer_moved) {
|
|
260
|
+
pointer_moved = false;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
updatePosition(e.clientX, e.clientY);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
267
|
+
if (skeleton) return;
|
|
268
|
+
const step = e.shiftKey ? 10 : 1;
|
|
269
|
+
let newPosition = position;
|
|
270
|
+
|
|
271
|
+
switch (e.key) {
|
|
272
|
+
case 'ArrowLeft':
|
|
273
|
+
case 'ArrowUp':
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
newPosition = position - step;
|
|
276
|
+
break;
|
|
277
|
+
case 'ArrowRight':
|
|
278
|
+
case 'ArrowDown':
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
newPosition = position + step;
|
|
281
|
+
break;
|
|
282
|
+
case 'Home':
|
|
283
|
+
e.preventDefault();
|
|
284
|
+
newPosition = 0;
|
|
285
|
+
break;
|
|
286
|
+
case 'End':
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
newPosition = 100;
|
|
289
|
+
break;
|
|
290
|
+
default:
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
newPosition = Math.min(100, Math.max(0, newPosition));
|
|
295
|
+
if (newPosition !== position) {
|
|
296
|
+
position = newPosition;
|
|
297
|
+
onchange?.({ position });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Window-level pointermove for overshoot (works even when pointer is outside container)
|
|
302
|
+
$effect(() => {
|
|
303
|
+
if (!dragging) return;
|
|
304
|
+
|
|
305
|
+
function onMove(e: PointerEvent) {
|
|
306
|
+
last_pointer_coord = vertical ? e.clientY : e.clientX;
|
|
307
|
+
updateOvershoot();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
window.addEventListener('pointermove', onMove);
|
|
311
|
+
return () => window.removeEventListener('pointermove', onMove);
|
|
312
|
+
});
|
|
313
|
+
</script>
|
|
314
|
+
|
|
315
|
+
{#if skeleton}
|
|
316
|
+
<!-- Ghost of the real component: image well + divider/handle + label
|
|
317
|
+
pills, so the user sees "a comparison slider will appear here". -->
|
|
318
|
+
<div
|
|
319
|
+
class={['comparison', 'skeleton', class_name].filter(Boolean).join(' ')}
|
|
320
|
+
class:vertical
|
|
321
|
+
{id}
|
|
322
|
+
aria-hidden="true">
|
|
323
|
+
<div class="skeleton-inner">
|
|
324
|
+
{#if show_labels}
|
|
325
|
+
<span class="skeleton-label label-before"></span>
|
|
326
|
+
<span class="skeleton-label label-after"></span>
|
|
327
|
+
{/if}
|
|
328
|
+
<div class="skeleton-divider">
|
|
329
|
+
<div class="skeleton-handle"></div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
{:else}
|
|
334
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
335
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
336
|
+
<div
|
|
337
|
+
class={['comparison', class_name].filter(Boolean).join(' ')}
|
|
338
|
+
class:vertical
|
|
339
|
+
class:dragging
|
|
340
|
+
class:overflowing={needs_visible_overflow}
|
|
341
|
+
{id}
|
|
342
|
+
bind:this={container}
|
|
343
|
+
onclick={handleContainerClick}
|
|
344
|
+
onpointerdown={handlePointerDown}
|
|
345
|
+
onpointermove={handlePointerMove}
|
|
346
|
+
onpointerup={handlePointerUp}
|
|
347
|
+
onpointercancel={handlePointerUp}>
|
|
348
|
+
<img class="before" src={before} alt={before_alt} draggable="false" />
|
|
349
|
+
|
|
350
|
+
<img
|
|
351
|
+
class="after"
|
|
352
|
+
src={after}
|
|
353
|
+
alt={after_alt}
|
|
354
|
+
draggable="false"
|
|
355
|
+
style:clip-path={afterClipPath} />
|
|
356
|
+
|
|
357
|
+
{#if show_labels}
|
|
358
|
+
<span class="label label-before">{label_before}</span>
|
|
359
|
+
<span class="label label-after">{label_after}</span>
|
|
360
|
+
{/if}
|
|
361
|
+
|
|
362
|
+
<div
|
|
363
|
+
class="divider"
|
|
364
|
+
class:vertical
|
|
365
|
+
style={dividerStyle}
|
|
366
|
+
style:--divider-overshoot="{overshoot_px}px">
|
|
367
|
+
<div
|
|
368
|
+
class="handle"
|
|
369
|
+
role="slider"
|
|
370
|
+
tabindex="0"
|
|
371
|
+
aria-valuenow={Math.round(clampedPosition)}
|
|
372
|
+
aria-valuemin={0}
|
|
373
|
+
aria-valuemax={100}
|
|
374
|
+
aria-label="Comparison slider"
|
|
375
|
+
onkeydown={handleKeyDown}>
|
|
376
|
+
{#if vertical}
|
|
377
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
378
|
+
<path d="M12 4l-4 4h8zM12 20l-4-4h8z" fill="currentColor" />
|
|
379
|
+
</svg>
|
|
380
|
+
{:else}
|
|
381
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
382
|
+
<path d="M4 12l4-4v8zM20 12l-4-4v8z" fill="currentColor" />
|
|
383
|
+
</svg>
|
|
384
|
+
{/if}
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
{/if}
|
|
389
|
+
|
|
390
|
+
<style>
|
|
391
|
+
.comparison {
|
|
392
|
+
--handle-size: 40px;
|
|
393
|
+
--handle-color: var(--cmp-handle, #fff);
|
|
394
|
+
--handle-shadow: 0 0 6px rgba(0, 0, 0, 0.3), 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
395
|
+
--divider-color: var(--cmp-divider, #fff);
|
|
396
|
+
--divider-width: var(--width-divider, 2px);
|
|
397
|
+
--label-bg: var(--cmp-label-bg, rgba(0, 0, 0, 0.55));
|
|
398
|
+
--label-color: var(--cmp-label-text, #fff);
|
|
399
|
+
--label-padding: var(--padding-label, 4px 10px);
|
|
400
|
+
--label-radius: var(--radius-md, 4px);
|
|
401
|
+
--label-font-size: var(--text-sm, 0.8125rem);
|
|
402
|
+
/* Rounded media corners — override with --cmp-radius (0 to disable). Clamped
|
|
403
|
+
so an over-rounded radius can't blob this large surface — see --radius-cap. */
|
|
404
|
+
--_radius: min(var(--cmp-radius, var(--radius-xl, 20px)), var(--radius-cap, 40px));
|
|
405
|
+
|
|
406
|
+
position: relative;
|
|
407
|
+
overflow: hidden;
|
|
408
|
+
border-radius: var(--_radius);
|
|
409
|
+
@supports (corner-shape: squircle) {
|
|
410
|
+
corner-shape: squircle;
|
|
411
|
+
border-radius: calc(var(--_radius) * var(--squircle-ratio, 2));
|
|
412
|
+
}
|
|
413
|
+
touch-action: none;
|
|
414
|
+
user-select: none;
|
|
415
|
+
-webkit-user-select: none;
|
|
416
|
+
cursor: ew-resize;
|
|
417
|
+
|
|
418
|
+
&.vertical {
|
|
419
|
+
cursor: ns-resize;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
&.dragging {
|
|
423
|
+
cursor: grabbing;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
&.overflowing {
|
|
427
|
+
overflow: visible;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
&.skeleton {
|
|
431
|
+
cursor: default;
|
|
432
|
+
touch-action: auto;
|
|
433
|
+
user-select: auto;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* The real component's height comes from the image, which is unknowable
|
|
438
|
+
while loading — default to a 16/9 media well (override with
|
|
439
|
+
--cmp-aspect to match the real images and avoid any layout shift). */
|
|
440
|
+
.skeleton-inner {
|
|
441
|
+
width: 100%;
|
|
442
|
+
min-height: 200px;
|
|
443
|
+
aspect-ratio: var(--cmp-aspect, 16 / 9);
|
|
444
|
+
border-radius: var(--_radius);
|
|
445
|
+
@supports (corner-shape: squircle) {
|
|
446
|
+
corner-shape: squircle;
|
|
447
|
+
border-radius: calc(var(--_radius) * var(--squircle-ratio, 2));
|
|
448
|
+
}
|
|
449
|
+
background-color: var(--skeleton-bg, rgb(from var(--color-text, #888) r g b / 0.1));
|
|
450
|
+
position: relative;
|
|
451
|
+
overflow: hidden;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.skeleton-inner::after {
|
|
455
|
+
content: '';
|
|
456
|
+
position: absolute;
|
|
457
|
+
inset: 0;
|
|
458
|
+
z-index: 1;
|
|
459
|
+
transform: translateX(-100%);
|
|
460
|
+
background-image: linear-gradient(
|
|
461
|
+
105deg,
|
|
462
|
+
transparent 25%,
|
|
463
|
+
var(--skeleton-sheen, rgb(from var(--color-text, #888) r g b / 0.12)) 50%,
|
|
464
|
+
transparent 75%
|
|
465
|
+
);
|
|
466
|
+
animation: delight-skeleton-shimmer var(--skeleton-duration, 2.4s) ease-in-out
|
|
467
|
+
infinite;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
@keyframes -global-delight-skeleton-shimmer {
|
|
471
|
+
0% {
|
|
472
|
+
transform: translateX(-100%);
|
|
473
|
+
}
|
|
474
|
+
55%,
|
|
475
|
+
100% {
|
|
476
|
+
transform: translateX(100%);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/* Ghost divider + handle at the resting snap position. The handle
|
|
481
|
+
breathes gently so the placeholder feels alive even between sweeps. */
|
|
482
|
+
.skeleton-divider {
|
|
483
|
+
position: absolute;
|
|
484
|
+
top: 0;
|
|
485
|
+
bottom: 0;
|
|
486
|
+
left: 50%;
|
|
487
|
+
width: var(--divider-width);
|
|
488
|
+
transform: translateX(-50%);
|
|
489
|
+
background: var(--divider-color);
|
|
490
|
+
opacity: 0.55;
|
|
491
|
+
|
|
492
|
+
.vertical & {
|
|
493
|
+
top: 50%;
|
|
494
|
+
bottom: auto;
|
|
495
|
+
left: 0;
|
|
496
|
+
right: 0;
|
|
497
|
+
width: auto;
|
|
498
|
+
height: var(--divider-width);
|
|
499
|
+
transform: translateY(-50%);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.skeleton-handle {
|
|
504
|
+
position: absolute;
|
|
505
|
+
top: 50%;
|
|
506
|
+
left: 50%;
|
|
507
|
+
translate: -50% -50%;
|
|
508
|
+
width: var(--handle-size);
|
|
509
|
+
height: var(--handle-size);
|
|
510
|
+
border-radius: var(--radius-full, 9999px);
|
|
511
|
+
background: var(--handle-color);
|
|
512
|
+
box-shadow: var(--handle-shadow);
|
|
513
|
+
opacity: 0.75;
|
|
514
|
+
animation: comparison-handle-breathe 2.4s ease-in-out infinite;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
@keyframes comparison-handle-breathe {
|
|
518
|
+
50% {
|
|
519
|
+
scale: 1.08;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/* Ghost label pills in the real labels' corners */
|
|
524
|
+
.skeleton-label {
|
|
525
|
+
position: absolute;
|
|
526
|
+
z-index: 2;
|
|
527
|
+
width: 4.5em;
|
|
528
|
+
height: calc(1em + 8px);
|
|
529
|
+
font-size: var(--label-font-size);
|
|
530
|
+
border-radius: var(--label-radius);
|
|
531
|
+
background: var(--label-bg);
|
|
532
|
+
opacity: 0.45;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
@media (prefers-reduced-motion: reduce) {
|
|
536
|
+
.skeleton-inner::after,
|
|
537
|
+
.skeleton-handle {
|
|
538
|
+
animation: none;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
img {
|
|
543
|
+
display: block;
|
|
544
|
+
width: 100%;
|
|
545
|
+
height: 100%;
|
|
546
|
+
object-fit: cover;
|
|
547
|
+
/* The container clips to the rounded corners except while the divider
|
|
548
|
+
overshoots (overflow: visible) — round the images too so the corners
|
|
549
|
+
hold up during that drag. */
|
|
550
|
+
border-radius: var(--_radius);
|
|
551
|
+
@supports (corner-shape: squircle) {
|
|
552
|
+
corner-shape: squircle;
|
|
553
|
+
border-radius: calc(var(--_radius) * var(--squircle-ratio, 2));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
&.before {
|
|
557
|
+
position: relative;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
&.after {
|
|
561
|
+
position: absolute;
|
|
562
|
+
inset: 0;
|
|
563
|
+
transition: clip-path 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
564
|
+
|
|
565
|
+
.dragging & {
|
|
566
|
+
transition: none;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.label {
|
|
572
|
+
position: absolute;
|
|
573
|
+
padding: var(--label-padding);
|
|
574
|
+
background: var(--label-bg);
|
|
575
|
+
color: var(--label-color);
|
|
576
|
+
font-size: var(--label-font-size);
|
|
577
|
+
font-weight: 500;
|
|
578
|
+
border-radius: var(--label-radius);
|
|
579
|
+
@supports (corner-shape: squircle) {
|
|
580
|
+
corner-shape: squircle;
|
|
581
|
+
border-radius: calc(var(--label-radius) * var(--squircle-ratio, 2));
|
|
582
|
+
}
|
|
583
|
+
pointer-events: none;
|
|
584
|
+
z-index: 2;
|
|
585
|
+
line-height: 1;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.label-before {
|
|
589
|
+
top: 12px;
|
|
590
|
+
left: 12px;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.label-after {
|
|
594
|
+
bottom: 12px;
|
|
595
|
+
right: 12px;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.divider {
|
|
599
|
+
position: absolute;
|
|
600
|
+
z-index: 3;
|
|
601
|
+
display: flex;
|
|
602
|
+
align-items: center;
|
|
603
|
+
justify-content: center;
|
|
604
|
+
pointer-events: none;
|
|
605
|
+
transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
606
|
+
|
|
607
|
+
&::before {
|
|
608
|
+
content: '';
|
|
609
|
+
position: absolute;
|
|
610
|
+
background: var(--divider-color);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
&:not(.vertical) {
|
|
614
|
+
width: 0;
|
|
615
|
+
transform: translateX(var(--divider-overshoot, 0px));
|
|
616
|
+
&::before {
|
|
617
|
+
width: var(--divider-width);
|
|
618
|
+
top: 0;
|
|
619
|
+
bottom: 0;
|
|
620
|
+
left: calc(var(--divider-width) / -2);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
&.vertical {
|
|
625
|
+
height: 0;
|
|
626
|
+
transform: translateY(var(--divider-overshoot, 0px));
|
|
627
|
+
&::before {
|
|
628
|
+
height: var(--divider-width);
|
|
629
|
+
left: 0;
|
|
630
|
+
right: 0;
|
|
631
|
+
top: calc(var(--divider-width) / -2);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.dragging & {
|
|
636
|
+
transition: none;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.handle {
|
|
641
|
+
position: relative;
|
|
642
|
+
width: var(--handle-size);
|
|
643
|
+
height: var(--handle-size);
|
|
644
|
+
border-radius: 50%;
|
|
645
|
+
background: color-mix(in oklch, var(--handle-color) 55%, transparent);
|
|
646
|
+
box-shadow: var(--handle-shadow);
|
|
647
|
+
display: flex;
|
|
648
|
+
align-items: center;
|
|
649
|
+
justify-content: center;
|
|
650
|
+
pointer-events: auto;
|
|
651
|
+
cursor: grab;
|
|
652
|
+
flex-shrink: 0;
|
|
653
|
+
z-index: 1;
|
|
654
|
+
outline: none;
|
|
655
|
+
backdrop-filter: blur(10px) saturate(140%);
|
|
656
|
+
-webkit-backdrop-filter: blur(10px) saturate(140%);
|
|
657
|
+
border: 1px solid rgba(255, 255, 255, 0.5);
|
|
658
|
+
transition:
|
|
659
|
+
box-shadow 150ms ease,
|
|
660
|
+
background 150ms ease;
|
|
661
|
+
|
|
662
|
+
&:hover {
|
|
663
|
+
background: color-mix(in oklch, var(--handle-color) 70%, transparent);
|
|
664
|
+
/* Snap the tint in on hover; the base rule eases it back out on leave. */
|
|
665
|
+
transition: box-shadow 150ms ease;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
&:focus-visible {
|
|
669
|
+
box-shadow:
|
|
670
|
+
var(--handle-shadow),
|
|
671
|
+
0 0 0 3px rgba(59, 130, 246, 0.5);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.dragging & {
|
|
675
|
+
cursor: grabbing;
|
|
676
|
+
background: color-mix(in oklch, var(--handle-color) 80%, transparent);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
svg {
|
|
680
|
+
width: 20px;
|
|
681
|
+
height: 20px;
|
|
682
|
+
color: rgba(0, 0, 0, 0.75);
|
|
683
|
+
pointer-events: none;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
</style>
|