@morphika/andami 0.5.1 → 0.5.2
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/app/admin/assets/page.tsx +6 -6
- package/app/admin/database/page.tsx +302 -302
- package/app/admin/error.tsx +53 -53
- package/app/admin/layout.tsx +320 -320
- package/app/admin/navigation/page.tsx +255 -255
- package/app/admin/pages/[slug]/page.tsx +6 -6
- package/app/admin/pages/page.tsx +11 -11
- package/app/admin/projects/page.tsx +14 -14
- package/app/admin/setup/page.tsx +1 -1
- package/app/admin/styles/page.tsx +1 -1
- package/components/admin/MetadataEditor.tsx +6 -6
- package/components/admin/nav-builder/NavBuilder.tsx +1 -1
- package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
- package/components/admin/nav-builder/NavGridCell.tsx +48 -48
- package/components/admin/nav-builder/NavGridItem.tsx +4 -4
- package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
- package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
- package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
- package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
- package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
- package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
- package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
- package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
- package/components/admin/setup-wizard/DoneStep.tsx +1 -1
- package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
- package/components/admin/setup-wizard/StorageStep.tsx +2 -2
- package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
- package/components/admin/styles/ColorsEditor.tsx +2 -2
- package/components/admin/styles/FontsEditor.tsx +6 -6
- package/components/admin/styles/GridLayoutEditor.tsx +9 -9
- package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
- package/components/admin/styles/TypographyEditor.tsx +6 -6
- package/components/admin/styles/shared.tsx +68 -68
- package/components/blocks/AudioBlockRenderer.tsx +286 -286
- package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
- package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
- package/components/builder/BlockCardIcons.tsx +316 -316
- package/components/builder/BlockTypePicker.tsx +1 -1
- package/components/builder/BubbleIcons.tsx +90 -0
- package/components/builder/BuilderCanvas.tsx +2 -0
- package/components/builder/CanvasMinimap.tsx +2 -2
- package/components/builder/CoverSectionCanvas.tsx +363 -363
- package/components/builder/DeviceFrame.tsx +1 -1
- package/components/builder/DndWrapper.tsx +3 -3
- package/components/builder/InsertionLines.tsx +1 -1
- package/components/builder/SectionCardIcons.tsx +421 -320
- package/components/builder/SectionEditorBar.tsx +1 -1
- package/components/builder/SectionTypePicker.tsx +4 -4
- package/components/builder/SectionV2Canvas.tsx +1 -1
- package/components/builder/SectionV2Column.tsx +69 -67
- package/components/builder/SortableBlock.tsx +93 -73
- package/components/builder/SortableRow.tsx +27 -26
- package/components/builder/VirtualAssetGrid.tsx +2 -2
- package/components/builder/asset-browser/R2BrowserContent.tsx +11 -11
- package/components/builder/blockStyles.tsx +192 -185
- package/components/builder/color-picker/AlphaSlider.tsx +141 -141
- package/components/builder/color-picker/ColorInputs.tsx +105 -105
- package/components/builder/color-picker/EyedropperButton.tsx +74 -74
- package/components/builder/color-picker/HueSlider.tsx +124 -124
- package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
- package/components/builder/color-picker/SwatchBar.tsx +93 -93
- package/components/builder/editors/AudioBlockEditor.tsx +242 -242
- package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
- package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
- package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
- package/components/builder/editors/HoverEffectPicker.tsx +2 -2
- package/components/builder/editors/ImageBlockEditor.tsx +2 -2
- package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
- package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
- package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
- package/components/builder/editors/ProjectGridEditor.tsx +9 -9
- package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
- package/components/builder/editors/StaggerSettings.tsx +109 -109
- package/components/builder/editors/TextBlockEditor.tsx +3 -3
- package/components/builder/editors/TextStylePicker.tsx +1 -1
- package/components/builder/editors/VideoBlockEditor.tsx +2 -2
- package/components/builder/editors/index.ts +11 -10
- package/components/builder/editors/shared.tsx +6 -6
- package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
- package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
- package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
- package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
- package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
- package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
- package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
- package/components/builder/settings-panel/AnimationTab.tsx +138 -138
- package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
- package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
- package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
- package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
- package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
- package/components/builder/settings-panel/PageSettings.tsx +3 -3
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
- package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
- package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
- package/lib/animation/enter-types.ts +1 -0
- package/lib/animation/hover-effect-presets.ts +210 -210
- package/lib/animation/hover-effect-types.ts +1 -0
- package/lib/builder/block-registrations.ts +468 -417
- package/lib/builder/constants.ts +111 -111
- package/lib/builder/store-sections.ts +2 -2
- package/lib/builder/types-slices.ts +414 -414
- package/lib/builder/types.ts +4 -1
- package/lib/config/index.ts +27 -27
- package/lib/sanity/types.ts +98 -1
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/audioBlock.ts +69 -69
- package/sanity/schemas/blocks/index.ts +12 -11
- package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
- package/sanity/schemas/index.ts +120 -117
- package/styles/admin.css +85 -85
- package/styles/animations.css +237 -237
- package/styles/base.css +114 -114
|
@@ -1,514 +1,514 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, type ReactNode } from "react";
|
|
4
|
-
import {
|
|
5
|
-
PositionIcon,
|
|
6
|
-
TypographyIcon,
|
|
7
|
-
AnimationIcon,
|
|
8
|
-
SpacingIcon,
|
|
9
|
-
ContentIcon,
|
|
10
|
-
LinkIcon,
|
|
11
|
-
GridIcon,
|
|
12
|
-
StyleIcon,
|
|
13
|
-
BackgroundIcon,
|
|
14
|
-
} from "../../builder/editors/section-icons";
|
|
15
|
-
|
|
16
|
-
// ── Nav viewport type for responsive overrides ──
|
|
17
|
-
export type NavViewport = "desktop" | "tablet" | "phone";
|
|
18
|
-
|
|
19
|
-
// ── Shared field components for NavBuilder settings panel ──
|
|
20
|
-
// v2: Card-based sections, colored icons, builder-aligned styling
|
|
21
|
-
|
|
22
|
-
// ============================================
|
|
23
|
-
// Field wrapper
|
|
24
|
-
// ============================================
|
|
25
|
-
|
|
26
|
-
export function Field({ label, children }: { label: string; children: ReactNode }) {
|
|
27
|
-
return (
|
|
28
|
-
<div className="flex items-center gap-2.5 py-[5px]">
|
|
29
|
-
<label className="text-[11px] text-neutral-400 w-[56px] min-w-[56px] shrink-0">
|
|
30
|
-
{label}
|
|
31
|
-
</label>
|
|
32
|
-
<div className="flex-1 min-w-0">{children}</div>
|
|
33
|
-
</div>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// ============================================
|
|
38
|
-
// Text input
|
|
39
|
-
// ============================================
|
|
40
|
-
|
|
41
|
-
export function TextInput({
|
|
42
|
-
value,
|
|
43
|
-
onChange,
|
|
44
|
-
placeholder,
|
|
45
|
-
type = "text",
|
|
46
|
-
}: {
|
|
47
|
-
value: string | number;
|
|
48
|
-
onChange: (value: string) => void;
|
|
49
|
-
placeholder?: string;
|
|
50
|
-
type?: "text" | "number";
|
|
51
|
-
}) {
|
|
52
|
-
return (
|
|
53
|
-
<input
|
|
54
|
-
type={type}
|
|
55
|
-
value={value}
|
|
56
|
-
onChange={(e) => onChange(e.target.value)}
|
|
57
|
-
placeholder={placeholder}
|
|
58
|
-
className="w-full rounded-lg border border-transparent bg-white px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-neutral-50 focus:bg-white focus:border-[#
|
|
59
|
-
/>
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ============================================
|
|
64
|
-
// Select input
|
|
65
|
-
// ============================================
|
|
66
|
-
|
|
67
|
-
export function SelectInput({
|
|
68
|
-
value,
|
|
69
|
-
onChange,
|
|
70
|
-
options,
|
|
71
|
-
}: {
|
|
72
|
-
value: string;
|
|
73
|
-
onChange: (value: string) => void;
|
|
74
|
-
options: { value: string; label: string }[];
|
|
75
|
-
}) {
|
|
76
|
-
return (
|
|
77
|
-
<div className="relative">
|
|
78
|
-
<select
|
|
79
|
-
value={value}
|
|
80
|
-
onChange={(e) => onChange(e.target.value)}
|
|
81
|
-
className="w-full rounded-lg border border-transparent bg-white px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none appearance-none cursor-pointer pr-7 transition-all hover:bg-neutral-50 focus:bg-white focus:border-[#
|
|
82
|
-
>
|
|
83
|
-
{options.map((opt) => (
|
|
84
|
-
<option key={opt.value} value={opt.value}>
|
|
85
|
-
{opt.label}
|
|
86
|
-
</option>
|
|
87
|
-
))}
|
|
88
|
-
</select>
|
|
89
|
-
<div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none text-neutral-400">
|
|
90
|
-
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
91
|
-
<path
|
|
92
|
-
d="M3 4.5l3 3 3-3"
|
|
93
|
-
stroke="currentColor"
|
|
94
|
-
strokeWidth="1.5"
|
|
95
|
-
strokeLinecap="round"
|
|
96
|
-
strokeLinejoin="round"
|
|
97
|
-
/>
|
|
98
|
-
</svg>
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ============================================
|
|
105
|
-
// Segmented control
|
|
106
|
-
// ============================================
|
|
107
|
-
|
|
108
|
-
export function SegmentedControl({
|
|
109
|
-
value,
|
|
110
|
-
onChange,
|
|
111
|
-
options,
|
|
112
|
-
}: {
|
|
113
|
-
value: string;
|
|
114
|
-
onChange: (value: string) => void;
|
|
115
|
-
options: { value: string; label: string | ReactNode }[];
|
|
116
|
-
}) {
|
|
117
|
-
return (
|
|
118
|
-
<div className="flex bg-white rounded-lg p-0.5 border border-[#f0f0f0]">
|
|
119
|
-
{options.map((opt) => (
|
|
120
|
-
<button
|
|
121
|
-
key={String(opt.value)}
|
|
122
|
-
onClick={() => onChange(opt.value)}
|
|
123
|
-
className={`flex-1 px-2 py-[5px] text-[10px] font-medium rounded-md transition-all flex items-center justify-center ${
|
|
124
|
-
value === opt.value
|
|
125
|
-
? "text-neutral-900 bg-neutral-100 shadow-[0_1px_2px_rgba(0,0,0,0.04)]"
|
|
126
|
-
: "text-neutral-400 hover:text-neutral-600"
|
|
127
|
-
}`}
|
|
128
|
-
>
|
|
129
|
-
{opt.label}
|
|
130
|
-
</button>
|
|
131
|
-
))}
|
|
132
|
-
</div>
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ============================================
|
|
137
|
-
// Toggle switch
|
|
138
|
-
// ============================================
|
|
139
|
-
|
|
140
|
-
export function Toggle({
|
|
141
|
-
value,
|
|
142
|
-
onChange,
|
|
143
|
-
}: {
|
|
144
|
-
value: boolean;
|
|
145
|
-
onChange: (value: boolean) => void;
|
|
146
|
-
}) {
|
|
147
|
-
return (
|
|
148
|
-
<button
|
|
149
|
-
onClick={() => onChange(!value)}
|
|
150
|
-
className="w-9 h-5 rounded-full relative transition-all cursor-pointer"
|
|
151
|
-
style={{
|
|
152
|
-
background: value ? "#
|
|
153
|
-
}}
|
|
154
|
-
>
|
|
155
|
-
<div
|
|
156
|
-
className="w-3.5 h-3.5 rounded-full bg-white absolute top-[2.5px] transition-all shadow-sm"
|
|
157
|
-
style={{ left: value ? 19 : 2 }}
|
|
158
|
-
/>
|
|
159
|
-
</button>
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ============================================
|
|
164
|
-
// Range slider
|
|
165
|
-
// ============================================
|
|
166
|
-
|
|
167
|
-
export function RangeSlider({
|
|
168
|
-
value,
|
|
169
|
-
onChange,
|
|
170
|
-
min = 0,
|
|
171
|
-
max = 100,
|
|
172
|
-
suffix,
|
|
173
|
-
}: {
|
|
174
|
-
value: number;
|
|
175
|
-
onChange: (value: number) => void;
|
|
176
|
-
min?: number;
|
|
177
|
-
max?: number;
|
|
178
|
-
suffix?: string;
|
|
179
|
-
}) {
|
|
180
|
-
return (
|
|
181
|
-
<div className="flex items-center gap-2">
|
|
182
|
-
<input
|
|
183
|
-
type="range"
|
|
184
|
-
min={min}
|
|
185
|
-
max={max}
|
|
186
|
-
value={value}
|
|
187
|
-
onChange={(e) => onChange(Number(e.target.value))}
|
|
188
|
-
className="flex-1"
|
|
189
|
-
style={{ accentColor: "#
|
|
190
|
-
/>
|
|
191
|
-
<span className="text-[10px] text-neutral-400 w-9 text-right tabular-nums">
|
|
192
|
-
{value}{suffix || ""}
|
|
193
|
-
</span>
|
|
194
|
-
</div>
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ============================================
|
|
199
|
-
// Card section — collapsible with colored icon
|
|
200
|
-
// ============================================
|
|
201
|
-
|
|
202
|
-
export function CardSection({
|
|
203
|
-
title,
|
|
204
|
-
icon,
|
|
205
|
-
iconBg,
|
|
206
|
-
defaultOpen = true,
|
|
207
|
-
children,
|
|
208
|
-
}: {
|
|
209
|
-
title: string;
|
|
210
|
-
icon: ReactNode;
|
|
211
|
-
iconBg: string;
|
|
212
|
-
defaultOpen?: boolean;
|
|
213
|
-
children: ReactNode;
|
|
214
|
-
}) {
|
|
215
|
-
const [open, setOpen] = useState(defaultOpen);
|
|
216
|
-
|
|
217
|
-
return (
|
|
218
|
-
<div
|
|
219
|
-
className="mx-2.5 my-1 rounded-[10px] bg-[#fafafa] border border-transparent transition-colors hover:border-[#f0f0f0]"
|
|
220
|
-
>
|
|
221
|
-
<button
|
|
222
|
-
onClick={() => setOpen(!open)}
|
|
223
|
-
className="flex items-center gap-2 w-full px-3 py-2.5 cursor-pointer select-none hover:bg-black/[0.01] rounded-[10px] transition-colors"
|
|
224
|
-
>
|
|
225
|
-
<div
|
|
226
|
-
className="w-[26px] h-[26px] rounded-[7px] flex items-center justify-center shrink-0"
|
|
227
|
-
style={{ background: iconBg }}
|
|
228
|
-
>
|
|
229
|
-
{icon}
|
|
230
|
-
</div>
|
|
231
|
-
<span className="text-[11px] font-semibold text-neutral-600 tracking-[0.2px]">
|
|
232
|
-
{title}
|
|
233
|
-
</span>
|
|
234
|
-
<span
|
|
235
|
-
className="ml-auto text-neutral-400 transition-transform"
|
|
236
|
-
style={{ transform: open ? "rotate(180deg)" : "rotate(0)" }}
|
|
237
|
-
>
|
|
238
|
-
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
239
|
-
<path
|
|
240
|
-
d="M3 4.5l3 3 3-3"
|
|
241
|
-
stroke="currentColor"
|
|
242
|
-
strokeWidth="1.5"
|
|
243
|
-
strokeLinecap="round"
|
|
244
|
-
strokeLinejoin="round"
|
|
245
|
-
/>
|
|
246
|
-
</svg>
|
|
247
|
-
</span>
|
|
248
|
-
</button>
|
|
249
|
-
{open && <div className="px-3 pb-3 pt-0.5">{children}</div>}
|
|
250
|
-
</div>
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ============================================
|
|
255
|
-
// Legacy Section — kept for NavItemSettings compatibility
|
|
256
|
-
// ============================================
|
|
257
|
-
|
|
258
|
-
export function Section({
|
|
259
|
-
title,
|
|
260
|
-
defaultOpen = true,
|
|
261
|
-
children,
|
|
262
|
-
}: {
|
|
263
|
-
title: string;
|
|
264
|
-
defaultOpen?: boolean;
|
|
265
|
-
children: ReactNode;
|
|
266
|
-
}) {
|
|
267
|
-
const [open, setOpen] = useState(defaultOpen);
|
|
268
|
-
|
|
269
|
-
return (
|
|
270
|
-
<div>
|
|
271
|
-
<button
|
|
272
|
-
onClick={() => setOpen(!open)}
|
|
273
|
-
className="flex items-center justify-between w-full py-2 border-t border-neutral-200 mt-1 text-[11px] font-semibold text-neutral-900 tracking-wide cursor-pointer uppercase"
|
|
274
|
-
>
|
|
275
|
-
{title}
|
|
276
|
-
<span
|
|
277
|
-
className="text-neutral-400 transition-transform"
|
|
278
|
-
style={{ transform: open ? "rotate(180deg)" : "rotate(0)" }}
|
|
279
|
-
>
|
|
280
|
-
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
281
|
-
<path
|
|
282
|
-
d="M3 4.5l3 3 3-3"
|
|
283
|
-
stroke="currentColor"
|
|
284
|
-
strokeWidth="1.5"
|
|
285
|
-
strokeLinecap="round"
|
|
286
|
-
strokeLinejoin="round"
|
|
287
|
-
/>
|
|
288
|
-
</svg>
|
|
289
|
-
</span>
|
|
290
|
-
</button>
|
|
291
|
-
{open && <div className="pb-2">{children}</div>}
|
|
292
|
-
</div>
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// ============================================
|
|
297
|
-
// Thin divider line
|
|
298
|
-
// ============================================
|
|
299
|
-
|
|
300
|
-
export function Divider() {
|
|
301
|
-
return <div className="h-px bg-[#f0f0f0] my-1.5" />;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// ============================================
|
|
305
|
-
// Color input (hex) with swatch preview
|
|
306
|
-
// ============================================
|
|
307
|
-
|
|
308
|
-
export function ColorInput({
|
|
309
|
-
value,
|
|
310
|
-
onChange,
|
|
311
|
-
placeholder = "Transparent",
|
|
312
|
-
}: {
|
|
313
|
-
value: string;
|
|
314
|
-
onChange: (value: string) => void;
|
|
315
|
-
placeholder?: string;
|
|
316
|
-
}) {
|
|
317
|
-
return (
|
|
318
|
-
<div className="flex gap-1.5 items-center">
|
|
319
|
-
<div
|
|
320
|
-
className="w-7 h-7 rounded-lg border border-neutral-200 shrink-0 cursor-pointer"
|
|
321
|
-
style={{
|
|
322
|
-
background: value || "transparent",
|
|
323
|
-
backgroundImage: !value
|
|
324
|
-
? "repeating-conic-gradient(#e5e5e5 0% 25%, transparent 0% 50%) 0 0 / 8px 8px"
|
|
325
|
-
: "none",
|
|
326
|
-
}}
|
|
327
|
-
/>
|
|
328
|
-
<TextInput value={value} onChange={onChange} placeholder={placeholder} />
|
|
329
|
-
{value && (
|
|
330
|
-
<button
|
|
331
|
-
onClick={() => onChange("")}
|
|
332
|
-
className="text-neutral-400 hover:text-neutral-600 shrink-0"
|
|
333
|
-
title="Clear"
|
|
334
|
-
>
|
|
335
|
-
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
336
|
-
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
|
|
337
|
-
</svg>
|
|
338
|
-
</button>
|
|
339
|
-
)}
|
|
340
|
-
</div>
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// ============================================
|
|
345
|
-
// Card section icon presets — re-exported from centralized section-icons.tsx
|
|
346
|
-
// Session 163: Unified icon system. All icons now live in section-icons.tsx.
|
|
347
|
-
// ============================================
|
|
348
|
-
export {
|
|
349
|
-
PositionIcon,
|
|
350
|
-
TypographyIcon,
|
|
351
|
-
AnimationIcon,
|
|
352
|
-
SpacingIcon,
|
|
353
|
-
ContentIcon,
|
|
354
|
-
LinkIcon,
|
|
355
|
-
GridIcon,
|
|
356
|
-
StyleIcon,
|
|
357
|
-
BackgroundIcon,
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
// ============================================
|
|
361
|
-
// Viewport Switcher — segmented control for Desktop / Tablet / Phone
|
|
362
|
-
// ============================================
|
|
363
|
-
|
|
364
|
-
const viewportOptions: { value: NavViewport; icon: ReactNode; label: string }[] = [
|
|
365
|
-
{
|
|
366
|
-
value: "desktop",
|
|
367
|
-
label: "Desktop",
|
|
368
|
-
icon: (
|
|
369
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
370
|
-
<rect x="2" y="3" width="20" height="14" rx="2" /><line x1="8" y1="21" x2="16" y2="21" /><line x1="12" y1="17" x2="12" y2="21" />
|
|
371
|
-
</svg>
|
|
372
|
-
),
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
value: "tablet",
|
|
376
|
-
label: "Tablet",
|
|
377
|
-
icon: (
|
|
378
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
379
|
-
<rect x="4" y="2" width="16" height="20" rx="2" /><line x1="12" y1="18" x2="12" y2="18" />
|
|
380
|
-
</svg>
|
|
381
|
-
),
|
|
382
|
-
},
|
|
383
|
-
{
|
|
384
|
-
value: "phone",
|
|
385
|
-
label: "Phone",
|
|
386
|
-
icon: (
|
|
387
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
388
|
-
<rect x="5" y="2" width="14" height="20" rx="2" /><line x1="12" y1="18" x2="12" y2="18" />
|
|
389
|
-
</svg>
|
|
390
|
-
),
|
|
391
|
-
},
|
|
392
|
-
];
|
|
393
|
-
|
|
394
|
-
export function ViewportSwitcher({
|
|
395
|
-
value,
|
|
396
|
-
onChange,
|
|
397
|
-
}: {
|
|
398
|
-
value: NavViewport;
|
|
399
|
-
onChange: (v: NavViewport) => void;
|
|
400
|
-
}) {
|
|
401
|
-
return (
|
|
402
|
-
<div className="flex bg-white rounded-lg p-0.5 border border-[#f0f0f0] mb-2">
|
|
403
|
-
{viewportOptions.map((opt) => {
|
|
404
|
-
const isActive = value === opt.value;
|
|
405
|
-
const isNonDesktop = opt.value !== "desktop";
|
|
406
|
-
return (
|
|
407
|
-
<button
|
|
408
|
-
key={opt.value}
|
|
409
|
-
onClick={() => onChange(opt.value)}
|
|
410
|
-
title={opt.label}
|
|
411
|
-
className={`flex-1 px-2 py-[5px] text-[10px] font-medium rounded-md transition-all flex items-center justify-center gap-1 ${
|
|
412
|
-
isActive
|
|
413
|
-
? isNonDesktop
|
|
414
|
-
? "text-[#
|
|
415
|
-
: "text-neutral-900 bg-neutral-100 shadow-[0_1px_2px_rgba(0,0,0,0.04)]"
|
|
416
|
-
: "text-neutral-400 hover:text-neutral-600"
|
|
417
|
-
}`}
|
|
418
|
-
>
|
|
419
|
-
{opt.icon}
|
|
420
|
-
</button>
|
|
421
|
-
);
|
|
422
|
-
})}
|
|
423
|
-
</div>
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// ============================================
|
|
428
|
-
// Viewport Badge — shows which viewport is being edited
|
|
429
|
-
// ============================================
|
|
430
|
-
|
|
431
|
-
export function NavViewportBadge({ viewport }: { viewport: NavViewport }) {
|
|
432
|
-
if (viewport === "desktop") return null;
|
|
433
|
-
|
|
434
|
-
const color = viewport === "tablet" ? "#
|
|
435
|
-
const bgColor = viewport === "tablet" ? "rgba(
|
|
436
|
-
const label = viewport === "tablet" ? "Tablet" : "Phone";
|
|
437
|
-
|
|
438
|
-
return (
|
|
439
|
-
<div
|
|
440
|
-
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md mb-2 text-[10px]"
|
|
441
|
-
style={{ background: bgColor, color }}
|
|
442
|
-
>
|
|
443
|
-
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
444
|
-
<circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
|
|
445
|
-
</svg>
|
|
446
|
-
<span>
|
|
447
|
-
Editing <strong>{label}</strong> overrides · empty fields inherit desktop
|
|
448
|
-
</span>
|
|
449
|
-
</div>
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// ============================================
|
|
454
|
-
// Responsive Field — wraps a field with inherited/overridden state
|
|
455
|
-
// ============================================
|
|
456
|
-
|
|
457
|
-
export function ResponsiveField({
|
|
458
|
-
label,
|
|
459
|
-
viewport,
|
|
460
|
-
isOverridden,
|
|
461
|
-
onReset,
|
|
462
|
-
children,
|
|
463
|
-
}: {
|
|
464
|
-
label: string;
|
|
465
|
-
viewport: NavViewport;
|
|
466
|
-
isOverridden: boolean;
|
|
467
|
-
onReset?: () => void;
|
|
468
|
-
children: ReactNode;
|
|
469
|
-
}) {
|
|
470
|
-
// Desktop mode — render as a normal field
|
|
471
|
-
if (viewport === "desktop") {
|
|
472
|
-
return <Field label={label}>{children}</Field>;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return (
|
|
476
|
-
<div className="flex items-center gap-2.5 py-[5px]">
|
|
477
|
-
<div className="w-[56px] min-w-[56px] shrink-0">
|
|
478
|
-
<label
|
|
479
|
-
className={`text-[11px] block ${
|
|
480
|
-
isOverridden ? "text-[#
|
|
481
|
-
}`}
|
|
482
|
-
>
|
|
483
|
-
{label}
|
|
484
|
-
</label>
|
|
485
|
-
{isOverridden ? (
|
|
486
|
-
<span className="text-[8px] text-[#
|
|
487
|
-
override
|
|
488
|
-
</span>
|
|
489
|
-
) : (
|
|
490
|
-
<span className="text-[8px] text-neutral-300 uppercase tracking-[0.5px]">
|
|
491
|
-
inherited
|
|
492
|
-
</span>
|
|
493
|
-
)}
|
|
494
|
-
</div>
|
|
495
|
-
<div
|
|
496
|
-
className="flex-1 min-w-0 transition-opacity"
|
|
497
|
-
style={{ opacity: isOverridden ? 1 : 0.5 }}
|
|
498
|
-
>
|
|
499
|
-
{children}
|
|
500
|
-
</div>
|
|
501
|
-
{isOverridden && onReset && (
|
|
502
|
-
<button
|
|
503
|
-
onClick={onReset}
|
|
504
|
-
className="shrink-0 text-[#
|
|
505
|
-
title="Reset to desktop value"
|
|
506
|
-
>
|
|
507
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
508
|
-
<polyline points="1 4 1 10 7 10" /><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
|
|
509
|
-
</svg>
|
|
510
|
-
</button>
|
|
511
|
-
)}
|
|
512
|
-
</div>
|
|
513
|
-
);
|
|
514
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from "react";
|
|
4
|
+
import {
|
|
5
|
+
PositionIcon,
|
|
6
|
+
TypographyIcon,
|
|
7
|
+
AnimationIcon,
|
|
8
|
+
SpacingIcon,
|
|
9
|
+
ContentIcon,
|
|
10
|
+
LinkIcon,
|
|
11
|
+
GridIcon,
|
|
12
|
+
StyleIcon,
|
|
13
|
+
BackgroundIcon,
|
|
14
|
+
} from "../../builder/editors/section-icons";
|
|
15
|
+
|
|
16
|
+
// ── Nav viewport type for responsive overrides ──
|
|
17
|
+
export type NavViewport = "desktop" | "tablet" | "phone";
|
|
18
|
+
|
|
19
|
+
// ── Shared field components for NavBuilder settings panel ──
|
|
20
|
+
// v2: Card-based sections, colored icons, builder-aligned styling
|
|
21
|
+
|
|
22
|
+
// ============================================
|
|
23
|
+
// Field wrapper
|
|
24
|
+
// ============================================
|
|
25
|
+
|
|
26
|
+
export function Field({ label, children }: { label: string; children: ReactNode }) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="flex items-center gap-2.5 py-[5px]">
|
|
29
|
+
<label className="text-[11px] text-neutral-400 w-[56px] min-w-[56px] shrink-0">
|
|
30
|
+
{label}
|
|
31
|
+
</label>
|
|
32
|
+
<div className="flex-1 min-w-0">{children}</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================
|
|
38
|
+
// Text input
|
|
39
|
+
// ============================================
|
|
40
|
+
|
|
41
|
+
export function TextInput({
|
|
42
|
+
value,
|
|
43
|
+
onChange,
|
|
44
|
+
placeholder,
|
|
45
|
+
type = "text",
|
|
46
|
+
}: {
|
|
47
|
+
value: string | number;
|
|
48
|
+
onChange: (value: string) => void;
|
|
49
|
+
placeholder?: string;
|
|
50
|
+
type?: "text" | "number";
|
|
51
|
+
}) {
|
|
52
|
+
return (
|
|
53
|
+
<input
|
|
54
|
+
type={type}
|
|
55
|
+
value={value}
|
|
56
|
+
onChange={(e) => onChange(e.target.value)}
|
|
57
|
+
placeholder={placeholder}
|
|
58
|
+
className="w-full rounded-lg border border-transparent bg-white px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-neutral-50 focus:bg-white focus:border-[#3580f9] focus:shadow-[0_0_0_3px_rgba(53, 128, 249,0.06)] font-[inherit] placeholder:text-neutral-400"
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================
|
|
64
|
+
// Select input
|
|
65
|
+
// ============================================
|
|
66
|
+
|
|
67
|
+
export function SelectInput({
|
|
68
|
+
value,
|
|
69
|
+
onChange,
|
|
70
|
+
options,
|
|
71
|
+
}: {
|
|
72
|
+
value: string;
|
|
73
|
+
onChange: (value: string) => void;
|
|
74
|
+
options: { value: string; label: string }[];
|
|
75
|
+
}) {
|
|
76
|
+
return (
|
|
77
|
+
<div className="relative">
|
|
78
|
+
<select
|
|
79
|
+
value={value}
|
|
80
|
+
onChange={(e) => onChange(e.target.value)}
|
|
81
|
+
className="w-full rounded-lg border border-transparent bg-white px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none appearance-none cursor-pointer pr-7 transition-all hover:bg-neutral-50 focus:bg-white focus:border-[#3580f9] focus:shadow-[0_0_0_3px_rgba(53, 128, 249,0.06)] font-[inherit]"
|
|
82
|
+
>
|
|
83
|
+
{options.map((opt) => (
|
|
84
|
+
<option key={opt.value} value={opt.value}>
|
|
85
|
+
{opt.label}
|
|
86
|
+
</option>
|
|
87
|
+
))}
|
|
88
|
+
</select>
|
|
89
|
+
<div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none text-neutral-400">
|
|
90
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
91
|
+
<path
|
|
92
|
+
d="M3 4.5l3 3 3-3"
|
|
93
|
+
stroke="currentColor"
|
|
94
|
+
strokeWidth="1.5"
|
|
95
|
+
strokeLinecap="round"
|
|
96
|
+
strokeLinejoin="round"
|
|
97
|
+
/>
|
|
98
|
+
</svg>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============================================
|
|
105
|
+
// Segmented control
|
|
106
|
+
// ============================================
|
|
107
|
+
|
|
108
|
+
export function SegmentedControl({
|
|
109
|
+
value,
|
|
110
|
+
onChange,
|
|
111
|
+
options,
|
|
112
|
+
}: {
|
|
113
|
+
value: string;
|
|
114
|
+
onChange: (value: string) => void;
|
|
115
|
+
options: { value: string; label: string | ReactNode }[];
|
|
116
|
+
}) {
|
|
117
|
+
return (
|
|
118
|
+
<div className="flex bg-white rounded-lg p-0.5 border border-[#f0f0f0]">
|
|
119
|
+
{options.map((opt) => (
|
|
120
|
+
<button
|
|
121
|
+
key={String(opt.value)}
|
|
122
|
+
onClick={() => onChange(opt.value)}
|
|
123
|
+
className={`flex-1 px-2 py-[5px] text-[10px] font-medium rounded-md transition-all flex items-center justify-center ${
|
|
124
|
+
value === opt.value
|
|
125
|
+
? "text-neutral-900 bg-neutral-100 shadow-[0_1px_2px_rgba(0,0,0,0.04)]"
|
|
126
|
+
: "text-neutral-400 hover:text-neutral-600"
|
|
127
|
+
}`}
|
|
128
|
+
>
|
|
129
|
+
{opt.label}
|
|
130
|
+
</button>
|
|
131
|
+
))}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============================================
|
|
137
|
+
// Toggle switch
|
|
138
|
+
// ============================================
|
|
139
|
+
|
|
140
|
+
export function Toggle({
|
|
141
|
+
value,
|
|
142
|
+
onChange,
|
|
143
|
+
}: {
|
|
144
|
+
value: boolean;
|
|
145
|
+
onChange: (value: boolean) => void;
|
|
146
|
+
}) {
|
|
147
|
+
return (
|
|
148
|
+
<button
|
|
149
|
+
onClick={() => onChange(!value)}
|
|
150
|
+
className="w-9 h-5 rounded-full relative transition-all cursor-pointer"
|
|
151
|
+
style={{
|
|
152
|
+
background: value ? "#3580f9" : "#d4d4d4",
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
<div
|
|
156
|
+
className="w-3.5 h-3.5 rounded-full bg-white absolute top-[2.5px] transition-all shadow-sm"
|
|
157
|
+
style={{ left: value ? 19 : 2 }}
|
|
158
|
+
/>
|
|
159
|
+
</button>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ============================================
|
|
164
|
+
// Range slider
|
|
165
|
+
// ============================================
|
|
166
|
+
|
|
167
|
+
export function RangeSlider({
|
|
168
|
+
value,
|
|
169
|
+
onChange,
|
|
170
|
+
min = 0,
|
|
171
|
+
max = 100,
|
|
172
|
+
suffix,
|
|
173
|
+
}: {
|
|
174
|
+
value: number;
|
|
175
|
+
onChange: (value: number) => void;
|
|
176
|
+
min?: number;
|
|
177
|
+
max?: number;
|
|
178
|
+
suffix?: string;
|
|
179
|
+
}) {
|
|
180
|
+
return (
|
|
181
|
+
<div className="flex items-center gap-2">
|
|
182
|
+
<input
|
|
183
|
+
type="range"
|
|
184
|
+
min={min}
|
|
185
|
+
max={max}
|
|
186
|
+
value={value}
|
|
187
|
+
onChange={(e) => onChange(Number(e.target.value))}
|
|
188
|
+
className="flex-1"
|
|
189
|
+
style={{ accentColor: "#3580f9" }}
|
|
190
|
+
/>
|
|
191
|
+
<span className="text-[10px] text-neutral-400 w-9 text-right tabular-nums">
|
|
192
|
+
{value}{suffix || ""}
|
|
193
|
+
</span>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ============================================
|
|
199
|
+
// Card section — collapsible with colored icon
|
|
200
|
+
// ============================================
|
|
201
|
+
|
|
202
|
+
export function CardSection({
|
|
203
|
+
title,
|
|
204
|
+
icon,
|
|
205
|
+
iconBg,
|
|
206
|
+
defaultOpen = true,
|
|
207
|
+
children,
|
|
208
|
+
}: {
|
|
209
|
+
title: string;
|
|
210
|
+
icon: ReactNode;
|
|
211
|
+
iconBg: string;
|
|
212
|
+
defaultOpen?: boolean;
|
|
213
|
+
children: ReactNode;
|
|
214
|
+
}) {
|
|
215
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<div
|
|
219
|
+
className="mx-2.5 my-1 rounded-[10px] bg-[#fafafa] border border-transparent transition-colors hover:border-[#f0f0f0]"
|
|
220
|
+
>
|
|
221
|
+
<button
|
|
222
|
+
onClick={() => setOpen(!open)}
|
|
223
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 cursor-pointer select-none hover:bg-black/[0.01] rounded-[10px] transition-colors"
|
|
224
|
+
>
|
|
225
|
+
<div
|
|
226
|
+
className="w-[26px] h-[26px] rounded-[7px] flex items-center justify-center shrink-0"
|
|
227
|
+
style={{ background: iconBg }}
|
|
228
|
+
>
|
|
229
|
+
{icon}
|
|
230
|
+
</div>
|
|
231
|
+
<span className="text-[11px] font-semibold text-neutral-600 tracking-[0.2px]">
|
|
232
|
+
{title}
|
|
233
|
+
</span>
|
|
234
|
+
<span
|
|
235
|
+
className="ml-auto text-neutral-400 transition-transform"
|
|
236
|
+
style={{ transform: open ? "rotate(180deg)" : "rotate(0)" }}
|
|
237
|
+
>
|
|
238
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
239
|
+
<path
|
|
240
|
+
d="M3 4.5l3 3 3-3"
|
|
241
|
+
stroke="currentColor"
|
|
242
|
+
strokeWidth="1.5"
|
|
243
|
+
strokeLinecap="round"
|
|
244
|
+
strokeLinejoin="round"
|
|
245
|
+
/>
|
|
246
|
+
</svg>
|
|
247
|
+
</span>
|
|
248
|
+
</button>
|
|
249
|
+
{open && <div className="px-3 pb-3 pt-0.5">{children}</div>}
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ============================================
|
|
255
|
+
// Legacy Section — kept for NavItemSettings compatibility
|
|
256
|
+
// ============================================
|
|
257
|
+
|
|
258
|
+
export function Section({
|
|
259
|
+
title,
|
|
260
|
+
defaultOpen = true,
|
|
261
|
+
children,
|
|
262
|
+
}: {
|
|
263
|
+
title: string;
|
|
264
|
+
defaultOpen?: boolean;
|
|
265
|
+
children: ReactNode;
|
|
266
|
+
}) {
|
|
267
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<div>
|
|
271
|
+
<button
|
|
272
|
+
onClick={() => setOpen(!open)}
|
|
273
|
+
className="flex items-center justify-between w-full py-2 border-t border-neutral-200 mt-1 text-[11px] font-semibold text-neutral-900 tracking-wide cursor-pointer uppercase"
|
|
274
|
+
>
|
|
275
|
+
{title}
|
|
276
|
+
<span
|
|
277
|
+
className="text-neutral-400 transition-transform"
|
|
278
|
+
style={{ transform: open ? "rotate(180deg)" : "rotate(0)" }}
|
|
279
|
+
>
|
|
280
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
281
|
+
<path
|
|
282
|
+
d="M3 4.5l3 3 3-3"
|
|
283
|
+
stroke="currentColor"
|
|
284
|
+
strokeWidth="1.5"
|
|
285
|
+
strokeLinecap="round"
|
|
286
|
+
strokeLinejoin="round"
|
|
287
|
+
/>
|
|
288
|
+
</svg>
|
|
289
|
+
</span>
|
|
290
|
+
</button>
|
|
291
|
+
{open && <div className="pb-2">{children}</div>}
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ============================================
|
|
297
|
+
// Thin divider line
|
|
298
|
+
// ============================================
|
|
299
|
+
|
|
300
|
+
export function Divider() {
|
|
301
|
+
return <div className="h-px bg-[#f0f0f0] my-1.5" />;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ============================================
|
|
305
|
+
// Color input (hex) with swatch preview
|
|
306
|
+
// ============================================
|
|
307
|
+
|
|
308
|
+
export function ColorInput({
|
|
309
|
+
value,
|
|
310
|
+
onChange,
|
|
311
|
+
placeholder = "Transparent",
|
|
312
|
+
}: {
|
|
313
|
+
value: string;
|
|
314
|
+
onChange: (value: string) => void;
|
|
315
|
+
placeholder?: string;
|
|
316
|
+
}) {
|
|
317
|
+
return (
|
|
318
|
+
<div className="flex gap-1.5 items-center">
|
|
319
|
+
<div
|
|
320
|
+
className="w-7 h-7 rounded-lg border border-neutral-200 shrink-0 cursor-pointer"
|
|
321
|
+
style={{
|
|
322
|
+
background: value || "transparent",
|
|
323
|
+
backgroundImage: !value
|
|
324
|
+
? "repeating-conic-gradient(#e5e5e5 0% 25%, transparent 0% 50%) 0 0 / 8px 8px"
|
|
325
|
+
: "none",
|
|
326
|
+
}}
|
|
327
|
+
/>
|
|
328
|
+
<TextInput value={value} onChange={onChange} placeholder={placeholder} />
|
|
329
|
+
{value && (
|
|
330
|
+
<button
|
|
331
|
+
onClick={() => onChange("")}
|
|
332
|
+
className="text-neutral-400 hover:text-neutral-600 shrink-0"
|
|
333
|
+
title="Clear"
|
|
334
|
+
>
|
|
335
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
336
|
+
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
|
|
337
|
+
</svg>
|
|
338
|
+
</button>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ============================================
|
|
345
|
+
// Card section icon presets — re-exported from centralized section-icons.tsx
|
|
346
|
+
// Session 163: Unified icon system. All icons now live in section-icons.tsx.
|
|
347
|
+
// ============================================
|
|
348
|
+
export {
|
|
349
|
+
PositionIcon,
|
|
350
|
+
TypographyIcon,
|
|
351
|
+
AnimationIcon,
|
|
352
|
+
SpacingIcon,
|
|
353
|
+
ContentIcon,
|
|
354
|
+
LinkIcon,
|
|
355
|
+
GridIcon,
|
|
356
|
+
StyleIcon,
|
|
357
|
+
BackgroundIcon,
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// ============================================
|
|
361
|
+
// Viewport Switcher — segmented control for Desktop / Tablet / Phone
|
|
362
|
+
// ============================================
|
|
363
|
+
|
|
364
|
+
const viewportOptions: { value: NavViewport; icon: ReactNode; label: string }[] = [
|
|
365
|
+
{
|
|
366
|
+
value: "desktop",
|
|
367
|
+
label: "Desktop",
|
|
368
|
+
icon: (
|
|
369
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
370
|
+
<rect x="2" y="3" width="20" height="14" rx="2" /><line x1="8" y1="21" x2="16" y2="21" /><line x1="12" y1="17" x2="12" y2="21" />
|
|
371
|
+
</svg>
|
|
372
|
+
),
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
value: "tablet",
|
|
376
|
+
label: "Tablet",
|
|
377
|
+
icon: (
|
|
378
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
379
|
+
<rect x="4" y="2" width="16" height="20" rx="2" /><line x1="12" y1="18" x2="12" y2="18" />
|
|
380
|
+
</svg>
|
|
381
|
+
),
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
value: "phone",
|
|
385
|
+
label: "Phone",
|
|
386
|
+
icon: (
|
|
387
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
388
|
+
<rect x="5" y="2" width="14" height="20" rx="2" /><line x1="12" y1="18" x2="12" y2="18" />
|
|
389
|
+
</svg>
|
|
390
|
+
),
|
|
391
|
+
},
|
|
392
|
+
];
|
|
393
|
+
|
|
394
|
+
export function ViewportSwitcher({
|
|
395
|
+
value,
|
|
396
|
+
onChange,
|
|
397
|
+
}: {
|
|
398
|
+
value: NavViewport;
|
|
399
|
+
onChange: (v: NavViewport) => void;
|
|
400
|
+
}) {
|
|
401
|
+
return (
|
|
402
|
+
<div className="flex bg-white rounded-lg p-0.5 border border-[#f0f0f0] mb-2">
|
|
403
|
+
{viewportOptions.map((opt) => {
|
|
404
|
+
const isActive = value === opt.value;
|
|
405
|
+
const isNonDesktop = opt.value !== "desktop";
|
|
406
|
+
return (
|
|
407
|
+
<button
|
|
408
|
+
key={opt.value}
|
|
409
|
+
onClick={() => onChange(opt.value)}
|
|
410
|
+
title={opt.label}
|
|
411
|
+
className={`flex-1 px-2 py-[5px] text-[10px] font-medium rounded-md transition-all flex items-center justify-center gap-1 ${
|
|
412
|
+
isActive
|
|
413
|
+
? isNonDesktop
|
|
414
|
+
? "text-[#3580f9] bg-blue-50 shadow-[0_1px_2px_rgba(53, 128, 249,0.08)]"
|
|
415
|
+
: "text-neutral-900 bg-neutral-100 shadow-[0_1px_2px_rgba(0,0,0,0.04)]"
|
|
416
|
+
: "text-neutral-400 hover:text-neutral-600"
|
|
417
|
+
}`}
|
|
418
|
+
>
|
|
419
|
+
{opt.icon}
|
|
420
|
+
</button>
|
|
421
|
+
);
|
|
422
|
+
})}
|
|
423
|
+
</div>
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ============================================
|
|
428
|
+
// Viewport Badge — shows which viewport is being edited
|
|
429
|
+
// ============================================
|
|
430
|
+
|
|
431
|
+
export function NavViewportBadge({ viewport }: { viewport: NavViewport }) {
|
|
432
|
+
if (viewport === "desktop") return null;
|
|
433
|
+
|
|
434
|
+
const color = viewport === "tablet" ? "#3580f9" : "#7c3aed";
|
|
435
|
+
const bgColor = viewport === "tablet" ? "rgba(53, 128, 249, 0.06)" : "rgba(124, 58, 237, 0.06)";
|
|
436
|
+
const label = viewport === "tablet" ? "Tablet" : "Phone";
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<div
|
|
440
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md mb-2 text-[10px]"
|
|
441
|
+
style={{ background: bgColor, color }}
|
|
442
|
+
>
|
|
443
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
444
|
+
<circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
|
|
445
|
+
</svg>
|
|
446
|
+
<span>
|
|
447
|
+
Editing <strong>{label}</strong> overrides · empty fields inherit desktop
|
|
448
|
+
</span>
|
|
449
|
+
</div>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ============================================
|
|
454
|
+
// Responsive Field — wraps a field with inherited/overridden state
|
|
455
|
+
// ============================================
|
|
456
|
+
|
|
457
|
+
export function ResponsiveField({
|
|
458
|
+
label,
|
|
459
|
+
viewport,
|
|
460
|
+
isOverridden,
|
|
461
|
+
onReset,
|
|
462
|
+
children,
|
|
463
|
+
}: {
|
|
464
|
+
label: string;
|
|
465
|
+
viewport: NavViewport;
|
|
466
|
+
isOverridden: boolean;
|
|
467
|
+
onReset?: () => void;
|
|
468
|
+
children: ReactNode;
|
|
469
|
+
}) {
|
|
470
|
+
// Desktop mode — render as a normal field
|
|
471
|
+
if (viewport === "desktop") {
|
|
472
|
+
return <Field label={label}>{children}</Field>;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return (
|
|
476
|
+
<div className="flex items-center gap-2.5 py-[5px]">
|
|
477
|
+
<div className="w-[56px] min-w-[56px] shrink-0">
|
|
478
|
+
<label
|
|
479
|
+
className={`text-[11px] block ${
|
|
480
|
+
isOverridden ? "text-[#3580f9] font-medium" : "text-neutral-400"
|
|
481
|
+
}`}
|
|
482
|
+
>
|
|
483
|
+
{label}
|
|
484
|
+
</label>
|
|
485
|
+
{isOverridden ? (
|
|
486
|
+
<span className="text-[8px] text-[#3580f9]/60 uppercase tracking-[0.5px]">
|
|
487
|
+
override
|
|
488
|
+
</span>
|
|
489
|
+
) : (
|
|
490
|
+
<span className="text-[8px] text-neutral-300 uppercase tracking-[0.5px]">
|
|
491
|
+
inherited
|
|
492
|
+
</span>
|
|
493
|
+
)}
|
|
494
|
+
</div>
|
|
495
|
+
<div
|
|
496
|
+
className="flex-1 min-w-0 transition-opacity"
|
|
497
|
+
style={{ opacity: isOverridden ? 1 : 0.5 }}
|
|
498
|
+
>
|
|
499
|
+
{children}
|
|
500
|
+
</div>
|
|
501
|
+
{isOverridden && onReset && (
|
|
502
|
+
<button
|
|
503
|
+
onClick={onReset}
|
|
504
|
+
className="shrink-0 text-[#3580f9]/60 hover:text-[#3580f9] transition-colors"
|
|
505
|
+
title="Reset to desktop value"
|
|
506
|
+
>
|
|
507
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
508
|
+
<polyline points="1 4 1 10 7 10" /><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
|
|
509
|
+
</svg>
|
|
510
|
+
</button>
|
|
511
|
+
)}
|
|
512
|
+
</div>
|
|
513
|
+
);
|
|
514
|
+
}
|