mtrl 0.3.5 → 0.3.7

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 (65) hide show
  1. package/package.json +1 -1
  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 +144 -267
  5. package/src/components/menu/config.ts +84 -40
  6. package/src/components/menu/features/anchor.ts +243 -0
  7. package/src/components/menu/features/controller.ts +1167 -0
  8. package/src/components/menu/features/index.ts +5 -0
  9. package/src/components/menu/features/position.ts +353 -0
  10. package/src/components/menu/index.ts +31 -63
  11. package/src/components/menu/menu.ts +72 -104
  12. package/src/components/menu/types.ts +264 -447
  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 +317 -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/core/dom/classes.ts +81 -9
  34. package/src/core/dom/create.ts +30 -19
  35. package/src/core/layout/README.md +531 -166
  36. package/src/core/layout/array.ts +3 -4
  37. package/src/core/layout/config.ts +193 -0
  38. package/src/core/layout/create.ts +1 -2
  39. package/src/core/layout/index.ts +12 -2
  40. package/src/core/layout/object.ts +2 -3
  41. package/src/core/layout/processor.ts +60 -12
  42. package/src/core/layout/result.ts +1 -2
  43. package/src/core/layout/types.ts +105 -50
  44. package/src/core/layout/utils.ts +69 -61
  45. package/src/index.ts +6 -2
  46. package/src/styles/abstract/_variables.scss +18 -0
  47. package/src/styles/components/_button.scss +21 -5
  48. package/src/styles/components/{_chip.scss → _chips.scss} +118 -4
  49. package/src/styles/components/_menu.scss +109 -18
  50. package/src/styles/components/_select.scss +265 -0
  51. package/src/styles/components/_textfield.scss +233 -42
  52. package/src/styles/main.scss +24 -23
  53. package/src/styles/utilities/_layout.scss +665 -0
  54. package/src/components/menu/features/items-manager.ts +0 -457
  55. package/src/components/menu/features/keyboard-navigation.ts +0 -133
  56. package/src/components/menu/features/positioning.ts +0 -127
  57. package/src/components/menu/features/visibility.ts +0 -230
  58. package/src/components/menu/menu-item.ts +0 -86
  59. package/src/components/menu/utils.ts +0 -67
  60. package/src/components/textfield/features.ts +0 -322
  61. package/src/core/collection/adapters/base.js +0 -26
  62. package/src/core/collection/collection.js +0 -259
  63. package/src/core/collection/list-manager.js +0 -157
  64. /package/src/core/collection/adapters/{route.js → route.ts} +0 -0
  65. /package/src/{core/build → styles/utilities}/_ripple.scss +0 -0
