mtrl 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/package.json +1 -1
  2. package/src/components/badge/_styles.scss +9 -9
  3. package/src/components/button/_styles.scss +0 -56
  4. package/src/components/button/button.ts +0 -2
  5. package/src/components/button/constants.ts +0 -6
  6. package/src/components/button/index.ts +2 -2
  7. package/src/components/button/types.ts +1 -7
  8. package/src/components/card/_styles.scss +67 -25
  9. package/src/components/card/api.ts +54 -3
  10. package/src/components/card/card.ts +33 -2
  11. package/src/components/card/config.ts +143 -21
  12. package/src/components/card/constants.ts +20 -19
  13. package/src/components/card/content.ts +299 -2
  14. package/src/components/card/features.ts +155 -4
  15. package/src/components/card/index.ts +31 -9
  16. package/src/components/card/types.ts +138 -15
  17. package/src/components/chip/chip.ts +1 -9
  18. package/src/components/chip/constants.ts +0 -10
  19. package/src/components/chip/index.ts +1 -1
  20. package/src/components/chip/types.ts +1 -4
  21. package/src/components/progress/_styles.scss +0 -65
  22. package/src/components/progress/config.ts +1 -2
  23. package/src/components/progress/constants.ts +0 -14
  24. package/src/components/progress/index.ts +1 -1
  25. package/src/components/progress/progress.ts +1 -4
  26. package/src/components/progress/types.ts +1 -4
  27. package/src/components/radios/_styles.scss +0 -45
  28. package/src/components/radios/api.ts +85 -60
  29. package/src/components/radios/config.ts +1 -2
  30. package/src/components/radios/constants.ts +0 -9
  31. package/src/components/radios/index.ts +1 -1
  32. package/src/components/radios/radio.ts +34 -11
  33. package/src/components/radios/radios.ts +2 -1
  34. package/src/components/radios/types.ts +1 -7
  35. package/src/components/slider/_styles.scss +149 -155
  36. package/src/components/slider/accessibility.md +59 -0
  37. package/src/components/slider/config.ts +4 -6
  38. package/src/components/slider/features/disabled.ts +41 -16
  39. package/src/components/slider/features/interactions.ts +153 -18
  40. package/src/components/slider/features/keyboard.ts +127 -6
  41. package/src/components/slider/features/structure.ts +32 -5
  42. package/src/components/slider/features/ui.ts +18 -8
  43. package/src/components/tabs/_styles.scss +285 -155
  44. package/src/components/tabs/api.ts +178 -400
  45. package/src/components/tabs/config.ts +46 -52
  46. package/src/components/tabs/constants.ts +85 -8
  47. package/src/components/tabs/features.ts +401 -0
  48. package/src/components/tabs/index.ts +60 -3
  49. package/src/components/tabs/indicator.ts +225 -0
  50. package/src/components/tabs/responsive.ts +144 -0
  51. package/src/components/tabs/scroll-indicators.ts +149 -0
  52. package/src/components/tabs/state.ts +186 -0
  53. package/src/components/tabs/tab-api.ts +258 -0
  54. package/src/components/tabs/tab.ts +255 -0
  55. package/src/components/tabs/tabs.ts +50 -31
  56. package/src/components/tabs/types.ts +324 -128
  57. package/src/components/tabs/utils.ts +107 -0
  58. package/src/components/textfield/_styles.scss +0 -98
  59. package/src/components/textfield/config.ts +2 -3
  60. package/src/components/textfield/constants.ts +0 -14
  61. package/src/components/textfield/index.ts +2 -2
  62. package/src/components/textfield/textfield.ts +0 -2
  63. package/src/components/textfield/types.ts +1 -4
  64. package/src/core/compose/component.ts +1 -1
  65. package/src/core/compose/features/badge.ts +79 -0
  66. package/src/core/compose/features/index.ts +3 -1
  67. package/src/styles/abstract/_theme.scss +106 -2
  68. package/src/components/card/actions.ts +0 -48
  69. package/src/components/card/header.ts +0 -88
  70. package/src/components/card/media.ts +0 -52
@@ -1,14 +1,14 @@
1
1
  // src/components/card/config.ts
