mtrl 0.3.6 → 0.3.8

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 (45) hide show
  1. package/package.json +2 -2
  2. package/src/components/button/api.ts +16 -0
  3. package/src/components/button/types.ts +9 -0
  4. package/src/components/menu/api.ts +61 -22
  5. package/src/components/menu/config.ts +10 -8
  6. package/src/components/menu/features/anchor.ts +254 -19
  7. package/src/components/menu/features/controller.ts +724 -271
  8. package/src/components/menu/features/index.ts +11 -2
  9. package/src/components/menu/features/position.ts +353 -0
  10. package/src/components/menu/index.ts +5 -5
  11. package/src/components/menu/menu.ts +21 -61
  12. package/src/components/menu/types.ts +30 -16
  13. package/src/components/select/api.ts +78 -0
  14. package/src/components/select/config.ts +76 -0
  15. package/src/components/select/features.ts +331 -0
  16. package/src/components/select/index.ts +38 -0
  17. package/src/components/select/select.ts +73 -0
  18. package/src/components/select/types.ts +355 -0
  19. package/src/components/textfield/api.ts +78 -6
  20. package/src/components/textfield/features/index.ts +17 -0
  21. package/src/components/textfield/features/leading-icon.ts +127 -0
  22. package/src/components/textfield/features/placement.ts +149 -0
  23. package/src/components/textfield/features/prefix-text.ts +107 -0
  24. package/src/components/textfield/features/suffix-text.ts +100 -0
  25. package/src/components/textfield/features/supporting-text.ts +113 -0
  26. package/src/components/textfield/features/trailing-icon.ts +108 -0
  27. package/src/components/textfield/textfield.ts +51 -15
  28. package/src/components/textfield/types.ts +70 -0
  29. package/src/core/collection/adapters/base.ts +62 -0
  30. package/src/core/collection/collection.ts +300 -0
  31. package/src/core/collection/index.ts +57 -0
  32. package/src/core/collection/list-manager.ts +333 -0
  33. package/src/index.ts +4 -45
  34. package/src/styles/abstract/_variables.scss +18 -0
  35. package/src/styles/components/_button.scss +21 -5
  36. package/src/styles/components/{_chip.scss → _chips.scss} +118 -4
  37. package/src/styles/components/_menu.scss +97 -24
  38. package/src/styles/components/_select.scss +272 -0
  39. package/src/styles/components/_textfield.scss +233 -42
  40. package/src/styles/main.scss +2 -1
  41. package/src/components/textfield/features.ts +0 -322
  42. package/src/core/collection/adapters/base.js +0 -26
  43. package/src/core/collection/collection.js +0 -259
  44. package/src/core/collection/list-manager.js +0 -157
  45. /package/src/core/collection/adapters/{route.js → route.ts} +0 -0
