@inglorious/ui 6.0.0 → 6.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/package.json +20 -10
- package/src/controls/controls.css +1 -1
- package/src/data-display/before-after/before-after.stories.js +74 -0
- package/src/data-display/before-after/handlers.js +44 -0
- package/src/data-display/before-after/helpers.js +4 -0
- package/src/data-display/before-after/index.js +17 -0
- package/src/data-display/before-after/style.css +128 -0
- package/src/data-display/before-after/template.js +225 -0
- package/src/data-display/carousel/carousel.stories.js +166 -0
- package/src/data-display/carousel/handlers.js +78 -0
- package/src/data-display/carousel/helpers.js +54 -0
- package/src/data-display/carousel/index.js +17 -0
- package/src/data-display/carousel/style.css +233 -0
- package/src/data-display/carousel/template.js +655 -0
- package/src/data-display/data-display.css +2 -0
- package/src/examples/dashboard/app-drawer.js +6 -0
- package/src/examples/dashboard/before-after-data.js +29 -0
- package/src/examples/dashboard/primitive-content.js +151 -33
- package/src/index.js +2 -0
- package/src/layout/flex/flex.stories.js +49 -0
- package/src/layout/flex/style.css +59 -0
- package/src/layout/flex/template.js +2 -0
- package/types/data-display/before-after.d.ts +39 -0
- package/types/data-display/carousel.d.ts +72 -0
- package/types/layout/flex.d.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/ui",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "A design system for Inglorious Web with render-function components and customizable themes",
|
|
5
5
|
"author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
"./themes/inglorious.css": "./src/styles/themes/inglorious.css",
|
|
39
39
|
"./themes/material.css": "./src/styles/themes/material.css",
|
|
40
40
|
"./themes/bootstrap.css": "./src/styles/themes/bootstrap.css",
|
|
41
|
+
"./before-after": {
|
|
42
|
+
"types": "./types/data-display/before-after.d.ts",
|
|
43
|
+
"import": "./src/data-display/before-after/index.js"
|
|
44
|
+
},
|
|
45
|
+
"./before-after.css": "./src/data-display/before-after/style.css",
|
|
41
46
|
"./button": {
|
|
42
47
|
"types": "./types/controls/button.d.ts",
|
|
43
48
|
"import": "./src/controls/button/index.js"
|
|
@@ -48,6 +53,11 @@
|
|
|
48
53
|
"import": "./src/controls/button-group/index.js"
|
|
49
54
|
},
|
|
50
55
|
"./button-group.css": "./src/controls/button-group/style.css",
|
|
56
|
+
"./carousel": {
|
|
57
|
+
"types": "./types/data-display/carousel.d.ts",
|
|
58
|
+
"import": "./src/data-display/carousel/index.js"
|
|
59
|
+
},
|
|
60
|
+
"./carousel.css": "./src/data-display/carousel/style.css",
|
|
51
61
|
"./checkbox": {
|
|
52
62
|
"types": "./types/controls/checkbox.d.ts",
|
|
53
63
|
"import": "./src/controls/checkbox/index.js"
|
|
@@ -280,28 +290,28 @@
|
|
|
280
290
|
"**/*.css"
|
|
281
291
|
],
|
|
282
292
|
"peerDependencies": {
|
|
283
|
-
"@inglorious/
|
|
293
|
+
"@inglorious/store": "11.0.0",
|
|
284
294
|
"@inglorious/utils": "3.8.0",
|
|
285
|
-
"@inglorious/
|
|
286
|
-
"@inglorious/
|
|
295
|
+
"@inglorious/charts": "9.0.0",
|
|
296
|
+
"@inglorious/web": "6.3.0"
|
|
287
297
|
},
|
|
288
298
|
"devDependencies": {
|
|
289
299
|
"@fontsource/material-symbols-outlined": "^5.2.36",
|
|
290
300
|
"@fortawesome/fontawesome-free": "^7.2.0",
|
|
291
|
-
"@storybook/addon-docs": "^10.
|
|
292
|
-
"@storybook/web-components-vite": "^10.
|
|
301
|
+
"@storybook/addon-docs": "^10.5.6",
|
|
302
|
+
"@storybook/web-components-vite": "^10.5.6",
|
|
293
303
|
"happy-dom": "^15.11.0",
|
|
294
304
|
"jsdom": "^25.0.0",
|
|
295
305
|
"lucide": "^0.575.0",
|
|
296
306
|
"prettier": "^3.6.2",
|
|
297
|
-
"storybook": "^10.
|
|
307
|
+
"storybook": "^10.5.6",
|
|
298
308
|
"vite": "^7.3.1",
|
|
299
309
|
"vitest": "^4.0.15",
|
|
300
|
-
"@inglorious/
|
|
310
|
+
"@inglorious/eslint-config": "1.1.2",
|
|
301
311
|
"@inglorious/vite-plugin-vue": "1.0.1",
|
|
312
|
+
"@inglorious/vite-plugin-jsx": "2.0.0",
|
|
302
313
|
"@inglorious/store": "11.0.0",
|
|
303
|
-
"@inglorious/web": "6.3.0"
|
|
304
|
-
"@inglorious/eslint-config": "1.1.2"
|
|
314
|
+
"@inglorious/web": "6.3.0"
|
|
305
315
|
},
|
|
306
316
|
"engines": {
|
|
307
317
|
"node": ">= 22"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
@import "./combobox/style.css";
|
|
2
1
|
@import "./button/style.css";
|
|
3
2
|
@import "./button-group/style.css";
|
|
4
3
|
@import "./checkbox/style.css";
|
|
4
|
+
@import "./combobox/style.css";
|
|
5
5
|
@import "./fab/style.css";
|
|
6
6
|
@import "./icon-button/style.css";
|
|
7
7
|
@import "./input/style.css";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEntityRender,
|
|
3
|
+
notifyActionArgType,
|
|
4
|
+
} from "../../stories/notifyStory.js"
|
|
5
|
+
import { BeforeAfter } from "./index.js"
|
|
6
|
+
|
|
7
|
+
const before = {
|
|
8
|
+
src: "https://picsum.photos/id/1015/800/500?grayscale",
|
|
9
|
+
alt: "Before",
|
|
10
|
+
}
|
|
11
|
+
const after = {
|
|
12
|
+
src: "https://picsum.photos/id/1015/800/500",
|
|
13
|
+
alt: "After",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
title: "Data Display/BeforeAfter",
|
|
18
|
+
tags: ["autodocs"],
|
|
19
|
+
render: createEntityRender({ BeforeAfter }),
|
|
20
|
+
argTypes: {
|
|
21
|
+
...notifyActionArgType,
|
|
22
|
+
label: { control: "text", description: "Accessible label for the slider." },
|
|
23
|
+
position: {
|
|
24
|
+
control: { type: "range", min: 0, max: 100, step: 1 },
|
|
25
|
+
description: "Divider position as a percentage (0-100).",
|
|
26
|
+
},
|
|
27
|
+
step: {
|
|
28
|
+
control: "number",
|
|
29
|
+
description: "Keyboard increment (percent) for arrow keys.",
|
|
30
|
+
},
|
|
31
|
+
isDisabled: { control: "boolean", description: "Disable interaction." },
|
|
32
|
+
isFullWidth: {
|
|
33
|
+
control: "boolean",
|
|
34
|
+
description: "Expand to container width.",
|
|
35
|
+
},
|
|
36
|
+
before: { control: "object", description: "Before image { src, alt }." },
|
|
37
|
+
after: { control: "object", description: "After image { src, alt }." },
|
|
38
|
+
},
|
|
39
|
+
parameters: {
|
|
40
|
+
docs: {
|
|
41
|
+
description: {
|
|
42
|
+
component:
|
|
43
|
+
"Before/after image comparison slider. The reveal is pure CSS (clip-path); the divider position is the only piece of entity state. Drag anywhere, or focus and use arrow keys / Home / End.",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const Default = {}
|
|
50
|
+
Default.args = {
|
|
51
|
+
id: "before-after",
|
|
52
|
+
type: "BeforeAfter",
|
|
53
|
+
label: "Before and after comparison",
|
|
54
|
+
position: 50,
|
|
55
|
+
step: 1,
|
|
56
|
+
isDisabled: false,
|
|
57
|
+
isFullWidth: true,
|
|
58
|
+
before,
|
|
59
|
+
after,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const StartLeft = {}
|
|
63
|
+
StartLeft.args = {
|
|
64
|
+
...Default.args,
|
|
65
|
+
id: "before-after-left",
|
|
66
|
+
position: 15,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const Disabled = {}
|
|
70
|
+
Disabled.args = {
|
|
71
|
+
...Default.args,
|
|
72
|
+
id: "before-after-disabled",
|
|
73
|
+
isDisabled: true,
|
|
74
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../../../types/data-display/before-after.js').BeforeAfterProps} BeforeAfterEntity
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { clamp } from "@inglorious/utils/math/numbers.js"
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_POSITION,
|
|
9
|
+
DEFAULT_STEP,
|
|
10
|
+
MAX_POSITION,
|
|
11
|
+
MIN_POSITION,
|
|
12
|
+
} from "./helpers.js"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Initialize missing before/after state defaults.
|
|
16
|
+
* The only persistent piece of state is `position`: the divider location as a
|
|
17
|
+
* percentage (0-100). Everything else is configuration.
|
|
18
|
+
* @param {BeforeAfterEntity} entity
|
|
19
|
+
*/
|
|
20
|
+
export function create(entity) {
|
|
21
|
+
entity.position ??= DEFAULT_POSITION
|
|
22
|
+
entity.step ??= DEFAULT_STEP
|
|
23
|
+
|
|
24
|
+
entity.before ??= null
|
|
25
|
+
entity.after ??= null
|
|
26
|
+
|
|
27
|
+
entity.label ??= ""
|
|
28
|
+
entity.isDisabled ??= false
|
|
29
|
+
entity.isFullWidth ??= false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Move the divider to a new position, clamped to the 0-100 range.
|
|
34
|
+
* @param {BeforeAfterEntity} entity
|
|
35
|
+
* @param {number} position
|
|
36
|
+
*/
|
|
37
|
+
export function positionChange(entity, position) {
|
|
38
|
+
if (entity.isDisabled) return
|
|
39
|
+
|
|
40
|
+
const value = Number(position)
|
|
41
|
+
if (Number.isNaN(value)) return
|
|
42
|
+
|
|
43
|
+
entity.position = clamp(value, MIN_POSITION, MAX_POSITION)
|
|
44
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as handlers from "./handlers.js"
|
|
2
|
+
import { BeforeAfter as renderers } from "./template.js"
|
|
3
|
+
|
|
4
|
+
export const BeforeAfter = {
|
|
5
|
+
...handlers,
|
|
6
|
+
...renderers,
|
|
7
|
+
render(entity, api) {
|
|
8
|
+
const id = entity.id
|
|
9
|
+
const props = {
|
|
10
|
+
...entity,
|
|
11
|
+
onPositionChange: (position) =>
|
|
12
|
+
api.notify(`#${id}:positionChange`, position),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return this.renderBeforeAfter(props)
|
|
16
|
+
},
|
|
17
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
.iw-before-after {
|
|
2
|
+
--iw-before-after-position: 50%;
|
|
3
|
+
|
|
4
|
+
/* Neutral in every theme on purpose: the divider should separate the two
|
|
5
|
+
pictures without competing with them for attention. */
|
|
6
|
+
--iw-before-after-divider-width: 2px;
|
|
7
|
+
--iw-before-after-divider-color: rgb(255 255 255 / 90%);
|
|
8
|
+
--iw-before-after-handle-size: 40px;
|
|
9
|
+
--iw-before-after-handle-bg: rgb(255 255 255 / 90%);
|
|
10
|
+
--iw-before-after-handle-color: rgb(17 24 39 / 65%);
|
|
11
|
+
--iw-before-after-handle-arrow: 5px;
|
|
12
|
+
|
|
13
|
+
position: relative;
|
|
14
|
+
display: inline-block;
|
|
15
|
+
max-width: 100%;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
line-height: 0;
|
|
18
|
+
cursor: ew-resize;
|
|
19
|
+
touch-action: none;
|
|
20
|
+
user-select: none;
|
|
21
|
+
-webkit-user-select: none;
|
|
22
|
+
outline: none;
|
|
23
|
+
|
|
24
|
+
&:focus-visible {
|
|
25
|
+
outline: 2px solid var(--iw-color-primary);
|
|
26
|
+
outline-offset: 2px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&.iw-before-after-full-width {
|
|
30
|
+
display: block;
|
|
31
|
+
width: 100%;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Greyed out and dimmed, so it reads as "you cannot move this". */
|
|
35
|
+
&.iw-before-after-disabled {
|
|
36
|
+
--iw-before-after-divider-color: rgb(156 163 175 / 60%);
|
|
37
|
+
--iw-before-after-handle-bg: rgb(156 163 175 / 60%);
|
|
38
|
+
--iw-before-after-handle-color: rgb(255 255 255 / 60%);
|
|
39
|
+
|
|
40
|
+
cursor: default;
|
|
41
|
+
|
|
42
|
+
.iw-before-after-image {
|
|
43
|
+
filter: grayscale(70%);
|
|
44
|
+
opacity: 0.75;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.iw-before-after-handle {
|
|
48
|
+
box-shadow: none;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.iw-before-after-before {
|
|
53
|
+
display: block;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.iw-before-after-before .iw-before-after-image {
|
|
57
|
+
width: 100%;
|
|
58
|
+
height: auto;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.iw-before-after-after {
|
|
62
|
+
position: absolute;
|
|
63
|
+
inset: 0;
|
|
64
|
+
clip-path: inset(0 0 0 var(--iw-before-after-position));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.iw-before-after-after .iw-before-after-image {
|
|
68
|
+
width: 100%;
|
|
69
|
+
height: 100%;
|
|
70
|
+
object-fit: cover;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.iw-before-after-image {
|
|
74
|
+
display: block;
|
|
75
|
+
pointer-events: none;
|
|
76
|
+
user-select: none;
|
|
77
|
+
-webkit-user-drag: none;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.iw-before-after-divider {
|
|
81
|
+
position: absolute;
|
|
82
|
+
inset-block: 0;
|
|
83
|
+
left: var(--iw-before-after-position);
|
|
84
|
+
width: var(--iw-before-after-divider-width);
|
|
85
|
+
transform: translateX(-50%);
|
|
86
|
+
background: var(--iw-before-after-divider-color);
|
|
87
|
+
box-shadow: 0 0 0 1px rgb(0 0 0 / 12%);
|
|
88
|
+
pointer-events: none;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.iw-before-after-handle {
|
|
92
|
+
position: absolute;
|
|
93
|
+
top: 50%;
|
|
94
|
+
left: 50%;
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
box-sizing: border-box;
|
|
99
|
+
width: var(--iw-before-after-handle-size);
|
|
100
|
+
height: var(--iw-before-after-handle-size);
|
|
101
|
+
border-radius: var(--iw-radius-full);
|
|
102
|
+
background: var(--iw-before-after-handle-bg);
|
|
103
|
+
transform: translate(-50%, -50%);
|
|
104
|
+
box-shadow: var(--iw-shadow-md);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Two chevron arrows drawn with borders, no icon font needed. */
|
|
108
|
+
.iw-before-after-handle::before,
|
|
109
|
+
.iw-before-after-handle::after {
|
|
110
|
+
content: "";
|
|
111
|
+
width: 0;
|
|
112
|
+
height: 0;
|
|
113
|
+
border-top: var(--iw-before-after-handle-arrow) solid transparent;
|
|
114
|
+
border-bottom: var(--iw-before-after-handle-arrow) solid transparent;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.iw-before-after-handle::before {
|
|
118
|
+
margin-right: var(--iw-before-after-handle-arrow);
|
|
119
|
+
border-right: var(--iw-before-after-handle-arrow) solid
|
|
120
|
+
var(--iw-before-after-handle-color);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.iw-before-after-handle::after {
|
|
124
|
+
margin-left: var(--iw-before-after-handle-arrow);
|
|
125
|
+
border-left: var(--iw-before-after-handle-arrow) solid
|
|
126
|
+
var(--iw-before-after-handle-color);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../../../types/data-display/before-after.js').BeforeAfterProps} BeforeAfterProps
|
|
3
|
+
* @typedef {import('@inglorious/web').TemplateResult} TemplateResult
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { clamp } from "@inglorious/utils/math/numbers.js"
|
|
7
|
+
import { html } from "@inglorious/web"
|
|
8
|
+
import { classMap } from "@inglorious/web/directives/class-map"
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_POSITION,
|
|
12
|
+
DEFAULT_STEP,
|
|
13
|
+
MAX_POSITION,
|
|
14
|
+
MIN_POSITION,
|
|
15
|
+
} from "./helpers.js"
|
|
16
|
+
|
|
17
|
+
const PRIMARY_BUTTON = 1
|
|
18
|
+
|
|
19
|
+
export const BeforeAfter = {
|
|
20
|
+
/**
|
|
21
|
+
* Main entrypoint for the before/after template. It delegates to the base renderer so overrides can still reuse it.
|
|
22
|
+
* Before/after overlays two images and reveals the top one with a draggable divider.
|
|
23
|
+
* @param {BeforeAfterProps} props The before/after props.
|
|
24
|
+
* @returns {TemplateResult} The rendered control.
|
|
25
|
+
*/
|
|
26
|
+
render(props) {
|
|
27
|
+
return this.renderBeforeAfter(props)
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Renders the container and wires the pointer and keyboard interactions.
|
|
32
|
+
* This is the canonical renderer that custom `render()` overrides should call.
|
|
33
|
+
* @param {BeforeAfterProps} props The before/after props.
|
|
34
|
+
* @returns {TemplateResult} The rendered control.
|
|
35
|
+
*/
|
|
36
|
+
renderBeforeAfter(props) {
|
|
37
|
+
const {
|
|
38
|
+
label,
|
|
39
|
+
position = DEFAULT_POSITION,
|
|
40
|
+
isDisabled = false,
|
|
41
|
+
isFullWidth = false,
|
|
42
|
+
} = props
|
|
43
|
+
|
|
44
|
+
const classes = {
|
|
45
|
+
"iw-before-after": true,
|
|
46
|
+
"iw-before-after-full-width": isFullWidth,
|
|
47
|
+
"iw-before-after-disabled": isDisabled,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return html`<div
|
|
51
|
+
class=${classMap(classes)}
|
|
52
|
+
style="--iw-before-after-position: ${position}%;"
|
|
53
|
+
role="slider"
|
|
54
|
+
tabindex=${isDisabled ? -1 : 0}
|
|
55
|
+
aria-label=${label || "Before and after comparison"}
|
|
56
|
+
aria-orientation="horizontal"
|
|
57
|
+
aria-valuemin=${MIN_POSITION}
|
|
58
|
+
aria-valuemax=${MAX_POSITION}
|
|
59
|
+
aria-valuenow=${Math.round(position)}
|
|
60
|
+
aria-disabled=${isDisabled}
|
|
61
|
+
@pointerdown=${handlePointerDown(props)}
|
|
62
|
+
@pointermove=${handlePointerMove(props)}
|
|
63
|
+
@pointerup=${handlePointerUp}
|
|
64
|
+
@pointercancel=${handlePointerUp}
|
|
65
|
+
@keydown=${handleKeyDown(props)}
|
|
66
|
+
>
|
|
67
|
+
${this.renderBefore?.(props)} ${this.renderAfter?.(props)}
|
|
68
|
+
${this.renderDivider?.(props)}
|
|
69
|
+
</div>`
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Renders the bottom layer, fully visible underneath the clipped one.
|
|
74
|
+
* It also gives the control its intrinsic size.
|
|
75
|
+
* @param {BeforeAfterProps} props The before/after props.
|
|
76
|
+
* @returns {TemplateResult} The rendered layer.
|
|
77
|
+
*/
|
|
78
|
+
renderBefore(props) {
|
|
79
|
+
return html`<div class="iw-before-after-item iw-before-after-before">
|
|
80
|
+
<img
|
|
81
|
+
class="iw-before-after-image"
|
|
82
|
+
src=${props.before?.src ?? ""}
|
|
83
|
+
alt=${props.before?.alt ?? ""}
|
|
84
|
+
draggable="false"
|
|
85
|
+
/>
|
|
86
|
+
</div>`
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Renders the top layer, clipped to the current divider position through CSS.
|
|
91
|
+
* @param {BeforeAfterProps} props The before/after props.
|
|
92
|
+
* @returns {TemplateResult} The rendered layer.
|
|
93
|
+
*/
|
|
94
|
+
renderAfter(props) {
|
|
95
|
+
return html`<div class="iw-before-after-item iw-before-after-after">
|
|
96
|
+
<img
|
|
97
|
+
class="iw-before-after-image"
|
|
98
|
+
src=${props.after?.src ?? ""}
|
|
99
|
+
alt=${props.after?.alt ?? ""}
|
|
100
|
+
draggable="false"
|
|
101
|
+
/>
|
|
102
|
+
</div>`
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Renders the vertical bar sitting on the seam between the two layers.
|
|
107
|
+
* Override this to change the separator, or drop the handle entirely.
|
|
108
|
+
* @param {BeforeAfterProps} props The before/after props.
|
|
109
|
+
* @returns {TemplateResult} The rendered divider.
|
|
110
|
+
*/
|
|
111
|
+
renderDivider(props) {
|
|
112
|
+
return html`<div class="iw-before-after-divider" aria-hidden="true">
|
|
113
|
+
${this.renderHandle?.(props)}
|
|
114
|
+
</div>`
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Renders the grip shown at the centre of the divider.
|
|
119
|
+
* Override this to supply a custom handle.
|
|
120
|
+
* @returns {TemplateResult} The rendered handle.
|
|
121
|
+
*/
|
|
122
|
+
renderHandle() {
|
|
123
|
+
return html`<div class="iw-before-after-handle"></div>`
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Start dragging: capture the pointer so moves keep tracking outside the box.
|
|
129
|
+
* @param {BeforeAfterProps} props
|
|
130
|
+
* @returns {(event: PointerEvent) => void}
|
|
131
|
+
*/
|
|
132
|
+
function handlePointerDown(props) {
|
|
133
|
+
return (event) => {
|
|
134
|
+
if (props.isDisabled) return
|
|
135
|
+
|
|
136
|
+
event.currentTarget.setPointerCapture?.(event.pointerId)
|
|
137
|
+
move(props, event)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Track the divider while the primary button or contact is held down.
|
|
143
|
+
* @param {BeforeAfterProps} props
|
|
144
|
+
* @returns {(event: PointerEvent) => void}
|
|
145
|
+
*/
|
|
146
|
+
function handlePointerMove(props) {
|
|
147
|
+
return (event) => {
|
|
148
|
+
if (props.isDisabled) return
|
|
149
|
+
if (!(event.buttons & PRIMARY_BUTTON)) return
|
|
150
|
+
|
|
151
|
+
move(props, event)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Stop dragging.
|
|
157
|
+
* @param {PointerEvent} event
|
|
158
|
+
*/
|
|
159
|
+
function handlePointerUp(event) {
|
|
160
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Move the divider with the arrow keys, or jump to either edge.
|
|
165
|
+
* @param {BeforeAfterProps} props
|
|
166
|
+
* @returns {(event: KeyboardEvent) => void}
|
|
167
|
+
*/
|
|
168
|
+
function handleKeyDown(props) {
|
|
169
|
+
return (event) => {
|
|
170
|
+
if (props.isDisabled) return
|
|
171
|
+
|
|
172
|
+
const position = props.position ?? DEFAULT_POSITION
|
|
173
|
+
const step = props.step ?? DEFAULT_STEP
|
|
174
|
+
|
|
175
|
+
let next
|
|
176
|
+
switch (event.key) {
|
|
177
|
+
case "ArrowLeft":
|
|
178
|
+
case "ArrowDown":
|
|
179
|
+
next = position - step
|
|
180
|
+
break
|
|
181
|
+
case "ArrowRight":
|
|
182
|
+
case "ArrowUp":
|
|
183
|
+
next = position + step
|
|
184
|
+
break
|
|
185
|
+
case "Home":
|
|
186
|
+
next = MIN_POSITION
|
|
187
|
+
break
|
|
188
|
+
case "End":
|
|
189
|
+
next = MAX_POSITION
|
|
190
|
+
break
|
|
191
|
+
default:
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
event.preventDefault()
|
|
196
|
+
props.onPositionChange?.(clamp(next, MIN_POSITION, MAX_POSITION))
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Report the divider position under the pointer.
|
|
202
|
+
* @param {BeforeAfterProps} props
|
|
203
|
+
* @param {PointerEvent} event
|
|
204
|
+
*/
|
|
205
|
+
function move(props, event) {
|
|
206
|
+
const position = positionFromEvent(event)
|
|
207
|
+
if (position != null) props.onPositionChange?.(position)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Convert a pointer event into a divider position (0-100) relative to the
|
|
212
|
+
* element the listener is attached to. Null when it has no measurable width.
|
|
213
|
+
* @param {PointerEvent} event
|
|
214
|
+
* @returns {number | null}
|
|
215
|
+
*/
|
|
216
|
+
function positionFromEvent(event) {
|
|
217
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
218
|
+
if (!rect.width) return null
|
|
219
|
+
|
|
220
|
+
return clamp(
|
|
221
|
+
((event.clientX - rect.left) / rect.width) * 100,
|
|
222
|
+
MIN_POSITION,
|
|
223
|
+
MAX_POSITION,
|
|
224
|
+
)
|
|
225
|
+
}
|