@inglorious/ui 9.7.3 → 9.8.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 (104) hide show
  1. package/package.json +5 -5
  2. package/src/controls/button/style.css +0 -2
  3. package/src/controls/button/template.js +2 -3
  4. package/src/controls/checkbox/style.css +0 -1
  5. package/src/controls/checkbox/template.js +0 -2
  6. package/src/controls/combobox/style.css +0 -14
  7. package/src/controls/combobox/template.js +0 -13
  8. package/src/controls/file-upload/template.js +1 -1
  9. package/src/controls/image-cropper/helpers.js +0 -2
  10. package/src/controls/image-cropper/image.js +0 -2
  11. package/src/controls/image-cropper/style.css +0 -6
  12. package/src/controls/image-cropper/template.js +3 -5
  13. package/src/controls/input/style.css +0 -1
  14. package/src/controls/radio-group/style.css +0 -2
  15. package/src/controls/rating/style.css +0 -3
  16. package/src/controls/select/style.css +0 -6
  17. package/src/controls/slider/style.css +0 -5
  18. package/src/controls/slider/template.js +0 -1
  19. package/src/controls/switch/style.css +0 -1
  20. package/src/controls/switch/template.js +0 -2
  21. package/src/controls/textarea/style.css +0 -2
  22. package/src/data-display/before-after/style.css +0 -4
  23. package/src/data-display/before-after/template.js +3 -3
  24. package/src/data-display/carousel/carousel.stories.js +0 -2
  25. package/src/data-display/carousel/handlers.js +0 -2
  26. package/src/data-display/carousel/style.css +0 -18
  27. package/src/data-display/carousel/template.js +0 -15
  28. package/src/data-display/chat/index.js +0 -1
  29. package/src/data-display/chat/style.css +0 -2
  30. package/src/data-display/chat/template.js +0 -1
  31. package/src/data-display/chip/template.js +0 -1
  32. package/src/data-display/data-grid/base.css +0 -2
  33. package/src/data-display/data-grid/handlers.js +1 -8
  34. package/src/data-display/data-grid/helpers.js +0 -5
  35. package/src/data-display/data-grid/template.js +0 -1
  36. package/src/data-display/data-grid/theme.css +0 -1
  37. package/src/data-display/icon/style.css +0 -7
  38. package/src/data-display/icon/template.js +2 -2
  39. package/src/data-display/table/style.css +0 -2
  40. package/src/data-display/tooltip/style.css +0 -3
  41. package/src/data-display/virtual-list/handlers.js +0 -2
  42. package/src/data-display/virtual-list/template.js +2 -4
  43. package/src/examples/dashboard/before-after-data.js +0 -3
  44. package/src/examples/dashboard/data.js +0 -1
  45. package/src/examples/dashboard/primitive-content.js +0 -2
  46. package/src/examples/dashboard/style.css +0 -3
  47. package/src/feedback/alert/style.css +0 -1
  48. package/src/feedback/dialog/dialog.stories.js +7 -2
  49. package/src/feedback/dialog/style.css +31 -18
  50. package/src/feedback/dialog/template.js +16 -16
  51. package/src/feedback/progress/style.css +0 -6
  52. package/src/feedback/progress/template.js +0 -3
  53. package/src/feedback/snackbar/snackbar.stories.js +0 -1
  54. package/src/feedback/snackbar/style.css +0 -10
  55. package/src/layout/flex/style.css +0 -2
  56. package/src/navigation/bottom-navigation/style.css +0 -7
  57. package/src/navigation/drawer/style.css +0 -21
  58. package/src/navigation/drawer/template.js +10 -9
  59. package/src/navigation/menu/menu.stories.js +0 -2
  60. package/src/navigation/menu/style.css +0 -4
  61. package/src/navigation/menu/template.js +0 -3
  62. package/src/navigation/speed-dial/style.css +0 -4
  63. package/src/navigation/speed-dial/template.js +0 -5
  64. package/src/navigation/tabs/template.js +0 -2
  65. package/src/navigation/view-transitions/index.js +0 -13
  66. package/src/navigation/view-transitions/style.css +0 -2
  67. package/src/shared/icons.js +0 -4
  68. package/src/styles/themes/bootstrap.css +0 -40
  69. package/src/styles/themes/inglorious.css +0 -21
  70. package/src/styles/themes/ios-18.css +0 -47
  71. package/src/styles/themes/ios-26.css +0 -55
  72. package/src/styles/themes/material-3.css +0 -52
  73. package/src/styles/themes/material-ui.css +0 -43
  74. package/src/styles/tokens.css +0 -40
  75. package/src/surfaces/accordion/style.css +0 -2
  76. package/src/surfaces/accordion/template.js +0 -4
  77. package/src/surfaces/app-bar/style.css +0 -3
  78. package/src/surfaces/card/style.css +0 -3
  79. package/types/controls/button-group.d.ts +5 -3
  80. package/types/controls/button.d.ts +3 -1
  81. package/types/controls/combobox.d.ts +13 -0
  82. package/types/controls/fab.d.ts +1 -1
  83. package/types/controls/icon-button.d.ts +7 -21
  84. package/types/controls/input.d.ts +2 -2
  85. package/types/data-display/avatar.d.ts +1 -1
  86. package/types/data-display/badge.d.ts +4 -2
  87. package/types/data-display/chip.d.ts +5 -3
  88. package/types/data-display/icon.d.ts +4 -2
  89. package/types/data-display/tooltip.d.ts +4 -2
  90. package/types/data-display/typography.d.ts +3 -1
  91. package/types/feedback/alert.d.ts +3 -1
  92. package/types/feedback/backdrop.d.ts +4 -2
  93. package/types/feedback/dialog.d.ts +11 -1
  94. package/types/index.d.ts +1 -0
  95. package/types/layout/container.d.ts +3 -1
  96. package/types/layout/flex.d.ts +9 -11
  97. package/types/layout/grid.d.ts +4 -2
  98. package/types/navigation/breadcrumbs.d.ts +1 -1
  99. package/types/navigation/drawer.d.ts +9 -1
  100. package/types/navigation/link.d.ts +4 -2
  101. package/types/shared/renderable.d.ts +12 -0
  102. package/types/surfaces/app-bar.d.ts +3 -1
  103. package/types/surfaces/card.d.ts +4 -2
  104. package/types/surfaces/paper.d.ts +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/ui",
