mtrl 0.2.1 → 0.2.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtrl",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "A functional JavaScript component library with composable architecture based on Material Design 3",
5
5
  "keywords": [
6
6
  "component",
@@ -21,6 +21,20 @@ export const CARD_ELEVATIONS = {
21
21
  DRAGGED: CardElevation.DRAGGED
22
22
  };
23
23
 
24
+ // Default width values following MD3 principles
25
+ export const CARD_WIDTHS = {
26
+ // Mobile-optimized default (MD3 recommends 344dp for small screens)
27
+ DEFAULT: '344px',
28
+ // Percentage-based responsive options
29
+ FULL: '100%',
30
+ HALF: '50%',
31
+ // Fixed widths for different breakpoints
32
+ SMALL: '344px',
33
+ MEDIUM: '480px',
34
+ LARGE: '624px'
35
+ }
36
+
37
+
24
38
  /**
25
39
  * Validation schema for card configuration
26
40
  */
@@ -13,25 +13,21 @@ $component: '#{base.$prefix}-chip';
13
13
  display: inline-flex;
14
14
  align-items: center;
15
15
  justify-content: center;
16
- height: 32px;
17
- padding: 0 12px;
18
- border: none;
19
- border-radius: 8px;
16
+ height: v.chip('height');
17
+ padding: 0 v.chip('padding-horizontal');
18
+ border-radius: v.chip('border-radius');
20
19
  background-color: transparent;
21
- color: inherit;
22
- font: inherit;
23
- text-decoration: none;
24
- cursor: pointer;
25
- user-select: none;
26
- vertical-align: middle;
27
- appearance: none;
20
+ max-width: 100%;
28
21
  overflow: hidden;
29
- transition: background-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease,
30
- border-color 0.15s ease, opacity 0.15s ease;
22
+ user-select: none;
23
+ cursor: pointer;
31
24
 
32
25
  // Typography
33
26
  @include m.typography('label-large');
34
27
 
28
+ // Interaction styles
29
+ @include m.motion-transition(background-color, color, border-color, box-shadow);
30
+
35
31
  // Focus styles
36
32
  &:focus {
37
33
  outline: none;
@@ -43,50 +39,57 @@ $component: '#{base.$prefix}-chip';
43
39
  }
44
40
 
45
41
  // Disabled state
46
- &[aria-disabled="true"] {
47
- pointer-events: none;
42
+ &--disabled {
48
43
  opacity: 0.38;
44
+ pointer-events: none;
49
45
  }
50
46
 
51
- // Leading icon styles
52
- &-icon {
53
- display: inline-flex;
47
+ // Content container
48
+ &-content {
49
+ display: flex;
54
50
  align-items: center;
55
51
  justify-content: center;
56
- width: 18px;
57
- height: 18px;
58
- margin-right: 8px;
59
-
60
- svg {
61
- width: 18px;
62
- height: 18px;
63
- }
52
+ width: 100%;
53
+ height: 100%;
64
54
  }
65
55
 
66
- // Trailing icon styles
56
+ // Ensure proper layout with icons
57
+ &-leading-icon,
67
58
  &-trailing-icon {
68
59
  display: inline-flex;
69
60
  align-items: center;
70
61
  justify-content: center;
71
62
  width: 18px;
72
63
  height: 18px;
73
- margin-left: 8px;
74
64
 
75
65
  svg {
76
66
  width: 18px;
77
67
  height: 18px;
78
68
  }
69
+ }
70
+
71
+ &-leading-icon {
72
+ margin-right: 8px;
73
+ }
74
+
75
+ &-trailing-icon {
76
+ margin-left: 8px;
79
77
 
80
78
  &:hover {
81
- opacity: 0.8;
79
+ opacity: 0.7;
82
80
  }
83
81
  }
84
82
 
85
- // Text content
86
83
  &-text {
87
84
  // Text truncation for long chip labels
88
85
  @include m.truncate;
89
- max-width: 150px;
86
+ }
87
+
88
+ // Selected state
89
+ &--selected {
90
+ // Default selected state styling (can be overridden by variants)
91
+ background-color: t.alpha('on-surface', 0.12);
92
+ font-weight: 500;
90
93
  }
91
94
 
92
95
  // Ripple container
@@ -99,9 +102,7 @@ $component: '#{base.$prefix}-chip';
99
102
  opacity: 0.12;
100
103
  }
101
104
 
102
- // === VARIANTS ===
103
-
104
- // Filled chip (default)
105
+ // Variants
105
106
  &--filled {
106
107
  background-color: t.color('surface-container-highest');
107
108
  color: t.color('on-surface');
@@ -128,7 +129,6 @@ $component: '#{base.$prefix}-chip';
128
129
  }
129
130
  }
130
131
 
131
- // Outlined chip
132
132
  &--outlined {
133
133
  border: 1px solid t.color('outline');
134
134
  color: t.color('on-surface');
@@ -142,21 +142,16 @@ $component: '#{base.$prefix}-chip';
142
142
  }
143
143
 
144
144
  &.#{$component}--selected {
145
- border-color: t.color('secondary');
145
+ border-color: t.color('outline');
146
146
  background-color: t.color('secondary-container');
147
147
  color: t.color('on-secondary-container');
148
148
 
149
149
  &:hover {
150
150
  @include m.state-layer(t.color('on-secondary-container'), 'hover');
151
151
  }
152
-
153
- &:active {
154
- @include m.state-layer(t.color('on-secondary-container'), 'pressed');
155
- }
156
152
  }
