mtrl 0.3.6 → 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 (45) 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 +15 -13
  5. package/src/components/menu/config.ts +5 -5
  6. package/src/components/menu/features/anchor.ts +99 -15
  7. package/src/components/menu/features/controller.ts +418 -221
  8. package/src/components/menu/features/index.ts +2 -1
  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 +18 -60
  12. package/src/components/menu/types.ts +17 -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 +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/index.ts +4 -1
  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 +103 -24
  38. package/src/styles/components/_select.scss +265 -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
@@ -97,6 +97,12 @@ export interface TextfieldConfig {
97
97
  /** Whether supporting text indicates an error */
98
98
  error?: boolean;
99
99
 
100
+ /** Prefix text to display before the input */
101
+ prefixText?: string;
102
+
103
+ /** Suffix text to display after the input */
104
+ suffixText?: string;
105
+
100
106
  /** Additional CSS classes */
101
107
  class?: string;
102
108
 
@@ -138,6 +144,54 @@ export interface TextfieldComponent {
138
144
  /** Gets the textfield's label text */
139
145
  getLabel: () => string;
140
146
 
147
+ /** Leading icon element (if present) */
148
+ leadingIcon: HTMLElement | null;
149
+
150
+ /** Sets the leading icon HTML content */
151
+ setLeadingIcon: (html: string) => TextfieldComponent;
152
+
153
+ /** Removes the leading icon */
154
+ removeLeadingIcon: () => TextfieldComponent;
155
+
156
+ /** Trailing icon element (if present) */
157
+ trailingIcon: HTMLElement | null;
158
+
159
+ /** Sets the trailing icon HTML content */
160
+ setTrailingIcon: (html: string) => TextfieldComponent;
161
+
162
+ /** Removes the trailing icon */
163
+ removeTrailingIcon: () => TextfieldComponent;
164
+
165
+ /** Supporting text element (if present) */
166
+ supportingTextElement: HTMLElement | null;
167
+
168
+ /** Sets the supporting text content */
169
+ setSupportingText: (text: string, isError?: boolean) => TextfieldComponent;
170
+
171
+ /** Removes the supporting text */
172
+ removeSupportingText: () => TextfieldComponent;
173
+
174
+ /** Prefix text element (if present) */
175
+ prefixTextElement: HTMLElement | null;
176
+
177
+ /** Sets the prefix text content */
178
+ setPrefixText: (text: string) => TextfieldComponent;
179
+
180
+ /** Removes the prefix text */
181
+ removePrefixText: () => TextfieldComponent;
182
+
183
+ /** Suffix text element (if present) */
184
+ suffixTextElement: HTMLElement | null;
185
+
186
+ /** Sets the suffix text content */
187
+ setSuffixText: (text: string) => TextfieldComponent;
188
+
189
+ /** Removes the suffix text */
190
+ removeSuffixText: () => TextfieldComponent;
191
+
192
+ /** Manually update element positions (useful after DOM changes) */
193
+ updatePositions: () => TextfieldComponent;
194
+
141
195
  /** Adds event listener */
142
196
  on: (event: string, handler: Function) => TextfieldComponent;
143
197
 
@@ -182,6 +236,22 @@ export interface BaseComponent {
182
236
  setText: (content: string) => any;
183
237
  getText: () => string;
184
238
  };
239
+ leadingIcon?: HTMLElement | null;
240
+ setLeadingIcon?: (html: string) => any;
241
+ removeLeadingIcon?: () => any;
242
+ trailingIcon?: HTMLElement | null;
243
+ setTrailingIcon?: (html: string) => any;
244
+ removeTrailingIcon?: () => any;
245
+ supportingTextElement?: HTMLElement | null;
246
+ setSupportingText?: (text: string, isError?: boolean) => any;
247
+ removeSupportingText?: () => any;
248
+ prefixTextElement?: HTMLElement | null;
249
+ setPrefixText?: (text: string) => any;
250
+ removePrefixText?: () => any;
251
+ suffixTextElement?: HTMLElement | null;
252
+ setSuffixText?: (text: string) => any;
253
+ removeSuffixText?: () => any;
254
+ updateElementPositions?: () => any;
185
255
  on?: (event: string, handler: Function) => any;
186
256
  off?: (event: string, handler: Function) => any;
187
257
  disabled?: {
@@ -0,0 +1,62 @@
1
+ // src/core/collection/adapters/base.ts
2
+
3
+ /**
4
+ * Query operators for filtering data
5
+ */
6
+ export const OPERATORS = {
7
+ EQ: 'eq',
8
+ NE: 'ne',
9
+ GT: 'gt',
10
+ GTE: 'gte',
11
+ LT: 'lt',
12
+ LTE: 'lte',
13
+ IN: 'in',
14
+ NIN: 'nin',
15
+ CONTAINS: 'contains',
16
+ STARTS_WITH: 'startsWith',
17
+ ENDS_WITH: 'endsWith'
18
+ } as const;
19
+
20
+ /**
21
+ * Type for query operators
22
+ */
23
+ export type Operator = keyof typeof OPERATORS;
24
+
25
+ /**
26
+ * Configuration for base adapter
27
+ */
28
+ export interface BaseAdapterConfig {
29
+ /**
30
+ * Error handler function
31
+ */
32
+ onError?: (error: Error, context?: any) => void;
33
+ }
34
+
35
+ /**
36
+ * Base adapter interface
37
+ */
38
+ export interface BaseAdapter {
39
+ /**
40
+ * Handles errors in adapter operations
41
+ * @param error - The error that occurred
42
+ * @param context - Optional context information about the error
43
+ * @throws The original error after processing
44
+ */
45
+ handleError(error: Error, context?: any): never;
46
+ }
47
+
48
+ /**
49
+ * Creates a base adapter with error handling
50
+ * @param config - Adapter configuration
51
+ * @returns Base adapter with error handling
52
+ */
53
+ export const createBaseAdapter = (config: BaseAdapterConfig = {}): BaseAdapter => {
54
+ const handleError = (error: Error, context?: any): never => {
55
+ config.onError?.(error, context);
56
+ throw error;
57
+ };
58
+
59
+ return {
60
+ handleError
61
+ };
62
+ };
@@ -0,0 +1,300 @@
1
+ // src/core/collection/collection.ts
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
+ } as const;
14
+
15
+ /**
16
+ * Collection event type
17
+ */
18
+ export type CollectionEvent = keyof typeof COLLECTION_EVENTS;
19
+
20
+ /**
21
+ * Observer callback type for collection events
22
+ */
23
+ export type CollectionObserver<T> = (payload: { event: CollectionEvent; data: any }) => void;
24
+
25
+ /**
26
+ * Query operators for filtering
27
+ */
28
+ export const OPERATORS = {
29
+ EQ: 'eq',
30
+ NE: 'ne',
31
+ GT: 'gt',
32
+ GTE: 'gte',
33
+ LT: 'lt',
34
+ LTE: 'lte',
35
+ IN: 'in',
36
+ NIN: 'nin',
37
+ CONTAINS: 'contains',
38
+ STARTS_WITH: 'startsWith',
39
+ ENDS_WITH: 'endsWith'
40
+ } as const;
41
+
42
+ /**
43
+ * Type for all collection items
44
+ */
45
+ export interface CollectionItem {
46
+ id: string;
47
+ [key: string]: any;
48
+ }
49
+
50
+ /**
51
+ * Collection configuration
52
+ */
53
+ export interface CollectionConfig<T extends CollectionItem> {
54
+ /**
55
+ * Transform function for items
56
+ */
57
+ transform?: (item: any) => T;
58
+
59
+ /**
60
+ * Validation function for items
61
+ */
62
+ validate?: (item: any) => boolean;
63
+ }
64
+
65
+ /**
66
+ * Base Collection class providing data management interface
67
+ * @template T - Type of items in collection
68
+ */
69
+ export class Collection<T extends CollectionItem> {
70
+ #items = new Map<string, T>();
71
+ #observers = new Set<CollectionObserver<T>>();
72
+ #query: ((item: T) => boolean) | null = null;
73
+ #sort: ((a: T, b: T) => number) | null = null;
74
+ #loading = false;
75
+ #error: Error | null = null;
76
+
77
+ /**
78
+ * Transform function for items
79
+ */
80
+ transform: (item: any) => T;
81
+
82
+ /**
83
+ * Validation function for items
84
+ */
85
+ validate: (item: any) => boolean;
86
+
87
+ /**
88
+ * Creates a new collection instance
89
+ * @param config - Collection configuration
90
+ */
91
+ constructor(config: CollectionConfig<T> = {}) {
92
+ this.transform = config.transform || ((item: any) => item as T);
93
+ this.validate = config.validate || (() => true);
94
+ }
95
+
96
+ /**
97
+ * Subscribe to collection changes
98
+ * @param observer - Observer callback
99
+ * @returns Unsubscribe function
100
+ */
101
+ subscribe(observer: CollectionObserver<T>): () => void {
102
+ this.#observers.add(observer);
103
+ return () => this.#observers.delete(observer);
104
+ }
105
+
106
+ /**
107
+ * Notify observers of collection changes
108
+ * @param event - Event type
109
+ * @param data - Event data
110
+ */
111
+ #notify(event: CollectionEvent, data: any): void {
112
+ this.#observers.forEach(observer => observer({ event, data }));
113
+ }
114
+
115
+ /**
116
+ * Set loading state
117
+ * @param loading - Loading state
118
+ */
119
+ #setLoading(loading: boolean): void {
120
+ this.#loading = loading;
121
+ this.#notify(COLLECTION_EVENTS.LOADING, loading);
122
+ }
123
+
124
+ /**
125
+ * Set error state
126
+ * @param error - Error object
127
+ */
128
+ #setError(error: Error): void {
129
+ this.#error = error;
130
+ this.#notify(COLLECTION_EVENTS.ERROR, error);
131
+ }
132
+
133
+ /**
134
+ * Get collection items based on current query and sort
135
+ * @returns Collection items
136
+ */
137
+ get items(): T[] {
138
+ let result = Array.from(this.#items.values());
139
+
140
+ if (this.#query) {
141
+ result = result.filter(this.#query);
142
+ }
143
+
144
+ if (this.#sort) {
145
+ result.sort(this.#sort);
146
+ }
147
+
148
+ return result;
149
+ }
150
+
151
+ /**
152
+ * Get collection size
153
+ * @returns Number of items
154
+ */
155
+ get size(): number {
156
+ return this.#items.size;
157
+ }
158
+
159
+ /**
160
+ * Get loading state
161
+ * @returns Loading state
162
+ */
163
+ get loading(): boolean {
164
+ return this.#loading;
165
+ }
166
+
167
+ /**
168
+ * Get error state
169
+ * @returns Error object
170
+ */
171
+ get error(): Error | null {
172
+ return this.#error;
173
+ }
174
+
175
+ /**
176
+ * Set query filter
177
+ * @param queryFn - Query function
178
+ */
179
+ query(queryFn: (item: T) => boolean): void {
180
+ this.#query = queryFn;
181
+ this.#notify(COLLECTION_EVENTS.CHANGE, this.items);
182
+ }
183
+
184
+ /**
185
+ * Set sort function
186
+ * @param sortFn - Sort function
187
+ */
188
+ sort(sortFn: (a: T, b: T) => number): void {
189
+ this.#sort = sortFn;
190
+ this.#notify(COLLECTION_EVENTS.CHANGE, this.items);
191
+ }
192
+
193
+ /**
194
+ * Add items to collection
195
+ * @param items - Items to add
196
+ * @returns Added items
197
+ */
198
+ async add(items: T | T[]): Promise<T[]> {
199
+ try {
200
+ this.#setLoading(true);
201
+ const toAdd = Array.isArray(items) ? items : [items];
202
+
203
+ const validated = toAdd.filter(this.validate);
204
+ const transformed = validated.map(this.transform);
205
+
206
+ transformed.forEach(item => {
207
+ if (!item.id) {
208
+ throw new Error('Items must have an id property');
209
+ }
210
+ this.#items.set(item.id, item);
211
+ });
212
+
213
+ this.#notify(COLLECTION_EVENTS.ADD, transformed);
214
+ this.#notify(COLLECTION_EVENTS.CHANGE, this.items);
215
+
216
+ return transformed;
217
+ } catch (error) {
218
+ this.#setError(error as Error);
219
+ throw error;
220
+ } finally {
221
+ this.#setLoading(false);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Update items in collection
227
+ * @param items - Items to update
228
+ * @returns Updated items
229
+ */
230
+ async update(items: T | T[]): Promise<T[]> {
231
+ try {
232
+ this.#setLoading(true);
233
+ const toUpdate = Array.isArray(items) ? items : [items];
234
+
235
+ const updated = toUpdate.map(item => {
236
+ if (!this.#items.has(item.id)) {
237
+ throw new Error(`Item with id ${item.id} not found`);
238
+ }
239
+
240
+ const validated = this.validate(item);
241
+ if (!validated) {
242
+ throw new Error(`Invalid item: ${item.id}`);
243
+ }
244
+
245
+ const transformed = this.transform(item);
246
+ this.#items.set(item.id, transformed);
247
+ return transformed;
248
+ });
249
+
250
+ this.#notify(COLLECTION_EVENTS.UPDATE, updated);
251
+ this.#notify(COLLECTION_EVENTS.CHANGE, this.items);
252
+
253
+ return updated;
254
+ } catch (error) {
255
+ this.#setError(error as Error);
256
+ throw error;
257
+ } finally {
258
+ this.#setLoading(false);
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Remove items from collection
264
+ * @param ids - Item IDs to remove
265
+ * @returns Removed item IDs
266
+ */
267
+ async remove(ids: string | string[]): Promise<string[]> {
268
+ try {
269
+ this.#setLoading(true);
270
+ const toRemove = Array.isArray(ids) ? ids : [ids];
271
+
272
+ toRemove.forEach(id => {
273
+ if (!this.#items.has(id)) {
274
+ throw new Error(`Item with id ${id} not found`);
275
+ }
276
+ this.#items.delete(id);
277
+ });
278
+
279
+ this.#notify(COLLECTION_EVENTS.REMOVE, toRemove);
280
+ this.#notify(COLLECTION_EVENTS.CHANGE, this.items);
281
+
282
+ return toRemove;
283
+ } catch (error) {
284
+ this.#setError(error as Error);
285
+ throw error;
286
+ } finally {
287
+ this.#setLoading(false);
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Clear all items from collection
293
+ */
294
+ clear(): void {
295
+ this.#items.clear();
296
+ this.#query = null;
297
+ this.#sort = null;
298
+ this.#notify(COLLECTION_EVENTS.CHANGE, this.items);
299
+ }
300
+ }
@@ -0,0 +1,57 @@
1
+ // src/core/collection/index.ts
2
+
3
+ // Export collection class and types
4
+ export {
5
+ Collection,
6
+ COLLECTION_EVENTS,
7
+ OPERATORS as COLLECTION_OPERATORS
8
+ } from './collection';
9
+
10
+ export type {
11
+ CollectionItem,
12
+ CollectionConfig,
13
+ CollectionEvent,
14
+ CollectionObserver
15
+ } from './collection';
16
+
17
+ // Export list manager
18
+ export {
19
+ createListManager,
20
+ transforms
21
+ } from './list-manager';
22
+
23
+ export type {
24
+ ListManager,
25
+ ListManagerConfig,
26
+ ListItem,
27
+ List,
28
+ PageLoader,
29
+ PageLoaderConfig,
30
+ LoadStatus,
31
+ PaginationMeta
32
+ } from './list-manager';
33
+
34
+ // Export adapters
35
+ export {
36
+ createBaseAdapter,
37
+ OPERATORS
38
+ } from './adapters/base';
39
+
40
+ export type {
41
+ BaseAdapter,
42
+ BaseAdapterConfig,
43
+ Operator
44
+ } from './adapters/base';
45
+
46
+ export {
47
+ createRouteAdapter
48
+ } from './adapters/route';
49
+
50
+ export type {
51
+ RouteAdapter,
52
+ RouteAdapterConfig,
53
+ RouteEndpoints,
54
+ QueryDefinition,
55
+ QueryCondition,
56
+ RequestOptions
57
+ } from './adapters/route';