mtrl 0.3.1 → 0.3.3

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 (162) hide show
  1. package/.env +15 -0
  2. package/CONTRIBUTING.md +62 -23
  3. package/DOCS.md +3 -3
  4. package/README.md +43 -20
  5. package/TESTING.md +128 -18
  6. package/dist/index.js +14865 -0
  7. package/git-user-stats.js +545 -0
  8. package/index.ts +9 -67
  9. package/package.json +8 -3
  10. package/src/components/badge/api.ts +15 -1
  11. package/src/components/badge/badge.ts +43 -4
  12. package/src/components/badge/config.ts +40 -8
  13. package/src/components/badge/index.ts +64 -3
  14. package/src/components/badge/types.ts +175 -33
  15. package/src/components/button/api.ts +63 -1
  16. package/src/components/button/button.ts +39 -3
  17. package/src/components/button/config.ts +21 -4
  18. package/src/components/button/index.ts +26 -1
  19. package/src/components/button/types.ts +7 -1
  20. package/src/components/card/api.ts +78 -9
  21. package/src/components/card/card.ts +58 -3
  22. package/src/components/card/config.ts +41 -11
  23. package/src/components/card/features.ts +39 -12
  24. package/src/components/card/index.ts +84 -19
  25. package/src/components/card/types.ts +218 -29
  26. package/src/components/carousel/carousel.ts +92 -28
  27. package/src/components/carousel/constants.ts +107 -21
  28. package/src/components/carousel/index.ts +31 -13
  29. package/src/components/checkbox/checkbox.ts +83 -16
  30. package/src/components/checkbox/index.ts +43 -1
  31. package/src/components/checkbox/types.ts +219 -32
  32. package/src/components/chips/api.ts +194 -0
  33. package/src/components/{chip → chips/chip}/api.ts +42 -2
  34. package/src/components/chips/chip/chip.ts +131 -0
  35. package/src/components/{chip → chips/chip}/config.ts +3 -3
  36. package/src/components/chips/chip/index.ts +3 -0
  37. package/src/components/chips/chips.md +481 -0
  38. package/src/components/chips/chips.ts +75 -0
  39. package/src/components/chips/config.ts +109 -0
  40. package/src/components/chips/constants.ts +61 -0
  41. package/src/components/chips/features/chip-items.ts +33 -0
  42. package/src/components/chips/features/container.ts +77 -0
  43. package/src/components/chips/features/controller.ts +448 -0
  44. package/src/components/chips/features/index.ts +5 -0
  45. package/src/components/chips/features/label.ts +108 -0
  46. package/src/components/chips/index.ts +11 -0
  47. package/src/components/chips/schema.ts +61 -0
  48. package/src/components/{chip → chips}/types.ts +203 -92
  49. package/src/components/dialog/dialog.ts +99 -16
  50. package/src/components/dialog/index.ts +97 -1
  51. package/src/components/dialog/types.ts +375 -69
  52. package/src/components/divider/config.ts +90 -6
  53. package/src/components/divider/divider.ts +32 -2
  54. package/src/components/divider/features.ts +26 -0
  55. package/src/components/divider/index.ts +30 -0
  56. package/src/components/divider/types.ts +86 -9
  57. package/src/components/extended-fab/api.ts +53 -1
  58. package/src/components/extended-fab/config.ts +29 -1
  59. package/src/components/extended-fab/extended-fab.ts +28 -0
  60. package/src/components/extended-fab/index.ts +36 -0
  61. package/src/components/extended-fab/types.ts +458 -13
  62. package/src/components/fab/api.ts +42 -2
  63. package/src/components/fab/config.ts +29 -1
  64. package/src/components/fab/fab.ts +16 -2
  65. package/src/components/fab/index.ts +35 -0
  66. package/src/components/fab/types.ts +374 -10
  67. package/src/components/list/api.ts +12 -2
  68. package/src/components/list/config.ts +21 -0
  69. package/src/components/list/features.ts +6 -0
  70. package/src/components/list/index.ts +56 -1
  71. package/src/components/list/list-item.ts +46 -2
  72. package/src/components/list/list.ts +73 -2
  73. package/src/components/list/types.ts +172 -0
  74. package/src/components/list/utils.ts +26 -2
  75. package/src/components/menu/api.ts +217 -20
  76. package/src/components/menu/config.ts +27 -0
  77. package/src/components/menu/features/visibility.ts +55 -6
  78. package/src/components/menu/index.ts +64 -0
  79. package/src/components/menu/menu-item.ts +46 -3
  80. package/src/components/menu/menu.ts +77 -1
  81. package/src/components/menu/types.ts +404 -39
  82. package/src/components/navigation/nav-item.ts +13 -2
  83. package/src/components/sheet/config.ts +1 -2
  84. package/src/components/sheet/features/gestures.ts +1 -1
  85. package/src/components/sheet/features/position.ts +1 -2
  86. package/src/components/sheet/features/state.ts +1 -1
  87. package/src/components/sheet/index.ts +10 -2
  88. package/src/components/sheet/sheet.ts +1 -2
  89. package/src/components/sheet/types.ts +29 -1
  90. package/src/components/slider/api.ts +1 -1
  91. package/src/components/slider/config.ts +1 -1
  92. package/src/components/slider/features/controller.ts +1 -1
  93. package/src/components/slider/features/handlers.ts +1 -1
  94. package/src/components/slider/features/states.ts +1 -1
  95. package/src/components/slider/index.ts +12 -5
  96. package/src/components/slider/schema.ts +1 -1
  97. package/src/components/slider/types.ts +31 -0
  98. package/src/components/tabs/tab-api.ts +1 -1
  99. package/src/components/tabs/types.ts +1 -1
  100. package/src/components/tooltip/api.ts +6 -2
  101. package/src/components/tooltip/config.ts +9 -28
  102. package/src/components/tooltip/index.ts +10 -1
  103. package/src/components/tooltip/types.ts +38 -3
  104. package/src/core/dom/create.ts +57 -51
  105. package/src/index.ts +129 -31
  106. package/src/styles/abstract/_mixins.scss +23 -9
  107. package/src/styles/abstract/_variables.scss +14 -4
  108. package/src/styles/components/_card.scss +1 -1
  109. package/src/styles/components/_chip.scss +323 -113
  110. package/src/styles/components/_tabs.scss +1 -1
  111. package/src/styles/themes/_autumn.scss +3 -0
  112. package/CLAUDE.md +0 -33
  113. package/src/components/checkbox/constants.ts +0 -37
  114. package/src/components/chip/chip-set.ts +0 -225
  115. package/src/components/chip/chip.ts +0 -118
  116. package/src/components/chip/constants.ts +0 -28
  117. package/src/components/chip/index.ts +0 -12
  118. package/src/components/list/constants.ts +0 -116
  119. package/src/components/sheet/constants.ts +0 -20
  120. package/src/components/slider/constants.ts +0 -32
  121. package/src/components/tooltip/constants.ts +0 -27
  122. package/test/components/badge.test.ts +0 -545
  123. package/test/components/bottom-app-bar.test.ts +0 -303
  124. package/test/components/button.test.ts +0 -233
  125. package/test/components/card.test.ts +0 -560
  126. package/test/components/carousel.test.ts +0 -951
  127. package/test/components/checkbox.test.ts +0 -462
  128. package/test/components/chip.test.ts +0 -692
  129. package/test/components/datepicker.test.ts +0 -1124
  130. package/test/components/dialog.test.ts +0 -990
  131. package/test/components/divider.test.ts +0 -412
  132. package/test/components/extended-fab.test.ts +0 -672
  133. package/test/components/fab.test.ts +0 -561
  134. package/test/components/list.test.ts +0 -365
  135. package/test/components/menu.test.ts +0 -718
  136. package/test/components/navigation.test.ts +0 -186
  137. package/test/components/progress.test.ts +0 -567
  138. package/test/components/radios.test.ts +0 -699
  139. package/test/components/search.test.ts +0 -1135
  140. package/test/components/segmented-button.test.ts +0 -732
  141. package/test/components/sheet.test.ts +0 -641
  142. package/test/components/slider.test.ts +0 -1220
  143. package/test/components/snackbar.test.ts +0 -461
  144. package/test/components/switch.test.ts +0 -452
  145. package/test/components/tabs.test.ts +0 -1369
  146. package/test/components/textfield.test.ts +0 -400
  147. package/test/components/timepicker.test.ts +0 -592
  148. package/test/components/tooltip.test.ts +0 -630
  149. package/test/components/top-app-bar.test.ts +0 -566
  150. package/test/core/dom.attributes.test.ts +0 -148
  151. package/test/core/dom.classes.test.ts +0 -152
  152. package/test/core/dom.events.test.ts +0 -243
  153. package/test/core/emitter.test.ts +0 -141
  154. package/test/core/ripple.test.ts +0 -99
  155. package/test/core/state.store.test.ts +0 -189
  156. package/test/core/utils.normalize.test.ts +0 -61
  157. package/test/core/utils.object.test.ts +0 -120
  158. package/test/setup.js +0 -371
  159. package/test/setup.ts +0 -451
  160. package/tsconfig.json +0 -22
  161. package/typedoc.json +0 -28
  162. package/typedoc.simple.json +0 -14
