@inglorious/ui 9.0.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 (46) 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/checkbox/checkbox.stories.js +12 -0
  5. package/src/controls/checkbox/style.css +11 -0
  6. package/src/controls/checkbox/template.js +9 -1
  7. package/src/controls/radio-group/radio-group.stories.js +21 -1
  8. package/src/controls/radio-group/style.css +11 -0
  9. package/src/controls/radio-group/template.js +11 -1
  10. package/src/controls/rating/rating.stories.js +8 -0
  11. package/src/controls/slider/slider.stories.js +28 -1
  12. package/src/controls/slider/style.css +102 -0
  13. package/src/controls/slider/template.js +114 -15
  14. package/src/controls/switch/style.css +11 -0
  15. package/src/controls/switch/switch.stories.js +10 -0
  16. package/src/controls/switch/template.js +11 -1
  17. package/src/data-display/chip/chip.stories.js +17 -0
  18. package/src/data-display/chip/style.css +5 -0
  19. package/src/data-display/chip/template.js +3 -1
  20. package/src/data-display/divider/divider.stories.js +9 -0
  21. package/src/data-display/divider/template.js +12 -4
  22. package/src/feedback/alert/alert.stories.js +9 -1
  23. package/src/feedback/alert/style.css +5 -0
  24. package/src/feedback/alert/template.js +2 -1
  25. package/src/feedback/dialog/dialog.stories.js +22 -0
  26. package/src/feedback/dialog/style.css +15 -2
  27. package/src/feedback/dialog/template.js +6 -1
  28. package/src/feedback/progress/progress.stories.js +5 -0
  29. package/src/layout/flex/flex.stories.js +1 -1
  30. package/src/layout/flex/style.css +8 -0
  31. package/src/surfaces/accordion/accordion.stories.js +9 -0
  32. package/src/surfaces/accordion/template.js +5 -1
  33. package/src/surfaces/card/card.stories.js +24 -0
  34. package/src/surfaces/card/style.css +15 -0
  35. package/src/surfaces/card/template.js +2 -0
  36. package/types/controls/button.d.ts +1 -1
  37. package/types/controls/checkbox.d.ts +2 -0
  38. package/types/controls/radio-group.d.ts +2 -0
  39. package/types/controls/slider.d.ts +6 -2
  40. package/types/controls/switch.d.ts +2 -0
  41. package/types/data-display/chip.d.ts +2 -0
  42. package/types/data-display/divider.d.ts +2 -0
  43. package/types/feedback/alert.d.ts +1 -1
  44. package/types/feedback/dialog.d.ts +2 -0
  45. package/types/layout/flex.d.ts +1 -1
  46. package/types/surfaces/card.d.ts +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/ui",
3
- "version": "9.0.0",
3
+ "version": "9.1.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",
@@ -315,7 +315,7 @@
315
315
  "@iconify-icons/fa7-regular": "^2.0.0",
316
316
  "@iconify-icons/fa7-solid": "^2.0.0",
317
317
  "@iconify-icons/material-symbols": "^2.0.6",
318
- "@inglorious/charts": "11.1.7",
318
+ "@inglorious/charts": "12.0.0",
319
319
  "@inglorious/eslint-config": "1.2.0",
320
320
  "@inglorious/store": "12.0.0",
321
321
  "@inglorious/vite-plugin-jsx": "2.0.5",
@@ -32,7 +32,7 @@ export default {
32
32
  },
33
33
  size: {
34
34
  control: "select",
35
- options: ["sm", "md", "lg"],
35
+ options: ["xs", "sm", "md", "lg"],
36
36
  description: "Size scale for paddings and font-size.",
37
37
  },
