mtrl 0.2.8 → 0.2.9

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 (42) hide show
  1. package/index.ts +2 -0
  2. package/package.json +1 -1
  3. package/src/components/navigation/api.ts +131 -96
  4. package/src/components/navigation/features/controller.ts +273 -0
  5. package/src/components/navigation/features/items.ts +133 -64
  6. package/src/components/navigation/navigation.ts +17 -2
  7. package/src/components/navigation/system-types.ts +124 -0
  8. package/src/components/navigation/system.ts +776 -0
  9. package/src/components/slider/config.ts +20 -2
  10. package/src/components/slider/features/controller.ts +761 -0
  11. package/src/components/slider/features/handlers.ts +18 -15
  12. package/src/components/slider/features/index.ts +3 -2
  13. package/src/components/slider/features/range.ts +104 -0
  14. package/src/components/slider/slider.ts +34 -14
  15. package/src/components/slider/structure.ts +152 -0
  16. package/src/components/textfield/api.ts +53 -0
  17. package/src/components/textfield/features.ts +322 -0
  18. package/src/components/textfield/textfield.ts +8 -0
  19. package/src/components/textfield/types.ts +12 -3
  20. package/src/components/timepicker/clockdial.ts +1 -4
  21. package/src/core/compose/features/textinput.ts +15 -2
  22. package/src/core/composition/features/dom.ts +33 -0
  23. package/src/core/composition/features/icon.ts +131 -0
  24. package/src/core/composition/features/index.ts +11 -0
  25. package/src/core/composition/features/label.ts +156 -0
  26. package/src/core/composition/features/structure.ts +22 -0
  27. package/src/core/composition/index.ts +26 -0
  28. package/src/core/index.ts +1 -1
  29. package/src/core/structure.ts +288 -0
  30. package/src/index.ts +1 -0
  31. package/src/styles/components/_navigation-mobile.scss +244 -0
  32. package/src/styles/components/_navigation-system.scss +151 -0
  33. package/src/styles/components/_textfield.scss +250 -11
  34. package/demo/build.ts +0 -349
  35. package/demo/index.html +0 -110
  36. package/demo/main.js +0 -448
  37. package/demo/styles.css +0 -239
  38. package/server.ts +0 -86
  39. package/src/components/slider/features/slider.ts +0 -318
  40. package/src/components/slider/features/structure.ts +0 -181
  41. package/src/components/slider/features/ui.ts +0 -388
  42. package/src/components/textfield/constants.ts +0 -100