3
- "version": "9.7.3",
3
+ "version": "9.8.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",
@@ -316,19 +316,19 @@
316
316
  "peerDependencies": {
317
317
  "@inglorious/store": "^12.1.1",
318
318
  "@inglorious/utils": "^5.1.0",
319
- "@inglorious/web": "^6.10.2"
319
+ "@inglorious/web": "^6.13.1"
320
320
  },
321
321
  "devDependencies": {
322
322
  "@iconify-icons/fa7-brands": "^2.0.0",
323
323
  "@iconify-icons/fa7-regular": "^2.0.0",
324
324
  "@iconify-icons/fa7-solid": "^2.0.0",
325
325
  "@iconify-icons/material-symbols": "^2.0.6",
326
- "@inglorious/charts": "12.2.5",
326
+ "@inglorious/charts": "12.2.6",
327
327
  "@inglorious/eslint-config": "1.2.0",
328
328
  "@inglorious/store": "12.1.1",
329
329
  "@inglorious/vite-plugin-jsx": "2.0.6",
330
330
  "@inglorious/vite-plugin-vue": "1.0.7",
331
- "@inglorious/web": "6.10.2",
331
+ "@inglorious/web": "6.13.1",
332
332
  "@storybook/addon-docs": "^10.6.0",
333
333
  "@storybook/web-components-vite": "^10.6.0",
334
334
  "happy-dom": "^20.14.0",
@@ -341,7 +341,7 @@
341
341
  "vitest": "^5.0.0"
342
342
  },
343
343
  "engines": {
344
- "node": ">= 22"
344
+ "node": ">= 26"
345
345
  },
346
346
  "scripts": {
347
347
  "dev": "vite --config app/vite.config.js",
@@ -60,7 +60,6 @@
60
60
  }
61
61
  }
62
62
 
