@inglorious/ui 9.9.0 → 9.10.1

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 (43) hide show
  1. package/package.json +29 -3
  2. package/src/controls/checkbox/template.js +3 -1
  3. package/src/controls/chip-picker/chip-picker.stories.js +22 -0
  4. package/src/controls/chip-picker/index.js +3 -0
  5. package/src/controls/chip-picker/style.css +26 -0
  6. package/src/controls/chip-picker/template.js +53 -0
  7. package/src/controls/combobox/handlers.js +83 -20
  8. package/src/controls/combobox/template.js +3 -1
  9. package/src/controls/controls.css +3 -2
  10. package/src/controls/radio-group/template.js +3 -1
  11. package/src/controls/rating/template.js +4 -1
  12. package/src/controls/select/template.js +6 -12
  13. package/src/controls/switch/template.js +3 -1
  14. package/src/data-display/badge/style.css +1 -0
  15. package/src/data-display/data-display.css +1 -0
  16. package/src/data-display/lightbox/index.js +3 -0
  17. package/src/data-display/lightbox/lightbox.stories.js +25 -0
  18. package/src/data-display/lightbox/style.css +97 -0
  19. package/src/data-display/lightbox/template.js +153 -0
  20. package/src/feedback/confirm-dialog/confirm-dialog.stories.js +33 -0
  21. package/src/feedback/confirm-dialog/index.js +3 -0
  22. package/src/feedback/confirm-dialog/style.css +16 -0
  23. package/src/feedback/confirm-dialog/template.js +70 -0
  24. package/src/feedback/crop-dialog/index.js +3 -0
  25. package/src/feedback/crop-dialog/style.css +43 -0
  26. package/src/feedback/crop-dialog/template.js +82 -0
  27. package/src/feedback/dialog/style.css +2 -1
  28. package/src/feedback/dialog/template.js +5 -2
  29. package/src/feedback/feedback.css +3 -0
  30. package/src/feedback/load-status/index.js +3 -0
  31. package/src/feedback/load-status/load-status.stories.js +16 -0
  32. package/src/feedback/load-status/style.css +10 -0
  33. package/src/feedback/load-status/template.js +43 -0
  34. package/src/index.js +5 -0
  35. package/src/shared/icons.js +12 -0
  36. package/src/shared/live-boolean.js +17 -0
  37. package/types/controls/chip-picker.d.ts +23 -0
  38. package/types/controls/combobox.d.ts +20 -2
  39. package/types/data-display/lightbox.d.ts +35 -0
  40. package/types/feedback/confirm-dialog.d.ts +34 -0
  41. package/types/feedback/crop-dialog.d.ts +35 -0
  42. package/types/feedback/load-status.d.ts +22 -0
  43. 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.1",
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,8 @@
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",
357
+ "@lit-labs/ssr": "^4.1.0",
332
358
  "@storybook/addon-docs": "^10.6.5",
333
359
  "@storybook/web-components-vite": "^10.6.5",
334
360
  "happy-dom": "^20.14.0",
@@ -9,6 +9,7 @@ import { ref } from "@inglorious/web/directives/ref"
9
9
  import { when } from "@inglorious/web/directives/when"
10
10
 
11
11
  import { applyElementProps } from "../../shared/apply-element-props.js"
12
+ import { liveBoolean } from "../../shared/live-boolean.js"
12
13
 
13
14
  /**
14
15
  * Renders a labeled checkbox for boolean input.
@@ -51,7 +52,8 @@ export function render(props) {
51
52
  class="iw-checkbox-input"
52
53
  type="checkbox"
53
54
  name=${name}
54
- .checked=${isChecked}
55
+ ?checked=${isChecked}
56
+ ${liveBoolean("checked", isChecked)}
55
57
  ?disabled=${isDisabled}
56
58
  ?required=${isRequired}
57
59
  @change=${(event) => onChange?.(event.target.checked)}
@@ -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,14 @@ 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.searchTerm = ""
50
+ entity.focusedIndex = NONE
50
51
  }
51
52
 
52
53
  /** @param {SelectEntity} entity */
