@hyperframes/studio 0.6.0-alpha.1 → 0.6.0-alpha.11
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/dist/assets/hyperframes-player-DjsVzYFP.js +418 -0
- package/dist/assets/index-FWg79aJz.css +1 -0
- package/dist/assets/index-xyVaWqe2.js +108 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +422 -71
- package/src/components/editor/PropertyPanel.test.ts +49 -0
- package/src/components/editor/PropertyPanel.tsx +277 -337
- package/src/components/editor/domEditing.test.ts +248 -0
- package/src/components/editor/domEditing.ts +126 -2
- package/src/components/editor/manualEditingAvailability.test.ts +15 -4
- package/src/components/editor/manualEditingAvailability.ts +4 -2
- package/src/components/editor/manualEdits.ts +15 -3
- package/src/components/nle/NLELayout.test.ts +12 -0
- package/src/components/nle/NLELayout.tsx +63 -24
- package/src/components/nle/NLEPreview.tsx +6 -0
- package/src/components/renders/RenderQueue.tsx +56 -4
- package/src/components/renders/useRenderQueue.ts +30 -6
- package/src/components/sidebar/LeftSidebar.tsx +186 -186
- package/src/player/components/Player.test.ts +58 -0
- package/src/player/components/Player.tsx +71 -4
- package/src/player/components/PlayerControls.tsx +20 -7
- package/src/player/components/Timeline.tsx +45 -20
- package/src/utils/timelineDiscovery.ts +1 -1
- package/dist/assets/hyperframes-player-Cd8vYWxP.js +0 -198
- package/dist/assets/index-D04_ZoMm.js +0 -107
- package/dist/assets/index-UWFaHilT.css +0 -1
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
hsvToRgb,
|
|
29
29
|
parseCssColor,
|
|
30
30
|
rgbToHsv,
|
|
31
|
-
toColorPickerValue,
|
|
32
31
|
toHexColor,
|
|
33
32
|
type ParsedColor,
|
|
34
33
|
} from "./colorValue";
|
|
@@ -54,6 +53,7 @@ interface PropertyPanelProps {
|
|
|
54
53
|
projectId: string;
|
|
55
54
|
assets: string[];
|
|
56
55
|
element: DomEditSelection | null;
|
|
56
|
+
multiSelectCount?: number;
|
|
57
57
|
copiedAgentPrompt: boolean;
|
|
58
58
|
onClearSelection: () => void;
|
|
59
59
|
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
|
@@ -366,13 +366,6 @@ function adjustNumericToken(
|
|
|
366
366
|
return `${formatNumericValue(nextValue)}${token.unit}`;
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
-
function formatColorToken(value: string): string {
|
|
370
|
-
const parsed = parseCssColor(value);
|
|
371
|
-
if (!parsed) return value;
|
|
372
|
-
const hex = toColorPickerValue(value).replace(/^#/, "").toUpperCase();
|
|
373
|
-
return `${hex} / ${Math.round(parsed.alpha * 100)}%`;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
369
|
function extractBackgroundImageUrl(value: string | undefined): string {
|
|
377
370
|
if (!value) return "";
|
|
378
371
|
const lowerValue = value.toLowerCase();
|
|
@@ -453,36 +446,6 @@ function resolveSelectedAsset(
|
|
|
453
446
|
return null;
|
|
454
447
|
}
|
|
455
448
|
|
|
456
|
-
function collectSelectionColors(styles: Record<string, string>) {
|
|
457
|
-
const candidates = [
|
|
458
|
-
{ source: "Fill", value: styles["background-color"] },
|
|
459
|
-
{ source: "Text", value: styles.color },
|
|
460
|
-
];
|
|
461
|
-
|
|
462
|
-
const deduped = new Map<string, { swatch: string; token: string; sources: string[] }>();
|
|
463
|
-
|
|
464
|
-
for (const candidate of candidates) {
|
|
465
|
-
if (!candidate.value) continue;
|
|
466
|
-
const parsed = parseCssColor(candidate.value);
|
|
467
|
-
if (!parsed || parsed.alpha <= 0) continue;
|
|
468
|
-
|
|
469
|
-
const key = `${toColorPickerValue(candidate.value)}-${Math.round(parsed.alpha * 100)}`;
|
|
470
|
-
const existing = deduped.get(key);
|
|
471
|
-
if (existing) {
|
|
472
|
-
existing.sources.push(candidate.source);
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
deduped.set(key, {
|
|
477
|
-
swatch: toColorPickerValue(candidate.value),
|
|
478
|
-
token: formatColorToken(candidate.value),
|
|
479
|
-
sources: [candidate.source],
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
return Array.from(deduped.values());
|
|
484
|
-
}
|
|
485
|
-
|
|
486
449
|
function CommitField({
|
|
487
450
|
value,
|
|
488
451
|
disabled,
|
|
@@ -497,13 +460,34 @@ function CommitField({
|
|
|
497
460
|
const [draft, setDraft] = useState(value);
|
|
498
461
|
const commitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
499
462
|
const valueRef = useRef(value);
|
|
463
|
+
const draftRef = useRef(draft);
|
|
464
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
500
465
|
|
|
501
466
|
valueRef.current = value;
|
|
467
|
+
draftRef.current = draft;
|
|
502
468
|
|
|
503
469
|
useEffect(() => {
|
|
504
470
|
setDraft(value);
|
|
505
471
|
}, [value]);
|
|
506
472
|
|
|
473
|
+
useEffect(() => {
|
|
474
|
+
const el = inputRef.current;
|
|
475
|
+
if (!el) return;
|
|
476
|
+
const handler = (e: WheelEvent) => {
|
|
477
|
+
if (disabled) return;
|
|
478
|
+
const delta = e.deltaY === 0 ? e.deltaX : e.deltaY;
|
|
479
|
+
if (delta === 0) return;
|
|
480
|
+
const nextDraft = adjustNumericToken(draftRef.current, delta < 0 ? 1 : -1, e);
|
|
481
|
+
if (!nextDraft) return;
|
|
482
|
+
e.preventDefault();
|
|
483
|
+
e.stopPropagation();
|
|
484
|
+
setDraft(nextDraft);
|
|
485
|
+
scheduleCommitRef.current(nextDraft);
|
|
486
|
+
};
|
|
487
|
+
el.addEventListener("wheel", handler, { passive: false });
|
|
488
|
+
return () => el.removeEventListener("wheel", handler);
|
|
489
|
+
}, [disabled]);
|
|
490
|
+
|
|
507
491
|
useEffect(
|
|
508
492
|
() => () => {
|
|
509
493
|
if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
|
|
@@ -526,9 +510,12 @@ function CommitField({
|
|
|
526
510
|
}
|
|
527
511
|
}, 120);
|
|
528
512
|
};
|
|
513
|
+
const scheduleCommitRef = useRef(scheduleCommit);
|
|
514
|
+
scheduleCommitRef.current = scheduleCommit;
|
|
529
515
|
|
|
530
516
|
return (
|
|
531
517
|
<input
|
|
518
|
+
ref={inputRef}
|
|
532
519
|
type="text"
|
|
533
520
|
value={draft}
|
|
534
521
|
disabled={disabled}
|
|
@@ -537,16 +524,6 @@ function CommitField({
|
|
|
537
524
|
if (liveCommit) scheduleCommit(e.target.value);
|
|
538
525
|
}}
|
|
539
526
|
onBlur={() => commitDraft(draft)}
|
|
540
|
-
onWheel={(e) => {
|
|
541
|
-
if (disabled) return;
|
|
542
|
-
const delta = e.deltaY === 0 ? e.deltaX : e.deltaY;
|
|
543
|
-
if (delta === 0) return;
|
|
544
|
-
const nextDraft = adjustNumericToken(draft, delta < 0 ? 1 : -1, e);
|
|
545
|
-
if (!nextDraft) return;
|
|
546
|
-
e.preventDefault();
|
|
547
|
-
setDraft(nextDraft);
|
|
548
|
-
scheduleCommit(nextDraft);
|
|
549
|
-
}}
|
|
550
527
|
onKeyDown={(e) => {
|
|
551
528
|
if (e.key === "Enter") {
|
|
552
529
|
(e.target as HTMLInputElement).blur();
|
|
@@ -2136,22 +2113,20 @@ function SelectField({
|
|
|
2136
2113
|
const renderedOptions = value && !options.includes(value) ? [value, ...options] : options;
|
|
2137
2114
|
|
|
2138
2115
|
return (
|
|
2139
|
-
<label className=
|
|
2140
|
-
<span className=
|
|
2141
|
-
<
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
{
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
</select>
|
|
2154
|
-
</div>
|
|
2116
|
+
<label className={`${FIELD} flex items-center gap-3`}>
|
|
2117
|
+
<span className="flex-shrink-0 text-[11px] font-medium text-neutral-500">{label}</span>
|
|
2118
|
+
<select
|
|
2119
|
+
value={value}
|
|
2120
|
+
disabled={disabled}
|
|
2121
|
+
onChange={(e) => onChange(e.target.value)}
|
|
2122
|
+
className="min-w-0 w-full appearance-none bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600"
|
|
2123
|
+
>
|
|
2124
|
+
{renderedOptions.map((option) => (
|
|
2125
|
+
<option key={option} value={option}>
|
|
2126
|
+
{option}
|
|
2127
|
+
</option>
|
|
2128
|
+
))}
|
|
2129
|
+
</select>
|
|
2155
2130
|
</label>
|
|
2156
2131
|
);
|
|
2157
2132
|
}
|
|
@@ -2183,35 +2158,11 @@ function Section({
|
|
|
2183
2158
|
);
|
|
2184
2159
|
}
|
|
2185
2160
|
|
|
2186
|
-
function SelectionColorRow({
|
|
2187
|
-
swatch,
|
|
2188
|
-
token,
|
|
2189
|
-
sources,
|
|
2190
|
-
}: {
|
|
2191
|
-
swatch: string;
|
|
2192
|
-
token: string;
|
|
2193
|
-
sources: string[];
|
|
2194
|
-
}) {
|
|
2195
|
-
return (
|
|
2196
|
-
<div className={`${FIELD} flex min-w-0 items-center gap-3`}>
|
|
2197
|
-
<div
|
|
2198
|
-
className="h-7 w-7 flex-shrink-0 rounded-lg border border-neutral-700"
|
|
2199
|
-
style={{ backgroundColor: swatch }}
|
|
2200
|
-
/>
|
|
2201
|
-
<div className="min-w-0 flex-1">
|
|
2202
|
-
<div className="truncate text-[11px] font-medium text-neutral-100">{token}</div>
|
|
2203
|
-
<div className="truncate text-[11px] uppercase tracking-[0.18em] text-neutral-500">
|
|
2204
|
-
{sources.join(" · ")}
|
|
2205
|
-
</div>
|
|
2206
|
-
</div>
|
|
2207
|
-
</div>
|
|
2208
|
-
);
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
2161
|
export const PropertyPanel = memo(function PropertyPanel({
|
|
2212
2162
|
projectId,
|
|
2213
2163
|
assets,
|
|
2214
2164
|
element,
|
|
2165
|
+
multiSelectCount = 0,
|
|
2215
2166
|
copiedAgentPrompt,
|
|
2216
2167
|
onClearSelection,
|
|
2217
2168
|
onSetStyle,
|
|
@@ -2228,7 +2179,6 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
2228
2179
|
onImportFonts,
|
|
2229
2180
|
}: PropertyPanelProps) {
|
|
2230
2181
|
const styles = element?.computedStyles ?? EMPTY_STYLES;
|
|
2231
|
-
const selectionColors = useMemo(() => collectSelectionColors(styles), [styles]);
|
|
2232
2182
|
const backgroundImage = styles["background-image"] ?? "none";
|
|
2233
2183
|
const fillMode =
|
|
2234
2184
|
backgroundImage && backgroundImage !== "none"
|
|
@@ -2258,12 +2208,29 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
2258
2208
|
if (!element) {
|
|
2259
2209
|
return (
|
|
2260
2210
|
<div className="flex h-full flex-col items-center justify-center bg-neutral-900 px-6 text-center">
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2211
|
+
{multiSelectCount > 1 ? (
|
|
2212
|
+
<>
|
|
2213
|
+
<Layers size={18} className="mb-3 text-neutral-600" />
|
|
2214
|
+
<p className="text-sm font-medium text-neutral-200">
|
|
2215
|
+
{multiSelectCount} elements selected
|
|
2216
|
+
</p>
|
|
2217
|
+
<p className="mt-2 max-w-[260px] text-xs leading-5 text-neutral-500">
|
|
2218
|
+
Select a single element to edit its properties. Click an element in the preview or use
|
|
2219
|
+
the timeline layer panel.
|
|
2220
|
+
</p>
|
|
2221
|
+
</>
|
|
2222
|
+
) : (
|
|
2223
|
+
<>
|
|
2224
|
+
<Eye size={18} className="mb-3 text-neutral-600" />
|
|
2225
|
+
<p className="text-sm font-medium text-neutral-200">
|
|
2226
|
+
Select an element in the preview.
|
|
2227
|
+
</p>
|
|
2228
|
+
<p className="mt-2 max-w-[260px] text-xs leading-5 text-neutral-500">
|
|
2229
|
+
The inspector is tuned for element edits with safer geometry controls, color picking,
|
|
2230
|
+
and cleaner grouped layer controls.
|
|
2231
|
+
</p>
|
|
2232
|
+
</>
|
|
2233
|
+
)}
|
|
2267
2234
|
</div>
|
|
2268
2235
|
);
|
|
2269
2236
|
}
|
|
@@ -2384,6 +2351,219 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
2384
2351
|
</div>
|
|
2385
2352
|
|
|
2386
2353
|
<div className="flex-1 overflow-y-auto">
|
|
2354
|
+
{hasTextControls && (
|
|
2355
|
+
<Section title="Text" icon={<Type size={15} />}>
|
|
2356
|
+
{(() => {
|
|
2357
|
+
const textFields = element.textFields;
|
|
2358
|
+
const activeField =
|
|
2359
|
+
textFields.find((field) => field.key === activeTextFieldKey) ?? textFields[0];
|
|
2360
|
+
if (!activeField) return null;
|
|
2361
|
+
|
|
2362
|
+
if (textFields.length === 1) {
|
|
2363
|
+
return (
|
|
2364
|
+
<div className="space-y-4 rounded-xl border border-neutral-800 bg-neutral-900/60 p-3">
|
|
2365
|
+
<div className="min-w-0">
|
|
2366
|
+
<div className="truncate text-[11px] font-medium text-neutral-100">
|
|
2367
|
+
{formatTextFieldPreview(activeField.value) || "Text"}
|
|
2368
|
+
</div>
|
|
2369
|
+
<div className="text-[10px] uppercase tracking-[0.12em] text-neutral-500">
|
|
2370
|
+
{activeField.tagName}
|
|
2371
|
+
</div>
|
|
2372
|
+
</div>
|
|
2373
|
+
|
|
2374
|
+
<TextAreaField
|
|
2375
|
+
key={activeField.key}
|
|
2376
|
+
label="Content"
|
|
2377
|
+
value={activeField.value}
|
|
2378
|
+
disabled={false}
|
|
2379
|
+
onCommit={(next) => onSetText(next, activeField.key)}
|
|
2380
|
+
/>
|
|
2381
|
+
|
|
2382
|
+
<ColorField
|
|
2383
|
+
label="Text color"
|
|
2384
|
+
value={getTextFieldColor(activeField, styles)}
|
|
2385
|
+
disabled={false}
|
|
2386
|
+
onCommit={(next) => onSetTextFieldStyle(activeField.key, "color", next)}
|
|
2387
|
+
/>
|
|
2388
|
+
|
|
2389
|
+
<div className={RESPONSIVE_GRID}>
|
|
2390
|
+
<MetricField
|
|
2391
|
+
label="Size"
|
|
2392
|
+
value={
|
|
2393
|
+
activeField.computedStyles["font-size"] || styles["font-size"] || "16px"
|
|
2394
|
+
}
|
|
2395
|
+
disabled={false}
|
|
2396
|
+
liveCommit
|
|
2397
|
+
onCommit={(next) => onSetTextFieldStyle(activeField.key, "font-size", next)}
|
|
2398
|
+
/>
|
|
2399
|
+
<FontWeightField
|
|
2400
|
+
value={
|
|
2401
|
+
activeField.computedStyles["font-weight"] ||
|
|
2402
|
+
styles["font-weight"] ||
|
|
2403
|
+
"400"
|
|
2404
|
+
}
|
|
2405
|
+
disabled={false}
|
|
2406
|
+
onCommit={(next) =>
|
|
2407
|
+
onSetTextFieldStyle(activeField.key, "font-weight", next)
|
|
2408
|
+
}
|
|
2409
|
+
/>
|
|
2410
|
+
</div>
|
|
2411
|
+
|
|
2412
|
+
<FontFamilyField
|
|
2413
|
+
value={
|
|
2414
|
+
activeField.computedStyles["font-family"] ||
|
|
2415
|
+
styles["font-family"] ||
|
|
2416
|
+
"inherit"
|
|
2417
|
+
}
|
|
2418
|
+
disabled={false}
|
|
2419
|
+
importedFonts={fontAssets}
|
|
2420
|
+
onImportFonts={onImportFonts}
|
|
2421
|
+
onCommit={(next) => onSetTextFieldStyle(activeField.key, "font-family", next)}
|
|
2422
|
+
/>
|
|
2423
|
+
|
|
2424
|
+
<AdvancedTextControls
|
|
2425
|
+
field={activeField}
|
|
2426
|
+
inheritedStyles={styles}
|
|
2427
|
+
disabled={false}
|
|
2428
|
+
onCommit={(property, value) =>
|
|
2429
|
+
onSetTextFieldStyle(activeField.key, property, value)
|
|
2430
|
+
}
|
|
2431
|
+
/>
|
|
2432
|
+
</div>
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
return (
|
|
2437
|
+
<div className="space-y-4">
|
|
2438
|
+
<div className="grid gap-1.5">
|
|
2439
|
+
<div className="flex min-w-0 flex-wrap items-center justify-between gap-2">
|
|
2440
|
+
<span className={LABEL}>Text layers</span>
|
|
2441
|
+
<button
|
|
2442
|
+
type="button"
|
|
2443
|
+
onClick={() => {
|
|
2444
|
+
void Promise.resolve(onAddTextField(activeField.key)).then((nextKey) => {
|
|
2445
|
+
if (nextKey) setActiveTextFieldKey(nextKey);
|
|
2446
|
+
});
|
|
2447
|
+
}}
|
|
2448
|
+
className="inline-flex h-7 max-w-full items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white"
|
|
2449
|
+
>
|
|
2450
|
+
<Plus size={12} className="flex-shrink-0" />
|
|
2451
|
+
<span className="truncate">Add text</span>
|
|
2452
|
+
</button>
|
|
2453
|
+
</div>
|
|
2454
|
+
<div className="grid gap-2">
|
|
2455
|
+
{textFields.map((field, index) => {
|
|
2456
|
+
const active = field.key === activeField.key;
|
|
2457
|
+
return (
|
|
2458
|
+
<button
|
|
2459
|
+
key={field.key}
|
|
2460
|
+
type="button"
|
|
2461
|
+
onClick={() => setActiveTextFieldKey(field.key)}
|
|
2462
|
+
className={`min-w-0 w-full rounded-xl border px-3 py-2 text-left transition-colors ${
|
|
2463
|
+
active
|
|
2464
|
+
? "border-studio-accent/50 bg-studio-accent/10"
|
|
2465
|
+
: "border-neutral-800 bg-neutral-900/80 hover:border-neutral-700 hover:bg-neutral-900"
|
|
2466
|
+
}`}
|
|
2467
|
+
>
|
|
2468
|
+
<div className="flex min-w-0 items-center justify-between gap-2">
|
|
2469
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
2470
|
+
<span
|
|
2471
|
+
className="h-4 w-4 flex-shrink-0 rounded border border-neutral-700 bg-neutral-950"
|
|
2472
|
+
style={{ backgroundColor: getTextFieldColor(field, styles) }}
|
|
2473
|
+
/>
|
|
2474
|
+
<span className="min-w-0 truncate text-[11px] font-medium text-neutral-100">
|
|
2475
|
+
{formatTextFieldPreview(field.value) || `Text ${index + 1}`}
|
|
2476
|
+
</span>
|
|
2477
|
+
</div>
|
|
2478
|
+
<span className="flex-shrink-0 rounded-md border border-neutral-700 bg-neutral-950 px-1.5 py-0.5 text-[10px] uppercase tracking-[0.12em] text-neutral-500">
|
|
2479
|
+
{field.tagName}
|
|
2480
|
+
</span>
|
|
2481
|
+
</div>
|
|
2482
|
+
</button>
|
|
2483
|
+
);
|
|
2484
|
+
})}
|
|
2485
|
+
</div>
|
|
2486
|
+
</div>
|
|
2487
|
+
|
|
2488
|
+
<div className="space-y-4 rounded-xl border border-neutral-800 bg-neutral-900/60 p-3">
|
|
2489
|
+
<div className="flex min-w-0 items-center justify-between gap-2">
|
|
2490
|
+
<div className="min-w-0">
|
|
2491
|
+
<div className="truncate text-[11px] font-medium text-neutral-100">
|
|
2492
|
+
{formatTextFieldPreview(activeField.value) || "Text"}
|
|
2493
|
+
</div>
|
|
2494
|
+
<div className="text-[10px] uppercase tracking-[0.12em] text-neutral-500">
|
|
2495
|
+
{activeField.tagName}
|
|
2496
|
+
</div>
|
|
2497
|
+
</div>
|
|
2498
|
+
<button
|
|
2499
|
+
type="button"
|
|
2500
|
+
onClick={() => onRemoveTextField(activeField.key)}
|
|
2501
|
+
className="inline-flex h-7 flex-shrink-0 items-center rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white"
|
|
2502
|
+
>
|
|
2503
|
+
Remove
|
|
2504
|
+
</button>
|
|
2505
|
+
</div>
|
|
2506
|
+
|
|
2507
|
+
<TextAreaField
|
|
2508
|
+
key={activeField.key}
|
|
2509
|
+
label="Content"
|
|
2510
|
+
value={activeField.value}
|
|
2511
|
+
disabled={false}
|
|
2512
|
+
autoFocus
|
|
2513
|
+
onCommit={(next) => onSetText(next, activeField.key)}
|
|
2514
|
+
/>
|
|
2515
|
+
|
|
2516
|
+
<ColorField
|
|
2517
|
+
label="Text color"
|
|
2518
|
+
value={getTextFieldColor(activeField, styles)}
|
|
2519
|
+
disabled={false}
|
|
2520
|
+
onCommit={(next) => onSetTextFieldStyle(activeField.key, "color", next)}
|
|
2521
|
+
/>
|
|
2522
|
+
|
|
2523
|
+
<div className={RESPONSIVE_GRID}>
|
|
2524
|
+
<MetricField
|
|
2525
|
+
label="Size"
|
|
2526
|
+
value={activeField.computedStyles["font-size"] || "16px"}
|
|
2527
|
+
disabled={false}
|
|
2528
|
+
liveCommit
|
|
2529
|
+
onCommit={(next) => onSetTextFieldStyle(activeField.key, "font-size", next)}
|
|
2530
|
+
/>
|
|
2531
|
+
<FontWeightField
|
|
2532
|
+
value={activeField.computedStyles["font-weight"] || "400"}
|
|
2533
|
+
disabled={false}
|
|
2534
|
+
onCommit={(next) =>
|
|
2535
|
+
onSetTextFieldStyle(activeField.key, "font-weight", next)
|
|
2536
|
+
}
|
|
2537
|
+
/>
|
|
2538
|
+
</div>
|
|
2539
|
+
|
|
2540
|
+
<FontFamilyField
|
|
2541
|
+
value={
|
|
2542
|
+
activeField.computedStyles["font-family"] ||
|
|
2543
|
+
styles["font-family"] ||
|
|
2544
|
+
"inherit"
|
|
2545
|
+
}
|
|
2546
|
+
disabled={false}
|
|
2547
|
+
importedFonts={fontAssets}
|
|
2548
|
+
onImportFonts={onImportFonts}
|
|
2549
|
+
onCommit={(next) => onSetTextFieldStyle(activeField.key, "font-family", next)}
|
|
2550
|
+
/>
|
|
2551
|
+
|
|
2552
|
+
<AdvancedTextControls
|
|
2553
|
+
field={activeField}
|
|
2554
|
+
inheritedStyles={styles}
|
|
2555
|
+
disabled={false}
|
|
2556
|
+
onCommit={(property, value) =>
|
|
2557
|
+
onSetTextFieldStyle(activeField.key, property, value)
|
|
2558
|
+
}
|
|
2559
|
+
/>
|
|
2560
|
+
</div>
|
|
2561
|
+
</div>
|
|
2562
|
+
);
|
|
2563
|
+
})()}
|
|
2564
|
+
</Section>
|
|
2565
|
+
)}
|
|
2566
|
+
|
|
2387
2567
|
<Section title="Layout" icon={<Move size={15} />}>
|
|
2388
2568
|
<div className={RESPONSIVE_GRID}>
|
|
2389
2569
|
<MetricField
|
|
@@ -2630,7 +2810,7 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
2630
2810
|
</div>
|
|
2631
2811
|
</Section>
|
|
2632
2812
|
|
|
2633
|
-
<Section title="
|
|
2813
|
+
<Section title="Transparency" icon={<Eye size={15} />}>
|
|
2634
2814
|
<div className="space-y-4">
|
|
2635
2815
|
<SliderControl
|
|
2636
2816
|
value={opacityValue}
|
|
@@ -2711,246 +2891,6 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
2711
2891
|
)}
|
|
2712
2892
|
</div>
|
|
2713
2893
|
</Section>
|
|
2714
|
-
|
|
2715
|
-
{hasTextControls && (
|
|
2716
|
-
<Section title="Text" icon={<Type size={15} />}>
|
|
2717
|
-
{(() => {
|
|
2718
|
-
const textFields = element.textFields;
|
|
2719
|
-
const activeField =
|
|
2720
|
-
textFields.find((field) => field.key === activeTextFieldKey) ?? textFields[0];
|
|
2721
|
-
if (!activeField) return null;
|
|
2722
|
-
|
|
2723
|
-
if (textFields.length === 1) {
|
|
2724
|
-
return (
|
|
2725
|
-
<div className="space-y-4 rounded-xl border border-neutral-800 bg-neutral-900/60 p-3">
|
|
2726
|
-
<div className="min-w-0">
|
|
2727
|
-
<div className="truncate text-[11px] font-medium text-neutral-100">
|
|
2728
|
-
{formatTextFieldPreview(activeField.value) || "Text"}
|
|
2729
|
-
</div>
|
|
2730
|
-
<div className="text-[10px] uppercase tracking-[0.12em] text-neutral-500">
|
|
2731
|
-
{activeField.tagName}
|
|
2732
|
-
</div>
|
|
2733
|
-
</div>
|
|
2734
|
-
|
|
2735
|
-
<TextAreaField
|
|
2736
|
-
key={activeField.key}
|
|
2737
|
-
label="Content"
|
|
2738
|
-
value={activeField.value}
|
|
2739
|
-
disabled={false}
|
|
2740
|
-
onCommit={(next) => onSetText(next, activeField.key)}
|
|
2741
|
-
/>
|
|
2742
|
-
|
|
2743
|
-
<ColorField
|
|
2744
|
-
label="Text color"
|
|
2745
|
-
value={getTextFieldColor(activeField, styles)}
|
|
2746
|
-
disabled={false}
|
|
2747
|
-
onCommit={(next) => onSetTextFieldStyle(activeField.key, "color", next)}
|
|
2748
|
-
/>
|
|
2749
|
-
|
|
2750
|
-
<div className={RESPONSIVE_GRID}>
|
|
2751
|
-
<MetricField
|
|
2752
|
-
label="Size"
|
|
2753
|
-
value={
|
|
2754
|
-
activeField.computedStyles["font-size"] ||
|
|
2755
|
-
styles["font-size"] ||
|
|
2756
|
-
"16px"
|
|
2757
|
-
}
|
|
2758
|
-
disabled={false}
|
|
2759
|
-
liveCommit
|
|
2760
|
-
onCommit={(next) =>
|
|
2761
|
-
onSetTextFieldStyle(activeField.key, "font-size", next)
|
|
2762
|
-
}
|
|
2763
|
-
/>
|
|
2764
|
-
<FontWeightField
|
|
2765
|
-
value={
|
|
2766
|
-
activeField.computedStyles["font-weight"] ||
|
|
2767
|
-
styles["font-weight"] ||
|
|
2768
|
-
"400"
|
|
2769
|
-
}
|
|
2770
|
-
disabled={false}
|
|
2771
|
-
onCommit={(next) =>
|
|
2772
|
-
onSetTextFieldStyle(activeField.key, "font-weight", next)
|
|
2773
|
-
}
|
|
2774
|
-
/>
|
|
2775
|
-
</div>
|
|
2776
|
-
|
|
2777
|
-
<FontFamilyField
|
|
2778
|
-
value={
|
|
2779
|
-
activeField.computedStyles["font-family"] ||
|
|
2780
|
-
styles["font-family"] ||
|
|
2781
|
-
"inherit"
|
|
2782
|
-
}
|
|
2783
|
-
disabled={false}
|
|
2784
|
-
importedFonts={fontAssets}
|
|
2785
|
-
onImportFonts={onImportFonts}
|
|
2786
|
-
onCommit={(next) =>
|
|
2787
|
-
onSetTextFieldStyle(activeField.key, "font-family", next)
|
|
2788
|
-
}
|
|
2789
|
-
/>
|
|
2790
|
-
|
|
2791
|
-
<AdvancedTextControls
|
|
2792
|
-
field={activeField}
|
|
2793
|
-
inheritedStyles={styles}
|
|
2794
|
-
disabled={false}
|
|
2795
|
-
onCommit={(property, value) =>
|
|
2796
|
-
onSetTextFieldStyle(activeField.key, property, value)
|
|
2797
|
-
}
|
|
2798
|
-
/>
|
|
2799
|
-
</div>
|
|
2800
|
-
);
|
|
2801
|
-
}
|
|
2802
|
-
|
|
2803
|
-
return (
|
|
2804
|
-
<div className="space-y-4">
|
|
2805
|
-
<div className="grid gap-1.5">
|
|
2806
|
-
<div className="flex min-w-0 flex-wrap items-center justify-between gap-2">
|
|
2807
|
-
<span className={LABEL}>Text layers</span>
|
|
2808
|
-
<button
|
|
2809
|
-
type="button"
|
|
2810
|
-
onClick={() => {
|
|
2811
|
-
void Promise.resolve(onAddTextField(activeField.key)).then(
|
|
2812
|
-
(nextKey) => {
|
|
2813
|
-
if (nextKey) setActiveTextFieldKey(nextKey);
|
|
2814
|
-
},
|
|
2815
|
-
);
|
|
2816
|
-
}}
|
|
2817
|
-
className="inline-flex h-7 max-w-full items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white"
|
|
2818
|
-
>
|
|
2819
|
-
<Plus size={12} className="flex-shrink-0" />
|
|
2820
|
-
<span className="truncate">Add text</span>
|
|
2821
|
-
</button>
|
|
2822
|
-
</div>
|
|
2823
|
-
<div className="grid gap-2">
|
|
2824
|
-
{textFields.map((field, index) => {
|
|
2825
|
-
const active = field.key === activeField.key;
|
|
2826
|
-
return (
|
|
2827
|
-
<button
|
|
2828
|
-
key={field.key}
|
|
2829
|
-
type="button"
|
|
2830
|
-
onClick={() => setActiveTextFieldKey(field.key)}
|
|
2831
|
-
className={`min-w-0 w-full rounded-xl border px-3 py-2 text-left transition-colors ${
|
|
2832
|
-
active
|
|
2833
|
-
? "border-studio-accent/50 bg-studio-accent/10"
|
|
2834
|
-
: "border-neutral-800 bg-neutral-900/80 hover:border-neutral-700 hover:bg-neutral-900"
|
|
2835
|
-
}`}
|
|
2836
|
-
>
|
|
2837
|
-
<div className="flex min-w-0 items-center justify-between gap-2">
|
|
2838
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
2839
|
-
<span
|
|
2840
|
-
className="h-4 w-4 flex-shrink-0 rounded border border-neutral-700 bg-neutral-950"
|
|
2841
|
-
style={{ backgroundColor: getTextFieldColor(field, styles) }}
|
|
2842
|
-
/>
|
|
2843
|
-
<span className="min-w-0 truncate text-[11px] font-medium text-neutral-100">
|
|
2844
|
-
{formatTextFieldPreview(field.value) || `Text ${index + 1}`}
|
|
2845
|
-
</span>
|
|
2846
|
-
</div>
|
|
2847
|
-
<span className="flex-shrink-0 rounded-md border border-neutral-700 bg-neutral-950 px-1.5 py-0.5 text-[10px] uppercase tracking-[0.12em] text-neutral-500">
|
|
2848
|
-
{field.tagName}
|
|
2849
|
-
</span>
|
|
2850
|
-
</div>
|
|
2851
|
-
</button>
|
|
2852
|
-
);
|
|
2853
|
-
})}
|
|
2854
|
-
</div>
|
|
2855
|
-
</div>
|
|
2856
|
-
|
|
2857
|
-
<div className="space-y-4 rounded-xl border border-neutral-800 bg-neutral-900/60 p-3">
|
|
2858
|
-
<div className="flex min-w-0 items-center justify-between gap-2">
|
|
2859
|
-
<div className="min-w-0">
|
|
2860
|
-
<div className="truncate text-[11px] font-medium text-neutral-100">
|
|
2861
|
-
{formatTextFieldPreview(activeField.value) || "Text"}
|
|
2862
|
-
</div>
|
|
2863
|
-
<div className="text-[10px] uppercase tracking-[0.12em] text-neutral-500">
|
|
2864
|
-
{activeField.tagName}
|
|
2865
|
-
</div>
|
|
2866
|
-
</div>
|
|
2867
|
-
<button
|
|
2868
|
-
type="button"
|
|
2869
|
-
onClick={() => onRemoveTextField(activeField.key)}
|
|
2870
|
-
className="inline-flex h-7 flex-shrink-0 items-center rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white"
|
|
2871
|
-
>
|
|
2872
|
-
Remove
|
|
2873
|
-
</button>
|
|
2874
|
-
</div>
|
|
2875
|
-
|
|
2876
|
-
<TextAreaField
|
|
2877
|
-
key={activeField.key}
|
|
2878
|
-
label="Content"
|
|
2879
|
-
value={activeField.value}
|
|
2880
|
-
disabled={false}
|
|
2881
|
-
autoFocus
|
|
2882
|
-
onCommit={(next) => onSetText(next, activeField.key)}
|
|
2883
|
-
/>
|
|
2884
|
-
|
|
2885
|
-
<ColorField
|
|
2886
|
-
label="Text color"
|
|
2887
|
-
value={getTextFieldColor(activeField, styles)}
|
|
2888
|
-
disabled={false}
|
|
2889
|
-
onCommit={(next) => onSetTextFieldStyle(activeField.key, "color", next)}
|
|
2890
|
-
/>
|
|
2891
|
-
|
|
2892
|
-
<div className={RESPONSIVE_GRID}>
|
|
2893
|
-
<MetricField
|
|
2894
|
-
label="Size"
|
|
2895
|
-
value={activeField.computedStyles["font-size"] || "16px"}
|
|
2896
|
-
disabled={false}
|
|
2897
|
-
liveCommit
|
|
2898
|
-
onCommit={(next) =>
|
|
2899
|
-
onSetTextFieldStyle(activeField.key, "font-size", next)
|
|
2900
|
-
}
|
|
2901
|
-
/>
|
|
2902
|
-
<FontWeightField
|
|
2903
|
-
value={activeField.computedStyles["font-weight"] || "400"}
|
|
2904
|
-
disabled={false}
|
|
2905
|
-
onCommit={(next) =>
|
|
2906
|
-
onSetTextFieldStyle(activeField.key, "font-weight", next)
|
|
2907
|
-
}
|
|
2908
|
-
/>
|
|
2909
|
-
</div>
|
|
2910
|
-
|
|
2911
|
-
<FontFamilyField
|
|
2912
|
-
value={
|
|
2913
|
-
activeField.computedStyles["font-family"] ||
|
|
2914
|
-
styles["font-family"] ||
|
|
2915
|
-
"inherit"
|
|
2916
|
-
}
|
|
2917
|
-
disabled={false}
|
|
2918
|
-
importedFonts={fontAssets}
|
|
2919
|
-
onImportFonts={onImportFonts}
|
|
2920
|
-
onCommit={(next) =>
|
|
2921
|
-
onSetTextFieldStyle(activeField.key, "font-family", next)
|
|
2922
|
-
}
|
|
2923
|
-
/>
|
|
2924
|
-
|
|
2925
|
-
<AdvancedTextControls
|
|
2926
|
-
field={activeField}
|
|
2927
|
-
inheritedStyles={styles}
|
|
2928
|
-
disabled={false}
|
|
2929
|
-
onCommit={(property, value) =>
|
|
2930
|
-
onSetTextFieldStyle(activeField.key, property, value)
|
|
2931
|
-
}
|
|
2932
|
-
/>
|
|
2933
|
-
</div>
|
|
2934
|
-
</div>
|
|
2935
|
-
);
|
|
2936
|
-
})()}
|
|
2937
|
-
</Section>
|
|
2938
|
-
)}
|
|
2939
|
-
|
|
2940
|
-
{selectionColors.length > 0 && (
|
|
2941
|
-
<Section title="Selection colors" icon={<Palette size={15} />}>
|
|
2942
|
-
<div className="space-y-3">
|
|
2943
|
-
{selectionColors.map((entry) => (
|
|
2944
|
-
<SelectionColorRow
|
|
2945
|
-
key={`${entry.swatch}-${entry.token}`}
|
|
2946
|
-
swatch={entry.swatch}
|
|
2947
|
-
token={entry.token}
|
|
2948
|
-
sources={entry.sources}
|
|
2949
|
-
/>
|
|
2950
|
-
))}
|
|
2951
|
-
</div>
|
|
2952
|
-
</Section>
|
|
2953
|
-
)}
|
|
2954
2894
|
</>
|
|
2955
2895
|
)}
|
|
2956
2896
|
</div>
|