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,45 @@
1
+ // src/components/container/container.js
2
+ import { PREFIX } from '../../core/config'
3
+ import { pipe } from '../../core/compose'
4
+ import { createBase, withElement } from '../../core/compose/component'
5
+ import { withEvents, withLifecycle } from '../../core/compose/features'
6
+ import { withAPI } from './api'
7
+
8
+ /**
9
+ * Creates a new Container component
10
+ * @param {Object} config - Container configuration
11
+ * @param {string} [config.variant] - Visual variant
12
+ * @param {number} [config.elevation] - Elevation level
13
+ * @param {string} [config.class] - Additional CSS classes
14
+ */
15
+ const createContainer = (config = {}) => {
16
+ const baseConfig = {
17
+ ...config,
18
+ componentName: 'container',
19
+ prefix: PREFIX
20
+ }
21
+
22
+ try {
23
+ return pipe(
24
+ createBase,
25
+ withElement({
26
+ tag: 'div',
27
+ componentName: 'container',
28
+ className: [
29
+ config.variant && `${PREFIX}-container--${config.variant}`,
30
+ config.elevation && `${PREFIX}-container--elevation-${config.elevation}`,
31
+ config.class
32
+ ]
33
+ }),
34
+ withEvents(),
35
+ withLifecycle(),
36
+ comp => withAPI({
37
+ lifecycle: comp.lifecycle
38
+ })(comp)
39
+ )(baseConfig)
40
+ } catch (error) {
41
+ throw new Error(`Failed to create container: ${error.message}`)
42
+ }
43
+ }
44
+
45
+ export default createContainer
@@ -0,0 +1,2 @@
1
+ // src/components/container/index.js
2
+ export { default } from './container.js'
@@ -0,0 +1,59 @@
1
+ // src/components/container/styles.scss
2
+ @use 'sass:map';
3
+ @use '../../styles/abstract/config' as c;
4
+
5
+ .#{c.$prefix}-container {
6
+ @include c.shape('medium');
7
+ @include c.motion-transition(
8
+ background-color,
9
+ box-shadow
10
+ );
11
+
12
+ padding: 16px;
13
+ background-color: var(--mtrl-sys-color-surface-container);
14
+
15
+ &--low {
16
+ background-color: var(--mtrl-sys-color-surface-container-low);
17
+ }
18
+
19
+ &--lowest {
20
+ background-color: var(--mtrl-sys-color-surface-container-lowest);
21
+ }
22
+
23
+ &--high {
24
+ background-color: var(--mtrl-sys-color-surface-container-high);
25
+ }
26
+
27
+ &--highest {
28
+ background-color: var(--mtrl-sys-color-surface-container-highest);
29
+ }
30
+
31
+ // Elevation variants
32
+ &--elevation-0 {
33
+ @include c.elevation(0);
34
+ }
35
+
36
+ &--elevation-1 {
37
+ @include c.elevation(1);
38
+ }
39
+
40
+ &--elevation-2 {
41
+ @include c.elevation(2);
42
+ }
43
+
44
+ &--elevation-3 {
45
+ @include c.elevation(3);
46
+ }
47
+
48
+ &--elevation-4 {
49
+ @include c.elevation(4);
50
+ }
51
+
52
+ @include c.reduced-motion {
53
+ transition: none;
54
+ }
55
+
56
+ @include c.high-contrast {
57
+ border: 1px solid currentColor;
58
+ }
59
+ }
@@ -0,0 +1,89 @@
1
+ // src/components/list/constants.js
2
+
3
+ /**
4
+ * List types/variants
5
+ */
6
+ export const LIST_TYPES = {
7
+ DEFAULT: 'default', // Standard list
8
+ SINGLE_SELECT: 'single', // Single selection list
9
+ MULTI_SELECT: 'multi', // Multiple selection list
10
+ RADIO: 'radio' // Radio button list
11
+ }
12
+
13
+ /**
14
+ * List layout variants
15
+ */
16
+ export const LIST_LAYOUTS = {
17
+ HORIZONTAL: 'horizontal', // Default horizontal layout
18
+ VERTICAL: 'vertical' // Items with more content stacked vertically
19
+ }
20
+
21
+ /**
22
+ * List element class names
23
+ */
24
+ export const LIST_CLASSES = {
25
+ ROOT: 'list',
26
+ GROUP: 'list-group',
27
+ GROUP_TITLE: 'list-group-title',
28
+ DIVIDER: 'list-divider',
29
+ SECTION: 'list-section',
30
+ SECTION_TITLE: 'list-section-title'
31
+ }
32
+
33
+ /**
34
+ * List configuration schema
35
+ */
36
+ export const LIST_SCHEMA = {
37
+ type: 'object',
38
+ properties: {
39
+ type: {
40
+ type: 'string',
41
+ enum: Object.values(LIST_TYPES),
42
+ default: LIST_TYPES.DEFAULT
43
+ },
44
+ layout: {
45
+ type: 'string',
46
+ enum: Object.values(LIST_LAYOUTS),
47
+ default: LIST_LAYOUTS.HORIZONTAL
48
+ },
49
+ items: {
50
+ type: 'array',
51
+ items: {
52
+ type: 'object'
53
+ },
54
+ default: []
55
+ },
56
+ groups: {
57
+ type: 'array',
58
+ items: {
59
+ type: 'object',
60
+ properties: {
61
+ id: { type: 'string', required: true },
62
+ title: { type: 'string' },
63
+ items: { type: 'array' }
64
+ }
65
+ },
66
+ optional: true
67
+ },
68
+ sections: {
69
+ type: 'array',
70
+ items: {
71
+ type: 'object',
72
+ properties: {
73
+ id: { type: 'string', required: true },
74
+ title: { type: 'string', required: true },
75
+ items: { type: 'array', required: true }
76
+ }
77
+ },
78
+ optional: true
79
+ },
80
+ disabled: {
81
+ type: 'boolean',
82
+ default: false
83
+ },
84
+ class: {
85
+ type: 'string',
86
+ optional: true
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,2 @@
1
+ // src/components/list/index.js
2
+ export { default } from './list.js'
@@ -0,0 +1,147 @@
1
+ // src/components/list/list-item.js
2
+
3
+ import { PREFIX } from '../../core/config'
4
+ import { pipe } from '../../core/compose'
5
+ import { createBase, withElement } from '../../core/compose/component'
6
+ import { withEvents, withDisabled } from '../../core/compose/features'
7
+
8
+ /**
9
+ * Supported list item layouts
10
+ */
11
+ export const LIST_ITEM_LAYOUTS = {
12
+ HORIZONTAL: 'horizontal', // Default horizontal layout
13
+ VERTICAL: 'vertical' // Stacked layout with vertical alignment
14
+ }
15
+
16
+ /**
17
+ * Creates a DOM element with optional class and content
18
+ * @param {string} tag - HTML tag name
19
+ * @param {string} className - CSS class name
20
+ * @param {string|HTMLElement} [content] - Element content or child element
21
+ * @returns {HTMLElement} Created element
22
+ */
23
+ const createElement = (tag, className, content) => {
24
+ const element = document.createElement(tag)
25
+ element.className = className
26
+ if (content) {
27
+ if (typeof content === 'string') {
28
+ element.textContent = content
29
+ } else {
30
+ element.appendChild(content)
31
+ }
32
+ }
33
+ return element
34
+ }
35
+
36
+ /**
37
+ * Creates a list item component
38
+ * @param {Object} config - List item configuration
39
+ * @param {string} [config.layout='horizontal'] - Item layout (horizontal/vertical)
40
+ * @param {string|HTMLElement} [config.leading] - Leading content (icon/avatar)
41
+ * @param {string} [config.headline] - Primary text
42
+ * @param {string} [config.supportingText] - Secondary text
43
+ * @param {string|HTMLElement} [config.trailing] - Trailing content (icon/meta)
44
+ * @param {string} [config.overline] - Text above headline (vertical only)
45
+ * @param {string|HTMLElement} [config.meta] - Meta information (vertical only)
46
+ * @param {boolean} [config.disabled] - Disabled state
47
+ * @param {boolean} [config.selected] - Selected state
48
+ * @param {string} [config.class] - Additional CSS classes
49
+ * @param {string} [config.role='listitem'] - ARIA role
50
+ */
51
+ const createListItem = (config = {}) => {
52
+ const baseConfig = {
53
+ ...config,
54
+ componentName: 'list-item',
55
+ prefix: PREFIX
56
+ }
57
+
58
+ const createContent = (component) => {
59
+ const { element } = component
60
+ const { prefix } = baseConfig
61
+ const isVertical = config.layout === LIST_ITEM_LAYOUTS.VERTICAL
62
+
63
+ // Create content container
64
+ const content = createElement('div', `${prefix}-list-item-content`)
65
+
66
+ // Add leading content (icon/avatar)
67
+ if (config.leading) {
68
+ const leading = createElement('div', `${prefix}-list-item-leading`)
69
+ if (typeof config.leading === 'string') {
70
+ leading.innerHTML = config.leading
71
+ } else {
72
+ leading.appendChild(config.leading)
73
+ }
74
+ element.appendChild(leading)
75
+ }
76
+
77
+ // Text wrapper for proper alignment
78
+ const textWrapper = createElement('div', `${prefix}-list-item-text`)
79
+
80
+ // Add overline text (vertical only)
81
+ if (isVertical && config.overline) {
82
+ const overline = createElement('div', `${prefix}-list-item-overline`, config.overline)
83
+ textWrapper.appendChild(overline)
84
+ }
85
+
86
+ // Add headline (primary text)
87
+ if (config.headline) {
88
+ const headline = createElement('div', `${prefix}-list-item-headline`, config.headline)
89
+ textWrapper.appendChild(headline)
90
+ }
91
+
92
+ // Add supporting text (secondary text)
93
+ if (config.supportingText) {
94
+ const supporting = createElement('div', `${prefix}-list-item-supporting`, config.supportingText)
95
+ textWrapper.appendChild(supporting)
96
+ }
97
+
98
+ content.appendChild(textWrapper)
99
+
100
+ // Add meta information (vertical only)
101
+ if (isVertical && config.meta) {
102
+ const meta = createElement('div', `${prefix}-list-item-meta`)
103
+ if (typeof config.meta === 'string') {
104
+ meta.textContent = config.meta
105
+ } else {
106
+ meta.appendChild(config.meta)
107
+ }
108
+ content.appendChild(meta)
109
+ }
110
+
111
+ element.appendChild(content)
112
+
113
+ // Add trailing content (icon/meta)
114
+ if (config.trailing) {
115
+ const trailing = createElement('div', `${prefix}-list-item-trailing`)
116
+ if (typeof config.trailing === 'string') {
117
+ trailing.innerHTML = config.trailing
118
+ } else {
119
+ trailing.appendChild(config.trailing)
120
+ }
121
+ element.appendChild(trailing)
122
+ }
123
+
124
+ // Handle selected state
125
+ if (config.selected) {
126
+ element.setAttribute('aria-selected', 'true')
127
+ element.classList.add(`${prefix}-list-item--selected`)
128
+ }
129
+
130
+ return component
131
+ }
132
+
133
+ return pipe(
134
+ createBase,
135
+ withEvents(),
136
+ withElement({
137
+ tag: 'div',
138
+ role: config.role || 'listitem',
139
+ componentName: 'list-item',
140
+ className: `${config.layout === LIST_ITEM_LAYOUTS.VERTICAL ? 'vertical' : ''} ${config.class || ''}`
141
+ }),
142
+ withDisabled(),
143
+ createContent
144
+ )(baseConfig)
145
+ }
146
+
147
+ export default createListItem
@@ -0,0 +1,267 @@
1
+ // src/components/list/list.js
2
+
3
+ import { PREFIX } from '../../core/config'
4
+ import { pipe } from '../../core/compose'
5
+ import { createBase, withElement } from '../../core/compose/component'
6
+ import { withEvents, withDisabled, withLifecycle } from '../../core/compose/features'
7
+ import createListItem from './list-item'
8
+ import { LIST_TYPES, LIST_LAYOUTS, LIST_CLASSES } from './constants'
9
+
10
+ /**
11
+ * Creates a divider element
12
+ * @param {string} prefix - CSS class prefix
13
+ * @returns {HTMLElement} Divider element
14
+ */
15
+ const createDivider = (prefix) => {
16
+ const divider = document.createElement('div')
17
+ divider.className = `${prefix}-${LIST_CLASSES.DIVIDER}`
18
+ divider.setAttribute('role', 'separator')
19
+ return divider
20
+ }
21
+
22
+ /**
23
+ * Creates a section title element
24
+ * @param {string} title - Section title text
25
+ * @param {string} prefix - CSS class prefix
26
+ * @returns {HTMLElement} Section title element
27
+ */
28
+ const createSectionTitle = (title, prefix) => {
29
+ const titleEl = document.createElement('div')
30
+ titleEl.className = `${prefix}-${LIST_CLASSES.SECTION_TITLE}`
31
+ titleEl.textContent = title
32
+ return titleEl
33
+ }
34
+
35
+ /**
36
+ * Creates a list component
37
+ * @param {Object} config - List configuration
38
+ */
39
+ const createList = (config = {}) => {
40
+ const baseConfig = {
41
+ ...config,
42
+ componentName: 'list',
43
+ prefix: PREFIX
44
+ }
45
+
46
+ const createContent = (component) => {
47
+ const { element, prefix } = component
48
+ const items = new Map()
49
+ const selectedItems = new Set()
50
+
51
+ // Set list type
52
+ element.setAttribute('data-type', config.type || LIST_TYPES.DEFAULT)
53
+
54
+ // Handle keyboard navigation
55
+ const handleKeyDown = (event) => {
56
+ const focusedItem = document.activeElement
57
+ if (!focusedItem?.classList.contains(`${prefix}-list-item`)) return
58
+
59
+ const items = Array.from(element.querySelectorAll(`.${prefix}-list-item`))
60
+ const currentIndex = items.indexOf(focusedItem)
61
+
62
+ switch (event.key) {
63
+ case 'ArrowDown':
64
+ case 'ArrowRight':
65
+ event.preventDefault()
66
+ const nextItem = items[currentIndex + 1]
67
+ if (nextItem) nextItem.focus()
68
+ break
69
+ case 'ArrowUp':
70
+ case 'ArrowLeft':
71
+ event.preventDefault()
72
+ const prevItem = items[currentIndex - 1]
73
+ if (prevItem) prevItem.focus()
74
+ break
75
+ case 'Home':
76
+ event.preventDefault()
77
+ items[0]?.focus()
78
+ break
79
+ case 'End':
80
+ event.preventDefault()
81
+ items[items.length - 1]?.focus()
82
+ break
83
+ case ' ':
84
+ case 'Enter':
85
+ event.preventDefault()
86
+ handleItemClick(focusedItem)
87
+ break
88
+ }
89
+ }
90
+
91
+ // Handle item selection
92
+ const handleItemClick = (itemElement) => {
93
+ const id = itemElement.dataset.id
94
+ if (!id) return
95
+
96
+ const itemData = items.get(id)
97
+ if (!itemData || itemData.disabled) return
98
+
99
+ switch (config.type) {
100
+ case LIST_TYPES.SINGLE_SELECT:
101
+ // Deselect previously selected item
102
+ selectedItems.forEach(selectedId => {
103
+ const selected = items.get(selectedId)
104
+ if (selected) {
105
+ selected.element.classList.remove(`${prefix}-list-item--selected`)
106
+ selected.element.setAttribute('aria-selected', 'false')
107
+ }
108
+ })
109
+ selectedItems.clear()
110
+
111
+ // Select new item
112
+ itemElement.classList.add(`${prefix}-list-item--selected`)
113
+ itemElement.setAttribute('aria-selected', 'true')
114
+ selectedItems.add(id)
115
+ break
116
+
117
+ case LIST_TYPES.MULTI_SELECT:
118
+ const isSelected = selectedItems.has(id)
119
+ if (isSelected) {
120
+ itemElement.classList.remove(`${prefix}-list-item--selected`)
121
+ itemElement.setAttribute('aria-selected', 'false')
122
+ selectedItems.delete(id)
123
+ } else {
124
+ itemElement.classList.add(`${prefix}-list-item--selected`)
125
+ itemElement.setAttribute('aria-selected', 'true')
126
+ selectedItems.add(id)
127
+ }
128
+ break
129
+ }
130
+
131
+ component.emit('selectionChange', {
132
+ selected: Array.from(selectedItems),
133
+ item: itemData,
134
+ type: config.type
135
+ })
136
+ }
137
+
138
+ // Create items from configuration
139
+ const createItems = (itemsConfig = [], container = element) => {
140
+ itemsConfig.forEach((itemConfig, index) => {
141
+ if (itemConfig.divider) {
142
+ container.appendChild(createDivider(prefix))
143
+ return
144
+ }
145
+
146
+ const item = createListItem({
147
+ ...itemConfig,
148
+ layout: config.layout || LIST_LAYOUTS.HORIZONTAL,
149
+ role: config.type === LIST_TYPES.RADIO ? 'radio' : 'option'
150
+ })
151
+
152
+ item.element.dataset.id = itemConfig.id
153
+ item.element.tabIndex = index === 0 ? 0 : -1
154
+ items.set(itemConfig.id, item)
155
+
156
+ if (itemConfig.selected) {
157
+ selectedItems.add(itemConfig.id)
158
+ item.element.classList.add(`${prefix}-list-item--selected`)
159
+ item.element.setAttribute('aria-selected', 'true')
160
+ }
161
+
162
+ container.appendChild(item.element)
163
+ })
164
+ }
165
+
166
+ // Create sections if configured
167
+ if (config.sections?.length) {
168
+ config.sections.forEach(section => {
169
+ const sectionEl = document.createElement('div')
170
+ sectionEl.className = `${prefix}-${LIST_CLASSES.SECTION}`
171
+ sectionEl.setAttribute('role', 'group')
172
+ if (section.title) {
173
+ sectionEl.appendChild(createSectionTitle(section.title, prefix))
174
+ }
175
+ createItems(section.items, sectionEl)
176
+ element.appendChild(sectionEl)
177
+ })
178
+ } else {
179
+ createItems(config.items)
180
+ }
181
+
182
+ // Add event listeners
183
+ element.addEventListener('click', (event) => {
184
+ const item = event.target.closest(`.${prefix}-list-item`)
185
+ if (item) handleItemClick(item)
186
+ })
187
+
188
+ element.addEventListener('keydown', handleKeyDown)
189
+
190
+ // Clean up
191
+ if (component.lifecycle) {
192
+ const originalDestroy = component.lifecycle.destroy
193
+ component.lifecycle.destroy = () => {
194
+ items.clear()
195
+ selectedItems.clear()
196
+ element.removeEventListener('keydown', handleKeyDown)
197
+ originalDestroy?.()
198
+ }
199
+ }
200
+
201
+ return {
202
+ ...component,
203
+ items,
204
+ selectedItems,
205
+
206
+ // Public methods
207
+ getSelected: () => Array.from(selectedItems),
208
+
209
+ setSelected: (ids) => {
210
+ selectedItems.clear()
211
+ items.forEach((item, id) => {
212
+ const isSelected = ids.includes(id)
213
+ item.element.classList.toggle(`${prefix}-list-item--selected`, isSelected)
214
+ item.element.setAttribute('aria-selected', isSelected.toString())
215
+ if (isSelected) selectedItems.add(id)
216
+ })
217
+ component.emit('selectionChange', {
218
+ selected: Array.from(selectedItems),
219
+ type: config.type
220
+ })
221
+ },
222
+
223
+ addItem: (itemConfig) => {
224
+ if (items.has(itemConfig.id)) return
225
+
226
+ const item = createListItem({
227
+ ...itemConfig,
228
+ layout: config.layout || LIST_LAYOUTS.HORIZONTAL
229
+ })
230
+
231
+ item.element.dataset.id = itemConfig.id
232
+ items.set(itemConfig.id, item)
233
+ element.appendChild(item.element)
234
+
235
+ component.emit('itemAdded', { id: itemConfig.id, item })
236
+ },
237
+
238
+ removeItem: (id) => {
239
+ const item = items.get(id)
240
+ if (!item) return
241
+
242
+ item.element.remove()
243
+ items.delete(id)
244
+ selectedItems.delete(id)
245
+
246
+ component.emit('itemRemoved', { id, item })
247
+ }
248
+ }
249
+ }
250
+
251
+ return pipe(
252
+ createBase,
253
+ withEvents(),
254
+ withElement({
255
+ tag: 'div',
256
+ role: config.type === LIST_TYPES.DEFAULT ? 'list' : 'listbox',
257
+ 'aria-multiselectable': config.type === LIST_TYPES.MULTI_SELECT ? 'true' : undefined,
258
+ componentName: LIST_CLASSES.ROOT,
259
+ className: config.class
260
+ }),
261
+ withDisabled(),
262
+ withLifecycle(),
263
+ createContent
264
+ )(baseConfig)
265
+ }
266
+
267
+ export default createList