38
38
  shape: {
@@ -106,6 +106,12 @@ Ghost.args = {
106
106
  variant: "ghost",
107
107
  }
108
108
 
109
+ export const ExtraSmall = {}
110
+ ExtraSmall.args = {
111
+ ...Default.args,
112
+ size: "xs",
113
+ }
114
+
109
115
  export const Small = {}
110
116
  Small.args = {
111
117
  ...Default.args,
@@ -153,6 +153,11 @@
153
153
  cursor: not-allowed;
154
154
  }
155
155
 
156
+ &.iw-button-xs {
157
+ padding: var(--iw-space-0) var(--iw-space-2);
158
+ font-size: var(--iw-font-size-xs);
159
+ }
160
+
156
161
  &.iw-button-sm {
157
162
  padding: var(--iw-space-1) var(--iw-space-3);
158
163
  font-size: var(--iw-font-size-sm);
@@ -179,6 +184,12 @@
179
184
  border-radius: var(--iw-radius-sm);
180
185
  }
181
186
 
187
+ &.iw-button-xs.iw-button-shape-round,
188
+ &.iw-button-xs.iw-button-shape-square {
189
+ width: 1.5rem;
190
+ height: 1.5rem;
191
+ }
192
+
182
193
  &.iw-button-sm.iw-button-shape-round,
183
194
  &.iw-button-sm.iw-button-shape-square {
184
195
  width: 2rem;
@@ -7,6 +7,10 @@ export default {
7
7
  render: createRender(Checkbox),
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 HTML name attribute." },
11
15
  isChecked: { control: "boolean", description: "Current checked state." },
12
16
  isDisabled: { control: "boolean", description: "Disables interaction." },
@@ -48,3 +52,11 @@ Checked.args = {
48
52
  ...Default.args,
49
53
  isChecked: true,
50
54
  }
55
+
56
+ export const WithDescription = {}
57
+ WithDescription.args = {
58
+ ...Default.args,
59
+ label: "Weekly summary",
60
+ name: "summary",
61
+ description: "An email every Monday with last week's totals.",
62
+ }
@@ -14,6 +14,17 @@
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
+ .iw-checkbox-text {
19
+ display: flex;
20
+ flex-direction: column;
21
+ }
22
+
23
+ .iw-checkbox-description {
24
+ font-size: var(--iw-font-size-sm);
25
+ color: var(--iw-color-text-secondary);
26
+ }
27
+
17
28
  .iw-checkbox-label {
18
29
  font-size: var(--iw-font-size-base);
19
30
  line-height: var(--iw-line-height-normal);
@@ -21,6 +21,7 @@ export function render(props) {
21
21
  type, // eslint-disable-line no-unused-vars
22
22
  name = "",
23
23
  label = "",
24
+ description,
24
25
  isChecked = false,
25
26
  isDisabled = false,
26
27
  isRequired = false,
@@ -54,7 +55,14 @@ export function render(props) {
54
55
  @change=${(event) => onChange?.(event.target.checked)}
55
56
  ${ref((element) => applyElementProps(element, rest))}
56
57
  />
57
- <span class="iw-checkbox-label">${label}</span>
58
+ ${
59
+ description
60
+ ? html`<span class="iw-checkbox-text">
61
+ <span class="iw-checkbox-label">${label}</span>
62
+ <span class="iw-checkbox-description">${description}</span>
63
+ </span>`
64
+ : html`<span class="iw-checkbox-label">${label}</span>`
65
+ }
58
66
  </label>
59
67
  `
60
68
  }
@@ -20,7 +20,11 @@ export default {
20
20
  description: "Semantic accent color.",
21
21
  },
22
22
  isDisabled: { control: "boolean", description: "Disables all options." },
23
- options: { control: "object", description: "List of radio options." },
23
+ options: {
24
+ control: "object",
25
+ description:
26
+ "List of radio options: `label`, `value`, and an optional `description` under the label.",
27
+ },
24
28
  onChange: { action: "onChange" },
25
29
  },
26
30
  parameters: {
@@ -52,3 +56,19 @@ Row.args = {
52
56
  ...Default.args,
53
57
  direction: "row",
54
58
  }
59
+
60
+ export const WithDescriptions = {}
61
+ WithDescriptions.args = {
62
+ ...Default.args,
63
+ label: "Shipping",
64
+ name: "shipping",
65
+ value: "standard",
66
+ options: [
67
+ {
68
+ label: "Standard",
69
+ value: "standard",
70
+ description: "3 to 5 working days, free",
71
+ },
72
+ { label: "Express", value: "express", description: "Next day, €9" },
73
+ ],
74
+ }
@@ -19,6 +19,17 @@
19
19
  cursor: pointer;
20
20
  }
21
21
 
22
+ /* With a description, the label and it stack beside the circle. */
23
+ .iw-radio-text {
24
+ display: flex;
25
+ flex-direction: column;
26
+ }
27
+
28
+ .iw-radio-description {
29
+ font-size: var(--iw-font-size-sm);
30
+ color: var(--iw-color-text-secondary);
31
+ }
32
+
22
33
  .iw-radio {
23
34
  margin: 0;
24
35
  accent-color: var(--iw-radio-accent);
@@ -55,6 +55,7 @@ export function render(props) {
55
55
  ${options.map((option, index) => {
56
56
  const {
57
57
  label: optionLabel,
58
+ description: optionDescription,
58
59
  value: optionValue,
59
60
  isDisabled: optionDisabled = false,
60
61
  ...optionRest
@@ -73,7 +74,16 @@ export function render(props) {
73
74
  @change=${(event) => onChange?.(event.target.value)}
74
75
  ${ref((element) => applyElementProps(element, optionRest))}
75
76
  />
76
- <span>${optionLabel}</span>
77
+ ${
78
+ optionDescription
79
+ ? html`<span class="iw-radio-text">
80
+ <span>${optionLabel}</span>
81
+ <span class="iw-radio-description"
82
+ >${optionDescription}</span
83
+ >
84
+ </span>`
85
+ : html`<span>${optionLabel}</span>`
86
+ }
77
87
  </label>
78
88
  `
79
89
  })}
@@ -8,6 +8,14 @@ export default {
8
8
  argTypes: {
9
9
  value: { control: "number", description: "Current rating value." },
10
10
  max: { control: "number", description: "Maximum rating value." },
11
+ name: {
12
+ control: "text",
13
+ description: "Groups the radios and names the value in a form.",
14
+ },
15
+ ariaLabel: {
16
+ control: "text",
17
+ description: 'Names the group for assistive tech. Defaults to "Rating".',
18
+ },
11
19
  isDisabled: { control: "boolean", description: "Disables interaction." },
12
20
  isReadOnly: {
13
21
  control: "boolean",
@@ -8,7 +8,11 @@ export default {
8
8
  argTypes: {
9
9
  label: { control: "text", description: "Field label." },
10
10
  name: { control: "text", description: "Native input name." },
11
- value: { control: "number", description: "Current slider value." },
11
+ value: {
12
+ control: "object",
13
+ description:
14
+ "One number, or `[low, high]` for a range with a thumb at each end.",
15
+ },
12
16
  min: { control: "number", description: "Minimum value." },
13
17
  max: { control: "number", description: "Maximum value." },
14
18
  step: { control: "number", description: "Step value." },
@@ -20,6 +24,10 @@ export default {
20
24
  isDisabled: { control: "boolean", description: "Disables interaction." },
21
25
  isValueVisible: { control: "boolean", description: "Shows current value." },
22
26
  isFullWidth: { control: "boolean", description: "Expands width to 100%." },
27
+ hasMarks: {
28
+ control: "boolean",
29
+ description: "Draws a tick at every step.",
30
+ },
23
31
  onChange: { action: "onChange" },
24
32
  },
25
33
  parameters: {
@@ -44,3 +52,22 @@ Default.args = {
44
52
  isValueVisible: true,
45
53
  isFullWidth: false,
46
54
  }
55
+
56
+ export const Range = {}
57
+ Range.args = {
58
+ ...Default.args,
59
+ label: "Price",
60
+ name: "price",
61
+ value: [20, 80],
62
+ }
63
+
64
+ export const Marks = {}
65
+ Marks.args = {
66
+ ...Default.args,
67
+ label: "Rating",
68
+ name: "rating",
69
+ value: 3,
70
+ min: 1,
71
+ max: 5,
72
+ hasMarks: true,
73
+ }
@@ -23,6 +23,108 @@
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
+ .iw-slider-marks {
29
+ display: flex;
30
+ justify-content: space-between;
31
+ padding-inline: calc(var(--iw-slider-thumb, 1rem) / 2);
32
+
33
+ span {
34
+ inline-size: 1px;
35
+ block-size: var(--iw-space-2);
36
+ background: var(--iw-color-border);
37
+ }
38
+ }
39
+
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
+ &.iw-slider-range {
44
+ --iw-slider-thumb: 1rem;
45
+
46
+ .iw-slider-track {
47
+ position: relative;
48
+ block-size: var(--iw-slider-thumb);
49
+
50
+ &::before,
51
+ &::after {
52
+ content: "";
53
+ position: absolute;
54
+ inset-block-start: 50%;
55
+ block-size: 0.25rem;
56
+ translate: 0 -50%;
57
+ border-radius: var(--iw-radius-full, 999px);
58
+ }
59
+
60
+ &::before {
61
+ inset-inline: 0;
62
+ background: color-mix(
63
+ in srgb,
64
+ var(--iw-slider-accent) 25%,
65
+ transparent
66
+ );
67
+ }
68
+
69
+ &::after {
70
+ inset-inline-start: var(--iw-slider-low);
71
+ inset-inline-end: calc(100% - var(--iw-slider-high));
72
+ background: var(--iw-slider-accent);
73
+ }
74
+ }
75
+
76
+ .iw-slider-input {
77
+ position: absolute;
78
+ inset: 0;
79
+ z-index: 1;
80
+ margin: 0;
81
+ appearance: none;
82
+ background: transparent;
83
+ pointer-events: none;
84
+
85
+ &::-webkit-slider-runnable-track {
86
+ background: transparent;
87
+ }
88
+
89
+ &::-moz-range-track {
90
+ background: transparent;
91
+ }
92
+
93
+ &::-webkit-slider-thumb {
94
+ appearance: none;
95
+ inline-size: var(--iw-slider-thumb);
96
+ block-size: var(--iw-slider-thumb);
97
+ border: 2px solid var(--iw-color-surface);
98
+ border-radius: 50%;
99
+ background: var(--iw-slider-accent);
100
+ box-shadow: var(--iw-shadow-sm);
101
+ cursor: pointer;
102
+ pointer-events: auto;
103
+ }
104
+
105
+ &::-moz-range-thumb {
106
+ inline-size: var(--iw-slider-thumb);
107
+ block-size: var(--iw-slider-thumb);
108
+ border: 2px solid var(--iw-color-surface);
109
+ border-radius: 50%;
110
+ background: var(--iw-slider-accent);
111
+ box-shadow: var(--iw-shadow-sm);
112
+ cursor: pointer;
113
+ pointer-events: auto;
114
+ }
115
+
116
+ &:focus-visible::-webkit-slider-thumb {
117
+ outline: 2px solid var(--iw-slider-accent);
118
+ outline-offset: 2px;
119
+ }
120
+
121
+ &:focus-visible::-moz-range-thumb {
122
+ outline: 2px solid var(--iw-slider-accent);
123
+ outline-offset: 2px;
124
+ }
125
+ }
126
+ }
127
+
26
128
  &.iw-slider-full-width {
27
129
  width: 100%;
28
130
  }
@@ -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
+ }
@@ -22,6 +22,7 @@ export function render(props) {
22
22
  type, // eslint-disable-line no-unused-vars
23
23
  name = "",
24
24
  label = "",
25
+ description,
25
26
  isChecked = false,
26
27
  isDisabled = false,
27
28
  color = "primary",
@@ -57,7 +58,16 @@ export function render(props) {
57
58
  <span class="iw-switch-track" aria-hidden="true">
58
59
  <span class="iw-switch-thumb"></span>
59
60
  </span>
60
- ${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
+ }
61
71
  </label>
62
72
  `
63
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
  }
@@ -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
  */
@@ -10,6 +10,7 @@ export default {
10
10
  argTypes: {
11
11
  isOpen: { control: "boolean" },
12
12
  isModal: { control: "boolean" },
13
+ size: { control: "select", options: ["sm", "md", "lg"] },
13
14
  title: { control: "text" },
14
15
  description: { control: "text" },
15
16
  onClose: { action: "onClose" },
@@ -29,6 +30,27 @@ export const Open = {
29
30
  },
30
31
  }
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
+
32
54
  // Modal takes over the page, so it is left out of the docs page.
33
55
  export const Modal = {
34
56
  tags: ["!autodocs"],
@@ -7,12 +7,25 @@
7
7
  color: var(--iw-color-text);
8
8
  border-radius: var(--iw-radius-md);
9
9
  border: 1px solid color-mix(in srgb, var(--iw-color-border) 70%, transparent);
10
- min-width: min(32rem, 90vw);
11
- max-width: min(48rem, 90vw);
12
10
  box-shadow: var(--iw-shadow-lg, 0 12px 40px rgba(0, 0, 0, 0.3));
13
11
  padding: 0;
14
12
  }
15
13
 
14
+ .iw-dialog-sm {
15
+ min-width: min(20rem, 90vw);
16
+ max-width: min(28rem, 90vw);
17
+ }
18
+
19
+ .iw-dialog-md {
20
+ min-width: min(32rem, 90vw);
21
+ max-width: min(48rem, 90vw);
22
+ }
23
+
24
+ .iw-dialog-lg {
25
+ min-width: min(40rem, 90vw);
26
+ max-width: min(64rem, 90vw);
27
+ }
28
+
16
29
  .iw-dialog::backdrop {
17
30
  background: color-mix(in srgb, var(--iw-color-black) 55%, transparent);
18
31
  backdrop-filter: blur(2px);
@@ -28,6 +28,7 @@ export function render(props) {
28
28
  id,
29
29
  isOpen = false,
30
30
  isModal = true,
31
+ size = "md",
31
32
  title,
32
33
  description,
33
34
  children,
@@ -54,7 +55,11 @@ export function render(props) {
54
55
  // also clicks the dialog, and must not close it.
55
56
  return html`<dialog
56
57
  id=${id || nothing}
57
- class=${classMap({ "iw-dialog": true, ...extraClasses })}
58
+ class=${classMap({
59
+ "iw-dialog": true,
60
+ [`iw-dialog-${size}`]: true,
61
+ ...extraClasses,
62
+ })}
58
63
  aria-labelledby=${titleId || nothing}
59
64
  aria-describedby=${descriptionId || nothing}
60
65
  @pointerdown=${(event) => {
@@ -8,6 +8,11 @@ export default {
8
8
  argTypes: {
9
9
  variant: { control: "select", options: ["linear", "circular"] },
10
10
  value: { control: "number" },
11
+ ariaLabel: {
12
+ control: "text",
13
+ description:
14
+ 'Names the indicator for assistive tech, e.g. "Uploading photo".',
15
+ },
11
16
  size: { control: "number" },
12
17
  thickness: { control: "number" },
13
18
  color: {
@@ -30,7 +30,7 @@ export default {
30
30
  },
31
31
  gap: {
32
32
  control: "select",
33
- options: ["none", "sm", "md", "lg", "xl"],
33
+ options: ["none", "xs", "sm", "md", "lg", "xl", "2xl"],
34
34
  description: "Spacing between children.",
35
35
  },
36
36
  padding: {
@@ -85,6 +85,10 @@
85
85
  gap: 0;
86
86
  }
87
87
 
88
+ &.iw-flex-gap-xs {
89
+ gap: var(--iw-space-1);
90
+ }
91
+
88
92
  &.iw-flex-gap-sm {
89
93
  gap: var(--iw-space-2);
90
94
  }
@@ -101,6 +105,10 @@
101
105
  gap: var(--iw-space-8);
102
106
  }
103
107
 
108
+ &.iw-flex-gap-2xl {
109
+ gap: var(--iw-space-12);
110
+ }
111
+
104
112
  &.iw-flex-padding-none {
105
113
  padding: 0;
106
114
  }
@@ -12,6 +12,11 @@ export default {
12
12
  control: "object",
13
13
  description: "Accordion sections with title, content, and state flags.",
14
14
  },
15
+ name: {
16
+ control: "text",
17
+ description:
18
+ "Items sharing a name close each other: at most one is open at a time.",
19
+ },
15
20
  onItemToggle: { action: "onItemToggle" },
16
21
  },
17
22
  parameters: {
@@ -44,6 +49,10 @@ export const Default = {
44
49
  },
45
50
  }
46
51
 
52
+ export const Exclusive = {
53
+ args: { ...Default.args, name: "settings" },
54
+ }
55
+
47
56
  export const WithIcons = {
48
57
  args: {
49
58
  items: [
@@ -85,7 +85,11 @@ export const Accordion = {
85
85
  name=${props.name || nothing}
86
86
  .open=${isExpanded}
87
87
  @toggle=${(event) => {
88
- const isOpen = event.newState === "open"
88
+ // newState is what the browser says; an environment whose toggle
89
+ // event lacks it, such as jsdom, still has the element's own state.
90
+ const isOpen = event.newState
91
+ ? event.newState === "open"
92
+ : event.currentTarget.open
89
93
  // Also fires when isExpanded opened it, which needs no report.
90
94
  if (isOpen === isExpanded) return
91
95
  onToggle?.(item, index, isOpen)
@@ -53,6 +53,12 @@ export default {
53
53
  options: ["none", "sm", "md", "lg", "xl"],
54
54
  description: "Footer padding preset.",
55
55
  },
56
+ variant: {
57
+ control: "select",
58
+ options: ["default", "sunk", "clear"],
59
+ description:
60
+ "The surface: raised, sunk into the page for content inside another surface, or clear.",
61
+ },
56
62
  isHoverable: {
57
63
  control: "boolean",
58
64
  description: "Enable hover visual feedback.",
@@ -97,6 +103,24 @@ export const WithFooter = {
97
103
  },
98
104
  }
99
105
 
106
+ export const Sunk = {
107
+ args: {
108
+ title: "Sunk card",
109
+ subtitle: "Content inside another surface",
110
+ children: "Body content",
111
+ variant: "sunk",
112
+ },
113
+ }
114
+
115
+ export const Clear = {
116
+ args: {
117
+ title: "Clear card",
118
+ subtitle: "Groups content with no surface of its own",
119
+ children: "Body content",
120
+ variant: "clear",
121
+ },
122
+ }
123
+
100
124
  export const Interactive = {
101
125
  args: {
102
126
  title: "Interactive Card",
@@ -11,6 +11,21 @@
11
11
  transform var(--iw-transition-normal);
12
12
  }
13
13
 
14
+ /* Set into the page rather than raised from it, for content grouped inside
15
+ another surface. */
16
+ .iw-card-sunk {
17
+ background-color: var(--iw-color-bg-secondary, var(--iw-card-bg));
18
+ border-color: transparent;
19
+ box-shadow: none;
20
+ }
21
+
22
+ /* Only the grouping: no surface at all. */
23
+ .iw-card-clear {
24
+ background-color: transparent;
25
+ border-color: transparent;
26
+ box-shadow: none;
27
+ }
28
+
14
29
  .iw-card-hoverable:hover {
15
30
  box-shadow: var(--iw-card-shadow-hover);
16
31
  }
@@ -30,6 +30,7 @@ export const Card = {
30
30
  const {
31
31
  type, // eslint-disable-line no-unused-vars
32
32
  element = "article",
33
+ variant = "default",
33
34
  isHoverable = false,
34
35
  isClickable = false,
35
36
  isFullWidth = false,
@@ -47,6 +48,7 @@ export const Card = {
47
48
 
48
49
  const classes = {
49
50
  "iw-card": true,
51
+ [`iw-card-${variant}`]: variant !== "default",
50
52
  "iw-card-hoverable": isHoverable || isClickable,
51
53
  "iw-card-clickable": isClickable,
52
54
  "iw-card-full-width": isFullWidth,
@@ -13,7 +13,7 @@ export interface ButtonProps {
13
13
  | "warning"
14
14
  | "error"
15
15
  | "info"
16
- size?: "sm" | "md" | "lg"
16
+ size?: "xs" | "sm" | "md" | "lg"
17
17
  shape?: "rectangle" | "pill" | "round" | "square"
18
18
  isDisabled?: boolean
19
19
  isPressed?: boolean
@@ -4,6 +4,8 @@ export interface CheckboxProps {
4
4
  id?: string
5
5
  name?: string
6
6
  label?: string
7
+ /** A line of detail under the label. */
8
+ description?: string
7
9
  isChecked?: boolean
8
10
  isDisabled?: boolean
9
11
  isRequired?: boolean
@@ -2,6 +2,8 @@ import type { TemplateResult } from "@inglorious/web"
2
2
 
3
3
  export interface RadioOption {
4
4
  label: string
5
+ /** A line of detail under the label. */
6
+ description?: string
5
7
  value: string
6
8
  isDisabled?: boolean
7
9
  [key: string]: unknown
@@ -4,7 +4,8 @@ export interface SliderProps {
4
4
  id?: string
5
5
  name?: string
6
6
  label?: string
7
- value?: number
7
+ /** One number, or `[low, high]` for a range with a thumb at each end. */
8
+ value?: number | [number, number]
8
9
  min?: number
9
10
  max?: number
10
11
  step?: number
@@ -12,7 +13,10 @@ export interface SliderProps {
12
13
  isDisabled?: boolean
13
14
  isValueVisible?: boolean
14
15
  isFullWidth?: boolean
15
- onChange?: (value: number) => void
16
+ /** Draws a tick at every step, for a slider that moves in clear steps. */
17
+ hasMarks?: boolean
18
+ /** The new value, in the shape `value` has: a number, or `[low, high]`. */
19
+ onChange?: (value: number | [number, number]) => void
16
20
  [key: string]: unknown
17
21
  }
18
22
 
@@ -4,6 +4,8 @@ export interface SwitchProps {
4
4
  id?: string
5
5
  name?: string
6
6
  label?: string
7
+ /** A line of detail under the label. */
8
+ description?: string
7
9
  isChecked?: boolean
8
10
  isDisabled?: boolean
9
11
  color?: "primary" | "secondary" | "success" | "warning" | "error" | "info"
@@ -3,6 +3,8 @@ import type { TemplateResult } from "@inglorious/web"
3
3
  export interface ChipProps {
4
4
  id?: string
5
5
  children?: TemplateResult | string | number
6
+ /** Shown before the label, such as an `Icon.render(...)`. Decorative. */
7
+ icon?: TemplateResult
6
8
  isRemovable?: boolean
7
9
  color?:
8
10
  | "default"
@@ -4,6 +4,8 @@ export interface DividerProps {
4
4
  id?: string
5
5
  orientation?: "horizontal" | "vertical"
6
6
  isInset?: boolean
7
+ /** Only visual: hidden from assistive tech, which otherwise announces a separator. */
8
+ isDecorative?: boolean
7
9
  [key: string]: unknown
8
10
  }
9
11
 
@@ -6,7 +6,7 @@ export interface AlertProps {
6
6
  title?: TemplateResult | string | number
7
7
  description?: TemplateResult | string | number
8
8
  children?: TemplateResult | string | number
9
- severity?: "info" | "success" | "warning" | "error"
9
+ severity?: "info" | "success" | "warning" | "error" | "neutral"
10
10
  variant?: "filled" | "outlined"
11
11
  icon?: TemplateResult | string | number
12
12
  action?: TemplateResult | string | number
@@ -6,6 +6,8 @@ export interface DialogProps {
6
6
  isOpen?: boolean
7
7
  /** Modal by default; `false` shows the dialog in place, with the page still usable. */
8
8
  isModal?: boolean
9
+ /** Its width: `"sm"`, `"md"` (default) or `"lg"`. */
10
+ size?: "sm" | "md" | "lg"
9
11
  title?: TemplateResult | string | number
10
12
  description?: TemplateResult | string | number
11
13
  children?: TemplateResult | string | number
@@ -16,7 +16,7 @@ export interface FlexProps {
16
16
  wrap?: "nowrap" | "wrap" | "wrap-reverse"
17
17
  justify?: "start" | "center" | "end" | "between" | "around" | "evenly"
18
18
  align?: "stretch" | "start" | "center" | "end" | "baseline"
19
- gap?: "none" | "sm" | "md" | "lg" | "xl"
19
+ gap?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl"
20
20
  padding?: "none" | "sm" | "md" | "lg" | "xl"
21
21
  overflow?: "visible" | "hidden" | "clip" | "scroll" | "auto"
22
22
  isInline?: boolean
@@ -12,6 +12,11 @@ export interface CardProps {
12
12
  | "footer"
13
13
  | "nav"
14
14
  | "aside"
15
+ /**
16
+ * The surface: raised (`"default"`), `"sunk"` into the page for content
17
+ * inside another surface, or `"clear"` with none.
18
+ */
19
+ variant?: "default" | "sunk" | "clear"
15
20
  title?: TemplateResult | string | number
16
21
  subtitle?: TemplateResult | string | number
17
22
  header?: TemplateResult | string | number