@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.
- package/README.md +1 -1
- package/dev-server/components/form/array-field.tsx +115 -0
- package/dev-server/components/form/file-field.tsx +48 -0
- package/dev-server/components/form/form-element.tsx +304 -0
- package/dev-server/components/form/link-behavior-field.tsx +68 -0
- package/dev-server/components/form/location-field.tsx +60 -0
- package/dev-server/components/settings-preview.tsx +138 -302
- package/dev-server/components/ui/checkbox.tsx +29 -0
- package/dev-server/components/ui/dialog.tsx +2 -10
- package/dev-server/components/ui/field.tsx +37 -0
- package/dev-server/components/ui/input.tsx +24 -0
- package/dev-server/components/ui/label.tsx +21 -0
- package/dev-server/components/ui/radio-group.tsx +37 -0
- package/dev-server/components/ui/select.tsx +153 -0
- package/dev-server/components/ui/switch.tsx +31 -0
- package/dev-server/components/ui/tabs.tsx +1 -1
- package/dev-server/components/ui/textarea.tsx +22 -0
- package/dev-server/env.d.ts +4 -1
- package/dev-server/expanded/main.tsx +20 -22
- package/dev-server/expanded.html +0 -1
- package/dev-server/featured/main.tsx +29 -36
- package/dev-server/featured-carousel.html +0 -1
- package/dev-server/featured.html +0 -1
- package/dev-server/index.html +1 -7
- package/dev-server/lib/utils.ts +3 -3
- package/dev-server/package.json +3 -3
- package/dev-server/postcss/tailwind-source-fallback.js +2 -7
- package/dev-server/postcss.config.mjs +2 -2
- package/dev-server/preview/Preview.tsx +316 -359
- package/dev-server/preview/main.tsx +8 -8
- package/dev-server/preview/preview.css +0 -1
- package/dev-server/public/site.webmanifest +1 -1
- package/dev-server/rsbuild.config.ts +1 -1
- package/dev-server/shared/dev-parent-simulator.ts +219 -0
- package/dev-server/shared/theme-presets.ts +71 -75
- package/dev-server/shared/theme-utils.ts +11 -11
- package/dist/cli.js +18 -12
- package/dist/cli.js.map +1 -1
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +27 -42
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +26 -16
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/deploy.d.ts +1 -11
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +3 -13
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +132 -388
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +17 -29
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.d.ts.map +1 -1
- package/dist/commands/logout.js +6 -11
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/rollback.d.ts +10 -0
- package/dist/commands/rollback.d.ts.map +1 -0
- package/dist/commands/rollback.js +148 -0
- package/dist/commands/rollback.js.map +1 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +96 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/test-url-match-rules.d.ts.map +1 -1
- package/dist/commands/test-url-match-rules.js +20 -26
- package/dist/commands/test-url-match-rules.js.map +1 -1
- package/dist/lib/auth/device-flow.d.ts +1 -1
- package/dist/lib/auth/device-flow.d.ts.map +1 -1
- package/dist/lib/auth/device-flow.js +3 -3
- package/dist/lib/auth/device-flow.js.map +1 -1
- package/dist/lib/auth/token-storage.d.ts.map +1 -1
- package/dist/lib/auth/token-storage.js +14 -37
- package/dist/lib/auth/token-storage.js.map +1 -1
- package/dist/lib/build/detect-layouts.d.ts.map +1 -1
- package/dist/lib/build/detect-layouts.js +27 -13
- package/dist/lib/build/detect-layouts.js.map +1 -1
- package/dist/lib/config/load-config.d.ts.map +1 -1
- package/dist/lib/config/load-config.js +0 -2
- package/dist/lib/config/load-config.js.map +1 -1
- package/dist/lib/deploy/deploy-output.d.ts +2 -1
- package/dist/lib/deploy/deploy-output.d.ts.map +1 -1
- package/dist/lib/deploy/deploy-output.js +9 -1
- package/dist/lib/deploy/deploy-output.js.map +1 -1
- package/dist/lib/deploy/deploy-phases.d.ts +2 -0
- package/dist/lib/deploy/deploy-phases.d.ts.map +1 -1
- package/dist/lib/deploy/deploy-phases.js +9 -23
- package/dist/lib/deploy/deploy-phases.js.map +1 -1
- package/dist/lib/deploy/deploy-utils.d.ts +15 -7
- package/dist/lib/deploy/deploy-utils.d.ts.map +1 -1
- package/dist/lib/deploy/deploy-utils.js +49 -36
- package/dist/lib/deploy/deploy-utils.js.map +1 -1
- package/dist/lib/deploy/generate-manifest-files.d.ts.map +1 -1
- package/dist/lib/deploy/generate-manifest-files.js +13 -39
- package/dist/lib/deploy/generate-manifest-files.js.map +1 -1
- package/dist/lib/deploy/pack-project.d.ts.map +1 -1
- package/dist/lib/deploy/pack-project.js +34 -20
- package/dist/lib/deploy/pack-project.js.map +1 -1
- package/dist/lib/deploy/slot-manager.d.ts +54 -0
- package/dist/lib/deploy/slot-manager.d.ts.map +1 -0
- package/dist/lib/deploy/slot-manager.js +72 -0
- package/dist/lib/deploy/slot-manager.js.map +1 -0
- package/dist/lib/deploy/test-url-match-rules.d.ts +10 -2
- package/dist/lib/deploy/test-url-match-rules.d.ts.map +1 -1
- package/dist/lib/deploy/test-url-match-rules.js +1 -1
- package/dist/lib/deploy/test-url-match-rules.js.map +1 -1
- package/dist/lib/deploy/upload.d.ts +1 -0
- package/dist/lib/deploy/upload.d.ts.map +1 -1
- package/dist/lib/deploy/upload.js +15 -24
- package/dist/lib/deploy/upload.js.map +1 -1
- package/dist/lib/deploy/validation.d.ts.map +1 -1
- package/dist/lib/deploy/validation.js +43 -48
- package/dist/lib/deploy/validation.js.map +1 -1
- package/dist/lib/rsbuild/config-factory.d.ts.map +1 -1
- package/dist/lib/rsbuild/config-factory.js +10 -17
- package/dist/lib/rsbuild/config-factory.js.map +1 -1
- package/dist/lib/rsbuild/plugins/asset-versioning.d.ts.map +1 -1
- package/dist/lib/rsbuild/plugins/asset-versioning.js +4 -14
- package/dist/lib/rsbuild/plugins/asset-versioning.js.map +1 -1
- package/dist/lib/rsbuild/plugins/brotli-compression.d.ts.map +1 -1
- package/dist/lib/rsbuild/plugins/brotli-compression.js +4 -4
- package/dist/lib/rsbuild/plugins/brotli-compression.js.map +1 -1
- package/dist/lib/rsbuild/plugins/copy-public.d.ts.map +1 -1
- package/dist/lib/rsbuild/plugins/copy-public.js.map +1 -1
- package/dist/lib/rsbuild/postcss/tailwind-source-fallback.d.ts.map +1 -1
- package/dist/lib/rsbuild/postcss/tailwind-source-fallback.js +1 -3
- package/dist/lib/rsbuild/postcss/tailwind-source-fallback.js.map +1 -1
- package/dist/lib/utils/console.d.ts +8 -0
- package/dist/lib/utils/console.d.ts.map +1 -0
- package/dist/lib/utils/console.js +10 -0
- package/dist/lib/utils/console.js.map +1 -0
- package/dist/lib/utils/filesystem.d.ts +9 -0
- package/dist/lib/utils/filesystem.d.ts.map +1 -0
- package/dist/lib/utils/filesystem.js +30 -0
- package/dist/lib/utils/filesystem.js.map +1 -0
- package/dist/lib/utils/formatters.d.ts +8 -0
- package/dist/lib/utils/formatters.d.ts.map +1 -0
- package/dist/lib/utils/formatters.js +22 -0
- package/dist/lib/utils/formatters.js.map +1 -0
- package/dist/lib/utils/index.d.ts +7 -0
- package/dist/lib/utils/index.d.ts.map +1 -0
- package/dist/lib/utils/index.js +7 -0
- package/dist/lib/utils/index.js.map +1 -0
- package/dist/lib/utils/setup-runtime.d.ts.map +1 -1
- package/dist/lib/utils/setup-runtime.js +36 -73
- package/dist/lib/utils/setup-runtime.js.map +1 -1
- package/dist/schema/config.schema.d.ts +9 -48
- package/dist/schema/config.schema.d.ts.map +1 -1
- package/dist/schema/config.schema.js +119 -120
- package/dist/schema/config.schema.js.map +1 -1
- package/dist/sdk/hooks/mocks.d.ts +9 -0
- package/dist/sdk/hooks/mocks.d.ts.map +1 -0
- package/dist/sdk/hooks/mocks.js +17 -0
- package/dist/sdk/hooks/mocks.js.map +1 -0
- package/dist/sdk/hooks/use-audience-manager.d.ts +44 -0
- package/dist/sdk/hooks/use-audience-manager.d.ts.map +1 -0
- package/dist/sdk/hooks/use-audience-manager.js +109 -0
- package/dist/sdk/hooks/use-audience-manager.js.map +1 -0
- package/dist/sdk/hooks/use-ip.d.ts +45 -0
- package/dist/sdk/hooks/use-ip.d.ts.map +1 -0
- package/dist/sdk/hooks/use-ip.js +46 -0
- package/dist/sdk/hooks/use-ip.js.map +1 -0
- package/dist/sdk/hooks/use-sdk-request.d.ts +46 -0
- package/dist/sdk/hooks/use-sdk-request.d.ts.map +1 -0
- package/dist/sdk/hooks/use-sdk-request.js +65 -0
- package/dist/sdk/hooks/use-sdk-request.js.map +1 -0
- package/dist/sdk/hooks/use-theme.d.ts +45 -0
- package/dist/sdk/hooks/use-theme.d.ts.map +1 -0
- package/dist/sdk/hooks/use-theme.js +97 -0
- package/dist/sdk/hooks/use-theme.js.map +1 -0
- package/dist/sdk/hooks/use-visitor.d.ts +41 -0
- package/dist/sdk/hooks/use-visitor.d.ts.map +1 -0
- package/dist/sdk/hooks/use-visitor.js +42 -0
- package/dist/sdk/hooks/use-visitor.js.map +1 -0
- package/dist/sdk/hooks/validation.d.ts +8 -0
- package/dist/sdk/hooks/validation.d.ts.map +1 -0
- package/dist/sdk/hooks/validation.js +13 -0
- package/dist/sdk/hooks/validation.js.map +1 -0
- package/dist/sdk/index.d.ts +17 -5
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +16 -5
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/message-bus.d.ts +59 -0
- package/dist/sdk/message-bus.d.ts.map +1 -0
- package/dist/sdk/message-bus.js +152 -0
- package/dist/sdk/message-bus.js.map +1 -0
- package/dist/sdk/messages.d.ts +121 -0
- package/dist/sdk/messages.d.ts.map +1 -0
- package/dist/sdk/messages.js +9 -0
- package/dist/sdk/messages.js.map +1 -0
- package/dist/sdk/send-message.d.ts +1 -1
- package/dist/sdk/send-message.js +18 -18
- package/dist/sdk/send-message.js.map +1 -1
- package/dist/sdk/use-expand-link-app.d.ts +3 -3
- package/dist/sdk/use-expand-link-app.d.ts.map +1 -1
- package/dist/sdk/use-expand-link-app.js +9 -5
- package/dist/sdk/use-expand-link-app.js.map +1 -1
- package/dist/types.d.ts +235 -55
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -3
- package/dist/types.js.map +1 -1
- package/package.json +8 -9
- 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,
|
|
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
|
+
}
|