mtrl 0.3.3 → 0.3.5

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.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "A functional TypeScript/JavaScript component library with composable architecture based on Material Design 3",
5
5
  "author": "floor",
6
6
  "license": "MIT License",
@@ -1,6 +1,6 @@
1
1
  // src/components/segmented-button/config.ts
2
2
  import { createComponentConfig } from '../../core/config/component-config';
3
- import { SegmentedButtonConfig, SelectionMode } from './types';
3
+ import { SegmentedButtonConfig, SelectionMode, Density } from './types';
4
4
 
5
5
  export const DEFAULT_CHECKMARK_ICON = `
6
6
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -13,7 +13,8 @@ export const DEFAULT_CHECKMARK_ICON = `
13
13
  */
14
14
  export const DEFAULT_CONFIG = {
15
15
  mode: SelectionMode.SINGLE,
16
- ripple: true
16
+ ripple: true,
17
+ density: Density.DEFAULT
17
18
  };
18
19
 
19
20
  /**
@@ -31,20 +32,56 @@ export const createBaseConfig = (config: SegmentedButtonConfig = {}): SegmentedB
31
32
  * @returns {Object} Element configuration object for withElement
32
33
  * @internal
33
34
  */
34
- export const getContainerConfig = (config: SegmentedButtonConfig) => ({
35
- tag: 'div',
36
- componentName: 'segmented-button',
37
- attrs: {
38
- role: 'group',
39
- 'aria-label': 'Segmented button',
40
- 'data-mode': config.mode || SelectionMode.SINGLE
41
- },
42
- className: [
43
- config.class,
44
- config.disabled ? `${config.prefix}-segmented-button--disabled` : null
45
- ],
46
- interactive: true
47
- });
35
+ export const getContainerConfig = (config: SegmentedButtonConfig) => {
36
+ const density = config.density || Density.DEFAULT;
37
+
38
+ return {
39
+ tag: 'div',
40
+ componentName: 'segmented-button',
41
+ attrs: {
42
+ role: 'group',
43
+ 'aria-label': 'Segmented button',
44
+ 'data-mode': config.mode || SelectionMode.SINGLE,
45
+ 'data-density': density
46
+ },
47
+ className: [
48
+ config.class,
49
+ config.disabled ? `${config.prefix}-segmented-button--disabled` : null,
50
+ density !== Density.DEFAULT ? `${config.prefix}-segmented-button--${density}` : null
51
+ ],
52
+ interactive: true
53
+ };
54
+ };
55
+
56
+ /**
57
+ * Gets density-specific sizing and spacing values
58
+ * @param {string} density - The density level
59
+ * @returns {Object} CSS variables with sizing values
60
+ * @internal
61
+ */
62
+ export const getDensityStyles = (density: string): Record<string, string> => {
63
+ switch (density) {
64
+ case Density.COMPACT:
65
+ return {
66
+ '--segment-padding': '4px 8px',
67
+ '--segment-height': '28px',
68
+ '--segment-font-size': '0.8125rem'
69
+ };
70
+ case Density.COMFORTABLE:
71
+ return {
72
+ '--segment-padding': '6px 12px',
73
+ '--segment-height': '32px',
74
+ '--segment-font-size': '0.875rem'
75
+ };
76
+ case Density.DEFAULT:
77
+ default:
78
+ return {
79
+ '--segment-padding': '8px 16px',
80
+ '--segment-height': '36px',
81
+ '--segment-font-size': '0.875rem'
82
+ };
83
+ }
84
+ };
48
85
 