@@ -0,0 +1,322 @@
1
+ // src/components/textfield/features.ts
2
+ import { BaseComponent, ElementComponent } from '../../core/compose/component';
3
+
4
+ /**
5
+ * Configuration for leading icon feature
6
+ */
7
+ export interface LeadingIconConfig {
8
+ /**
9
+ * Leading icon HTML content
10
+ */
11
+ leadingIcon?: string;
12
+
13
+ /**
14
+ * CSS class prefix
15
+ */
16
+ prefix?: string;
17
+
18
+ /**
19
+ * Component name
20
+ */
21
+ componentName?: string;
22
+
23
+ [key: string]: any;
24
+ }
25
+
26
+ /**
27
+ * Configuration for trailing icon feature
28
+ */
29
+ export interface TrailingIconConfig {
30
+ /**
31
+ * Trailing icon HTML content
32
+ */
33
+ trailingIcon?: string;
34
+
35
+ /**
36
+ * CSS class prefix
37
+ */
38
+ prefix?: string;
39
+
40
+ /**
41
+ * Component name
42
+ */
43
+ componentName?: string;
44
+
45
+ [key: string]: any;
46
+ }
47
+
48
+ /**
49
+ * Configuration for supporting text feature
50
+ */
51
+ export interface SupportingTextConfig {
52
+ /**
53
+ * Supporting text content
54
+ */
55
+ supportingText?: string;
56
+
57
+ /**
58
+ * Whether supporting text indicates an error
59
+ */
60
+ error?: boolean;
61
+
62
+ /**
63
+ * CSS class prefix
64
+ */
65
+ prefix?: string;
66
+
67
+ /**
68
+ * Component name
69
+ */
70
+ componentName?: string;
71
+
72
+ [key: string]: any;
73
+ }
74
+
75
+ /**
76
+ * Component with leading icon capabilities
77
+ */
78
+ export interface LeadingIconComponent extends BaseComponent {
79
+ /**
80
+ * Leading icon element
81
+ */
82
+ leadingIcon: HTMLElement | null;
83
+
84
+ /**
85
+ * Sets leading icon content
86
+ * @param html - HTML content for the icon
87
+ * @returns Component instance for chaining
88
+ */
89
+ setLeadingIcon: (html: string) => LeadingIconComponent;
90
+
91
+ /**
92
+ * Removes leading icon
93
+ * @returns Component instance for chaining
94
+ */
95
+ removeLeadingIcon: () => LeadingIconComponent;
96
+ }
97
+
98
+ /**
99
+ * Component with trailing icon capabilities
100
+ */
101
+ export interface TrailingIconComponent extends BaseComponent {
102
+ /**
103
+ * Trailing icon element
104
+ */
105
+ trailingIcon: HTMLElement | null;
106
+
107
+ /**
108
+ * Sets trailing icon content
109
+ * @param html - HTML content for the icon
110
+ * @returns Component instance for chaining
111
+ */
112
+ setTrailingIcon: (html: string) => TrailingIconComponent;
113
+
114
+ /**
115
+ * Removes trailing icon
116
+ * @returns Component instance for chaining
117
+ */
118
+ removeTrailingIcon: () => TrailingIconComponent;
119
+ }
120
+
121
+ /**
122
+ * Component with supporting text capabilities
123
+ */
124
+ export interface SupportingTextComponent extends BaseComponent {
125
+ /**
126
+ * Supporting text element
127
+ */
128
+ supportingTextElement: HTMLElement | null;
129
+
130
+ /**
131
+ * Sets supporting text content
132
+ * @param text - Text content
133
+ * @param isError - Whether text represents an error
134
+ * @returns Component instance for chaining
135
+ */
136
+ setSupportingText: (text: string, isError?: boolean) => SupportingTextComponent;
137
+
138
+ /**
139
+ * Removes supporting text
140
+ * @returns Component instance for chaining
141
+ */
142
+ removeSupportingText: () => SupportingTextComponent;
143
+ }
144
+
145
+ /**
146
+ * Creates and manages a leading icon for a component
147
+ * @param config - Configuration object with leading icon settings
148
+ * @returns Function that enhances a component with leading icon functionality
149
+ */
150
+ export const withLeadingIcon = <T extends LeadingIconConfig>(config: T) =>
151
+ <C extends ElementComponent>(component: C): C & LeadingIconComponent => {
152
+ if (!config.leadingIcon) {
153
+ return component as C & LeadingIconComponent;
154
+ }
155
+
156
+ // Create icon element
157
+ const PREFIX = config.prefix || 'mtrl';
158
+ const iconElement = document.createElement('span');
159
+ iconElement.className = `${PREFIX}-${config.componentName || 'textfield'}-leading-icon`;
160
+ iconElement.innerHTML = config.leadingIcon;
161
+
162
+ // Add leading icon to the component
163
+ component.element.appendChild(iconElement);
164
+
165
+ // Add leading-icon class to the component
166
+ component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-leading-icon`);
167
+
168
+ // When there's a leading icon, adjust input padding
169
+ if (component.input) {
170
+ component.input.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-input--with-leading-icon`);
171
+ }
172
+
173
+ // Add lifecycle integration if available
174
+ if ('lifecycle' in component && component.lifecycle?.destroy) {
175
+ const originalDestroy = component.lifecycle.destroy;
176
+ component.lifecycle.destroy = () => {
177
+ iconElement.remove();
178
+ originalDestroy.call(component.lifecycle);
179
+ };
180
+ }
181
+
182
+ return {
183
+ ...component,
184
+ leadingIcon: iconElement,
185
+
186
+ setLeadingIcon(html: string) {
187
+ iconElement.innerHTML = html;
188
+ return this;
189
+ },
190
+
191
+ removeLeadingIcon() {
192
+ if (iconElement.parentNode) {
193
+ iconElement.remove();
194
+ component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-leading-icon`);
195
+ if (component.input) {
196
+ component.input.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}-input--with-leading-icon`);
197
+ }
198
+ this.leadingIcon = null;
199
+ }
200
+ return this;
201
+ }
202
+ };
203
+ };
204
+
205
+ /**
206
+ * Creates and manages a trailing icon for a component
207
+ * @param config - Configuration object with trailing icon settings
208
+ * @returns Function that enhances a component with trailing icon functionality
209
+ */
210
+ export const withTrailingIcon = <T extends TrailingIconConfig>(config: T) =>
211
+ <C extends ElementComponent>(component: C): C & TrailingIconComponent => {
212
+ if (!config.trailingIcon) {
213
+ return component as C & TrailingIconComponent;
214
+ }
215
+
216
+ // Create icon element
217
+ const PREFIX = config.prefix || 'mtrl';
218
+ const iconElement = document.createElement('span');
219
+ iconElement.className = `${PREFIX}-${config.componentName || 'textfield'}-trailing-icon`;
220
+ iconElement.innerHTML = config.trailingIcon;
221
+
222
+ // Add trailing icon to the component
223
+ component.element.appendChild(iconElement);
224
+
225
+ // Add trailing-icon class to the component
226
+ component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-trailing-icon`);
227
+
228
+ // When there's a trailing icon, adjust input padding
229
+ if (component.input) {
230
+ component.input.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-input--with-trailing-icon`);
231
+ }
232
+
233
+ // Add lifecycle integration if available
234
+ if ('lifecycle' in component && component.lifecycle?.destroy) {
235
+ const originalDestroy = component.lifecycle.destroy;
236
+ component.lifecycle.destroy = () => {
237
+ iconElement.remove();
238
+ originalDestroy.call(component.lifecycle);
239
+ };
240
+ }
241
+
242
+ return {
243
+ ...component,
244
+ trailingIcon: iconElement,
245
+
246
+ setTrailingIcon(html: string) {
247
+ iconElement.innerHTML = html;
248
+ return this;
249
+ },
250
+
251
+ removeTrailingIcon() {
252
+ if (iconElement.parentNode) {
253
+ iconElement.remove();
254
+ component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-trailing-icon`);
255
+ if (component.input) {
256
+ component.input.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}-input--with-trailing-icon`);
257
+ }
258
+ this.trailingIcon = null;
259
+ }
260
+ return this;
261
+ }
262
+ };
263
+ };
264
+
265
+ /**
266
+ * Creates and manages supporting text for a component
267
+ * @param config - Configuration object with supporting text settings
268
+ * @returns Function that enhances a component with supporting text functionality
269
+ */
270
+ export const withSupportingText = <T extends SupportingTextConfig>(config: T) =>
271
+ <C extends ElementComponent>(component: C): C & SupportingTextComponent => {
272
+ if (!config.supportingText) {
273
+ return component as C & SupportingTextComponent;
274
+ }
275
+
276
+ // Create supporting text element
277
+ const PREFIX = config.prefix || 'mtrl';
278
+ const supportingElement = document.createElement('div');
279
+ supportingElement.className = `${PREFIX}-${config.componentName || 'textfield'}-helper`;
280
+ supportingElement.textContent = config.supportingText;
281
+
282
+ if (config.error) {
283
+ supportingElement.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-helper--error`);
284
+ component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--error`);
285
+ }
286
+
287
+ // Add supporting text to the component
288
+ component.element.appendChild(supportingElement);
289
+
290
+ // Add lifecycle integration if available
291
+ if ('lifecycle' in component && component.lifecycle?.destroy) {
292
+ const originalDestroy = component.lifecycle.destroy;
293
+ component.lifecycle.destroy = () => {
294
+ supportingElement.remove();
295
+ originalDestroy.call(component.lifecycle);
296
+ };
297
+ }
298
+
299
+ return {
300
+ ...component,
301
+ supportingTextElement: supportingElement,
302
+
303
+ setSupportingText(text: string, isError = false) {
304
+ supportingElement.textContent = text;
305
+
306
+ // Handle error state
307
+ supportingElement.classList.toggle(`${PREFIX}-${config.componentName || 'textfield'}-helper--error`, isError);
308
+ component.element.classList.toggle(`${PREFIX}-${config.componentName || 'textfield'}--error`, isError);
309
+
310
+ return this;
311
+ },
312
+
313
+ removeSupportingText() {
314
+ if (supportingElement.parentNode) {
315
+ supportingElement.remove();
316
+ this.supportingTextElement = null;
317
+ component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--error`);
318
+ }
319
+ return this;
320
+ }
321
+ };
322
+ };
@@ -10,6 +10,11 @@ import {
10
10
  withTextLabel
11
11
  } from '../../core/compose/features';
12
12
  import { withAPI } from './api';
13
+ import {
14
+ withLeadingIcon,
15
+ withTrailingIcon,
16
+ withSupportingText
17
+ } from './features';
13
18
  import { TextfieldConfig, TextfieldComponent } from './types';
14
19
  import {
15
20
  createBaseConfig,
@@ -33,6 +38,9 @@ const createTextfield = (config: TextfieldConfig = {}): TextfieldComponent => {
33
38
  withVariant(baseConfig),
34
39
  withTextInput(baseConfig),
35
40
  withTextLabel(baseConfig),
41
+ withLeadingIcon(baseConfig),
42
+ withTrailingIcon(baseConfig),
43
+ withSupportingText(baseConfig),
36
44
  withDisabled(baseConfig),
37
45
  withLifecycle(),
38
46
  comp => withAPI(getApiConfig(comp))(comp)
@@ -31,9 +31,6 @@ export interface TextfieldConfig {
31
31
  /** Label text */
32
32
  label?: string;
33
33
 
34
- /** Placeholder text */
35
- placeholder?: string;
36
-
37
34
  /** Initial value */
38
35
  value?: string;
39
36
 
@@ -52,6 +49,18 @@ export interface TextfieldConfig {
52
49
  /** Autocomplete attribute */
53
50
  autocomplete?: string;
54
51
 
52
+ /** Leading icon HTML content */
53
+ leadingIcon?: string;
54
+
55
+ /** Trailing icon HTML content */
56
+ trailingIcon?: string;
57
+
58
+ /** Supporting text content */
59
+ supportingText?: string;
60
+
61
+ /** Whether supporting text indicates an error */
62
+ error?: boolean;
63
+
55
64
  /** Additional CSS classes */
56
65
  class?: string;
57
66
 
@@ -48,12 +48,9 @@ const CLOCK_CONSTANTS = {
48
48
  function getThemeColors(prefix: string): ThemeColors {
49
49
  const root = document.documentElement;
50
50
  const styles = getComputedStyle(root);
51
- console.log('styles', styles)
51
+
52
52
  // Extract primary color
53
53
  const primaryColor = styles.getPropertyValue(`--${prefix}-sys-color-primary`).trim() || '#6750A4';
54
-
55
-
56
- console.log('primaryColor', primaryColor)
57
54
 
58
55
  // Extract on-primary color
59
56
  const onPrimaryColor = styles.getPropertyValue(`--${prefix}-sys-color-on-primary`).trim() || '#FFFFFF';
@@ -112,14 +112,14 @@ export interface TextInputComponent extends ElementComponent {
112
112
  */
113
113
  export const withTextInput = <T extends TextInputConfig>(config: T = {} as T) =>
114
114
  <C extends ElementComponent>(component: C): C & TextInputComponent => {
115
- const input = document.createElement(config.multiline ? 'textarea' : 'input') as
115
+ const isMultiline = config.multiline || config.type === 'multiline';
116
+ const input = document.createElement(isMultiline ? 'textarea' : 'input') as
116
117
  HTMLInputElement | HTMLTextAreaElement;
117
118
 
118
119
  input.className = `${component.getClass('textfield')}-input`;
119
120
 
120
121
  // Set input attributes
121
122
  const attributes: Record<string, string | number | boolean | undefined> = {
122
- type: config.multiline ? undefined : (config.type || 'text'),
123
123
  name: config.name,
124
124
  required: config.required,
125
125
  disabled: config.disabled,
@@ -129,6 +129,14 @@ export const withTextInput = <T extends TextInputConfig>(config: T = {} as T) =>
129
129
  value: config.value || ''
130
130
  };
131
131
 
132
+ // Only set type attribute for input elements, not for textarea
133
+ if (!isMultiline) {
134
+ attributes.type = config.type || 'text';
135
+ } else {
136
+ // For textarea, add a data attribute to identify it as multiline
137
+ attributes['data-type'] = 'multiline';
138
+ }
139
+
132
140
  Object.entries(attributes).forEach(([key, value]) => {
133
141
  if (value !== null && value !== undefined) {
134
142
  if (typeof value === 'boolean') {
@@ -188,6 +196,11 @@ export const withTextInput = <T extends TextInputConfig>(config: T = {} as T) =>
188
196
  // Initial state
189
197
  updateInputState();
190
198
 
199
+ // Add multiline class to the component if it's a textarea
200
+ if (isMultiline) {
201
+ component.element.classList.add(`${component.getClass('textfield')}--multiline`);
202
+ }
203
+
191
204
  component.element.appendChild(input);
192
205
 
193
206
  // Cleanup
@@ -0,0 +1,33 @@
1
+ // src/core/composition/features/dom.ts
2
+ import { createStructure, flattenStructure } from '../../structure';
3
+
4
+ /**
5
+ * Creates DOM elements from structure definition using the core createStructure utility
6
+ * This is a key feature that bridges the gap between declarative structure and actual DOM
7
+ *
8
+ * @returns Component enhancer that creates DOM structure
9
+ */
10
+ export const withDom = () => component => {
11
+ // Return unmodified component if no structure definition
12
+ if (!component.structureDefinition) {
13
+ return component;
14
+ }
15
+
16
+ try {
17
+ // Use the existing createStructure function to build the DOM
18
+ const structure = createStructure(component.structureDefinition);
19
+
20
+ // Use the existing flattenStructure function to create a flat reference map
21
+ const components = structure.getAll();
22
+
23
+ // Return enhanced component with DOM structure
24
+ return {
25
+ ...component,
26
+ element: structure.element, // Root element
27
+ components // All component elements
28
+ };
29
+ } catch (error) {
30
+ console.error('Failed to create DOM structure:', error);
31
+ throw new Error(`Failed to create slider DOM: ${error.message}`);
32
+ }
33
+ };
@@ -0,0 +1,131 @@
1
+ // src/core/composition/features/icon.ts
2
+ import { createElement } from '../../dom/create';
3
+
4
+ /**
5
+ * Configuration for icon feature
6
+ */
7
+ export interface IconConfig {
8
+ /**
9
+ * Icon HTML content
10
+ */
11
+ icon?: string;
12
+
13
+ /**
14
+ * Position of the icon ('start' or 'end')
15
+ */
16
+ iconPosition?: 'start' | 'end';
17
+
18
+ /**
19
+ * Size variant for the icon
20
+ */
21
+ iconSize?: string;
22
+
23
+ /**
24
+ * CSS class prefix
25
+ */
26
+ prefix?: string;
27
+
28
+ /**
29
+ * Component name for class generation
30
+ */
31
+ componentName?: string;
32
+
33
+ [key: string]: any;
34
+ }
35
+
36
+ /**
37
+ * Enhances structure definition with an icon element
38
+ * Unlike the traditional withIcon, this modifies the structure definition
39
+ * without creating actual DOM elements.
40
+ *
41
+ * @param config Configuration containing icon information
42
+ * @returns Component enhancer that adds icon to structure definition
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * // Add icon to a structure definition
47
+ * const component = pipe(
48
+ * createBase,
49
+ * withStructure(config),
50
+ * withIcon(config)
51
+ * )(config);
52
+ * ```
53
+ */
54
+ export const withIcon = (config: IconConfig) => component => {
55
+ // If no icon or missing structure definition, return unmodified
56
+ if (!config.icon || !component.structureDefinition) {
57
+ return component;
58
+ }
59
+
60
+ try {
61
+ // Get component details for class names
62
+ const prefix = config.prefix || component.config?.prefix || 'mtrl';
63
+ const componentName = config.componentName || component.componentName || 'component';
64
+
65
+ // Clone the structure definition
66
+ const structureDefinition = JSON.parse(JSON.stringify(component.structureDefinition));
67
+
68
+ // Determine icon position
69
+ const position = config.iconPosition || 'start';
70
+
71
+ // Add the --icon modifier class to the main element
72
+ const elementClasses = structureDefinition.element.options.className || [];
73
+ const iconModifierClass = `${prefix}-${componentName}--icon`;
74
+
75
+ if (Array.isArray(elementClasses)) {
76
+ if (!elementClasses.includes(iconModifierClass)) {
77
+ elementClasses.push(iconModifierClass);
78
+ }
79
+ } else if (typeof elementClasses === 'string') {
80
+ if (!elementClasses.includes(iconModifierClass)) {
81
+ structureDefinition.element.options.className = `${elementClasses} ${iconModifierClass}`.trim();
82
+ }
83
+ } else {
84
+ structureDefinition.element.options.className = [iconModifierClass];
85
+ }
86
+
87
+ // Create icon element definition with component-specific class
88
+ const iconDef = {
89
+ name: 'icon',
90
+ creator: createElement,
91
+ options: {
92
+ tag: 'span',
93
+ className: [
94
+ `${prefix}-${componentName}-icon`,
95
+ `${prefix}-${componentName}-icon--${position}`
96
+ ],
97
+ html: config.icon
98
+ }
99
+ };
100
+
101
+ // Add size class if specified
102
+ if (config.iconSize) {
103
+ const classes = iconDef.options.className;
104
+ if (Array.isArray(classes)) {
105
+ classes.push(`${prefix}-${componentName}-icon--${config.iconSize}`);
106
+ }
107
+ }
108
+
109
+ // Add icon directly to the main element's children
110
+ if (position === 'start') {
111
+ // Create new children object with icon first
112
+ const existingChildren = { ...structureDefinition.element.children };
113
+ structureDefinition.element.children = {
114
+ icon: iconDef,
115
+ ...existingChildren
116
+ };
117
+ } else {
118
+ // Add icon after existing children
119
+ structureDefinition.element.children.icon = iconDef;
120
+ }
121
+
122
+ // Return component with updated structure definition
123
+ return {
124
+ ...component,
125
+ structureDefinition
126
+ };
127
+ } catch (error) {
128
+ console.warn('Error enhancing structure with icon:', error);
129
+ return component;
130
+ }
131
+ };
@@ -0,0 +1,11 @@
1
+ // src/core/composition/features/index.ts
2
+
3
+ // Export composition features
4
+ export { withIcon } from './icon';
5
+ export { withLabel } from './label';
6
+ export { withDom } from './dom';
7
+ export { withStructure } from './structure';
8
+
9
+ // Re-export interface types for better developer experience
10
+ export type { IconConfig } from './icon';
11
+ export type { LabelConfig } from './label';