lightview 1.8.1-b → 2.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 (224) hide show
  1. package/.agent/workflows/daisyui-component-migration.md +155 -0
  2. package/.codacy/cli.sh +149 -0
  3. package/.codacy/codacy.yaml +15 -0
  4. package/.github/instructions/codacy.instructions.md +72 -0
  5. package/.wranglerignore +21 -0
  6. package/README.md +1331 -21
  7. package/_headers +4 -0
  8. package/build.js +70 -0
  9. package/components/actions/button.js +151 -0
  10. package/components/actions/dropdown.js +120 -0
  11. package/components/actions/modal.js +146 -0
  12. package/components/actions/swap.js +118 -0
  13. package/components/daisyui.js +288 -0
  14. package/components/data-display/accordion.js +128 -0
  15. package/components/data-display/alert.js +112 -0
  16. package/components/data-display/avatar.js +170 -0
  17. package/components/data-display/badge.js +82 -0
  18. package/components/data-display/card.js +151 -0
  19. package/components/data-display/carousel.js +94 -0
  20. package/components/data-display/chart.js +220 -0
  21. package/components/data-display/chat.js +128 -0
  22. package/components/data-display/collapse.js +103 -0
  23. package/components/data-display/countdown.js +69 -0
  24. package/components/data-display/diff.js +111 -0
  25. package/components/data-display/kbd.js +65 -0
  26. package/components/data-display/loading.js +75 -0
  27. package/components/data-display/progress.js +79 -0
  28. package/components/data-display/radial-progress.js +88 -0
  29. package/components/data-display/skeleton.js +66 -0
  30. package/components/data-display/stats.js +159 -0
  31. package/components/data-display/table.js +146 -0
  32. package/components/data-display/timeline.js +146 -0
  33. package/components/data-display/toast.js +72 -0
  34. package/components/data-display/tooltip.js +74 -0
  35. package/components/data-input/checkbox.js +253 -0
  36. package/components/data-input/file-input.js +224 -0
  37. package/components/data-input/input.js +264 -0
  38. package/components/data-input/radio.js +338 -0
  39. package/components/data-input/range.js +204 -0
  40. package/components/data-input/rating.js +219 -0
  41. package/components/data-input/select.js +287 -0
  42. package/components/data-input/textarea.js +287 -0
  43. package/components/data-input/toggle.js +201 -0
  44. package/components/index.js +137 -0
  45. package/components/layout/divider.js +72 -0
  46. package/components/layout/drawer.js +142 -0
  47. package/components/layout/footer.js +100 -0
  48. package/components/layout/hero.js +109 -0
  49. package/components/layout/indicator.js +90 -0
  50. package/components/layout/join.js +78 -0
  51. package/components/layout/navbar.js +110 -0
  52. package/components/navigation/breadcrumbs.js +91 -0
  53. package/components/navigation/dock.js +103 -0
  54. package/components/navigation/menu.js +126 -0
  55. package/components/navigation/pagination.js +105 -0
  56. package/components/navigation/steps.js +89 -0
  57. package/components/navigation/tabs.css +177 -0
  58. package/components/navigation/tabs.js +123 -0
  59. package/components/theme/theme-switch.css +65 -0
  60. package/components/theme/theme-switch.js +177 -0
  61. package/docs/about.html +164 -0
  62. package/docs/api/computed.html +184 -0
  63. package/docs/api/effects.html +173 -0
  64. package/docs/api/elements.html +180 -0
  65. package/docs/api/enhance.html +225 -0
  66. package/docs/api/hypermedia.html +165 -0
  67. package/docs/api/index.html +178 -0
  68. package/docs/api/nav.html +18 -0
  69. package/docs/api/signals.html +136 -0
  70. package/docs/api/state.html +217 -0
  71. package/docs/assets/images/logo-favicon.svg +42 -0
  72. package/docs/assets/images/logo-static.svg +40 -0
  73. package/docs/assets/images/logo.svg +66 -0
  74. package/docs/assets/js/examplify.js +395 -0
  75. package/docs/assets/styles/site.css +1102 -0
  76. package/docs/assets/styles/themes.css +236 -0
  77. package/docs/components/accordion.html +439 -0
  78. package/docs/components/alert.html +528 -0
  79. package/docs/components/avatar.html +586 -0
  80. package/docs/components/badge.html +531 -0
  81. package/docs/components/breadcrumbs.html +278 -0
  82. package/docs/components/button.html +579 -0
  83. package/docs/components/card.html +561 -0
  84. package/docs/components/carousel.html +286 -0
  85. package/docs/components/chart-area.html +702 -0
  86. package/docs/components/chart-bar.html +782 -0
  87. package/docs/components/chart-column.html +735 -0
  88. package/docs/components/chart-line.html +794 -0
  89. package/docs/components/chart-pie.html +823 -0
  90. package/docs/components/chart.html +612 -0
  91. package/docs/components/chat.html +547 -0
  92. package/docs/components/checkbox.html +641 -0
  93. package/docs/components/collapse.html +536 -0
  94. package/docs/components/component-nav.html +53 -0
  95. package/docs/components/countdown.html +470 -0
  96. package/docs/components/diff.html +245 -0
  97. package/docs/components/divider.html +240 -0
  98. package/docs/components/dock.html +277 -0
  99. package/docs/components/drawer.html +515 -0
  100. package/docs/components/dropdown.html +479 -0
  101. package/docs/components/file-input.html +591 -0
  102. package/docs/components/footer.html +301 -0
  103. package/docs/components/gallery.html +504 -0
  104. package/docs/components/hero.html +264 -0
  105. package/docs/components/index.css +840 -0
  106. package/docs/components/index.html +735 -0
  107. package/docs/components/indicator.html +342 -0
  108. package/docs/components/input.html +644 -0
  109. package/docs/components/join.html +285 -0
  110. package/docs/components/kbd.html +322 -0
  111. package/docs/components/loading.html +521 -0
  112. package/docs/components/menu.html +461 -0
  113. package/docs/components/modal.html +639 -0
  114. package/docs/components/navbar.html +321 -0
  115. package/docs/components/pagination.html +279 -0
  116. package/docs/components/progress.html +514 -0
  117. package/docs/components/radial-progress.html +434 -0
  118. package/docs/components/radio.html +655 -0
  119. package/docs/components/range.html +611 -0
  120. package/docs/components/rating.html +642 -0
  121. package/docs/components/select.html +696 -0
  122. package/docs/components/sidebar-setup.js +93 -0
  123. package/docs/components/skeleton.html +447 -0
  124. package/docs/components/spinner.html +68 -0
  125. package/docs/components/stats.html +486 -0
  126. package/docs/components/steps.html +356 -0
  127. package/docs/components/swap.html +517 -0
  128. package/docs/components/switch.html +68 -0
  129. package/docs/components/table.html +668 -0
  130. package/docs/components/tabs.html +506 -0
  131. package/docs/components/text-input.html +68 -0
  132. package/docs/components/textarea.html +603 -0
  133. package/docs/components/timeline.html +487 -0
  134. package/docs/components/toast.html +474 -0
  135. package/docs/components/toggle.html +564 -0
  136. package/docs/components/tooltip.html +423 -0
  137. package/docs/examples/getting-started-example.html +40 -0
  138. package/docs/examples/index.html +93 -0
  139. package/docs/getting-started/index.html +739 -0
  140. package/docs/getting-started/reviews.html +23 -0
  141. package/docs/getting-started/reviews.odom +108 -0
  142. package/docs/getting-started/reviews.vdom +84 -0
  143. package/docs/index.html +134 -0
  144. package/docs/playground.html +416 -0
  145. package/docs/router.html +285 -0
  146. package/docs/styles/index.html +190 -0
  147. package/functions/_middleware.js +32 -0
  148. package/index.html +309 -0
  149. package/lightview-router.js +364 -0
  150. package/lightview-x.js +1577 -0
  151. package/lightview.js +658 -1109
  152. package/lightview.js.backup +793 -0
  153. package/middleware/locale.js +25 -0
  154. package/middleware/markdown.js +44 -0
  155. package/middleware/notFound.js +37 -0
  156. package/package.json +27 -41
  157. package/watch.js +92 -0
  158. package/wrangler.toml +12 -0
  159. package/.idea/lightview.iml +0 -12
  160. package/.idea/modules.xml +0 -8
  161. package/.idea/vcs.xml +0 -6
  162. package/LICENSE +0 -21
  163. package/codepen-no-tabs-embed.css +0 -2
  164. package/components/chart/chart.html +0 -17
  165. package/components/chart/example.html +0 -32
  166. package/components/chart.html +0 -83
  167. package/components/components.js +0 -113
  168. package/components/gantt/example.html +0 -22
  169. package/components/gantt/gantt.html +0 -42
  170. package/components/gauge/example.html +0 -28
  171. package/components/gauge/gauge.html +0 -20
  172. package/components/gauge.html +0 -60
  173. package/components/orgchart/example.html +0 -25
  174. package/components/orgchart/orgchart.html +0 -41
  175. package/components/repl/code-editor.html +0 -64
  176. package/components/repl/editor.html +0 -37
  177. package/components/repl/editorjs-inline-tool/index.js +0 -3
  178. package/components/repl/editorjs-inline-tool/inline-tools.js +0 -28
  179. package/components/repl/editorjs-inline-tool/tool.js +0 -175
  180. package/components/repl/repl-with-wysiwyg.html +0 -355
  181. package/components/repl/repl.html +0 -345
  182. package/components/repl/sup.js +0 -44
  183. package/components/repl/wysiwyg-repl.html +0 -258
  184. package/components/timeline/example.html +0 -33
  185. package/components/timeline/timeline.html +0 -44
  186. package/components/timeline.html +0 -81
  187. package/examples/anchor.html +0 -11
  188. package/examples/chart.html +0 -34
  189. package/examples/counter.html +0 -26
  190. package/examples/counter.test.mjs +0 -47
  191. package/examples/counter2.html +0 -26
  192. package/examples/directives.html +0 -79
  193. package/examples/foreign.html +0 -50
  194. package/examples/forgeinform.html +0 -98
  195. package/examples/form.html +0 -61
  196. package/examples/gauge.html +0 -18
  197. package/examples/invalid-template-literals.html +0 -44
  198. package/examples/medium/remote.html +0 -60
  199. package/examples/message.html +0 -18
  200. package/examples/nested.html +0 -11
  201. package/examples/object-bound-form.html +0 -34
  202. package/examples/remote-server.js +0 -51
  203. package/examples/remote.html +0 -34
  204. package/examples/remote.json +0 -1
  205. package/examples/scratch.html +0 -69
  206. package/examples/sensors/index.html +0 -30
  207. package/examples/sensors/sensor-server.js +0 -30
  208. package/examples/shared.html +0 -41
  209. package/examples/template.html +0 -33
  210. package/examples/timeline.html +0 -21
  211. package/examples/todo.html +0 -38
  212. package/examples/top.html +0 -10
  213. package/examples/types.html +0 -94
  214. package/examples/xor.html +0 -62
  215. package/jest-puppeteer.config.js +0 -5
  216. package/jest.config.json +0 -12
  217. package/sites/client.html +0 -48
  218. package/sites/index.html +0 -247
  219. package/test/basic.html +0 -93
  220. package/test/basic.test.mjs +0 -315
  221. package/test/extended.html +0 -29
  222. package/test/extended.test.mjs +0 -448
  223. package/types.js +0 -534
  224. package/unsplash.key +0 -1
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Lightview Components - Range
3
+ * A range slider component using DaisyUI 5 styling
4
+ * @see https://daisyui.com/components/range/
5
+ *
6
+ * Uses DaisyUI's fieldset pattern for labeled ranges:
7
+ * <fieldset class="fieldset">
8
+ * <legend class="fieldset-legend">Label</legend>
9
+ * <input type="range" class="range" />
10
+ * <p class="label">Helper</p>
11
+ * </fieldset>
12
+ */
13
+
14
+ import '../daisyui.js';
15
+
16
+ /**
17
+ * Range Component
18
+ * @param {Object} props - Range properties
19
+ * @param {number|Signal} props.value - Current value (controlled)
20
+ * @param {number} props.defaultValue - Default value (uncontrolled)
21
+ * @param {number} props.min - Minimum value (default: 0)
22
+ * @param {number} props.max - Maximum value (default: 100)
23
+ * @param {number} props.step - Step increment (default: 1)
24
+ * @param {string} props.size - 'xs' | 'sm' | 'md' | 'lg' (default: 'md')
25
+ * @param {string} props.color - 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error'
26
+ * @param {boolean} props.disabled - Disable range
27
+ * @param {string} props.label - Label text
28
+ * @param {string} props.helper - Helper text
29
+ * @param {boolean} props.showValue - Show current value display
30
+ * @param {Function} props.formatValue - Format function for displayed value
31
+ * @param {Function} props.onChange - Change handler
32
+ * @param {boolean} props.useShadow - Render in Shadow DOM
33
+ */
34
+ const Range = (props = {}) => {
35
+ const { tags, signal } = window.Lightview || {};
36
+ const LVX = window.LightviewX || {};
37
+
38
+ if (!tags) {
39
+ console.error('Lightview not found');
40
+ return null;
41
+ }
42
+
43
+ const { div, input, fieldset, legend, p, span, shadowDOM } = tags;
44
+
45
+ const {
46
+ value,
47
+ defaultValue = 0,
48
+ min = 0,
49
+ max = 100,
50
+ step = 1,
51
+ size = 'md',
52
+ color,
53
+ disabled = false,
54
+ label: labelText,
55
+ helper,
56
+ showValue = false,
57
+ formatValue = (v) => v,
58
+ onChange,
59
+ name,
60
+ id,
61
+ class: className = '',
62
+ useShadow,
63
+ ...rest
64
+ } = props;
65
+
66
+ const rangeId = id || `range-${Math.random().toString(36).slice(2, 9)}`;
67
+
68
+ // Internal state
69
+ const internalValue = signal ? signal(defaultValue) : { value: defaultValue };
70
+
71
+ const isControlled = value !== undefined;
72
+
73
+ const getValue = () => {
74
+ if (isControlled) {
75
+ return typeof value === 'function' ? value() :
76
+ (value && typeof value.value !== 'undefined') ? value.value : value;
77
+ }
78
+ return internalValue.value;
79
+ };
80
+
81
+ const handleInput = (e) => {
82
+ const newValue = Number(e.target.value);
83
+
84
+ if (!isControlled) {
85
+ internalValue.value = newValue;
86
+ }
87
+
88
+ if (isControlled && value && typeof value.value !== 'undefined') {
89
+ value.value = newValue;
90
+ }
91
+
92
+ if (onChange) onChange(newValue, e);
93
+ };
94
+
95
+ // Build DaisyUI range classes
96
+ const getRangeClass = () => {
97
+ const classes = ['range', 'w-full'];
98
+
99
+ if (size && size !== 'md') {
100
+ classes.push(`range-${size}`);
101
+ }
102
+
103
+ if (color) {
104
+ classes.push(`range-${color}`);
105
+ }
106
+
107
+ return classes.join(' ');
108
+ };
109
+
110
+ const rangeInput = input({
111
+ type: 'range',
112
+ class: getRangeClass(),
113
+ value: isControlled
114
+ ? (typeof value === 'function' ? value : () => getValue())
115
+ : () => internalValue.value,
116
+ min,
117
+ max,
118
+ step,
119
+ disabled: typeof disabled === 'function' ? disabled : disabled,
120
+ name,
121
+ id: rangeId,
122
+ oninput: handleInput,
123
+ ...rest
124
+ });
125
+
126
+ // If no label and no showValue, return just the range
127
+ if (!labelText && !showValue && !helper) {
128
+ // Check if we should use shadow DOM
129
+ let usesShadow = false;
130
+ if (LVX.shouldUseShadow) {
131
+ usesShadow = LVX.shouldUseShadow(useShadow);
132
+ } else {
133
+ usesShadow = useShadow === true;
134
+ }
135
+
136
+ if (usesShadow) {
137
+ const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
138
+ const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
139
+
140
+ return div({ class: 'content', style: 'display: inline-block' },
141
+ shadowDOM({ mode: 'open', adoptedStyleSheets },
142
+ div({ 'data-theme': themeValue }, rangeInput)
143
+ )
144
+ );
145
+ }
146
+
147
+ return rangeInput;
148
+ }
149
+
150
+ // Build the component using DaisyUI fieldset pattern
151
+ const fieldsetContent = [];
152
+
153
+ // Legend/Label with optional value display
154
+ if (labelText || showValue) {
155
+ fieldsetContent.push(
156
+ div({ class: 'flex justify-between items-center mb-1' },
157
+ labelText ? legend({ class: 'fieldset-legend' }, labelText) : span(),
158
+ showValue ? span({ class: 'text-sm font-mono opacity-70' },
159
+ () => formatValue(getValue())
160
+ ) : null
161
+ )
162
+ );
163
+ }
164
+
165
+ // Range input
166
+ fieldsetContent.push(rangeInput);
167
+
168
+ // Helper text
169
+ if (helper) {
170
+ fieldsetContent.push(p({ class: 'label' }, helper));
171
+ }
172
+
173
+ // Wrapper
174
+ const wrapperEl = fieldset({
175
+ class: `fieldset ${className}`.trim()
176
+ }, ...fieldsetContent);
177
+
178
+ // Check if we should use shadow DOM
179
+ let usesShadow = false;
180
+ if (LVX.shouldUseShadow) {
181
+ usesShadow = LVX.shouldUseShadow(useShadow);
182
+ } else {
183
+ usesShadow = useShadow === true;
184
+ }
185
+
186
+ if (usesShadow) {
187
+ const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
188
+
189
+ const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
190
+
191
+ return span({ style: 'margin-right: 0.5rem' },
192
+ shadowDOM({ mode: 'open', adoptedStyleSheets },
193
+ div({ 'data-theme': themeValue, style: 'display: inline-block' }, wrapperEl)
194
+ )
195
+ );
196
+ }
197
+
198
+ return wrapperEl;
199
+ };
200
+
201
+ // Auto-register
202
+ window.Lightview.tags.Range = Range;
203
+
204
+ export default Range;
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Lightview Components - Rating
3
+ * A rating component using DaisyUI 5 styling
4
+ * @see https://daisyui.com/components/rating/
5
+ */
6
+
7
+ import '../daisyui.js';
8
+
9
+ /**
10
+ * Rating Component
11
+ * @param {Object} props - Rating properties
12
+ * @param {number|Signal} props.value - Current rating value (controlled)
13
+ * @param {number} props.defaultValue - Default rating value (uncontrolled)
14
+ * @param {number} props.max - Maximum stars (default: 5)
15
+ * @param {string} props.size - 'xs' | 'sm' | 'md' | 'lg'
16
+ * @param {boolean} props.half - Allow half stars
17
+ * @param {string} props.color - Color for stars (default: 'orange-400')
18
+ * @param {string} props.mask - 'star' | 'star-2' | 'heart' | 'circle' | 'square' | 'diamond' (default: 'star-2')
19
+ * @param {boolean} props.hidden - Include hidden 0-star option for clearing
20
+ * @param {boolean} props.disabled - Disable rating
21
+ * @param {boolean} props.readOnly - Make read-only (just display)
22
+ * @param {string} props.label - Label text
23
+ * @param {string} props.helper - Helper text
24
+ * @param {Function} props.onChange - Change handler
25
+ * @param {boolean} props.useShadow - Render in Shadow DOM
26
+ */
27
+ const Rating = (props = {}) => {
28
+ const { tags, signal } = window.Lightview || {};
29
+ const LVX = window.LightviewX || {};
30
+
31
+ if (!tags) {
32
+ console.error('Lightview not found');
33
+ return null;
34
+ }
35
+
36
+ const { div, input, fieldset, legend, p, span, shadowDOM } = tags;
37
+
38
+ const {
39
+ value,
40
+ defaultValue = 0,
41
+ max = 5,
42
+ size,
43
+ half = false,
44
+ color = 'orange-400',
45
+ mask = 'star-2',
46
+ hidden = false,
47
+ disabled = false,
48
+ readOnly = false,
49
+ label: labelText,
50
+ helper,
51
+ name = `rating-${Math.random().toString(36).slice(2, 9)}`,
52
+ onChange,
53
+ class: className = '',
54
+ useShadow,
55
+ ...rest
56
+ } = props;
57
+
58
+ // Internal state
59
+ const internalValue = signal ? signal(defaultValue) : { value: defaultValue };
60
+
61
+ const isControlled = value !== undefined;
62
+
63
+ const getValue = () => {
64
+ if (isControlled) {
65
+ return typeof value === 'function' ? value() :
66
+ (value && typeof value.value !== 'undefined') ? value.value : value;
67
+ }
68
+ return internalValue.value;
69
+ };
70
+
71
+ const handleChange = (rating) => {
72
+ if (readOnly || disabled) return;
73
+
74
+ if (!isControlled) {
75
+ internalValue.value = rating;
76
+ }
77
+
78
+ if (isControlled && value && typeof value.value !== 'undefined') {
79
+ value.value = rating;
80
+ }
81
+
82
+ if (onChange) onChange(rating);
83
+ };
84
+
85
+ // Build rating classes
86
+ const getRatingClass = () => {
87
+ const classes = ['rating'];
88
+ if (size) classes.push(`rating-${size}`);
89
+ if (half) classes.push('rating-half');
90
+ return classes.join(' ');
91
+ };
92
+
93
+ const bgClass = `bg-${color}`;
94
+
95
+ // Build rating inputs
96
+ const buildInputs = () => {
97
+ const inputs = [];
98
+ const currentValue = getValue();
99
+
100
+ if (hidden) {
101
+ inputs.push(input({
102
+ type: 'radio',
103
+ name,
104
+ class: 'rating-hidden',
105
+ checked: () => getValue() === 0,
106
+ disabled,
107
+ onchange: () => handleChange(0)
108
+ }));
109
+ }
110
+
111
+ for (let i = 1; i <= max; i++) {
112
+ if (half) {
113
+ // Half stars
114
+ inputs.push(input({
115
+ type: 'radio',
116
+ name,
117
+ class: `mask mask-${mask} mask-half-1 ${bgClass}`,
118
+ 'aria-label': `${i - 0.5} stars`,
119
+ checked: () => getValue() === i - 0.5,
120
+ disabled,
121
+ onchange: () => handleChange(i - 0.5)
122
+ }));
123
+ inputs.push(input({
124
+ type: 'radio',
125
+ name,
126
+ class: `mask mask-${mask} mask-half-2 ${bgClass}`,
127
+ 'aria-label': `${i} stars`,
128
+ checked: () => getValue() === i,
129
+ disabled,
130
+ onchange: () => handleChange(i)
131
+ }));
132
+ } else {
133
+ inputs.push(input({
134
+ type: 'radio',
135
+ name,
136
+ class: `mask mask-${mask} ${bgClass}`,
137
+ 'aria-label': `${i} stars`,
138
+ checked: () => getValue() === i,
139
+ disabled,
140
+ onchange: () => handleChange(i)
141
+ }));
142
+ }
143
+ }
144
+
145
+ return inputs;
146
+ };
147
+
148
+ const ratingEl = div({
149
+ class: getRatingClass(),
150
+ ...rest
151
+ }, ...buildInputs());
152
+
153
+ // If no label and no helper, return just the rating
154
+ if (!labelText && !helper) {
155
+ let usesShadow = false;
156
+ if (LVX.shouldUseShadow) {
157
+ usesShadow = LVX.shouldUseShadow(useShadow);
158
+ } else {
159
+ usesShadow = useShadow === true;
160
+ }
161
+
162
+ if (usesShadow) {
163
+ const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
164
+ const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
165
+
166
+ return div({ class: 'content', style: 'display: inline-block' },
167
+ shadowDOM({ mode: 'open', adoptedStyleSheets },
168
+ div({ 'data-theme': themeValue }, ratingEl)
169
+ )
170
+ );
171
+ }
172
+
173
+ return ratingEl;
174
+ }
175
+
176
+ // Build with fieldset pattern
177
+ const fieldsetContent = [];
178
+
179
+ if (labelText) {
180
+ fieldsetContent.push(legend({ class: 'fieldset-legend' }, labelText));
181
+ }
182
+
183
+ fieldsetContent.push(ratingEl);
184
+
185
+ if (helper) {
186
+ fieldsetContent.push(p({ class: 'label' }, helper));
187
+ }
188
+
189
+ const wrapperEl = fieldset({
190
+ class: `fieldset ${className}`.trim()
191
+ }, ...fieldsetContent);
192
+
193
+ // Check if we should use shadow DOM
194
+ let usesShadow = false;
195
+ if (LVX.shouldUseShadow) {
196
+ usesShadow = LVX.shouldUseShadow(useShadow);
197
+ } else {
198
+ usesShadow = useShadow === true;
199
+ }
200
+
201
+ if (usesShadow) {
202
+ const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
203
+
204
+ const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
205
+
206
+ return span({ style: 'margin-right: 0.5rem' },
207
+ shadowDOM({ mode: 'open', adoptedStyleSheets },
208
+ div({ 'data-theme': themeValue, style: 'display: inline-block' }, wrapperEl)
209
+ )
210
+ );
211
+ }
212
+
213
+ return wrapperEl;
214
+ };
215
+
216
+ // Auto-register
217
+ window.Lightview.tags.Rating = Rating;
218
+
219
+ export default Rating;
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Lightview Components - Select
3
+ * A select dropdown component using DaisyUI 5 styling with validation support
4
+ * @see https://daisyui.com/components/select/
5
+ *
6
+ * Uses DaisyUI's fieldset pattern:
7
+ * <fieldset class="fieldset">
8
+ * <legend class="fieldset-legend">Label</legend>
9
+ * <select class="select" />
10
+ * <p class="label">Helper text</p>
11
+ * </fieldset>
12
+ */
13
+
14
+ import '../daisyui.js';
15
+
16
+ /**
17
+ * Select Component
18
+ * @param {Object} props - Select properties
19
+ * @param {Array} props.options - Array of options: string[] or {value, label, disabled}[]
20
+ * @param {*|Signal} props.value - Selected value (controlled)
21
+ * @param {*} props.defaultValue - Default value (uncontrolled)
22
+ * @param {string} props.placeholder - Placeholder text (shows as disabled first option)
23
+ * @param {string} props.size - 'xs' | 'sm' | 'md' | 'lg' (default: 'md')
24
+ * @param {string} props.color - 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error'
25
+ * @param {boolean} props.ghost - Ghost style (no background)
26
+ * @param {boolean} props.disabled - Disable select
27
+ * @param {boolean} props.required - Required field
28
+ * @param {string} props.label - Label text (rendered as fieldset legend)
29
+ * @param {string} props.helper - Helper text (rendered below select)
30
+ * @param {string|Function} props.error - Error message (string or validation function)
31
+ * @param {Function} props.validate - Validation function (value) => errorMessage | null
32
+ * @param {Function} props.onChange - Change handler
33
+ * @param {boolean} props.useShadow - Render in Shadow DOM with isolated DaisyUI styles
34
+ */
35
+ const Select = (props = {}) => {
36
+ const { tags, signal } = window.Lightview || {};
37
+ const LVX = window.LightviewX || {};
38
+
39
+ if (!tags) {
40
+ console.error('Lightview not found');
41
+ return null;
42
+ }
43
+
44
+ const { div, select, option, fieldset, legend, p, span, shadowDOM } = tags;
45
+
46
+ const {
47
+ options = [],
48
+ value,
49
+ defaultValue = '',
50
+ placeholder,
51
+ size = 'md',
52
+ color,
53
+ ghost = false,
54
+ disabled = false,
55
+ required = false,
56
+ label: labelText,
57
+ helper,
58
+ error,
59
+ validate,
60
+ onChange,
61
+ name,
62
+ id,
63
+ class: className = '',
64
+ useShadow,
65
+ ...rest
66
+ } = props;
67
+
68
+ // Generate unique ID if not provided
69
+ const selectId = id || `select-${Math.random().toString(36).slice(2, 9)}`;
70
+ const selectName = name || selectId;
71
+
72
+ // Internal state
73
+ const internalValue = signal ? signal(defaultValue) : { value: defaultValue };
74
+ const internalError = signal ? signal(null) : { value: null };
75
+ const touched = signal ? signal(false) : { value: false };
76
+
77
+ const isControlled = value !== undefined;
78
+
79
+ // Normalize options - first parse from JSON string if needed (from HTML attribute)
80
+ let parsedOptions = options;
81
+ if (typeof parsedOptions === 'string') {
82
+ try {
83
+ parsedOptions = JSON.parse(parsedOptions);
84
+ } catch (e) {
85
+ console.error('Select: Failed to parse options JSON:', e);
86
+ parsedOptions = [];
87
+ }
88
+ }
89
+
90
+ const normalizedOptions = (Array.isArray(parsedOptions) ? parsedOptions : []).map(opt =>
91
+ typeof opt === 'string' ? { value: opt, label: opt } : opt
92
+ );
93
+
94
+ const getValue = () => {
95
+ if (isControlled) {
96
+ return typeof value === 'function' ? value() :
97
+ (value && typeof value.value !== 'undefined') ? value.value : value;
98
+ }
99
+ return internalValue.value;
100
+ };
101
+
102
+ const getError = () => {
103
+ // External error takes priority
104
+ if (error) {
105
+ const err = typeof error === 'function' ? error() : error;
106
+ if (err) return err;
107
+ }
108
+ // Then internal validation error
109
+ return internalError.value;
110
+ };
111
+
112
+ const runValidation = (val) => {
113
+ if (!validate) return null;
114
+ const result = validate(val);
115
+ internalError.value = result;
116
+ return result;
117
+ };
118
+
119
+ const handleChange = (e) => {
120
+ const newValue = e.target.value;
121
+
122
+ if (!isControlled) {
123
+ internalValue.value = newValue;
124
+ }
125
+
126
+ if (isControlled && value && typeof value.value !== 'undefined') {
127
+ value.value = newValue;
128
+ }
129
+
130
+ // Validate on change
131
+ touched.value = true;
132
+ runValidation(newValue);
133
+
134
+ if (onChange) onChange(newValue, e);
135
+ };
136
+
137
+ // Build DaisyUI select classes
138
+ const getSelectClass = () => {
139
+ const classes = ['select', 'w-full'];
140
+
141
+ // Ghost style
142
+ if (ghost) {
143
+ classes.push('select-ghost');
144
+ }
145
+
146
+ // Size
147
+ if (size && size !== 'md') {
148
+ classes.push(`select-${size}`);
149
+ }
150
+
151
+ // Color
152
+ if (color) {
153
+ classes.push(`select-${color}`);
154
+ }
155
+
156
+ // Error state
157
+ const currentError = getError();
158
+ if (currentError) {
159
+ classes.push('select-error');
160
+ }
161
+
162
+ return classes.join(' ');
163
+ };
164
+
165
+ // Build options elements
166
+ const buildOptions = () => {
167
+ const optionEls = [];
168
+
169
+ // Placeholder option
170
+ if (placeholder) {
171
+ optionEls.push(
172
+ option({
173
+ disabled: true,
174
+ selected: () => !getValue(),
175
+ value: ''
176
+ }, placeholder)
177
+ );
178
+ }
179
+
180
+ // Regular options
181
+ normalizedOptions.forEach(opt => {
182
+ optionEls.push(
183
+ option({
184
+ value: opt.value,
185
+ selected: () => getValue() === opt.value,
186
+ disabled: opt.disabled
187
+ }, opt.label || opt.value)
188
+ );
189
+ });
190
+
191
+ return optionEls;
192
+ };
193
+
194
+ // Build select attributes
195
+ const selectAttrs = {
196
+ class: validate || error ? () => getSelectClass() : getSelectClass(),
197
+ disabled: typeof disabled === 'function' ? disabled : disabled,
198
+ required,
199
+ name: selectName,
200
+ id: selectId,
201
+ onchange: handleChange,
202
+ 'aria-invalid': () => !!getError(),
203
+ ...rest
204
+ };
205
+
206
+ const selectEl = select(selectAttrs, ...buildOptions());
207
+
208
+ // Build the component using DaisyUI fieldset pattern
209
+ const fieldsetContent = [];
210
+
211
+ // Legend/Label (DaisyUI fieldset-legend)
212
+ if (labelText) {
213
+ fieldsetContent.push(
214
+ legend({ class: 'fieldset-legend' },
215
+ labelText,
216
+ required ? span({ class: 'text-error' }, ' *') : null
217
+ )
218
+ );
219
+ }
220
+
221
+ // Select element
222
+ fieldsetContent.push(selectEl);
223
+
224
+ // Helper or error text
225
+ if (helper || validate || error) {
226
+ fieldsetContent.push(
227
+ () => {
228
+ const currentError = getError();
229
+ if (currentError) {
230
+ return p({
231
+ class: 'label text-error',
232
+ role: 'alert'
233
+ }, currentError);
234
+ }
235
+ if (helper) {
236
+ return p({
237
+ class: 'label'
238
+ }, helper);
239
+ }
240
+ return null;
241
+ }
242
+ );
243
+ }
244
+
245
+ // Wrapper with DaisyUI fieldset class
246
+ const wrapperEl = fieldset({
247
+ class: `fieldset ${className}`.trim()
248
+ }, ...fieldsetContent);
249
+
250
+ // Check if we should use shadow DOM
251
+ let usesShadow = false;
252
+ if (LVX.shouldUseShadow) {
253
+ usesShadow = LVX.shouldUseShadow(useShadow);
254
+ } else {
255
+ usesShadow = useShadow === true;
256
+ }
257
+
258
+ if (usesShadow) {
259
+ const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
260
+
261
+ // Get current theme from document
262
+ const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
263
+
264
+ return div({ class: 'content', style: 'display: inline-block' },
265
+ shadowDOM({ mode: 'open', adoptedStyleSheets },
266
+ div({ 'data-theme': themeValue },
267
+ wrapperEl
268
+ )
269
+ )
270
+ );
271
+ }
272
+
273
+ return wrapperEl;
274
+ };
275
+
276
+ // Auto-register
277
+ window.Lightview.tags.Select = Select;
278
+
279
+ // Register as Custom Element
280
+ if (window.LightviewX?.createCustomElement) {
281
+ const SelectElement = window.LightviewX.createCustomElement(Select);
282
+ if (!customElements.get('lv-select')) {
283
+ customElements.define('lv-select', SelectElement);
284
+ }
285
+ }
286
+
287
+ export default Select;