@linktr.ee/linkapp 0.0.47 → 0.0.49

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 (204) hide show
  1. package/README.md +1 -1
  2. package/dev-server/components/form/array-field.tsx +115 -0
  3. package/dev-server/components/form/file-field.tsx +48 -0
  4. package/dev-server/components/form/form-element.tsx +304 -0
  5. package/dev-server/components/form/link-behavior-field.tsx +68 -0
  6. package/dev-server/components/form/location-field.tsx +60 -0
  7. package/dev-server/components/settings-preview.tsx +138 -302
  8. package/dev-server/components/ui/checkbox.tsx +29 -0
  9. package/dev-server/components/ui/dialog.tsx +2 -10
  10. package/dev-server/components/ui/field.tsx +37 -0
  11. package/dev-server/components/ui/input.tsx +24 -0
  12. package/dev-server/components/ui/label.tsx +21 -0
  13. package/dev-server/components/ui/radio-group.tsx +37 -0
  14. package/dev-server/components/ui/select.tsx +153 -0
  15. package/dev-server/components/ui/switch.tsx +31 -0
  16. package/dev-server/components/ui/tabs.tsx +1 -1
  17. package/dev-server/components/ui/textarea.tsx +22 -0
  18. package/dev-server/env.d.ts +4 -1
  19. package/dev-server/expanded/main.tsx +20 -22
  20. package/dev-server/expanded.html +0 -1
  21. package/dev-server/featured/main.tsx +29 -36
  22. package/dev-server/featured-carousel.html +0 -1
  23. package/dev-server/featured.html +0 -1
  24. package/dev-server/index.html +1 -7
  25. package/dev-server/lib/utils.ts +3 -3
  26. package/dev-server/package.json +3 -3
  27. package/dev-server/postcss/tailwind-source-fallback.js +2 -7
  28. package/dev-server/postcss.config.mjs +2 -2
  29. package/dev-server/preview/Preview.tsx +316 -359
  30. package/dev-server/preview/main.tsx +8 -8
  31. package/dev-server/preview/preview.css +0 -1
  32. package/dev-server/public/site.webmanifest +1 -1
  33. package/dev-server/rsbuild.config.ts +1 -1
  34. package/dev-server/shared/dev-parent-simulator.ts +219 -0
  35. package/dev-server/shared/theme-presets.ts +71 -75
  36. package/dev-server/shared/theme-utils.ts +11 -11
  37. package/dist/cli.js +18 -12
  38. package/dist/cli.js.map +1 -1
  39. package/dist/commands/add.d.ts.map +1 -1
  40. package/dist/commands/add.js +27 -42
  41. package/dist/commands/add.js.map +1 -1
  42. package/dist/commands/build.d.ts.map +1 -1
  43. package/dist/commands/build.js +26 -16
  44. package/dist/commands/build.js.map +1 -1
  45. package/dist/commands/deploy.d.ts +1 -11
  46. package/dist/commands/deploy.d.ts.map +1 -1
  47. package/dist/commands/deploy.js +3 -13
  48. package/dist/commands/deploy.js.map +1 -1
  49. package/dist/commands/dev.d.ts.map +1 -1
  50. package/dist/commands/dev.js +132 -388
  51. package/dist/commands/dev.js.map +1 -1
  52. package/dist/commands/login.d.ts.map +1 -1
  53. package/dist/commands/login.js +17 -29
  54. package/dist/commands/login.js.map +1 -1
  55. package/dist/commands/logout.d.ts.map +1 -1
  56. package/dist/commands/logout.js +6 -11
  57. package/dist/commands/logout.js.map +1 -1
  58. package/dist/commands/rollback.d.ts +10 -0
  59. package/dist/commands/rollback.d.ts.map +1 -0
  60. package/dist/commands/rollback.js +148 -0
  61. package/dist/commands/rollback.js.map +1 -0
  62. package/dist/commands/status.d.ts +8 -0
  63. package/dist/commands/status.d.ts.map +1 -0
  64. package/dist/commands/status.js +96 -0
  65. package/dist/commands/status.js.map +1 -0
  66. package/dist/commands/test-url-match-rules.d.ts.map +1 -1
  67. package/dist/commands/test-url-match-rules.js +20 -26
  68. package/dist/commands/test-url-match-rules.js.map +1 -1
  69. package/dist/lib/auth/device-flow.d.ts +1 -1
  70. package/dist/lib/auth/device-flow.d.ts.map +1 -1
  71. package/dist/lib/auth/device-flow.js +3 -3
  72. package/dist/lib/auth/device-flow.js.map +1 -1
  73. package/dist/lib/auth/token-storage.d.ts.map +1 -1
  74. package/dist/lib/auth/token-storage.js +14 -37
  75. package/dist/lib/auth/token-storage.js.map +1 -1
  76. package/dist/lib/build/detect-layouts.d.ts.map +1 -1
  77. package/dist/lib/build/detect-layouts.js +27 -13
  78. package/dist/lib/build/detect-layouts.js.map +1 -1
  79. package/dist/lib/config/load-config.d.ts.map +1 -1
  80. package/dist/lib/config/load-config.js +0 -2
  81. package/dist/lib/config/load-config.js.map +1 -1
  82. package/dist/lib/deploy/deploy-output.d.ts +2 -1
  83. package/dist/lib/deploy/deploy-output.d.ts.map +1 -1
  84. package/dist/lib/deploy/deploy-output.js +9 -1
  85. package/dist/lib/deploy/deploy-output.js.map +1 -1
  86. package/dist/lib/deploy/deploy-phases.d.ts +2 -0
  87. package/dist/lib/deploy/deploy-phases.d.ts.map +1 -1
  88. package/dist/lib/deploy/deploy-phases.js +9 -23
  89. package/dist/lib/deploy/deploy-phases.js.map +1 -1
  90. package/dist/lib/deploy/deploy-utils.d.ts +15 -7
  91. package/dist/lib/deploy/deploy-utils.d.ts.map +1 -1
  92. package/dist/lib/deploy/deploy-utils.js +49 -36
  93. package/dist/lib/deploy/deploy-utils.js.map +1 -1
  94. package/dist/lib/deploy/generate-manifest-files.d.ts.map +1 -1
  95. package/dist/lib/deploy/generate-manifest-files.js +13 -39
  96. package/dist/lib/deploy/generate-manifest-files.js.map +1 -1
  97. package/dist/lib/deploy/pack-project.d.ts.map +1 -1
  98. package/dist/lib/deploy/pack-project.js +34 -20
  99. package/dist/lib/deploy/pack-project.js.map +1 -1
  100. package/dist/lib/deploy/slot-manager.d.ts +54 -0
  101. package/dist/lib/deploy/slot-manager.d.ts.map +1 -0
  102. package/dist/lib/deploy/slot-manager.js +72 -0
  103. package/dist/lib/deploy/slot-manager.js.map +1 -0
  104. package/dist/lib/deploy/test-url-match-rules.d.ts +10 -2
  105. package/dist/lib/deploy/test-url-match-rules.d.ts.map +1 -1
  106. package/dist/lib/deploy/test-url-match-rules.js +1 -1
  107. package/dist/lib/deploy/test-url-match-rules.js.map +1 -1
  108. package/dist/lib/deploy/upload.d.ts +1 -0
  109. package/dist/lib/deploy/upload.d.ts.map +1 -1
  110. package/dist/lib/deploy/upload.js +15 -24
  111. package/dist/lib/deploy/upload.js.map +1 -1
  112. package/dist/lib/deploy/validation.d.ts.map +1 -1
  113. package/dist/lib/deploy/validation.js +43 -48
  114. package/dist/lib/deploy/validation.js.map +1 -1
  115. package/dist/lib/rsbuild/config-factory.d.ts.map +1 -1
  116. package/dist/lib/rsbuild/config-factory.js +10 -17
  117. package/dist/lib/rsbuild/config-factory.js.map +1 -1
  118. package/dist/lib/rsbuild/plugins/asset-versioning.d.ts.map +1 -1
  119. package/dist/lib/rsbuild/plugins/asset-versioning.js +4 -14
  120. package/dist/lib/rsbuild/plugins/asset-versioning.js.map +1 -1
  121. package/dist/lib/rsbuild/plugins/brotli-compression.d.ts.map +1 -1
  122. package/dist/lib/rsbuild/plugins/brotli-compression.js +4 -4
  123. package/dist/lib/rsbuild/plugins/brotli-compression.js.map +1 -1
  124. package/dist/lib/rsbuild/plugins/copy-public.d.ts.map +1 -1
  125. package/dist/lib/rsbuild/plugins/copy-public.js.map +1 -1
  126. package/dist/lib/rsbuild/postcss/tailwind-source-fallback.d.ts.map +1 -1
  127. package/dist/lib/rsbuild/postcss/tailwind-source-fallback.js +1 -3
  128. package/dist/lib/rsbuild/postcss/tailwind-source-fallback.js.map +1 -1
  129. package/dist/lib/utils/console.d.ts +8 -0
  130. package/dist/lib/utils/console.d.ts.map +1 -0
  131. package/dist/lib/utils/console.js +10 -0
  132. package/dist/lib/utils/console.js.map +1 -0
  133. package/dist/lib/utils/filesystem.d.ts +9 -0
  134. package/dist/lib/utils/filesystem.d.ts.map +1 -0
  135. package/dist/lib/utils/filesystem.js +30 -0
  136. package/dist/lib/utils/filesystem.js.map +1 -0
  137. package/dist/lib/utils/formatters.d.ts +8 -0
  138. package/dist/lib/utils/formatters.d.ts.map +1 -0
  139. package/dist/lib/utils/formatters.js +22 -0
  140. package/dist/lib/utils/formatters.js.map +1 -0
  141. package/dist/lib/utils/index.d.ts +7 -0
  142. package/dist/lib/utils/index.d.ts.map +1 -0
  143. package/dist/lib/utils/index.js +7 -0
  144. package/dist/lib/utils/index.js.map +1 -0
  145. package/dist/lib/utils/setup-runtime.d.ts.map +1 -1
  146. package/dist/lib/utils/setup-runtime.js +36 -73
  147. package/dist/lib/utils/setup-runtime.js.map +1 -1
  148. package/dist/schema/config.schema.d.ts +9 -48
  149. package/dist/schema/config.schema.d.ts.map +1 -1
  150. package/dist/schema/config.schema.js +119 -120
  151. package/dist/schema/config.schema.js.map +1 -1
  152. package/dist/sdk/hooks/mocks.d.ts +9 -0
  153. package/dist/sdk/hooks/mocks.d.ts.map +1 -0
  154. package/dist/sdk/hooks/mocks.js +17 -0
  155. package/dist/sdk/hooks/mocks.js.map +1 -0
  156. package/dist/sdk/hooks/use-audience-manager.d.ts +44 -0
  157. package/dist/sdk/hooks/use-audience-manager.d.ts.map +1 -0
  158. package/dist/sdk/hooks/use-audience-manager.js +109 -0
  159. package/dist/sdk/hooks/use-audience-manager.js.map +1 -0
  160. package/dist/sdk/hooks/use-ip.d.ts +45 -0
  161. package/dist/sdk/hooks/use-ip.d.ts.map +1 -0
  162. package/dist/sdk/hooks/use-ip.js +46 -0
  163. package/dist/sdk/hooks/use-ip.js.map +1 -0
  164. package/dist/sdk/hooks/use-sdk-request.d.ts +46 -0
  165. package/dist/sdk/hooks/use-sdk-request.d.ts.map +1 -0
  166. package/dist/sdk/hooks/use-sdk-request.js +65 -0
  167. package/dist/sdk/hooks/use-sdk-request.js.map +1 -0
  168. package/dist/sdk/hooks/use-theme.d.ts +45 -0
  169. package/dist/sdk/hooks/use-theme.d.ts.map +1 -0
  170. package/dist/sdk/hooks/use-theme.js +97 -0
  171. package/dist/sdk/hooks/use-theme.js.map +1 -0
  172. package/dist/sdk/hooks/use-visitor.d.ts +41 -0
  173. package/dist/sdk/hooks/use-visitor.d.ts.map +1 -0
  174. package/dist/sdk/hooks/use-visitor.js +42 -0
  175. package/dist/sdk/hooks/use-visitor.js.map +1 -0
  176. package/dist/sdk/hooks/validation.d.ts +8 -0
  177. package/dist/sdk/hooks/validation.d.ts.map +1 -0
  178. package/dist/sdk/hooks/validation.js +13 -0
  179. package/dist/sdk/hooks/validation.js.map +1 -0
  180. package/dist/sdk/index.d.ts +17 -5
  181. package/dist/sdk/index.d.ts.map +1 -1
  182. package/dist/sdk/index.js +16 -5
  183. package/dist/sdk/index.js.map +1 -1
  184. package/dist/sdk/message-bus.d.ts +59 -0
  185. package/dist/sdk/message-bus.d.ts.map +1 -0
  186. package/dist/sdk/message-bus.js +152 -0
  187. package/dist/sdk/message-bus.js.map +1 -0
  188. package/dist/sdk/messages.d.ts +121 -0
  189. package/dist/sdk/messages.d.ts.map +1 -0
  190. package/dist/sdk/messages.js +9 -0
  191. package/dist/sdk/messages.js.map +1 -0
  192. package/dist/sdk/send-message.d.ts +1 -1
  193. package/dist/sdk/send-message.js +18 -18
  194. package/dist/sdk/send-message.js.map +1 -1
  195. package/dist/sdk/use-expand-link-app.d.ts +3 -3
  196. package/dist/sdk/use-expand-link-app.d.ts.map +1 -1
  197. package/dist/sdk/use-expand-link-app.js +9 -5
  198. package/dist/sdk/use-expand-link-app.js.map +1 -1
  199. package/dist/types.d.ts +235 -55
  200. package/dist/types.d.ts.map +1 -1
  201. package/dist/types.js +8 -3
  202. package/dist/types.js.map +1 -1
  203. package/package.json +8 -9
  204. package/runtime/index.html +28 -0