49
86
  /**
50
87
  * Generates configuration for a segment element
@@ -57,6 +94,7 @@ export const getContainerConfig = (config: SegmentedButtonConfig) => ({
57
94
  export const getSegmentConfig = (segment, prefix, groupDisabled = false) => {
58
95
  const isDisabled = groupDisabled || segment.disabled;
59
96
 
97
+ // We use button as our base class, but add segment-specific classes for states
60
98
  return {
61
99
  tag: 'button',
62
100
  attrs: {
@@ -67,10 +105,11 @@ export const getSegmentConfig = (segment, prefix, groupDisabled = false) => {
67
105
  value: segment.value
68
106
  },
69
107
  className: [
70
- `${prefix}-segment`,
71
- segment.selected ? `${prefix}-segment--selected` : null,
72
- isDisabled ? `${prefix}-segment--disabled` : null,
73
- segment.class
108
+ `${prefix}-button`, // Base button class
109
+ `${prefix}-segmented-button-segment`, // Specific segment class
110
+ segment.selected ? `${prefix}-segment--selected` : null, // Selected state
111
+ isDisabled ? `${prefix}-segment--disabled` : null, // Disabled state
112
+ segment.class // Custom class if provided
74
113
  ],
75
114
  forwardEvents: {
76
115
  click: (component) => !isDisabled
@@ -1,4 +1,4 @@
1
1
  // src/components/segmented-button/index.ts
2
2
  export { default, default as createSegmentedButton } from './segmented-button';
3
- export { SelectionMode } from './types';
3
+ export { SelectionMode, Density } from './types';
4
4
  export type { SegmentedButtonConfig, SegmentedButtonComponent, SegmentConfig, Segment } from './types';
@@ -1,11 +1,10 @@
1
1
  // src/components/segmented-button/segment.ts
2
- import { createElement } from '../../core/dom/create';
3
- import { createRipple } from '../../core/build/ripple';
2
+ import createButton from '../button';
4
3
  import { SegmentConfig, Segment } from './types';
5
- import { getSegmentConfig } from './config';
4
+ import { DEFAULT_CHECKMARK_ICON } from './config';
6
5
 
7
6
  /**
8
- * Creates a segment for the segmented button
7
+ * Creates a segment for the segmented button using the button component
9
8
  * @param {SegmentConfig} config - Segment configuration
10
9
  * @param {HTMLElement} container - Container element
11
10
  * @param {string} prefix - Component prefix
@@ -21,59 +20,36 @@ export const createSegment = (
21
20
  groupDisabled = false,
22
21
  options = { ripple: true, rippleConfig: {} }
23
22
  ): Segment => {
24
- const segmentConfig = getSegmentConfig(config, prefix, groupDisabled);
25
- const element = createElement(segmentConfig);
26
-
27
- // Add to container
28
- container.appendChild(element);
29
-
30
- // Create ripple effect if enabled
31
- let ripple;
32
- if (options.ripple) {
33
- ripple = createRipple(options.rippleConfig);
34
- ripple.mount(element);
35
- }
23
+ const isDisabled = groupDisabled || config.disabled;
24
+ const originalIcon = config.icon;
25
+ const checkmarkIcon = config.checkmarkIcon || DEFAULT_CHECKMARK_ICON;
26
+
27
+ // Create segment using button component with appropriate initial icon
28
+ const button = createButton({
29
+ text: config.text,
30
+ // If selected and has both icon and text, use checkmark instead of the original icon
31
+ icon: (config.selected && config.text && originalIcon) ? checkmarkIcon : originalIcon,
32
+ value: config.value || config.text || '',
33
+ disabled: isDisabled,
34
+ ripple: options.ripple,
35
+ rippleConfig: options.rippleConfig,
36
+ class: config.class,
37
+ // No variant - we'll style it via the segmented button styles
38
+ });
39
+
40
+ // Add segment-specific classes
41
+ button.element.classList.add(`${prefix}-segmented-button-segment`);
36
42
 
37
- // Create text element if provided
38
- let textElement;
39
- if (config.text) {
40
- textElement = createElement({
41
- tag: 'span',
42
- className: `${prefix}-segmentedbutton-segment-text`,
43
- text: config.text,
44
- container: element
45
- });
43
+ // Set initial selected state
44
+ if (config.selected) {
45
+ button.element.classList.add(`${prefix}-segment--selected`);
46
+ button.element.setAttribute('aria-pressed', 'true');
47
+ } else {
48
+ button.element.setAttribute('aria-pressed', 'false');
46
49
  }
47
50
 
48
- // Create icon and checkmark elements
49
- let iconElement, checkmarkElement;
50
- if (config.icon) {
51
- // Create icon element
52
- iconElement = createElement({
53
- tag: 'span',
54
- className: `${prefix}-segmentedbutton-segment-icon`,
55
- html: config.icon,
56
- container: element
57
- });
58
-
59
- // Create checkmark element (hidden initially)
60
- checkmarkElement = createElement({
61
- tag: 'span',
62
- className: `${prefix}-segmentedbutton-segment-'checkmark'`,
63
- html: 'icon',
64
- container: element
65
- });
66
-
67
- // Hide checkmark if not selected
68
- if (!config.selected) {
69
- checkmarkElement.style.display = 'none';
70
- }
71
-
72
- // Hide icon if selected and we have text (icon replaced by checkmark)
73
- if (config.selected && config.text) {
74
- iconElement.style.display = 'none';
75
- }
76
- }
51
+ // Add to container
52
+ container.appendChild(button.element);
77
53
 
78
54
  /**
79
55
  * Updates the visual state based on selection
@@ -81,45 +57,27 @@ export const createSegment = (
81
57
  * @private
82
58
  */