2
2
  import {
3
3
  createComponentConfig,
4
- createElementConfig,
5
- BaseComponentConfig
4
+ createElementConfig
6
5
  } from '../../core/config/component-config';
7
6
  import { BaseComponent, CardSchema } from './types';
8
7
  import { CARD_VARIANTS, CARD_ELEVATIONS } from './constants';
9
8
 
10
9
  /**
11
10
  * Default configuration for the Card component
11
+ * @const {CardSchema}
12
12
  */
13
13
  export const defaultConfig: CardSchema = {
14
14
  variant: CARD_VARIANTS.ELEVATED,
@@ -18,8 +18,63 @@ export const defaultConfig: CardSchema = {
18
18
  draggable: false
19
19
  };
20
20
 
21
+ /**
22
+ * Initializes a card component with its configured elements in the correct order
23
+ *
24
+ * Creates and adds all configured elements to the card in the following order:
25
+ * 1. Top media elements (position='top')
26
+ * 2. Header element
27
+ * 3. Content elements
28
+ * 4. Bottom media elements (position='bottom')
29
+ * 5. Actions elements
30
+ *
31
+ * This ordering ensures that media appears before header when both are configured,
32
+ * maintaining proper visual hierarchy according to Material Design guidelines.
33
+ *
34
+ * @param {CardComponent} card - Card component to initialize
35
+ * @param {CardSchema} config - Card configuration
36
+ * @returns {CardComponent} Initialized card component
37
+ * @internal This is an internal utility for the Card component
38
+ */
39
+ export const initializeCardElements = (card: CardComponent, config: CardSchema): CardComponent => {
40
+ // 1. Add top media first
41
+ if (config.mediaConfig && (!config.mediaConfig.position || config.mediaConfig.position === 'top')) {
42
+ const { position, ...mediaConfigWithoutPosition } = config.mediaConfig;
43
+ const mediaElement = createCardMedia(mediaConfigWithoutPosition);
44
+ card.addMedia(mediaElement, 'top');
45
+ }
46
+
47
+ // 2. Add header AFTER top media
48
+ if (config.headerConfig) {
49
+ const headerElement = createCardHeader(config.headerConfig);
50
+ card.setHeader(headerElement);
51
+ }
52
+
53
+ // 3. Add content AFTER header
54
+ if (config.contentConfig) {
55
+ const contentElement = createCardContent(config.contentConfig);
56
+ card.addContent(contentElement);
57
+ }
58
+
59
+ // 4. Add bottom media AFTER content
60
+ if (config.mediaConfig && config.mediaConfig.position === 'bottom') {
61
+ const { position, ...mediaConfigWithoutPosition } = config.mediaConfig;
62
+ const mediaElement = createCardMedia(mediaConfigWithoutPosition);
63
+ card.addMedia(mediaElement, 'bottom');
64
+ }
65
+
66
+ // 5. Add actions LAST
67
+ if (config.actionsConfig) {
68
+ const actionsElement = createCardActions(config.actionsConfig);
69
+ card.setActions(actionsElement);
70
+ }
71
+
72
+ return card;
73
+ };
74
+
21
75
  /**
22
76
  * Creates the base configuration for Card component
77
+ *
23
78
  * @param {CardSchema} config - User provided configuration
24
79
  * @returns {CardSchema} Complete configuration with defaults applied
25
80
  */
@@ -28,31 +83,64 @@ export const createBaseConfig = (config: CardSchema = {}): CardSchema =>
28
83
 
29
84
  /**
30
85
  * Generates element configuration for the Card component
86
+ *
31
87
  * @param {CardSchema} config - Card configuration
32
88
  * @returns {Object} Element configuration object for withElement
33
89
  */
34
- export const getElementConfig = (config: CardSchema) =>
35
- createElementConfig(config, {
90
+ export const getElementConfig = (config: CardSchema) => {
91
+ const isInteractive = config.interactive || config.clickable;
92
+ const defaultRole = isInteractive ? 'button' : 'region';
93
+
94
+ // Prepare ARIA attributes
95
+ const ariaAttrs: Record<string, string> = {};
96
+ if (config.aria) {
97
+ // Add all ARIA attributes from config
98
+ Object.entries(config.aria).forEach(([key, value]) => {
99
+ if (value !== undefined) {
100
+ // Convert attribute name to aria-* format if not already
101
+ const attrName = key.startsWith('aria-') ? key : `aria-${key}`;
102
+ ariaAttrs[attrName] = value;
103
+ }
104
+ });
105
+ }
106
+
107
+ // Set default ARIA role if not specified
108
+ if (!ariaAttrs['role'] && !config.aria?.role) {
109
+ ariaAttrs['role'] = defaultRole;
110
+ }
111
+
112
+ // Add tabindex for interactive cards if not specified
113
+ if (isInteractive && !ariaAttrs['tabindex']) {
114
+ ariaAttrs['tabindex'] = '0';
115
+ }
116
+
117
+ return createElementConfig(config, {
36
118
  tag: 'div',
37
119
  className: [
38
120
  config.class,
39
121
  config.fullWidth ? `${config.prefix}-card--full-width` : null,
40
- config.interactive ? `${config.prefix}-card--interactive` : null
122
+ isInteractive ? `${config.prefix}-card--interactive` : null
41
123
  ],
124
+ attrs: ariaAttrs,
42
125
  forwardEvents: {
43
126
  click: (component: BaseComponent) => !!config.clickable,
44
- mouseenter: (component: BaseComponent) => !!config.interactive,
45
- mouseleave: (component: BaseComponent) => !!config.interactive
127
+ mouseenter: (component: BaseComponent) => !!isInteractive,
128
+ mouseleave: (component: BaseComponent) => !!isInteractive,
129
+ keydown: (component: BaseComponent) => !!isInteractive,
130
+ focus: (component: BaseComponent) => !!isInteractive,
131
+ blur: (component: BaseComponent) => !!isInteractive
46
132
  },
47
- interactive: config.interactive || config.clickable
133
+ interactive: isInteractive
48
134
  });
135
+ };
49
136
 
50
137
  /**
51
138
  * Creates API configuration for the Card component
139
+ *
52
140
  * @param {Object} comp - Component with lifecycle feature
53
141
  * @returns {Object} API configuration object
54
142
  */
55
- export const getApiConfig = (comp) => ({
143
+ export const getApiConfig = (comp: any) => ({
56
144
  lifecycle: {
57
145
  destroy: () => comp.lifecycle?.destroy?.()
58
146
  }
@@ -60,35 +148,69 @@ export const getApiConfig = (comp) => ({
60
148
 
61
149
  /**
62
150
  * Adds interactive behavior to card component
151
+ * Uses the MTRL elevation system for proper elevation levels
152
+ *
63
153
  * @param {BaseComponent} comp - Card component
64
154
  * @returns {BaseComponent} Enhanced card component
65
155
  */
66
156
  export const withInteractiveBehavior = (comp: BaseComponent): BaseComponent => {
67
- // Implement hover state elevation changes for interactive cards
68
- if (comp.config.interactive) {
157
+ const config = comp.config;
158
+ const isInteractive = config.interactive || config.clickable;
159
+
160
+ // Implement MD3 elevation changes for interactive cards
161
+ if (isInteractive) {
162
+ // Mouse interactions
69
163
  comp.element.addEventListener('mouseenter', () => {
70
- if (comp.config.variant === CARD_VARIANTS.ELEVATED) {
71
- comp.element.style.setProperty('--card-elevation', String(CARD_ELEVATIONS.HOVERED));
164
+ if (config.variant === CARD_VARIANTS.ELEVATED) {
165
+ comp.element.style.setProperty('--card-elevation', String(CARD_ELEVATIONS.LEVEL2));
72
166
  }
73
167
  });
74
168
 
75
169
  comp.element.addEventListener('mouseleave', () => {
76
- if (comp.config.variant === CARD_VARIANTS.ELEVATED) {
77
- comp.element.style.setProperty('--card-elevation', String(CARD_ELEVATIONS.RESTING));
170
+ if (config.variant === CARD_VARIANTS.ELEVATED) {
171
+ comp.element.style.setProperty('--card-elevation', String(CARD_ELEVATIONS.LEVEL1));
78
172
  }
79
173
  });
174
+
175
+ // Keyboard interactions for accessibility
176
+ comp.element.addEventListener('keydown', (e: KeyboardEvent) => {
177
+ // Activate on Enter or Space
178
+ if ((e.key === 'Enter' || e.key === ' ') && config.clickable) {
179
+ e.preventDefault();
180
+ comp.element.click();
181
+ }
182
+ });
183
+
184
+ // Focus state handling
185
+ comp.element.addEventListener('focus', () => {
186
+ comp.element.classList.add(`${comp.getClass('card')}--focused`);
187
+ });
188
+
189
+ comp.element.addEventListener('blur', () => {
190
+ comp.element.classList.remove(`${comp.getClass('card')}--focused`);
191
+ });
80
192
  }
81
193
 
82
- // Set up draggable
83
- if (comp.config.draggable) {
194
+ // Set up draggable behavior
195
+ if (config.draggable) {
84
196
  comp.element.setAttribute('draggable', 'true');
85
- comp.element.addEventListener('dragstart', (e) => {
86
- comp.element.style.setProperty('--card-elevation', String(CARD_ELEVATIONS.DRAGGED));
197
+
198
+ comp.element.addEventListener('dragstart', (e: DragEvent) => {
199
+ comp.element.style.setProperty('--card-elevation', String(CARD_ELEVATIONS.LEVEL4));
200
+ comp.element.classList.add(`${comp.getClass('card')}--dragging`);
87
201
  comp.emit?.('dragstart', { event: e });
202
+
203
+ // Ensure keyboard users can see what's being dragged
204
+ if (e.dataTransfer) {
205
+ // Set drag image and data
206
+ const cardTitle = comp.element.querySelector(`.${comp.getClass('card')}-header-title`)?.textContent || 'Card';
207
+ e.dataTransfer.setData('text/plain', cardTitle);
208
+ }
88
209
  });
89
210
 
90
- comp.element.addEventListener('dragend', (e) => {
91
- comp.element.style.setProperty('--card-elevation', String(CARD_ELEVATIONS.RESTING));
211
+ comp.element.addEventListener('dragend', (e: DragEvent) => {
212
+ comp.element.style.setProperty('--card-elevation', String(CARD_ELEVATIONS.LEVEL1));
213
+ comp.element.classList.remove(`${comp.getClass('card')}--dragging`);
92
214
  comp.emit?.('dragend', { event: e });
93
215
  });
94
216
  }
@@ -1,4 +1,5 @@
1
1
  // src/components/card/constants.ts
2
+
2
3
  import { CardVariant, CardElevation } from './types';
3
4
 
4
5
  /**
@@ -6,37 +7,33 @@ import { CardVariant, CardElevation } from './types';
6
7
  * @enum {string}
7
8
  */
8
9
  export const CARD_VARIANTS = {
10
+ /** Elevated card with shadow */
9
11
  ELEVATED: CardVariant.ELEVATED,
12
+ /** Filled card with higher surface container color */
10
13
  FILLED: CardVariant.FILLED,
14
+ /** Outlined card with border */
11
15
  OUTLINED: CardVariant.OUTLINED
12
16
  };
13
17
 
14
18
  /**
15
- * Card elevation levels
19
+ * Card elevation levels based on MD3 guidelines
20
+ * Uses the MTRL elevation system values
16
21
  * @enum {number}
17
22
  */
18
23
  export const CARD_ELEVATIONS = {
19
- RESTING: CardElevation.RESTING,
20
- HOVERED: CardElevation.HOVERED,
21
- DRAGGED: CardElevation.DRAGGED
24
+ /** No elevation (for filled and outlined variants) */
25
+ LEVEL0: CardElevation.LEVEL0,
26
+ /** Default elevation for elevated cards */
27
+ LEVEL1: CardElevation.LEVEL1,
28
+ /** Elevation for hovered state */
29
+ LEVEL2: CardElevation.LEVEL2,
30
+ /** Elevation for dragged state */
31
+ LEVEL4: CardElevation.LEVEL4
22
32
  };
23
33
 
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
-
38
34
  /**
39
- * Validation schema for card configuration
35
+ * Card validation schema
36
+ * @const {Object}
40
37
  */
41
38
  export const CARD_SCHEMA = {
42
39
  variant: {
@@ -79,5 +76,9 @@ export const CARD_SCHEMA = {
79
76
  mediaConfig: {
80
77
  type: 'object',
81
78
  required: false
79
+ },
80
+ aria: {
81
+ type: 'object',
82
+ required: false
82
83
  }
83
84
  };
@@ -3,11 +3,25 @@ import { PREFIX } from '../../core/config';
3
3
  import { pipe } from '../../core/compose';
4
4
  import { createBase, withElement } from '../../core/compose/component';
5
5
  import { CardContentConfig } from './types';
6
+ import { CARD_CONTENT_PADDING } from './constants';
6
7
 
7
8
  /**
8
9
  * Creates a card content component
10
+ *
9
11
  * @param {CardContentConfig} config - Content configuration
10
12
  * @returns {HTMLElement} Card content element
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * // Create text content
17
+ * const textContent = createCardContent({ text: 'Simple text content' });
18
+ *
19
+ * // Create HTML content with no padding
20
+ * const htmlContent = createCardContent({
21
+ * html: '<p>Formatted <strong>HTML</strong> content</p>',
22
+ * padding: false
23
+ * });
24
+ * ```
11
25
  */
12
26
  export const createCardContent = (config: CardContentConfig = {}): HTMLElement => {
13
27
  const baseConfig = {
@@ -17,6 +31,8 @@ export const createCardContent = (config: CardContentConfig = {}): HTMLElement =
17
31
  };
18
32
 
19
33
  try {
34
+ // Create element with innerHTML instead of html/text properties
35
+ // for more reliable content rendering
20
36
  const content = pipe(
21
37
  createBase,
22
38
  withElement({
@@ -26,11 +42,22 @@ export const createCardContent = (config: CardContentConfig = {}): HTMLElement =
26
42
  config.class,
27
43
  config.padding === false ? `${PREFIX}-card-content--no-padding` : null
28
44
  ],
29
- html: config.html,
30
- text: config.text
45
+ attrs: {
46
+ 'role': 'region',
47
+ // Add explicit style attributes to ensure visibility
48
+ 'style': 'display: block; color: inherit;'
49
+ }
31
50
  })
32
51
  )(baseConfig);
33
52
 
53
+ // Explicitly set the innerHTML for more reliable rendering
54
+ if (config.html) {
55
+ content.element.innerHTML = config.html;
56
+ } else if (config.text) {
57
+ // Wrap text in paragraph for proper formatting
58
+ content.element.innerHTML = `<p>${config.text}</p>`;
59
+ }
60
+
34
61
  // Add children if provided
35
62
  if (Array.isArray(config.children)) {
36
63
  config.children.forEach(child => {
@@ -40,9 +67,279 @@ export const createCardContent = (config: CardContentConfig = {}): HTMLElement =
40
67
  });
41
68
  }
42
69
 
70
+ // Add debug class to make troubleshooting easier
71
+ // Remove this in production
72
+ content.element.classList.add('debug-content');
73
+
43
74
  return content.element;
44
75
  } catch (error) {
45
76
  console.error('Card content creation error:', error instanceof Error ? error.message : String(error));
46
77
  throw new Error(`Failed to create card content: ${error instanceof Error ? error.message : String(error)}`);
47
78
  }
79
+ };
80
+
81
+ // src/components/card/header.ts
82
+ import { createElement } from '../../core/dom/create';
83
+
84
+ /**
85
+ * Creates a card header component
86
+ *
87
+ * @param {CardHeaderConfig} config - Header configuration
88
+ * @returns {HTMLElement} Card header element
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * // Create a header with title and subtitle
93
+ * const header = createCardHeader({
94
+ * title: 'Card Title',
95
+ * subtitle: 'Supporting text'
96
+ * });
97
+ *
98
+ * // Create a header with an avatar and action
99
+ * const avatarHeader = createCardHeader({
100
+ * title: 'User Profile',
101
+ * avatar: '<img src="user.jpg" alt="User avatar">',
102
+ * action: createIconButton({ icon: 'more_vert' })
103
+ * });
104
+ * ```
105
+ */
106
+ export const createCardHeader = (config: any = {}): HTMLElement => {
107
+ const baseConfig = {
108
+ ...config,
109
+ componentName: 'card-header',
110
+ prefix: PREFIX
111
+ };
112
+
113
+ try {
114
+ const header = pipe(
115
+ createBase,
116
+ withElement({
117
+ tag: 'div',
118
+ componentName: 'card-header',
119
+ className: config.class,
120
+ attrs: {
121
+ 'role': 'heading',
122
+ 'aria-level': '3' // Default heading level
123
+ }
124
+ })
125
+ )(baseConfig);
126
+
127
+ // Create text container for title and subtitle
128
+ const textContainer = createElement({
129
+ tag: 'div',
130
+ className: `${PREFIX}-card-header-text`,
131
+ container: header.element
132
+ });
133
+
134
+ // Add title if provided
135
+ if (config.title) {
136
+ createElement({
137
+ tag: 'h3',
138
+ className: `${PREFIX}-card-header-title`,
139
+ text: config.title,
140
+ container: textContainer,
141
+ attrs: {
142
+ id: `${header.element.id || 'card-header'}-title`
143
+ }
144
+ });
145
+
146
+ // Link the title ID to the card for accessibility if parent card exists
147
+ const parentCard = header.element.closest(`.${PREFIX}-card`);
148
+ if (parentCard && !parentCard.hasAttribute('aria-labelledby')) {
149
+ parentCard.setAttribute('aria-labelledby', `${header.element.id || 'card-header'}-title`);
150
+ }
151
+ }
152
+
153
+ // Add subtitle if provided
154
+ if (config.subtitle) {
155
+ createElement({
156
+ tag: 'h4',
157
+ className: `${PREFIX}-card-header-subtitle`,
158
+ text: config.subtitle,
159
+ container: textContainer
160
+ });
161
+ }
162
+
163
+ // Add avatar if provided
164
+ if (config.avatar) {
165
+ const avatarElement = typeof config.avatar === 'string'
166
+ ? createElement({
167
+ tag: 'div',
168
+ className: `${PREFIX}-card-header-avatar`,
169
+ html: config.avatar
170
+ })
171
+ : config.avatar;
172
+
173
+ // Ensure avatar has correct ARIA attributes if it's an image
174
+ const avatarImg = avatarElement.querySelector('img');
175
+ if (avatarImg && !avatarImg.hasAttribute('alt')) {
176
+ avatarImg.setAttribute('alt', ''); // Decorative image
177
+ avatarImg.setAttribute('aria-hidden', 'true');
178
+ }
179
+
180
+ header.element.insertBefore(avatarElement, header.element.firstChild);
181
+ }
182
+
183
+ // Add action if provided
184
+ if (config.action) {
185
+ const actionElement = typeof config.action === 'string'
186
+ ? createElement({
187
+ tag: 'div',
188
+ className: `${PREFIX}-card-header-action`,
189
+ html: config.action
190
+ })
191
+ : config.action;
192
+
193
+ header.element.appendChild(actionElement);
194
+ }
195
+
196
+ return header.element;
197
+ } catch (error) {
198
+ console.error('Card header creation error:', error instanceof Error ? error.message : String(error));
199
+ throw new Error(`Failed to create card header: ${error instanceof Error ? error.message : String(error)}`);
200
+ }
201
+ };
202
+
203
+ // src/components/card/actions.ts
204
+ /**
205
+ * Creates a card actions component
206
+ *
207
+ * @param {CardActionsConfig} config - Actions configuration
208
+ * @returns {HTMLElement} Card actions element
209
+ *
210
+ * @example
211
+ * ```typescript
212
+ * // Create simple actions container with buttons
213
+ * const actions = createCardActions({
214
+ * actions: [
215
+ * createButton({ text: 'Cancel' }),
216
+ * createButton({ text: 'OK', variant: 'filled' })
217
+ * ],
218
+ * align: 'end'
219
+ * });
220
+ *
221
+ * // Create full-bleed actions
222
+ * const fullBleedActions = createCardActions({
223
+ * actions: [createButton({ text: 'View Details', fullWidth: true })],
224
+ * fullBleed: true
225
+ * });
226
+ * ```
227
+ */
228
+ export const createCardActions = (config: any = {}): HTMLElement => {
229
+ const baseConfig = {
230
+ ...config,
231
+ componentName: 'card-actions',
232
+ prefix: PREFIX
233
+ };
234
+
235
+ try {
236
+ const actions = pipe(
237
+ createBase,
238
+ withElement({
239
+ tag: 'div',
240
+ componentName: 'card-actions',
241
+ className: [
242
+ config.class,
243
+ config.fullBleed ? `${PREFIX}-card-actions--full-bleed` : null,
244
+ config.vertical ? `${PREFIX}-card-actions--vertical` : null,
245
+ config.align ? `${PREFIX}-card-actions--${config.align}` : null
246
+ ],
247
+ attrs: {
248
+ 'role': 'group' // Semantically group actions together
249
+ }
250
+ })
251
+ )(baseConfig);
252
+
253
+ // Add action elements if provided
254
+ if (Array.isArray(config.actions)) {
255
+ config.actions.forEach((action, index) => {
256
+ if (action instanceof HTMLElement) {
257
+ // Ensure each action has accessible attributes
258
+ if (!action.hasAttribute('aria-label') &&
259
+ !action.hasAttribute('aria-labelledby') &&
260
+ action.textContent?.trim() === '') {
261
+ action.setAttribute('aria-label', `Action ${index + 1}`);
262
+ }
263
+
264
+ actions.element.appendChild(action);
265
+ }
266
+ });
267
+ }
268
+
269
+ return actions.element;
270
+ } catch (error) {
271
+ console.error('Card actions creation error:', error instanceof Error ? error.message : String(error));
272
+ throw new Error(`Failed to create card actions: ${error instanceof Error ? error.message : String(error)}`);
273
+ }
274
+ };
275
+
276
+ // src/components/card/media.ts
277
+ /**
278
+ * Creates a card media component
279
+ *
280
+ * @param {CardMediaConfig} config - Media configuration
281
+ * @returns {HTMLElement} Card media element
282
+ *
283
+ * @example
284
+ * ```typescript
285
+ * // Create a media component with an image
286
+ * const media = createCardMedia({
287
+ * src: 'image.jpg',
288
+ * alt: 'Descriptive alt text',
289
+ * aspectRatio: '16:9'
290
+ * });
291
+ *
292
+ * // Create a media component with a custom element
293
+ * const customMedia = createCardMedia({
294
+ * element: videoElement,
295
+ * aspectRatio: '4:3'
296
+ * });
297
+ * ```
298
+ */
299
+ export const createCardMedia = (config: any = {}): HTMLElement => {
300
+ const baseConfig = {
301
+ ...config,
302
+ componentName: 'card-media',
303
+ prefix: PREFIX
304
+ };
305
+
306
+ try {
307
+ const media = pipe(
308
+ createBase,
309
+ withElement({
310
+ tag: 'div',
311
+ componentName: 'card-media',
312
+ className: [
313
+ config.class,
314
+ config.aspectRatio ? `${PREFIX}-card-media--${config.aspectRatio.replace(':', '-')}` : null,
315
+ config.contain ? `${PREFIX}-card-media--contain` : null
316
+ ]
317
+ })
318
+ )(baseConfig);
319
+
320
+ // If custom element is provided, use it
321
+ if (config.element instanceof HTMLElement) {
322
+ media.element.appendChild(config.element);
323
+ }
324
+ // Otherwise create an image if src is provided
325
+ else if (config.src) {
326
+ const img = document.createElement('img');
327
+ img.src = config.src;
328
+ img.className = `${PREFIX}-card-media-img`;
329
+
330
+ // Ensure alt text is always provided for accessibility
331
+ img.alt = config.alt || '';
332
+ if (!config.alt) {
333
+ // If no alt text is provided, mark as decorative
334
+ img.setAttribute('aria-hidden', 'true');
335
+ }
336
+
337
+ media.element.appendChild(img);
338
+ }
339
+
340
+ return media.element;
341
+ } catch (error) {
342
+ console.error('Card media creation error:', error instanceof Error ? error.message : String(error));
343
+ throw new Error(`Failed to create card media: ${error instanceof Error ? error.message : String(error)}`);
344
+ }
48
345
  };