package/README.md CHANGED
@@ -132,7 +132,7 @@ export default function ExpandedLayout({ settings, theme }: AppProps<MySettings>
132
132
  ```typescript
133
133
  import { useExpandLinkApp } from '@linktr.ee/linkapp/sdk'
134
134
 
135
- // In featured layout, trigger popup/modal
135
+ // In featured layout, expand to overlay
136
136
  function FeaturedLayout() {
137
137
  const expandLinkApp = useExpandLinkApp()
138
138
 
@@ -0,0 +1,115 @@
1
+ import { Plus } from 'lucide-react'
2
+ import type * as React from 'react'
3
+
4
+ import type { SettingsElement } from '../../../src/types'
5
+ import { cn } from '../../lib/utils'
6
+
7
+ // Forward declaration - FormElement will be passed as a prop to avoid circular dependency
8
+ interface ArrayFieldProps {
9
+ element: SettingsElement
10
+ value?: unknown[]
11
+ renderElement: (element: SettingsElement, value?: unknown) => React.ReactNode
12
+ }
13
+
14
+ export function ArrayField({ element, value, renderElement }: ArrayFieldProps) {
15
+ const minItems = element.array_options?.min ?? 0
16
+ const maxItems = element.array_options?.max
17
+ const addButtonText = element.array_options?.add_item_button_text || 'Add item'
18
+
19
+ // Show at least one sample item to demonstrate the structure
20
+ const sampleItems = value && Array.isArray(value) && value.length > 0 ? value : [{}] // Show one empty sample
21
+
22
+ return (
23
+ <div className="space-y-3">
24
+ {/* Item count info */}
25
+ <div className="text-xs text-gray-500">
26
+ Items: {minItems} - {maxItems ?? 'unlimited'}
27
+ </div>
28
+
29
+ {/* Sample items showing structure */}
30
+ <div className="space-y-3">
31
+ {sampleItems.map((itemValue, index) => (
32
+ <ArrayItem
33
+ key={index}
34
+ index={index}
35
+ element={element}
36
+ value={itemValue as Record<string, unknown>}
37
+ renderElement={renderElement}
38
+ />
39
+ ))}
40
+ </div>
41
+
42
+ {/* Add button */}
43
+ <button
44
+ type="button"
45
+ disabled
46
+ className={cn(
47
+ 'flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md',
48
+ 'border border-dashed border-gray-300 text-gray-500',
49
+ 'cursor-not-allowed hover:bg-gray-50'
50
+ )}
51
+ >
52
+ <Plus className="h-4 w-4" />
53
+ {addButtonText}
54
+ </button>
55
+ </div>
56
+ )
57
+ }
58
+
59
+ interface ArrayItemProps {
60
+ index: number
61
+ element: SettingsElement
62
+ value?: Record<string, unknown>
63
+ renderElement: (element: SettingsElement, value?: unknown) => React.ReactNode
64
+ }
65
+
66
+ function ArrayItem({ index, element, value, renderElement }: ArrayItemProps) {
67
+ const itemFormat = element.array_options?.item_format
68
+ const arrayElements = element.array_elements || []
69
+
70
+ // Format the item title using the format string
71
+ const itemTitle = itemFormat ? formatItemTitle(itemFormat, value, index) : `Item ${index + 1}`
72
+
73
+ return (
74
+ <div className="border border-gray-200 rounded-lg bg-white">
75
+ {/* Item header */}
76
+ <div className="flex items-center justify-between px-4 py-2 border-b border-gray-100 bg-gray-50 rounded-t-lg">
77
+ <span className="text-sm font-medium text-gray-700">{itemTitle}</span>
78
+ <span className="text-xs text-gray-400">#{index + 1}</span>
79
+ </div>
80
+
81
+ {/* Nested elements */}
82
+ <div className="p-4 space-y-4">
83
+ {arrayElements.map((nestedElement) => (
84
+ <div key={nestedElement.id}>{renderElement(nestedElement, value?.[nestedElement.id])}</div>
85
+ ))}
86
+ </div>
87
+ </div>
88
+ )
89
+ }
90
+
91
+ function formatItemTitle(format: string, value: Record<string, unknown> | undefined, index: number): string {
92
+ if (!value) return `Item ${index + 1}`
93
+
94
+ // Replace {{fieldId}} placeholders with values
95
+ let title = format
96
+ const placeholders = format.match(/\{\{(\w+)\}\}/g) || []
97
+
98
+ for (const placeholder of placeholders) {
99
+ const fieldId = placeholder.slice(2, -2)
100
+ const fieldValue = value[fieldId]
101
+ if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
102
+ title = title.replace(placeholder, String(fieldValue))
103
+ } else {
104
+ title = title.replace(placeholder, `[${fieldId}]`)
105
+ }
106
+ }
107
+
108
+ // Handle conditional sections like {{#price}}...{{/price}}
109
+ title = title.replace(/\{\{#(\w+)\}\}(.*?)\{\{\/\1\}\}/g, (_, fieldId, content) => {
110
+ const fieldValue = value[fieldId]
111
+ return fieldValue !== undefined && fieldValue !== null && fieldValue !== '' ? content : ''
112
+ })
113
+
114
+ return title || `Item ${index + 1}`
115
+ }
@@ -0,0 +1,48 @@
1
+ import { Upload } from 'lucide-react'
2
+ import * as React from 'react'
3
+
4
+ import type { SettingsElement } from '../../../src/types'
5
+ import { cn } from '../../lib/utils'
6
+
7
+ interface FileFieldProps {
8
+ element: SettingsElement
9
+ value?: string | string[]
10
+ }
11
+
12
+ export function FileField({ element, value }: FileFieldProps) {
13
+ const acceptTypes = element.accept?.join(', ') || 'All files'
14
+ const hasValue = value && (Array.isArray(value) ? value.length > 0 : value)
15
+
16
+ return (
17
+ <div
18
+ className={cn(
19
+ 'flex flex-col items-center justify-center w-full h-32 rounded-lg border-2 border-dashed',
20
+ 'border-gray-300 bg-gray-50',
21
+ 'cursor-not-allowed'
22
+ )}
23
+ >
24
+ <Upload className="h-8 w-8 text-gray-400 mb-2" />
25
+ {hasValue ? (
26
+ <p className="text-sm text-gray-600">
27
+ {Array.isArray(value) ? `${value.length} file(s) selected` : 'File selected'}
28
+ </p>
29
+ ) : (
30
+ <>
31
+ <p className="text-sm text-gray-500">
32
+ {element.multiple ? 'Drop files here or click to upload' : 'Drop file here or click to upload'}
33
+ </p>
34
+ <p className="text-xs text-gray-400 mt-1">Accepts: {acceptTypes}</p>
35
+ </>
36
+ )}
37
+ {element.validation?.maxSize && (
38
+ <p className="text-xs text-gray-400 mt-1">Max size: {formatFileSize(element.validation.maxSize)}</p>
39
+ )}
40
+ </div>
41
+ )
42
+ }
43
+
44
+ function formatFileSize(bytes: number): string {
45
+ if (bytes < 1024) return `${bytes} B`
46
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
47
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
48
+ }
@@ -0,0 +1,304 @@
1
+ import { MousePointer, Plug } from 'lucide-react'
2
+ import * as React from 'react'
3
+
4
+ import { type PlaceValue, type SettingsElement, SettingsElementInput } from '../../../src/types'
5
+ import { cn } from '../../lib/utils'
6
+ import { Checkbox } from '../ui/checkbox'
7
+ import { Field, FieldDescription, FieldLabel } from '../ui/field'
8
+ import { Input } from '../ui/input'
9
+ import { Label } from '../ui/label'
10
+ import { RadioGroup, RadioGroupItem } from '../ui/radio-group'
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
12
+ import { Switch } from '../ui/switch'
13
+ import { Textarea } from '../ui/textarea'
14
+ import { ArrayField } from './array-field'
15
+ import { FileField } from './file-field'
16
+ import { LinkBehaviorField } from './link-behavior-field'
17
+ import { LocationField } from './location-field'
18
+
19
+ interface FormElementProps {
20
+ element: SettingsElement
21
+ value?: unknown
22
+ }
23
+
24
+ export function FormElement({ element, value }: FormElementProps) {
25
+ const label = element.title || element.label || element.id
26
+ const description = element.description
27
+ const defaultValue = element.defaultValue
28
+ const displayValue = value ?? defaultValue
29
+
30
+ // Helper to render nested elements (for array fields)
31
+ const renderElement = (el: SettingsElement, val?: unknown) => <FormElement element={el} value={val} />
32
+
33
+ const renderInput = () => {
34
+ switch (element.inputType) {
35
+ case SettingsElementInput.text:
36
+ case SettingsElementInput.url:
37
+ case SettingsElementInput.email:
38
+ return (
39
+ <Input
40
+ id={element.id}
41
+ type="text"
42
+ placeholder={element.placeholder}
43
+ defaultValue={typeof displayValue === 'string' ? displayValue : ''}
44
+ disabled
45
+ />
46
+ )
47
+
48
+ case SettingsElementInput.number:
49
+ return (
50
+ <Input
51
+ id={element.id}
52
+ type="number"
53
+ placeholder={element.placeholder}
54
+ defaultValue={
55
+ typeof displayValue === 'string' || typeof displayValue === 'number' ? String(displayValue) : ''
56
+ }
57
+ min={element.validation?.min}
58
+ max={element.validation?.max}
59
+ disabled
60
+ />
61
+ )
62
+
63
+ case SettingsElementInput.textarea:
64
+ return (
65
+ <Textarea
66
+ id={element.id}
67
+ placeholder={element.placeholder}
68
+ defaultValue={typeof displayValue === 'string' ? displayValue : ''}
69
+ disabled
70
+ />
71
+ )
72
+
73
+ case SettingsElementInput.select:
74
+ return (
75
+ <Select defaultValue={typeof displayValue === 'string' ? displayValue : undefined} disabled>
76
+ <SelectTrigger id={element.id}>
77
+ <SelectValue placeholder={element.placeholder || 'Select an option'} />
78
+ </SelectTrigger>
79
+ <SelectContent>
80
+ {element.options?.map((option) => (
81
+ <SelectItem key={option.value} value={option.value}>
82
+ {option.label}
83
+ </SelectItem>
84
+ ))}
85
+ </SelectContent>
86
+ </Select>
87
+ )
88
+
89
+ case SettingsElementInput.radio: {
90
+ // Support both radio_elements (id, title, description) and options (value, label)
91
+ const radioOptions = element.radio_elements
92
+ ? element.radio_elements.map((el) => ({
93
+ value: el.id,
94
+ label: el.title,
95
+ description: el.description,
96
+ }))
97
+ : element.options?.map((opt) => ({
98
+ value: opt.value,
99
+ label: opt.label,
100
+ description: undefined,
101
+ }))
102
+
103
+ return (
104
+ <RadioGroup
105
+ defaultValue={typeof displayValue === 'string' ? displayValue : undefined}
106
+ disabled
107
+ className="space-y-2"
108
+ >
109
+ {radioOptions?.map((option) => (
110
+ <div
111
+ key={option.value}
112
+ className={cn(
113
+ 'flex items-start gap-3 px-3 py-2.5 rounded-lg border transition-colors',
114
+ displayValue === option.value ? 'border-gray-300 bg-gray-50' : 'border-gray-200 bg-white'
115
+ )}
116
+ >
117
+ <RadioGroupItem value={option.value} id={`${element.id}-${option.value}`} className="mt-0.5" />
118
+ <div className="flex flex-col">
119
+ <Label
120
+ htmlFor={`${element.id}-${option.value}`}
121
+ className="text-sm font-normal text-gray-700 cursor-not-allowed"
122
+ >
123
+ {option.label}
124
+ </Label>
125
+ {option.description && <span className="text-xs text-gray-500 mt-0.5">{option.description}</span>}
126
+ </div>
127
+ </div>
128
+ ))}
129
+ </RadioGroup>
130
+ )
131
+ }
132
+
133
+ case SettingsElementInput.checkbox:
134
+ return (
135
+ <div className="flex items-center gap-3 px-3 py-2.5 rounded-lg border border-gray-200 bg-white">
136
+ <Checkbox id={element.id} checked={displayValue === true} disabled />
137
+ <Label htmlFor={element.id} className="text-sm font-normal text-gray-700 cursor-not-allowed">
138
+ {element.label || label}
139
+ </Label>
140
+ </div>
141
+ )
142
+
143
+ case SettingsElementInput.switch:
144
+ return (
145
+ <div className="flex items-center justify-between px-3 py-2.5 rounded-lg border border-gray-200 bg-white">
146
+ <Label htmlFor={element.id} className="text-sm font-normal text-gray-700 cursor-not-allowed">
147
+ {element.label || label}
148
+ </Label>
149
+ <Switch id={element.id} checked={displayValue === true} disabled />
150
+ </div>
151
+ )
152
+
153
+ case SettingsElementInput.file:
154
+ return <FileField element={element} value={displayValue as string | string[] | undefined} />
155
+
156
+ case SettingsElementInput.linkBehavior:
157
+ return (
158
+ <LinkBehaviorField element={element} value={typeof displayValue === 'string' ? displayValue : undefined} />
159
+ )
160
+
161
+ case SettingsElementInput.array:
162
+ return (
163
+ <ArrayField
164
+ element={element}
165
+ value={Array.isArray(displayValue) ? displayValue : undefined}
166
+ renderElement={renderElement}
167
+ />
168
+ )
169
+
170
+ case SettingsElementInput.button:
171
+ return (
172
+ <button
173
+ type="button"
174
+ disabled
175
+ className={cn(
176
+ 'inline-flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium rounded-lg',
177
+ 'bg-gray-900 text-white',
178
+ 'cursor-not-allowed opacity-60'
179
+ )}
180
+ >
181
+ {element.label || 'Button'}
182
+ </button>
183
+ )
184
+
185
+ case SettingsElementInput.integration:
186
+ return (
187
+ <div
188
+ className={cn(
189
+ 'flex items-center gap-3 px-3 py-2.5 rounded-lg border border-dashed',
190
+ 'border-gray-300 bg-gray-50/50 cursor-not-allowed'
191
+ )}
192
+ >
193
+ <div className="flex items-center justify-center w-8 h-8 rounded-md bg-gray-100">
194
+ <Plug className="h-4 w-4 text-gray-400" />
195
+ </div>
196
+ <div>
197
+ <p className="text-sm font-medium text-gray-600">
198
+ {element.capability || element.vendor || 'Connect service'}
199
+ </p>
200
+ {element.vendor && <p className="text-xs text-gray-400">Vendor: {element.vendor}</p>}
201
+ </div>
202
+ </div>
203
+ )
204
+
205
+ case SettingsElementInput.location:
206
+ return <LocationField element={element} value={displayValue as PlaceValue | undefined} />
207
+
208
+ default:
209
+ return (
210
+ <div className="text-sm text-gray-400 italic p-2 bg-gray-50 rounded border border-dashed border-gray-200">
211
+ Unsupported input type: {element.inputType}
212
+ </div>
213
+ )
214
+ }
215
+ }
216
+
217
+ // For checkbox and switch, don't wrap in Field with label (they have inline labels)
218
+ if (element.inputType === SettingsElementInput.checkbox || element.inputType === SettingsElementInput.switch) {
219
+ return (
220
+ <Field>
221
+ {renderInput()}
222
+ {description && <FieldDescription>{description}</FieldDescription>}
223
+ <ValidationBadges element={element} />
224
+ </Field>
225
+ )
226
+ }
227
+
228
+ return (
229
+ <Field>
230
+ <FieldLabel htmlFor={element.id}>{label}</FieldLabel>
231
+ {renderInput()}
232
+ {description && <FieldDescription>{description}</FieldDescription>}
233
+ <ValidationBadges element={element} />
234
+ </Field>
235
+ )
236
+ }
237
+
238
+ function ValidationBadges({ element }: { element: SettingsElement }) {
239
+ const validation = element.validation
240
+ if (!validation) return null
241
+
242
+ const badges: React.ReactNode[] = []
243
+
244
+ if (validation.required) {
245
+ badges.push(
246
+ <span key="required" className="text-xs text-red-500">
247
+ Required
248
+ </span>
249
+ )
250
+ }
251
+
252
+ if (validation.minLength !== undefined) {
253
+ badges.push(
254
+ <span key="minLength" className="text-xs text-gray-400">
255
+ Min {validation.minLength} chars
256
+ </span>
257
+ )
258
+ }
259
+
260
+ if (validation.maxLength !== undefined) {
261
+ badges.push(
262
+ <span key="maxLength" className="text-xs text-gray-400">
263
+ Max {validation.maxLength} chars
264
+ </span>
265
+ )
266
+ }
267
+
268
+ if (validation.min !== undefined) {
269
+ badges.push(
270
+ <span key="min" className="text-xs text-gray-400">
271
+ Min {validation.min}
272
+ </span>
273
+ )
274
+ }
275
+
276
+ if (validation.max !== undefined) {
277
+ badges.push(
278
+ <span key="max" className="text-xs text-gray-400">
279
+ Max {validation.max}
280
+ </span>
281
+ )
282
+ }
283
+
284
+ if (validation.pattern) {
285
+ badges.push(
286
+ <span key="pattern" className="text-xs text-gray-400" title={validation.pattern}>
287
+ Pattern
288
+ </span>
289
+ )
290
+ }
291
+
292
+ if (badges.length === 0) return null
293
+
294
+ return (
295
+ <div className="flex flex-wrap items-center gap-2 mt-1.5">
296
+ {badges.map((badge, index) => (
297
+ <React.Fragment key={index}>
298
+ {badge}
299
+ {index < badges.length - 1 && <span className="text-gray-300">·</span>}
300
+ </React.Fragment>
301
+ ))}
302
+ </div>
303
+ )
304
+ }
@@ -0,0 +1,68 @@
1
+ import { ExternalLink, Play } from 'lucide-react'
2
+ import type * as React from 'react'
3
+
4
+ import type { SettingsElement } from '../../../src/types'
5
+ import { cn } from '../../lib/utils'
6
+
7
+ interface LinkBehaviorFieldProps {
8
+ element: SettingsElement
9
+ value?: string
10
+ }
11
+
12
+ export function LinkBehaviorField({ element, value }: LinkBehaviorFieldProps) {
13
+ const embedLabel = element.linkBehaviorLabels?.embedLabel || 'Embed content'
14
+ const linkOffLabel = element.linkBehaviorLabels?.linkOffLabel || 'Go directly to URL'
15
+ const selectedValue = value || element.defaultValue || 'embedLabel'
16
+
17
+ return (
18
+ <div className="space-y-2">
19
+ <LinkBehaviorOption
20
+ icon={<Play className="h-5 w-5" />}
21
+ label={embedLabel}
22
+ selected={selectedValue === 'embedLabel'}
23
+ />
24
+ <LinkBehaviorOption
25
+ icon={<ExternalLink className="h-5 w-5" />}
26
+ label={linkOffLabel}
27
+ selected={selectedValue === 'linkOffLabel'}
28
+ />
29
+ </div>
30
+ )
31
+ }
32
+
33
+ interface LinkBehaviorOptionProps {
34
+ icon: React.ReactNode
35
+ label: string
36
+ selected: boolean
37
+ }
38
+
39
+ function LinkBehaviorOption({ icon, label, selected }: LinkBehaviorOptionProps) {
40
+ return (
41
+ <div
42
+ className={cn(
43
+ 'flex items-center gap-3 p-3 rounded-lg border-2 transition-colors cursor-not-allowed',
44
+ selected ? 'border-blue-500 bg-blue-50' : 'border-gray-200 bg-white'
45
+ )}
46
+ >
47
+ <div
48
+ className={cn(
49
+ 'flex items-center justify-center w-10 h-10 rounded-lg',
50
+ selected ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-500'
51
+ )}
52
+ >
53
+ {icon}
54
+ </div>
55
+ <div className="flex-1">
56
+ <p className={cn('text-sm font-medium', selected ? 'text-blue-900' : 'text-gray-700')}>{label}</p>
57
+ </div>
58
+ <div
59
+ className={cn(
60
+ 'w-5 h-5 rounded-full border-2 flex items-center justify-center',
61
+ selected ? 'border-blue-500' : 'border-gray-300'
62
+ )}
63
+ >
64
+ {selected && <div className="w-2.5 h-2.5 rounded-full bg-blue-500" />}
65
+ </div>
66
+ </div>
67
+ )
68
+ }
@@ -0,0 +1,60 @@
1
+ import { MapPin, Search } from 'lucide-react'
2
+ import * as React from 'react'
3
+
4
+ import type { PlaceValue, SettingsElement } from '../../../src/types'
5
+ import { cn } from '../../lib/utils'
6
+
7
+ interface LocationFieldProps {
8
+ element: SettingsElement
9
+ value?: PlaceValue
10
+ }
11
+
12
+ export function LocationField({ element, value }: LocationFieldProps) {
13
+ const placeholderText = element.placeholder || 'Search for a place...'
14
+ const hasValue = value?.placeId
15
+
16
+ return (
17
+ <div className="space-y-3">
18
+ {/* Search input placeholder */}
19
+ <div
20
+ className={cn(
21
+ 'flex items-center gap-2 px-3 py-2.5 rounded-lg border',
22
+ 'border-gray-200 bg-gray-50 cursor-not-allowed'
23
+ )}
24
+ >
25
+ <Search className="h-4 w-4 text-gray-400" />
26
+ <span className="text-sm text-gray-400">{placeholderText}</span>
27
+ </div>
28
+
29
+ {/* Selected place preview */}
30
+ {hasValue ? (
31
+ <div className={cn('flex items-start gap-3 p-3 rounded-lg border', 'border-blue-200 bg-blue-50')}>
32
+ <div className="flex items-center justify-center w-10 h-10 rounded-lg bg-blue-100">
33
+ <MapPin className="h-5 w-5 text-blue-600" />
34
+ </div>
35
+ <div className="flex-1 min-w-0">
36
+ <p className="text-sm font-medium text-gray-900 truncate">{value.name}</p>
37
+ <p className="text-xs text-gray-500 truncate">{value.address}</p>
38
+ {value.rating && (
39
+ <p className="text-xs text-gray-400 mt-1">
40
+ Rating: {value.rating}/5
41
+ {value.userRatingsTotal && ` (${value.userRatingsTotal} reviews)`}
42
+ </p>
43
+ )}
44
+ </div>
45
+ </div>
46
+ ) : (
47
+ <div
48
+ className={cn(
49
+ 'flex flex-col items-center justify-center py-8 rounded-lg border-2 border-dashed',
50
+ 'border-gray-300 bg-gray-50'
51
+ )}
52
+ >
53
+ <MapPin className="h-8 w-8 text-gray-400 mb-2" />
54
+ <p className="text-sm text-gray-500">No location selected</p>
55
+ <p className="text-xs text-gray-400 mt-1">Google Places integration (preview mode)</p>
56
+ </div>
57
+ )}
58
+ </div>
59
+ )
60
+ }