83
59
  const updateSelectedState = (selected: boolean) => {
84
- element.classList.toggle(`${prefix}-segmentedbutton-segment--selected`, selected);
85
- element.setAttribute('aria-pressed', selected ? 'true' : 'false');
60
+ button.element.classList.toggle(`${prefix}-segment--selected`, selected);
61
+ button.element.setAttribute('aria-pressed', selected ? 'true' : 'false');
86
62
 
87
- // Handle icon/checkmark swap if we have both text and icon
88
- if (iconElement && checkmarkElement && config.text) {
89
- iconElement.style.display = selected ? 'none' : '';
90
- checkmarkElement.style.display = selected ? '' : 'none';
91
- } else if (checkmarkElement) {
92
- // If we have only icons (no text), show checkmark based on selection
93
- checkmarkElement.style.display = selected ? '' : 'none';
94
- }
95
- };
96
-
97
- /**
98
- * Updates the disabled state
99
- * @param {boolean} disabled - Whether the segment is disabled
100
- * @private
101
- */
102
- const updateDisabledState = (disabled: boolean) => {
103
- const isDisabled = disabled || groupDisabled;
104
- element.classList.toggle(`${prefix}-segmentedbutton-segment--disabled`, isDisabled);
105
-
106
- if (isDisabled) {
107
- element.setAttribute('disabled', 'true');
108
- } else {
109
- element.removeAttribute('disabled');
63
+ // Handle icon/checkmark swap if we have both text and original icon
64
+ if (config.text && originalIcon) {
65
+ if (selected) {
66
+ // When selected and has both text and icon, show checkmark
67
+ button.setIcon(checkmarkIcon);
68
+ } else {
69
+ // When not selected, restore original icon
70
+ button.setIcon(originalIcon);
71
+ }
110
72
  }
111
73
  };
112
74
 
113
- // Value to use for the segment
114
- const value = config.value || config.text || '';
115
-
116
75
  // Initialize state
117
76
  let isSelected = config.selected || false;
118
- let isDisabled = config.disabled || false;
119
77
 
120
78
  return {
121
- element,
122
- value,
79
+ element: button.element,
80
+ value: config.value || config.text || '',
123
81
 
124
82
  isSelected() {
125
83
  return isSelected;
@@ -131,24 +89,20 @@ export const createSegment = (
131
89
  },
132
90
 
133
91
  isDisabled() {
134
- return isDisabled || groupDisabled;
92
+ return button.disabled?.isDisabled?.() || false;
135
93
  },
136
94
 
137
95
  setDisabled(disabled: boolean) {
138
- isDisabled = disabled;
139
- updateDisabledState(disabled);
96
+ if (disabled) {
97
+ button.disable();
98
+ } else {
99
+ button.enable();
100
+ }
140
101
  },
141
102
 
142
103
  destroy() {
143
- // Clean up ripple if it exists
144
- if (ripple) {
145
- ripple.unmount(element);
146
- }
147
-
148
- // Remove from DOM
149
- if (element.parentNode) {
150
- element.parentNode.removeChild(element);
151
- }
104
+ // Use the button's built-in destroy method for cleanup
105
+ button.destroy();
152
106
  }
153
107
  };
154
108
  };
@@ -3,19 +3,61 @@ import { pipe } from '../../core/compose/pipe';
3
3
  import { createBase, withElement } from '../../core/compose/component';
4
4
  import { withEvents, withLifecycle } from '../../core/compose/features';
5
5
  import { createEmitter } from '../../core/state/emitter';
6
- import { SegmentedButtonConfig, SegmentedButtonComponent, SelectionMode, Segment } from './types';
7
- import { createBaseConfig, getContainerConfig } from './config';
6
+ import { SegmentedButtonConfig, SegmentedButtonComponent, SelectionMode, Density, Segment } from './types';
7
+ import { createBaseConfig, getContainerConfig, getDensityStyles } from './config';
8
8
  import { createSegment } from './segment';
9
9
 
10
10
  /**
11
11
  * Creates a new Segmented Button component
12
+ *
13
+ * The Segmented Button component provides a group of related buttons that can
14
+ * be used for selection and filtering. It supports single or multiple selection modes,
15
+ * configurable density, disabled states, and event handling.
16
+ *
12
17
  * @param {SegmentedButtonConfig} config - Segmented Button configuration
13
18
  * @returns {SegmentedButtonComponent} Segmented Button component instance
19
+ *
20
+ * @example
21
+ * // Create a segmented button with three segments in single selection mode
22
+ * const viewToggle = createSegmentedButton({
23
+ * segments: [
24
+ * { text: 'Day', value: 'day', selected: true },
25
+ * { text: 'Week', value: 'week' },
26
+ * { text: 'Month', value: 'month' }
27
+ * ],
28
+ * mode: SelectionMode.SINGLE
29
+ * });
30
+ *
31
+ * // Listen for selection changes
32
+ * viewToggle.on('change', (event) => {
33
+ * console.log('Selected view:', event.value[0]);
34
+ * updateCalendarView(event.value[0]);
35
+ * });
36
+ *
37
+ * @example
38
+ * // Create a compact multi-select segmented button with icons
39
+ * const filterOptions = createSegmentedButton({
40
+ * segments: [
41
+ * {
42
+ * icon: '<svg>...</svg>',
43
+ * text: 'Filter 1',
44
+ * value: 'filter1'
45
+ * },
46
+ * {
47
+ * icon: '<svg>...</svg>',
48
+ * text: 'Filter 2',
49
+ * value: 'filter2'
50
+ * }
51
+ * ],
52
+ * mode: SelectionMode.MULTI,
53
+ * density: Density.COMPACT
54
+ * });
14
55
  */
15
56
  const createSegmentedButton = (config: SegmentedButtonConfig = {}): SegmentedButtonComponent => {
16
57
  // Process configuration
17
58
  const baseConfig = createBaseConfig(config);
18
59
  const mode = baseConfig.mode || SelectionMode.SINGLE;
60
+ const density = baseConfig.density || Density.DEFAULT;
19
61
  const emitter = createEmitter();
20
62
 
21
63
  try {
@@ -27,6 +69,12 @@ const createSegmentedButton = (config: SegmentedButtonConfig = {}): SegmentedBut
27
69
  withLifecycle()
28
70
  )(baseConfig);
29
71
 
72
+ // Apply density styles
73
+ const densityStyles = getDensityStyles(density as string);
74
+ Object.entries(densityStyles).forEach(([prop, value]) => {
75
+ component.element.style.setProperty(prop, value);
76
+ });
77
+
30
78
  // Create segments
31
79
  const segments: Segment[] = [];
32
80
  if (baseConfig.segments && baseConfig.segments.length) {
@@ -125,6 +173,34 @@ const createSegmentedButton = (config: SegmentedButtonConfig = {}): SegmentedBut
125
173
  */
126
174
  const findSegmentByValue = (value: string) => segments.find(segment => segment.value === value);
127
175
 
176
+ /**
177
+ * Updates the density of the segmented button
178
+ * @param {string} newDensity - New density value
179
+ * @private
180
+ */
181
+ const updateDensity = (newDensity: string) => {
182
+ // Remove existing density classes
183
+ [Density.DEFAULT, Density.COMFORTABLE, Density.COMPACT].forEach(d => {
184
+ if (d !== Density.DEFAULT) {
185
+ component.element.classList.remove(`${baseConfig.prefix}-segmented-button--${d}`);
186
+ }
187
+ });
188
+
189
+ // Add new density class if not default
190
+ if (newDensity !== Density.DEFAULT) {
191
+ component.element.classList.add(`${baseConfig.prefix}-segmented-button--${newDensity}`);
192
+ }
193
+
194
+ // Update data attribute
195
+ component.element.setAttribute('data-density', newDensity);
196
+
197
+ // Apply density styles
198
+ const densityStyles = getDensityStyles(newDensity);
199
+ Object.entries(densityStyles).forEach(([prop, value]) => {
200
+ component.element.style.setProperty(prop, value);
201
+ });
202
+ };
203
+
128
204
  // Create the component API
129
205
  const segmentedButton: SegmentedButtonComponent = {
130
206
  element: component.element,
@@ -205,15 +281,51 @@ const createSegmentedButton = (config: SegmentedButtonConfig = {}): SegmentedBut
205
281
  enable() {
206
282
  // Enable the entire component
207
283
  component.element.classList.remove(`${baseConfig.prefix}-segmented-button--disabled`);
284
+ // Enable all segments (unless individually disabled)
285
+ segments.forEach(segment => {
286
+ // Only enable if it wasn't individually disabled
287
+ if (!baseConfig.segments?.find(s => s.value === segment.value)?.disabled) {
288
+ segment.setDisabled(false);
289
+ }
290
+ });
208
291
  return this;
209
292
  },
210
293
 
211
294
  disable() {
212
295
  // Disable the entire component
213
296
  component.element.classList.add(`${baseConfig.prefix}-segmented-button--disabled`);
297
+ // Disable all segments
298
+ segments.forEach(segment => {
299
+ segment.setDisabled(true);
300
+ });
214
301
  return this;
215
302
  },
216
303
 
304
+ enableSegment(value) {
305
+ const segment = findSegmentByValue(value);
306
+ if (segment) {
307
+ segment.setDisabled(false);
308
+ }
309
+ return this;
310
+ },
311
+
312
+ disableSegment(value) {
313
+ const segment = findSegmentByValue(value);
314
+ if (segment) {
315
+ segment.setDisabled(true);
316
+ }
317
+ return this;
318
+ },
319
+
320
+ setDensity(newDensity) {
321
+ updateDensity(newDensity);
322
+ return this;
323
+ },
324
+
325
+ getDensity() {
326
+ return component.element.getAttribute('data-density') || Density.DEFAULT;
327
+ },
328
+
217
329
  on(event, handler) {
218
330
  emitter.on(event, handler);
219
331
  return this;
@@ -11,6 +11,20 @@ export enum SelectionMode {
11
11
  MULTI = 'multi'
12
12
  }
13
13
 
14
+ /**
15
+ * Density options for segmented button
16
+ * Controls the overall sizing and spacing of the component.
17
+ * @category Components
18
+ */
19
+ export enum Density {
20
+ /** Default size with standard spacing */
21
+ DEFAULT = 'default',
22
+ /** Reduced size and spacing, more compact */
23
+ COMFORTABLE = 'comfortable',
24
+ /** Minimal size and spacing, most compact */
25
+ COMPACT = 'compact'
26
+ }
27
+
14
28
  /**
15
29
  * Event types for segmented button
16
30
  */
@@ -77,6 +91,11 @@ export interface SegmentConfig {
77
91
  * Additional CSS class names for this segment
78
92
  */
79
93
  class?: string;
94
+
95
+ /**
96
+ * Custom icon to display when segment is selected (replaces default checkmark)
97
+ */
98
+ checkmarkIcon?: string;
80
99
  }
81
100
 
82
101
  /**
@@ -122,6 +141,12 @@ export interface SegmentedButtonConfig {
122
141
  * @default true
123
142
  */
124
143
  ripple?: boolean;
144
+
145
+ /**
146
+ * Density setting that controls the component's size and spacing
147
+ * @default Density.DEFAULT
148
+ */
149
+ density?: Density | string;
125
150
 
126
151
  /**
127
152
  * Ripple effect configuration
@@ -230,6 +255,33 @@ export interface SegmentedButtonComponent {
230
255
  * @returns The SegmentedButtonComponent for chaining
231
256
  */
232
257
  disable: () => SegmentedButtonComponent;
258
+
259
+ /**
260
+ * Enables a specific segment by its value
261
+ * @param value - The value of the segment to enable
262
+ * @returns The SegmentedButtonComponent for chaining
263
+ */
264
+ enableSegment: (value: string) => SegmentedButtonComponent;
265
+
266
+ /**
267
+ * Disables a specific segment by its value
268
+ * @param value - The value of the segment to disable
269
+ * @returns The SegmentedButtonComponent for chaining
270
+ */
271
+ disableSegment: (value: string) => SegmentedButtonComponent;
272
+
273
+ /**
274
+ * Sets the density of the segmented button
275
+ * @param density - The density level to set
276
+ * @returns The SegmentedButtonComponent for chaining
277
+ */
278
+ setDensity: (density: Density | string) => SegmentedButtonComponent;
279
+
280
+ /**
281
+ * Gets the current density setting
282
+ * @returns The current density
283
+ */
284
+ getDensity: () => string;
233
285
 
234
286
  /**
235
287
  * Adds an event listener to the segmented button
@@ -41,29 +41,31 @@ const updateCircularStyle = (component: ElementComponent, config: IconConfig): v
41
41
 
42
42
  /**
43
43
  * Adds icon management to a component
44
- *
45
44
  * @param config - Configuration object containing icon information
46
45
  * @returns Function that enhances a component with icon capabilities
47
46
  */
48
47
  export const withIcon = <T extends IconConfig>(config: T) =>
49
48
  <C extends ElementComponent>(component: C): C & IconComponent => {
49
+ // Create the icon with configuration settings
50
50
  const icon = createIcon(component.element, {
51
51
  prefix: config.prefix,
52
52
  type: config.componentName || 'component',
53
53
  position: config.iconPosition,
54
54
  iconSize: config.iconSize
55
55
  });
56
-
57
- if (config.icon) {
58
- icon.setIcon(config.icon);
59
- }
60
-
56
+
57
+ // Set icon if provided in config
58
+ config.icon && icon.setIcon(config.icon);
59
+
60
+ // Apply button-specific styling if the component is a button
61
61
  if (component.componentName === 'button') {
62
- updateCircularStyle(component, config);
62
+ if (!config.text) {
63
+ updateCircularStyle(component, config);
64
+ } else if (config.icon && config.text) {
65
+ component.element.classList.add(`${component.getClass('button')}--icon`);
66
+ }
63
67
  }
64
-
65
- return {
66
- ...component,
67
- icon
68
- };
69
- };
68
+
69
+ // Return enhanced component with icon capabilities
70
+ return Object.assign(component, { icon });
71
+ };
@@ -96,6 +96,12 @@ $component: '#{base.$prefix}-button';
96
96
  }
97
97
  }
98
98
 
99
+
100
+ &--icon {
101
+ padding: 0 v.button('padding-horizontal') 0 calc(v.button('padding-horizontal') / 2 + 6px);
102
+ }
103
+
104
+
99
105
  &--disabled {
100
106
  opacity: 0.38
101
107
  }
@@ -124,7 +124,7 @@ $container: '#{base.$prefix}-chips';
124
124
  background-color: t.color('on-surface');
125
125
  opacity: 0.08;
126
126
  pointer-events: none;
127
- border-radius: inherit;
127
+ border-radius: v.chip('border-radius');
128
128
  }
129
129
 
130
130
  &:active::after {
@@ -253,7 +253,6 @@ $container: '#{base.$prefix}-chips';
253
253
 
254
254
  // Filter chip
255
255
  &--filter {
256
-
257
256
  color: t.color('on-surface');
258
257
  position: relative;
259
258
  border: 1px solid t.alpha('outline', v.chip('outlined-border-opacity'));
@@ -266,7 +265,7 @@ $container: '#{base.$prefix}-chips';
266
265
  background-color: t.color('on-surface');
267
266
  opacity: 0.08;
268
267
  pointer-events: none;
269
- border-radius: inherit;
268
+ border-radius: calc(v.chip('border-radius') - 1px);
270
269
  }
271
270
 
272
271
  &.#{$component}--selected {
@@ -309,7 +308,7 @@ $container: '#{base.$prefix}-chips';
309
308
  background-color: t.color('on-secondary-container');
310
309
  opacity: 0.08;
311
310
  pointer-events: none;
312
- border-radius: inherit;
311
+ border-radius: calc(v.chip('border-radius') - 1px);
313
312
  }
314
313
  }