@@ -0,0 +1,131 @@
1
+ // src/components/chips/chip/chip.ts
2
+ import { PREFIX } from '../../../core/config';
3
+ import { pipe } from '../../../core/compose';
4
+ import { createBase, withElement } from '../../../core/compose/component';
5
+ import {
6
+ withEvents,
7
+ withText,
8
+ withIcon,
9
+ withVariant,
10
+ withRipple,
11
+ withDisabled,
12
+ withLifecycle
13
+ } from '../../../core/compose/features';
14
+ import { withAPI } from './api';
15
+ import { ChipConfig, ChipComponent } from '../types';
16
+ import { createBaseConfig, getElementConfig, getApiConfig } from './config';
17
+
18
+ /**
19
+ * Creates a new Chip component
20
+ * @param {ChipConfig} config - Chip configuration
21
+ * @returns {ChipComponent} Chip component instance
22
+ */
23
+ const createChip = (config: ChipConfig = {}): ChipComponent => {
24
+ const baseConfig = createBaseConfig(config);
25
+
26
+ try {
27
+ // Create base component with core features
28
+ const chip = pipe(
29
+ createBase,
30
+ withEvents(),
31
+ withElement(getElementConfig(baseConfig)),
32
+ withVariant(baseConfig),
33
+ withDisabled(baseConfig),
34
+ withRipple(baseConfig),
35
+ withLifecycle()
36
+ )(baseConfig);
37
+
38
+ // Create a container for the chip content to ensure proper ordering
39
+ const contentContainer = document.createElement('div');
40
+ contentContainer.className = `${chip.getClass('chip')}-content`;
41
+ contentContainer.style.display = 'flex';
42
+ contentContainer.style.alignItems = 'center';
43
+ contentContainer.style.gap = '8px';
44
+ contentContainer.style.width = '100%';
45
+ chip.element.appendChild(contentContainer);
46
+
47
+ // Add leading icon if provided
48
+ if (config.leadingIcon || config.icon) {
49
+ const leadingIconElement = document.createElement('span');
50
+ leadingIconElement.className = `${chip.getClass('chip')}-leading-icon`;
51
+ leadingIconElement.innerHTML = config.leadingIcon || config.icon || '';
52
+ contentContainer.appendChild(leadingIconElement);
53
+ }
54
+
55
+ // Add text element if provided
56
+ if (config.text) {
57
+ const textElement = document.createElement('span');
58
+ textElement.className = `${chip.getClass('chip')}-text`;
59
+ textElement.textContent = config.text;
60
+ contentContainer.appendChild(textElement);
61
+ }
62
+
63
+ // Add trailing icon if provided
64
+ if (config.trailingIcon) {
65
+ const trailingIconElement = document.createElement('span');
66
+ trailingIconElement.className = `${chip.getClass('chip')}-trailing-icon`;
67
+ trailingIconElement.innerHTML = config.trailingIcon;
68
+
69
+ // Add click handler for trailing icon
70
+ if (config.onTrailingIconClick) {
71
+ trailingIconElement.addEventListener('click', (e) => {
72
+ e.stopPropagation(); // Prevent chip click event
73
+ config.onTrailingIconClick(chip as unknown as ChipComponent);
74
+ });
75
+ }
76
+
77
+ contentContainer.appendChild(trailingIconElement);
78
+ }
79
+
80
+ // Add selected class if needed
81
+ if (config.selected) {
82
+ chip.element.classList.add(`${chip.getClass('chip')}--selected`);
83
+ chip.element.setAttribute('aria-selected', 'true');
84
+ }
85
+
86
+ // Set data-value attribute if provided
87
+ if (config.value) {
88
+ chip.element.setAttribute('data-value', config.value);
89
+ }
90
+
91
+ // Add API methods to the component
92
+ const enhancedChip = withAPI(getApiConfig(chip))(chip);
93
+
94
+ // Initialize value if not already set
95
+ if (!chip.element.hasAttribute('data-value') && config.text) {
96
+ enhancedChip.getValue(); // This will trigger the automatic value generation in our fixed API
97
+ }
98
+
99
+ // Add click handler for selection toggle
100
+ if (config.variant === 'filter' ||
101
+ config.variant === 'assist' ||
102
+ config.variant === 'suggestion' ||
103
+ config.selectable) {
104
+
105
+ chip.element.addEventListener('click', () => {
106
+ if (enhancedChip.isDisabled()) return;
107
+
108
+ const wasSelected = enhancedChip.isSelected();
109
+ enhancedChip.toggleSelected();
110
+ const isNowSelected = enhancedChip.isSelected();
111
+
112
+ // Call onChange with correct parameters
113
+ if (config.onChange && wasSelected !== isNowSelected) {
114
+ config.onChange(isNowSelected, enhancedChip);
115
+ }
116
+
117
+ // Call onSelect with consistent parameters
118
+ if (config.onSelect) {
119
+ config.onSelect(enhancedChip);
120
+ }
121
+ });
122
+ }
123
+
124
+ return enhancedChip as unknown as ChipComponent;
125
+ } catch (error) {
126
+ console.error('Chip creation error:', error);
127
+ throw new Error(`Failed to create chip: ${(error as Error).message}`);
128
+ }
129
+ };
130
+
131
+ export default createChip;
@@ -1,10 +1,10 @@
1
- // src/components/chip/config.ts
1
+ // src/components/chips/chip/config.ts
2
2
  import {
3
3
  createComponentConfig,
4
4
  createElementConfig,
5
5
  BaseComponentConfig
6
- } from '../../core/config/component-config';
7
- import { ChipConfig, BaseComponent } from './types';
6
+ } from '../../../core/config/component-config';
7
+ import { ChipConfig, BaseComponent } from '../types';
8
8
 
