@redvars/peacock 3.3.3 → 3.5.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 (280) hide show
  1. package/dist/IndividualComponent-DUINtMGK.js +67 -0
  2. package/dist/IndividualComponent-DUINtMGK.js.map +1 -0
  3. package/dist/assets/images/empty-state/no-document.svg +11 -12
  4. package/dist/assets/images/empty-state/page.svg +15 -9
  5. package/dist/assets/styles.css +1 -1
  6. package/dist/assets/styles.css.map +1 -1
  7. package/dist/banner.js +202 -0
  8. package/dist/banner.js.map +1 -0
  9. package/dist/bottom-sheet.js +238 -0
  10. package/dist/bottom-sheet.js.map +1 -0
  11. package/dist/{button-ClzS8JLq.js → button-DMN1dPAg.js} +358 -218
  12. package/dist/button-DMN1dPAg.js.map +1 -0
  13. package/dist/button-group-CX9CUUXk.js +435 -0
  14. package/dist/button-group-CX9CUUXk.js.map +1 -0
  15. package/dist/button-group.js +11 -6
  16. package/dist/button-group.js.map +1 -1
  17. package/dist/button.js +10 -5
  18. package/dist/button.js.map +1 -1
  19. package/dist/card-content.js +29 -0
  20. package/dist/card-content.js.map +1 -0
  21. package/dist/card.js +428 -44
  22. package/dist/card.js.map +1 -1
  23. package/dist/{chart-bar-DbnXQgvS.js → chart-bar-cn6rrna-.js} +2 -2
  24. package/dist/{chart-bar-DbnXQgvS.js.map → chart-bar-cn6rrna-.js.map} +1 -1
  25. package/dist/chart-bar.js +5 -4
  26. package/dist/chart-bar.js.map +1 -1
  27. package/dist/chart-doughnut.js +2 -1
  28. package/dist/chart-doughnut.js.map +1 -1
  29. package/dist/chart-pie.js +2 -1
  30. package/dist/chart-pie.js.map +1 -1
  31. package/dist/chart-stacked-bar.js +5 -4
  32. package/dist/chart-stacked-bar.js.map +1 -1
  33. package/dist/{class-map-59YGWLnx.js → class-map-YU7g0o3B.js} +4 -10
  34. package/dist/class-map-YU7g0o3B.js.map +1 -0
  35. package/dist/clock.js +2 -1
  36. package/dist/clock.js.map +1 -1
  37. package/dist/code-editor.js +8 -6
  38. package/dist/code-editor.js.map +1 -1
  39. package/dist/code-highlighter.js +6 -4
  40. package/dist/code-highlighter.js.map +1 -1
  41. package/dist/custom-elements-jsdocs.json +6270 -5026
  42. package/dist/custom-elements.json +5763 -2049
  43. package/dist/directive-ZPhl09Yt.js +9 -0
  44. package/dist/directive-ZPhl09Yt.js.map +1 -0
  45. package/dist/dispatch-event-utils-CuEqjlPT.js +127 -0
  46. package/dist/dispatch-event-utils-CuEqjlPT.js.map +1 -0
  47. package/dist/fab-C5Nzxk0E.js +497 -0
  48. package/dist/fab-C5Nzxk0E.js.map +1 -0
  49. package/dist/fab.js +11 -0
  50. package/dist/fab.js.map +1 -0
  51. package/dist/index.js +24 -12
  52. package/dist/index.js.map +1 -1
  53. package/dist/{observe-theme-change-pALI5fmV.js → is-dark-mode-DicqGkCJ.js} +8 -3
  54. package/dist/is-dark-mode-DicqGkCJ.js.map +1 -0
  55. package/dist/notification.js +417 -0
  56. package/dist/notification.js.map +1 -0
  57. package/dist/number-counter.js +4 -3
  58. package/dist/number-counter.js.map +1 -1
  59. package/dist/observe-slot-change-BGJfgg2E.js +31 -0
  60. package/dist/observe-slot-change-BGJfgg2E.js.map +1 -0
  61. package/dist/peacock-loader.js +59 -10
  62. package/dist/peacock-loader.js.map +1 -1
  63. package/dist/property-1psGvXOq.js +10 -0
  64. package/dist/property-1psGvXOq.js.map +1 -0
  65. package/dist/search.js +452 -0
  66. package/dist/search.js.map +1 -0
  67. package/dist/{radio-b70_Ie9n.js → select-4pl4XBj7.js} +2439 -521
  68. package/dist/select-4pl4XBj7.js.map +1 -0
  69. package/dist/side-sheet.js +186 -0
  70. package/dist/side-sheet.js.map +1 -0
  71. package/dist/spread-B5cgadZl.js +32 -0
  72. package/dist/spread-B5cgadZl.js.map +1 -0
  73. package/dist/src/__base_element/BaseHyperlink.d.ts +20 -0
  74. package/dist/src/__utils/cache-fetch.d.ts +1 -0
  75. package/dist/src/__utils/is-dark-mode.d.ts +1 -0
  76. package/dist/src/__utils/is-in-viewport.d.ts +1 -0
  77. package/dist/src/__utils/observe-slot-change.d.ts +1 -0
  78. package/dist/src/__utils/sanitize-svg.d.ts +1 -0
  79. package/dist/src/__utils/throttle.d.ts +4 -0
  80. package/dist/src/accordion/accordion-item.d.ts +33 -9
  81. package/dist/src/accordion/accordion.d.ts +21 -5
  82. package/dist/src/banner/banner.d.ts +47 -0
  83. package/dist/src/banner/index.d.ts +1 -0
  84. package/dist/src/bottom-sheet/bottom-sheet.d.ts +42 -0
  85. package/dist/src/bottom-sheet/index.d.ts +1 -0
  86. package/dist/src/button/BaseButton.d.ts +7 -13
  87. package/dist/src/button/button/button.d.ts +4 -0
  88. package/dist/src/button/button-group/button-group.d.ts +32 -3
  89. package/dist/src/button/icon-button/icon-button.d.ts +4 -0
  90. package/dist/src/card/card-content.d.ts +15 -0
  91. package/dist/src/card/card.d.ts +37 -3
  92. package/dist/src/card/index.d.ts +1 -0
  93. package/dist/src/container/container.d.ts +1 -1
  94. package/dist/src/empty-state/empty-state.d.ts +1 -1
  95. package/dist/src/fab/fab.d.ts +111 -0
  96. package/dist/src/fab/index.d.ts +1 -0
  97. package/dist/src/focus-ring/focus-ring.d.ts +4 -1
  98. package/dist/src/index.d.ts +11 -1
  99. package/dist/src/link/link.d.ts +3 -10
  100. package/dist/src/menu/menu/menu.d.ts +4 -2
  101. package/dist/src/menu/menu-item/menu-item.d.ts +0 -1
  102. package/dist/src/menu/sub-menu/sub-menu.d.ts +1 -0
  103. package/dist/src/notification/index.d.ts +1 -0
  104. package/dist/src/notification/notification.d.ts +69 -0
  105. package/dist/src/pagination/pagination.d.ts +8 -1
  106. package/dist/src/ripple/ripple.d.ts +19 -3
  107. package/dist/src/search/index.d.ts +1 -0
  108. package/dist/src/search/search.d.ts +76 -0
  109. package/dist/src/segmented-button/index.d.ts +2 -0
  110. package/dist/src/segmented-button/segmented-button-group.d.ts +46 -0
  111. package/dist/src/segmented-button/segmented-button.d.ts +65 -0
  112. package/dist/src/select/index.d.ts +3 -0
  113. package/dist/src/select/option.d.ts +55 -0
  114. package/dist/src/select/select.d.ts +114 -0
  115. package/dist/src/side-sheet/index.d.ts +1 -0
  116. package/dist/src/side-sheet/side-sheet.d.ts +41 -0
  117. package/dist/src/slider/slider.d.ts +4 -0
  118. package/dist/src/snackbar/snackbar.d.ts +14 -1
  119. package/dist/src/tabs/tab-group.d.ts +0 -1
  120. package/dist/src/tabs/tab.d.ts +8 -2
  121. package/dist/src/tabs/tabs.d.ts +13 -1
  122. package/dist/src/toolbar/index.d.ts +1 -0
  123. package/dist/src/toolbar/toolbar.d.ts +86 -0
  124. package/dist/state-DwbEjqVk.js +10 -0
  125. package/dist/state-DwbEjqVk.js.map +1 -0
  126. package/dist/{style-map-DcB52w-l.js → style-map-DVmWOuYy.js} +3 -3
  127. package/dist/{style-map-DcB52w-l.js.map → style-map-DVmWOuYy.js.map} +1 -1
  128. package/dist/test/search.test.d.ts +1 -0
  129. package/dist/test/toolbar.test.d.ts +1 -0
  130. package/dist/throttle-C7ZAPqtu.js +24 -0
  131. package/dist/throttle-C7ZAPqtu.js.map +1 -0
  132. package/dist/toolbar.js +306 -0
  133. package/dist/toolbar.js.map +1 -0
  134. package/dist/tsconfig.tsbuildinfo +1 -1
  135. package/dist/{unsafe-html-C2r3PyzF.js → unsafe-html-BsGUjx94.js} +3 -3
  136. package/dist/{unsafe-html-C2r3PyzF.js.map → unsafe-html-BsGUjx94.js.map} +1 -1
  137. package/package.json +1 -1
  138. package/readme.md +2 -2
  139. package/scss/styles.scss +4 -0
  140. package/src/__base_element/BaseHyperlink.ts +42 -0
  141. package/src/__base_element/README.md +19 -0
  142. package/src/__utils/cache-fetch.ts +65 -0
  143. package/src/{utils → __utils}/dispatch-event-utils.ts +1 -0
  144. package/src/__utils/is-dark-mode.ts +3 -0
  145. package/src/__utils/is-in-viewport.ts +6 -0
  146. package/src/__utils/observe-slot-change.ts +38 -0
  147. package/src/__utils/sanitize-svg.ts +27 -0
  148. package/src/__utils/throttle.ts +27 -0
  149. package/src/accordion/accordion-item.scss +136 -65
  150. package/src/accordion/accordion-item.ts +117 -44
  151. package/src/accordion/accordion.scss +24 -5
  152. package/src/accordion/accordion.ts +29 -23
  153. package/src/accordion/demo/index.html +74 -35
  154. package/src/banner/banner.scss +87 -0
  155. package/src/banner/banner.ts +107 -0
  156. package/src/banner/index.ts +1 -0
  157. package/src/bottom-sheet/bottom-sheet.scss +88 -0
  158. package/src/bottom-sheet/bottom-sheet.ts +135 -0
  159. package/src/bottom-sheet/index.ts +1 -0
  160. package/src/button/BaseButton.ts +26 -30
  161. package/src/button/button/button-colors.scss +90 -19
  162. package/src/button/button/button-sizes.scss +39 -19
  163. package/src/button/button/button.scss +117 -116
  164. package/src/button/button/button.ts +29 -6
  165. package/src/button/button-group/button-group.scss +25 -22
  166. package/src/button/button-group/button-group.ts +122 -5
  167. package/src/button/icon-button/icon-button-sizes.scss +35 -15
  168. package/src/button/icon-button/icon-button.ts +25 -12
  169. package/src/card/card-colors.scss +10 -0
  170. package/src/card/card-content.ts +26 -0
  171. package/src/card/card.scss +221 -41
  172. package/src/card/card.ts +251 -8
  173. package/src/card/index.ts +1 -0
  174. package/src/chart-bar/chart-bar.ts +1 -1
  175. package/src/chart-bar/chart-stacked-bar.ts +3 -1
  176. package/src/chart-doughnut/chart-doughnut.ts +1 -1
  177. package/src/chart-pie/chart-pie.ts +1 -1
  178. package/src/checkbox/checkbox.ts +1 -1
  179. package/src/clock/clock.ts +1 -1
  180. package/src/code-editor/code-editor.ts +5 -5
  181. package/src/code-highlighter/code-highlighter.ts +2 -2
  182. package/src/container/container.ts +1 -1
  183. package/src/date-picker/date-picker.ts +5 -2
  184. package/src/divider/divider.ts +3 -1
  185. package/src/empty-state/empty-state.scss +9 -3
  186. package/src/empty-state/empty-state.ts +2 -2
  187. package/src/fab/fab-colors.scss +49 -0
  188. package/src/fab/fab-sizes.scss +47 -0
  189. package/src/fab/fab.scss +137 -0
  190. package/src/fab/fab.ts +285 -0
  191. package/src/fab/index.ts +1 -0
  192. package/src/field/field.ts +3 -1
  193. package/src/focus-ring/focus-ring.ts +37 -19
  194. package/src/icon/datasource.ts +1 -1
  195. package/src/icon/icon.ts +3 -1
  196. package/src/image/image.ts +3 -2
  197. package/src/index.ts +12 -1
  198. package/src/input/input.ts +5 -2
  199. package/src/link/link.ts +2 -15
  200. package/src/menu/menu/menu.scss +31 -3
  201. package/src/menu/menu/menu.ts +30 -6
  202. package/src/menu/menu-item/menu-item.scss +1 -0
  203. package/src/menu/menu-item/menu-item.ts +1 -9
  204. package/src/menu/sub-menu/sub-menu.ts +1 -0
  205. package/src/notification/index.ts +1 -0
  206. package/src/notification/notification.scss +201 -0
  207. package/src/notification/notification.ts +206 -0
  208. package/src/number-counter/number-counter.ts +3 -1
  209. package/src/number-field/number-field.ts +4 -2
  210. package/src/pagination/pagination.scss +33 -24
  211. package/src/pagination/pagination.ts +113 -60
  212. package/src/peacock-loader.ts +48 -0
  213. package/src/radio/radio.ts +3 -1
  214. package/src/ripple/ripple.ts +19 -3
  215. package/src/search/index.ts +1 -0
  216. package/src/search/search-colors.scss +14 -0
  217. package/src/search/search.scss +204 -0
  218. package/src/search/search.ts +240 -0
  219. package/src/segmented-button/index.ts +2 -0
  220. package/src/segmented-button/segmented-button-group.scss +21 -0
  221. package/src/segmented-button/segmented-button-group.ts +110 -0
  222. package/src/segmented-button/segmented-button.scss +115 -0
  223. package/src/segmented-button/segmented-button.ts +175 -0
  224. package/src/select/index.ts +3 -0
  225. package/src/select/option.ts +109 -0
  226. package/src/select/select.scss +125 -0
  227. package/src/select/select.ts +520 -0
  228. package/src/side-sheet/index.ts +1 -0
  229. package/src/side-sheet/side-sheet.scss +79 -0
  230. package/src/side-sheet/side-sheet.ts +100 -0
  231. package/src/slider/slider.scss +19 -1
  232. package/src/slider/slider.ts +30 -19
  233. package/src/snackbar/snackbar.scss +62 -31
  234. package/src/snackbar/snackbar.ts +92 -12
  235. package/src/switch/switch.ts +3 -1
  236. package/src/table/table.ts +3 -1
  237. package/src/tabs/demo/index.html +90 -0
  238. package/src/tabs/tab-group.ts +0 -3
  239. package/src/tabs/tab.scss +237 -25
  240. package/src/tabs/tab.ts +91 -14
  241. package/src/tabs/tabs.scss +37 -3
  242. package/src/tabs/tabs.ts +118 -2
  243. package/src/textarea/textarea.ts +4 -2
  244. package/src/time-picker/time-picker.ts +4 -2
  245. package/src/toolbar/index.ts +1 -0
  246. package/src/toolbar/toolbar-colors.scss +16 -0
  247. package/src/toolbar/toolbar.scss +165 -0
  248. package/src/toolbar/toolbar.ts +137 -0
  249. package/dist/IndividualComponent-Dt5xirYG.js +0 -73
  250. package/dist/IndividualComponent-Dt5xirYG.js.map +0 -1
  251. package/dist/button-ClzS8JLq.js.map +0 -1
  252. package/dist/button-group-BMS5WvaF.js +0 -292
  253. package/dist/button-group-BMS5WvaF.js.map +0 -1
  254. package/dist/chart-donut.js +0 -309
  255. package/dist/chart-donut.js.map +0 -1
  256. package/dist/class-map-59YGWLnx.js.map +0 -1
  257. package/dist/directive-Cuw6h7YA.js +0 -9
  258. package/dist/directive-Cuw6h7YA.js.map +0 -1
  259. package/dist/dispatch-event-utils-B4odODQf.js +0 -277
  260. package/dist/dispatch-event-utils-B4odODQf.js.map +0 -1
  261. package/dist/observe-theme-change-pALI5fmV.js.map +0 -1
  262. package/dist/radio-b70_Ie9n.js.map +0 -1
  263. package/dist/src/chart-donut/chart-donut.d.ts +0 -53
  264. package/dist/src/chart-donut/index.d.ts +0 -1
  265. package/dist/src/styleMixins.css.d.ts +0 -9
  266. package/dist/src/utils.d.ts +0 -9
  267. package/src/chart-donut/chart-donut.scss +0 -37
  268. package/src/chart-donut/chart-donut.ts +0 -287
  269. package/src/chart-donut/demo/index.html +0 -51
  270. package/src/chart-donut/index.ts +0 -1
  271. package/src/styleMixins.css.ts +0 -55
  272. package/src/utils.ts +0 -193
  273. /package/dist/src/{spread.d.ts → __directive/spread.d.ts} +0 -0
  274. /package/dist/src/{utils → __utils}/copy-to-clipboard.d.ts +0 -0
  275. /package/dist/src/{utils → __utils}/dispatch-event-utils.d.ts +0 -0
  276. /package/dist/src/{utils → __utils}/observe-theme-change.d.ts +0 -0
  277. /package/dist/test/{card.test.d.ts → banner.test.d.ts} +0 -0
  278. /package/src/{spread.ts → __directive/spread.ts} +0 -0
  279. /package/src/{utils → __utils}/copy-to-clipboard.ts +0 -0
  280. /package/src/{utils → __utils}/observe-theme-change.ts +0 -0
