@inglorious/ui 8.6.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/package.json +2 -2
  2. package/src/controls/button/button.stories.js +7 -1
  3. package/src/controls/button/style.css +11 -0
  4. package/src/controls/button/template.js +4 -4
  5. package/src/controls/checkbox/checkbox.stories.js +12 -0
  6. package/src/controls/checkbox/style.css +11 -0
  7. package/src/controls/checkbox/template.js +9 -1
  8. package/src/controls/combobox/style.css +14 -7
  9. package/src/controls/combobox/template.js +12 -2
  10. package/src/controls/radio-group/radio-group.stories.js +21 -1
  11. package/src/controls/radio-group/style.css +11 -0
  12. package/src/controls/radio-group/template.js +11 -1
  13. package/src/controls/rating/rating.stories.js +8 -0
  14. package/src/controls/rating/style.css +26 -4
  15. package/src/controls/rating/template.js +29 -12
  16. package/src/controls/slider/slider.stories.js +28 -1
  17. package/src/controls/slider/style.css +102 -0
  18. package/src/controls/slider/template.js +114 -15
  19. package/src/controls/switch/style.css +11 -0
  20. package/src/controls/switch/switch.stories.js +10 -0
  21. package/src/controls/switch/template.js +14 -2
  22. package/src/data-display/chip/chip.stories.js +17 -0
  23. package/src/data-display/chip/style.css +5 -0
  24. package/src/data-display/chip/template.js +3 -1
  25. package/src/data-display/divider/divider.stories.js +9 -0
  26. package/src/data-display/divider/template.js +12 -4
  27. package/src/data-display/tooltip/style.css +22 -14
  28. package/src/examples/dashboard/primitive-content.js +6 -6
  29. package/src/examples/dashboard/style.css +11 -5
  30. package/src/feedback/alert/alert.stories.js +9 -1
  31. package/src/feedback/alert/style.css +5 -0
  32. package/src/feedback/alert/template.js +2 -1
  33. package/src/feedback/dialog/dialog.stories.js +31 -0
  34. package/src/feedback/dialog/style.css +27 -13
  35. package/src/feedback/dialog/template.js +68 -18
  36. package/src/feedback/progress/progress.stories.js +5 -0
  37. package/src/feedback/progress/style.css +47 -23
  38. package/src/feedback/progress/template.js +18 -11
  39. package/src/feedback/snackbar/style.css +13 -0
  40. package/src/feedback/snackbar/template.js +7 -0
  41. package/src/layout/flex/flex.stories.js +1 -1
  42. package/src/layout/flex/style.css +8 -0
  43. package/src/navigation/drawer/drawer.stories.js +3 -7
  44. package/src/navigation/drawer/style.css +59 -18
  45. package/src/navigation/drawer/template.js +137 -80
  46. package/src/navigation/menu/menu.stories.js +15 -0
  47. package/src/navigation/menu/style.css +19 -0
  48. package/src/navigation/menu/template.js +59 -6
  49. package/src/navigation/speed-dial/style.css +32 -5
  50. package/src/navigation/speed-dial/template.js +41 -12
  51. package/src/navigation/tabs/template.js +52 -4
  52. package/src/shared/anchor.js +11 -0
  53. package/src/shared/top-layer.js +83 -0
  54. package/src/surfaces/accordion/accordion.stories.js +9 -0
  55. package/src/surfaces/accordion/style.css +14 -7
  56. package/src/surfaces/accordion/template.js +31 -20
  57. package/src/surfaces/card/card.stories.js +24 -0
  58. package/src/surfaces/card/style.css +15 -0
  59. package/src/surfaces/card/template.js +2 -0
  60. package/types/controls/button.d.ts +1 -1
  61. package/types/controls/checkbox.d.ts +2 -0
  62. package/types/controls/radio-group.d.ts +2 -0
  63. package/types/controls/rating.d.ts +4 -0
  64. package/types/controls/slider.d.ts +6 -2
  65. package/types/controls/switch.d.ts +2 -0
  66. package/types/data-display/chip.d.ts +2 -0
  67. package/types/data-display/divider.d.ts +2 -0
  68. package/types/feedback/alert.d.ts +1 -1
  69. package/types/feedback/dialog.d.ts +5 -1
  70. package/types/feedback/progress.d.ts +2 -0
  71. package/types/layout/flex.d.ts +1 -1
  72. package/types/navigation/drawer.d.ts +8 -3
  73. package/types/navigation/menu.d.ts +17 -0
  74. package/types/navigation/speed-dial.d.ts +5 -0
  75. package/types/surfaces/accordion.d.ts +2 -0
  76. package/types/surfaces/card.d.ts +5 -0