9
9
  /**
10
10
  * Default configuration for the Chip component
@@ -0,0 +1,3 @@
1
+ // src/components/chips/chip/index.ts
2
+ export { default } from './chip';
3
+ export * from './config';
@@ -0,0 +1,481 @@
1
+ # MTRL Chips Component
2
+
3
+ ## Overview
4
+
5
+ The Chips component is a lightweight, composable UI element used to represent discrete information, such as attributes, selections, filters, or user input. This TypeScript implementation provides a comprehensive API with zero dependencies, focusing on performance, accessibility, and extensibility.
6
+
7
+ ## Features
8
+
9
+ - **Multiple Variants**: Filled, outlined, elevated, assist, filter, input, and suggestion chips
10
+ - **Selection Support**: Single or multi-select modes with clear API for selection management
11
+ - **Layout Options**: Horizontal or vertical arrangement with optional scrolling
12
+ - **Icon Support**: Leading and trailing icons with customizable click handlers
13
+ - **Keyboard Navigation**: Full accessibility support with keyboard controls
14
+ - **Type Safety**: Comprehensive TypeScript interfaces for improved developer experience
15
+ - **Zero Dependencies**: Built with vanilla TypeScript for minimal bundle size
16
+ - **Composition-based Architecture**: Functional composition for extensibility
17
+ - **Accessibility**: ARIA attributes and keyboard support built-in
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ # Using npm
23
+ npm install mtrl
24
+
25
+ # Using yarn
26
+ yarn add mtrl
27
+
28
+ # Using bun
29
+ bun add mtrl
30
+ ```
31
+
32
+ ## Basic Usage
33
+
34
+ ### Single Chip
35
+
36
+ ```typescript
37
+ import { createChip } from 'mtrl';
38
+
39
+ // Create a basic chip
40
+ const basicChip = createChip({
41
+ text: 'Basic Chip',
42
+ variant: 'filled'
43
+ });
44
+
45
+ // Add to DOM
46
+ document.querySelector('.container').appendChild(basicChip.element);
47
+ ```
48
+
49
+ ### Chips Container
50
+
51
+ ```typescript
52
+ import { createChips } from 'mtrl';
53
+
54
+ // Create a chips container with multiple chips
55
+ const filterChips = createChips({
56
+ multiSelect: true,
57
+ label: 'Categories',
58
+ chips: [
59
+ { text: 'JavaScript', variant: 'filter', value: 'js' },
60
+ { text: 'TypeScript', variant: 'filter', value: 'ts' },
61
+ { text: 'HTML', variant: 'filter', value: 'html' },
62
+ { text: 'CSS', variant: 'filter', value: 'css' }
63
+ ],
64
+ onChange: (selectedValues) => {
65
+ console.log('Selected categories:', selectedValues);
66
+ }
67
+ });
68
+
69
+ // Add to DOM
70
+ document.querySelector('.filters').appendChild(filterChips.element);
71
+ ```
72
+
73
+ ## Component API
74
+
75
+ ### Chip Configuration
76
+
77
+ ```typescript
78
+ interface ChipConfig {
79
+ variant?: 'filled' | 'outlined' | 'elevated' | 'assist' | 'filter' | 'input' | 'suggestion';
80
+ disabled?: boolean;
81
+ selected?: boolean;
82
+ text?: string;
83
+ icon?: string;
84
+ leadingIcon?: string;
85
+ trailingIcon?: string;
86
+ class?: string;
87
+ value?: string;
88
+ ripple?: boolean;
89
+ selectable?: boolean;
90
+ onTrailingIconClick?: (chip: ChipComponent) => void;
91
+ onSelect?: (chip: ChipComponent) => void;
92
+ onChange?: (selected: boolean, chip: ChipComponent) => void;
93
+ }
94
+ ```
95
+
96
+ ### Chips Configuration
97
+
98
+ ```typescript
99
+ interface ChipsConfig {
100
+ chips?: ChipConfig[];
101
+ scrollable?: boolean;
102
+ vertical?: boolean;
103
+ class?: string;
104
+ selector?: string | null;
105
+ multiSelect?: boolean;
106
+ onChange?: (selectedValues: (string | null)[], changedValue: string | null) => void;
107
+ label?: string;
108
+ labelPosition?: 'start' | 'end';
109
+ }
110
+ ```
111
+
112
+ ### Chip Variants
113
+
114
+ | Variant | Description |
115
+ |---------|-------------|
116
+ | `filled` | Standard chip with solid background color (default) |
117
+ | `outlined` | Transparent background with outlined border |
118
+ | `elevated` | Chip with subtle shadow effect |
119
+ | `assist` | For suggesting actions to the user |
120
+ | `filter` | For filtering content with selection behavior |
121
+ | `input` | Represents user input in form fields |
122
+ | `suggestion` | For presenting options and suggestions |
123
+
124
+ ## Chip API Methods
125
+
126
+ ### Content Management
127
+
128
+ | Method | Description |
129
+ |--------|-------------|
130
+ | `setText(content)` | Sets the chip's text content |
131
+ | `getText()` | Gets the chip's text content |
132
+ | `setIcon(html)` | Sets the chip's leading icon |
133
+ | `getIcon()` | Gets the chip's leading icon HTML |
134
+ | `setLeadingIcon(html)` | Sets the chip's leading icon |
135
+ | `setTrailingIcon(html, onClick?)` | Sets the chip's trailing icon with optional click handler |
136
+
137
+ ### State Management
138
+
139
+ | Method | Description |
140
+ |--------|-------------|
141
+ | `setValue(value)` | Sets the chip's value attribute |
142
+ | `getValue()` | Gets the chip's value attribute |
143
+ | `isSelected()` | Checks if the chip is selected |
144
+ | `setSelected(selected)` | Sets the chip's selected state |
145
+ | `toggleSelected()` | Toggles the chip's selected state |
146
+ | `isDisabled()` | Checks if the chip is disabled |
147
+ | `enable()` | Enables the chip |
148
+ | `disable()` | Disables the chip |
149
+
150
+ ### Event Handling
151
+
152
+ | Method | Description |
153
+ |--------|-------------|
154
+ | `on(event, handler)` | Adds an event listener |
155
+ | `off(event, handler)` | Removes an event listener |
156
+
157
+ ### Lifecycle
158
+
159
+ | Method | Description |
160
+ |--------|-------------|
161
+ | `destroy()` | Cleans up the chip and removes it from the DOM |
162
+
163
+ ## Chips Container API Methods
164
+
165
+ ### Chip Management
166
+
167
+ | Method | Description |
168
+ |--------|-------------|
169
+ | `addChip(chipConfig)` | Adds a new chip to the container |
170
+ | `removeChip(chipOrIndex)` | Removes a chip by instance or index |
171
+ | `getChips()` | Gets all chip instances |
172
+ | `scrollToChip(chipOrIndex)` | Scrolls to make a specific chip visible |
173
+
174
+ ### Selection Management
175
+
176
+ | Method | Description |
177
+ |--------|-------------|
178
+ | `getSelectedChips()` | Gets currently selected chip instances |
179
+ | `getSelectedValues()` | Gets values of selected chips |
180
+ | `selectByValue(values, triggerEvent?)` | Selects chips by their values |
181
+ | `clearSelection()` | Clears all selections |
182
+
183
+ ### Layout Management
184
+
185
+ | Method | Description |
186
+ |--------|-------------|
187
+ | `setScrollable(isScrollable)` | Sets the scrollable state |
188
+ | `setVertical(isVertical)` | Sets the vertical layout state |
189
+
190
+ ### Label Management
191
+
192
+ | Method | Description |
193
+ |--------|-------------|
194
+ | `setLabel(text)` | Sets the label text |
195
+ | `getLabel()` | Gets the label text |
196
+ | `setLabelPosition(position)` | Sets the label position ('start' or 'end') |
197
+ | `getLabelPosition()` | Gets the label position |
198
+
199
+ ### Other Methods
200
+
201
+ | Method | Description |
202
+ |--------|-------------|
203
+ | `enableKeyboardNavigation()` | Enables keyboard navigation between chips |
204
+ | `on(event, handler)` | Adds an event listener |
205
+ | `off(event, handler)` | Removes an event listener |
206
+ | `destroy()` | Destroys the chips container and all contained chips |
207
+
208
+ ## Events
209
+
210
+ ### Chip Events
211
+
212
+ | Event | Description |
213
+ |-------|-------------|
214
+ | `change` | Fired when chip selection state changes |
215
+ | `select` | Fired when a chip is selected |
216
+ | `deselect` | Fired when a chip is deselected |
217
+ | `remove` | Fired when a chip is about to be removed |
218
+
219
+ ### Chips Container Events
220
+
221
+ | Event | Description |
222
+ |-------|-------------|
223
+ | `change` | Fired when any chip selection changes |
224
+ | `add` | Fired when a chip is added to the container |
225
+ | `remove` | Fired when a chip is removed from the container |
226
+
227
+ ## Advanced Examples
228
+
229
+ ### Filter Chips with Icons
230
+
231
+ ```typescript
232
+ import { createChips } from 'mtrl';
233
+
234
+ const filterChips = createChips({
235
+ multiSelect: true,
236
+ scrollable: true,
237
+ label: 'Filter by:',
238
+ chips: [
239
+ {
240
+ text: 'Completed',
241
+ variant: 'filter',
242
+ value: 'completed',
243
+ leadingIcon: '<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>'
244
+ },
245
+ {
246
+ text: 'In Progress',
247
+ variant: 'filter',
248
+ value: 'in-progress',
249
+ leadingIcon: '<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/><path fill="currentColor" d="M12 5v7l5 5-1.41 1.41L10.59 13H7V11h3.59l3-3H12z"/></svg>'
250
+ },
251
+ {
252
+ text: 'Pending',
253
+ variant: 'filter',
254
+ value: 'pending',
255
+ leadingIcon: '<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/><path fill="currentColor" d="M7 12h10v2H7z"/></svg>'
256
+ }
257
+ ],
258
+ onChange: (selectedValues) => {
259
+ console.log('Selected filters:', selectedValues);
260
+ updateTaskList(selectedValues);
261
+ }
262
+ });
263
+
264
+ document.querySelector('.filters-container').appendChild(filterChips.element);
265
+
266
+ // Pre-select certain values
267
+ filterChips.selectByValue(['completed', 'in-progress']);
268
+ ```
269
+
270
+ ### Input Chips for Email Tags
271
+
272
+ ```typescript
273
+ import { createChips } from 'mtrl';
274
+
275
+ const emailChips = createChips({
276
+ chips: [],
277
+ label: 'Recipients:',
278
+ onChange: (values) => {
279
+ document.querySelector('#recipients-input').value = values.join(',');
280
+ }
281
+ });
282
+
283
+ // Add to form
284
+ const emailForm = document.querySelector('.email-form');
285
+ emailForm.insertBefore(emailChips.element, emailForm.firstChild);
286
+
287
+ // Handle input for adding emails
288
+ const input = document.querySelector('#email-input');
289
+ input.addEventListener('keydown', (e) => {
290
+ if (e.key === 'Enter' || e.key === ',') {
291
+ e.preventDefault();
292
+
293
+ const email = input.value.trim().replace(',', '');
294
+ if (validateEmail(email)) {
295
+ emailChips.addChip({
296
+ text: email,
297
+ variant: 'input',
298
+ value: email,
299
+ trailingIcon: '<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>',
300
+ onTrailingIconClick: (chip) => {
301
+ emailChips.removeChip(chip);
302
+ }
303
+ });
304
+ input.value = '';
305
+ }
306
+ }
307
+ });
308
+
309
+ function validateEmail(email) {
310
+ return /\S+@\S+\.\S+/.test(email);
311
+ }
312
+ ```
313
+
314
+ ### Theme Selector with Single Selection
315
+
316
+ ```typescript
317
+ import { createChips } from 'mtrl';
318
+
319
+ const themeChips = createChips({
320
+ multiSelect: false, // Single selection mode
321
+ label: 'Select theme:',
322
+ chips: [
323
+ {
324
+ text: 'Light Theme',
325
+ variant: 'filter',
326
+ value: 'light',
327
+ selected: true // Pre-select Light theme
328
+ },
329
+ {
330
+ text: 'Dark Theme',
331
+ variant: 'filter',
332
+ value: 'dark'
333
+ },
334
+ {
335
+ text: 'System Theme',
336
+ variant: 'filter',
337
+ value: 'system'
338
+ }
339
+ ],
340
+ onChange: (selectedValues) => {
341
+ if (selectedValues.length > 0) {
342
+ applyTheme(selectedValues[0]);
343
+ }
344
+ }
345
+ });
346
+
347
+ document.querySelector('.theme-settings').appendChild(themeChips.element);
348
+
349
+ function applyTheme(theme) {
350
+ document.documentElement.setAttribute('data-theme', theme);
351
+ localStorage.setItem('theme', theme);
352
+ }
353
+ ```
354
+
355
+ ### Vertical Layout with Category Groups
356
+
357
+ ```typescript
358
+ import { createChips } from 'mtrl';
359
+
360
+ const categoryChips = createChips({
361
+ vertical: true,
362
+ multiSelect: true,
363
+ label: 'Categories:',
364
+ labelPosition: 'start',
365
+ chips: [
366
+ { text: 'Technology', variant: 'filter', value: 'tech' },
367
+ { text: 'Science', variant: 'filter', value: 'science' },
368
+ { text: 'Health', variant: 'filter', value: 'health' },
369
+ { text: 'Business', variant: 'filter', value: 'business' },
370
+ { text: 'Entertainment', variant: 'filter', value: 'entertainment' },
371
+ { text: 'Sports', variant: 'filter', value: 'sports' },
372
+ { text: 'Politics', variant: 'filter', value: 'politics' },
373
+ { text: 'Travel', variant: 'filter', value: 'travel' }
374
+ ],
375
+ onChange: (selectedValues) => {
376
+ updateArticlesList(selectedValues);
377
+ }
378
+ });
379
+
380
+ document.querySelector('.sidebar-filters').appendChild(categoryChips.element);
381
+ ```
382
+
383
+ ### Dynamic Chip Creation and Removal
384
+
385
+ ```typescript
386
+ import { createChips } from 'mtrl';
387
+
388
+ // Create empty chips container
389
+ const dynamicChips = createChips({
390
+ scrollable: true,
391
+ label: 'Tags:',
392
+ onChange: (selectedValues) => {
393
+ console.log('Selected tags:', selectedValues);
394
+ }
395
+ });
396
+
397
+ document.querySelector('.tags-container').appendChild(dynamicChips.element);
398
+
399
+ // Add new tag button
400
+ document.querySelector('#add-tag').addEventListener('click', () => {
401
+ const tagInput = document.querySelector('#tag-input');
402
+ const tagText = tagInput.value.trim();
403
+
404
+ if (tagText) {
405
+ dynamicChips.addChip({
406
+ text: tagText,
407
+ variant: 'input',
408
+ value: tagText.toLowerCase().replace(/\s+/g, '-'),
409
+ trailingIcon: '<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>',
410
+ onTrailingIconClick: (chip) => {
411
+ if (confirm(`Remove tag "${chip.getText()}"?`)) {
412
+ dynamicChips.removeChip(chip);
413
+ }
414
+ }
415
+ });
416
+
417
+ tagInput.value = '';
418
+ }
419
+ });
420
+ ```
421
+
422
+ ## Accessibility
423
+
424
+ The MTRL Chips component is built with accessibility in mind:
425
+
426
+ - **ARIA Attributes**: Proper `aria-selected`, `aria-disabled`, and `aria-multiselectable` attributes
427
+ - **Keyboard Navigation**: Navigate between chips using arrow keys, select with Space/Enter
428
+ - **Focus Management**: Visible focus indicators and proper focus handling
429
+ - **Semantic Structure**: Appropriate roles and accessible names
430
+ - **Screen Reader Support**: Meaningful announcements for selection changes
431
+
432
+ ## Technical Architecture
433
+
434
+ The MTRL Chips component follows a functional composition pattern:
435
+
436
+ 1. **Base Component**: Core structure with element creation
437
+ 2. **Feature Enhancement**: Functional mixins that add specific capabilities
438
+ 3. **DOM Generation**: Creates the actual DOM elements from the structure
439
+ 4. **Controller**: Manages state, events, and behavior
440
+ 5. **Lifecycle Management**: Handles component lifecycle events
441
+ 6. **Public API**: Exposes a clean, consistent interface
442
+
443
+ This architecture enables:
444
+ - Efficient code reuse and maintainability
445
+ - Clear separation of concerns
446
+ - Extensibility through composition
447
+ - Predictable behavior and clean API surface
448
+
449
+ ## CSS Customization
450
+
451
+ MTRL Chips use BEM-style class naming for easy styling. Primary classes:
452
+
453
+ ```
454
+ .mtrl-chip /* Base chip class */
455
+ .mtrl-chip--{variant} /* Variant classes */
456
+ .mtrl-chip--selected /* Selected state */
457
+ .mtrl-chip--disabled /* Disabled state */
458
+ .mtrl-chip-content /* Content container */
459
+ .mtrl-chip-text /* Text element */
460
+ .mtrl-chip-leading-icon /* Leading icon */
461
+ .mtrl-chip-trailing-icon /* Trailing icon */
462
+
463
+ .mtrl-chips /* Chips container */
464
+ .mtrl-chips--scrollable /* Scrollable state */
465
+ .mtrl-chips--vertical /* Vertical layout */
466
+ .mtrl-chips-container /* Inner container */
467
+ .mtrl-chips-label /* Label element */
468
+ ```
469
+
470
+ ## Browser Support
471
+
472
+ This component supports all modern browsers that implement ES6+ standards:
473
+
474
+ - Chrome (latest)
475
+ - Firefox (latest)
476
+ - Safari (latest)
477
+ - Edge (latest)
478
+
479
+ ## License
480
+
481
+ MTRL is licensed under the MIT License.
@@ -0,0 +1,75 @@
1
+ // src/components/chips/chips.ts
2
+ import { pipe } from '../../core/compose/pipe';
3
+ import { createBase } from '../../core/compose/component';
4
+ import { withEvents, withLifecycle } from '../../core/compose/features';
5
+ import { withLayout, withIcon, withLabel, withDom } from '../../core/composition/features';
6
+ import {
7
+ withContainer,
8
+ withChipItems,
9
+ withController,
10
+ withLabel
11
+ } from './features';
12
+ import { withAPI } from './api';
13
+ import { ChipsConfig, ChipsComponent } from './types';
14
+ import { createBaseConfig, getApiConfig } from './config';
15
+
16
+ /**
17
+ * Creates a chips container for grouping related chips
18
+ *
19
+ * Chips follows a clear architectural pattern:
20
+ * 1. Structure definition - Describes the DOM structure declaratively
21
+ * 2. Feature enhancement - Adds specific capabilities (container, items, etc.)
22
+ * 3. DOM creation - Turns the structure into actual DOM elements
23
+ * 4. Controller - Manages behavior, events, and UI rendering
24
+ * 5. Lifecycle - Handles component lifecycle events
25
+ * 6. Public API - Exposes a clean, consistent API
26
+ *
27
+ * @param {ChipsConfig} config - Chips configuration object
28
+ * @returns {ChipsComponent} Chips component instance
29
+ */
30
+ const createChips = (config: ChipsConfig = {}): ChipsComponent => {
31
+ // Process configuration with defaults
32
+ const baseConfig = createBaseConfig(config);
33
+
34
+ try {
35
+ // Create the component by composing features in a specific order
36
+ const component = pipe(
37
+ // Base component with event system
38
+ createBase,
39
+ withEvents(),
40
+ withLayout(baseConfig),
41
+ withContainer(baseConfig),
42
+ withLabel(baseConfig),
43
+ withChipItems(baseConfig),
44
+
45
+ // Now create the actual DOM elements from the complete structure
46
+ withDom(),
47
+
48
+ // Add state management and behavior
49
+ withController(baseConfig),
50
+ withLifecycle()
51
+ )(baseConfig);
52
+
53
+ // Generate the API configuration based on the enhanced component
54
+ const apiOptions = getApiConfig(component);
55
+
56
+ // Apply the public API layer
57
+ const chips = withAPI(apiOptions)(component);
58
+
59
+ // Register event handlers from config for convenience
60
+ if (baseConfig.on && typeof chips.on === 'function') {
61
+ Object.entries(baseConfig.on).forEach(([event, handler]) => {
62
+ if (typeof handler === 'function') {
63
+ chips.on(event, handler);
64
+ }
65
+ });
66
+ }
67
+
68
+ return chips;
69
+ } catch (error) {
70
+ console.error('Chips creation error:', error);
71
+ throw new Error(`Failed to create chip: ${(error as Error).message}`);
72
+ }
73
+ };
74
+
75
+ export default createChips;