157
153
  }
158
154
 
159
- // Elevated chip
160
155
  &--elevated {
161
156
  background-color: t.color('surface-container-low');
162
157
  color: t.color('on-surface');
@@ -179,109 +174,79 @@ $component: '#{base.$prefix}-chip';
179
174
  &:hover {
180
175
  @include m.state-layer(t.color('on-secondary-container'), 'hover');
181
176
  }
182
-
183
- &:active {
184
- @include m.state-layer(t.color('on-secondary-container'), 'pressed');
185
- }
186
- }
187
-
188
- &[aria-disabled="true"] {
189
- @include m.elevation(0);
190
- box-shadow: none;
191
177
  }
192
178
  }
193
179
 
194
- // Assist chip
195
- &--assist {
196
- background-color: t.color('surface-container');
180
+ // Filter chip specific styling
181
+ &--filter {
182
+ background-color: t.color('surface-container-highest');
197
183
  color: t.color('on-surface');
198
184
 
199
185
  &:hover {
200
186
  @include m.state-layer(t.color('on-surface'), 'hover');
201
187
  }
202
188
 
203
- &:active {
204
- @include m.state-layer(t.color('on-surface'), 'pressed');
205
- }
206
-
207
- .#{$component}-icon {
208
- color: t.color('primary');
189
+ &.#{$component}--selected {
190
+ background-color: t.color('secondary-container');
191
+ color: t.color('on-secondary-container');
192
+
193
+ .#{$component}-leading-icon {
194
+ color: t.color('on-secondary-container');
195
+ }
209
196
  }
210
197
  }
211
198
 
212
- // Filter chip
213
- &--filter {
214
- background-color: t.color('surface-container-highest');
199
+ // Assist chip specific styling
200
+ &--assist {
201
+ background-color: t.color('surface-container-low');
215
202
  color: t.color('on-surface');
216
203
 
217
- &:hover {
218
- @include m.state-layer(t.color('on-surface'), 'hover');
219
- }
220
-
221
- &:active {
222
- @include m.state-layer(t.color('on-surface'), 'pressed');
204
+ .#{$component}-leading-icon {
205
+ color: t.color('primary');
223
206
  }
224
207
 
225
208
  &.#{$component}--selected {
226
209
  background-color: t.color('secondary-container');
227
210
  color: t.color('on-secondary-container');
228
211
 
229
- &:hover {
230
- @include m.state-layer(t.color('on-secondary-container'), 'hover');
231
- }
232
-
233
- &:active {
234
- @include m.state-layer(t.color('on-secondary-container'), 'pressed');
235
- }
236
-
237
- // Checkmark icon for selected filter chips
238
- .#{$component}-icon {
212
+ .#{$component}-leading-icon {
239
213
  color: t.color('on-secondary-container');
240
214
  }
241
215
  }
242
216
  }
243
217
 
244
- // Input chip
218
+ // Input chip specific styling
245
219
  &--input {
246
220
  background-color: t.color('surface-container-highest');
247
221
  color: t.color('on-surface');
248
- padding-right: 8px; // Less padding on the right to accommodate the trailing icon
249
-
250
- &:hover {
251
- @include m.state-layer(t.color('on-surface'), 'hover');
252
- }
253
222
 
254
223
  .#{$component}-trailing-icon {
255
- color: t.color('on-surface-variant');
224
+ cursor: pointer;
256
225
 
257
226
  &:hover {
258
- color: t.color('on-surface');
227
+ color: t.color('error');
259
228
  }
260
229
  }
261
230
  }
262
231
 
263
- // Suggestion chip
232
+ // Suggestion chip styling
264
233
  &--suggestion {
265
234
  background-color: t.color('surface-container');
266
235
  color: t.color('on-surface');
267
236
 
268
- &:hover {
269
- @include m.state-layer(t.color('on-surface'), 'hover');
270
- }
271
-
272
- &:active {
273
- @include m.state-layer(t.color('on-surface'), 'pressed');
237
+ &.#{$component}--selected {
238
+ background-color: t.color('secondary-container');
239
+ color: t.color('on-secondary-container');
274
240
  }
275
241
  }
276
242
 
277
- // === SIZES ===
278
-
243
+ // Size variants
279
244
  &--small {
280
245
  height: 24px;
281
246
  padding: 0 8px;
282
247
  font-size: 12px;
283
248
 
284
- .#{$component}-icon,
249
+ .#{$component}-leading-icon,
285
250
  .#{$component}-trailing-icon {
286
251
  width: 16px;
287
252
  height: 16px;
@@ -291,23 +256,18 @@ $component: '#{base.$prefix}-chip';
291
256
  height: 16px;
292
257
  }
293
258
  }
294
-
295
- .#{$component}-icon {
296
- margin-right: 4px;
297
- }
298
-
299
- .#{$component}-trailing-icon {
300
- margin-left: 4px;
301
- }
259
+ }
260
+
261
+ &--medium {
262
+ // Default size, styles already defined
302
263
  }
303
264
 
304
265
  &--large {
305
266
  height: 40px;
306
267
  padding: 0 16px;
307
- font-size: 15px;
308
- border-radius: 12px;
268
+ font-size: 16px;
309
269
 
310
- .#{$component}-icon,
270
+ .#{$component}-leading-icon,
311
271
  .#{$component}-trailing-icon {
312
272
  width: 20px;
313
273
  height: 20px;
@@ -317,40 +277,11 @@ $component: '#{base.$prefix}-chip';
317
277
  height: 20px;
318
278
  }