@@ -0,0 +1,520 @@
1
+ import { html, nothing } from 'lit';
2
+ import { property, query, state } from 'lit/decorators.js';
3
+ import { classMap } from 'lit/directives/class-map.js';
4
+ import BaseInput from '../input/BaseInput.js';
5
+ import styles from './select.scss';
6
+ import type { Menu } from '../menu/menu/menu.js';
7
+ import { SelectOptionElement } from './option.js';
8
+
9
+ export interface SelectOption {
10
+ label: string;
11
+ value: string;
12
+ icon?: string;
13
+ }
14
+
15
+ /**
16
+ * @label Select
17
+ * @tag wc-select
18
+ * @rawTag select
19
+ *
20
+ * @summary A dropdown select component supporting single and multi-select with optional typeahead search.
21
+ * @overview
22
+ * Select builds on wc-field and wc-menu to provide a fully-featured dropdown picker.
23
+ *
24
+ * - Single and multi-select modes
25
+ * - Client-side typeahead with `search="contains"`
26
+ * - Server-side typeahead with `search="managed"`
27
+ * - Multi-select chips display
28
+ *
29
+ * @example
30
+ * ```html
31
+ * <wc-select label="Fruit" placeholder="Pick a fruit...">
32
+ * <wc-option value="apple">Apple</wc-option>
33
+ * <wc-option value="banana">Banana</wc-option>
34
+ * </wc-select>
35
+ * ```
36
+ * @tags form
37
+ */
38
+ export class Select extends BaseInput {
39
+ static styles = [styles];
40
+
41
+ private readonly _menuId = `wc-select-menu-${Math.random().toString(36).slice(2, 9)}`;
42
+
43
+ /**
44
+ * Array of options to display in the dropdown.
45
+ * Setting this property creates matching `<wc-option>` children automatically.
46
+ */
47
+ @property({ type: Array })
48
+ options: SelectOption[] = [];
49
+
50
+ /**
51
+ * The selected value. For multi-select, a comma-separated list of values.
52
+ */
53
+ @property({ type: String, reflect: true })
54
+ value: string = '';
55
+
56
+ /**
57
+ * Enable multi-select mode.
58
+ */
59
+ @property({ type: Boolean, reflect: true })
60
+ multiple: boolean = false;
61
+
62
+ /**
63
+ * Enable typeahead search.
64
+ * - `'contains'`: filters options client-side.
65
+ * - `'managed'`: emits a `select-search` event for server-controlled filtering.
66
+ */
67
+ @property({ type: String })
68
+ search: '' | 'contains' | 'managed' = '';
69
+
70
+ /**
71
+ * Placeholder text shown when no value is selected.
72
+ */
73
+ @property({ type: String })
74
+ placeholder: string = '';
75
+
76
+ /**
77
+ * Label displayed above the field.
78
+ */
79
+ @property({ type: String })
80
+ label: string = '';
81
+
82
+ /**
83
+ * Visual variant of the field.
84
+ */
85
+ @property({ type: String })
86
+ variant: 'filled' | 'outlined' | 'default' = 'default';
87
+
88
+ /**
89
+ * Helper text displayed below the field.
90
+ */
91
+ @property({ type: String, attribute: 'helper-text' })
92
+ helperText: string = '';
93
+
94
+ @property({ type: Boolean })
95
+ error: boolean = false;
96
+
97
+ @property({ type: String, attribute: 'error-text' })
98
+ errorText: string = '';
99
+
100
+ @property({ type: Boolean })
101
+ warning: boolean = false;
102
+
103
+ @property({ type: String, attribute: 'warning-text' })
104
+ warningText: string = '';
105
+
106
+ @state()
107
+ private _open: boolean = false;
108
+
109
+ @state()
110
+ private _focused: boolean = false;
111
+
112
+ @state()
113
+ private _searchQuery: string = '';
114
+
115
+ /** True when all options are filtered out by the current search query. */
116
+ @state()
117
+ private _noOptionsVisible: boolean = false;
118
+
119
+ @query('.select-trigger')
120
+ private _triggerEl?: HTMLElement;
121
+
122
+ private get _menu(): Menu | null {
123
+ return (this.renderRoot?.querySelector('wc-menu') as unknown as Menu) ?? null;
124
+ }
125
+
126
+ @query('.search-input')
127
+ private _searchInputEl?: HTMLInputElement;
128
+
129
+ override focus() {
130
+ this._triggerEl?.focus();
131
+ }
132
+
133
+ override blur() {
134
+ this._triggerEl?.blur();
135
+ }
136
+
137
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
138
+
139
+ protected override updated(changedProperties: Map<string, unknown>) {
140
+ if (changedProperties.has('options')) {
141
+ this._syncProgrammaticOptions();
142
+ }
143
+ this._syncOptionStates();
144
+ }
145
+
146
+ // ── Programmatic options ───────────────────────────────────────────────────
147
+
148
+ /**
149
+ * Reconciles the `options` property with auto-generated `<wc-option>` light-DOM
150
+ * children (marked `data-generated`). Declarative children placed by the
151
+ * consumer are left untouched.
152
+ */
153
+ private _syncProgrammaticOptions() {
154
+ this.querySelectorAll('wc-option[data-generated]').forEach(el => el.remove());
155
+ for (const opt of this.options) {
156
+ const el = new SelectOptionElement();
157
+ el.value = opt.value;
158
+ if (opt.icon) el.icon = opt.icon;
159
+ el.textContent = opt.label || (opt.value === '' ? 'None' : '');
160
+ el.dataset.generated = '';
161
+ this.appendChild(el);
162
+ }
163
+ }
164
+
165
+ // ── Option state sync ──────────────────────────────────────────────────────
166
+
167
+ /**
168
+ * Pushes `selected`, `keepOpen`, and `filtered` state onto every `<wc-option>`
169
+ * child element so each one can render itself correctly.
170
+ */
171
+ private _syncOptionStates() {
172
+ const optEls = Array.from(
173
+ this.querySelectorAll<SelectOptionElement>('wc-option'),
174
+ );
175
+ let visibleCount = 0;
176
+ for (const opt of optEls) {
177
+ opt.selected = this._isSelected(opt.value);
178
+ opt.keepOpen = this.multiple;
179
+ if (this.search && this.search !== 'managed' && this._searchQuery) {
180
+ const q = this._searchQuery.toLowerCase();
181
+ const label = opt.textContent?.trim() ?? '';
182
+ opt.filtered = !label.toLowerCase().includes(q);
183
+ if (!opt.filtered) visibleCount += 1;
184
+ } else {
185
+ opt.filtered = false;
186
+ visibleCount += 1;
187
+ }
188
+ }
189
+ this._noOptionsVisible = visibleCount === 0;
190
+ }
191
+
192
+ // ── Helpers ────────────────────────────────────────────────────────────────
193
+
194
+ private get _selectedValues(): string[] {
195
+ if (!this.value) return [];
196
+ return this.value
197
+ .split(',')
198
+ .map(v => v.trim())
199
+ .filter(Boolean);
200
+ }
201
+
202
+ private _isSelected(value: string): boolean {
203
+ if (!this.multiple) {
204
+ return this.value === value;
205
+ }
206
+ return this._selectedValues.includes(value);
207
+ }
208
+
209
+ /** Returns the display label for a given option value. */
210
+ private _getLabelForValue(val: string): string {
211
+ for (const opt of this.querySelectorAll<SelectOptionElement>('wc-option')) {
212
+ if (opt.value === val) {
213
+ const label = opt.textContent?.trim();
214
+ if (label) return label;
215
+ return val === '' ? 'None' : val;
216
+ }
217
+ }
218
+ // Fallback to options array (before wc-option children are created)
219
+ const programmaticLabel = this.options.find(o => o.value === val)?.label;
220
+ if (programmaticLabel) return programmaticLabel;
221
+ return val === '' ? 'None' : val;
222
+ }
223
+
224
+ private get _displayLabel(): string {
225
+ if (!this.value) return '';
226
+ const firstValue = this._selectedValues[0];
227
+ if (!firstValue) return '';
228
+ return this._getLabelForValue(firstValue);
229
+ }
230
+
231
+ private get _isPopulated(): boolean {
232
+ return !!this.value;
233
+ }
234
+
235
+ // ── Menu open/close ────────────────────────────────────────────────────────
236
+
237
+ private _openMenu() {
238
+ if (this.disabled || this.readonly) return;
239
+ this._open = true;
240
+ this._focused = true;
241
+ this._triggerEl?.focus();
242
+ const menu = this._menu;
243
+ if (menu && this._triggerEl) {
244
+ menu.anchorElement = this._triggerEl;
245
+ const triggerWidth = this._triggerEl.getBoundingClientRect().width;
246
+ if (triggerWidth < 240) {
247
+ menu.style.setProperty('--menu-width', '240px');
248
+ } else {
249
+ menu.style.setProperty('--menu-width', `${Math.ceil(triggerWidth)}px`);
250
+ }
251
+ menu.show();
252
+ }
253
+ if (this.search) {
254
+ this._searchQuery = '';
255
+ // Use rAF so that the search input receives focus *after* wc-menu has
256
+ // finished showing and potentially focused a menu item.
257
+ this.updateComplete.then(() => {
258
+ requestAnimationFrame(() => {
259
+ this._searchInputEl?.focus();
260
+ });
261
+ });
262
+ }
263
+ }
264
+
265
+ private _closeMenu() {
266
+ if (!this._open) return;
267
+ this._open = false;
268
+ this._focused = false;
269
+ this._searchQuery = '';
270
+ this._menu?.close();
271
+ }
272
+
273
+ // ── Event handlers ─────────────────────────────────────────────────────────
274
+
275
+ private _handleTriggerClick(event: MouseEvent) {
276
+ event.stopPropagation();
277
+ // Ignore clicks that originated inside the search input — those should not
278
+ // toggle the menu (the input needs to stay open so the user can type).
279
+ if (event.target instanceof HTMLInputElement) {
280
+ return;
281
+ }
282
+ if (this._open) {
283
+ this._closeMenu();
284
+ } else {
285
+ this._openMenu();
286
+ }
287
+ }
288
+
289
+ private _handleFieldClick(event: MouseEvent) {
290
+ const eventPath = event.composedPath();
291
+
292
+ if (
293
+ eventPath.includes(this._triggerEl as EventTarget) ||
294
+ eventPath.some(
295
+ target =>
296
+ target instanceof HTMLInputElement ||
297
+ (target instanceof HTMLElement &&
298
+ (target.closest('.clear-btn') != null ||
299
+ target.matches('wc-icon-button'))),
300
+ )
301
+ ) {
302
+ return;
303
+ }
304
+
305
+ if (this._open) {
306
+ this._closeMenu();
307
+ } else {
308
+ this._openMenu();
309
+ }
310
+ }
311
+
312
+ private _handleTriggerKeyDown(event: KeyboardEvent) {
313
+ // When the typeahead search input is active, let the input handle its own
314
+ // keys (Space, Enter, etc.). Only intercept Escape to close the menu.
315
+ if (event.target instanceof HTMLInputElement) {
316
+ if (event.key === 'Escape') {
317
+ event.preventDefault();
318
+ this._closeMenu();
319
+ }
320
+ return;
321
+ }
322
+ switch (event.key) {
323
+ case 'Enter':
324
+ case ' ':
325
+ case 'ArrowDown':
326
+ event.preventDefault();
327
+ if (!this._open) this._openMenu();
328
+ break;
329
+ case 'Escape':
330
+ if (this._open) {
331
+ event.preventDefault();
332
+ this._closeMenu();
333
+ }
334
+ break;
335
+ default:
336
+ break;
337
+ }
338
+ }
339
+
340
+ private _handleMenuClosed() {
341
+ this._open = false;
342
+ this._focused = false;
343
+ this._searchQuery = '';
344
+ }
345
+
346
+ private _handleMenuItemActivate(event: CustomEvent) {
347
+ const item = event.detail?.item as { value: string } | undefined;
348
+ if (!item) return;
349
+
350
+ const val = item.value;
351
+
352
+ if (val === undefined) return;
353
+
354
+ if (this.multiple) {
355
+ if (val === '') return;
356
+ const values = this._selectedValues;
357
+ const idx = values.indexOf(val);
358
+ if (idx >= 0) {
359
+ values.splice(idx, 1);
360
+ } else {
361
+ values.push(val);
362
+ }
363
+ this.value = values.join(',');
364
+ } else {
365
+ this.value = val;
366
+ this._closeMenu();
367
+ }
368
+
369
+ this._dispatchChange();
370
+ }
371
+
372
+ private _dispatchChange() {
373
+ this.dispatchEvent(
374
+ new CustomEvent('change', {
375
+ detail: { value: this.value },
376
+ bubbles: true,
377
+ composed: true,
378
+ }),
379
+ );
380
+ }
381
+
382
+ private _handleSearchInput(event: InputEvent) {
383
+ this._searchQuery = (event.target as HTMLInputElement).value;
384
+ if (this.search === 'managed') {
385
+ this.dispatchEvent(
386
+ new CustomEvent('select-search', {
387
+ detail: { value: this._searchQuery },
388
+ bubbles: true,
389
+ composed: true,
390
+ }),
391
+ );
392
+ }
393
+ }
394
+
395
+ private _handleChipDismiss(event: CustomEvent, chipValue: string) {
396
+ event.stopPropagation();
397
+ const values = this._selectedValues.filter(v => v !== chipValue);
398
+ this.value = values.join(',');
399
+ this._dispatchChange();
400
+ }
401
+
402
+ private _renderEmptyState() {
403
+ const hasSearchQuery = this._searchQuery.trim().length > 0;
404
+
405
+ return html`
406
+ <wc-empty-state
407
+ class="select-empty-state content-center"
408
+ illustration="no-document"
409
+ headline=${hasSearchQuery ? 'No results found' : 'No options available'}
410
+ description=${hasSearchQuery
411
+ ? 'Try a different search term.'
412
+ : 'There is nothing to select right now.'}
413
+ ></wc-empty-state>
414
+ `;
415
+ }
416
+
417
+ // ── Render helpers ─────────────────────────────────────────────────────────
418
+
419
+ private _renderTriggerContent() {
420
+ // Typeahead: when open, show a text input for filtering
421
+ if (this.search && this._open) {
422
+ return html`<input
423
+ class="search-input"
424
+ .value=${this._searchQuery}
425
+ placeholder=${this._displayLabel || this.placeholder}
426
+ @input=${this._handleSearchInput}
427
+ />`;
428
+ }
429
+
430
+ // Multi-select: show chips for selected items
431
+ if (this.multiple && this._selectedValues.length > 0) {
432
+ return html`<div class="chips-container">
433
+ ${this._selectedValues.map(
434
+ val => html`
435
+ <wc-chip
436
+ dismissible
437
+ value=${val}
438
+ @tag--dismiss=${(e: CustomEvent) =>
439
+ this._handleChipDismiss(e, val)}
440
+ >${this._getLabelForValue(val)}</wc-chip
441
+ >
442
+ `,
443
+ )}
444
+ </div>`;
445
+ }
446
+
447
+ // Single select: show selected label or placeholder
448
+ const label = this._displayLabel;
449
+ if (!label) {
450
+ return html`<span class="placeholder">${this.placeholder}</span>`;
451
+ }
452
+ return html`<span class="display-value">${label}</span>`;
453
+ }
454
+
455
+ private _renderFieldEnd() {
456
+ return html`
457
+ <wc-icon
458
+ class=${classMap({
459
+ 'dropdown-icon': true,
460
+ 'dropdown-icon--open': this._open,
461
+ })}
462
+ name="arrow_drop_down"
463
+ ></wc-icon>
464
+ `;
465
+ }
466
+
467
+ render() {
468
+ return html`
469
+ <wc-field
470
+ label=${this.label}
471
+ ?required=${this.required}
472
+ ?disabled=${this.disabled}
473
+ ?readonly=${this.readonly}
474
+ ?skeleton=${this.skeleton}
475
+ helper-text=${this.helperText}
476
+ ?error=${this.error}
477
+ error-text=${this.errorText}
478
+ ?warning=${this.warning}
479
+ warning-text=${this.warningText}
480
+ variant=${this.variant}
481
+ ?populated=${this._isPopulated || this._open}
482
+ ?focused=${this._focused}
483
+ .host=${this}
484
+ class="select-field"
485
+ @click=${this._handleFieldClick}
486
+ >
487
+ <div
488
+ class="select-trigger"
489
+ tabindex=${this.disabled ? -1 : 0}
490
+ role="combobox"
491
+ aria-label=${this.label || this.placeholder || 'Select option'}
492
+ aria-controls=${this._menuId}
493
+ aria-expanded=${String(this._open)}
494
+ aria-haspopup="listbox"
495
+ @click=${this._handleTriggerClick}
496
+ @keydown=${this._handleTriggerKeyDown}
497
+ >
498
+ ${this._renderTriggerContent()}
499
+ </div>
500
+
501
+ <div slot="field-end" class="field-end-wrapper">
502
+ ${this._renderFieldEnd()}
503
+ </div>
504
+ </wc-field>
505
+
506
+ <wc-menu
507
+ id=${this._menuId}
508
+ placement="bottom-start"
509
+ aria-label=${this.label || 'Options'}
510
+ @closed=${this._handleMenuClosed}
511
+ @menu-item-activate=${(e: CustomEvent) =>
512
+ this._handleMenuItemActivate(e)}
513
+ >
514
+ <slot></slot>
515
+ ${this._noOptionsVisible ? this._renderEmptyState() : nothing}
516
+ </wc-menu>
517
+ `;
518
+ }
519
+ }
520
+
@@ -0,0 +1 @@
1
+ export { SideSheet } from './side-sheet.js';
@@ -0,0 +1,79 @@
1
+ @use "../../scss/mixin";
2
+
3
+ @include mixin.base-styles;
4
+
5
+ :host {
6
+ display: contents;
7
+
8
+ --side-sheet-container-color: var(--color-surface-container-low, #f7f2fa);
9
+ --side-sheet-scrim-color: rgba(0, 0, 0, 0.32);
10
+ --side-sheet-shape: var(--shape-corner-extra-large, 28px);
11
+ --side-sheet-width: 360px;
12
+ --side-sheet-transition-duration: var(--duration-medium2, 300ms);
13
+ --side-sheet-transition-easing: var(--easing-standard, cubic-bezier(0.2, 0, 0, 1));
14
+ }
15
+
16
+ .scrim {
17
+ inset: 0;
18
+ position: fixed;
19
+ background-color: var(--side-sheet-scrim-color);
20
+ opacity: 0;
21
+ pointer-events: none;
22
+ transition:
23
+ opacity var(--side-sheet-transition-duration) var(--side-sheet-transition-easing);
24
+ z-index: 1000;
25
+ }
26
+
27
+ .scrim.visible {
28
+ opacity: 1;
29
+ pointer-events: auto;
30
+ }
31
+
32
+ .sheet {
33
+ background-color: var(--side-sheet-container-color);
34
+ bottom: 0;
35
+ display: flex;
36
+ flex-direction: column;
37
+ max-width: 100%;
38
+ overflow: hidden;
39
+ position: fixed;
40
+ top: 0;
41
+ transition:
42
+ transform var(--side-sheet-transition-duration) var(--side-sheet-transition-easing);
43
+ width: var(--side-sheet-width);
44
+ will-change: transform;
45
+ z-index: 1001;
46
+ }
47
+
48
+ .sheet.variant-standard {
49
+ position: relative;
50
+ z-index: 1;
51
+ }
52
+
53
+ /* Position: right */
54
+ .sheet.position-right {
55
+ border-radius: var(--side-sheet-shape) 0 0 var(--side-sheet-shape);
56
+ right: 0;
57
+ transform: translateX(100%);
58
+ }
59
+
60
+ .sheet.position-right.open {
61
+ transform: translateX(0);
62
+ }
63
+
64
+ /* Position: left */
65
+ .sheet.position-left {
66
+ border-radius: 0 var(--side-sheet-shape) var(--side-sheet-shape) 0;
67
+ left: 0;
68
+ transform: translateX(-100%);
69
+ }
70
+
71
+ .sheet.position-left.open {
72
+ transform: translateX(0);
73
+ }
74
+
75
+ .content {
76
+ flex: 1 1 auto;
77
+ overflow-y: auto;
78
+ padding: var(--spacing-300, 1.5rem);
79
+ }
@@ -0,0 +1,100 @@
1
+ import { LitElement, html, nothing } from 'lit';
2
+ import { property } from 'lit/decorators.js';
3
+ import { classMap } from 'lit/directives/class-map.js';
4
+ import IndividualComponent from '../IndividualComponent.js';
5
+ import styles from './side-sheet.scss';
6
+
7
+ /**
8
+ * @label Side Sheet
9
+ * @tag wc-side-sheet
10
+ * @rawTag side-sheet
11
+ * @summary Side sheets slide in from the edge of the screen to reveal supplemental content. Supports standard and modal variants per Material Design 3.
12
+ *
13
+ * @cssprop --side-sheet-container-color - Background color of the sheet container.
14
+ * @cssprop --side-sheet-scrim-color - Color of the modal scrim overlay.
15
+ * @cssprop --side-sheet-shape - Corner radius of the leading edge.
16
+ * @cssprop --side-sheet-width - Width of the side sheet.
17
+ *
18
+ * @example
19
+ * ```html
20
+ * Side Sheet
21
+
22
+ * ```
23
+ * @tags navigation, overlay
24
+ */
25
+ @IndividualComponent
26
+ export class SideSheet extends LitElement {
27
+ static styles = [styles];
28
+
29
+ /** Whether the sheet is visible. */
30
+ @property({ type: Boolean, reflect: true }) open = false;
31
+
32
+ /**
33
+ * Sheet variant.
34
+ * - `"standard"`: Coexists with page content; no scrim.
35
+ * - `"modal"`: Overlays page content with a scrim backdrop.
36
+ */
37
+ @property({ type: String, reflect: true }) variant: 'standard' | 'modal' =
38
+ 'modal';
39
+
40
+ /**
41
+ * Side from which the sheet slides in.
42
+ * - `"left"`: Sheet opens from the left edge.
43
+ * - `"right"`: Sheet opens from the right edge.
44
+ */
45
+ @property({ type: String, reflect: true }) position: 'left' | 'right' =
46
+ 'right';
47
+
48
+ show() {
49
+ this.open = true;
50
+ }
51
+
52
+ hide() {
53
+ this._close('programmatic');
54
+ }
55
+
56
+ private _close(reason: string) {
57
+ if (!this.open) return;
58
+ this.open = false;
59
+ this.dispatchEvent(
60
+ new CustomEvent('side-sheet-close', {
61
+ detail: { reason },
62
+ bubbles: true,
63
+ composed: true,
64
+ }),
65
+ );
66
+ }
67
+
68
+ private _handleScrimClick() {
69
+ if (this.variant === 'modal') {
70
+ this._close('scrim');
71
+ }
72
+ }
73
+
74
+ render() {
75
+ return html`
76
+ ${this.variant === 'modal'
77
+ ? html`<div
78
+ class=${classMap({ scrim: true, visible: this.open })}
79
+ @click=${this._handleScrimClick}
80
+ ></div>`
81
+ : nothing}
82
+
83
+ <div
84
+ class=${classMap({
85
+ sheet: true,
86
+ open: this.open,
87
+ [`variant-${this.variant}`]: true,
88
+ [`position-${this.position}`]: true,
89
+ })}
90
+ role="dialog"
91
+ aria-modal=${this.variant === 'modal' ? 'true' : 'false'}
92
+ aria-hidden=${!this.open ? 'true' : 'false'}
93
+ >
94
+ <div class="content">
95
+ <slot></slot>
96
+ </div>
97
+ </div>
98
+ `;
99
+ }
100
+ }