mtrl 0.3.2 → 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/CONTRIBUTING.md CHANGED
@@ -1,18 +1,18 @@
1
1
  # Contributing to mtrl
2
2
 
3
- Thank you for your interest in contributing to mtrl! This document provides guidelines and instructions for contributing to this lightweight, ES6-focused JavaScript UI component library.
3
+ Thank you for your interest in contributing to mtrl! This document provides guidelines and instructions for contributing to this lightweight, TypeScript-focused UI component library.
4
4
 
5
5
  ## Why Contribute?
6
6
 
7
7
  mtrl aims to be a modern, flexible UI component library with:
8
8
 
9
9
  - Zero dependencies (except Bun for development)
10
- - ES6+ focused codebase
10
+ - TypeScript-first codebase
11
11
  - Lightweight, tree-shakable components
12
12
  - Simple and extensible API
13
13
  - Excellent documentation
14
14
 
15
- By contributing to mtrl, you'll help create a lean alternative to heavier frameworks while gaining experience with modern JavaScript patterns and component design.
15
+ By contributing to mtrl, you'll help create a lean alternative to heavier frameworks while gaining experience with modern TypeScript patterns and component design.
16
16
 
17
17
  ## Getting Started
18
18
 
@@ -76,16 +76,29 @@ mtrl uses a separate repository called mtrl.app (https://mtrl.app) for showcasin
76
76
 
77
77
  mtrl components follow a consistent pattern:
78
78
 
79
- ```javascript
80
- // src/components/mycomponent/index.js
79
+ ```typescript
80
+ // src/components/mycomponent/index.ts
81
81
  export { createMyComponent } from './mycomponent';
82
+ export type { MyComponentOptions } from './types';
82
83
 
83
- // src/components/mycomponent/mycomponent.js
84
+ // src/components/mycomponent/types.ts
85
+ export interface MyComponentOptions {
86
+ text?: string;
87
+ onClick?: (event: MouseEvent) => void;
88
+ // other options...
89
+ }
90
+
91
+ // src/components/mycomponent/mycomponent.ts
84
92
  import { createElement } from '../../core/dom/create';
85
93
  import { createLifecycle } from '../../core/state/lifecycle';
86
- // etc...
87
-
88
- export const createMyComponent = (options = {}) => {
94
+ import type { MyComponentOptions } from './types';
95
+
96
+ /**
97
+ * Creates a new MyComponent instance
98
+ * @param options - Configuration options for MyComponent
99
+ * @returns The MyComponent instance
100
+ */
101
+ export const createMyComponent = (options: MyComponentOptions = {}) => {
89
102
  // Create DOM elements
90
103
  const element = createElement({...});
91
104
 
@@ -108,21 +121,30 @@ export const createMyComponent = (options = {}) => {
108
121
  The mtrl.app showcase application is the best way to develop and test your components:
109
122
 
110
123
  1. Clone the mtrl.app repository alongside your mtrl clone.
111
- 2. Create a new view file in `src/client/views/components/` for your component.
112
- 3. Add the route in `src/client/core/navigation.js` of the mtrl.app repository.
124
+ 2. Create a new view file in `src/client/content/components/` for your component.
125
+ 3. Add the route in `src/client/core/navigation.ts` of the mtrl.app repository.
113
126
  4. Implement different variants and states for testing.
114
127
  5. Run the showcase server with `bun run dev` in the mtrl.app directory.
115
128
 
116
129
  This separation of the library code (mtrl) and the showcase app (mtrl.app) keeps the core library clean while providing a rich development environment.
117
130
 
131
+ ### TypeScript Standards
132
+
133
+ - Use TypeScript's type system to create clear interfaces and types
134
+ - Export types and interfaces separately from implementations
135
+ - Use strict typing and avoid `any` when possible
136
+ - Prefer interfaces for public APIs and type aliases for complex types
137
+ - Add proper return types to all functions
138
+
118
139
  ### Coding Standards
119
140
 
120
- - Use ES6+ features but maintain browser compatibility
121
- - Follow functional programming principles when possible
141
+ - Add file path as a comment on the first line of each file
142
+ - Use functional programming principles when possible
122
143
  - Use consistent naming conventions:
123
144
  - Factory functions should be named `createXyz`
124
145
  - Utilities should use clear, descriptive names
125
- - Write JSDoc comments for all public functions
146
+ - Interfaces should be named in PascalCase (e.g., `ButtonOptions`)
147
+ - Write TypeDoc comments for all public functions and types
126
148
 
127
149
  ### CSS/SCSS Guidelines
128
150
 
@@ -143,7 +165,7 @@ This separation of the library code (mtrl) and the showcase app (mtrl.app) keeps
143
165
 
144
166
  Please add appropriate tests for your changes:
145
167
 
146
- ```javascript
168
+ ```typescript
147
169
  // Example test structure
148
170
  describe('myComponent', () => {
149
171
  it('should render correctly', () => {
@@ -158,12 +180,29 @@ describe('myComponent', () => {
158
180
 
159
181
  ## Documentation
160
182
 
161
- For any new feature or component:
183
+ Documentation is crucial for this project:
162
184
 
163
- - Add JSDoc comments for API methods
185
+ - Add TypeDoc comments for all public API methods and types
186
+ - Comment the file path at the top of each file
164
187
  - Update the component's README.md (if applicable)
165
188
  - Consider adding example code in the playground
166
189
 
190
+ Example of proper TypeDoc:
191
+
192
+ ```typescript
193
+ /**
194
+ * Creates a button element with specified options
195
+ *
196
+ * @param options - The button configuration options
197
+ * @returns A button component instance
198
+ * @example
199
+ * ```ts
200
+ * const button = createButton({ text: 'Click me', variant: 'primary' });
201
+ * document.body.appendChild(button.element);
202
+ * ```
203
+ */
204
+ ```
205
+
167
206
  ## Community and Communication
168
207
 
169
208
  - Submit issues for bugs or feature requests
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtrl",
3
- "version": "0.3.2",
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",
@@ -107,11 +107,22 @@ export const createNavItem = (
107
107
  itemElement.appendChild(icon);
108
108
  }
109
109
 
110
- // Add label if provided
110
+ // Add label if provided - CONVERTED TO ANCHOR FOR SEO
111
111
  if (config.label) {
112
- const label = document.createElement('span');
112
+ // Create anchor instead of span for the label
113
+ const label = document.createElement('a');
113
114
  label.className = `${prefix}-${NavClass.LABEL}`;
114
115
  label.textContent = config.label;
116
+ label.href = `/${config.id}`; // Create path based on ID
117
+
118
+ // Ensure the anchor looks the same as a span would
119
+ label.style.textDecoration = 'none';
120
+ label.style.color = 'inherit';
121
+ label.style.pointerEvents = 'none'; // Let the button handle click events
122
+
123
+ // Add SEO attributes
124
+ label.setAttribute('title', config.label);
125
+
115
126
  itemElement.appendChild(label);
116
127
  itemElement.setAttribute('aria-label', config.label);
117
128
  }
@@ -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
+ };
@@ -6,6 +6,17 @@
6
6
 
7
7
  import { setAttributes } from './attributes';
8
8
  import { normalizeClasses } from '../utils';
9
+ import { PREFIX } from '../config';
10
+
11
+ /**
12
+ * Event handler function type
13
+ */
14
+ export type EventHandler = (event: Event) => void;
15
+
16
+ /**
17
+ * Event condition type - either a boolean or a function that returns a boolean
18
+ */
19
+ export type EventCondition = boolean | ((context: any, event: Event) => boolean);
9
20
 
10
21
  /**
11
22
  * Options for element creation
@@ -59,7 +70,7 @@ export interface CreateElementOptions {
59
70
  /**
60
71
  * Events to forward when component has emit method
61
72
  */
62
- forwardEvents?: Record<string, boolean | ((context: any, event: Event) => boolean)>;
73
+ forwardEvents?: Record<string, EventCondition>;
63
74
 
64
75
  /**
65
76
  * Callback after element creation
@@ -81,9 +92,12 @@ export interface CreateElementOptions {
81
92
  * Event handler storage to facilitate cleanup
82
93
  */
83
94
  export interface EventHandlerStorage {
84
- [eventName: string]: EventListener;
95
+ [eventName: string]: EventHandler;
85
96
  }
86
97
 
98
+ // Constant for prefix with dash
99
+ const PREFIX_WITH_DASH = `${PREFIX}-`;
100
+
87
101
  /**
88
102
  * Creates a DOM element with the specified options
89
103
  *
@@ -108,89 +122,78 @@ export const createElement = (options: CreateElementOptions = {}): HTMLElement =
108
122
 
109
123
  const element = document.createElement(tag);
110
124
 
111
- // Handle content
125
+ // Apply basic properties
112
126
  if (html) element.innerHTML = html;
113
127
  if (text) element.textContent = text;
114
128
  if (id) element.id = id;
115
129
 
116
130
  // Handle classes
117
131
  if (className) {
118
- const normalizedClasses = normalizeClasses(className);
119
- if (normalizedClasses.length) {
120
- element.classList.add(...normalizedClasses);
132
+ const classes = normalizeClasses(className);
133
+ if (classes.length) {
134
+ // Apply prefix to classes in a single operation
135
+ element.classList.add(...classes.map(cls =>
136
+ cls && !cls.startsWith(PREFIX_WITH_DASH) ? PREFIX_WITH_DASH + cls : cls
137
+ ).filter(Boolean));
121
138
  }
122
139
  }
123
140
 
124
- // Handle data attributes
125
- Object.entries(data).forEach(([key, value]) => {
126
- element.dataset[key] = value;
127
- });
141
+ // Handle data attributes directly
142
+ for (const key in data) {
143
+ element.dataset[key] = data[key];
144
+ }
128
145
 
129
- // Handle all other attributes
146
+ // Handle regular attributes
130
147
  const allAttrs = { ...attrs, ...rest };
131
- Object.entries(allAttrs).forEach(([key, value]) => {
148
+ for (const key in allAttrs) {
149
+ const value = allAttrs[key];
132
150
  if (value != null) element.setAttribute(key, String(value));
133
- });
134
-
135
- // Initialize event handler storage if not present
136
- if (!element.__eventHandlers) {
137
- element.__eventHandlers = {};
138
151
  }
139
152
 
140
- // Handle event forwarding if context has emit method or is a component with on method
153
+ // Handle event forwarding
141
154
  if (forwardEvents && (context?.emit || context?.on)) {
142
- Object.entries(forwardEvents).forEach(([nativeEvent, eventConfig]) => {
143
- // Create a wrapper handler function to evaluate condition and forward event
155
+ element.__eventHandlers = {};
156
+
157
+ for (const nativeEvent in forwardEvents) {
158
+ const eventConfig = forwardEvents[nativeEvent];
159
+
144
160
  const handler = (event: Event) => {
145
- // Determine if the event should be forwarded
146
161
  let shouldForward = true;
147
162
 
148
163
  if (typeof eventConfig === 'function') {
149
164
  try {
150
- // If it's a function, call with component context and event
151
- shouldForward = eventConfig({ ...context, element }, event);
165
+ // Create a lightweight context clone
166
+ const ctxWithElement = { ...context, element };
167
+ shouldForward = eventConfig(ctxWithElement, event);
152
168
  } catch (error) {
153
169
  console.warn(`Error in event condition for ${nativeEvent}:`, error);
154
170
  shouldForward = false;
155
171
  }
156
172
  } else {
157
- // If it's a boolean, use directly
158
173
  shouldForward = Boolean(eventConfig);
159
174
  }
160
175
 
161
- // Forward the event if condition passes
162
176
  if (shouldForward) {
163
177
  if (context.emit) {
164
178
  context.emit(nativeEvent, { event, element, originalEvent: event });
165
179
  } else if (context.on) {
166
- // This is a component with on method but no emit method
167
- // Dispatch a custom event that can be listened to
168
- const customEvent = new CustomEvent(nativeEvent, {
180
+ element.dispatchEvent(new CustomEvent(nativeEvent, {
169
181
  detail: { event, element, originalEvent: event },
170
182
  bubbles: true,
171
183
  cancelable: true
172
- });
173
- element.dispatchEvent(customEvent);
184
+ }));
174
185
  }
175
186
  }
176
187
  };
177
188
 
178
- // Store the handler for future removal
179
189
  element.__eventHandlers[nativeEvent] = handler;
180
-
181
- // Add the actual event listener
182
190
  element.addEventListener(nativeEvent, handler);
183
- });
191
+ }
184
192
  }
185
193
 
186
194
  // Append to container if provided
187
- if (container) {
188
- container.appendChild(element);
189
- }
190
-
191
- if (typeof onCreate === 'function') {
192
- onCreate(element, context);
193
- }
195
+ if (container) container.appendChild(element);
196
+ if (onCreate) onCreate(element, context);
194
197
 
195
198
  return element;
196
199
  };
@@ -200,10 +203,11 @@ export const createElement = (options: CreateElementOptions = {}): HTMLElement =
200
203
  * @param element - Element to cleanup
201
204
  */
202
205
  export const removeEventHandlers = (element: HTMLElement): void => {
203
- if (element.__eventHandlers) {
204
- Object.entries(element.__eventHandlers).forEach(([eventName, handler]) => {
205
- element.removeEventListener(eventName, handler);
206
- });
206
+ const handlers = element.__eventHandlers;
207
+ if (handlers) {
208
+ for (const event in handlers) {
209
+ element.removeEventListener(event, handlers[event]);
210
+ }
207
211
  delete element.__eventHandlers;
208
212
  }
209
213
  };
@@ -228,7 +232,9 @@ export const withClasses = (...classes: (string | string[])[]) =>
228
232
  (element: HTMLElement): HTMLElement => {
229
233
  const normalizedClasses = normalizeClasses(...classes);
230
234
  if (normalizedClasses.length) {
231
- element.classList.add(...normalizedClasses);
235
+ element.classList.add(...normalizedClasses.map(cls =>
236
+ cls && !cls.startsWith(PREFIX_WITH_DASH) ? PREFIX_WITH_DASH + cls : cls
237
+ ).filter(Boolean));
232
238
  }
233
239
  return element;
234
240
  };
@@ -240,17 +246,17 @@ export const withClasses = (...classes: (string | string[])[]) =>
240
246
  */
241
247
  export const withContent = (content: Node | string) =>
242
248
  (element: HTMLElement): HTMLElement => {
243
- if (content instanceof Node) {
244
- element.appendChild(content);
245
- } else {
246
- element.textContent = content;
247
- }
249
+ if (content instanceof Node) element.appendChild(content);
250
+ else element.textContent = content;
248
251
  return element;
249
252
  };
250
253
 
251
254
  // Extend HTMLElement interface to add eventHandlers property
252
255
  declare global {
253
256
  interface HTMLElement {
257
+ /**
258
+ * Storage for event handlers to enable cleanup
259
+ */
254
260
  __eventHandlers?: EventHandlerStorage;
255
261
  }
256
262
  }
@@ -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
  }
@@ -58,6 +58,9 @@
58
58
  --#{$prefix}-sys-color-on-info: #003060;
59
59
  --#{$prefix}-sys-color-on-info-rgb: 0, 48, 96;
60
60
 
61
+ // Include status colors for light theme
62
+ @include status-colors-light();
63
+
61
64
  &[data-theme-mode="dark"] {
62
65
  // Key colors
63
66
  --#{$prefix}-sys-color-primary: #DDB995; // Softer brown