315
314
  }
@@ -330,7 +329,7 @@ $container: '#{base.$prefix}-chips';
330
329
  background-color: t.color('on-surface');
331
330
  opacity: 0.08;
332
331
  pointer-events: none;
333
- border-radius: inherit;
332
+ border-radius: v.chip('border-radius');
334
333
  }
335
334
 
336
335
  &.#{$component}--selected {
@@ -13,49 +13,103 @@ $component: '#{base.$prefix}-segmented-button';
13
13
  display: inline-flex;
14
14
  align-items: center;
15
15
  justify-content: center;
16
- height: 40px;
17
- border-radius: v.shape('full');
16
+ border-radius: 20px; // Half of height per MD3 specs
18
17
  border: 1px solid t.color('outline');
19
18
  background-color: transparent;
20
19
  overflow: hidden;
21
20
 
22
- // Disabled state
21
+ // Density variables with defaults (Material Design 3 standard density)
22
+ --segment-height: 40px;
23
+ --segment-padding-x: 24px;
24
+ --segment-padding-icon-only: 12px;
25
+ --segment-padding-icon-text-left: 16px;
26
+ --segment-padding-icon-text-right: 24px;
27
+ --segment-icon-size: 18px;
28
+ --segment-text-size: 0.875rem;
29
+ --segment-border-radius: 20px;
30
+
31
+ // Set height from the CSS variable
32
+ height: var(--segment-height);
33
+ // Adjust border radius based on height
34
+ border-radius: calc(var(--segment-height) / 2);
35
+
36
+ // Comfortable density (medium)
37
+ &--comfortable {
38
+ --segment-height: 36px;
39
+ --segment-padding-x: 20px;
40
+ --segment-padding-icon-only: 10px;
41
+ --segment-padding-icon-text-left: 14px;
42
+ --segment-padding-icon-text-right: 20px;
43
+ --segment-icon-size: 16px;
44
+ --segment-text-size: 0.8125rem;
45
+ --segment-border-radius: 18px;
46
+
47
+ border-radius: var(--segment-border-radius);
48
+ }
49
+
50
+ // Compact density (small)
51
+ &--compact {
52
+ --segment-height: 32px;
53
+ --segment-padding-x: 16px;
54
+ --segment-padding-icon-only: 8px;
55
+ --segment-padding-icon-text-left: 12px;
56
+ --segment-padding-icon-text-right: 16px;
57
+ --segment-icon-size: 16px;
58
+ --segment-text-size: 0.75rem;
59
+ --segment-border-radius: 16px;
60
+
61
+ border-radius: var(--segment-border-radius);
62
+ }
63
+
64
+ // Disabled state for whole component
23
65
  &--disabled {
24
66
  opacity: 0.38;
25
67
  pointer-events: none;
26
68
  }
