@inglorious/ui 0.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 (83) hide show
  1. package/LICENSE +9 -0
  2. package/package.json +142 -0
  3. package/src/controls/button/button.stories.js +139 -0
  4. package/src/controls/button/index.js +5 -0
  5. package/src/controls/button/style.css +148 -0
  6. package/src/controls/button/template.js +74 -0
  7. package/src/controls/button-group/button-group.stories.js +108 -0
  8. package/src/controls/button-group/index.js +4 -0
  9. package/src/controls/button-group/style.css +58 -0
  10. package/src/controls/button-group/template.js +117 -0
  11. package/src/controls/checkbox/checkbox.stories.js +47 -0
  12. package/src/controls/checkbox/index.js +4 -0
  13. package/src/controls/checkbox/style.css +33 -0
  14. package/src/controls/checkbox/template.js +54 -0
  15. package/src/controls/fab/fab.stories.js +52 -0
  16. package/src/controls/fab/index.js +4 -0
  17. package/src/controls/fab/style.css +76 -0
  18. package/src/controls/fab/template.js +33 -0
  19. package/src/controls/icon-button/icon-button.stories.js +70 -0
  20. package/src/controls/icon-button/index.js +4 -0
  21. package/src/controls/icon-button/style.css +14 -0
  22. package/src/controls/icon-button/template.js +47 -0
  23. package/src/controls/input/index.js +4 -0
  24. package/src/controls/input/input.stories.js +180 -0
  25. package/src/controls/input/style.css +121 -0
  26. package/src/controls/input/template.js +111 -0
  27. package/src/controls/radio-group/index.js +4 -0
  28. package/src/controls/radio-group/radio-group.stories.js +51 -0
  29. package/src/controls/radio-group/style.css +41 -0
  30. package/src/controls/radio-group/template.js +71 -0
  31. package/src/controls/rating/index.js +4 -0
  32. package/src/controls/rating/rating.stories.js +47 -0
  33. package/src/controls/rating/style.css +29 -0
  34. package/src/controls/rating/template.js +53 -0
  35. package/src/controls/select/handlers.js +133 -0
  36. package/src/controls/select/helpers.js +91 -0
  37. package/src/controls/select/index.js +15 -0
  38. package/src/controls/select/select.stories.js +90 -0
  39. package/src/controls/select/style.css +211 -0
  40. package/src/controls/select/template.js +423 -0
  41. package/src/controls/slider/index.js +4 -0
  42. package/src/controls/slider/slider.stories.js +43 -0
  43. package/src/controls/slider/style.css +28 -0
  44. package/src/controls/slider/template.js +66 -0
  45. package/src/controls/switch/index.js +4 -0
  46. package/src/controls/switch/style.css +82 -0
  47. package/src/controls/switch/switch.stories.js +45 -0
  48. package/src/controls/switch/template.js +55 -0
  49. package/src/index.js +14 -0
  50. package/src/layout/flex/flex.stories.js +103 -0
  51. package/src/layout/flex/index.js +4 -0
  52. package/src/layout/flex/style.css +103 -0
  53. package/src/layout/flex/template.js +41 -0
  54. package/src/layout/grid/grid.stories.js +82 -0
  55. package/src/layout/grid/index.js +4 -0
  56. package/src/layout/grid/style.css +51 -0
  57. package/src/layout/grid/template.js +47 -0
  58. package/src/shared/applyElementProps.js +79 -0
  59. package/src/shared/withRenderValidation.js +19 -0
  60. package/src/stories/notifyStory.js +43 -0
  61. package/src/themes/bootstrap.css +118 -0
  62. package/src/themes/inglorious.css +106 -0
  63. package/src/themes/material.css +124 -0
  64. package/src/tokens/index.css +133 -0
  65. package/src/widgets/card/card.stories.js +135 -0
  66. package/src/widgets/card/index.js +4 -0
  67. package/src/widgets/card/style.css +53 -0
  68. package/src/widgets/card/template.js +97 -0
  69. package/types/controls/button-group.d.ts +35 -0
  70. package/types/controls/button.d.ts +23 -0
  71. package/types/controls/checkbox.d.ts +18 -0
  72. package/types/controls/fab.d.ts +19 -0
  73. package/types/controls/icon-button.d.ts +23 -0
  74. package/types/controls/input.d.ts +29 -0
  75. package/types/controls/radio-group.d.ts +25 -0
  76. package/types/controls/rating.d.ts +18 -0
  77. package/types/controls/select.d.ts +56 -0
  78. package/types/controls/slider.d.ts +21 -0
  79. package/types/controls/switch.d.ts +17 -0
  80. package/types/index.d.ts +14 -0
  81. package/types/layout/flex.d.ts +20 -0
  82. package/types/layout/grid.d.ts +19 -0
  83. package/types/widgets/card.d.ts +21 -0
