mtrl 0.0.0

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 (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +251 -0
  3. package/index.js +10 -0
  4. package/package.json +17 -0
  5. package/src/components/button/api.js +54 -0
  6. package/src/components/button/button.js +81 -0
  7. package/src/components/button/config.js +8 -0
  8. package/src/components/button/constants.js +63 -0
  9. package/src/components/button/index.js +2 -0
  10. package/src/components/button/styles.scss +231 -0
  11. package/src/components/checkbox/api.js +45 -0
  12. package/src/components/checkbox/checkbox.js +95 -0
  13. package/src/components/checkbox/constants.js +88 -0
  14. package/src/components/checkbox/index.js +2 -0
  15. package/src/components/checkbox/styles.scss +183 -0
  16. package/src/components/container/api.js +42 -0
  17. package/src/components/container/container.js +45 -0
  18. package/src/components/container/index.js +2 -0
  19. package/src/components/container/styles.scss +59 -0
  20. package/src/components/list/constants.js +89 -0
  21. package/src/components/list/index.js +2 -0
  22. package/src/components/list/list-item.js +147 -0
  23. package/src/components/list/list.js +267 -0
  24. package/src/components/list/styles/_list-item.scss +142 -0
  25. package/src/components/list/styles/_list.scss +89 -0
  26. package/src/components/list/styles/_variables.scss +13 -0
  27. package/src/components/list/styles.scss +19 -0
  28. package/src/components/navigation/api.js +43 -0
  29. package/src/components/navigation/constants.js +235 -0
  30. package/src/components/navigation/features/items.js +192 -0
  31. package/src/components/navigation/index.js +2 -0
  32. package/src/components/navigation/nav-item.js +137 -0
  33. package/src/components/navigation/navigation.js +55 -0
  34. package/src/components/navigation/styles/_bar.scss +51 -0
  35. package/src/components/navigation/styles/_base.scss +129 -0
  36. package/src/components/navigation/styles/_drawer.scss +169 -0
  37. package/src/components/navigation/styles/_rail.scss +65 -0
  38. package/src/components/navigation/styles.scss +6 -0
  39. package/src/components/snackbar/api.js +125 -0
  40. package/src/components/snackbar/constants.js +41 -0
  41. package/src/components/snackbar/features.js +69 -0
  42. package/src/components/snackbar/index.js +2 -0
  43. package/src/components/snackbar/position.js +63 -0
  44. package/src/components/snackbar/queue.js +74 -0
  45. package/src/components/snackbar/snackbar.js +70 -0
  46. package/src/components/snackbar/styles.scss +182 -0
  47. package/src/components/switch/api.js +44 -0
  48. package/src/components/switch/constants.js +80 -0
  49. package/src/components/switch/index.js +2 -0
  50. package/src/components/switch/styles.scss +172 -0
  51. package/src/components/switch/switch.js +71 -0
  52. package/src/components/textfield/api.js +49 -0
  53. package/src/components/textfield/constants.js +81 -0
  54. package/src/components/textfield/index.js +2 -0
  55. package/src/components/textfield/styles/base.scss +107 -0
  56. package/src/components/textfield/styles/filled.scss +58 -0
  57. package/src/components/textfield/styles/outlined.scss +66 -0
  58. package/src/components/textfield/styles.scss +6 -0
  59. package/src/components/textfield/textfield.js +68 -0
  60. package/src/core/build/constants.js +51 -0
  61. package/src/core/build/icon.js +78 -0
  62. package/src/core/build/ripple.js +92 -0
  63. package/src/core/build/text.js +54 -0
  64. package/src/core/collection/adapters/base.js +26 -0
  65. package/src/core/collection/adapters/mongodb.js +232 -0
  66. package/src/core/collection/adapters/route.js +201 -0
  67. package/src/core/collection/collection.js +259 -0
  68. package/src/core/collection/list-manager.js +157 -0
  69. package/src/core/compose/base.js +8 -0
  70. package/src/core/compose/component.js +225 -0
  71. package/src/core/compose/features/checkable.js +114 -0
  72. package/src/core/compose/features/disabled.js +25 -0
  73. package/src/core/compose/features/events.js +48 -0
  74. package/src/core/compose/features/icon.js +33 -0
  75. package/src/core/compose/features/index.js +20 -0
  76. package/src/core/compose/features/input.js +92 -0
  77. package/src/core/compose/features/lifecycle.js +69 -0
  78. package/src/core/compose/features/position.js +60 -0
  79. package/src/core/compose/features/ripple.js +32 -0
  80. package/src/core/compose/features/size.js +9 -0
  81. package/src/core/compose/features/style.js +12 -0
  82. package/src/core/compose/features/text.js +17 -0
  83. package/src/core/compose/features/textinput.js +118 -0
  84. package/src/core/compose/features/textlabel.js +28 -0
  85. package/src/core/compose/features/track.js +49 -0
  86. package/src/core/compose/features/variant.js +9 -0
  87. package/src/core/compose/features/withEvents.js +67 -0
  88. package/src/core/compose/index.js +16 -0
  89. package/src/core/compose/pipe.js +69 -0
  90. package/src/core/config.js +140 -0
  91. package/src/core/dom/attributes.js +33 -0
  92. package/src/core/dom/classes.js +70 -0
  93. package/src/core/dom/create.js +133 -0
  94. package/src/core/dom/events.js +175 -0
  95. package/src/core/dom/index.js +5 -0
  96. package/src/core/dom/utils.js +22 -0
  97. package/src/core/index.js +23 -0
  98. package/src/core/layout/index.js +93 -0
  99. package/src/core/state/disabled.js +14 -0
  100. package/src/core/state/emitter.js +63 -0
  101. package/src/core/state/events.js +29 -0
  102. package/src/core/state/index.js +6 -0
  103. package/src/core/state/lifecycle.js +64 -0
  104. package/src/core/state/store.js +112 -0
  105. package/src/core/utils/index.js +39 -0
  106. package/src/core/utils/mobile.js +74 -0
  107. package/src/core/utils/object.js +22 -0
  108. package/src/core/utils/validate.js +37 -0
  109. package/src/index.js +11 -0
  110. package/src/styles/abstract/_base.scss +2 -0
  111. package/src/styles/abstract/_config.scss +28 -0
  112. package/src/styles/abstract/_functions.scss +124 -0
  113. package/src/styles/abstract/_mixins.scss +261 -0
  114. package/src/styles/abstract/_variables.scss +158 -0
  115. package/src/styles/main.scss +78 -0
  116. package/src/styles/themes/_base-theme.scss +49 -0
  117. package/src/styles/themes/_baseline.scss +90 -0
  118. package/src/styles/themes/_forest.scss +71 -0
  119. package/src/styles/themes/_index.scss +6 -0
  120. package/src/styles/themes/_ocean.scss +71 -0
  121. package/src/styles/themes/_sunset.scss +55 -0
@@ -0,0 +1,232 @@
1
+ // src/core/collection/adapters/mongodb.js
2
+
3
+ import { MongoClient, ObjectId } from 'mongodb'
4
+ import { OPERATORS, createBaseAdapter } from './base'
5
+
6
+ const MONGODB_OPERATORS = {
7
+ [OPERATORS.EQ]: '$eq',
8
+ [OPERATORS.NE]: '$ne',
9
+ [OPERATORS.GT]: '$gt',
10
+ [OPERATORS.GTE]: '$gte',
11
+ [OPERATORS.LT]: '$lt',
12
+ [OPERATORS.LTE]: '$lte',
13
+ [OPERATORS.IN]: '$in',
14
+ [OPERATORS.NIN]: '$nin',
15
+ [OPERATORS.CONTAINS]: '$regex',
16
+ [OPERATORS.STARTS_WITH]: '$regex',
17
+ [OPERATORS.ENDS_WITH]: '$regex'
18
+ }
19
+
20
+ export const createMongoAdapter = (config = {}) => {
21
+ const base = createBaseAdapter(config)
22
+ let client = null
23
+ let db = null
24
+ let collection = null
25
+
26
+ const transformDocument = (doc) => {
27
+ if (!doc) return null
28
+ const { _id, ...rest } = doc
29
+ return { id: _id.toString(), ...rest }
30
+ }
31
+
32
+ const transformForMongo = (doc) => {
33
+ if (!doc) return null
34
+ const { id, ...rest } = doc
35
+ return { _id: id ? new ObjectId(id) : new ObjectId(), ...rest }
36
+ }
37
+
38
+ const transformQuery = (query) => {
39
+ const transformed = {}
40
+
41
+ Object.entries(query).forEach(([field, conditions]) => {
42
+ if (typeof conditions === 'object') {
43
+ transformed[field] = Object.entries(conditions).reduce((acc, [op, value]) => {
44
+ const mongoOp = MONGODB_OPERATORS[op]
45
+ if (!mongoOp) return acc
46
+
47
+ if (op === OPERATORS.CONTAINS) {
48
+ acc[mongoOp] = new RegExp(value, 'i')
49
+ } else if (op === OPERATORS.STARTS_WITH) {
50
+ acc[mongoOp] = new RegExp(`^${value}`, 'i')
51
+ } else if (op === OPERATORS.ENDS_WITH) {
52
+ acc[mongoOp] = new RegExp(`${value}$`, 'i')
53
+ } else {
54
+ acc[mongoOp] = value
55
+ }
56
+
57
+ return acc
58
+ }, {})
59
+ } else {
60
+ transformed[field] = conditions
61
+ }
62
+ })
63
+
64
+ return transformed
65
+ }
66
+
67
+ return {
68
+ ...base,
69
+
70
+ connect: async () => {
71
+ try {
72
+ client = new MongoClient(config.uri, {
73
+ useUnifiedTopology: true,
74
+ maxPoolSize: 10,
75
+ ...config.options
76
+ })
77
+
78
+ await client.connect()
79
+ db = client.db(config.dbName)
80
+ collection = db.collection(config.collection)
81
+
82
+ // Optional: Create indexes
83
+ // await collection.createIndex({ field: 1 })
84
+ } catch (error) {
85
+ return base.handleError(new Error(`MongoDB connection failed: ${error.message}`))
86
+ }
87
+ },
88
+
89
+ disconnect: async () => {
90
+ if (client) {
91
+ await client.close()
92
+ client = null
93
+ db = null
94
+ collection = null
95
+ }
96
+ },
97
+
98
+ create: async (items) => {
99
+ if (!collection) {
100
+ return base.handleError(new Error('Not connected to MongoDB'))
101
+ }
102
+
103
+ const docs = items.map(item => transformForMongo(item))
104
+ const result = await collection.insertMany(docs)
105
+
106
+ return items.map((item, index) => ({
107
+ ...item,
108
+ id: result.insertedIds[index].toString()
109
+ }))
110
+ },
111
+
112
+ read: async (query = {}, options = {}) => {
113
+ if (!collection) {
114
+ return base.handleError(new Error('Not connected to MongoDB'))
115
+ }
116
+
117
+ const {
118
+ skip = 0,
119
+ limit = 0,
120
+ sort = {},
121
+ projection = {}
122
+ } = options
123
+
124
+ const mongoQuery = transformQuery(query)
125
+
126
+ const cursor = collection
127
+ .find(mongoQuery)
128
+ .skip(skip)
129
+ .limit(limit)
130
+ .project(projection)
131
+ .sort(sort)
132
+
133
+ const docs = await cursor.toArray()
134
+ return docs.map(doc => transformDocument(doc))
135
+ },
136
+
137
+ update: async (items) => {
138
+ if (!collection) {
139
+ return base.handleError(new Error('Not connected to MongoDB'))
140
+ }
141
+
142
+ const operations = items.map(item => ({
143
+ updateOne: {
144
+ filter: { _id: new ObjectId(item.id) },
145
+ update: { $set: transformForMongo(item) },
146
+ upsert: false
147
+ }
148
+ }))
149
+
150
+ await collection.bulkWrite(operations)
151
+ return items
152
+ },
153
+
154
+ delete: async (ids) => {
155
+ if (!collection) {
156
+ return base.handleError(new Error('Not connected to MongoDB'))
157
+ }
158
+
159
+ const mongoIds = ids.map(id => new ObjectId(id))
160
+ await collection.deleteMany({ _id: { $in: mongoIds } })
161
+ return ids
162
+ },
163
+
164
+ query: async (query = {}, options = {}) => {
165
+ if (!collection) {
166
+ return base.handleError(new Error('Not connected to MongoDB'))
167
+ }
168
+
169
+ const {
170
+ pipeline = [],
171
+ skip = 0,
172
+ limit = 0
173
+ } = options
174
+
175
+ const mongoQuery = transformQuery(query)
176
+
177
+ const aggregation = [
178
+ { $match: mongoQuery },
179
+ ...pipeline
180
+ ]
181
+
182
+ if (skip > 0) {
183
+ aggregation.push({ $skip: skip })
184
+ }
185
+
186
+ if (limit > 0) {
187
+ aggregation.push({ $limit: limit })
188
+ }
189
+
190
+ const docs = await collection.aggregate(aggregation).toArray()
191
+
192
+ return {
193
+ results: docs.map(doc => transformDocument(doc)),
194
+ total: await collection.countDocuments(mongoQuery)
195
+ }
196
+ },
197
+
198
+ watch: (callback, pipeline = []) => {
199
+ if (!collection) {
200
+ return base.handleError(new Error('Not connected to MongoDB'))
201
+ }
202
+
203
+ const changeStream = collection.watch(pipeline, {
204
+ fullDocument: 'updateLookup'
205
+ })
206
+
207
+ changeStream.on('change', async (change) => {
208
+ const { operationType, documentKey, fullDocument } = change
209
+
210
+ switch (operationType) {
211
+ case 'insert':
212
+ case 'update':
213
+ case 'replace':
214
+ callback({
215
+ type: operationType,
216
+ document: transformDocument(fullDocument)
217
+ })
218
+ break
219
+
220
+ case 'delete':
221
+ callback({
222
+ type: operationType,
223
+ documentId: documentKey._id.toString()
224
+ })
225
+ break
226
+ }
227
+ })
228
+
229
+ return () => changeStream.close()
230
+ }
231
+ }
232
+ }
@@ -0,0 +1,201 @@
1
+ // src/core/collection/adapters/route.js
2
+
3
+ import { OPERATORS, createBaseAdapter } from './base'
4
+
5
+ const QUERY_PARAMS = {
6
+ [OPERATORS.EQ]: 'eq',
7
+ [OPERATORS.NE]: 'ne',
8
+ [OPERATORS.GT]: 'gt',
9
+ [OPERATORS.GTE]: 'gte',
10
+ [OPERATORS.LT]: 'lt',
11
+ [OPERATORS.LTE]: 'lte',
12
+ [OPERATORS.IN]: 'in',
13
+ [OPERATORS.NIN]: 'nin',
14
+ [OPERATORS.CONTAINS]: 'contains',
15
+ [OPERATORS.STARTS_WITH]: 'startsWith',
16
+ [OPERATORS.ENDS_WITH]: 'endsWith'
17
+ }
18
+
19
+ export const createRouteAdapter = (config = {}) => {
20
+ const base = createBaseAdapter(config)
21
+ let controller = null
22
+ const cache = new Map()
23
+
24
+ const buildUrl = (endpoint, params = {}) => {
25
+ const url = new URL(config.base + endpoint)
26
+ Object.entries(params).forEach(([key, value]) => {
27
+ if (Array.isArray(value)) {
28
+ value.forEach(v => url.searchParams.append(key, v))
29
+ } else if (value !== null && value !== undefined) {
30
+ url.searchParams.append(key, value)
31
+ }
32
+ })
33
+ return url.toString()
34
+ }
35
+
36
+ const transformQuery = (query) => {
37
+ const params = {}
38
+ Object.entries(query).forEach(([field, conditions]) => {
39
+ if (typeof conditions === 'object') {
40
+ Object.entries(conditions).forEach(([op, value]) => {
41
+ const paramKey = QUERY_PARAMS[op]
42
+ if (paramKey) {
43
+ params[`${field}_${paramKey}`] = value
44
+ }
45
+ })
46
+ } else {
47
+ params[field] = conditions
48
+ }
49
+ })
50
+ return params
51
+ }
52
+
53
+ const request = async (url, options = {}) => {
54
+ if (controller) {
55
+ controller.abort()
56
+ }
57
+ controller = new AbortController()
58
+
59
+ try {
60
+ const response = await fetch(url, {
61
+ ...options,
62
+ headers: {
63
+ 'Content-Type': 'application/json',
64
+ ...config.headers,
65
+ ...options.headers
66
+ },
67
+ signal: controller.signal
68
+ })
69
+
70
+ if (!response.ok) {
71
+ const error = await response.json()
72
+ return base.handleError(new Error(error.message || 'API request failed'))
73
+ }
74
+
75
+ return await response.json()
76
+ } catch (error) {
77
+ if (error.name === 'AbortError') return null
78
+ return base.handleError(error)
79
+ } finally {
80
+ controller = null
81
+ }
82
+ }
83
+
84
+ const getCache = (key) => {
85
+ if (!config.cache) return null
86
+
87
+ const cached = cache.get(key)
88
+ if (!cached) return null
89
+
90
+ const { data, timestamp } = cached
91
+ const now = Date.now()
92
+
93
+ if (now - timestamp > 5 * 60 * 1000) {
94
+ cache.delete(key)
95
+ return null
96
+ }
97
+
98
+ return data
99
+ }
100
+
101
+ const setCache = (key, data) => {
102
+ if (!config.cache) return
103
+ cache.set(key, { data, timestamp: Date.now() })
104
+ }
105
+
106
+ return {
107
+ ...base,
108
+
109
+ create: async (items) => {
110
+ const url = buildUrl(config.endpoints.create)
111
+ const response = await request(url, {
112
+ method: 'POST',
113
+ body: JSON.stringify({ items })
114
+ })
115
+ return response.items
116
+ },
117
+
118
+ read: async (query = {}, options = {}) => {
119
+ const {
120
+ page = 1,
121
+ limit = 20,
122
+ sort,
123
+ fields
124
+ } = options
125
+
126
+ const params = {
127
+ ...transformQuery(query),
128
+ page,
129
+ limit,
130
+ sort,
131
+ fields
132
+ }
133
+
134
+ const url = buildUrl(config.endpoints.list, params)
135
+ const cacheKey = url.toString()
136
+
137
+ const cached = getCache(cacheKey)
138
+ if (cached) return cached
139
+
140
+ const response = await request(url)
141
+ setCache(cacheKey, response.items)
142
+
143
+ return response.items
144
+ },
145
+
146
+ update: async (items) => {
147
+ const url = buildUrl(config.endpoints.update)
148
+ const response = await request(url, {
149
+ method: 'PUT',
150
+ body: JSON.stringify({ items })
151
+ })
152
+ return response.items
153
+ },
154
+
155
+ delete: async (ids) => {
156
+ const url = buildUrl(config.endpoints.delete)
157
+ const response = await request(url, {
158
+ method: 'DELETE',
159
+ body: JSON.stringify({ ids })
160
+ })
161
+ return response.ids
162
+ },
163
+
164
+ query: async (query = {}, options = {}) => {
165
+ const {
166
+ page = 1,
167
+ limit = 20,
168
+ sort,
169
+ fields,
170
+ ...rest
171
+ } = options
172
+
173
+ const params = {
174
+ ...transformQuery(query),
175
+ ...rest,
176
+ page,
177
+ limit,
178
+ sort,
179
+ fields
180
+ }
181
+
182
+ const url = buildUrl(config.endpoints.list, params)
183
+ const response = await request(url)
184
+
185
+ return {
186
+ items: response.items,
187
+ total: response.total,
188
+ page: response.page,
189
+ pages: response.pages
190
+ }
191
+ },
192
+
193
+ disconnect: () => {
194
+ if (controller) {
195
+ controller.abort()
196
+ controller = null
197
+ }
198
+ cache.clear()
199
+ }
200
+ }
201
+ }
@@ -0,0 +1,259 @@
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
+ }