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,264 @@
1
+ /**
2
+ * Lightview Components - Input
3
+ * A text input component using DaisyUI 5 styling with validation support
4
+ * @see https://daisyui.com/components/input/
5
+ *
6
+ * Uses DaisyUI's fieldset pattern:
7
+ * <fieldset class="fieldset">
8
+ * <legend class="fieldset-legend">Label</legend>
9
+ * <input class="input" />
10
+ * <p class="label">Helper text</p>
11
+ * </fieldset>
12
+ */
13
+
14
+ import '../daisyui.js';
15
+
16
+ /**
17
+ * Input Component
18
+ * @param {Object} props - Input properties
19
+ * @param {string} props.type - Input type (default: 'text')
20
+ * @param {string|Signal} props.value - Input value
21
+ * @param {string} props.defaultValue - Default value (uncontrolled)
22
+ * @param {string} props.placeholder - Placeholder text
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 input
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 input)
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 {Function} props.onBlur - Blur handler
34
+ * @param {boolean} props.useShadow - Render in Shadow DOM with isolated DaisyUI styles
35
+ */
36
+ const Input = (props = {}) => {
37
+ const { tags, signal } = window.Lightview || {};
38
+ const LVX = window.LightviewX || {};
39
+
40
+ if (!tags) {
41
+ console.error('Lightview not found');
42
+ return null;
43
+ }
44
+
45
+ const { div, input, fieldset, legend, p, span, shadowDOM } = tags;
46
+
47
+ const {
48
+ type = 'text',
49
+ value,
50
+ defaultValue = '',
51
+ placeholder,
52
+ size = 'md',
53
+ color,
54
+ ghost = false,
55
+ disabled = false,
56
+ required = false,
57
+ label: labelText,
58
+ helper,
59
+ error,
60
+ validate,
61
+ onChange,
62
+ onBlur,
63
+ onInput,
64
+ name,
65
+ id,
66
+ class: className = '',
67
+ useShadow,
68
+ ...rest
69
+ } = props;
70
+
71
+ // Generate unique ID if not provided
72
+ const inputId = id || `input-${Math.random().toString(36).slice(2, 9)}`;
73
+ const inputName = name || inputId;
74
+
75
+ // Internal state
76
+ const internalValue = signal ? signal(defaultValue) : { value: defaultValue };
77
+ const internalError = signal ? signal(null) : { value: null };
78
+ const touched = signal ? signal(false) : { value: false };
79
+
80
+ const isControlled = value !== undefined;
81
+
82
+ const getValue = () => {
83
+ if (isControlled) {
84
+ return typeof value === 'function' ? value() :
85
+ (value && typeof value.value !== 'undefined') ? value.value : value;
86
+ }
87
+ return internalValue.value;
88
+ };
89
+
90
+ const getError = () => {
91
+ // External error takes priority
92
+ if (error) {
93
+ const err = typeof error === 'function' ? error() : error;
94
+ if (err) return err;
95
+ }
96
+ // Then internal validation error
97
+ return internalError.value;
98
+ };
99
+
100
+ const runValidation = (val) => {
101
+ if (!validate) return null;
102
+ const result = validate(val);
103
+ internalError.value = result;
104
+ return result;
105
+ };
106
+
107
+ const handleInput = (e) => {
108
+ const newValue = e.target.value;
109
+
110
+ if (!isControlled) {
111
+ internalValue.value = newValue;
112
+ }
113
+
114
+ if (isControlled && value && typeof value.value !== 'undefined') {
115
+ value.value = newValue;
116
+ }
117
+
118
+ // Validate on input if already touched
119
+ if (touched.value && validate) {
120
+ runValidation(newValue);
121
+ }
122
+
123
+ if (onInput) onInput(e);
124
+ if (onChange) onChange(newValue, e);
125
+ };
126
+
127
+ const handleBlur = (e) => {
128
+ touched.value = true;
129
+ runValidation(e.target.value);
130
+ if (onBlur) onBlur(e);
131
+ };
132
+
133
+ // Build DaisyUI input classes
134
+ const getInputClass = () => {
135
+ const classes = ['input', 'w-full'];
136
+
137
+ // Ghost style
138
+ if (ghost) {
139
+ classes.push('input-ghost');
140
+ }
141
+
142
+ // Size
143
+ if (size && size !== 'md') {
144
+ classes.push(`input-${size}`);
145
+ }
146
+
147
+ // Color
148
+ if (color) {
149
+ classes.push(`input-${color}`);
150
+ }
151
+
152
+ // Error state
153
+ const currentError = getError();
154
+ if (currentError) {
155
+ classes.push('input-error');
156
+ }
157
+
158
+ return classes.join(' ');
159
+ };
160
+
161
+ // Build input attributes
162
+ const inputAttrs = {
163
+ type,
164
+ class: validate || error ? () => getInputClass() : getInputClass(),
165
+ value: isControlled
166
+ ? (typeof value === 'function' ? value : () => getValue())
167
+ : () => internalValue.value,
168
+ disabled: typeof disabled === 'function' ? disabled : disabled,
169
+ required,
170
+ name: inputName,
171
+ id: inputId,
172
+ oninput: handleInput,
173
+ onblur: handleBlur,
174
+ 'aria-invalid': () => !!getError(),
175
+ ...rest
176
+ };
177
+
178
+ // Only add placeholder if defined
179
+ if (placeholder !== undefined) {
180
+ inputAttrs.placeholder = placeholder;
181
+ }
182
+
183
+ const inputEl = input(inputAttrs);
184
+
185
+ // Build the component using DaisyUI fieldset pattern
186
+ const fieldsetContent = [];
187
+
188
+ // Legend/Label (DaisyUI fieldset-legend)
189
+ if (labelText) {
190
+ fieldsetContent.push(
191
+ legend({ class: 'fieldset-legend' },
192
+ labelText,
193
+ required ? span({ class: 'text-error' }, ' *') : null
194
+ )
195
+ );
196
+ }
197
+
198
+ // Input element
199
+ fieldsetContent.push(inputEl);
200
+
201
+ // Helper or error text (DaisyUI label class for helper text below)
202
+ if (helper || validate || error) {
203
+ fieldsetContent.push(
204
+ () => {
205
+ const currentError = getError();
206
+ if (currentError) {
207
+ return p({
208
+ class: 'label text-error',
209
+ role: 'alert'
210
+ }, currentError);
211
+ }
212
+ if (helper) {
213
+ return p({
214
+ class: 'label'
215
+ }, helper);
216
+ }
217
+ return null;
218
+ }
219
+ );
220
+ }
221
+
222
+ // Wrapper with DaisyUI fieldset class
223
+ const wrapperEl = fieldset({
224
+ class: `fieldset ${className}`.trim()
225
+ }, ...fieldsetContent);
226
+
227
+ // Check if we should use shadow DOM
228
+ let usesShadow = false;
229
+ if (LVX.shouldUseShadow) {
230
+ usesShadow = LVX.shouldUseShadow(useShadow);
231
+ } else {
232
+ usesShadow = useShadow === true;
233
+ }
234
+
235
+ if (usesShadow) {
236
+ const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
237
+
238
+ // Get current theme from document
239
+ const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
240
+
241
+ return div({ class: 'content', style: 'display: inline-block' },
242
+ shadowDOM({ mode: 'open', adoptedStyleSheets },
243
+ div({ 'data-theme': themeValue },
244
+ wrapperEl
245
+ )
246
+ )
247
+ );
248
+ }
249
+
250
+ return wrapperEl;
251
+ };
252
+
253
+ // Auto-register
254
+ window.Lightview.tags.Input = Input;
255
+
256
+ // Register as Custom Element
257
+ if (window.LightviewX?.createCustomElement) {
258
+ const InputElement = window.LightviewX.createCustomElement(Input);
259
+ if (!customElements.get('lv-input')) {
260
+ customElements.define('lv-input', InputElement);
261
+ }
262
+ }
263
+
264
+ export default Input;
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Lightview Components - Radio & RadioGroup
3
+ * Radio button components using DaisyUI 5 styling with validation support
4
+ * @see https://daisyui.com/components/radio/
5
+ *
6
+ * Uses DaisyUI's form-control pattern:
7
+ * <div class="form-control">
8
+ * <label class="label cursor-pointer">
9
+ * <span class="label-text">Option</span>
10
+ * <input type="radio" class="radio" />
11
+ * </label>
12
+ * </div>
13
+ */
14
+
15
+ import '../daisyui.js';
16
+
17
+ /**
18
+ * Radio Component (Single radio button)
19
+ * @param {Object} props - Radio properties
20
+ * @param {string} props.name - Radio group name
21
+ * @param {*} props.value - Radio value
22
+ * @param {boolean|function} props.checked - Checked state
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.disabled - Disable radio
26
+ * @param {string} props.label - Label text
27
+ * @param {Function} props.onChange - Change handler
28
+ */
29
+ const Radio = (props = {}) => {
30
+ const { tags } = window.Lightview || {};
31
+ const LVX = window.LightviewX || {};
32
+
33
+ if (!tags) {
34
+ console.error('Lightview not found');
35
+ return null;
36
+ }
37
+
38
+ const { div, input, label, span, shadowDOM } = tags;
39
+
40
+ const {
41
+ name,
42
+ value,
43
+ checked = false,
44
+ size = 'md',
45
+ color,
46
+ disabled = false,
47
+ label: labelText,
48
+ onChange,
49
+ id,
50
+ class: className = '',
51
+ useShadow,
52
+ ...rest
53
+ } = props;
54
+
55
+ const radioId = id || `radio-${Math.random().toString(36).slice(2, 9)}`;
56
+
57
+ // Build DaisyUI radio classes
58
+ const getRadioClass = () => {
59
+ const classes = ['radio'];
60
+
61
+ if (size && size !== 'md') {
62
+ classes.push(`radio-${size}`);
63
+ }
64
+
65
+ if (color) {
66
+ classes.push(`radio-${color}`);
67
+ }
68
+
69
+ return classes.join(' ');
70
+ };
71
+
72
+ const radioInput = input({
73
+ type: 'radio',
74
+ class: getRadioClass(),
75
+ name,
76
+ value,
77
+ checked: typeof checked === 'function' ? checked : checked,
78
+ disabled,
79
+ id: radioId,
80
+ onchange: onChange ? (e) => onChange(e.target.value, e) : undefined,
81
+ ...rest
82
+ });
83
+
84
+ // If no label, return just the radio
85
+ if (!labelText) {
86
+ return radioInput;
87
+ }
88
+
89
+ const formControl = div({
90
+ class: `form-control ${className}`.trim()
91
+ },
92
+ label({ class: 'label cursor-pointer', style: 'justify-content: flex-start; gap: 0;' },
93
+ radioInput,
94
+ span({ class: 'label-text', style: 'margin-left: 0.5rem;' }, labelText)
95
+ )
96
+ );
97
+
98
+ // Check if we should use shadow DOM
99
+ let usesShadow = false;
100
+ if (LVX.shouldUseShadow) {
101
+ usesShadow = LVX.shouldUseShadow(useShadow);
102
+ } else {
103
+ usesShadow = useShadow === true;
104
+ }
105
+
106
+ if (usesShadow) {
107
+ const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
108
+ const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
109
+
110
+ return div({ class: 'content', style: 'display: inline-block' },
111
+ shadowDOM({ mode: 'open', adoptedStyleSheets },
112
+ div({ 'data-theme': themeValue }, formControl)
113
+ )
114
+ );
115
+ }
116
+
117
+ return formControl;
118
+ };
119
+
120
+ /**
121
+ * RadioGroup Component
122
+ * @param {Object} props - RadioGroup properties
123
+ * @param {Array} props.options - Array of options: string[] or {value, label, description, disabled}[]
124
+ * @param {*|Signal} props.value - Selected value (controlled)
125
+ * @param {*} props.defaultValue - Default value (uncontrolled)
126
+ * @param {string} props.name - Group name for form submission
127
+ * @param {string} props.label - Group label
128
+ * @param {string} props.helper - Helper text
129
+ * @param {string|Function} props.error - Error message
130
+ * @param {Function} props.validate - Validation function (value) => errorMessage | null
131
+ * @param {string} props.color - 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error'
132
+ * @param {string} props.size - 'xs' | 'sm' | 'md' | 'lg' (default: 'md')
133
+ * @param {boolean} props.horizontal - Horizontal layout
134
+ * @param {boolean} props.disabled - Disable all options
135
+ * @param {boolean} props.required - Mark as required
136
+ * @param {Function} props.onChange - Value change handler
137
+ * @param {boolean} props.useShadow - Render in Shadow DOM
138
+ */
139
+ const RadioGroup = (props = {}) => {
140
+ const { tags, signal } = window.Lightview || {};
141
+ const LVX = window.LightviewX || {};
142
+
143
+ if (!tags) {
144
+ console.error('Lightview not found');
145
+ return null;
146
+ }
147
+
148
+ const { div, fieldset, legend, input, label, span, p, shadowDOM } = tags;
149
+
150
+ const {
151
+ options = [],
152
+ value,
153
+ defaultValue,
154
+ name = `radio-group-${Math.random().toString(36).slice(2, 9)}`,
155
+ label: groupLabel,
156
+ helper,
157
+ error,
158
+ validate,
159
+ color,
160
+ size = 'md',
161
+ horizontal = false,
162
+ disabled = false,
163
+ required = false,
164
+ onChange,
165
+ class: className = '',
166
+ useShadow,
167
+ ...rest
168
+ } = props;
169
+
170
+ // Normalize options
171
+ const normalizedOptions = options.map(opt =>
172
+ typeof opt === 'string' ? { value: opt, label: opt } : opt
173
+ );
174
+
175
+ // Internal state
176
+ const internalValue = signal
177
+ ? signal(defaultValue !== undefined ? defaultValue : null)
178
+ : { value: defaultValue !== undefined ? defaultValue : null };
179
+ const internalError = signal ? signal(null) : { value: null };
180
+
181
+ const isControlled = value !== undefined;
182
+
183
+ const getValue = () => {
184
+ if (isControlled) {
185
+ return typeof value === 'function' ? value() :
186
+ (value && typeof value.value !== 'undefined') ? value.value : value;
187
+ }
188
+ return internalValue.value;
189
+ };
190
+
191
+ const getError = () => {
192
+ if (error) {
193
+ const err = typeof error === 'function' ? error() : error;
194
+ if (err) return err;
195
+ }
196
+ return internalError.value;
197
+ };
198
+
199
+ const runValidation = (val) => {
200
+ if (validate) {
201
+ const result = validate(val);
202
+ internalError.value = result;
203
+ return result;
204
+ }
205
+ if (required && !val) {
206
+ internalError.value = 'Please select an option';
207
+ return 'Please select an option';
208
+ }
209
+ internalError.value = null;
210
+ return null;
211
+ };
212
+
213
+ const handleChange = (optValue) => {
214
+ if (!isControlled) {
215
+ internalValue.value = optValue;
216
+ }
217
+
218
+ if (isControlled && value && typeof value.value !== 'undefined') {
219
+ value.value = optValue;
220
+ }
221
+
222
+ runValidation(optValue);
223
+
224
+ if (onChange) onChange(optValue);
225
+ };
226
+
227
+ // Build radio class
228
+ const getRadioClass = () => {
229
+ const classes = ['radio'];
230
+ if (size && size !== 'md') classes.push(`radio-${size}`);
231
+ if (color) classes.push(`radio-${color}`);
232
+ return classes.join(' ');
233
+ };
234
+
235
+ // Build options
236
+ const radioOptions = normalizedOptions.map(opt => {
237
+ const optDisabled = disabled || opt.disabled;
238
+ const isChecked = () => getValue() === opt.value;
239
+
240
+ return label({ class: 'label cursor-pointer', style: 'justify-content: flex-start; gap: 0;' },
241
+ input({
242
+ type: 'radio',
243
+ class: getRadioClass(),
244
+ name,
245
+ value: opt.value,
246
+ checked: isChecked,
247
+ disabled: optDisabled,
248
+ onchange: () => handleChange(opt.value)
249
+ }),
250
+ div({ style: 'display: flex; flex-direction: column; margin-left: 0.5rem;' },
251
+ span({ class: 'label-text' }, opt.label),
252
+ opt.description ? span({ class: 'label-text-alt', style: 'opacity: 0.7;' }, opt.description) : null
253
+ )
254
+ );
255
+ });
256
+
257
+ // Build the component
258
+ const fieldsetContent = [];
259
+
260
+ if (groupLabel) {
261
+ fieldsetContent.push(
262
+ legend({ class: 'fieldset-legend' },
263
+ groupLabel,
264
+ required ? span({ class: 'text-error' }, ' *') : null
265
+ )
266
+ );
267
+ }
268
+
269
+ fieldsetContent.push(
270
+ div({
271
+ style: horizontal ? 'display: flex; flex-wrap: wrap; gap: 1rem;' : 'display: flex; flex-direction: column; gap: 0.5rem;',
272
+ role: 'radiogroup',
273
+ 'aria-label': groupLabel
274
+ }, ...radioOptions)
275
+ );
276
+
277
+ // Helper or error text
278
+ if (helper || validate || error || required) {
279
+ fieldsetContent.push(
280
+ () => {
281
+ const currentError = getError();
282
+ if (currentError) {
283
+ return p({ class: 'label text-error', role: 'alert' }, currentError);
284
+ }
285
+ if (helper) {
286
+ return p({ class: 'label' }, helper);
287
+ }
288
+ return null;
289
+ }
290
+ );
291
+ }
292
+
293
+ const wrapperEl = fieldset({
294
+ class: `fieldset ${className}`.trim(),
295
+ ...rest
296
+ }, ...fieldsetContent);
297
+
298
+ // Check if we should use shadow DOM
299
+ let usesShadow = false;
300
+ if (LVX.shouldUseShadow) {
301
+ usesShadow = LVX.shouldUseShadow(useShadow);
302
+ } else {
303
+ usesShadow = useShadow === true;
304
+ }
305
+
306
+ if (usesShadow) {
307
+ const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
308
+
309
+ const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
310
+
311
+ return span({ style: 'margin-right: 0.5rem' },
312
+ shadowDOM({ mode: 'open', adoptedStyleSheets },
313
+ div({ 'data-theme': themeValue, style: 'display: inline-block' }, wrapperEl)
314
+ )
315
+ );
316
+ }
317
+
318
+ return wrapperEl;
319
+ };
320
+
321
+ // Auto-register
322
+ window.Lightview.tags.Radio = Radio;
323
+ window.Lightview.tags.RadioGroup = RadioGroup;
324
+
325
+ // Register as Custom Elements
326
+ if (window.LightviewX?.createCustomElement) {
327
+ const RadioElement = window.LightviewX.createCustomElement(Radio);
328
+ if (!customElements.get('lv-radio')) {
329
+ customElements.define('lv-radio', RadioElement);
330
+ }
331
+ const RadioGroupElement = window.LightviewX.createCustomElement(RadioGroup);
332
+ if (!customElements.get('lv-radio-group')) {
333
+ customElements.define('lv-radio-group', RadioGroupElement);
334
+ }
335
+ }
336
+
337
+ export default Radio;
338
+ export { Radio, RadioGroup };