diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc index 7b40c8fbb..5719aac67 100644 --- a/.fallowrc.jsonc +++ b/.fallowrc.jsonc @@ -257,6 +257,12 @@ "file": "packages/studio/src/components/editor/manualEditingAvailability.ts", "exports": ["STUDIO_FLAT_INSPECTOR_ENABLED"], }, + // TextAreaField: newly exported for FlatTextSection (flat inspector + // redesign, Task 8), which lands in a later commit on this branch. + { + "file": "packages/studio/src/components/editor/propertyPanelSections.tsx", + "exports": ["TextAreaField"], + }, ], "ignoreDependencies": [ // Runtime/dynamic deps not visible to static analysis: tsup `external`, diff --git a/packages/studio/src/components/editor/propertyPanelColor.test.tsx b/packages/studio/src/components/editor/propertyPanelColor.test.tsx new file mode 100644 index 000000000..f49c22386 --- /dev/null +++ b/packages/studio/src/components/editor/propertyPanelColor.test.tsx @@ -0,0 +1,28 @@ +// @vitest-environment happy-dom + +import React, { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { ColorField } from "./propertyPanelColor"; + +(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +afterEach(() => { + document.body.innerHTML = ""; +}); + +describe("ColorField flat trigger", () => { + it("renders label and value inline with a small swatch, no boxed border", () => { + const host = document.createElement("div"); + document.body.append(host); + const root = createRoot(host); + act(() => { + root.render(); + }); + const trigger = host.querySelector('[data-flat-color-trigger="true"]'); + expect(trigger).not.toBeNull(); + expect(trigger?.className).not.toContain("border-neutral-800"); + expect(host.textContent).toContain("Color"); + act(() => root.unmount()); + }); +}); diff --git a/packages/studio/src/components/editor/propertyPanelColor.tsx b/packages/studio/src/components/editor/propertyPanelColor.tsx index 962f0e917..ea6dbb600 100644 --- a/packages/studio/src/components/editor/propertyPanelColor.tsx +++ b/packages/studio/src/components/editor/propertyPanelColor.tsx @@ -121,11 +121,13 @@ export function ColorField({ label, value, disabled, + flat, onCommit, }: { label: string; value: string; disabled?: boolean; + flat?: boolean; onCommit: (nextValue: string) => void; }) { const buttonRef = useRef(null); @@ -349,6 +351,30 @@ export function ColorField({ } }; + if (flat) { + return ( +
+ {label} + + {picker} +
+ ); + } + return (
{label} diff --git a/packages/studio/src/components/editor/propertyPanelFont.test.tsx b/packages/studio/src/components/editor/propertyPanelFont.test.tsx new file mode 100644 index 000000000..fe4d32a68 --- /dev/null +++ b/packages/studio/src/components/editor/propertyPanelFont.test.tsx @@ -0,0 +1,30 @@ +// @vitest-environment happy-dom + +import React, { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { FontFamilyField } from "./propertyPanelFont"; + +(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +afterEach(() => { + document.body.innerHTML = ""; +}); + +describe("FontFamilyField flat trigger", () => { + it("renders as a label/value row with a trailing dropdown caret, no boxed border", () => { + const host = document.createElement("div"); + document.body.append(host); + const root = createRoot(host); + act(() => { + root.render( + , + ); + }); + const trigger = host.querySelector('[data-flat-font-trigger="true"]'); + expect(trigger).not.toBeNull(); + expect(trigger?.className).not.toContain("border-neutral-800"); + expect(host.textContent).toContain("JetBrains Mono"); + act(() => root.unmount()); + }); +}); diff --git a/packages/studio/src/components/editor/propertyPanelFont.tsx b/packages/studio/src/components/editor/propertyPanelFont.tsx index a0fd50d7a..47f9cce4c 100644 --- a/packages/studio/src/components/editor/propertyPanelFont.tsx +++ b/packages/studio/src/components/editor/propertyPanelFont.tsx @@ -123,12 +123,14 @@ function loadImportedFontStylesheet(asset: ImportedFontAsset): void { export function FontFamilyField({ value, disabled, + flat, importedFonts, onImportFonts, onCommit, }: { value: string; disabled?: boolean; + flat?: boolean; importedFonts: ImportedFontAsset[]; onImportFonts?: (files: FileList | File[]) => Promise; onCommit: (nextValue: string) => void; @@ -366,6 +368,130 @@ export function FontFamilyField({ setOpen(false); }; + const dropdown = open && ( +
+
+ setQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Escape") { + e.preventDefault(); + setOpen(false); + } + if (e.key === "Enter" && filteredOptions[0]) { + e.preventDefault(); + commitFamily(filteredOptions[0]); + } + }} + className="min-w-0 rounded-lg border border-neutral-800 bg-neutral-900 px-2.5 py-2 text-[11px] font-medium text-neutral-100 outline-none placeholder:text-neutral-600 focus:border-neutral-600" + /> + {canQueryLocalFonts && ( + + )} + + { + await handleImportFonts(event.target.files); + event.target.value = ""; + }} + /> +
+ {fontNotice && ( +
+ {fontNotice} +
+ )} +
+ {filteredOptions.length === 0 ? ( +
No fonts found.
+ ) : ( + filteredOptions.map((option) => ( + + )) + )} +
+
+ ); + + if (flat) { + return ( +
+ Font + + {dropdown} +
+ ); + } + return (
Font family @@ -385,98 +511,7 @@ export function FontFamilyField({ Font - - {open && ( -
-
- setQuery(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Escape") { - e.preventDefault(); - setOpen(false); - } - if (e.key === "Enter" && filteredOptions[0]) { - e.preventDefault(); - commitFamily(filteredOptions[0]); - } - }} - className="min-w-0 rounded-lg border border-neutral-800 bg-neutral-900 px-2.5 py-2 text-[11px] font-medium text-neutral-100 outline-none placeholder:text-neutral-600 focus:border-neutral-600" - /> - {canQueryLocalFonts && ( - - )} - - { - await handleImportFonts(event.target.files); - event.target.value = ""; - }} - /> -
- {fontNotice && ( -
- {fontNotice} -
- )} -
- {filteredOptions.length === 0 ? ( -
No fonts found.
- ) : ( - filteredOptions.map((option) => ( - - )) - )} -
-
- )} + {dropdown}
); } diff --git a/packages/studio/src/components/editor/propertyPanelSections.tsx b/packages/studio/src/components/editor/propertyPanelSections.tsx index 2eaaa1792..e81a5aad8 100644 --- a/packages/studio/src/components/editor/propertyPanelSections.tsx +++ b/packages/studio/src/components/editor/propertyPanelSections.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, type ChangeEvent } from "react"; import { Plus, Type } from "../../icons/SystemIcons"; import { isTextEditableSelection, type DomEditSelection } from "./domEditing"; import type { ImportedFontAsset } from "./fontAssets"; @@ -58,17 +58,19 @@ function detectAvailableWeights(fontFamily: string): string[] { return available.length > 0 ? available : ALL_WEIGHTS; } -function TextAreaField({ +export function TextAreaField({ label, value, disabled, autoFocus, + flat, onCommit, }: { label: string; value: string; disabled?: boolean; autoFocus?: boolean; + flat?: boolean; onCommit: (nextValue: string) => void; }) { const [draft, setDraft] = useState(value); @@ -104,6 +106,38 @@ function TextAreaField({ }, 120); }; + const handleFocus = () => { + focusedRef.current = true; + }; + const handleChange = (e: ChangeEvent) => { + setDraft(e.target.value); + scheduleCommit(e.target.value); + }; + const handleBlur = () => { + focusedRef.current = false; + commitDraft(draft); + }; + + if (flat) { + return ( +
+
+ {label} +
+