@@ -14,10 +14,17 @@ const DEFAULT_VALUE = 0
14
14
  const DEFAULT_MIN = 0
15
15
  const DEFAULT_MAX = 100
16
16
  const DEFAULT_STEP = 1
17
+ const PERCENT = 100
18
+ /** Past this many steps, marks would be a smear rather than a scale. */
19
+ const MAX_MARKS = 100
20
+ const RANGE_LENGTH = 2
17
21
 
18
22
  /**
19
23
  * Renders a range slider with optional label and current value display.
20
24
  * Supports min/max/step, color, disabled state, and full-width layout.
25
+ * With a `[low, high]` value it has two thumbs, one for each end of a range:
26
+ * two native range inputs on one track, so each keeps the keyboard and
27
+ * assistive tech support of the platform. `hasMarks` draws a tick per step.
21
28
  * @param {SliderProps} props
22
29
  * @returns {TemplateResult}
23
30
  */
@@ -35,14 +42,18 @@ export function render(props) {
35
42
  color = "primary",
36
43
  isValueVisible = true,
37
44
  isFullWidth = false,
45
+ hasMarks = false,
38
46
  onChange,
39
47
  ...rest
40
48
  } = props
41
49
 
42
50
  const inputId = id || name
51
+ const isRange = Array.isArray(value) && value.length === RANGE_LENGTH
52
+ const shown = isRange ? `${value[0]} – ${value[1]}` : value
43
53
 
44
54
  const classes = {
45
55
  "iw-slider": true,
56
+ "iw-slider-range": isRange,
46
57
  [`iw-slider-${color}`]: color !== "primary",
47
58
  "iw-slider-full-width": isFullWidth,
48
59
  }
@@ -59,25 +70,113 @@ export function render(props) {
59
70
  }
60
71
  ${
61
72
  isValueVisible
62
- ? html`<output class="iw-slider-value">${value}</output>`
73
+ ? html`<output class="iw-slider-value">${shown}</output>`
63
74
  : null
64
75
  }
65
76
  </div>`
66
77
  }
67
- <input
68
- id=${inputId || nothing}
69
- class="iw-slider-input"
70
- aria-label=${fallbackLabel(inputId, label)}
71
- type="range"
72
- name=${name}
73
- min=${min}
74
- max=${max}
75
- step=${step}
76
- .value=${String(value)}
77
- ?disabled=${isDisabled}
78
- @input=${(event) => onChange?.(Number(event.target.value))}
79
- ${ref((element) => applyElementProps(element, rest))}
80
- />
78
+ ${
79
+ isRange
80
+ ? renderRange({
81
+ inputId,
82
+ name,
83
+ label,
84
+ value,
85
+ min,
86
+ max,
87
+ step,
88
+ isDisabled,
89
+ onChange,
90
+ rest,
91
+ })
92
+ : html`<input
93
+ id=${inputId || nothing}
94
+ class="iw-slider-input"
95
+ aria-label=${fallbackLabel(inputId, label)}
96
+ type="range"
97
+ name=${name}
98
+ min=${min}
99
+ max=${max}
100
+ step=${step}
101
+ .value=${String(value)}
102
+ ?disabled=${isDisabled}
103
+ @input=${(event) => onChange?.(Number(event.target.value))}
104
+ ${ref((element) => applyElementProps(element, rest))}
105
+ />`
106
+ }
107
+ ${hasMarks ? renderMarks({ min, max, step }) : nothing}
81
108
  </div>
82
109
  `