@@ -0,0 +1,333 @@
1
+ // src/core/collection/list-manager.ts
2
+
3
+ import { createRouteAdapter, RouteAdapter, QueryDefinition } from './adapters/route';
4
+
5
+ /**
6
+ * List item interface
7
+ */
8
+ export interface ListItem {
9
+ id: string;
10
+ [key: string]: any;
11
+ }
12
+
13
+ /**
14
+ * List with items setter
15
+ */
16
+ export interface List {
17
+ setItems: (items: any[]) => void;
18
+ }
19
+
20
+ /**
21
+ * Metadata for pagination
22
+ */
23
+ export interface PaginationMeta {
24
+ cursor: string | null;
25
+ hasNext: boolean;
26
+ }
27
+
28
+ /**
29
+ * Response format from loadItems
30
+ */
31
+ export interface LoadItemsResponse<T = any> {
32
+ items: T[];
33
+ meta: PaginationMeta;
34
+ }
35
+
36
+ /**
37
+ * Load status callback data
38
+ */
39
+ export interface LoadStatus<T = any> {
40
+ loading: boolean;
41
+ hasNext?: boolean;
42
+ hasPrev?: boolean;
43
+ items?: T[];
44
+ }
45
+
46
+ /**
47
+ * Page loader configuration
48
+ */
49
+ export interface PageLoaderConfig<T = any> {
50
+ onLoad?: (status: LoadStatus<T>) => void;
51
+ pageSize?: number;
52
+ }
53
+
54
+ /**
55
+ * Page loader interface
56
+ */
57
+ export interface PageLoader<T = any> {
58
+ /**
59
+ * Load a specific page by cursor
60
+ * @param cursor - Cursor pointing to the page
61
+ * @param addToHistory - Whether to add current cursor to history
62
+ * @returns Page status
63
+ */
64
+ load: (cursor?: string | null, addToHistory?: boolean) => Promise<{
65
+ hasNext: boolean;
66
+ hasPrev: boolean;
67
+ } | undefined>;
68
+
69
+ /**
70
+ * Load next page
71
+ * @returns Page status
72
+ */
73
+ loadNext: () => Promise<{
74
+ hasNext: boolean;
75
+ hasPrev: boolean;
76
+ } | undefined>;
77
+
78
+ /**
79
+ * Load previous page
80
+ * @returns Page status
81
+ */
82
+ loadPrev: () => Promise<{
83
+ hasNext: boolean;
84
+ hasPrev: boolean;
85
+ } | undefined>;
86
+
87
+ /**
88
+ * Current loading state
89
+ */
90
+ readonly loading: boolean;
91
+
92
+ /**
93
+ * Current cursor
94
+ */
95
+ readonly cursor: string | null;
96
+ }
97
+
98
+ /**
99
+ * List manager config
100
+ */
101
+ export interface ListManagerConfig<T = any> {
102
+ /**
103
+ * Transform function to convert API items to app format
104
+ */
105
+ transform?: (item: any) => T;
106
+
107
+ /**
108
+ * Base URL for API requests
109
+ */
110
+ baseUrl?: string;
111
+ }
112
+
113
+ /**
114
+ * List manager interface
115
+ */
116
+ export interface ListManager<T = any> {
117
+ /**
118
+ * Load items with optional parameters
119
+ * @param params - Query parameters
120
+ * @returns Loaded items with pagination metadata
121
+ */
122
+ loadItems: (params?: Record<string, any>) => Promise<LoadItemsResponse<T>>;
123
+
124
+ /**
125
+ * Create a page loader for the specified list
126
+ * @param list - List to manage
127
+ * @param config - Page loader configuration
128
+ * @returns Page loader
129
+ */
130
+ createPageLoader: (list: List, config?: PageLoaderConfig<T>) => PageLoader<T>;
131
+ }
132
+
133
+ /**
134
+ * Creates a list manager for a specific collection
135
+ * @param collection - Collection name
136
+ * @param config - Configuration options
137
+ * @returns List manager methods
138
+ */
139
+ export const createListManager = <T = any>(
140
+ collection: string,
141
+ config: ListManagerConfig<T> = {}
142
+ ): ListManager<T> => {
143
+ const {
144
+ transform = (item: any) => item as T,
145
+ baseUrl = 'http://localhost:4000/api'
146
+ } = config;
147
+
148
+ // Initialize route adapter
149
+ const adapter: RouteAdapter = createRouteAdapter({
150
+ base: baseUrl,
151
+ endpoints: {
152
+ list: `/${collection}`
153
+ },
154
+ headers: {
155
+ 'Content-Type': 'application/json'
156
+ }
157
+ });
158
+
159
+ /**
160
+ * Load items with cursor pagination
161
+ * @param params - Query parameters
162
+ * @returns Loaded items with pagination metadata
163
+ */
164
+ const loadItems = async (params: Record<string, any> = {}): Promise<LoadItemsResponse<T>> => {
165
+ try {
166
+ const response = await adapter.read(params as QueryDefinition);
167
+
168
+ return {
169
+ items: response.items.map(transform),
170
+ meta: response.meta as PaginationMeta
171
+ };
172
+ } catch (error) {
173
+ console.error(`Error loading ${collection}:`, error);
174
+ return {
175
+ items: [],
176
+ meta: {
177
+ cursor: null,
178
+ hasNext: false
179
+ }
180
+ };
181
+ }
182
+ };
183
+
184
+ /**
185
+ * Create a page loader for the specified list
186
+ * @param list - List to manage
187
+ * @param config - Page loader configuration
188
+ * @returns Page loader
189
+ */
190
+ const createPageLoader = (
191
+ list: List,
192
+ { onLoad, pageSize = 20 }: PageLoaderConfig<T> = {}
193
+ ): PageLoader<T> => {
194
+ let currentCursor: string | null = null;
195
+ let loading = false;
196
+ const pageHistory: Array<string | null> = [];
197
+
198
+ /**
199
+ * Load a specific page by cursor
200
+ * @param cursor - Cursor pointing to the page
201
+ * @param addToHistory - Whether to add current cursor to history
202
+ * @returns Page status
203
+ */
204
+ const load = async (
205
+ cursor: string | null = null,
206
+ addToHistory = true
207
+ ): Promise<{ hasNext: boolean; hasPrev: boolean } | undefined> => {
208
+ if (loading) return;
209
+
210
+ loading = true;
211
+ onLoad?.({ loading: true });
212
+
213
+ const { items, meta } = await loadItems({
214
+ limit: pageSize,
215
+ cursor
216
+ });
217
+
218
+ if (addToHistory && cursor) {
219
+ pageHistory.push(currentCursor);
220
+ }
221
+ currentCursor = meta.cursor;
222
+
223
+ list.setItems(items);
224
+ loading = false;
225
+
226
+ const status = {
227
+ loading: false,
228
+ hasNext: meta.hasNext,
229
+ hasPrev: pageHistory.length > 0,
230
+ items
231
+ };
232
+
233
+ onLoad?.(status);
234
+
235
+ return {
236
+ hasNext: meta.hasNext,
237
+ hasPrev: pageHistory.length > 0
238
+ };
239
+ };
240
+
241
+ /**
242
+ * Load next page
243
+ * @returns Page status
244
+ */
245
+ const loadNext = (): Promise<{ hasNext: boolean; hasPrev: boolean } | undefined> =>
246
+ load(currentCursor);
247
+
248
+ /**
249
+ * Load previous page
250
+ * @returns Page status
251
+ */
252
+ const loadPrev = (): Promise<{ hasNext: boolean; hasPrev: boolean } | undefined> => {
253
+ const previousCursor = pageHistory.pop();
254
+ return load(previousCursor, false);
255
+ };
256
+
257
+ return {
258
+ load,
259
+ loadNext,
260
+ loadPrev,
261
+ get loading(): boolean { return loading; },
262
+ get cursor(): string | null { return currentCursor; }
263
+ };
264
+ };
265
+
266
+ return {
267
+ loadItems,
268
+ createPageLoader
269
+ };
270
+ };
271
+
272
+ /**
273
+ * Transform functions for common collections
274
+ */
275
+ export const transforms = {
276
+ /**
277
+ * Transform track data
278
+ * @param track - Raw track data
279
+ * @returns Formatted track data
280
+ */
281
+ track: (track: any): ListItem => ({
282
+ id: track._id,
283
+ headline: track.title || 'Untitled',
284
+ supportingText: track.artist || 'Unknown Artist',
285
+ meta: track.year?.toString() || ''
286
+ }),
287
+
288
+ /**
289
+ * Transform playlist data
290
+ * @param playlist - Raw playlist data
291
+ * @returns Formatted playlist data
292
+ */
293
+ playlist: (playlist: any): ListItem => ({
294
+ id: playlist._id,
295
+ headline: playlist.name || 'Untitled Playlist',
296
+ supportingText: `${playlist.tracks?.length || 0} tracks`,
297
+ meta: playlist.creator || ''
298
+ }),
299
+
300
+ /**
301
+ * Transform country data
302
+ * @param country - Raw country data
303
+ * @returns Formatted country data
304
+ */
305
+ country: (country: any): ListItem => ({
306
+ id: country._id,
307
+ headline: country.name || country.code,
308
+ supportingText: country.continent || '',
309
+ meta: country.code || ''
310
+ })
311
+ };
312
+
313
+ /**
314
+ * Usage example:
315
+ *
316
+ * const trackManager = createListManager<TrackItem>('track', {
317
+ * transform: transforms.track
318
+ * });
319
+ *
320
+ * const loader = trackManager.createPageLoader(list, {
321
+ * onLoad: ({ loading, hasNext, items }) => {
322
+ * updateNavigation({ loading, hasNext });
323
+ * logEvent(`Loaded ${items.length} tracks`);
324
+ * }
325
+ * });
326
+ *
327
+ * // Initial load
328
+ * await loader.load();
329
+ *
330
+ * // Navigation
331
+ * nextButton.onclick = () => loader.loadNext();
332
+ * prevButton.onclick = () => loader.loadPrev();
333
+ */
@@ -1,60 +1,132 @@
1
1
  // src/core/dom/classes.ts