63
- /* Neutral, unless a theme gives the default color its own tokens */
64
63
  &.iw-button-default {
65
64
  --iw-button-color: var(--iw-button-default-color, var(--iw-color-text));
66
65
  --iw-button-bg: var(--iw-button-default-bg, var(--iw-color-surface));
@@ -144,7 +143,6 @@
144
143
  &.iw-button-pressed {
145
144
  background-color: var(--iw-button-bg-active);
146
145
  color: var(--iw-button-text);
147
- /* box-shadow: var(--iw-button-shadow-active); */
148
146
  }
149
147
 
150
148
  &:disabled,
@@ -16,7 +16,7 @@ import { applyElementProps } from "../../shared/apply-element-props.js"
16
16
  * With `onFileSelect`, the button also opens a file picker on click.
17
17
  * @example
18
18
  * // Stateless usage
19
- * button.render({ children: 'Click me', variant: 'primary' })
19
+ * button.render({ children: 'Click me', color: 'primary' })
20
20
  *
21
21
  * // File picker
22
22
  * button.render({ children: 'Upload', accept: 'image/*', onFileSelect })
@@ -89,7 +89,7 @@ export function render(props) {
89
89
  () =>
90
90
  html`<input
91
91
  type="file"
92
- accept=${accept ?? ""}
92
+ accept=${accept ?? nothing}
93
93
  ?multiple=${isMultiple}
94
94
  ?disabled=${isDisabled}
95
95
  hidden
@@ -102,6 +102,5 @@ export function render(props) {
102
102
  function handleFileChange(event, onFileSelect) {
103
103
  const { files } = event.target
104
104
  if (files?.length) onFileSelect(files)
105
- // Clear the input so picking the same file again still fires `change`
106
105
  event.target.value = ""
107
106
  }
@@ -14,7 +14,6 @@
14
14
  accent-color: var(--iw-checkbox-accent);
15
15
  }
16
16
 
17
- /* With a description, the label and it stack beside the box. */
18
17
  .iw-checkbox-text {
19
18
  display: flex;
20
19
  flex-direction: column;
@@ -34,8 +34,6 @@ export function render(props) {
34
34
  ...rest
35
35
  } = props
36
36
 
37
- // The label wraps the input, which is what ties the two together: it needs no
38
- // `for`, and an empty one would point at nothing and break the click-through.
39
37
  const inputId = id || name
40
38
 
41
39
  const classes = {
@@ -5,7 +5,6 @@
5
5
  flex-direction: column;
6
6
  gap: var(--iw-space-1);
7
7
  font-family: var(--iw-font-family-sans);
8
- /* Each combobox anchors its own dropdown to its own frame. */
9
8
  anchor-scope: --iw-combobox;
10
9
 
11
10
  &.iw-combobox-full-width {
@@ -42,7 +41,6 @@
42
41
  anchor-name: --iw-combobox;
43
42
  display: flex;
44
43
  align-items: center;
45
- /* The gap spaces the text from the arrow, as it does for Input's icons. */
46
44
  gap: var(--iw-space-2);
47
45
  padding: 0 var(--iw-space-3);
48
46
  border: 1px solid var(--iw-input-border);
@@ -82,7 +80,6 @@
82
80
  box-shadow: var(--iw-input-shadow-focus);
83
81
  }
84
82
 
85
- /* The control is focusable so it can be opened from the keyboard. */
86
83
  &:focus-visible {
87
84
  outline: none;
88
85
  border-color: var(--iw-input-border-focus);
@@ -110,9 +107,6 @@
110
107
  }
111
108
  }
112
109
 
113
- /* The text element, whatever it is showing. Same role as
114
- .iw-input-control in Input, so the shared rules live here and the
115
- variant classes only add what is specific to them. */
116
110
  .iw-combobox-control {
117
111
  flex: 1;
118
112
  min-width: 0;
@@ -168,11 +162,6 @@
168
162
  justify-content: center;
169
163
  font-size: var(--iw-font-size-sm);
170
164
  line-height: 1;
171
- /* A flex child, like Input's icons and the Select arrow. Absolutely
172
- positioning it anchored to .iw-combobox rather than the frame, so
173
- the glyph drifted whenever a label sat above the control, and it
174
- needed a reserved padding strip to stop text running under it. The
175
- row's gap does both. */
176
165
  flex: none;
177
166
  pointer-events: none;
178
167
  color: var(--iw-color-text-muted);
@@ -184,9 +173,6 @@
184
173
  }
185
174
  }
186
175
 
187
- /* A popover in the top layer, placed under the frame and as wide as it,
188
- flipping above when there is no room below. The browser's popover box
189
- is centred in the viewport, so its insets, margin and colours are reset. */
190
176
  .iw-combobox-dropdown {
191
177
  position-anchor: --iw-combobox;
192
178
  position-area: block-end span-inline-end;
@@ -51,11 +51,6 @@ export const Combobox = {
51
51
  isFullWidth = false,
52
52
  isOpen = false,
53
53
  } = props
54
- // The control is a div, not a labellable element, so a `for` binding is not
55
- // available. It is referenced by id from the control's aria-labelledby
56
- // instead, which is what a screen reader actually reads here.
57
- // Only minted when there is something to point at. A reference to an id
58
- // that is not in the DOM is worse than no reference at all.
59
54
  const labelId = label && id ? `${id}-label` : ""
60
55
  const messageId = (error || hint) && id ? `${id}-message` : ""
61
56
 
@@ -535,25 +530,17 @@ function trackOutsideClick(props, el) {
535
530
 
536
531
  const existing = outsideListeners.get(el)
537
532
  if (existing) {
538
- // A re-render of the same dropdown: keep the one listener, but point it at
539
- // the current props so onClose is not captured from the first render.
540
533
  existing.props = props
541
534
  return
542
535
  }
543
536
 
544
537
  const entry = { props }
545
538
  entry.listener = (event) => {
546
- // The dropdown left the DOM: this press is the last one to hear from it.
547
- // Retiring here is what keeps a torn-down dropdown from closing whatever
548
- // opens next.
549
539
  if (!el.isConnected) {
550
540
  document.removeEventListener("pointerdown", entry.listener)
551
541
  return
552
542
  }
553
543
 
554
- // The root, not the dropdown: the control is a sibling of the dropdown, and a
555
- // press on it has to count as inside. Resolved per event rather than at mount
556
- // because the dropdown is not connected yet when the ref first runs.
557
544
  const root = el.closest(".iw-combobox") ?? el
558
545
  if (!event.composedPath().includes(root)) entry.props.onClose?.()
559
546
  }
@@ -79,7 +79,7 @@ export function render(props) {
79
79
  aria-describedby=${messageId || nothing}
80
80
  aria-invalid=${error ? "true" : "false"}
81
81
  type="file"
82
- accept=${accept ?? ""}
82
+ accept=${accept ?? nothing}
83
83
  ?multiple=${isMultiple}
84
84
  ?disabled=${isDisabled}
85
85
  ?required=${isRequired}
@@ -5,8 +5,6 @@
5
5
 
6
6
  import { clamp } from "@inglorious/utils/math/number.js"
7
7
 
8
- // Every rect is in fractions of the image (0-1 on both axes), so it survives
9
- // any display size and maps onto source pixels only when the image is cropped.
10
8
  export const DEFAULT_MIN_SIZE = 0.1
11
9
  export const DEFAULT_STEP = 0.02
12
10
  export const FULL_RECT = { x: 0, y: 0, w: 1, h: 1 }
@@ -50,7 +50,6 @@ export async function cropImage(file, rect, options = {}) {
50
50
  const context = canvas.getContext("2d")
51
51
  if (!context) throw new Error("Canvas is unavailable")
52
52
 
53
- // Formats without alpha would turn transparent pixels black.
54
53
  if (OPAQUE_TYPES.includes(type) && background) {
55
54
  context.fillStyle = background
56
55
  context.fillRect(0, 0, output.width, output.height)
@@ -86,7 +85,6 @@ export async function decodeImage(file) {
86
85
  try {
87
86
  return await createImageBitmap(file, { imageOrientation: "from-image" })
88
87
  } catch {
89
- // Some engines reject the options object; try the plain decoder.
90
88
  try {
91
89
  return await createImageBitmap(file)
92
90
  } catch {
@@ -1,11 +1,7 @@
1
1
  .iw-image-cropper {
2
- /* Set from the image's natural size once it loads. */
3
2
  --iw-image-cropper-aspect: 16 / 9;
4
- /* Caps the stage height; the width follows the image's proportions. */
5
3
  --iw-image-cropper-max-height: 70vh;
6
4
 
7
- /* Neutral in every theme on purpose, like the before/after divider: the
8
- frame must read on any photo without competing with it. */
9
5
  --iw-image-cropper-bg: var(--iw-color-gray-900);
10
6
  --iw-image-cropper-overlay: rgb(0 0 0 / 55%);
11
7
  --iw-image-cropper-frame-width: 2px;
@@ -57,8 +53,6 @@
57
53
  -webkit-user-drag: none;
58
54
  }
59
55
 
60
- /* The events land on the stage either way; the frame and handles only take
61
- pointer events so the cursor shows what a drag there would do. */
62
56
  .iw-image-cropper-frame {
63
57
  position: absolute;
64
58
  box-sizing: border-box;
@@ -4,7 +4,7 @@
4
4
  * @typedef {import('@inglorious/web').TemplateResult} TemplateResult
5
5
  */
6
6
 
7
- import { html } from "@inglorious/web"
7
+ import { html, nothing } from "@inglorious/web"
8
8
  import { classMap } from "@inglorious/web/directives/class-map"
9
9
  import { when } from "@inglorious/web/directives/when"
10
10
 
@@ -34,8 +34,6 @@ const ARROW_MOVES = {
34
34
  ArrowDown: [0, 1],
35
35
  }
36
36
 
37
- // Pointers and the drag in progress belong to one element and never reach the
38
- // store: they change on every move and are meaningless once the gesture ends.
39
37
  /** @type {WeakMap<Element, { pointers: Map<number, { x: number, y: number }>, drag: object | null, pinch: object | null }>} */
40
38
  const gestures = new WeakMap()
41
39
 
@@ -68,7 +66,7 @@ export const ImageCropper = {
68
66
  }
69
67
  const style = isLoaded
70
68
  ? `--iw-image-cropper-aspect: ${props.naturalWidth / props.naturalHeight};`
71
- : ""
69
+ : nothing
72
70
 
73
71
  return html`<div
74
72
  class=${classMap(classes)}
@@ -131,7 +129,7 @@ export const ImageCropper = {
131
129
  renderImage(props) {
132
130
  return html`<img
133
131
  class="iw-image-cropper-image"
134
- src=${props.src ?? ""}
132
+ src=${props.src ?? nothing}
135
133
  alt=${props.alt ?? ""}
136
134
  draggable="false"
137
135
  @load=${handleLoad(props)}
@@ -60,7 +60,6 @@
60
60
  }
61
61
 
62
62
  .iw-input-control {
63
- /* Fills the frame, which is now the box. */
64
63
  flex: 1;
65
64
  min-width: 0;
66
65
  font-family: inherit;
@@ -19,7 +19,6 @@
19
19
  cursor: pointer;
20
20
  }
21
21
 
22
- /* With a description, the label and it stack beside the circle. */
23
22
  .iw-radio-text {
24
23
  display: flex;
25
24
  flex-direction: column;
@@ -71,7 +70,6 @@
71
70
  --iw-radio-accent: var(--iw-color-info);
72
71
  }
73
72
 
74
- /* After the directions, which it overrides with a grid. */
75
73
  &.iw-radio-group-card {
76
74
  display: grid;
77
75
  grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
@@ -1,6 +1,3 @@
1
- /* A fieldset of native radios: the inputs stay in the page for the keyboard
2
- and assistive tech but out of sight, and each star is the label that
3
- checks its radio. */
4
1
  .iw-rating {
5
2
  display: inline-flex;
6
3
  align-items: center;
@@ -37,8 +37,6 @@
37
37
  .iw-select-frame {
38
38
  display: flex;
39
39
  align-items: center;
40
- /* The gap spaces the text from the arrow, the same way it spaces an
41
- Input's text from its icons. */
42
40
  gap: var(--iw-space-2);
43
41
  padding: 0 var(--iw-space-3);
44
42
  border: 1px solid var(--iw-input-border);
@@ -77,7 +75,6 @@
77
75
  }
78
76
  }
79
77
 
80
- /* The control is focusable so it can be opened from the keyboard. */
81
78
  &:focus-visible {
82
79
  outline: none;
83
80
  border-color: var(--iw-input-border-focus);
@@ -149,9 +146,6 @@
149
146
  justify-content: center;
150
147
  font-size: var(--iw-font-size-sm);
151
148
  line-height: 1;
152
- /* A flex child, like Input's icons. Absolutely positioning it needed a
153
- positioning context on the frame and a reserved padding strip so the
154
- text would not run underneath it; the row's gap does both. */
155
149
  flex: none;
156
150
  /* Decorative. A popup only opens from a trusted gesture on the select
157
151
  itself, so the arrow cannot open it; leaving clicks to pass through
@@ -23,8 +23,6 @@
23
23
  accent-color: var(--iw-slider-accent);
24
24
  }
25
25
 
26
- /* A tick per step, under the track: their ends sit under the thumb's ends
27
- of travel, half a thumb in from the track's. */
28
26
  .iw-slider-marks {
29
27
  display: flex;
30
28
  justify-content: space-between;
@@ -37,9 +35,6 @@
37
35
  }
38
36
  }
39
37
 
40
- /* Two thumbs on one track. The inputs lie on top of each other with their
41
- own tracks hidden, and only their thumbs take presses, so either can be
42
- dragged; the track below is drawn here, filled between the two. */
43
38
  &.iw-slider-range {
44
39
  --iw-slider-thumb: 1rem;
45
40
 
@@ -132,7 +132,6 @@ function renderRange({
132
132
  const span = max - min || DEFAULT_STEP
133
133
  const fill = `--iw-slider-low: ${((low - min) / span) * PERCENT}%; --iw-slider-high: ${((high - min) / span) * PERCENT}%`
134
134
  const endName = (end) => (name ? `${name}-${end}` : nothing)
135
- // Each thumb is named for its end of the range, after the slider's label.
136
135
  const endLabel = (end) =>
137
136
  typeof label === "string" && label ? `${label} ${end}` : end
138
137
 
@@ -51,7 +51,6 @@
51
51
  transform: translateX(calc(1rem * var(--iw-dir-sign, 1)));
52
52
  }
53
53
 
54
- /* With a description, the label and it stack beside the track. */
55
54
  .iw-switch-text {
56
55
  display: flex;
57
56
  flex-direction: column;
@@ -32,8 +32,6 @@ export function render(props) {
32
32
  ...rest
33
33
  } = props
34
34
 
35
- // The label wraps the input, which is what ties the two together: it needs no
36
- // `for`, and an empty one would point at nothing and break the click-through.
37
35
  const inputId = id || name
38
36
 
39
37
  const classes = {
@@ -4,7 +4,6 @@
4
4
  gap: var(--iw-space-1);
5
5
  font-family: var(--iw-font-family-sans);
6
6
 
7
- /* The field takes the width it is given, and the box inside fills it. */
8
7
  &.iw-textarea-full-width {
9
8
  width: 100%;
10
9
 
@@ -24,7 +23,6 @@
24
23
  margin-inline-start: var(--iw-space-1);
25
24
  }
26
25
 
27
- /* Its height is its rows', or its content's with isAutoSize. */
28
26
  .iw-textarea {
29
27
  width: min(100%, 100%);
30
28
  resize: none;
@@ -1,8 +1,6 @@
1
1
  .iw-before-after {
2
2
  --iw-before-after-position: 50%;
3
3
 
4
- /* Neutral in every theme on purpose: the divider should separate the two
5
- pictures without competing with them for attention. */
6
4
  --iw-before-after-divider-width: 2px;
7
5
  --iw-before-after-divider-color: rgb(255 255 255 / 90%);
8
6
  --iw-before-after-handle-size: 40px;
@@ -31,7 +29,6 @@
31
29
  width: 100%;
32
30
  }
33
31
 
34
- /* Greyed out and dimmed, so it reads as "you cannot move this". */
35
32
  &.iw-before-after-disabled {
36
33
  --iw-before-after-divider-color: rgb(156 163 175 / 60%);
37
34
  --iw-before-after-handle-bg: rgb(156 163 175 / 60%);
@@ -104,7 +101,6 @@
104
101
  box-shadow: var(--iw-shadow-md);
105
102
  }
106
103
 
107
- /* Two chevron arrows drawn with borders, no icon font needed. */
108
104
  .iw-before-after-handle::before,
109
105
  .iw-before-after-handle::after {
110
106
  content: "";
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { clamp } from "@inglorious/utils/math/number.js"
7
- import { html } from "@inglorious/web"
7
+ import { html, nothing } from "@inglorious/web"
8
8
  import { classMap } from "@inglorious/web/directives/class-map"
9
9
 
10
10
  import {
@@ -79,7 +79,7 @@ export const BeforeAfter = {
79
79
  return html`<div class="iw-before-after-item iw-before-after-before">
80
80
  <img
81
81
  class="iw-before-after-image"
82
- src=${props.before?.src ?? ""}
82
+ src=${props.before?.src ?? nothing}
83
83
  alt=${props.before?.alt ?? ""}
84
84
  draggable="false"
85
85
  />
@@ -95,7 +95,7 @@ export const BeforeAfter = {
95
95
  return html`<div class="iw-before-after-item iw-before-after-after">
96
96
  <img
97
97
  class="iw-before-after-image"
98
- src=${props.after?.src ?? ""}
98
+ src=${props.after?.src ?? nothing}
99
99
  alt=${props.after?.alt ?? ""}
100
100
  draggable="false"
101
101
  />
@@ -149,8 +149,6 @@ export const ManyPerView = {}
149
149
  ManyPerView.args = {
150
150
  ...Default.args,
151
151
  id: "carousel-many",
152
- // The infinite treadmill keeps one slide at its centre, so it is not a good
153
- // fit when several slides are intentionally visible at once.
154
152
  isInfinite: false,
155
153
  }
156
154
  ManyPerView.decorators = [
@@ -40,8 +40,6 @@ export function create(entity) {
40
40
  entity.hasIndicators ??= true
41
41
  entity.isFullWidth ??= false
42
42
 
43
- // An infinite carousel keeps the current item in the middle slot, so the
44
- // starting item is placed there by rotating the strip up front.
45
43
  if (entity.isInfinite && entity.items.length > ENOUGH_TO_WRAP) {
46
44
  const home = Math.floor(entity.items.length / 2)
47
45
  entity.rotation = normalizeRotation(entity.page - home, entity.items.length)
@@ -17,14 +17,11 @@
17
17
  width: 100%;
18
18
  }
19
19
 
20
- /* A column stage otherwise shrink-wraps to its arrows instead of the
21
- available inline size. */
22
20
  &.iw-carousel-vertical {
23
21
  display: block;
24
22
  width: 100%;
25
23
  }
26
24
 
27
- /* Snapping, swiping, momentum and RTL all come from the browser. */
28
25
  .iw-carousel-viewport {
29
26
  position: relative;
30
27
  display: flex;
@@ -45,19 +42,11 @@
45
42
  outline-offset: 2px;
46
43
  }
47
44
 
48
- /* One item per view by default, so every item can reach the start of the
49
- viewport and each one is exactly one page. Override
50
- --iw-carousel-item-size to show several at once. */
51
45
  .iw-carousel-item {
52
46
  flex: 0 0 var(--iw-carousel-item-size, 100%);
53
47
  scroll-snap-align: start;
54
48
  }
55
49
 
56
- /* Smooth scrolling would animate every drag step, so it is turned off while
57
- dragging. Snapping stays on: the browser then lands on the nearest slide by
58
- itself, and re-enabling it instead would snap back to the item it
59
- remembered. Doubled up on the class so this outranks the reduced-motion
60
- rule at the end of the file. */
61
50
  .iw-carousel-viewport.iw-carousel-viewport-dragging {
62
51
  scroll-behavior: auto;
63
52
  scroll-snap-type: none;
@@ -77,7 +66,6 @@
77
66
  overscroll-behavior-y: contain;
78
67
  }
79
68
 
80
- /* Gap tokens mirror Flex. */
81
69
  &.iw-carousel-gap-sm .iw-carousel-viewport {
82
70
  gap: var(--iw-space-2);
83
71
  }
@@ -94,7 +82,6 @@
94
82
  gap: var(--iw-space-8);
95
83
  }
96
84
 
97
- /* Off-axis alignment. */
98
85
  &.iw-carousel-align-start .iw-carousel-viewport {
99
86
  align-items: flex-start;
100
87
  }
@@ -107,7 +94,6 @@
107
94
  align-items: flex-end;
108
95
  }
109
96
 
110
- /* The stage holds the viewport and its arrows, so centring ignores the dots. */
111
97
  .iw-carousel-stage {
112
98
  position: relative;
113
99
  display: flex;
@@ -125,7 +111,6 @@
125
111
  min-width: 0;
126
112
  }
127
113
 
128
- /* Inside: float the arrows over the slides. */
129
114
  &.iw-carousel-arrows-inside .iw-carousel-stage {
130
115
  gap: 0;
131
116
  }
@@ -160,13 +145,11 @@
160
145
  bottom: var(--iw-carousel-arrow-inset);
161
146
  }
162
147
 
163
- /* Outside: the arrows sit beside the scrolling area, flex handles centring. */
164
148
  &.iw-carousel-arrows-outside .iw-carousel-arrow {
165
149
  position: static;
166
150
  flex: 0 0 auto;
167
151
  }
168
152
 
169
- /* Chevrons drawn with borders, no icon font needed. */
170
153
  .iw-carousel-chevron {
171
154
  display: block;
172
155
  width: 0;
@@ -225,7 +208,6 @@
225
208
  }
226
209
  }
227
210
 
228
- /* Respect reduced motion: only animate paging when the user allows it. */
229
211
  @media (prefers-reduced-motion: no-preference) {
230
212
  .iw-carousel .iw-carousel-viewport {
231
213
  scroll-behavior: smooth;
@@ -69,8 +69,6 @@ export const Carousel = {
69
69
  "iw-carousel-full-width": isFullWidth,
70
70
  }
71
71
 
72
- // The arrows live in the stage, next to the viewport, so that centring them
73
- // ignores the indicators underneath.
74
72
  return html`<div class=${classMap(classes)}>
75
73
  <div class="iw-carousel-stage">
76
74
  ${when(hasArrows, () => this.renderArrow?.(props, PREVIOUS))}
@@ -152,13 +150,10 @@ export const Carousel = {
152
150
  } = props
153
151
  const isPrevious = direction === PREVIOUS
154
152
 
155
- // Infinite carousels wrap, so their arrows never run out.
156
153
  const isDisabled =
157
154
  !isInfinite &&
158
155
  (isPrevious ? page <= FIRST_PAGE : page >= getLastPage(items))
159
156
 
160
- // The click is caught on the wrapper: Button's onClick carries no event, and
161
- // a disabled button emits no click at all, so nothing can leak through.
162
157
  return html`<span
163
158
  class=${classMap({
164
159
  "iw-carousel-arrow": true,
@@ -238,7 +233,6 @@ export const Carousel = {
238
233
  */
239
234
  function handleScroll(props) {
240
235
  return (event) => {
241
- // Infinite carousels re-centre on settle instead of tracking every scroll.
242
236
  if (props.isInfinite) return
243
237
 
244
238
  const page = pageFromScroll(event.currentTarget, props.axis)
@@ -257,7 +251,6 @@ function handleScrollEnd(props) {
257
251
  return (event) => {
258
252
  const viewport = event.currentTarget
259
253
 
260
- // Mid-drag pauses fire scrollend too; leave the drag alone until it ends.
261
254
  if (drags.has(viewport)) return
262
255
 
263
256
  viewport.classList.remove("iw-carousel-viewport-dragging")
@@ -341,7 +334,6 @@ function handlePointerDown(props) {
341
334
  scroll: isVertical ? viewport.scrollTop : viewport.scrollLeft,
342
335
  isVertical,
343
336
  })
344
- // Snapping would fight every move; it is restored on pointerup.
345
337
  viewport.classList.add("iw-carousel-viewport-dragging")
346
338
  viewport.setPointerCapture?.(event.pointerId)
347
339
  }
@@ -379,8 +371,6 @@ function handlePointerUp(props) {
379
371
  drags.delete(viewport)
380
372
  viewport.releasePointerCapture?.(event.pointerId)
381
373
 
382
- // Land on the nearest slide; handleScrollEnd restores snapping and, when
383
- // infinite, re-centres from there.
384
374
  goToPage(viewport, pageFromScroll(viewport, props.axis), props, "smooth")
385
375
  }
386
376
  }
@@ -415,9 +405,6 @@ function pageBy(props, step, from) {
415
405
  return
416
406
  }
417
407
 
418
- // Several items can be visible at once, so their later offsets may extend
419
- // past the physical scroll range. At the end, choosing the first such offset
420
- // would make a right arrow jump backwards after the browser clamps it.
421
408
  const limit = maxScroll(viewport, isVertical)
422
409
  if (step > 0 && limit > SCROLL_EPSILON && scroll >= limit - SCROLL_EPSILON) {
423
410
  return
@@ -503,8 +490,6 @@ function goToPage(viewport, page, props, behavior = "auto") {
503
490
  const item = viewport.children[index]
504
491
  if (!item) return
505
492
 
506
- // Guarded like setPointerCapture: real browsers always have scrollTo, but
507
- // non-layout environments such as jsdom do not.
508
493
  viewport.scrollTo?.(
509
494
  (props.axis ?? DEFAULT_AXIS) === VERTICAL_AXIS
510
495
  ? { top: item.offsetTop, behavior }
@@ -10,7 +10,6 @@ export const Chat = {
10
10
  ...entity,
11
11
  onDraftChange: (draft) => api.notify(`#${id}:draftChange`, draft),
12
12
  onSend: (text) => api.notify(`#${id}:messageSend`, text),
13
- // A message naming an entity, such as a generated answer, shows it.
14
13
  renderEntity: (entityId) => api.render(entityId),
15
14
  }
16
15
 
@@ -42,7 +42,6 @@
42
42
  min-inline-size: 0;
43
43
  }
44
44
 
45
- /* A message's own line breaks show; the template's around it do not. */
46
45
  .iw-chat-text {
47
46
  white-space: pre-wrap;
48
47
  }
@@ -85,7 +84,6 @@
85
84
  min-inline-size: 0;
86
85
  }
87
86
 
88
- /* The box grows with the message up to a point, then scrolls. */
89
87
  .iw-textarea {
90
88
  max-block-size: 10rem;
91
89
  }