27
69
 
28
- // Segment container
29
- &-segment {
30
- // Base styles
31
- position: relative;
32
- display: flex;
33
- align-items: center;
34
- justify-content: center;
35
- height: 100%;
36
- min-width: 48px;
37
- padding: 0 12px;
38
- border: none;
70
+ // Style for button elements used as segments
71
+ .#{base.$prefix}-button {
72
+ // Reset button styles that we don't want
73
+ margin: 0;
74
+ box-shadow: none;
39
75
  background-color: transparent;
40
- color: t.color('on-surface');
41
- cursor: pointer;
42
- user-select: none;
76
+ border: none;
77
+ position: relative; // For pseudo-elements
78
+ border-radius: 0; // Reset any border-radius
79
+ min-width: 48px;
80
+ height: 100%;
81
+ color: t.color('on-surface'); // Original color
82
+
83
+ // Use CSS variables for dynamic sizing based on density
84
+ padding: 0 var(--segment-padding-x);
43
85
 
44
- // Fix segmented borders
45
- &:not(:first-child) {
46
- border-left: 1px solid t.color('outline');
86
+ // Icon-only segments have equal padding all around
87
+ &.#{base.$prefix}-button--circular {
88
+ padding: 0 var(--segment-padding-icon-only);
47
89
  }
48
90
 
49
- // Typography
50
- @include m.typography('label-large');
91
+ // Segments with both icon and text
92
+ &:has(.#{base.$prefix}-button-icon + .#{base.$prefix}-button-text) {
93
+ padding: 0 var(--segment-padding-icon-text-right) 0 var(--segment-padding-icon-text-left);
94
+ }
51
95
 
52
- // Transition
53
- @include m.motion-transition(
54
- background-color,
55
- color
56
- );
96
+ // Only add border radius to first and last segments
97
+ &:first-child {
98
+ border-top-left-radius: var(--segment-border-radius);
99
+ border-bottom-left-radius: var(--segment-border-radius);
100
+ }
57
101
 
58
- // States
102
+ &:last-child {
103
+ border-top-right-radius: var(--segment-border-radius);
104
+ border-bottom-right-radius: var(--segment-border-radius);
105
+ }
106
+
107
+ // Hover state - keeping original color
108
+ &:hover:not([disabled]) {
109
+ background-color: t.alpha('on-surface', 0.08);
110
+ }
111
+
112
+ // Focus state
59
113
  &:focus {
60
114
  outline: none;
61
115
  }
@@ -65,53 +119,109 @@ $component: '#{base.$prefix}-segmented-button';
65
119
  outline-offset: -2px;
66
120
  }