2
2
  /**
3
3
  * @module core/dom
4
- * @description DOM manipulation utilities
4
+ * @description DOM manipulation utilities optimized for performance
5
5
  */
6
6
 
7
7
  import { normalizeClasses } from '../utils';
8
+ import { PREFIX } from '../config';
9
+
10
+ // Constant for prefix with dash for better performance
11
+ const PREFIX_WITH_DASH = `${PREFIX}-`;
8
12
 
9
13
  /**
10
14
  * Adds multiple classes to an element
15
+ * Automatically adds prefix to classes that don't already have it
16
+ * Optimized for minimal array operations and DOM interactions
17
+ *
11
18
  * @param {HTMLElement} element - Target element
12
19
  * @param {...(string | string[])} classes - Classes to add
13
20
  * @returns {HTMLElement} Modified element
14
21
  */
15
22
  export const addClass = (element: HTMLElement, ...classes: (string | string[])[]): HTMLElement => {
16
23
  const normalizedClasses = normalizeClasses(...classes);
17
- if (normalizedClasses.length) {
18
- element.classList.add(...normalizedClasses);
24
+
25
+ // Early return for empty classes
26
+ if (!normalizedClasses.length) return element;
27
+
28
+ // Using DOMTokenList's add() with multiple arguments is faster than multiple add() calls
29
+ // But we need to handle prefixing first
30
+ const prefixedClasses: string[] = [];
31
+
32
+ for (let i = 0; i < normalizedClasses.length; i++) {
33
+ const cls = normalizedClasses[i];
34
+ if (!cls) continue;
35
+
36
+ prefixedClasses.push(
37
+ cls.startsWith(PREFIX_WITH_DASH) ? cls : PREFIX_WITH_DASH + cls
38
+ );
39
+ }
40
+
41
+ // Add all classes in a single operation if possible
42
+ if (prefixedClasses.length) {
43
+ element.classList.add(...prefixedClasses);
19
44
  }
45
+
20
46
  return element;
21
47
  };
22
48
 
23
49
  /**
24
50
  * Removes multiple classes from an element
51
+ * Handles only exact class names as specified (no automatic prefixing)
52
+ * For better performance, clients should know exactly which classes to remove
53
+ *
25
54
  * @param {HTMLElement} element - Target element
26
55
  * @param {...(string | string[])} classes - Classes to remove
27
56
  * @returns {HTMLElement} Modified element
28
57
  */
29
58
  export const removeClass = (element: HTMLElement, ...classes: (string | string[])[]): HTMLElement => {
30
59
  const normalizedClasses = normalizeClasses(...classes);
31
- if (normalizedClasses.length) {
32
- element.classList.remove(...normalizedClasses);
60
+
61
+ // Early return for empty classes
62
+ if (!normalizedClasses.length) return element;
63
+
64
+ // Prepare prefixed class names
65
+ const prefixedClasses: string[] = [];
66
+
67
+ for (let i = 0; i < normalizedClasses.length; i++) {
68
+ const cls = normalizedClasses[i];
69
+ if (!cls) continue;
70
+
71
+ // Only add the prefixed version
72
+ prefixedClasses.push(
73
+ cls.startsWith(PREFIX_WITH_DASH) ? cls : PREFIX_WITH_DASH + cls
74
+ );
75
+ }
76
+
77
+ // Remove all classes in a single operation if possible
78
+ if (prefixedClasses.length) {
79
+ element.classList.remove(...prefixedClasses);
33
80
  }
81
+
34
82
  return element;
35
83
  };
36
84
 
37
85
  /**
38
86
  * Toggles multiple classes on an element
87
+ * Automatically adds prefix to classes that don't already have it
88
+ *
39
89
  * @param {HTMLElement} element - Target element
40
90
  * @param {...(string | string[])} classes - Classes to toggle
41
91
  * @returns {HTMLElement} Modified element
42
92
  */
43
93
  export const toggleClass = (element: HTMLElement, ...classes: (string | string[])[]): HTMLElement => {
44
94
  const normalizedClasses = normalizeClasses(...classes);
45
- normalizedClasses.forEach(cls => {
46
- element.classList.toggle(cls);
47
- });
95
+
96
+ for (let i = 0; i < normalizedClasses.length; i++) {
97
+ const cls = normalizedClasses[i];
98
+ if (!cls) continue;
99
+
100
+ const prefixedClass = cls.startsWith(PREFIX_WITH_DASH) ? cls : PREFIX_WITH_DASH + cls;
101
+ element.classList.toggle(prefixedClass);
102
+ }
103
+
48
104
  return element;
49
105
  };
50
106
 
51
107
  /**
52
108
  * Checks if an element has all specified classes
109
+ * Automatically adds prefix to classes that don't already have it
110
+ *
53
111
  * @param {HTMLElement} element - Target element
54
112
  * @param {...(string | string[])} classes - Classes to check
55
113
  * @returns {boolean} True if element has all specified classes
56
114
  */
57
115
  export const hasClass = (element: HTMLElement, ...classes: (string | string[])[]): boolean => {
58
116
  const normalizedClasses = normalizeClasses(...classes);
59
- return normalizedClasses.every(cls => element.classList.contains(cls));
117
+
118
+ // Early return for empty classes (technically all are present)
119
+ if (!normalizedClasses.length) return true;
120
+
121
+ for (let i = 0; i < normalizedClasses.length; i++) {
122
+ const cls = normalizedClasses[i];
123
+ if (!cls) continue;
124
+
125
+ const prefixedClass = cls.startsWith(PREFIX_WITH_DASH) ? cls : PREFIX_WITH_DASH + cls;
126
+ if (!element.classList.contains(prefixedClass)) {
127
+ return false;
128
+ }
129
+ }
130
+
131
+ return true;
60
132
  };
@@ -7,6 +7,7 @@
7
7
  import { setAttributes } from './attributes';
8
8
  import { normalizeClasses } from '../utils';
9
9
  import { PREFIX } from '../config';
10
+ import { addClass } from './classes'; // Import addClass
10
11
 
11
12
  /**
12
13
  * Event handler function type
@@ -58,9 +59,22 @@ export interface CreateElementOptions {
58
59
  data?: Record<string, string>;
59
60
 
60
61
  /**
61
- * CSS classes
62
+ * CSS classes (will be automatically prefixed with 'mtrl-')
63
+ * Alias for 'className'
62
64
  */
63
- className?: string | string[] | null;
65
+ class?: string | string[];
66
+
67
+ /**
68
+ * CSS classes (will be automatically prefixed with 'mtrl-')
69
+ * Alias for 'class'
70
+ */
71
+ className?: string | string[];
72
+
73
+ /**
74
+ * CSS classes that will NOT be prefixed
75
+ * Added as-is to the element
76
+ */
77
+ rawClass?: string | string[];
64
78
 
65
79
  /**
66
80
  * HTML attributes
@@ -95,9 +109,6 @@ export interface EventHandlerStorage {
95
109
  [eventName: string]: EventHandler;
96
110
  }
97
111
 
98
- // Constant for prefix with dash
99
- const PREFIX_WITH_DASH = `${PREFIX}-`;
100
-
101
112
  /**
102
113
  * Creates a DOM element with the specified options
103
114
  *
@@ -112,7 +123,9 @@ export const createElement = (options: CreateElementOptions = {}): HTMLElement =
112
123
  text = '',
113
124
  id = '',
114
125
  data = {},
126
+ class: classOption,
115
127
  className,
128
+ rawClass,
116
129
  attrs = {},
117
130
  forwardEvents = {},
118
131
  onCreate,
@@ -127,14 +140,17 @@ export const createElement = (options: CreateElementOptions = {}): HTMLElement =
127
140
  if (text) element.textContent = text;
128
141
  if (id) element.id = id;
129
142
 
130
- // Handle classes
131
- if (className) {
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));
143
+ // 1. Handle prefixed classes using addClass
144
+ const prefixedClassSource = classOption || className;
145
+ if (prefixedClassSource) {
146
+ addClass(element, prefixedClassSource);
147
+ }
148
+
149
+ // 2. Handle raw classes (no prefix)
150
+ if (rawClass) {
151
+ const rawClasses = normalizeClasses(rawClass);
152
+ if (rawClasses.length) {
153
+ element.classList.add(...rawClasses);
138
154
  }
139
155
  }
140
156
 
@@ -230,12 +246,7 @@ export const withAttributes = (attrs: Record<string, any>) =>
230
246
  */
231
247
  export const withClasses = (...classes: (string | string[])[]) =>
232
248
  (element: HTMLElement): HTMLElement => {
233
- const normalizedClasses = normalizeClasses(...classes);
234
- if (normalizedClasses.length) {
235
- element.classList.add(...normalizedClasses.map(cls =>
236
- cls && !cls.startsWith(PREFIX_WITH_DASH) ? PREFIX_WITH_DASH + cls : cls
237
- ).filter(Boolean));
238
- }
249
+ addClass(element, ...classes);
239
250
  return element;
240
251
  };
241
252