@@ -1,322 +0,0 @@
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
- };
@@ -1,26 +0,0 @@
1
- // src/core/collection/adapters/base.js
2
-
3
- export const OPERATORS = {
4
- EQ: 'eq',
5
- NE: 'ne',
6
- GT: 'gt',
7
- GTE: 'gte',
8
- LT: 'lt',
9
- LTE: 'lte',
10
- IN: 'in',
11
- NIN: 'nin',
12
- CONTAINS: 'contains',
13
- STARTS_WITH: 'startsWith',
14
- ENDS_WITH: 'endsWith'
15
- }
16
-
17
- export const createBaseAdapter = ({ onError } = {}) => {
18
- const handleError = (error, context) => {
19
- onError?.(error, context)
20
- throw error
21
- }
22
-
23
- return {
24
- handleError
25
- }
26
- }
@@ -1,259 +0,0 @@
1
- // src/core/collection/collection.js
2
-
3
- /**
4
- * Event types for collection changes
5
- */
6
- export const COLLECTION_EVENTS = {
7
- CHANGE: 'change',
8
- ADD: 'add',
9
- UPDATE: 'update',
10
- REMOVE: 'remove',
11
- ERROR: 'error',
12
- LOADING: 'loading'
13
- }
14
-
15
- /**
16
- * Query operators for filtering
17
- */
18
- export const OPERATORS = {
19
- EQ: 'eq',
20
- NE: 'ne',
21
- GT: 'gt',
22
- GTE: 'gte',
23
- LT: 'lt',
24
- LTE: 'lte',
25
- IN: 'in',
26
- NIN: 'nin',
27
- CONTAINS: 'contains',
28
- STARTS_WITH: 'startsWith',
29
- ENDS_WITH: 'endsWith'
30
- }
31
-
32
- /**
33
- * Base Collection class providing data management interface
34
- * @template T - Type of items in collection
35
- */
36
- export class Collection {
37
- #items = new Map()
38
- #observers = new Set()
39
- #query = null
40
- #sort = null
41
- #loading = false
42
- #error = null
43
-
44
- /**
45
- * Creates a new collection instance
46
- * @param {Object} config - Collection configuration
47
- * @param {Function} config.transform - Transform function for items
48
- * @param {Function} config.validate - Validation function for items
49
- */
50
- constructor (config = {}) {
51
- this.transform = config.transform || (item => item)
52
- this.validate = config.validate || (() => true)
53
- }
54
-
55
- /**
56
- * Subscribe to collection changes
57
- * @param {Function} observer - Observer callback
58
- * @returns {Function} Unsubscribe function
59
- */
60
- subscribe (observer) {
61
- this.#observers.add(observer)
62
- return () => this.#observers.delete(observer)
63
- }
64
-
65
- /**
66
- * Notify observers of collection changes
67
- * @param {string} event - Event type
68
- * @param {*} data - Event data
69
- */
70
- #notify (event, data) {
71
- this.#observers.forEach(observer => observer({ event, data }))
72
- }
73
-
74
- /**
75
- * Set loading state
76
- * @param {boolean} loading - Loading state
77
- */
78
- #setLoading (loading) {
79
- this.#loading = loading
80
- this.#notify(COLLECTION_EVENTS.LOADING, loading)
81
- }
82
-
83
- /**
84
- * Set error state
85
- * @param {Error} error - Error object
86
- */
87
- #setError (error) {
88
- this.#error = error
89
- this.#notify(COLLECTION_EVENTS.ERROR, error)
90
- }
91
-
92
- /**
93
- * Get collection items based on current query and sort
94
- * @returns {Array<T>} Collection items
95
- */
96
- get items () {
97
- let result = Array.from(this.#items.values())
98
-
99
- if (this.#query) {
100
- result = result.filter(this.#query)
101
- }
102
-
103
- if (this.#sort) {
104
- result.sort(this.#sort)
105
- }
106
-
107
- return result
108
- }
109
-
110
- /**
111
- * Get collection size
112
- * @returns {number} Number of items
113
- */
114
- get size () {
115
- return this.#items.size
116
- }
117
-
118
- /**
119
- * Get loading state
120
- * @returns {boolean} Loading state
121
- */
122
- get loading () {
123
- return this.#loading
124
- }
125
-
126
- /**
127
- * Get error state
128
- * @returns {Error|null} Error object
129
- */
130
- get error () {
131
- return this.#error
132
- }
133
-
134
- /**
135
- * Set query filter
136
- * @param {Function} queryFn - Query function
137
- */
138
- query (queryFn) {
139
- this.#query = queryFn
140
- this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
141
- }
142
-
143
- /**
144
- * Set sort function
145
- * @param {Function} sortFn - Sort function
146
- */
147
- sort (sortFn) {
148
- this.#sort = sortFn
149
- this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
150
- }
151
-
152
- /**
153
- * Add items to collection
154
- * @param {T|Array<T>} items - Items to add
155
- * @returns {Promise<Array<T>>} Added items
156
- */
157
- async add (items) {
158
- try {
159
- this.#setLoading(true)
160
- const toAdd = Array.isArray(items) ? items : [items]
161
-
162
- const validated = toAdd.filter(this.validate)
163
- const transformed = validated.map(this.transform)
164
-
165
- transformed.forEach(item => {
166
- if (!item.id) {
167
- throw new Error('Items must have an id property')
168
- }
169
- this.#items.set(item.id, item)
170
- })
171
-
172
- this.#notify(COLLECTION_EVENTS.ADD, transformed)
173
- this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
174
-
175
- return transformed
176
- } catch (error) {
177
- this.#setError(error)
178
- throw error
179
- } finally {
180
- this.#setLoading(false)
181
- }
182
- }
183
-
184
- /**
185
- * Update items in collection
186
- * @param {T|Array<T>} items - Items to update
187
- * @returns {Promise<Array<T>>} Updated items
188
- */
189
- async update (items) {
190
- try {
191
- this.#setLoading(true)
192
- const toUpdate = Array.isArray(items) ? items : [items]
193
-
194
- const updated = toUpdate.map(item => {
195
- if (!this.#items.has(item.id)) {
196
- throw new Error(`Item with id ${item.id} not found`)
197
- }
198
-
199
- const validated = this.validate(item)
200
- if (!validated) {
201
- throw new Error(`Invalid item: ${item.id}`)
202
- }
203
-
204
- const transformed = this.transform(item)
205
- this.#items.set(item.id, transformed)
206
- return transformed
207
- })
208
-
209
- this.#notify(COLLECTION_EVENTS.UPDATE, updated)
210
- this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
211
-
212
- return updated
213
- } catch (error) {
214
- this.#setError(error)
215
- throw error
216
- } finally {
217
- this.#setLoading(false)
218
- }
219
- }
220
-
221
- /**
222
- * Remove items from collection
223
- * @param {string|Array<string>} ids - Item IDs to remove
224
- * @returns {Promise<Array<string>>} Removed item IDs
225
- */
226
- async remove (ids) {
227
- try {
228
- this.#setLoading(true)
229
- const toRemove = Array.isArray(ids) ? ids : [ids]
230
-
231
- toRemove.forEach(id => {
232
- if (!this.#items.has(id)) {
233
- throw new Error(`Item with id ${id} not found`)
234
- }
235
- this.#items.delete(id)
236
- })
237
-
238
- this.#notify(COLLECTION_EVENTS.REMOVE, toRemove)
239
- this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
240
-
241
- return toRemove
242
- } catch (error) {
243
- this.#setError(error)
244
- throw error
245
- } finally {
246
- this.#setLoading(false)
247
- }
248
- }
249
-
250
- /**
251
- * Clear all items from collection
252
- */
253
- clear () {
254
- this.#items.clear()
255
- this.#query = null
256
- this.#sort = null
257
- this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
258
- }
259
- }