67
121
 
68
- &:hover:not(.#{$component}-segment--disabled) {
69
- background-color: t.alpha('on-surface', 0.08);
122
+ // Replace border with pseudo-elements for better control
123
+ // Each segment has its own right border (except last)
124
+ &:not(:last-child)::after {
125
+ content: '';
126
+ position: absolute;
127
+ top: 0;
128
+ right: 0;
129
+ height: 100%;
130
+ width: 1px;
131
+ background-color: t.color('outline');
132
+ pointer-events: none;
70
133
  }
71
134
 
72
- // Selected state
73
- &--selected {
74
- background-color: t.color('secondary-container');
75
- color: t.color('on-secondary-container');
135
+ // Disabled state handling
136
+ &[disabled] {
137
+ opacity: 0.38;
138
+
139
+ // When a disabled button has a right border, make it lower opacity
140
+ &::after {
141
+ background-color: t.alpha('outline', 0.38);
142
+ }
143
+
144
+ // When a disabled button is before a non-disabled button, let the non-disabled handle the border
145
+ + .#{base.$prefix}-button:not([disabled])::before {
146
+ content: '';
147
+ position: absolute;
148
+ top: 0;
149
+ left: 0;
150
+ height: 100%;
151
+ width: 1px;
152
+ background-color: t.color('outline');
153
+ pointer-events: none;
154
+ }
76
155
 
77
- &:hover:not(.#{$component}-segment--disabled) {
78
- background-color: t.alpha('secondary-container', 0.8);
156
+ // When a disabled button is after a non-disabled button, make the non-disabled button's border low opacity
157
+ &:not(:first-child) {
158
+ &::before {
159
+ content: '';
160
+ position: absolute;
161
+ top: 0;
162
+ left: 0;
163
+ height: 100%;
164
+ width: 1px;
165
+ background-color: t.alpha('outline', 0.38);
166
+ pointer-events: none;
167
+ }
79
168
  }
80
169
  }
81
170
 
82
- // Disabled state
83
- &--disabled {
84
- opacity: 0.38;
85
- cursor: not-allowed;
171
+ // Ensure all button styles are reset appropriately
172
+ &.#{base.$prefix}-button--filled,
173
+ &.#{base.$prefix}-button--outlined,
174
+ &.#{base.$prefix}-button--tonal,
175
+ &.#{base.$prefix}-button--elevated,
176
+ &.#{base.$prefix}-button--text {
177
+ background-color: transparent;
178
+ box-shadow: none;
179
+ color: t.color('on-surface');
86
180
  }
181
+ }
182
+
183
+ // Selected state
184
+ .#{base.$prefix}-segment--selected {
185
+ background-color: t.color('secondary-container');
186
+ color: t.color('on-secondary-container');
87
187
 
88
- // Text element
89
- &-text {
90
- // For when both icon and text are present
91
- margin: 0 auto;
188
+ &:hover:not([disabled]) {
189
+ background-color: t.alpha('secondary-container', 0.8);
92
190
  }
93
191
 
94
- // Icon styles
95
- &-icon, &-checkmark {
96
- display: inline-flex;
97
- align-items: center;
98
- justify-content: center;
99
- width: 18px;
100
- height: 18px;
101
-
102
- svg {
103
- width: 18px;
104
- height: 18px;
105
- }
106
-
107
- + .#{$component}-segment-text {
108
- margin-left: 8px;
109
- }
192
+ // Ensure color change even with different button variants
193
+ &.#{base.$prefix}-button--filled,
194
+ &.#{base.$prefix}-button--outlined,
195
+ &.#{base.$prefix}-button--tonal,
196
+ &.#{base.$prefix}-button--elevated,
197
+ &.#{base.$prefix}-button--text {
198
+ background-color: t.color('secondary-container');
199
+ color: t.color('on-secondary-container');
110
200
  }
201
+ }
202
+
203
+ // Ensure proper spacing in button contents
204
+ .#{base.$prefix}-button-text {
205
+ margin: 0;
206
+ white-space: nowrap;
207
+ @include m.typography('label-large');
208
+ // Apply density-specific font sizing
209
+ font-size: var(--segment-text-size);
210
+ }
211
+
212
+ .#{base.$prefix}-button-icon + .#{base.$prefix}-button-text {
213
+ margin-left: 8px; // MD3 spec for space between icon and text
214
+ }
215
+
216
+ // Icon sizing per MD3
217
+ .#{base.$prefix}-button-icon {
218
+ display: flex;
219
+ align-items: center;
220
+ justify-content: center;
111
221
 
112
- // Space the checkmark icon
113
- &-checkmark + .#{$component}-segment-text {
114
- margin-left: 8px;
222
+ svg {
223
+ width: var(--segment-icon-size);
224
+ height: var(--segment-icon-size);
115
225
  }
116
226
  }
117
227
  }