@@ -60,15 +61,16 @@ export function toggle(entity) {
60
61
  * @param {SelectEntity} entity
61
62
  * @param {SelectOption} option
62
63
  */
63
- export function optionSelect(entity, option) {
64
+ export function optionSelect(entity, option, api) {
64
65
  if (entity.isDisabled) return
65
66
 
66
67
  const normalizedOption = formatOption(option)
68
+ if (normalizedOption.isDisabled) return
67
69
  const optionValue = getOptionValue(normalizedOption)
68
70
 
69
- if (normalizedOption.isCreated) {
71
+ if (normalizedOption.isCreated || entity.searchTerm) {
70
72
  entity.searchTerm = ""
71
- entity.focusedIndex = -1
73
+ entity.focusedIndex = NONE
72
74
  }
73
75
 
74
76
  if (entity.isMulti) {
@@ -84,17 +86,24 @@ export function optionSelect(entity, option) {
84
86
  }
85
87
 
86
88
  entity.selectedValue = values
89
+ notifyChange(entity, api)
87
90
  return
88
91
  }
89
92
 
90
93
  entity.selectedValue = optionValue
91
94
  close(entity)
95
+ notifyChange(entity, api)
92
96
  }
93
97
 
94
- /** @param {SelectEntity} entity */
95
- export function clear(entity) {
98
+ /**
99
+ * @param {SelectEntity} entity
100
+ * @param {unknown} _payload
101
+ * @param {import('@inglorious/web').Api} [api]
102
+ */
103
+ export function clear(entity, _payload, api) {
96
104
  if (entity.isDisabled) return
97
105
  entity.selectedValue = entity.isMulti ? [] : null
106
+ notifyChange(entity, api)
98
107
  }
99
108
 
100
109
  /**
@@ -103,32 +112,36 @@ export function clear(entity) {
103
112
  */
104
113
  export function searchChange(entity, searchTerm) {
105
114
  entity.searchTerm = searchTerm
106
- const filtered = visibleOptions(entity)
107
- entity.focusedIndex = filtered.length ? 0 : -1
115
+ entity.focusedIndex = enabledIndex(visibleOptions(entity), 0, 1)
108
116
  }
109
117
 
110
118
  /** @param {SelectEntity} entity */
111
119
  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)
120
+ const next = enabledIndex(visibleOptions(entity), entity.focusedIndex + 1, 1)
121
+ if (next !== NONE) entity.focusedIndex = next
115
122
  }
116
123
 
117
- /** @param {SelectEntity} entity */
124
+ /**
125
+ * Before the first enabled option, focus goes back to the search box.
126
+ * @param {SelectEntity} entity
127
+ */
118
128
  export function focusPrev(entity) {
119
- entity.focusedIndex = Math.max(entity.focusedIndex - 1, -1)
129
+ entity.focusedIndex = enabledIndex(
130
+ visibleOptions(entity),
131
+ entity.focusedIndex - 1,
132
+ -1,
133
+ )
120
134
  }
121
135
 
122
136
  /** @param {SelectEntity} entity */
123
137
  export function focusFirst(entity) {
124
- const filtered = visibleOptions(entity)
125
- if (filtered.length) entity.focusedIndex = 0
138
+ entity.focusedIndex = enabledIndex(visibleOptions(entity), 0, 1)
126
139
  }
127
140
 
128
141
  /** @param {SelectEntity} entity */
129
142
  export function focusLast(entity) {
130
143
  const filtered = visibleOptions(entity)
131
- if (filtered.length) entity.focusedIndex = filtered.length - 1
144
+ entity.focusedIndex = enabledIndex(filtered, filtered.length - 1, -1)
132
145
  }
133
146
 
134
147
  /**
@@ -139,3 +152,53 @@ export function focusLast(entity) {
139
152
  export function focusSet(entity, index) {
140
153
  entity.focusedIndex = Number.isInteger(index) ? index : entity.focusedIndex
141
154
  }
155
+
156
+ /**
157
+ * Sets the value from outside, such as a form being filled with saved data.
158
+ * An empty string, as an empty form field holds, is no value. Unlike a
159
+ * choice, it does not notify `changeEvent`.
160
+ * @param {SelectEntity} entity
161
+ * @param {string | number | (string | number)[] | null} value
162
+ */
163
+ export function valueSet(entity, value) {
164
+ if (entity.isMulti) entity.selectedValue = [...(value ?? [])]
165
+ else entity.selectedValue = value === "" ? null : (value ?? null)
166
+ entity.searchTerm = ""
167
+ entity.focusedIndex = NONE
168
+ }
169
+
170
+ /**
171
+ * Replaces the options, such as when they depend on another field.
172
+ * @param {SelectEntity} entity
173
+ * @param {SelectOption[]} options
174
+ */
175
+ export function optionsSet(entity, options) {
176
+ entity.options = (options ?? []).map(formatOption)
177
+ entity.searchTerm = ""
178
+ entity.focusedIndex = NONE
179
+ }
180
+
181
+ /**
182
+ * Shows a validation message. The combobox is its own entity, so the form
183
+ * that validates it hands the message over.
184
+ * @param {SelectEntity} entity
185
+ * @param {string | null} error
186
+ */
187
+ export function errorSet(entity, error) {
188
+ entity.error = error ?? null
189
+ }
190
+
191
+ function notifyChange(entity, api) {
192
+ if (!entity.changeEvent || !api) return
193
+ const value = entity.isMulti
194
+ ? [...entity.selectedValue]
195
+ : entity.selectedValue
196
+ api.notify(entity.changeEvent, { ...entity.changePayload, value })
197
+ }
198
+
199
+ function enabledIndex(options, start, step) {
200
+ for (let index = start; index >= 0 && index < options.length; index += step) {
201
+ if (!formatOption(options[index]).isDisabled) return index
202
+ }
203
+ return NONE
204
+ }
@@ -12,6 +12,7 @@ import { when } from "@inglorious/web/directives/when"
12
12
 
13
13
  import { Chip } from "../../data-display/chip/index.js"
14
14
  import { fallbackLabel } from "../../shared/fallback-label.js"
15
+ import { liveBoolean } from "../../shared/live-boolean.js"
15
16
  import { syncPopover } from "../../shared/top-layer.js"
16
17
  import {
17
18
  formatOption,
@@ -384,7 +385,8 @@ export const Combobox = {
384
385
  () =>
385
386
  html`<input
386
387
  type="checkbox"
387
- .checked=${isSelected}
388
+ ?checked=${isSelected}
389
+ ${liveBoolean("checked", isSelected)}
388
390
  ?disabled=${!!normalized.isDisabled}
389
391
  />`,
390
392
  )}
@@ -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";
@@ -9,6 +9,7 @@ import { ref } from "@inglorious/web/directives/ref"
9
9
  import { when } from "@inglorious/web/directives/when"
10
10
 
11
11
  import { applyElementProps } from "../../shared/apply-element-props.js"
12
+ import { liveBoolean } from "../../shared/live-boolean.js"
12
13
 
13
14
  /**
14
15
  * Renders a group of radio options for single selection.
@@ -72,7 +73,8 @@ export function render(props) {
72
73
  type="radio"
73
74
  name=${groupName || nothing}
74
75
  value=${optionValue}
75
- .checked=${value === optionValue}
76
+ ?checked=${value === optionValue}
77
+ ${liveBoolean("checked", value === optionValue)}
76
78
  ?disabled=${isDisabled || optionDisabled}
77
79
  @change=${(event) => onChange?.(event.target.value)}
78
80
  ${ref((element) => applyElementProps(element, optionRest))}
@@ -6,6 +6,8 @@
6
6
  import { html, nothing } from "@inglorious/web"
7
7
  import { classMap } from "@inglorious/web/directives/class-map"
8
8
 
9
+ import { liveBoolean } from "../../shared/live-boolean.js"
10
+
9
11
  const DEFAULT_VALUE = 0
10
12
  const DEFAULT_MAX = 5
11
13
  const INDEX_TO_RATING = 1
@@ -62,7 +64,8 @@ export function render(props) {
62
64
  name=${groupName || nothing}
63
65
  value=${ratingValue}
64
66
  aria-label=${`${ratingValue} of ${max}`}
65
- .checked=${ratingValue === value}
67
+ ?checked=${ratingValue === value}
68
+ ${liveBoolean("checked", ratingValue === value)}
66
69
  @change=${() => onChange?.(ratingValue)}
67
70
  />
68
71
  <span aria-hidden="true">${isActive ? symbol : emptySymbol}</span>
@@ -10,6 +10,7 @@ import { when } from "@inglorious/web/directives/when"
10
10
 
11
11
  import { Icon } from "../../data-display/icon/index.js"
12
12
  import { fallbackLabel } from "../../shared/fallback-label.js"
13
+ import { liveBoolean } from "../../shared/live-boolean.js"
13
14
  import { formatOption, getOptionValue, isOptionSelected } from "./helpers.js"
14
15
 
15
16
  /**
@@ -111,10 +112,6 @@ export const Select = {
111
112
  "iw-select-control-error": !!error,
112
113
  }
113
114
 
114
- const selectedOption = options.find(
115
- (option) => getOptionValue(option) === value,
116
- )
117
-
118
115
  return html`<div class="iw-select-frame" @click=${openFromArrowStrip}>
119
116
  ${when(icon, () => Icon.render({ children: icon, size }))}
120
117
 
@@ -137,15 +134,11 @@ export const Select = {
137
134
  (option) => getOptionValue(option),
138
135
  (option) => {
139
136
  const {
140
- value,
137
+ value: optionValue,
141
138
  label,
142
139
  isDisabled: isOptionDisabled,
143
140
  } = formatOption(option)
144
- const isSelected = isOptionSelected(
145
- option,
146
- selectedOption?.value,
147
- isMulti,
148
- )
141
+ const isSelected = isOptionSelected(option, value, isMulti)
149
142
 
150
143
  const optionClasses = {
151
144
  "iw-select-option": true,
@@ -153,8 +146,9 @@ export const Select = {
153
146
  }
154
147
 
155
148
  return html`<option
156
- value=${value}
157
- .selected=${value === selectedOption?.value}
149
+ value=${optionValue}
150
+ ?selected=${isSelected}
151
+ ${liveBoolean("selected", isSelected)}
158
152
  ?disabled=${isOptionDisabled}
159
153
  class=${classMap(optionClasses)}
160
154
  >
@@ -9,6 +9,7 @@ import { ref } from "@inglorious/web/directives/ref"
9
9
  import { when } from "@inglorious/web/directives/when"
10
10
 
11
11
  import { applyElementProps } from "../../shared/apply-element-props.js"
12
+ import { liveBoolean } from "../../shared/live-boolean.js"
12
13
 
13
14
  /**
14
15
  * Renders a toggle switch backed by a checkbox input, which `role="switch"`
@@ -49,7 +50,8 @@ export function render(props) {
49
50
  type="checkbox"
50
51
  role="switch"
51
52
  name=${name}
52
- .checked=${isChecked}
53
+ ?checked=${isChecked}
54
+ ${liveBoolean("checked", isChecked)}
53
55
  ?disabled=${isDisabled}
54
56
  @change=${(event) => onChange?.(event.target.checked)}
55
57
  ${ref((element) => applyElementProps(element, rest))}
@@ -13,6 +13,7 @@
13
13
  color: var(--iw-badge-on-accent);
14
14
  font-size: var(--iw-font-size-xs, 0.75rem);
15
15
  line-height: 1;
16
+ white-space: nowrap;
16
17
 
17
18
  &.iw-badge-sm {
18
19
  --iw-badge-shape-size: 0.5rem;
@@ -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
+ }