83
110
  }
111
+
112
+ /**
113
+ * Two thumbs on one track. Each input stops at the other's value, so the
114
+ * range never turns inside out, and the track is filled between them.
115
+ */
116
+ function renderRange({
117
+ inputId,
118
+ name,
119
+ label,
120
+ value,
121
+ min,
122
+ max,
123
+ step,
124
+ isDisabled,
125
+ onChange,
126
+ rest,
127
+ }) {
128
+ const [low, high] = value
129
+ const span = max - min || DEFAULT_STEP
130
+ const fill = `--iw-slider-low: ${((low - min) / span) * PERCENT}%; --iw-slider-high: ${((high - min) / span) * PERCENT}%`
131
+ const endName = (end) => (name ? `${name}-${end}` : nothing)
132
+ // Each thumb is named for its end of the range, after the slider's label.
133
+ const endLabel = (end) =>
134
+ typeof label === "string" && label ? `${label} ${end}` : end
135
+
136
+ return html`<div class="iw-slider-track" style=${fill}>
137
+ <input
138
+ id=${inputId || nothing}
139
+ class="iw-slider-input"
140
+ aria-label=${endLabel("minimum")}
141
+ type="range"
142
+ name=${endName("min")}
143
+ min=${min}
144
+ max=${max}
145
+ step=${step}
146
+ .value=${String(low)}
147
+ ?disabled=${isDisabled}
148
+ @input=${(event) => {
149
+ const next = Math.min(Number(event.target.value), high)
150
+ event.target.value = String(next)
151
+ onChange?.([next, high])
152
+ }}
153
+ ${ref((element) => applyElementProps(element, rest))}
154
+ />
155
+ <input
156
+ class="iw-slider-input"
157
+ aria-label=${endLabel("maximum")}
158
+ type="range"
159
+ name=${endName("max")}
160
+ min=${min}
161
+ max=${max}
162
+ step=${step}
163
+ .value=${String(high)}
164
+ ?disabled=${isDisabled}
165
+ @input=${(event) => {
166
+ const next = Math.max(Number(event.target.value), low)
167
+ event.target.value = String(next)
168
+ onChange?.([low, next])
169
+ }}
170
+ />
171
+ </div>`
172
+ }
173
+
174
+ /** A tick per step, spread along the track. Decorative. */
175
+ function renderMarks({ min, max, step }) {
176
+ const count = Math.floor((max - min) / step) + 1
177
+ if (!(count > 1) || count > MAX_MARKS) return nothing
178
+
179
+ return html`<div class="iw-slider-marks" aria-hidden="true">
180
+ ${Array.from({ length: count }, () => html`<span></span>`)}
181
+ </div>`
182
+ }
@@ -43,6 +43,17 @@
43
43
  transform: translateX(calc(1rem * var(--iw-dir-sign, 1)));
44
44
  }
45
45
 