319
279
  }
320
-
321
- .#{$component}-icon {
322
- margin-right: 10px;
323
- }
324
-
325
- .#{$component}-trailing-icon {
326
- margin-left: 10px;
327
- }
328
- }
329
-
330
- // === SPECIAL CASES ===
331
-
332
- // For chips with only icons (no text)
333
- &--icon-only {
334
- padding: 0;
335
- width: 32px;
336
-
337
- &.#{$component}--small {
338
- width: 24px;
339
- }
340
-
341
- &.#{$component}--large {
342
- width: 40px;
343
- }
344
-
345
- .#{$component}-icon {
346
- margin-right: 0;
347
- }
348
280
  }
349
281
  }
350
282
 
351
- // === CHIP SET STYLES ===
352
-
353
- .#{$component}-set {
283
+ // Chip set container
284
+ .#{base.$prefix}-chip-set {
354
285
  display: flex;
355
286
  flex-wrap: wrap;
356
287
  gap: 8px;
@@ -358,11 +289,23 @@ $component: '#{base.$prefix}-chip';
358
289
  &--scrollable {
359
290
  flex-wrap: nowrap;
360
291
  overflow-x: auto;
361
- scrollbar-width: none; // Firefox
292
+ padding-bottom: 8px;
293
+ margin-bottom: -8px; // Compensate for padding to maintain vertical alignment
294
+ -webkit-overflow-scrolling: touch; // Smooth scrolling on iOS
362
295
 
296
+ // Hide scrollbar in various browsers while maintaining functionality
363
297
  &::-webkit-scrollbar {
364
- display: none; // Chrome, Safari, Edge
298
+ height: 4px;
365
299
  }
300
+
301
+ &::-webkit-scrollbar-thumb {
302
+ background-color: t.alpha('on-surface', 0.2);
303
+ border-radius: 4px;
304
+ }
305
+
306
+ // Style for Firefox
307
+ scrollbar-width: thin;
308
+ scrollbar-color: t.alpha('on-surface', 0.2) transparent;
366
309
  }
367
310
 
