@inglorious/ui 9.9.0 → 9.10.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 +28 -3
- package/src/controls/chip-picker/chip-picker.stories.js +22 -0
- package/src/controls/chip-picker/index.js +3 -0
- package/src/controls/chip-picker/style.css +26 -0
- package/src/controls/chip-picker/template.js +53 -0
- package/src/controls/combobox/handlers.js +81 -19
- package/src/controls/controls.css +3 -2
- package/src/data-display/data-display.css +1 -0
- package/src/data-display/lightbox/index.js +3 -0
- package/src/data-display/lightbox/lightbox.stories.js +25 -0
- package/src/data-display/lightbox/style.css +97 -0
- package/src/data-display/lightbox/template.js +153 -0
- package/src/feedback/confirm-dialog/confirm-dialog.stories.js +33 -0
- package/src/feedback/confirm-dialog/index.js +3 -0
- package/src/feedback/confirm-dialog/style.css +16 -0
- package/src/feedback/confirm-dialog/template.js +70 -0
- package/src/feedback/crop-dialog/index.js +3 -0
- package/src/feedback/crop-dialog/style.css +43 -0
- package/src/feedback/crop-dialog/template.js +82 -0
- package/src/feedback/dialog/style.css +2 -1
- package/src/feedback/dialog/template.js +5 -2
- package/src/feedback/feedback.css +3 -0
- package/src/feedback/load-status/index.js +3 -0
- package/src/feedback/load-status/load-status.stories.js +16 -0
- package/src/feedback/load-status/style.css +10 -0
- package/src/feedback/load-status/template.js +43 -0
- package/src/index.js +5 -0
- package/src/shared/icons.js +12 -0
- package/types/controls/chip-picker.d.ts +23 -0
- package/types/controls/combobox.d.ts +20 -2
- package/types/data-display/lightbox.d.ts +35 -0
- package/types/feedback/confirm-dialog.d.ts +34 -0
- package/types/feedback/crop-dialog.d.ts +35 -0
- package/types/feedback/load-status.d.ts +22 -0
- package/types/index.d.ts +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/ui",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.10.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",
|
|
@@ -211,6 +211,31 @@
|
|
|
211
211
|
"import": "./src/feedback/backdrop/index.js"
|
|
212
212
|
},
|
|
213
213
|
"./backdrop.css": "./src/feedback/backdrop/style.css",
|
|
214
|
+
"./chip-picker": {
|
|
215
|
+
"types": "./types/controls/chip-picker.d.ts",
|
|
216
|
+
"import": "./src/controls/chip-picker/index.js"
|
|
217
|
+
},
|
|
218
|
+
"./chip-picker.css": "./src/controls/chip-picker/style.css",
|
|
219
|
+
"./confirm-dialog": {
|
|
220
|
+
"types": "./types/feedback/confirm-dialog.d.ts",
|
|
221
|
+
"import": "./src/feedback/confirm-dialog/index.js"
|
|
222
|
+
},
|
|
223
|
+
"./confirm-dialog.css": "./src/feedback/confirm-dialog/style.css",
|
|
224
|
+
"./crop-dialog": {
|
|
225
|
+
"types": "./types/feedback/crop-dialog.d.ts",
|
|
226
|
+
"import": "./src/feedback/crop-dialog/index.js"
|
|
227
|
+
},
|
|
228
|
+
"./crop-dialog.css": "./src/feedback/crop-dialog/style.css",
|
|
229
|
+
"./lightbox": {
|
|
230
|
+
"types": "./types/data-display/lightbox.d.ts",
|
|
231
|
+
"import": "./src/data-display/lightbox/index.js"
|
|
232
|
+
},
|
|
233
|
+
"./lightbox.css": "./src/data-display/lightbox/style.css",
|
|
234
|
+
"./load-status": {
|
|
235
|
+
"types": "./types/feedback/load-status.d.ts",
|
|
236
|
+
"import": "./src/feedback/load-status/index.js"
|
|
237
|
+
},
|
|
238
|
+
"./load-status.css": "./src/feedback/load-status/style.css",
|
|
214
239
|
"./dialog": {
|
|
215
240
|
"types": "./types/feedback/dialog.d.ts",
|
|
216
241
|
"import": "./src/feedback/dialog/index.js"
|
|
@@ -316,7 +341,7 @@
|
|
|
316
341
|
"peerDependencies": {
|
|
317
342
|
"@inglorious/store": "^12.1.1",
|
|
318
343
|
"@inglorious/utils": "^5.1.0",
|
|
319
|
-
"@inglorious/web": "^6.
|
|
344
|
+
"@inglorious/web": "^6.14.0"
|
|
320
345
|
},
|
|
321
346
|
"devDependencies": {
|
|
322
347
|
"@iconify-icons/fa7-brands": "^2.0.0",
|
|
@@ -328,7 +353,7 @@
|
|
|
328
353
|
"@inglorious/store": "12.1.1",
|
|
329
354
|
"@inglorious/vite-plugin-jsx": "2.0.6",
|
|
330
355
|
"@inglorious/vite-plugin-vue": "1.0.7",
|
|
331
|
-
"@inglorious/web": "6.
|
|
356
|
+
"@inglorious/web": "6.14.0",
|
|
332
357
|
"@storybook/addon-docs": "^10.6.5",
|
|
333
358
|
"@storybook/web-components-vite": "^10.6.5",
|
|
334
359
|
"happy-dom": "^20.14.0",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createRender } from "../../stories/notify-story.js"
|
|
2
|
+
import { ChipPicker } from "."
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: "Controls/ChipPicker",
|
|
6
|
+
tags: ["autodocs"],
|
|
7
|
+
render: createRender(ChipPicker),
|
|
8
|
+
argTypes: { onToggle: { action: "onToggle" } },
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Default = {
|
|
12
|
+
args: {
|
|
13
|
+
label: "What do you do?",
|
|
14
|
+
hint: "Pick as many as you like.",
|
|
15
|
+
options: [
|
|
16
|
+
{ value: "photographer", label: "Photographer" },
|
|
17
|
+
{ value: "videographer", label: "Videographer" },
|
|
18
|
+
{ value: "dj", label: "DJ" },
|
|
19
|
+
],
|
|
20
|
+
value: ["videographer"],
|
|
21
|
+
},
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.iw-chip-picker {
|
|
2
|
+
min-width: 0;
|
|
3
|
+
margin: 0;
|
|
4
|
+
padding: 0;
|
|
5
|
+
border: none;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.iw-chip-picker-legend {
|
|
9
|
+
padding: 0;
|
|
10
|
+
margin-bottom: var(--iw-space-2);
|
|
11
|
+
font-size: var(--iw-font-size-sm);
|
|
12
|
+
font-weight: var(--iw-font-weight-medium);
|
|
13
|
+
color: var(--iw-color-text);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.iw-chip-picker-chips {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-wrap: wrap;
|
|
19
|
+
gap: var(--iw-space-2);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.iw-chip-picker-hint {
|
|
23
|
+
margin: var(--iw-space-2) 0 0;
|
|
24
|
+
font-size: var(--iw-font-size-xs);
|
|
25
|
+
color: var(--iw-color-text-muted, var(--iw-color-text));
|
|
26
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../../../types/controls/chip-picker').ChipPickerProps} ChipPickerProps
|
|
3
|
+
* @typedef {import('@inglorious/web').TemplateResult} TemplateResult
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { html } from "@inglorious/web"
|
|
7
|
+
import { when } from "@inglorious/web/directives/when"
|
|
8
|
+
|
|
9
|
+
import { Chip } from "../../data-display/chip/index.js"
|
|
10
|
+
import { formatOption } from "../combobox/helpers.js"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Renders a group of toggle chips for picking several options, as a fieldset
|
|
14
|
+
* whose legend is `label`.
|
|
15
|
+
* @param {ChipPickerProps} props
|
|
16
|
+
* @returns {TemplateResult}
|
|
17
|
+
*/
|
|
18
|
+
export function render(props) {
|
|
19
|
+
const {
|
|
20
|
+
label,
|
|
21
|
+
hint,
|
|
22
|
+
options = [],
|
|
23
|
+
value = [],
|
|
24
|
+
isDisabled = false,
|
|
25
|
+
className = "",
|
|
26
|
+
onToggle,
|
|
27
|
+
} = props
|
|
28
|
+
|
|
29
|
+
return html`<fieldset
|
|
30
|
+
class=${["iw-chip-picker", className].filter(Boolean).join(" ")}
|
|
31
|
+
>
|
|
32
|
+
${when(
|
|
33
|
+
label,
|
|
34
|
+
() => html`<legend class="iw-chip-picker-legend">${label}</legend>`,
|
|
35
|
+
)}
|
|
36
|
+
<div class="iw-chip-picker-chips">
|
|
37
|
+
${options.map((option) => {
|
|
38
|
+
const {
|
|
39
|
+
value: optionValue,
|
|
40
|
+
label: optionLabel,
|
|
41
|
+
...rest
|
|
42
|
+
} = formatOption(option)
|
|
43
|
+
return Chip.render({
|
|
44
|
+
children: optionLabel,
|
|
45
|
+
isSelected: value.includes(optionValue),
|
|
46
|
+
isDisabled: isDisabled || Boolean(rest.isDisabled),
|
|
47
|
+
onClick: () => onToggle?.(optionValue),
|
|
48
|
+
})
|
|
49
|
+
})}
|
|
50
|
+
</div>
|
|
51
|
+
${when(hint, () => html`<p class="iw-chip-picker-hint">${hint}</p>`)}
|
|
52
|
+
</fieldset>`
|
|
53
|
+
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { formatOption, getOptionValue, visibleOptions } from "./helpers.js"
|
|
7
7
|
|
|
8
|
+
const NONE = -1
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Initialize missing select state defaults.
|
|
10
12
|
* @param {SelectEntity} entity
|
|
@@ -16,7 +18,7 @@ export function create(entity) {
|
|
|
16
18
|
|
|
17
19
|
entity.isOpen ??= false
|
|
18
20
|
entity.searchTerm ??= ""
|
|
19
|
-
entity.focusedIndex ??=
|
|
21
|
+
entity.focusedIndex ??= NONE
|
|
20
22
|
|
|
21
23
|
entity.isMulti ??= false
|
|
22
24
|
entity.selectedValue ??= entity.isMulti ? [] : null
|
|
@@ -38,15 +40,13 @@ export function create(entity) {
|
|
|
38
40
|
export function open(entity) {
|
|
39
41
|
if (entity.isDisabled) return
|
|
40
42
|
entity.isOpen = true
|
|
41
|
-
|
|
42
|
-
const filtered = visibleOptions(entity)
|
|
43
|
-
entity.focusedIndex = filtered.length ? 0 : -1
|
|
43
|
+
entity.focusedIndex = enabledIndex(visibleOptions(entity), 0, 1)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/** @param {SelectEntity} entity */
|
|
47
47
|
export function close(entity) {
|
|
48
48
|
entity.isOpen = false
|
|
49
|
-
entity.focusedIndex =
|
|
49
|
+
entity.focusedIndex = NONE
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/** @param {SelectEntity} entity */
|
|
@@ -60,15 +60,16 @@ export function toggle(entity) {
|
|
|
60
60
|
* @param {SelectEntity} entity
|
|
61
61
|
* @param {SelectOption} option
|
|
62
62
|
*/
|
|
63
|
-
export function optionSelect(entity, option) {
|
|
63
|
+
export function optionSelect(entity, option, api) {
|
|
64
64
|
if (entity.isDisabled) return
|
|
65
65
|
|
|
66
66
|
const normalizedOption = formatOption(option)
|
|
67
|
+
if (normalizedOption.isDisabled) return
|
|
67
68
|
const optionValue = getOptionValue(normalizedOption)
|
|
68
69
|
|
|
69
70
|
if (normalizedOption.isCreated) {
|
|
70
71
|
entity.searchTerm = ""
|
|
71
|
-
entity.focusedIndex =
|
|
72
|
+
entity.focusedIndex = NONE
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
if (entity.isMulti) {
|
|
@@ -84,17 +85,24 @@ export function optionSelect(entity, option) {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
entity.selectedValue = values
|
|
88
|
+
notifyChange(entity, api)
|
|
87
89
|
return
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
entity.selectedValue = optionValue
|
|
91
93
|
close(entity)
|
|
94
|
+
notifyChange(entity, api)
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
/**
|
|
95
|
-
|
|
97
|
+
/**
|
|
98
|
+
* @param {SelectEntity} entity
|
|
99
|
+
* @param {unknown} _payload
|
|
100
|
+
* @param {import('@inglorious/web').Api} [api]
|
|
101
|
+
*/
|
|
102
|
+
export function clear(entity, _payload, api) {
|
|
96
103
|
if (entity.isDisabled) return
|
|
97
104
|
entity.selectedValue = entity.isMulti ? [] : null
|
|
105
|
+
notifyChange(entity, api)
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
/**
|
|
@@ -103,32 +111,36 @@ export function clear(entity) {
|
|
|
103
111
|
*/
|
|
104
112
|
export function searchChange(entity, searchTerm) {
|
|
105
113
|
entity.searchTerm = searchTerm
|
|
106
|
-
|
|
107
|
-
entity.focusedIndex = filtered.length ? 0 : -1
|
|
114
|
+
entity.focusedIndex = enabledIndex(visibleOptions(entity), 0, 1)
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
/** @param {SelectEntity} entity */
|
|
111
118
|
export function focusNext(entity) {
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
114
|
-
entity.focusedIndex = Math.min(entity.focusedIndex + 1, filtered.length - 1)
|
|
119
|
+
const next = enabledIndex(visibleOptions(entity), entity.focusedIndex + 1, 1)
|
|
120
|
+
if (next !== NONE) entity.focusedIndex = next
|
|
115
121
|
}
|
|
116
122
|
|
|
117
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* Before the first enabled option, focus goes back to the search box.
|
|
125
|
+
* @param {SelectEntity} entity
|
|
126
|
+
*/
|
|
118
127
|
export function focusPrev(entity) {
|
|
119
|
-
entity.focusedIndex =
|
|
128
|
+
entity.focusedIndex = enabledIndex(
|
|
129
|
+
visibleOptions(entity),
|
|
130
|
+
entity.focusedIndex - 1,
|
|
131
|
+
-1,
|
|
132
|
+
)
|
|
120
133
|
}
|
|
121
134
|
|
|
122
135
|
/** @param {SelectEntity} entity */
|
|
123
136
|
export function focusFirst(entity) {
|
|
124
|
-
|
|
125
|
-
if (filtered.length) entity.focusedIndex = 0
|
|
137
|
+
entity.focusedIndex = enabledIndex(visibleOptions(entity), 0, 1)
|
|
126
138
|
}
|
|
127
139
|
|
|
128
140
|
/** @param {SelectEntity} entity */
|
|
129
141
|
export function focusLast(entity) {
|
|
130
142
|
const filtered = visibleOptions(entity)
|
|
131
|
-
|
|
143
|
+
entity.focusedIndex = enabledIndex(filtered, filtered.length - 1, -1)
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
/**
|
|
@@ -139,3 +151,53 @@ export function focusLast(entity) {
|
|
|
139
151
|
export function focusSet(entity, index) {
|
|
140
152
|
entity.focusedIndex = Number.isInteger(index) ? index : entity.focusedIndex
|
|
141
153
|
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Sets the value from outside, such as a form being filled with saved data.
|
|
157
|
+
* An empty string, as an empty form field holds, is no value. Unlike a
|
|
158
|
+
* choice, it does not notify `changeEvent`.
|
|
159
|
+
* @param {SelectEntity} entity
|
|
160
|
+
* @param {string | number | (string | number)[] | null} value
|
|
161
|
+
*/
|
|
162
|
+
export function valueSet(entity, value) {
|
|
163
|
+
if (entity.isMulti) entity.selectedValue = [...(value ?? [])]
|
|
164
|
+
else entity.selectedValue = value === "" ? null : (value ?? null)
|
|
165
|
+
entity.searchTerm = ""
|
|
166
|
+
entity.focusedIndex = NONE
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Replaces the options, such as when they depend on another field.
|
|
171
|
+
* @param {SelectEntity} entity
|
|
172
|
+
* @param {SelectOption[]} options
|
|
173
|
+
*/
|
|
174
|
+
export function optionsSet(entity, options) {
|
|
175
|
+
entity.options = (options ?? []).map(formatOption)
|
|
176
|
+
entity.searchTerm = ""
|
|
177
|
+
entity.focusedIndex = NONE
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Shows a validation message. The combobox is its own entity, so the form
|
|
182
|
+
* that validates it hands the message over.
|
|
183
|
+
* @param {SelectEntity} entity
|
|
184
|
+
* @param {string | null} error
|
|
185
|
+
*/
|
|
186
|
+
export function errorSet(entity, error) {
|
|
187
|
+
entity.error = error ?? null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function notifyChange(entity, api) {
|
|
191
|
+
if (!entity.changeEvent || !api) return
|
|
192
|
+
const value = entity.isMulti
|
|
193
|
+
? [...entity.selectedValue]
|
|
194
|
+
: entity.selectedValue
|
|
195
|
+
api.notify(entity.changeEvent, { ...entity.changePayload, value })
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function enabledIndex(options, start, step) {
|
|
199
|
+
for (let index = start; index >= 0 && index < options.length; index += step) {
|
|
200
|
+
if (!formatOption(options[index]).isDisabled) return index
|
|
201
|
+
}
|
|
202
|
+
return NONE
|
|
203
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
@import "./button/style.css";
|
|
2
1
|
@import "./button-group/style.css";
|
|
2
|
+
@import "./button/style.css";
|
|
3
3
|
@import "./checkbox/style.css";
|
|
4
|
+
@import "./chip-picker/style.css";
|
|
4
5
|
@import "./combobox/style.css";
|
|
5
6
|
@import "./fab/style.css";
|
|
7
|
+
@import "./file-upload/style.css";
|
|
6
8
|
@import "./icon-button/style.css";
|
|
7
9
|
@import "./image-cropper/style.css";
|
|
8
10
|
@import "./input/style.css";
|
|
@@ -12,4 +14,3 @@
|
|
|
12
14
|
@import "./slider/style.css";
|
|
13
15
|
@import "./switch/style.css";
|
|
14
16
|
@import "./textarea/style.css";
|
|
15
|
-
@import "./file-upload/style.css";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createRender } from "../../stories/notify-story.js"
|
|
2
|
+
import { Lightbox } from "."
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: "Data Display/Lightbox",
|
|
6
|
+
tags: ["autodocs"],
|
|
7
|
+
render: createRender(Lightbox),
|
|
8
|
+
argTypes: {
|
|
9
|
+
index: { control: "number" },
|
|
10
|
+
onStep: { action: "onStep" },
|
|
11
|
+
onClose: { action: "onClose" },
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Default = {
|
|
16
|
+
args: {
|
|
17
|
+
isOpen: true,
|
|
18
|
+
index: 0,
|
|
19
|
+
images: [
|
|
20
|
+
"https://picsum.photos/id/1011/1200/800",
|
|
21
|
+
"https://picsum.photos/id/1015/1200/800",
|
|
22
|
+
"https://picsum.photos/id/1016/800/1200",
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
.iw-lightbox {
|
|
2
|
+
width: 100vw;
|
|
3
|
+
max-width: none;
|
|
4
|
+
height: 100dvh;
|
|
5
|
+
max-height: none;
|
|
6
|
+
margin: 0;
|
|
7
|
+
padding: var(--iw-space-4);
|
|
8
|
+
border: none;
|
|
9
|
+
background: #0c0c0c;
|
|
10
|
+
color: #fff;
|
|
11
|
+
|
|
12
|
+
&[open] {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
&::backdrop {
|
|
19
|
+
background: #0c0c0c;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.iw-button.iw-lightbox-close {
|
|
23
|
+
position: absolute;
|
|
24
|
+
top: calc(var(--iw-space-3) + var(--safe-area-top, 0px));
|
|
25
|
+
inset-inline-end: var(--iw-space-3);
|
|
26
|
+
z-index: 1;
|
|
27
|
+
width: 2.5rem;
|
|
28
|
+
height: 2.5rem;
|
|
29
|
+
background: color-mix(in srgb, #fff 14%, transparent);
|
|
30
|
+
color: #fff;
|
|
31
|
+
|
|
32
|
+
&:hover {
|
|
33
|
+
background: color-mix(in srgb, #fff 24%, transparent);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.iw-button.iw-lightbox-nav {
|
|
38
|
+
position: absolute;
|
|
39
|
+
top: 50%;
|
|
40
|
+
transform: translateY(-50%);
|
|
41
|
+
z-index: 1;
|
|
42
|
+
width: 2.5rem;
|
|
43
|
+
height: 2.5rem;
|
|
44
|
+
background: color-mix(in srgb, #000 45%, transparent);
|
|
45
|
+
color: #fff;
|
|
46
|
+
|
|
47
|
+
&:hover {
|
|
48
|
+
background: color-mix(in srgb, #000 62%, transparent);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&:dir(rtl) .iw-icon {
|
|
52
|
+
scale: -1 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.iw-lightbox-stage {
|
|
58
|
+
position: relative;
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
justify-content: center;
|
|
62
|
+
width: min(100%, 56rem);
|
|
63
|
+
height: min(100%, 90dvh);
|
|
64
|
+
user-select: none;
|
|
65
|
+
touch-action: pan-y;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.iw-lightbox-image {
|
|
69
|
+
display: block;
|
|
70
|
+
max-width: 100%;
|
|
71
|
+
max-height: 100%;
|
|
72
|
+
width: auto;
|
|
73
|
+
height: auto;
|
|
74
|
+
object-fit: contain;
|
|
75
|
+
border-radius: var(--iw-radius-xl);
|
|
76
|
+
pointer-events: none;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.iw-lightbox-previous {
|
|
80
|
+
inset-inline-start: var(--iw-space-2);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.iw-lightbox-next {
|
|
84
|
+
inset-inline-end: var(--iw-space-2);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.iw-lightbox-counter {
|
|
88
|
+
position: absolute;
|
|
89
|
+
inset-inline-end: var(--iw-space-3);
|
|
90
|
+
bottom: var(--iw-space-2);
|
|
91
|
+
padding: 0.125rem 0.5rem;
|
|
92
|
+
border-radius: 999px;
|
|
93
|
+
background: color-mix(in srgb, #000 50%, transparent);
|
|
94
|
+
color: #fff;
|
|
95
|
+
font-size: var(--iw-font-size-xs);
|
|
96
|
+
font-variant-numeric: tabular-nums;
|
|
97
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../../../types/data-display/lightbox').LightboxProps} LightboxProps
|
|
3
|
+
* @typedef {import('../../../types/data-display/lightbox').LightboxImage} LightboxImage
|
|
4
|
+
* @typedef {import('@inglorious/web').TemplateResult} TemplateResult
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { html, nothing } from "@inglorious/web"
|
|
8
|
+
import { ref } from "@inglorious/web/directives/ref"
|
|
9
|
+
import { when } from "@inglorious/web/directives/when"
|
|
10
|
+
|
|
11
|
+
import { IconButton } from "../../controls/icon-button/index.js"
|
|
12
|
+
import { chevronLeft, chevronRight, close } from "../../shared/icons.js"
|
|
13
|
+
import { syncDialog } from "../../shared/top-layer.js"
|
|
14
|
+
import { Icon } from "../icon/index.js"
|
|
15
|
+
|
|
16
|
+
const SWIPE_PX = 40
|
|
17
|
+
const NEXT = 1
|
|
18
|
+
const PREVIOUS = -1
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Where each lightbox's current swipe started.
|
|
22
|
+
* @type {WeakMap<HTMLDialogElement, number>}
|
|
23
|
+
*/
|
|
24
|
+
const swipeStarts = new WeakMap()
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Renders images full screen in a modal `<dialog>`, one at a time. With more
|
|
28
|
+
* than one, the arrows, the arrow keys and a horizontal swipe step through
|
|
29
|
+
* them, following the writing direction. Esc, the close button and a press
|
|
30
|
+
* outside the image close it.
|
|
31
|
+
* @param {LightboxProps} props
|
|
32
|
+
* @returns {TemplateResult | typeof nothing}
|
|
33
|
+
*/
|
|
34
|
+
export function render(props) {
|
|
35
|
+
const {
|
|
36
|
+
id,
|
|
37
|
+
images = [],
|
|
38
|
+
index = 0,
|
|
39
|
+
isOpen = false,
|
|
40
|
+
label = "Photos",
|
|
41
|
+
closeLabel = "Close",
|
|
42
|
+
previousLabel = "Previous photo",
|
|
43
|
+
nextLabel = "Next photo",
|
|
44
|
+
className = "",
|
|
45
|
+
onStep,
|
|
46
|
+
onClose,
|
|
47
|
+
} = props
|
|
48
|
+
|
|
49
|
+
const total = images.length
|
|
50
|
+
if (!total) return nothing
|
|
51
|
+
|
|
52
|
+
const current = ((index % total) + total) % total
|
|
53
|
+
const image = toImage(images[current])
|
|
54
|
+
const isNavigable = total > 1 && Boolean(onStep)
|
|
55
|
+
|
|
56
|
+
return html`<dialog
|
|
57
|
+
id=${id || nothing}
|
|
58
|
+
class=${["iw-lightbox", className].filter(Boolean).join(" ")}
|
|
59
|
+
aria-label=${label}
|
|
60
|
+
@click=${(event) => {
|
|
61
|
+
if (event.target === event.currentTarget) onClose?.()
|
|
62
|
+
}}
|
|
63
|
+
@cancel=${(event) => {
|
|
64
|
+
if (!onClose) event.preventDefault()
|
|
65
|
+
}}
|
|
66
|
+
@keydown=${(event) => {
|
|
67
|
+
if (!isNavigable) return
|
|
68
|
+
const step = arrowStep(event)
|
|
69
|
+
if (!step) return
|
|
70
|
+
event.preventDefault()
|
|
71
|
+
onStep(step)
|
|
72
|
+
}}
|
|
73
|
+
@pointerdown=${(event) => {
|
|
74
|
+
swipeStarts.set(event.currentTarget, event.clientX)
|
|
75
|
+
}}
|
|
76
|
+
@pointerup=${(event) => {
|
|
77
|
+
const startX = swipeStarts.get(event.currentTarget)
|
|
78
|
+
swipeStarts.delete(event.currentTarget)
|
|
79
|
+
if (!isNavigable || startX == null) return
|
|
80
|
+
const distance = event.clientX - startX
|
|
81
|
+
if (Math.abs(distance) < SWIPE_PX) return
|
|
82
|
+
const towardsEnd = isRtl(event.currentTarget)
|
|
83
|
+
? distance > 0
|
|
84
|
+
: distance < 0
|
|
85
|
+
onStep(towardsEnd ? NEXT : PREVIOUS)
|
|
86
|
+
}}
|
|
87
|
+
@pointercancel=${(event) => swipeStarts.delete(event.currentTarget)}
|
|
88
|
+
${ref((element) => syncDialog(element, { isOpen, onClose }))}
|
|
89
|
+
>
|
|
90
|
+
${when(onClose, () =>
|
|
91
|
+
IconButton.render({
|
|
92
|
+
icon: Icon.render({ icon: close, size: "lg" }),
|
|
93
|
+
variant: "ghost",
|
|
94
|
+
shape: "round",
|
|
95
|
+
ariaLabel: closeLabel,
|
|
96
|
+
className: "iw-lightbox-close",
|
|
97
|
+
onClick: onClose,
|
|
98
|
+
}),
|
|
99
|
+
)}
|
|
100
|
+
<div class="iw-lightbox-stage">
|
|
101
|
+
<img
|
|
102
|
+
class="iw-lightbox-image"
|
|
103
|
+
src=${image.src}
|
|
104
|
+
alt=${image.alt ?? ""}
|
|
105
|
+
draggable="false"
|
|
106
|
+
/>
|
|
107
|
+
${when(
|
|
108
|
+
isNavigable,
|
|
109
|
+
() => html`
|
|
110
|
+
${IconButton.render({
|
|
111
|
+
icon: Icon.render({ icon: chevronLeft, size: "lg" }),
|
|
112
|
+
variant: "ghost",
|
|
113
|
+
shape: "round",
|
|
114
|
+
ariaLabel: previousLabel,
|
|
115
|
+
className: "iw-lightbox-nav iw-lightbox-previous",
|
|
116
|
+
onClick: () => onStep(PREVIOUS),
|
|
117
|
+
})}
|
|
118
|
+
${IconButton.render({
|
|
119
|
+
icon: Icon.render({ icon: chevronRight, size: "lg" }),
|
|
120
|
+
variant: "ghost",
|
|
121
|
+
shape: "round",
|
|
122
|
+
ariaLabel: nextLabel,
|
|
123
|
+
className: "iw-lightbox-nav iw-lightbox-next",
|
|
124
|
+
onClick: () => onStep(NEXT),
|
|
125
|
+
})}
|
|
126
|
+
<div class="iw-lightbox-counter" aria-hidden="true">
|
|
127
|
+
${current + 1}/${total}
|
|
128
|
+
</div>
|
|
129
|
+
`,
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
</dialog>`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {string | LightboxImage} image
|
|
137
|
+
* @returns {LightboxImage}
|
|
138
|
+
*/
|
|
139
|
+
function toImage(image) {
|
|
140
|
+
return typeof image === "string" ? { src: image } : image
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function arrowStep(event) {
|
|
144
|
+
const towardsEnd = isRtl(event.currentTarget) ? "ArrowLeft" : "ArrowRight"
|
|
145
|
+
const towardsStart = isRtl(event.currentTarget) ? "ArrowRight" : "ArrowLeft"
|
|
146
|
+
if (event.key === towardsEnd) return NEXT
|
|
147
|
+
if (event.key === towardsStart) return PREVIOUS
|
|
148
|
+
return 0
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isRtl(element) {
|
|
152
|
+
return getComputedStyle(element).direction === "rtl"
|
|
153
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createRender } from "../../stories/notify-story.js"
|
|
2
|
+
import { ConfirmDialog } from "."
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: "Feedback/ConfirmDialog",
|
|
6
|
+
tags: ["autodocs"],
|
|
7
|
+
render: createRender(ConfirmDialog),
|
|
8
|
+
argTypes: {
|
|
9
|
+
tone: { control: "select", options: ["danger", "primary"] },
|
|
10
|
+
isBusy: { control: "boolean" },
|
|
11
|
+
onConfirm: { action: "onConfirm" },
|
|
12
|
+
onCancel: { action: "onCancel" },
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const Danger = {
|
|
17
|
+
args: {
|
|
18
|
+
isOpen: true,
|
|
19
|
+
message: "Delete this event? This cannot be undone.",
|
|
20
|
+
confirmLabel: "Delete",
|
|
21
|
+
busyLabel: "Deleting…",
|
|
22
|
+
cancelLabel: "Keep it",
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const Primary = {
|
|
27
|
+
args: {
|
|
28
|
+
isOpen: true,
|
|
29
|
+
tone: "primary",
|
|
30
|
+
message: "Publish this event now?",
|
|
31
|
+
confirmLabel: "Publish",
|
|
32
|
+
},
|
|
33
|
+
}
|