@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.
Files changed (35) hide show
  1. package/package.json +28 -3
  2. package/src/controls/chip-picker/chip-picker.stories.js +22 -0
  3. package/src/controls/chip-picker/index.js +3 -0
  4. package/src/controls/chip-picker/style.css +26 -0
  5. package/src/controls/chip-picker/template.js +53 -0
  6. package/src/controls/combobox/handlers.js +81 -19
  7. package/src/controls/controls.css +3 -2
  8. package/src/data-display/data-display.css +1 -0
  9. package/src/data-display/lightbox/index.js +3 -0
  10. package/src/data-display/lightbox/lightbox.stories.js +25 -0
  11. package/src/data-display/lightbox/style.css +97 -0
  12. package/src/data-display/lightbox/template.js +153 -0
  13. package/src/feedback/confirm-dialog/confirm-dialog.stories.js +33 -0
  14. package/src/feedback/confirm-dialog/index.js +3 -0
  15. package/src/feedback/confirm-dialog/style.css +16 -0
  16. package/src/feedback/confirm-dialog/template.js +70 -0
  17. package/src/feedback/crop-dialog/index.js +3 -0
  18. package/src/feedback/crop-dialog/style.css +43 -0
  19. package/src/feedback/crop-dialog/template.js +82 -0
  20. package/src/feedback/dialog/style.css +2 -1
  21. package/src/feedback/dialog/template.js +5 -2
  22. package/src/feedback/feedback.css +3 -0
  23. package/src/feedback/load-status/index.js +3 -0
  24. package/src/feedback/load-status/load-status.stories.js +16 -0
  25. package/src/feedback/load-status/style.css +10 -0
  26. package/src/feedback/load-status/template.js +43 -0
  27. package/src/index.js +5 -0
  28. package/src/shared/icons.js +12 -0
  29. package/types/controls/chip-picker.d.ts +23 -0
  30. package/types/controls/combobox.d.ts +20 -2
  31. package/types/data-display/lightbox.d.ts +35 -0
  32. package/types/feedback/confirm-dialog.d.ts +34 -0
  33. package/types/feedback/crop-dialog.d.ts +35 -0
  34. package/types/feedback/load-status.d.ts +22 -0
  35. 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.9.0",
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.13.1"
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.13.1",
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,3 @@
1
+ import * as renderers from "./template.js"
2
+
3
+ export const ChipPicker = { ...renderers }
@@ -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 ??= -1
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 = -1
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 = -1
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
- /** @param {SelectEntity} entity */
95
- export function clear(entity) {
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
- const filtered = visibleOptions(entity)
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 filtered = visibleOptions(entity)
113
- if (!filtered.length) return
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
- /** @param {SelectEntity} entity */
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 = Math.max(entity.focusedIndex - 1, -1)
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
- const filtered = visibleOptions(entity)
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
- if (filtered.length) entity.focusedIndex = filtered.length - 1
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";
@@ -7,6 +7,7 @@
7
7
  @import "./data-grid/style.css";
8
8
  @import "./divider/style.css";
9
9
  @import "./icon/style.css";
10
+ @import "./lightbox/style.css";
10
11
  @import "./list/style.css";
11
12
  @import "./table/style.css";
12
13
  @import "./tooltip/style.css";
@@ -0,0 +1,3 @@
1
+ import * as renderers from "./template.js"
2
+
3
+ export const Lightbox = { ...renderers }
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import * as renderers from "./template.js"
2
+
3
+ export const ConfirmDialog = { ...renderers }