46
+ /* With a description, the label and it stack beside the track. */
47
+ .iw-switch-text {
48
+ display: flex;
49
+ flex-direction: column;
50
+ }
51
+
52
+ .iw-switch-description {
53
+ font-size: var(--iw-font-size-sm);
54
+ color: var(--iw-color-text-secondary);
55
+ }
56
+
46
57
  .iw-switch-label {
47
58
  color: var(--iw-color-text);
48
59
  font-size: var(--iw-font-size-base);
@@ -7,6 +7,10 @@ export default {
7
7
  render: createRender(Switch),
8
8
  argTypes: {
9
9
  label: { control: "text", description: "Label text." },
10
+ description: {
11
+ control: "text",
12
+ description: "A line of detail under the label.",
13
+ },
10
14
  name: { control: "text", description: "Native input name." },
11
15
  isChecked: { control: "boolean", description: "Current checked state." },
12
16
  isDisabled: { control: "boolean", description: "Disables interaction." },
@@ -46,3 +50,9 @@ On.args = {
46
50
  ...Default.args,
47
51
  isChecked: true,
48
52
  }
53
+
54
+ export const WithDescription = {}
55
+ WithDescription.args = {
56
+ ...Default.args,
57
+ description: "Alerts when a payment is due or a budget runs out.",
58
+ }
@@ -10,7 +10,8 @@ import { ref } from "@inglorious/web/directives/ref"
10
10
  import { applyElementProps } from "../../shared/apply-element-props.js"
11
11
 
12
12
  /**
13
- * Renders a toggle switch backed by a checkbox input.
13
+ * Renders a toggle switch backed by a checkbox input, which `role="switch"`
14
+ * has assistive tech announce as on/off rather than checked.
14
15
  * Use it for boolean settings with optional labels and disabled state.
15
16
  * @param {SwitchProps} props
16
17
  * @returns {TemplateResult}
@@ -21,6 +22,7 @@ export function render(props) {
21
22
  type, // eslint-disable-line no-unused-vars
22
23
  name = "",
23
24
  label = "",
25
+ description,
24
26
  isChecked = false,
25
27
  isDisabled = false,
26
28
  color = "primary",
@@ -46,6 +48,7 @@ export function render(props) {
46
48
  id=${inputId || nothing}
47
49
  class="iw-switch-input"
48
50
  type="checkbox"
51
+ role="switch"
49
52
  name=${name}
50
53
  .checked=${isChecked}
51
54
  ?disabled=${isDisabled}
@@ -55,7 +58,16 @@ export function render(props) {
55
58
  <span class="iw-switch-track" aria-hidden="true">
56
59
  <span class="iw-switch-thumb"></span>
57
60
  </span>
58
- ${label ? html`<span class="iw-switch-label">${label}</span>` : null}
61
+ ${
62
+ description
63
+ ? html`<span class="iw-switch-text">
64
+ ${label ? html`<span class="iw-switch-label">${label}</span>` : null}
65
+ <span class="iw-switch-description">${description}</span>
66
+ </span>`
67
+ : label
68
+ ? html`<span class="iw-switch-label">${label}</span>`
69
+ : null
70
+ }
59
71
  </label>
60
72
  `
61
73
  }
@@ -1,4 +1,7 @@
1
+ import sell from "@iconify-icons/material-symbols/sell"
2
+
1
3
  import { createRender } from "../../stories/notify-story.js"
4
+ import { Icon } from "../icon"
2
5
  import { Chip } from "."
3
6
 
4
7
  export default {
@@ -7,6 +10,10 @@ export default {
7
10
  render: createRender(Chip),
8
11
  argTypes: {
9
12
  children: { control: "text", description: "Chip label content." },
13
+ icon: {
14
+ control: false,
15
+ description: "Shown before the label, such as an `Icon.render(...)`.",
16
+ },
10
17
  isRemovable: {
11
18
  control: "boolean",
12
19
  description: "Shows a remove affordance and emits `#id:remove`.",
@@ -55,3 +62,13 @@ export const Default = {
55
62
  shape: "pill",
56
63
  },
57
64
  }
65
+
66
+ export const WithIcon = {
67
+ args: {
68
+ ...Default.args,
69
+ children: "Paid",
70
+ icon: Icon.render({ icon: sell, size: "sm" }),
71
+ isRemovable: false,
72
+ color: "success",
73
+ },
74
+ }
@@ -10,6 +10,11 @@
10
10
  line-height: 1;
11
11
  white-space: nowrap;
12
12
 
13
+ .iw-chip-icon {
14
+ display: inline-flex;
15
+ align-items: center;
16
+ }
17
+
13
18
  .iw-chip-label {
14
19
  display: inline-flex;
15
20
  align-items: center;
@@ -7,7 +7,7 @@ import { html } from "@inglorious/web"
7
7
  import { classMap } from "@inglorious/web/directives/class-map"
8
8
 
9
9
  /**
10
- * Renders a compact chip label with optional remove action.
10
+ * Renders a compact chip label with an optional leading icon and remove action.
11
11
  * Use chips for tags, filters, or selected values.
12
12
  * @param {ChipProps} props
13
13
  * @returns {TemplateResult}
@@ -15,6 +15,7 @@ import { classMap } from "@inglorious/web/directives/class-map"
15
15
  export function render(props) {
16
16
  const {
17
17
  children,
18
+ icon,
18
19
  isRemovable = false,
19
20
  color = "default",
20
21
  size = "md",
@@ -30,6 +31,7 @@ export function render(props) {
30
31
  }
31
32
 
32
33
  return html`<span class=${classMap(classes)}>
34
+ ${icon ? html`<span class="iw-chip-icon" aria-hidden="true">${icon}</span>` : null}
33
35
  <span class="iw-chip-label">${children}</span>
34
36
  ${
35
37
  isRemovable
@@ -17,6 +17,11 @@ export default {
17
17
  control: "boolean",
18
18
  description: "Applies start inset for sectional separators.",
19
19
  },
20
+ isDecorative: {
21
+ control: "boolean",
22
+ description:
23
+ "Only visual: hidden from assistive tech, which otherwise announces a separator.",
24
+ },
20
25
  },
21
26
  parameters: {
22
27
  docs: {
@@ -31,6 +36,10 @@ export const Default = {
31
36
  args: {},
32
37
  }
33
38
 
39
+ export const Decorative = {
40
+ args: { isDecorative: true },
41
+ }
42
+
34
43
  export const Vertical = {
35
44
  args: {
36
45
  ...Default.args,
@@ -3,23 +3,31 @@
3
3
  * @typedef {import('@inglorious/web').TemplateResult} TemplateResult
4
4
  */
5
5
 
6
- import { html } from "@inglorious/web"
6
+ import { html, nothing } from "@inglorious/web"
7
7
  import { classMap } from "@inglorious/web/directives/class-map"
8
8
 
9
9
  /**
10
- * Renders a horizontal or vertical divider line.
10
+ * Renders a horizontal or vertical divider line: a native `<hr>`, which
11
+ * assistive tech announces as a separator, unless it is only decorative.
11
12
  * Supports inset styling for aligned content blocks.
12
13
  * @param {DividerProps} props
13
14
  * @returns {TemplateResult}
14
15
  */
15
16
  export function render(props) {
16
- const { orientation = "horizontal", isInset = false } = props
17
+ const {
18
+ orientation = "horizontal",
19
+ isInset = false,
20
+ isDecorative = false,
21
+ } = props
22
+ const isVertical = orientation === "vertical"
17
23
 
18
24
  return html`<hr
19
25
  class=${classMap({
20
26
  "iw-divider": true,
21
- "iw-divider-vertical": orientation === "vertical",
27
+ "iw-divider-vertical": isVertical,
22
28
  "iw-divider-inset": isInset,
23
29
  })}
30
+ role=${isDecorative ? "none" : nothing}
31
+ aria-orientation=${isVertical && !isDecorative ? "vertical" : nothing}
24
32
  />`
25
33
  }
@@ -1,10 +1,22 @@
1
+ /* The bubble is anchored to the trigger by CSS: the browser places it on the
2
+ requested side and flips it to the opposite one when it would leave the
3
+ viewport. Fixed, so a container that clips its overflow does not cut it. */
1
4
  .iw-tooltip {
2
- position: relative;
5
+ anchor-scope: --iw-tooltip;
3
6
  display: inline-flex;
4
7
  align-items: center;
5
8
 
9
+ .iw-tooltip-trigger {
10
+ anchor-name: --iw-tooltip;
11
+ }
12
+
6
13
  .iw-tooltip-bubble {
7
- position: absolute;
14
+ position: fixed;
15
+ position-anchor: --iw-tooltip;
16
+ position-area: top;
17
+ position-try-fallbacks: flip-block;
18
+ margin-block-end: var(--iw-space-2);
19
+ width: max-content;
8
20
  z-index: var(--iw-z-tooltip);
9
21
  opacity: 0;
10
22
  pointer-events: none;
@@ -15,9 +27,6 @@
15
27
  padding: var(--iw-space-2) var(--iw-space-3);
16
28
  box-shadow: var(--iw-shadow-md);
17
29
  transition: opacity 150ms ease;
18
- left: 50%;
19
- bottom: calc(100% + var(--iw-space-2));
20
- transform: translateX(-50%);
21
30
  white-space: normal;
22
31
  }
23
32
 
@@ -38,20 +47,19 @@
38
47
  }
39
48
 
40
49
  &.iw-tooltip-bottom .iw-tooltip-bubble {
41
- top: calc(100% + var(--iw-space-2));
42
- bottom: auto;
50
+ position-area: bottom;
51
+ margin: var(--iw-space-2) 0 0;
43
52
  }
44
53
 
45
54
  &.iw-tooltip-left .iw-tooltip-bubble {
46
- left: auto;
47
- right: calc(100% + var(--iw-space-2));
48
- bottom: 50%;
49
- transform: translateY(50%);
55
+ position-area: left;
56
+ position-try-fallbacks: flip-inline;
57
+ margin: 0 var(--iw-space-2) 0 0;
50
58
  }
51
59
 
52
60
  &.iw-tooltip-right .iw-tooltip-bubble {
53
- left: calc(100% + var(--iw-space-2));
54
- bottom: 50%;
55
- transform: translateY(50%);
61
+ position-area: right;
62
+ position-try-fallbacks: flip-inline;
63
+ margin: 0 0 0 var(--iw-space-2);
56
64
  }
57
65
  }
@@ -1028,6 +1028,7 @@ Dialog.render({
1028
1028
  preview: () =>
1029
1029
  Dialog.render({
1030
1030
  isOpen: true,
1031
+ isModal: false,
1031
1032
  title: "Delete record",
1032
1033
  description: "This action cannot be undone.",
1033
1034
  actions: previewRow([
@@ -1173,8 +1174,7 @@ Drawer.render({
1173
1174
  })`,
1174
1175
  preview: () =>
1175
1176
  Drawer.render({
1176
- isOpen: true,
1177
- hasBackdrop: false,
1177
+ variant: "permanent",
1178
1178
  title: "Navigation",
1179
1179
  children: List.render({
1180
1180
  items: [
@@ -1225,7 +1225,8 @@ Link.render({
1225
1225
  code: `import { Menu } from "@inglorious/ui/menu"
1226
1226
 
1227
1227
  Menu.render({
1228
- isOpen: true,
1228
+ id: "record-actions",
1229
+ trigger: "Actions",
1229
1230
  items: [
1230
1231
  { label: "Edit" },
1231
1232
  { label: "Duplicate" },
@@ -1235,7 +1236,8 @@ Menu.render({
1235
1236
  })`,
1236
1237
  preview: () =>
1237
1238
  Menu.render({
1238
- isOpen: true,
1239
+ id: "record-actions",
1240
+ trigger: "Actions",
1239
1241
  items: [
1240
1242
  { label: "Edit" },
1241
1243
  { label: "Duplicate" },
@@ -1268,7 +1270,6 @@ Pagination.render({
1268
1270
  code: `import { SpeedDial } from "@inglorious/ui/speed-dial"
1269
1271
 
1270
1272
  SpeedDial.render({
1271
- isOpen: true,
1272
1273
  actions: [
1273
1274
  { label: "Add", icon: "+" },
1274
1275
  { label: "Share", icon: "↗" }
@@ -1276,7 +1277,6 @@ SpeedDial.render({
1276
1277
  })`,
1277
1278
  preview: () =>
1278
1279
  SpeedDial.render({
1279
- isOpen: true,
1280
1280
  actions: [
1281
1281
  { label: "Add", icon: "+" },
1282
1282
  { label: "Share", icon: "↗" },
@@ -251,17 +251,23 @@
251
251
  }
252
252
 
253
253
  .iw-primitive-preview .iw-backdrop,
254
- .iw-primitive-preview .iw-dialog-backdrop,
255
254
  .iw-primitive-preview .iw-snackbar,
256
255
  .iw-primitive-preview .iw-drawer {
257
256
  position: absolute;
258
257
  }
259
258
 
260
- .iw-primitive-preview .iw-backdrop,
261
- .iw-primitive-preview .iw-dialog-backdrop {
259
+ .iw-primitive-preview .iw-backdrop {
262
260
  inset: 0;
263
261
  }
264
262
 
263
+ /* Shown in place (isModal: false), so it can sit in the preview's flow
264
+ rather than where the browser positions a non-modal dialog. */
265
+ .iw-primitive-preview .iw-dialog {
266
+ position: static;
267
+ min-width: 0;
268
+ max-width: 100%;
269
+ }
270
+
265
271
  .iw-primitive-preview .iw-drawer {
266
272
  top: var(--iw-space-3);
267
273
  bottom: var(--iw-space-3);
@@ -278,8 +284,8 @@
278
284
  color: var(--iw-color-text-secondary);
279
285
  }
280
286
 
281
- .iw-dashboard-progress-list .iw-progress-linear-track {
282
- height: 0.25rem;
287
+ .iw-dashboard-progress-list .iw-progress-linear {
288
+ --iw-progress-height: 0.25rem;
283
289
  }
284
290
 
285
291
  .iw-dashboard-user-subtitle {
@@ -10,7 +10,7 @@ export default {
10
10
  description: { control: "text" },
11
11
  severity: {
12
12
  control: "select",
13
- options: ["info", "success", "warning", "error"],
13
+ options: ["info", "success", "warning", "error", "neutral"],
14
14
  },
15
15
  variant: { control: "select", options: ["filled", "outlined"] },
16
16
  icon: { control: "text" },
@@ -50,6 +50,14 @@ export const Error = {
50
50
  },
51
51
  }
52
52
 
53
+ export const Neutral = {
54
+ args: {
55
+ title: "Note",
56
+ description: "A plain remark, with no tone of its own.",
57
+ severity: "neutral",
58
+ },
59
+ }
60
+
53
61
  export const Outlined = {
54
62
  args: {
55
63
  title: "Outlined Alert",
@@ -81,6 +81,11 @@
81
81
  --iw-alert-color: var(--iw-color-error, #d14343);
82
82
  }
83
83
 
84
+ /* A plain note: no tone of its own, just the text colour. */
85
+ .iw-alert-neutral {
86
+ --iw-alert-color: var(--iw-color-text-secondary, var(--iw-color-text));
87
+ }
88
+
84
89
  .iw-alert-outlined {
85
90
  background: transparent;
86
91
  border-color: color-mix(in srgb, var(--iw-alert-color) 55%, transparent);
@@ -11,7 +11,8 @@ import { applyElementProps } from "../../shared/apply-element-props.js"
11
11
 
12
12
  /**
13
13
  * Renders an alert message with optional title, description, icon, and actions.
14
- * Supports severity and variant styles for info, success, warning, and error.
14
+ * Supports severity and variant styles for info, success, warning, error and
15
+ * neutral, a plain note.
15
16
  * @param {AlertProps} props
16
17
  * @returns {TemplateResult}
17
18
  */
@@ -9,15 +9,19 @@ export default {
9
9
  render: createRender(Dialog),
10
10
  argTypes: {
11
11
  isOpen: { control: "boolean" },
12
+ isModal: { control: "boolean" },
13
+ size: { control: "select", options: ["sm", "md", "lg"] },
12
14
  title: { control: "text" },
13
15
  description: { control: "text" },
14
16
  onClose: { action: "onClose" },
15
17
  },
16
18
  }
17
19
 
20
+ // In place, so the docs page that shows every story stays usable.
18
21
  export const Open = {
19
22
  args: {
20
23
  isOpen: true,
24
+ isModal: false,
21
25
  title: "Delete item",
22
26
  description: "This action cannot be undone.",
23
27
  children: html`<p>Are you sure you want to continue?</p>`,
@@ -25,3 +29,30 @@ export const Open = {
25
29
  <button class="iw-button iw-button-primary">Confirm</button>`,
26
30
  },
27
31
  }
32
+
33
+ export const Small = {
34
+ args: { ...Open.args, size: "sm" },
35
+ }
36
+
37
+ export const Medium = {
38
+ args: { ...Open.args, size: "md" },
39
+ }
40
+
41
+ export const Large = {
42
+ args: {
43
+ ...Open.args,
44
+ size: "lg",
45
+ title: "Release notes",
46
+ description: "What changed in this version.",
47
+ children: html`<p>
48
+ A wider dialog, for content that needs the room: tables, previews or
49
+ longer text.
50
+ </p>`,
51
+ },
52
+ }
53
+
54
+ // Modal takes over the page, so it is left out of the docs page.
55
+ export const Modal = {
56
+ tags: ["!autodocs"],
57
+ args: { ...Open.args, isModal: true },
58
+ }
@@ -1,23 +1,37 @@
1
- .iw-dialog-backdrop {
2
- position: fixed;
3
- inset: 0;
4
- background: color-mix(in srgb, var(--iw-color-black) 55%, transparent);
5
- display: flex;
6
- align-items: center;
7
- justify-content: center;
8
- z-index: var(--iw-z-modal, 1050);
9
- padding: var(--iw-space-4);
10
- backdrop-filter: blur(2px);
11
- }
12
-
1
+ /* A native <dialog>, so the browser's own box is reset: the surface inside
2
+ carries the padding, which leaves the dialog element itself reachable only
3
+ through the backdrop. Modal dialogs sit in the top layer, above any
4
+ z-index, and the browser centres them. */
13
5
  .iw-dialog {
14
6
  background: var(--iw-color-surface);
15
7
  color: var(--iw-color-text);
16
8
  border-radius: var(--iw-radius-md);
17
9
  border: 1px solid color-mix(in srgb, var(--iw-color-border) 70%, transparent);
10
+ box-shadow: var(--iw-shadow-lg, 0 12px 40px rgba(0, 0, 0, 0.3));
11
+ padding: 0;
12
+ }
13
+
14
+ .iw-dialog-sm {
15
+ min-width: min(20rem, 90vw);
16
+ max-width: min(28rem, 90vw);
17
+ }
18
+
19
+ .iw-dialog-md {
18
20
  min-width: min(32rem, 90vw);
19
21
  max-width: min(48rem, 90vw);
20
- box-shadow: var(--iw-shadow-lg, 0 12px 40px rgba(0, 0, 0, 0.3));
22
+ }
23
+
24
+ .iw-dialog-lg {
25
+ min-width: min(40rem, 90vw);
26
+ max-width: min(64rem, 90vw);
27
+ }
28
+
29
+ .iw-dialog::backdrop {
30
+ background: color-mix(in srgb, var(--iw-color-black) 55%, transparent);
31
+ backdrop-filter: blur(2px);
32
+ }
33
+
34
+ .iw-dialog-surface {
21
35
  padding: var(--iw-space-4);
22
36
  }
23
37