@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.
@@ -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="grid min-w-0 gap-1.5">
2140
- <span className={LABEL}>{label}</span>
2141
- <div className={FIELD}>
2142
- <select
2143
- value={value}
2144
- disabled={disabled}
2145
- onChange={(e) => onChange(e.target.value)}
2146
- 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"
2147
- >
2148
- {renderedOptions.map((option) => (
2149
- <option key={option} value={option}>
2150
- {option}
2151
- </option>
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
- <Eye size={18} className="mb-3 text-neutral-600" />
2262
- <p className="text-sm font-medium text-neutral-200">Select an element in the preview.</p>
2263
- <p className="mt-2 max-w-[260px] text-xs leading-5 text-neutral-500">
2264
- The inspector is tuned for element edits with safer geometry controls, color picking, and
2265
- cleaner grouped layer controls.
2266
- </p>
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="Blending" icon={<Eye size={15} />}>
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>