368
311
  &--vertical {
@@ -8,108 +8,237 @@
8
8
  * @returns {Function} Higher-order function that adds API methods to component
9
9
  * @internal This is an internal utility for the Chip component
10
10
  */
11
- export const withAPI = ({ disabled, lifecycle }) => (component) => ({
12
- ...component,
13
- element: component.element,
14
-
15
- /**
16
- * Gets the chip's value
17
- * @returns {string} The chip's value attribute
18
- */
19
- getValue: () => component.element.getAttribute('data-value'),
20
-
21
- /**
22
- * Sets the chip's value
23
- * @param {string} value - Value to set
24
- * @returns {Object} The chip instance for chaining
25
- */
26
- setValue (value) {
27
- component.element.setAttribute('data-value', value)
28
- return this
29
- },
30
-
31
- /**
32
- * Enables the chip
33
- * @returns {Object} The chip instance for chaining
34
- */
35
- enable () {
36
- disabled.enable()
37
- component.element.setAttribute('aria-disabled', 'false')
38
- return this
39
- },
40
-
41
- /**
42
- * Disables the chip
43
- * @returns {Object} The chip instance for chaining
44
- */
45
- disable () {
46
- disabled.disable()
47
- component.element.setAttribute('aria-disabled', 'true')
48
- return this
49
- },
50
-
51
- /**
52
- * Sets the chip's text content
53
- * @param {string} content - Text content
54
- * @returns {Object} The chip instance for chaining
55
- */
56
- setText (content) {
57
- component.text.setText(content)
58
- return this
59
- },
60
-
61
- /**
62
- * Gets the chip's text content
63
- * @returns {string} The chip's text content
64
- */
65
- getText () {
66
- return component.text.getText()
67
- },
68
-
69
- /**
70
- * Sets the chip's leading icon
71
- * @param {string} icon - Icon HTML content
72
- * @returns {Object} The chip instance for chaining
73
- */
74
- setIcon (icon) {
75
- component.icon.setIcon(icon)
76
- return this
77
- },
78
-
79
- /**
80
- * Gets the chip's icon content
81
- * @returns {string} The chip's icon HTML
82
- */
83
- getIcon () {
84
- return component.icon.getIcon()
85
- },
86
-
87
- /**
88
- * Sets the chip's trailing icon
89
- * @param {string} icon - Icon HTML content
90
- * @returns {Object} The chip instance for chaining
91
- */
92
- setTrailingIcon (icon) {
93
- const trailingIconSelector = `.${component.getClass('chip')}-trailing-icon`
94
- let trailingIconElement = component.element.querySelector(trailingIconSelector)
95
-
96
- if (!trailingIconElement && icon) {
97
- trailingIconElement = document.createElement('span')
98
- trailingIconElement.className = `${component.getClass('chip')}-trailing-icon`
99
- component.element.appendChild(trailingIconElement)
100
- }
11
+ export const withAPI = ({ disabled, lifecycle }) => (component) => {
12
+ // Track selected state internally
13
+ let isSelected = component.element.classList.contains(`${component.getClass('chip')}--selected`);
14
+
15
+ return {
16
+ ...component,
17
+ element: component.element,
101
18
 
102
- if (trailingIconElement) {
103
- trailingIconElement.innerHTML = icon || ''
104
- }
19
+ /**
20
+ * Gets the chip's value
21
+ * @returns {string} The chip's value attribute
22
+ */
23
+ getValue() {
24
+ return component.element.getAttribute('data-value') || '';
25
+ },
26
+
27
+ /**
28
+ * Sets the chip's value
29
+ * @param {string} value - Value to set
30
+ * @returns {Object} The chip instance for chaining
31
+ */
32
+ setValue(value) {
33
+ component.element.setAttribute('data-value', value || '');
34
+ return this;
35
+ },
36
+
37
+ /**
38
+ * Checks if the chip is disabled
39
+ * @returns {boolean} True if the chip is disabled
40
+ */
41
+ isDisabled() {
42
+ return component.element.getAttribute('aria-disabled') === 'true';
43
+ },
44
+
45
+ /**
46
+ * Enables the chip
47
+ * @returns {Object} The chip instance for chaining
48
+ */
49
+ enable() {
50
+ disabled.enable();
51
+ component.element.classList.remove(`${component.getClass('chip')}--disabled`);
52
+ component.element.setAttribute('aria-disabled', 'false');
53
+ component.element.setAttribute('tabindex', '0');
54
+ return this;
55
+ },
56
+
57
+ /**
58
+ * Disables the chip
59
+ * @returns {Object} The chip instance for chaining
60
+ */
61
+ disable() {
62
+ disabled.disable();
63
+ component.element.classList.add(`${component.getClass('chip')}--disabled`);
64
+ component.element.setAttribute('aria-disabled', 'true');
65
+ component.element.setAttribute('tabindex', '-1');
66
+ return this;
67
+ },
68
+
69
+ /**
70
+ * Sets the chip's text content
71
+ * @param {string} content - Text content
72
+ * @returns {Object} The chip instance for chaining
73
+ */
74
+ setText(content) {
75
+ const containerSelector = `.${component.getClass('chip')}-content`;
76
+ const contentContainer = component.element.querySelector(containerSelector) || component.element;
77
+
78
+ const textSelector = `.${component.getClass('chip')}-text`;
79
+ let textElement = component.element.querySelector(textSelector);
80
+
81
+ if (!textElement && content) {
82
+ textElement = document.createElement('span');
83
+ textElement.className = `${component.getClass('chip')}-text`;
84
+
85
+ // Find the right position to insert (after leading icon if present, or as first child)
86
+ const leadingIcon = component.element.querySelector(`.${component.getClass('chip')}-leading-icon`);
87
+ if (leadingIcon) {
88
+ contentContainer.insertBefore(textElement, leadingIcon.nextSibling);
89
+ } else {
90
+ contentContainer.insertBefore(textElement, contentContainer.firstChild);
91
+ }
92
+ }
93
+
94
+ if (textElement) {
95
+ textElement.textContent = content || '';
96
+
97
+ // Remove the element if content is empty
98
+ if (!content && textElement.parentNode) {
99
+ textElement.parentNode.removeChild(textElement);
100
+ }
101
+ }
102
+
103
+ return this;
104
+ },
105
+
106
+ /**
107
+ * Gets the chip's text content
108
+ * @returns {string} The chip's text content
109
+ */
110
+ getText() {
111
+ const textElement = component.element.querySelector(`.${component.getClass('chip')}-text`);
112
+ return textElement ? textElement.textContent : '';
113
+ },
114
+
115
+ /**
116
+ * Sets the chip's leading icon (alias for setLeadingIcon)
117
+ * @param {string} icon - Icon HTML content
118
+ * @returns {Object} The chip instance for chaining
119
+ */
120
+ setIcon(icon) {
121
+ return this.setLeadingIcon(icon);
122
+ },
105
123
 
106
- return this
107
- },
124
+ /**
125
+ * Gets the chip's icon content
126
+ * @returns {string} The chip's icon HTML
127
+ */
128
+ getIcon() {
129
+ const iconElement = component.element.querySelector(`.${component.getClass('chip')}-leading-icon`);
130
+ return iconElement ? iconElement.innerHTML : '';
131
+ },
108
132
 
109
- /**
110
- * Destroys the chip component and cleans up resources
111
- */
112
- destroy () {
113
- lifecycle.destroy()
114
- }
115
- })
133
+ /**
134
+ * Sets the chip's leading icon
135
+ * @param {string} icon - Icon HTML content
136
+ * @returns {Object} The chip instance for chaining
137
+ */
138
+ setLeadingIcon(icon) {
139
+ const contentContainer = component.element.querySelector(`.${component.getClass('chip')}-content`) || component.element;
140
+ const leadingIconSelector = `.${component.getClass('chip')}-leading-icon`;
141
+ let leadingIconElement = component.element.querySelector(leadingIconSelector);
142
+
143
+ if (!leadingIconElement && icon) {
144
+ leadingIconElement = document.createElement('span');
145
+ leadingIconElement.className = `${component.getClass('chip')}-leading-icon`;
146
+
147
+ // Insert as first child of the content container
148
+ contentContainer.insertBefore(leadingIconElement, contentContainer.firstChild);
149
+ }
150
+
151
+ if (leadingIconElement) {
152
+ leadingIconElement.innerHTML = icon || '';
153
+
154
+ // Remove the element if icon is empty
155
+ if (!icon && leadingIconElement.parentNode) {
156
+ leadingIconElement.parentNode.removeChild(leadingIconElement);
157
+ }
158
+ }
159
+
160
+ return this;
161
+ },
162
+
163
+ /**
164
+ * Sets the chip's trailing icon
165
+ * @param {string} icon - Icon HTML content
166
+ * @param {Function} [onClick] - Click handler for the trailing icon
167
+ * @returns {Object} The chip instance for chaining
168
+ */
169
+ setTrailingIcon(icon, onClick) {
170
+ const contentContainer = component.element.querySelector(`.${component.getClass('chip')}-content`) || component.element;
171
+ const trailingIconSelector = `.${component.getClass('chip')}-trailing-icon`;
172
+ let trailingIconElement = component.element.querySelector(trailingIconSelector);
173
+
174
+ if (!trailingIconElement && icon) {
175
+ trailingIconElement = document.createElement('span');
176
+ trailingIconElement.className = `${component.getClass('chip')}-trailing-icon`;
177
+
178
+ // Add as last child of content container
179
+ contentContainer.appendChild(trailingIconElement);
180
+
181
+ // Add click handler if provided
182
+ if (onClick) {
183
+ trailingIconElement.addEventListener('click', (e) => {
184
+ e.stopPropagation(); // Prevent chip click event
185
+ onClick(this);
186
+ });
187
+ }
188
+ }
189
+
190
+ if (trailingIconElement) {
191
+ trailingIconElement.innerHTML = icon || '';
192
+
193
+ // Remove the element if icon is empty
194
+ if (!icon && trailingIconElement.parentNode) {
195
+ trailingIconElement.parentNode.removeChild(trailingIconElement);
196
+ }
197
+ }
198
+
199
+ return this;
200
+ },
201
+
202
+ /**
203
+ * Checks if the chip is selected
204
+ * @returns {boolean} True if the chip is selected
205
+ */
206
+ isSelected() {
207
+ return isSelected;
208
+ },
209
+
210
+ /**
211
+ * Sets the chip's selected state
212
+ * @param {boolean} selected - Whether the chip should be selected
213
+ * @returns {Object} The chip instance for chaining
214
+ */
215
+ setSelected(selected) {
216
+ isSelected = !!selected;
217
+
218
+ if (selected) {
219
+ component.element.classList.add(`${component.getClass('chip')}--selected`);
220
+ component.element.setAttribute('aria-selected', 'true');
221
+ } else {
222
+ component.element.classList.remove(`${component.getClass('chip')}--selected`);
223
+ component.element.setAttribute('aria-selected', 'false');
224
+ }
225
+
226
+ return this;
227
+ },
228
+
229
+ /**
230
+ * Toggles the chip's selected state
231
+ * @returns {Object} The chip instance for chaining
232
+ */
233
+ toggleSelected() {
234
+ return this.setSelected(!isSelected);
235
+ },
236
+
237
+ /**
238
+ * Destroys the chip component and cleans up resources
239
+ */
240
+ destroy() {
241
+ lifecycle.destroy();
242
+ }
243
+ };
244
+ };
@@ -1,6 +1,7 @@
1
- // src/components/chip/chip.ts
2
- import { pipe } from '../../core/compose';
3
- import { createBase, withElement } from '../../core/compose/component';
1
+ // src/components/chip/chip.js
2
+ import { PREFIX } from '../../core/config'
3
+ import { pipe } from '../../core/compose'
4
+ import { createBase, withElement } from '../../core/compose/component'
4
5
  import {
5
6
  withEvents,
6
7
  withText,
@@ -10,72 +11,383 @@ import {
10
11
  withRipple,
11
12
  withDisabled,
12
13
  withLifecycle
13
- } from '../../core/compose/features';
14
- import { withAPI } from './api';
15
- import { ChipConfig, ChipComponent, BaseComponent } from './types';
16
- import { createBaseConfig, getElementConfig, getApiConfig } from './config';
14
+ } from '../../core/compose/features'
15
+ import { withAPI } from './api'
16
+ import { CHIP_VARIANTS, CHIP_SIZES } from './constants'
17
17
 
18
18
  /**
19
19
  * Creates a new Chip component
20
- * @param {ChipConfig} config - Chip configuration object
21
- * @returns {ChipComponent} Chip component instance
20
+ * @param {Object} config - Chip configuration
21
+ * @param {string} [config.variant='filled'] - Chip variant
22
+ * @param {string} [config.size='medium'] - Chip size
23
+ * @param {boolean} [config.selected=false] - Whether the chip is initially selected
24
+ * @param {boolean} [config.disabled=false] - Whether the chip is initially disabled
25
+ * @param {string} [config.text] - Chip text content
26
+ * @param {string} [config.leadingIcon] - Leading icon HTML content
27
+ * @param {string} [config.trailingIcon] - Trailing icon HTML content
28
+ * @param {string} [config.class] - Additional CSS classes
29
+ * @param {string} [config.value] - Chip value
30
+ * @param {boolean} [config.ripple=true] - Whether to enable ripple effect
31
+ * @param {Function} [config.onTrailingIconClick] - Callback when trailing icon is clicked
32
+ * @param {Function} [config.onSelect] - Callback when chip is selected
33
+ * @param {Function} [config.onChange] - Callback when chip selection changes
34
+ * @returns {Object} Chip component instance
22
35
  */
23
- const createChip = (config: ChipConfig = {}): ChipComponent => {
24
- const baseConfig = createBaseConfig(config);
36
+ const createChip = (config = {}) => {
37
+ const baseConfig = {
38
+ ...config,
39
+ variant: config.variant || CHIP_VARIANTS.FILLED,
40
+ size: config.size || CHIP_SIZES.MEDIUM,
41
+ componentName: 'chip',
42
+ prefix: PREFIX,
43
+ ripple: config.ripple !== false
44
+ }
25
45
 
26
46
  try {
47
+ // Create base component with core features
27
48
  const chip = pipe(
28
49
  createBase,
29
50
  withEvents(),
30
- withElement(getElementConfig(baseConfig)),
31
- withVariant(baseConfig),
32
- withSize(baseConfig),
33
- withText(baseConfig),
34
- withIcon({
35
- ...baseConfig,
36
- position: 'start',
37
- iconContent: config.leadingIcon || config.icon
51
+ withElement({
52
+ tag: 'div',
53
+ componentName: 'chip',
54
+ attrs: {
55
+ role: 'button',
56
+ tabindex: '0',
57
+ 'aria-disabled': config.disabled ? 'true' : 'false',
58
+ 'aria-selected': config.selected ? 'true' : 'false',
59
+ 'data-value': config.value || ''
60
+ },
61
+ className: config.class,
62
+ forwardEvents: {
63
+ click: (component) => component.element.getAttribute('aria-disabled') !== 'true',
64
+ focus: true,
65
+ blur: true
66
+ }
38
67
  }),
39
- withDisabled(baseConfig),
40
- withRipple(baseConfig),
41
- withLifecycle(),
42
- comp => withAPI(getApiConfig(comp))(comp)
43
- )(baseConfig);
68
+ withLifecycle()
69
+ )(baseConfig)
70
+
71
+ // Track selected state
72
+ let isSelectedState = !!config.selected;
73
+
74
+ // Manually add the variant class
75
+ if (config.variant) {
76
+ chip.element.classList.add(`${chip.getClass('chip')}--${config.variant}`)
77
+ }
78
+
79
+ // Manually add the size class
80
+ if (config.size) {
81
+ chip.element.classList.add(`${chip.getClass('chip')}--${config.size}`)
82
+ }
83
+
84
+ // Add ripple if enabled
85
+ if (config.ripple) {
86
+ withRipple(baseConfig)(chip)
87
+ }
88
+
89
+ // Add disabled state if needed
90
+ if (config.disabled) {
91
+ withDisabled(baseConfig)(chip)
92
+ }
93
+
94
+ // Add selected class if needed
95
+ if (config.selected) {
96
+ chip.element.classList.add(`${chip.getClass('chip')}--selected`)
97
+ }
98
+
99
+ // Create a container for the chip content to ensure proper ordering
100
+ const contentContainer = document.createElement('div')
101
+ contentContainer.className = `${chip.getClass('chip')}-content`
102
+ contentContainer.style.display = 'flex'
103
+ contentContainer.style.alignItems = 'center'
104
+ contentContainer.style.justifyContent = 'center'
105
+ contentContainer.style.width = '100%'
106
+ chip.element.appendChild(contentContainer)
107
+
108
+ // Add leading icon if provided
109
+ if (config.leadingIcon) {
110
+ const leadingIconElement = document.createElement('span')
111
+ leadingIconElement.className = `${chip.getClass('chip')}-leading-icon`
112
+ leadingIconElement.innerHTML = config.leadingIcon
113
+ contentContainer.appendChild(leadingIconElement)
114
+ }
115
+
116
+ // Add text element if provided
117
+ if (config.text) {
118
+ const textElement = document.createElement('span')
119
+ textElement.className = `${chip.getClass('chip')}-text`
120
+ textElement.textContent = config.text
121
+ contentContainer.appendChild(textElement)
122
+ }
44
123
 
45
124
  // Add trailing icon if provided
46
125
  if (config.trailingIcon) {
47
- const trailingIconElement = document.createElement('span');
48
- trailingIconElement.className = `${baseConfig.prefix}-chip-trailing-icon`;
49
- trailingIconElement.innerHTML = config.trailingIcon;
50
- chip.element.appendChild(trailingIconElement);
51
-
52
- // Add event listener for remove/close action if needed
126
+ const trailingIconElement = document.createElement('span')
127
+ trailingIconElement.className = `${chip.getClass('chip')}-trailing-icon`
128
+ trailingIconElement.innerHTML = config.trailingIcon
129
+
130
+ // Add click handler for trailing icon
53
131
  if (config.onTrailingIconClick) {
54
132
  trailingIconElement.addEventListener('click', (e) => {
55
- e.stopPropagation();
56
- config.onTrailingIconClick!(chip as ChipComponent);
57
- });
133
+ e.stopPropagation() // Prevent chip click event
134
+ config.onTrailingIconClick(enhancedChip)
135
+ })
58
136
  }
137
+
138
+ contentContainer.appendChild(trailingIconElement)
59
139
  }
60
140
 
61
- // Initialize selected state if needed
62
- if (config.selected) {
63
- (chip as ChipComponent).setSelected(true);
64
- }
141
+ // Create enhanced component with API
142
+ const enhancedChip = {
143
+ ...chip,
144
+
145
+ /**
146
+ * Checks if the chip is disabled
147
+ * @returns {boolean} True if the chip is disabled
148
+ */
149
+ isDisabled() {
150
+ return chip.element.getAttribute('aria-disabled') === 'true';
151
+ },
152
+
153
+ /**
154
+ * Checks if the chip is selected
155
+ * @returns {boolean} True if the chip is selected
156
+ */
157
+ isSelected() {
158
+ return isSelectedState;
159
+ },
160
+
161
+ /**
162
+ * Sets the chip's selected state
163
+ * @param {boolean} selected - Whether the chip should be selected
164
+ * @returns {Object} The chip instance for chaining
165
+ */
166
+ setSelected(selected) {
167
+ isSelectedState = !!selected;
168
+
169
+ if (selected) {
170
+ chip.element.classList.add(`${chip.getClass('chip')}--selected`);
171
+ chip.element.setAttribute('aria-selected', 'true');
172
+ } else {
173
+ chip.element.classList.remove(`${chip.getClass('chip')}--selected`);
174
+ chip.element.setAttribute('aria-selected', 'false');
175
+ }
176
+
177
+ return this;
178
+ },
179
+
180
+ /**
181
+ * Toggles the chip's selected state
182
+ * @returns {Object} The chip instance for chaining
183
+ */
184
+ toggleSelected() {
185
+ return this.setSelected(!isSelectedState);
186
+ },
187
+
188
+ /**
189
+ * Gets the chip's value
190
+ * @returns {string} The chip's value
191
+ */
192
+ getValue() {
193
+ return chip.element.getAttribute('data-value');
194
+ },
195
+
196
+ /**
197
+ * Sets the chip's value
198
+ * @param {string} value - Value to set
199
+ * @returns {Object} The chip instance for chaining
200
+ */
201
+ setValue(value) {
202
+ chip.element.setAttribute('data-value', value);
203
+ return this;
204
+ },
205
+
206
+ /**
207
+ * Enables the chip
208
+ * @returns {Object} The chip instance for chaining
209
+ */
210
+ enable() {
211
+ chip.element.classList.remove(`${chip.getClass('chip')}--disabled`);
212
+ chip.element.setAttribute('aria-disabled', 'false');
213
+ chip.element.setAttribute('tabindex', '0');
214
+ return this;
215
+ },
216
+
217
+ /**
218
+ * Disables the chip
219
+ * @returns {Object} The chip instance for chaining
220
+ */
221
+ disable() {
222
+ chip.element.classList.add(`${chip.getClass('chip')}--disabled`);
223
+ chip.element.setAttribute('aria-disabled', 'true');
224
+ chip.element.setAttribute('tabindex', '-1');
225
+ return this;
226
+ },
227
+
228
+ /**
229
+ * Sets the chip's text content
230
+ * @param {string} content - Text content
231
+ * @returns {Object} The chip instance for chaining
232
+ */
233
+ setText(content) {
234
+ const textElement = chip.element.querySelector(`.${chip.getClass('chip')}-text`);
235
+
236
+ if (textElement) {
237
+ textElement.textContent = content;
238
+ } else if (content) {
239
+ const newTextElement = document.createElement('span');
240
+ newTextElement.className = `${chip.getClass('chip')}-text`;
241
+ newTextElement.textContent = content;
242
+ contentContainer.appendChild(newTextElement);
243
+ }
244
+
245
+ return this;
246
+ },
247
+
248
+ /**
249
+ * Gets the chip's text content
250
+ * @returns {string} The chip's text content
251
+ */
252
+ getText() {
253
+ const textElement = chip.element.querySelector(`.${chip.getClass('chip')}-text`);
254
+ return textElement ? textElement.textContent : '';
255
+ },
256
+
257
+ /**
258
+ * Sets the chip's icon
259
+ * @param {string} icon - Icon HTML content
260
+ * @returns {Object} The chip instance for chaining
261
+ */
262
+ setIcon(icon) {
263
+ return this.setLeadingIcon(icon);
264
+ },
265
+
266
+ /**
267
+ * Gets the chip's icon
268
+ * @returns {string} The chip's icon HTML
269
+ */
270
+ getIcon() {
271
+ const iconElement = chip.element.querySelector(`.${chip.getClass('chip')}-leading-icon`);
272
+ return iconElement ? iconElement.innerHTML : '';
273
+ },
274
+
275
+ /**
276
+ * Sets the chip's leading icon
277
+ * @param {string} icon - Icon HTML content
278
+ * @returns {Object} The chip instance for chaining
279
+ */
280
+ setLeadingIcon(icon) {
281
+ const leadingIconSelector = `.${chip.getClass('chip')}-leading-icon`;
282
+ let leadingIconElement = chip.element.querySelector(leadingIconSelector);
283
+
284
+ if (!leadingIconElement && icon) {
285
+ leadingIconElement = document.createElement('span');
286
+ leadingIconElement.className = `${chip.getClass('chip')}-leading-icon`;
287
+
288
+ // Insert at the beginning of the content container
289
+ contentContainer.insertBefore(leadingIconElement, contentContainer.firstChild);
290
+ }
291
+
292
+ if (leadingIconElement) {
293
+ leadingIconElement.innerHTML = icon || '';
294
+
295
+ // Remove the element if icon is empty
296
+ if (!icon && leadingIconElement.parentNode) {
297
+ leadingIconElement.parentNode.removeChild(leadingIconElement);
298
+ }
299
+ }
300
+
301
+ return this;
302
+ },
303
+
304
+ /**
305
+ * Sets the chip's trailing icon
306
+ * @param {string} icon - Icon HTML content
307
+ * @param {Function} [onClick] - Click handler for the trailing icon
308
+ * @returns {Object} The chip instance for chaining
309
+ */
310
+ setTrailingIcon(icon, onClick) {
311
+ const trailingIconSelector = `.${chip.getClass('chip')}-trailing-icon`;
312
+ let trailingIconElement = chip.element.querySelector(trailingIconSelector);
313
+
314
+ if (!trailingIconElement && icon) {
315
+ trailingIconElement = document.createElement('span');
316
+ trailingIconElement.className = `${chip.getClass('chip')}-trailing-icon`;
317
+
318
+ // Add at the end of the content container
319
+ contentContainer.appendChild(trailingIconElement);
320
+
321
+ // Add click handler if provided
322
+ if (onClick) {
323
+ trailingIconElement.addEventListener('click', (e) => {
324
+ e.stopPropagation(); // Prevent chip click event
325
+ onClick(this);
326
+ });
327
+ }
328
+ }
329
+
330
+ if (trailingIconElement) {
331
+ trailingIconElement.innerHTML = icon || '';
332
+
333
+ // Remove the element if icon is empty
334
+ if (!icon && trailingIconElement.parentNode) {
335
+ trailingIconElement.parentNode.removeChild(trailingIconElement);
336
+ }
337
+ }
338
+
339
+ return this;
340
+ },
341
+
342
+ /**
343
+ * Destroys the chip component and cleans up resources
344
+ */
345
+ destroy() {
346
+ chip.lifecycle && chip.lifecycle.destroy && chip.lifecycle.destroy();
347
+ chip.element.remove();
348
+ },
349
+
350
+ // Forward event methods from the original chip
351
+ on: chip.on,
352
+ off: chip.off,
353
+
354
+ /**
355
+ * Add CSS classes to the chip element
356
+ * @param {...string} classes - CSS classes to add
357
+ * @returns {Object} The chip instance for chaining
358
+ */
359
+ addClass(...classes) {
360
+ chip.element.classList.add(...classes);
361
+ return this;
362
+ }
363
+ };
65
364
 
66
- // Handle selection callback
67
- if (config.onSelect) {
365
+ // Add click handler for selection toggle
366
+ if (config.variant === CHIP_VARIANTS.FILTER ||
367
+ config.variant === CHIP_VARIANTS.ASSIST ||
368
+ config.selectable) {
369
+
68
370
  chip.element.addEventListener('click', () => {
69
- if (chip.element.getAttribute('aria-disabled') !== 'true') {
70
- config.onSelect!(chip as ChipComponent);
371
+ if (enhancedChip.isDisabled()) return;
372
+
373
+ enhancedChip.toggleSelected();
374
+
375
+ // Call onChange callback if provided
376
+ if (config.onChange) {
377
+ config.onChange(enhancedChip);
378
+ }
379
+
380
+ // Call onSelect callback if provided
381
+ if (config.onSelect) {
382
+ config.onSelect(enhancedChip);
71
383
  }
72
384
  });
73
385
  }
74
386
 
75
- return chip as ChipComponent;
387
+ return enhancedChip;
76
388
  } catch (error) {
77
- console.error('Chip creation error:', error instanceof Error ? error.message : String(error));
78
- throw new Error(`Failed to create chip: ${error instanceof Error ? error.message : String(error)}`);
389
+ console.error('Chip creation error:', error);
390
+ throw new Error(`Failed to create chip: ${error.message}`);
79
391
  }
80
392
  };
81
393
 
@@ -1,4 +1,4 @@
1
- // src/components/chip/constants.ts
1
+ // src/components/chip/constants.js
2
2
 
3
3
  /**
4
4
  * Available variants for the Chip component
@@ -25,7 +25,7 @@ export const CHIP_VARIANTS = {
25
25
 
26
26
  /** Suggestion chip for presenting options */
27
27
  SUGGESTION: 'suggestion'
28
- } as const;
28
+ };
29
29
 
30
30
  /**
31
31
  * Available sizes for the Chip component
@@ -35,4 +35,4 @@ export const CHIP_SIZES = {
35
35
  SMALL: 'small',
36
36
  MEDIUM: 'medium',
37
37
  LARGE: 'large'
38
- } as const;
38
+ };
@@ -1,4 +1,4 @@
1
- // src/components/chip/index.ts
1
+ // src/components/chip/index.js
2
2
  export { default } from './chip'
3
- export { CHIP_VARIANTS, CHIP_SIZES } from './constants'
4
- export { ChipConfig, ChipComponent } from './types'
3
+ export { default as createChipSet } from './chip-set'
4
+ export { CHIP_VARIANTS, CHIP_SIZES } from './constants'
@@ -180,6 +180,18 @@ $z-index: (
180
180
  ) !default;
181
181
 
182
182
  // Component-specific tokens
183
+ @function card($key) {
184
+ $card: (
185
+ 'width': 344px,
186
+ 'width-small': 344px,
187
+ 'width-medium': 480px,
188
+ 'width-large': 624px,
189
+ 'border-radius': 12px,
190
+ 'padding': 16px
191
+ );
192
+
193
+ @return map-get($card, $key);
194
+ }
183
195
  $button: (
184
196
  'height': 40px,
185
197
  'min-width': 64px,