@@ -0,0 +1,423 @@
1
+ /**
2
+ * @typedef {import('../../../types/controls/select').SelectEntity} SelectEntity
3
+ * @typedef {import('../../../types/controls/select').SelectOption} SelectOption
4
+ * @typedef {import('@inglorious/web').Api} Api
5
+ * @typedef {import('@inglorious/web').TemplateResult} TemplateResult
6
+ */
7
+
8
+ import { classMap, html, ref, repeat, when } from "@inglorious/web"
9
+ import { applyElementProps } from "@/shared/applyElementProps.js"
10
+
11
+ import {
12
+ filterOptions,
13
+ formatOption,
14
+ getOptionLabel,
15
+ getOptionValue,
16
+ isOptionSelected,
17
+ } from "./helpers.js"
18
+
19
+ const NO_FOCUSED_INDEX = -1
20
+
21
+ /**
22
+ * Select control render function.
23
+ * @param {SelectEntity} entity
24
+ * @param {Api} api
25
+ * @returns {TemplateResult}
26
+ */
27
+ export function render(entity, api) {
28
+ const type = api.getType?.(entity.type)
29
+ const normalized = ensureDefaults(entity)
30
+
31
+ return html`<div
32
+ class=${classMap({
33
+ "iw-select": true,
34
+ "iw-select-full-width": !!normalized.fullWidth,
35
+ [`iw-select-${normalized.size}`]: normalized.size !== "md",
36
+ })}
37
+ >
38
+ ${normalized.label
39
+ ? html`<label class="iw-select-label">${normalized.label}</label>`
40
+ : null}
41
+ ${type?.renderControl?.(normalized, api) ?? renderControl(normalized, api)}
42
+ ${when(
43
+ normalized.isOpen,
44
+ () =>
45
+ type?.renderDropdown?.(normalized, api) ??
46
+ renderDropdown(normalized, api),
47
+ )}
48
+ </div>`
49
+ }
50
+
51
+ /**
52
+ * Render trigger control.
53
+ * @param {SelectEntity} entity
54
+ * @param {Api} api
55
+ * @returns {TemplateResult}
56
+ */
57
+ export function renderControl(entity, api) {
58
+ const type = api.getType?.(entity.type)
59
+
60
+ return html`<div
61
+ class="iw-select-control ${classMap({
62
+ "iw-select-control-open": entity.isOpen,
63
+ "iw-select-control-disabled": entity.isDisabled,
64
+ "iw-select-control-selection":
65
+ entity.isMulti &&
66
+ Array.isArray(entity.selectedValue) &&
67
+ entity.selectedValue.length,
68
+ })}"
69
+ @click=${() => !entity.isDisabled && api.notify(`#${entity.id}:toggle`)}
70
+ ${ref((element) => applyElementProps(element, entity.controlProps ?? {}))}
71
+ >
72
+ ${when(
73
+ entity.isMulti,
74
+ () =>
75
+ type?.renderMultiValue?.(entity, api) ?? renderMultiValue(entity, api),
76
+ () => type?.renderSingleValue?.(entity) ?? renderSingleValue(entity),
77
+ )}
78
+ ${when(
79
+ entity.isClearable &&
80
+ ((entity.isMulti &&
81
+ Array.isArray(entity.selectedValue) &&
82
+ entity.selectedValue.length) ||
83
+ (!entity.isMulti && entity.selectedValue !== null)),
84
+ () =>
85
+ html`<button
86
+ class="iw-select-clear"
87
+ type="button"
88
+ @click=${(event) => {
89
+ event.stopPropagation()
90
+ api.notify(`#${entity.id}:clear`)
91
+ }}
92
+ >
93
+ ×
94
+ </button>`,
95
+ )}
96
+
97
+ <div class="iw-select-arrow"><span>▼</span></div>
98
+ </div>`
99
+ }
100
+
101
+ /**
102
+ * Render single selected value.
103
+ * @param {SelectEntity} entity
104
+ * @returns {TemplateResult}
105
+ */
106
+ export function renderSingleValue(entity) {
107
+ if (entity.selectedValue === null) {
108
+ return html`<span class="iw-select-placeholder"
109
+ >${entity.placeholder}</span
110
+ >`
111
+ }
112
+
113
+ const selectedOption = entity.options.find(
114
+ (option) => getOptionValue(option) === entity.selectedValue,
115
+ )
116
+
117
+ return html`<span class="iw-select-value"
118
+ >${selectedOption
119
+ ? getOptionLabel(selectedOption)
120
+ : String(entity.selectedValue)}</span
121
+ >`
122
+ }
123
+
124
+ /**
125
+ * Render multi selected value tags.
126
+ * @param {SelectEntity} entity
127
+ * @param {Api} api
128
+ * @returns {TemplateResult}
129
+ */
130
+ export function renderMultiValue(entity, api) {
131
+ const type = api.getType?.(entity.type)
132
+ if (!Array.isArray(entity.selectedValue) || !entity.selectedValue.length) {
133
+ return html`<span class="iw-select-placeholder"
134
+ >${entity.placeholder}</span
135
+ >`
136
+ }
137
+
138
+ return html`<div class="iw-select-multi-value">
139
+ ${repeat(
140
+ entity.selectedValue,
141
+ (value) => value,
142
+ (value) =>
143
+ type?.renderMultiValueTag?.(entity, value, api) ??
144
+ renderMultiValueTag(entity, value, api),
145
+ )}
146
+ </div>`
147
+ }
148
+
149
+ /**
150
+ * Render a selected multi-value tag.
151
+ * @param {SelectEntity} entity
152
+ * @param {string|number} value
153
+ * @param {Api} api
154
+ * @returns {TemplateResult}
155
+ */
156
+ export function renderMultiValueTag(entity, value, api) {
157
+ const option = entity.options.find((opt) => getOptionValue(opt) === value)
158
+
159
+ return html`<div
160
+ class="iw-select-multi-value-tag"
161
+ @click=${(event) => {
162
+ event.stopPropagation()
163
+ api.notify(`#${entity.id}:optionSelect`, option || { value })
164
+ }}
165
+ >
166
+ <span class="iw-select-multi-value-tag-label"
167
+ >${option ? getOptionLabel(option) : String(value)}</span
168
+ >
169
+ <span class="iw-select-multi-value-remove">×</span>
170
+ </div>`
171
+ }
172
+
173
+ /**
174
+ * Render dropdown.
175
+ * @param {SelectEntity} entity
176
+ * @param {Api} api
177
+ * @returns {TemplateResult}
178
+ */
179
+ export function renderDropdown(entity, api) {
180
+ const type = api.getType?.(entity.type)
181
+ const filteredOptions = filterOptions(entity.options, entity.searchTerm)
182
+
183
+ return html`<div
184
+ class="iw-select-dropdown"
185
+ ${ref((el) => {
186
+ if (el) {
187
+ setTimeout(() => {
188
+ document.addEventListener(
189
+ "click",
190
+ (event) => {
191
+ if (!el.contains(event.target)) {
192
+ api.notify(`#${entity.id}:close`)
193
+ }
194
+ },
195
+ { once: true },
196
+ )
197
+ })
198
+ }
199
+ })}
200
+ >
201
+ ${when(
202
+ entity.isSearchable,
203
+ () =>
204
+ type?.renderSearchInput?.(entity, api) ??
205
+ renderSearchInput(entity, api),
206
+ )}
207
+ ${when(
208
+ entity.isLoading,
209
+ () => type?.renderLoading?.(entity) ?? renderLoading(entity),
210
+ )}
211
+ ${when(
212
+ !entity.isLoading && !filteredOptions.length,
213
+ () => type?.renderNoOptions?.(entity) ?? renderNoOptions(entity),
214
+ )}
215
+ ${when(
216
+ !entity.isLoading && filteredOptions.length,
217
+ () => type?.renderOptions?.(entity, api) ?? renderOptions(entity, api),
218
+ )}
219
+ </div>`
220
+ }
221
+
222
+ /**
223
+ * Render search input.
224
+ * @param {SelectEntity} entity
225
+ * @param {Api} api
226
+ * @returns {TemplateResult}
227
+ */
228
+ export function renderSearchInput(entity, api) {
229
+ return html`<input
230
+ class="iw-select-dropdown-search"
231
+ type="text"
232
+ placeholder="Search..."
233
+ .value=${entity.searchTerm ?? ""}
234
+ @input=${(event) =>
235
+ api.notify(`#${entity.id}:searchChange`, event.target.value)}
236
+ @keydown=${(event) => handleKeyDown(entity, event, api)}
237
+ ${ref((el) => {
238
+ if (el && entity.isOpen) queueMicrotask(() => el.focus())
239
+ })}
240
+ />`
241
+ }
242
+
243
+ /**
244
+ * Render options list.
245
+ * @param {SelectEntity} entity
246
+ * @param {Api} api
247
+ * @returns {TemplateResult}
248
+ */
249
+ export function renderOptions(entity, api) {
250
+ const filteredOptions = filterOptions(entity.options, entity.searchTerm)
251
+ const type = api.getType?.(entity.type)
252
+
253
+ return html`<div class="iw-select-dropdown-options">
254
+ ${repeat(
255
+ filteredOptions,
256
+ (option) => getOptionValue(option),
257
+ (option, index) =>
258
+ type?.renderOption?.(entity, { option, index }, api) ??
259
+ renderOption(entity, { option, index }, api),
260
+ )}
261
+ </div>`
262
+ }
263
+
264
+ /**
265
+ * Render an option row.
266
+ * @param {SelectEntity} entity
267
+ * @param {{ option: SelectOption, index: number }} payload
268
+ * @param {Api} api
269
+ * @returns {TemplateResult}
270
+ */
271
+ export function renderOption(entity, { option, index }, api) {
272
+ const normalized = formatOption(option)
273
+ const isSelected = isOptionSelected(
274
+ option,
275
+ entity.selectedValue,
276
+ entity.isMulti,
277
+ )
278
+ const isFocused = index === entity.focusedIndex
279
+
280
+ return html`<div
281
+ class="iw-select-dropdown-options-option ${classMap({
282
+ "iw-select-dropdown-options-option-selected": isSelected,
283
+ "iw-select-dropdown-options-option-focused": isFocused,
284
+ "iw-select-dropdown-options-option-disabled": !!normalized.disabled,
285
+ })}"
286
+ @click=${() =>
287
+ !normalized.disabled &&
288
+ api.notify(`#${entity.id}:optionSelect`, normalized)}
289
+ @mouseenter=${() => api.notify(`#${entity.id}:focusSet`, index)}
290
+ >
291
+ ${when(
292
+ entity.isMulti,
293
+ () =>
294
+ html`<input
295
+ type="checkbox"
296
+ .checked=${isSelected}
297
+ ?disabled=${!!normalized.disabled}
298
+ />`,
299
+ )}
300
+ <span>${getOptionLabel(normalized)}</span>
301
+ </div>`
302
+ }
303
+
304
+ /**
305
+ * Render loading state.
306
+ * @param {SelectEntity} entity
307
+ * @returns {TemplateResult}
308
+ */
309
+ export function renderLoading(entity) {
310
+ return html`<div class="iw-select-loading">${entity.loadingMessage}</div>`
311
+ }
312
+
313
+ /**
314
+ * Render no options state.
315
+ * @param {SelectEntity} entity
316
+ * @returns {TemplateResult}
317
+ */
318
+ export function renderNoOptions(entity) {
319
+ return html`<div class="iw-select-no-options">
320
+ ${entity.noOptionsMessage}
321
+ </div>`
322
+ }
323
+
324
+ /**
325
+ * Handle key navigation.
326
+ * @param {SelectEntity} entity
327
+ * @param {KeyboardEvent} event
328
+ * @param {Api} api
329
+ */
330
+ function handleKeyDown(entity, event, api) {
331
+ const filteredOptions = filterOptions(entity.options, entity.searchTerm)
332
+
333
+ switch (event.key) {
334
+ case "ArrowDown":
335
+ event.preventDefault()
336
+ api.notify(`#${entity.id}:focusNext`)
337
+ break
338
+ case "ArrowUp":
339
+ event.preventDefault()
340
+ api.notify(`#${entity.id}:focusPrev`)
341
+ break
342
+ case "Enter":
343
+ event.preventDefault()
344
+ if (
345
+ entity.focusedIndex > NO_FOCUSED_INDEX &&
346
+ filteredOptions[entity.focusedIndex]
347
+ ) {
348
+ const option = formatOption(filteredOptions[entity.focusedIndex])
349
+ if (!option.disabled) {
350
+ api.notify(`#${entity.id}:optionSelect`, option)
351
+ }
352
+ }
353
+ break
354
+ case "Escape":
355
+ event.preventDefault()
356
+ api.notify(`#${entity.id}:close`)
357
+ break
358
+ case "Home":
359
+ event.preventDefault()
360
+ api.notify(`#${entity.id}:focusFirst`)
361
+ break
362
+ case "End":
363
+ event.preventDefault()
364
+ api.notify(`#${entity.id}:focusLast`)
365
+ break
366
+ default:
367
+ break
368
+ }
369
+ }
370
+
371
+ /**
372
+ * @param {SelectEntity} entity
373
+ * @returns {SelectEntity}
374
+ */
375
+ function ensureDefaults(entity) {
376
+ const isMulti = entity.isMulti ?? false
377
+ const controlProps = Object.fromEntries(
378
+ Object.entries(entity).filter(([key]) => !KNOWN_SELECT_KEYS.has(key)),
379
+ )
380
+
381
+ return {
382
+ ...entity,
383
+ options: (entity.options ?? []).map(formatOption),
384
+ isOpen: entity.isOpen ?? false,
385
+ searchTerm: entity.searchTerm ?? "",
386
+ focusedIndex: entity.focusedIndex ?? NO_FOCUSED_INDEX,
387
+ isMulti,
388
+ selectedValue: entity.selectedValue ?? (isMulti ? [] : null),
389
+ isLoading: entity.isLoading ?? false,
390
+ isDisabled: entity.isDisabled ?? false,
391
+ isSearchable: entity.isSearchable ?? true,
392
+ isClearable: entity.isClearable ?? true,
393
+ placeholder: entity.placeholder ?? "Select...",
394
+ noOptionsMessage: entity.noOptionsMessage ?? "No options",
395
+ loadingMessage: entity.loadingMessage ?? "Loading...",
396
+ size: entity.size ?? "md",
397
+ fullWidth: entity.fullWidth ?? false,
398
+ label: entity.label ?? "",
399
+ controlProps,
400
+ }
401
+ }
402
+
403
+ const KNOWN_SELECT_KEYS = new Set([
404
+ "id",
405
+ "type",
406
+ "label",
407
+ "options",
408
+ "isOpen",
409
+ "searchTerm",
410
+ "focusedIndex",
411
+ "isMulti",
412
+ "selectedValue",
413
+ "isLoading",
414
+ "isDisabled",
415
+ "isSearchable",
416
+ "isClearable",
417
+ "placeholder",
418
+ "noOptionsMessage",
419
+ "loadingMessage",
420
+ "fullWidth",
421
+ "size",
422
+ "controlProps",
423
+ ])
@@ -0,0 +1,4 @@
1
+ import { withRenderValidation } from "../../shared/withRenderValidation.js"
2
+ import * as renderers from "./template.js"
3
+
4
+ export const slider = [{ ...renderers }, withRenderValidation]
@@ -0,0 +1,43 @@
1
+ import {
2
+ makeStoryRender,
3
+ notifyActionArgType,
4
+ } from "../../stories/notifyStory.js"
5
+ import { slider } from "."
6
+
7
+ export default {
8
+ title: "Controls/Slider",
9
+ tags: ["autodocs"],
10
+ render: makeStoryRender(slider, "story-slider"),
11
+ argTypes: {
12
+ ...notifyActionArgType,
13
+ label: { control: "text", description: "Field label." },
14
+ name: { control: "text", description: "Native input name." },
15
+ value: { control: "number", description: "Current slider value." },
16
+ min: { control: "number", description: "Minimum value." },
17
+ max: { control: "number", description: "Maximum value." },
18
+ step: { control: "number", description: "Step value." },
19
+ disabled: { control: "boolean", description: "Disables interaction." },
20
+ showValue: { control: "boolean", description: "Shows current value." },
21
+ fullWidth: { control: "boolean", description: "Expands width to 100%." },
22
+ },
23
+ parameters: {
24
+ docs: {
25
+ description: {
26
+ component: "Range input control with optional value indicator.",
27
+ },
28
+ },
29
+ },
30
+ }
31
+
32
+ export const Default = {}
33
+ Default.args = {
34
+ label: "Volume",
35
+ name: "volume",
36
+ value: 30,
37
+ min: 0,
38
+ max: 100,
39
+ step: 1,
40
+ disabled: false,
41
+ showValue: true,
42
+ fullWidth: false,
43
+ }
@@ -0,0 +1,28 @@
1
+ .iw-slider-field {
2
+ display: inline-flex;
3
+ flex-direction: column;
4
+ gap: var(--iw-space-2);
5
+ font-family: var(--iw-font-family-sans);
6
+ color: var(--iw-color-text);
7
+
8
+ .iw-slider-header {
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: space-between;
12
+ gap: var(--iw-space-4);
13
+ font-size: var(--iw-font-size-sm);
14
+ }
15
+
16
+ .iw-slider-value {
17
+ color: var(--iw-color-text-secondary);
18
+ }
19
+
20
+ .iw-slider {
21
+ width: 100%;
22
+ accent-color: var(--iw-color-primary);
23
+ }
24
+
25
+ &.iw-slider-full-width {
26
+ width: 100%;
27
+ }
28
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @typedef {import('../../../types/controls/slider').SliderEntity} SliderEntity
3
+ * @typedef {import('@inglorious/web').Api} Api
4
+ * @typedef {import('@inglorious/web').TemplateResult} TemplateResult
5
+ */
6
+
7
+ import { classMap, html, ref } from "@inglorious/web"
8
+
9
+ import { applyElementProps } from "@/shared/applyElementProps.js"
10
+
11
+ /**
12
+ * Slider control based on native range input.
13
+ *
14
+ * @param {SliderEntity} entity
15
+ * @param {Api} api
16
+ * @returns {TemplateResult}
17
+ */
18
+ export function render(entity, api) {
19
+ const {
20
+ name = "",
21
+ label,
22
+ value = 0,
23
+ min = 0,
24
+ max = 100,
25
+ step = 1,
26
+ disabled = false,
27
+ showValue = true,
28
+ fullWidth = false,
29
+ ...rest
30
+ } = entity
31
+
32
+ const inputId = entity.id || name
33
+
34
+ const classes = {
35
+ "iw-slider-field": true,
36
+ "iw-slider-full-width": fullWidth,
37
+ }
38
+
39
+ return html`
40
+ <div class=${classMap(classes)}>
41
+ ${(label || showValue) &&
42
+ html`<div class="iw-slider-header">
43
+ ${label
44
+ ? html`<label for=${inputId}>${label}</label>`
45
+ : html`<span></span>`}
46
+ ${showValue
47
+ ? html`<output class="iw-slider-value">${value}</output>`
48
+ : null}
49
+ </div>`}
50
+ <input
51
+ id=${inputId}
52
+ class="iw-slider"
53
+ type="range"
54
+ name=${name}
55
+ min=${min}
56
+ max=${max}
57
+ step=${step}
58
+ .value=${String(value)}
59
+ ?disabled=${disabled}
60
+ @input=${(event) =>
61
+ api.notify(`#${entity.id}:change`, Number(event.target.value))}
62
+ ${ref((element) => applyElementProps(element, rest))}
63
+ />
64
+ </div>
65
+ `
66
+ }
@@ -0,0 +1,4 @@
1
+ import { withRenderValidation } from "../../shared/withRenderValidation.js"
2
+ import * as renderers from "./template.js"
3
+
4
+ export const switchControl = [{ ...renderers }, withRenderValidation]
@@ -0,0 +1,82 @@
1
+ .iw-switch-field {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: var(--iw-space-2);
5
+ font-family: var(--iw-font-family-sans);
6
+ cursor: pointer;
7
+
8
+ .iw-switch-input {
9
+ position: absolute;
10
+ width: 1px;
11
+ height: 1px;
12
+ opacity: 0;
13
+ pointer-events: none;
14
+ }
15
+
16
+ .iw-switch-track {
17
+ width: 2.25rem;
18
+ height: 1.25rem;
19
+ border-radius: 999px;
20
+ background: var(--iw-color-gray-500);
21
+ display: inline-flex;
22
+ align-items: center;
23
+ padding: 2px;
24
+ transition: background-color var(--iw-transition-fast);
25
+ }
26
+
27
+ .iw-switch-thumb {
28
+ width: 1rem;
29
+ height: 1rem;
30
+ border-radius: 50%;
31
+ background: var(--iw-color-white);
32
+ box-shadow: var(--iw-shadow-sm);
33
+ transform: translateX(0);
34
+ transition: transform var(--iw-transition-fast);
35
+ }
36
+
37
+ .iw-switch-input:checked + .iw-switch-track {
38
+ background: var(--iw-color-primary);
39
+ }
40
+
41
+ .iw-switch-input:checked + .iw-switch-track .iw-switch-thumb {
42
+ transform: translateX(1rem);
43
+ }
44
+
45
+ .iw-switch-label {
46
+ color: var(--iw-color-text);
47
+ font-size: var(--iw-font-size-base);
48
+ }
49
+
50
+ &.iw-switch-sm .iw-switch-track {
51
+ width: 2rem;
52
+ height: 1.125rem;
53
+ }
54
+
55
+ &.iw-switch-sm .iw-switch-thumb {
56
+ width: 0.875rem;
57
+ height: 0.875rem;
58
+ }
59
+
60
+ &.iw-switch-sm .iw-switch-input:checked + .iw-switch-track .iw-switch-thumb {
61
+ transform: translateX(0.875rem);
62
+ }
63
+
64
+ &.iw-switch-lg .iw-switch-track {
65
+ width: 2.75rem;
66
+ height: 1.5rem;
67
+ }
68
+
69
+ &.iw-switch-lg .iw-switch-thumb {
70
+ width: 1.25rem;
71
+ height: 1.25rem;
72
+ }
73
+
74
+ &.iw-switch-lg .iw-switch-input:checked + .iw-switch-track .iw-switch-thumb {
75
+ transform: translateX(1.25rem);
76
+ }
77
+
78
+ &.iw-switch-disabled {
79
+ opacity: 0.6;
80
+ cursor: not-allowed;
81
+ }
82
+ }
@@ -0,0 +1,45 @@
1
+ import {
2
+ makeStoryRender,
3
+ notifyActionArgType,
4
+ } from "../../stories/notifyStory.js"
5
+ import { switchControl } from "."
6
+
7
+ export default {
8
+ title: "Controls/Switch",
9
+ tags: ["autodocs"],
10
+ render: makeStoryRender(switchControl, "story-switch"),
11
+ argTypes: {
12
+ ...notifyActionArgType,
13
+ label: { control: "text", description: "Label text." },
14
+ name: { control: "text", description: "Native input name." },
15
+ checked: { control: "boolean", description: "Current checked state." },
16
+ disabled: { control: "boolean", description: "Disables interaction." },
17
+ size: {
18
+ control: "select",
19
+ options: ["sm", "md", "lg"],
20
+ description: "Switch size.",
21
+ },
22
+ },
23
+ parameters: {
24
+ docs: {
25
+ description: {
26
+ component: "Binary on/off control with a styled switch track.",
27
+ },
28
+ },
29
+ },
30
+ }
31
+
32
+ export const Default = {}
33
+ Default.args = {
34
+ label: "Enable notifications",
35
+ name: "notifications",
36
+ checked: false,
37
+ disabled: false,
38
+ size: "md",
39
+ }
40
+
41
+ export const On = {}
42
+ On.args = {
43
+ ...Default.args,
44
+ checked: true,
45
+ }