Merge branch 'ui-polish'
Comprehensive UI polish: unified design tokens, transparent-idle fields, 24px/11px control density, banded section headers, shadow-elevation for floating surfaces, dropdown scroll affordances, segmented-control and icon-button consistency, accessible names for icon buttons, and a ruler light-theme fix. Updates panel snapshots and adds a theme regression test.
34
src/app.css
|
|
@ -29,26 +29,18 @@
|
|||
}
|
||||
|
||||
@theme {
|
||||
--spacing-control: 26px;
|
||||
--spacing-panel-x: 12px;
|
||||
--spacing-panel-y: 8px;
|
||||
--spacing-panel: 6px;
|
||||
--spacing-panel-rail: 26px;
|
||||
--spacing-panel-icon: 14px;
|
||||
--radius-panel: 4px;
|
||||
|
||||
--color-panel: #2a2a2a;
|
||||
--color-panel-secondary: #242424;
|
||||
--color-panel-field: #333333;
|
||||
--color-panel-field-hover: #3b3b3b;
|
||||
--color-panel-focus: #4c8dff;
|
||||
--color-panel-selected: #0d64d8;
|
||||
--color-panel-selected-muted: #344054;
|
||||
--color-panel-field: transparent;
|
||||
--color-panel-field-hover: #383838;
|
||||
--color-panel-focus: #3b82f6;
|
||||
--color-panel-selected: #2f6fdd;
|
||||
--color-panel-selected-muted: #3d4450;
|
||||
--color-canvas: #1e1e1e;
|
||||
--color-border: #3a3a3a;
|
||||
--color-hover: #353535;
|
||||
--color-accent: #3b82f6;
|
||||
--color-surface: #e0e0e0;
|
||||
--color-surface: #f0f0f0;
|
||||
--color-muted: #888888;
|
||||
--color-input: #1e1e1e;
|
||||
--color-component: #9747ff;
|
||||
|
|
@ -76,16 +68,16 @@
|
|||
html[data-theme='light'] {
|
||||
--color-panel: #ffffff;
|
||||
--color-panel-secondary: #f7f8fa;
|
||||
--color-panel-field: #f0f1f3;
|
||||
--color-panel-field-hover: #e7e9ed;
|
||||
--color-panel-field: transparent;
|
||||
--color-panel-field-hover: #eceef1;
|
||||
--color-panel-focus: #2563eb;
|
||||
--color-panel-selected: #dbeafe;
|
||||
--color-panel-selected: #2563eb;
|
||||
--color-panel-selected-muted: #e5e7eb;
|
||||
--color-canvas: #f3f4f6;
|
||||
--color-border: #d8dce3;
|
||||
--color-hover: #eef1f5;
|
||||
--color-accent: #2563eb;
|
||||
--color-surface: #202124;
|
||||
--color-surface: #1f2328;
|
||||
--color-muted: #6b7280;
|
||||
--color-input: #ffffff;
|
||||
--color-component: #7c3aed;
|
||||
|
|
@ -104,9 +96,9 @@ html[data-theme='light'] {
|
|||
--color-code-punctuation: #64748b;
|
||||
--color-checkerboard: #d1d5db;
|
||||
--color-checkerboard-muted: #e5e7eb;
|
||||
--color-ruler-bg: rgb(229, 231, 235);
|
||||
--color-ruler-tick: rgb(148, 163, 184);
|
||||
--color-ruler-text: rgb(100, 116, 139);
|
||||
--color-ruler-bg: rgb(250, 250, 251);
|
||||
--color-ruler-tick: rgb(163, 171, 182);
|
||||
--color-ruler-text: rgb(90, 99, 112);
|
||||
--color-ruler-label: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ export type { EditorStore }
|
|||
|
||||
const storeRef = shallowRef<EditorStore>()
|
||||
|
||||
export function useActiveEditorStoreRef() {
|
||||
return storeRef
|
||||
}
|
||||
|
||||
export function setActiveEditorStore(store: EditorStore) {
|
||||
storeRef.value = store
|
||||
triggerRef(storeRef)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import type { RulerTheme } from '@open-pencil/core/canvas'
|
|||
import { parseColor } from '@open-pencil/core/color'
|
||||
import { IS_BROWSER } from '@open-pencil/core/constants'
|
||||
|
||||
import { getActiveEditorStoreOrNull } from '@/app/editor/active-store'
|
||||
import { getActiveEditorStoreOrNull, useActiveEditorStoreRef } from '@/app/editor/active-store'
|
||||
|
||||
export type AppTheme = 'dark' | 'light' | 'auto'
|
||||
|
||||
|
|
@ -51,6 +51,11 @@ export function useAppTheme() {
|
|||
immediate: true
|
||||
})
|
||||
|
||||
// Editors may mount after the theme was applied; push the canvas (ruler)
|
||||
// theme whenever the active editor changes so rulers always match.
|
||||
const activeStoreRef = useActiveEditorStoreRef()
|
||||
watch([activeStoreRef, resolvedTheme], () => updateCanvasTheme(), { flush: 'post' })
|
||||
|
||||
const isLight = computed(() => resolvedTheme.value === 'light')
|
||||
|
||||
function setTheme(value: AppTheme): void {
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ function insertSelectedAsset() {
|
|||
|
||||
<template>
|
||||
<section data-test-id="assets-panel" class="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<header class="shrink-0 px-3 py-2 text-[11px] tracking-wider text-muted uppercase">
|
||||
<header class="shrink-0 px-3 py-2 text-[11px] font-semibold text-surface">
|
||||
{{ panels.assets }}
|
||||
</header>
|
||||
<div class="shrink-0 px-2 pb-2">
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const { panels } = useI18n()
|
|||
>
|
||||
<PanelHeader>
|
||||
<template #icon>
|
||||
<icon-lucide-layers-3 class="size-panel-icon" aria-hidden="true" />
|
||||
<icon-lucide-layers-3 class="size-3.5" aria-hidden="true" />
|
||||
</template>
|
||||
<span role="heading" aria-level="2">
|
||||
{{ panels.layersCount({ count: String(multiCount) }) }}
|
||||
|
|
@ -75,7 +75,7 @@ const { panels } = useI18n()
|
|||
<template #icon>
|
||||
<Tip :label="node.type">
|
||||
<span role="img" :aria-label="node.type" class="contents">
|
||||
<component :is="selectedIcon" class="size-panel-icon" />
|
||||
<component :is="selectedIcon" class="size-3.5" />
|
||||
</span>
|
||||
</Tip>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const cls = usePopoverUI({ content: 'isolate z-[51] w-80 p-3' })
|
|||
const trigger = useButtonUI({
|
||||
tone: 'ghost',
|
||||
size: 'iconSm',
|
||||
ui: { base: 'shrink-0 border border-border bg-input' }
|
||||
ui: { base: 'shrink-0' }
|
||||
})
|
||||
const secondaryButton = useButtonUI({
|
||||
tone: 'ghost',
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ const visibilityStyles = computed(() => layerTree({ actionActive: !node.visible
|
|||
<button
|
||||
type="button"
|
||||
data-slot="action"
|
||||
:aria-label="node.locked ? t.unlock : t.lock"
|
||||
:class="styles.action({ class: ui?.action })"
|
||||
@pointerdown.stop
|
||||
@click.stop="emit('toggleLock')"
|
||||
|
|
@ -60,6 +61,7 @@ const visibilityStyles = computed(() => layerTree({ actionActive: !node.visible
|
|||
<button
|
||||
type="button"
|
||||
data-slot="action"
|
||||
:aria-label="node.visible ? t.hide : t.show"
|
||||
:class="styles.action({ class: ui?.action })"
|
||||
@pointerdown.stop
|
||||
@click.stop="emit('toggleVisibility')"
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ const panelTabsUI = { root: 'w-full' }
|
|||
<SplitterPanel :default-size="70" :min-size="20" class="flex flex-col overflow-hidden">
|
||||
<header
|
||||
data-test-id="layers-header"
|
||||
class="shrink-0 px-3 py-2 text-[11px] tracking-wider text-muted uppercase"
|
||||
class="shrink-0 px-3 py-2 text-[11px] font-semibold text-surface"
|
||||
>
|
||||
{{ panels.layers }}
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -24,14 +24,14 @@ const { panels } = useI18n()
|
|||
<TabsTrigger
|
||||
value="design"
|
||||
data-test-id="properties-tab-design"
|
||||
class="rounded px-2.5 py-1 text-xs text-muted hover:text-surface data-[state=active]:font-semibold data-[state=active]:text-surface"
|
||||
class="relative rounded px-2.5 py-1 text-[11px] text-muted hover:text-surface data-[state=active]:font-semibold data-[state=active]:text-surface after:absolute after:inset-x-2 after:-bottom-[9px] after:h-0.5 after:rounded-full after:bg-transparent data-[state=active]:after:bg-accent"
|
||||
>
|
||||
{{ panels.design }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="code"
|
||||
data-test-id="properties-tab-code"
|
||||
class="flex items-center gap-1 rounded px-2.5 py-1 text-xs text-muted hover:text-surface data-[state=active]:font-semibold data-[state=active]:text-surface"
|
||||
class="relative flex items-center gap-1 rounded px-2.5 py-1 text-[11px] text-muted hover:text-surface data-[state=active]:font-semibold data-[state=active]:text-surface after:absolute after:inset-x-2 after:-bottom-[9px] after:h-0.5 after:rounded-full after:bg-transparent data-[state=active]:after:bg-accent"
|
||||
>
|
||||
<icon-lucide-code class="size-3" />
|
||||
{{ panels.code }}
|
||||
|
|
@ -39,7 +39,7 @@ const { panels } = useI18n()
|
|||
<TabsTrigger
|
||||
value="ai"
|
||||
data-test-id="properties-tab-ai"
|
||||
class="flex items-center gap-1 rounded px-2.5 py-1 text-xs text-muted hover:text-surface data-[state=active]:font-semibold data-[state=active]:text-surface"
|
||||
class="relative flex items-center gap-1 rounded px-2.5 py-1 text-[11px] text-muted hover:text-surface data-[state=active]:font-semibold data-[state=active]:text-surface after:absolute after:inset-x-2 after:-bottom-[9px] after:h-0.5 after:rounded-full after:bg-transparent data-[state=active]:after:bg-accent"
|
||||
>
|
||||
<icon-lucide-sparkles class="size-3" />
|
||||
{{ panels.ai }}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ const subMenuCls = useMenuUI({ content: 'min-w-44' })
|
|||
<Tip :label="`${t.toggleUI} (${appMenuShortcutLabel('toggle-ui')})`">
|
||||
<button
|
||||
data-test-id="app-toggle-ui"
|
||||
:aria-label="t.toggleUI"
|
||||
class="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded text-muted transition-colors hover:bg-hover hover:text-surface"
|
||||
@click="store.state.showUI = !store.state.showUI"
|
||||
>
|
||||
|
|
@ -92,7 +93,7 @@ const subMenuCls = useMenuUI({ content: 'min-w-44' })
|
|||
<MenubarMenu v-for="menu in topMenus" :key="menu.label">
|
||||
<MenubarTrigger
|
||||
v-test-id="`menubar-${menu.label.toLowerCase()}`"
|
||||
class="flex cursor-pointer items-center rounded px-2 py-1 text-xs text-muted transition-colors select-none hover:bg-hover hover:text-surface data-[state=open]:bg-hover data-[state=open]:text-surface"
|
||||
class="flex cursor-pointer items-center rounded px-2 py-1 text-[11px] text-surface/80 transition-colors select-none hover:bg-hover hover:text-surface data-[state=open]:bg-hover data-[state=open]:text-surface"
|
||||
>
|
||||
{{ menu.label }}
|
||||
</MenubarTrigger>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ function activeKeyForTool(tool: EditorToolDef) {
|
|||
<div class="absolute bottom-4 left-1/2 z-10 flex -translate-x-1/2 items-center">
|
||||
<div
|
||||
data-test-id="toolbar"
|
||||
class="flex gap-0.5 rounded-xl border border-border bg-panel p-1 shadow-lg"
|
||||
class="flex gap-0.5 rounded-xl bg-panel p-1 shadow-[0_8px_30px_rgb(0_0_0/0.4)]"
|
||||
>
|
||||
<template v-for="tool in tools" :key="tool.key">
|
||||
<Tip
|
||||
|
|
@ -57,6 +57,7 @@ function activeKeyForTool(tool: EditorToolDef) {
|
|||
<ToolButton
|
||||
:data-test-id="toolbarToolTestId(tool.key)"
|
||||
:icon="toolIcons[tool.key]"
|
||||
:label="toolLabels[tool.key]"
|
||||
:active="active || isActive(tool)"
|
||||
:ui="ui"
|
||||
@click="actions.select"
|
||||
|
|
|
|||
|
|
@ -9,12 +9,13 @@ import type { ToolbarUI } from '@/components/Toolbar/types'
|
|||
|
||||
interface ToolButtonProps {
|
||||
icon: Component
|
||||
label?: string
|
||||
active?: boolean
|
||||
mobile?: boolean
|
||||
ui?: ToolbarUI
|
||||
}
|
||||
|
||||
const { icon, active = false, mobile = false, ui } = defineProps<ToolButtonProps>()
|
||||
const { icon, label, active = false, mobile = false, ui } = defineProps<ToolButtonProps>()
|
||||
const toolbar = tv(toolbarTheme)
|
||||
const styles = computed(() => toolbar({ active, mobile }))
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ const emit = defineEmits<{
|
|||
<button
|
||||
:data-active="active || undefined"
|
||||
:data-mobile="mobile || undefined"
|
||||
:aria-label="label"
|
||||
:class="styles.button({ class: ui?.button })"
|
||||
@click="emit('click')"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ function flyoutItemClass(subActive: boolean) {
|
|||
<ToolButton
|
||||
:data-test-id="toolbarToolTestId(activeKeyForTool(), mobile)"
|
||||
:icon="toolIcons[activeKeyForTool()]"
|
||||
:label="toolLabels[activeKeyForTool()]"
|
||||
:active="triggerActive"
|
||||
:mobile="mobile"
|
||||
:ui="ui"
|
||||
|
|
@ -91,6 +92,7 @@ function flyoutItemClass(subActive: boolean) {
|
|||
v-test-id="toolbarFlyoutTestId(tool.key, mobile)"
|
||||
:data-active="triggerActive || undefined"
|
||||
:data-mobile="mobile || undefined"
|
||||
:aria-label="`${toolLabels[tool.key]} options`"
|
||||
:class="styles.flyoutTrigger({ class: ui?.flyoutTrigger })"
|
||||
>
|
||||
<IconChevronDown :class="styles.flyoutTriggerIcon({ class: ui?.flyoutTriggerIcon })" />
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ function blendModeOptions(value: BlendMode | typeof MIXED) {
|
|||
</IconButton>
|
||||
</template>
|
||||
|
||||
<PanelGrid columns="two">
|
||||
<PanelGrid columns="appearance">
|
||||
<PanelFieldGroup :label="panels.blendMode">
|
||||
<AppSelect
|
||||
:model-value="blendModeValue === MIXED ? 'MIXED' : blendModeValue"
|
||||
|
|
@ -106,7 +106,7 @@ function blendModeOptions(value: BlendMode | typeof MIXED) {
|
|||
<PanelGrid
|
||||
v-if="hasCornerRadius && !showIndependentCorners"
|
||||
columns="fill-rail"
|
||||
class="mt-panel"
|
||||
class="mt-1.5"
|
||||
>
|
||||
<PanelFieldGroup :label="panels.radius">
|
||||
<VariableNumberField
|
||||
|
|
@ -152,7 +152,7 @@ function blendModeOptions(value: BlendMode | typeof MIXED) {
|
|||
<PanelGrid
|
||||
v-else-if="hasCornerRadius && !isMulti && node"
|
||||
columns="two-rail"
|
||||
class="mt-panel"
|
||||
class="mt-1.5"
|
||||
data-corner-grid
|
||||
>
|
||||
<VariableNumberField
|
||||
|
|
@ -204,7 +204,7 @@ function blendModeOptions(value: BlendMode | typeof MIXED) {
|
|||
<PanelRail />
|
||||
</PanelGrid>
|
||||
|
||||
<PanelGrid v-if="hasCornerRadius" columns="fill" class="mt-panel">
|
||||
<PanelGrid v-if="hasCornerRadius" columns="fill" class="mt-1.5">
|
||||
<PanelFieldGroup :label="panels.cornerSmoothing">
|
||||
<NumberField
|
||||
suffix="%"
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ function effectPreview(effect: Effect): Fill {
|
|||
@remove="effectsCtx.adjustExpandedAfterRemove(index)"
|
||||
>
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<div class="flex min-w-0 items-center gap-panel">
|
||||
<div class="flex min-w-0 items-center gap-1.5">
|
||||
<Tip
|
||||
:label="
|
||||
effectsCtx.expandedIndex.value === index
|
||||
|
|
@ -111,7 +111,7 @@ function effectPreview(effect: Effect): Fill {
|
|||
|
||||
<div
|
||||
v-if="effectsCtx.expandedIndex.value === index"
|
||||
class="flex flex-col gap-panel py-panel"
|
||||
class="flex flex-col gap-1.5 py-1.5"
|
||||
data-slot="effect-settings"
|
||||
>
|
||||
<PanelFieldGroup :label="panels.blendMode">
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ function updateSolidColor(
|
|||
:visibility-label="panels.toggleVisibility"
|
||||
:remove-label="panels.removeFill"
|
||||
>
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-panel">
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||
<BindableValueRoot
|
||||
v-slot="binding"
|
||||
:provider="colorProvider"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const layoutModes = computed<Array<{ mode: LayoutMode; label: string }>>(() => [
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-panel">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<IconButton
|
||||
v-if="ctx.node.layoutMode === 'NONE'"
|
||||
:label="panels.addAutoLayout"
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ const visibleSizeLimits = computed(() =>
|
|||
<SizeAxisField axis="height" icon="H" :label="panels.height" />
|
||||
</PanelGrid>
|
||||
|
||||
<PanelGrid v-if="visibleSizeLimits.length" columns="two" class="mt-panel">
|
||||
<PanelGrid v-if="visibleSizeLimits.length" columns="two" class="mt-1.5">
|
||||
<SizeLimitField v-for="item in visibleSizeLimits" :key="item.prop" :item="item" />
|
||||
</PanelGrid>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ function handleAlign(
|
|||
v-slot="{ active, isMulti, xValue, yValue, wValue, hValue, rotationValue, actions }"
|
||||
>
|
||||
<PanelSection v-if="active" :label="panels.position">
|
||||
<div role="toolbar" :aria-label="panels.position" class="mb-panel flex justify-between">
|
||||
<div role="toolbar" :aria-label="panels.position" class="mb-1.5 flex justify-between">
|
||||
<div class="flex gap-0.5">
|
||||
<IconButton
|
||||
:label="panels.alignLeft"
|
||||
|
|
@ -102,7 +102,7 @@ function handleAlign(
|
|||
</Tip>
|
||||
</PanelGrid>
|
||||
|
||||
<PanelGrid v-if="isMulti" columns="two" class="mt-panel">
|
||||
<PanelGrid v-if="isMulti" columns="two" class="mt-1.5">
|
||||
<Tip :label="panels.width">
|
||||
<NumberField
|
||||
icon="W"
|
||||
|
|
@ -127,7 +127,7 @@ function handleAlign(
|
|||
</Tip>
|
||||
</PanelGrid>
|
||||
|
||||
<div class="mt-panel grid grid-cols-[minmax(0,1fr)_repeat(3,var(--spacing-control))] gap-0.5">
|
||||
<div class="mt-1.5 grid grid-cols-[minmax(0,1fr)_repeat(3,24px)] gap-0.5">
|
||||
<Tip :label="panels.rotation">
|
||||
<NumberField
|
||||
suffix="°"
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ function onToggleSides(activeNode: SceneNode | null) {
|
|||
</template>
|
||||
</div>
|
||||
|
||||
<PanelGrid v-if="advancedActive" columns="three" class="mt-panel">
|
||||
<PanelGrid v-if="advancedActive" columns="three" class="mt-1.5">
|
||||
<PanelFieldGroup :label="panels.strokeCap">
|
||||
<SegmentedControl
|
||||
:model-value="cap === MIXED ? 'MIXED' : cap"
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
<PanelSection v-if="ctx.node.value" :label="panels.typography">
|
||||
<SharedStyleField kind="text" :label="panels.textStyle" />
|
||||
|
||||
<div class="mb-panel flex min-w-0 items-center gap-panel">
|
||||
<div class="mb-1.5 flex min-w-0 items-center gap-1.5">
|
||||
<FontPicker
|
||||
class="min-w-0 flex-1"
|
||||
:model-value="ctx.node.value.fontFamily"
|
||||
|
|
@ -88,7 +88,7 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
</Tip>
|
||||
</div>
|
||||
|
||||
<PanelGrid columns="two" class="mb-panel">
|
||||
<PanelGrid columns="two" class="mb-1.5">
|
||||
<PanelFieldGroup :label="panels.fontWeight">
|
||||
<AppSelect
|
||||
:label="panels.fontWeight"
|
||||
|
|
@ -111,7 +111,7 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
</PanelFieldGroup>
|
||||
</PanelGrid>
|
||||
|
||||
<PanelGrid columns="two" class="mb-panel">
|
||||
<PanelGrid columns="two" class="mb-1.5">
|
||||
<PanelFieldGroup :label="panels.lineHeight">
|
||||
<VariableNumberField
|
||||
:model-value="
|
||||
|
|
@ -146,7 +146,7 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
</PanelFieldGroup>
|
||||
</PanelGrid>
|
||||
|
||||
<PanelFieldGroup :label="panels.direction" class="mb-panel">
|
||||
<PanelFieldGroup :label="panels.direction" class="mb-1.5">
|
||||
<AppSelect
|
||||
:label="panels.direction"
|
||||
:model-value="ctx.node.value.textDirection"
|
||||
|
|
@ -159,7 +159,7 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
/>
|
||||
</PanelFieldGroup>
|
||||
|
||||
<PanelFieldGroup :label="panels.textAlignment" class="mb-panel">
|
||||
<PanelFieldGroup :label="panels.textAlignment" class="mb-1.5">
|
||||
<SegmentedControl
|
||||
:model-value="ctx.node.value.textAlignHorizontal"
|
||||
:options="alignmentOptions"
|
||||
|
|
@ -175,7 +175,7 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
</SegmentedControl>
|
||||
</PanelFieldGroup>
|
||||
|
||||
<PanelFieldGroup :label="panels.verticalTextAlignment" class="mb-panel">
|
||||
<PanelFieldGroup :label="panels.verticalTextAlignment" class="mb-1.5">
|
||||
<SegmentedControl
|
||||
:model-value="ctx.node.value.textAlignVertical"
|
||||
:options="verticalAlignmentOptions"
|
||||
|
|
@ -196,7 +196,7 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
</SegmentedControl>
|
||||
</PanelFieldGroup>
|
||||
|
||||
<PanelGrid columns="two" class="mb-panel">
|
||||
<PanelGrid columns="two" class="mb-1.5">
|
||||
<PanelFieldGroup :label="panels.textCase">
|
||||
<AppSelect
|
||||
:label="panels.textCase"
|
||||
|
|
@ -220,7 +220,7 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
<PanelFieldGroup
|
||||
v-if="ctx.node.value.textTruncation === 'ENDING'"
|
||||
:label="panels.maxLines"
|
||||
class="mb-panel"
|
||||
class="mb-1.5"
|
||||
>
|
||||
<NumberField
|
||||
:model-value="ctx.node.value.maxLines ?? 1"
|
||||
|
|
@ -235,12 +235,12 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
/>
|
||||
</PanelFieldGroup>
|
||||
|
||||
<PanelFieldGroup :label="panels.openTypeFeatures" class="mb-panel">
|
||||
<PanelFieldGroup :label="panels.openTypeFeatures" class="mb-1.5">
|
||||
<div class="grid gap-1.5">
|
||||
<label
|
||||
v-for="feature in commonFeatures"
|
||||
:key="feature.tag"
|
||||
class="flex items-center justify-between gap-panel text-[11px] text-muted"
|
||||
class="flex items-center justify-between gap-1.5 text-[11px] text-muted"
|
||||
>
|
||||
<span>{{ feature.label }}</span>
|
||||
<AppSwitch
|
||||
|
|
@ -253,8 +253,8 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
</div>
|
||||
</PanelFieldGroup>
|
||||
|
||||
<PanelFieldGroup :label="panels.textFormatting" :ui="{ container: 'flex-row gap-panel' }">
|
||||
<div class="flex items-center gap-panel" role="toolbar" :aria-label="panels.textFormatting">
|
||||
<PanelFieldGroup :label="panels.textFormatting" :ui="{ container: 'flex-row gap-1.5' }">
|
||||
<div class="flex items-center gap-1.5" role="toolbar" :aria-label="panels.textFormatting">
|
||||
<IconButton
|
||||
:label="`${menu.bold} (${appMenuShortcutLabel('text.bold')})`"
|
||||
size="md"
|
||||
|
|
|
|||
|
|
@ -113,12 +113,12 @@ const mixedTargets: BindingTarget[] = [
|
|||
|
||||
<template>
|
||||
<div class="w-[320px] overflow-hidden rounded-lg border border-border bg-panel shadow-xl">
|
||||
<header class="border-b border-border px-panel-x py-panel-y">
|
||||
<header class="border-b border-border px-3 py-2">
|
||||
<p class="text-xs font-semibold">Binding field states</p>
|
||||
<p class="mt-1 text-[11px] text-muted">Pill at rest, resolved value while editing</p>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-2 gap-panel p-panel-x">
|
||||
<div class="grid grid-cols-2 gap-1.5 px-3">
|
||||
<label class="space-y-1">
|
||||
<span class="text-[11px] text-muted">Unbound</span>
|
||||
<BindingFieldDemoItem
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ function tooltip(variableName: string, resolvedValue: unknown) {
|
|||
<div
|
||||
v-bind="{ ...attrs, ...binding.stateAttrs }"
|
||||
data-story-control
|
||||
class="group/binding flex h-control min-w-0 items-center rounded-panel border border-transparent bg-panel-field text-xs text-surface outline-none hover:bg-panel-field-hover focus-within:border-panel-focus data-[derived]:text-muted"
|
||||
class="group/binding flex h-6 min-w-0 items-center rounded border border-transparent bg-panel-field text-xs text-surface outline-none hover:bg-panel-field-hover focus-within:border-panel-focus data-[derived]:text-muted"
|
||||
:data-derived="derived ? '' : undefined"
|
||||
@pointerdown="
|
||||
!editing &&
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const sectionLabel = computed(() =>
|
|||
|
||||
<template>
|
||||
<PanelSection v-if="active" :label="sectionLabel" :ui="componentSectionUI">
|
||||
<div class="flex flex-col gap-panel">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<PanelFieldGroup v-for="control in controls" :key="control.id" :label="control.name">
|
||||
<ComponentPropertyTextField
|
||||
v-if="control.type === 'TEXT'"
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ defineSlots<{
|
|||
:prop-key="propKey"
|
||||
:index="index"
|
||||
:aria-label="visibilityLabel"
|
||||
class="flex size-control shrink-0 cursor-pointer items-center justify-center rounded-panel border-none bg-transparent p-0 text-muted hover:bg-hover hover:text-surface"
|
||||
class="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded border-none bg-transparent p-0 text-muted hover:bg-hover hover:text-surface"
|
||||
@toggle="emit('toggleVisibility', $event)"
|
||||
>
|
||||
<icon-lucide-eye v-if="!item.hidden" class="size-3.5" />
|
||||
|
|
@ -68,7 +68,7 @@ defineSlots<{
|
|||
:aria-label="removeLabel"
|
||||
:class="[
|
||||
removeClass,
|
||||
'flex size-control shrink-0 cursor-pointer items-center justify-center rounded-panel border-none bg-transparent p-0 text-muted hover:bg-hover hover:text-surface'
|
||||
'flex size-6 shrink-0 cursor-pointer items-center justify-center rounded border-none bg-transparent p-0 text-muted hover:bg-hover hover:text-surface'
|
||||
]"
|
||||
@remove="emit('remove', $event)"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ function update(value: string) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<PanelGrid v-if="visible" columns="fill" class="mb-panel">
|
||||
<PanelGrid v-if="visible" columns="fill" class="mb-1.5">
|
||||
<PanelFieldGroup :label="label">
|
||||
<AppSelect
|
||||
:model-value="styleId === MIXED ? 'MIXED' : (styleId ?? 'NONE')"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import {
|
|||
SelectItemText,
|
||||
SelectPortal,
|
||||
SelectRoot,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectViewport
|
||||
|
|
@ -46,6 +48,9 @@ const styles = tv(theme)()
|
|||
:side-offset="2"
|
||||
:class="styles.content({ class: ui?.content })"
|
||||
>
|
||||
<SelectScrollUpButton class="flex items-center justify-center py-0.5 text-muted">
|
||||
<icon-lucide-chevron-up class="size-3.5" />
|
||||
</SelectScrollUpButton>
|
||||
<SelectViewport :class="styles.viewport({ class: ui?.viewport })">
|
||||
<SelectItem
|
||||
v-for="opt in options"
|
||||
|
|
@ -59,6 +64,9 @@ const styles = tv(theme)()
|
|||
<SelectItemText>{{ opt.label }}</SelectItemText>
|
||||
</SelectItem>
|
||||
</SelectViewport>
|
||||
<SelectScrollDownButton class="flex items-center justify-center py-0.5 text-muted">
|
||||
<icon-lucide-chevron-down class="size-3.5" />
|
||||
</SelectScrollDownButton>
|
||||
</SelectContent>
|
||||
</SelectPortal>
|
||||
</SelectRoot>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export const dialog = tv({
|
|||
slots: {
|
||||
overlay: 'fixed inset-0 z-40 bg-black/50',
|
||||
content:
|
||||
'fixed top-1/2 left-1/2 z-50 -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-panel shadow-2xl outline-none',
|
||||
'fixed top-1/2 left-1/2 z-50 -translate-x-1/2 -translate-y-1/2 rounded-xl bg-panel shadow-[0_8px_30px_rgb(0_0_0/0.4)] outline-none',
|
||||
title: 'text-sm font-semibold text-surface',
|
||||
description: 'text-xs text-muted'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ import { tv } from 'tailwind-variants'
|
|||
|
||||
export const menu = tv({
|
||||
slots: {
|
||||
content: 'z-50 rounded-lg border border-border bg-panel p-1 shadow-lg',
|
||||
item: 'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs outline-none select-none data-[disabled]:cursor-default data-[disabled]:text-muted/50 data-[highlighted]:bg-hover',
|
||||
content: 'z-50 rounded-xl bg-panel p-1 shadow-[0_8px_30px_rgb(0_0_0/0.4)]',
|
||||
item: 'flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-[11px] outline-none select-none data-[disabled]:cursor-default data-[disabled]:text-muted/50 data-[highlighted]:bg-hover',
|
||||
separator: 'mx-1 my-1 h-px bg-border',
|
||||
shortcut: 'text-[11px] text-muted',
|
||||
icon: 'size-3 text-muted',
|
||||
subTrigger:
|
||||
'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs outline-none select-none'
|
||||
'flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-[11px] outline-none select-none'
|
||||
},
|
||||
variants: {
|
||||
tone: {
|
||||
|
|
|
|||
|
|
@ -87,16 +87,16 @@ export const StateMatrix: Story = {
|
|||
<TooltipProvider>
|
||||
<div class="w-[320px] overflow-hidden rounded-lg border border-border bg-panel shadow-xl">
|
||||
<PanelHeader>
|
||||
<template #icon><SquareIcon class="size-panel-icon" /></template>
|
||||
<template #icon><SquareIcon class="size-3.5" /></template>
|
||||
<span role="heading" aria-level="2">Rectangle</span>
|
||||
<template #actions>
|
||||
<IconButton label="Selection actions"><MoreIcon class="size-panel-icon" /></IconButton>
|
||||
<IconButton label="Selection actions"><MoreIcon class="size-3.5" /></IconButton>
|
||||
</template>
|
||||
</PanelHeader>
|
||||
|
||||
<PanelSection label="Layout">
|
||||
<template #actions>
|
||||
<IconButton label="Reset layout"><RotateIcon class="size-panel-icon" /></IconButton>
|
||||
<IconButton label="Reset layout"><RotateIcon class="size-3.5" /></IconButton>
|
||||
</template>
|
||||
<PanelGrid columns="two-rail">
|
||||
<PanelFieldGroup label="Width">
|
||||
|
|
@ -106,7 +106,7 @@ export const StateMatrix: Story = {
|
|||
<AppInput v-model="height" tone="panel" data-story-control data-state="focus" aria-label="Height" />
|
||||
</PanelFieldGroup>
|
||||
<PanelRail>
|
||||
<IconButton label="Constrain proportions" size="md"><LinkIcon class="size-panel-icon" /></IconButton>
|
||||
<IconButton label="Constrain proportions" size="md"><LinkIcon class="size-3.5" /></IconButton>
|
||||
</PanelRail>
|
||||
</PanelGrid>
|
||||
</PanelSection>
|
||||
|
|
@ -120,13 +120,13 @@ export const StateMatrix: Story = {
|
|||
<AppInput v-model="mixed" tone="panel" state="mixed" readonly data-story-control aria-label="Mixed opacity" />
|
||||
</PanelFieldGroup>
|
||||
<PanelRail>
|
||||
<IconButton label="Toggle visibility"><EyeIcon class="size-panel-icon" /></IconButton>
|
||||
<IconButton label="Toggle visibility"><EyeIcon class="size-3.5" /></IconButton>
|
||||
</PanelRail>
|
||||
</PanelGrid>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection label="States">
|
||||
<div class="grid grid-cols-2 gap-panel">
|
||||
<div class="grid grid-cols-2 gap-1.5">
|
||||
<PanelFieldGroup label="Bound">
|
||||
<AppInput v-model="bound" tone="panel" state="bound" readonly data-story-control aria-label="Bound value" />
|
||||
</PanelFieldGroup>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { tv } from 'tailwind-variants'
|
|||
|
||||
export const popover = tv({
|
||||
slots: {
|
||||
content: 'z-[100] rounded-lg border border-border bg-panel shadow-xl',
|
||||
content: 'z-[100] rounded-xl bg-panel shadow-[0_8px_30px_rgb(0_0_0/0.4)]',
|
||||
header: '',
|
||||
body: '',
|
||||
footer: ''
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { tv } from 'tailwind-variants'
|
|||
export const tooltip = tv({
|
||||
slots: {
|
||||
content:
|
||||
'z-50 rounded-md border border-border bg-panel px-2 py-1 text-xs text-surface shadow-lg'
|
||||
'z-50 rounded-md bg-panel px-2 py-1 text-[11px] text-surface shadow-[0_8px_30px_rgb(0_0_0/0.4)]'
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import selectTheme from './select'
|
|||
const appSelectTheme = {
|
||||
slots: {
|
||||
...selectTheme.slots,
|
||||
trigger: [selectTheme.slots.trigger, 'w-full min-w-0 px-1.5 text-xs'],
|
||||
trigger: [selectTheme.slots.trigger, 'w-full min-w-0 px-1.5'],
|
||||
value: [selectTheme.slots.value, 'min-w-0 flex-1 truncate text-left'],
|
||||
content: [selectTheme.slots.content, 'max-h-56'],
|
||||
viewport: [selectTheme.slots.viewport, 'p-0.5'],
|
||||
item: [selectTheme.slots.item, 'rounded py-1.5 pr-2 pl-6 text-xs'],
|
||||
item: [selectTheme.slots.item, 'py-0 pr-2 pl-6 text-[11px]'],
|
||||
indicator: 'absolute left-1.5 inline-flex items-center justify-center'
|
||||
},
|
||||
variants: selectTheme.variants,
|
||||
|
|
|
|||
|
|
@ -8,22 +8,22 @@ const bindingFieldTheme = {
|
|||
pickerContent:
|
||||
'z-[100] w-56 overflow-hidden rounded-lg border border-border bg-panel text-surface shadow-xl',
|
||||
pickerSearch:
|
||||
'h-control w-full border-0 border-b border-border bg-transparent px-2 text-[11px] text-surface outline-none placeholder:text-muted focus:border-panel-focus',
|
||||
'h-6 w-full border-0 border-b border-border bg-transparent px-2 text-[11px] text-surface outline-none placeholder:text-muted focus:border-panel-focus',
|
||||
pickerViewport: 'max-h-48 overflow-y-auto p-1',
|
||||
pickerItem:
|
||||
'flex h-control cursor-pointer items-center gap-2 rounded-panel px-2 text-[11px] text-surface outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-hover',
|
||||
'flex h-6 cursor-pointer items-center gap-2 rounded px-2 text-[11px] text-surface outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-hover',
|
||||
pickerItemIcon: 'size-3 shrink-0 text-component',
|
||||
pickerItemLabel: 'min-w-0 flex-1 truncate',
|
||||
pickerItemIndicator: 'flex size-3 shrink-0 items-center justify-center text-component',
|
||||
pickerEmpty: 'px-2 py-3 text-center text-[11px] text-muted',
|
||||
pickerFooter: 'border-t border-border p-1',
|
||||
pickerAction:
|
||||
'flex h-control w-full cursor-pointer items-center gap-2 rounded-panel border-0 bg-transparent px-2 text-left text-[11px] text-muted outline-none hover:bg-hover hover:text-surface focus-visible:border-panel-focus',
|
||||
'flex h-6 w-full cursor-pointer items-center gap-2 rounded border-0 bg-transparent px-2 text-left text-[11px] text-muted outline-none hover:bg-hover hover:text-surface focus-visible:border-panel-focus',
|
||||
createForm: 'flex items-center gap-1.5 p-1',
|
||||
createInput:
|
||||
'h-control min-w-0 flex-1 rounded-panel border border-transparent bg-panel-field px-2 text-[11px] text-surface outline-none placeholder:text-muted focus:border-panel-focus',
|
||||
'h-6 min-w-0 flex-1 rounded border border-transparent bg-panel-field px-2 text-[11px] text-surface outline-none placeholder:text-muted focus:border-panel-focus',
|
||||
createSubmit:
|
||||
'h-control shrink-0 rounded-panel border border-transparent bg-panel-field px-2 text-[11px] text-surface outline-none hover:bg-panel-field-hover focus-visible:border-panel-focus disabled:cursor-not-allowed disabled:opacity-50'
|
||||
'h-6 shrink-0 rounded border border-transparent bg-panel-field px-2 text-[11px] text-surface outline-none hover:bg-panel-field-hover focus-visible:border-panel-focus disabled:cursor-not-allowed disabled:opacity-50'
|
||||
},
|
||||
variants: {
|
||||
state: {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ const collaborationTheme = {
|
|||
'flex shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white',
|
||||
peerAvatar: 'cursor-pointer transition-all',
|
||||
shareButton:
|
||||
'flex h-7 cursor-pointer items-center gap-1.5 rounded-md border-none px-3 text-xs font-medium transition-colors outline-none focus-visible:ring-1 focus-visible:ring-accent',
|
||||
'flex h-7 cursor-pointer items-center gap-1.5 rounded border-none px-3 text-[11px] font-medium transition-colors outline-none focus-visible:ring-1 focus-visible:ring-accent',
|
||||
presenceTrigger:
|
||||
'flex h-8 cursor-pointer items-center gap-1.5 rounded-full border border-white/10 bg-panel/70 px-3 shadow-md backdrop-blur-xl outline-none select-none active:bg-hover focus-visible:ring-1 focus-visible:ring-accent',
|
||||
presenceDot: 'size-2 rounded-full bg-green-500',
|
||||
presenceContent: 'z-50 w-56 rounded-xl border border-border bg-panel p-3 shadow-xl',
|
||||
presenceContent: 'z-50 w-56 rounded-xl bg-panel p-3 shadow-[0_8px_30px_rgb(0_0_0/0.4)]',
|
||||
peerRow:
|
||||
'flex cursor-pointer items-center gap-2 rounded-md px-0.5 py-0.5 outline-none select-none active:bg-hover focus-visible:ring-1 focus-visible:ring-accent',
|
||||
disconnect:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
const constraintsTheme = {
|
||||
slots: {
|
||||
root: 'grid grid-cols-[72px_minmax(0,1fr)] items-center gap-panel',
|
||||
diagram: 'relative h-16 w-[72px] rounded-panel border border-border bg-panel-field',
|
||||
root: 'grid grid-cols-[72px_minmax(0,1fr)] items-center gap-1.5',
|
||||
diagram: 'relative h-16 w-[72px] rounded border border-border bg-panel-field',
|
||||
pin: 'absolute flex cursor-pointer items-center justify-center rounded-sm border border-transparent bg-transparent text-muted outline-none hover:bg-hover hover:text-surface focus-visible:ring-1 focus-visible:ring-panel-focus',
|
||||
pinMark: 'block rounded-full bg-current',
|
||||
scaleBadge:
|
||||
'pointer-events-none absolute inset-2 flex items-center justify-center text-[9px] font-medium tracking-wide text-accent uppercase',
|
||||
selects: 'grid min-w-0 gap-panel'
|
||||
selects: 'grid min-w-0 gap-1.5'
|
||||
},
|
||||
variants: {
|
||||
active: {
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ export default {
|
|||
base: 'w-full tabular-nums',
|
||||
variants: {
|
||||
tone: {
|
||||
default:
|
||||
'rounded border border-border bg-input text-surface outline-none focus:border-accent',
|
||||
default: panelFieldBase,
|
||||
panel: panelFieldBase
|
||||
},
|
||||
size: {
|
||||
sm: 'px-2 py-1 text-[11px]',
|
||||
md: 'px-2 py-1 text-xs'
|
||||
sm: 'px-2 text-[11px]',
|
||||
md: 'px-2 text-[11px]'
|
||||
},
|
||||
state: panelFieldState
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const layerTreeTheme = {
|
||||
slots: {
|
||||
viewport: 'scrollbar-thin h-full overflow-y-auto px-1',
|
||||
row: 'group/row relative flex w-full cursor-pointer items-center gap-1 rounded border-none bg-transparent py-1 pr-1 text-left text-xs text-surface hover:bg-hover',
|
||||
row: 'group/row relative flex w-full cursor-pointer items-center gap-1 rounded border-none bg-transparent py-1.5 pr-1 text-left text-[11px] text-surface hover:bg-hover',
|
||||
disclosure:
|
||||
'flex w-4 shrink-0 cursor-pointer items-center justify-center text-muted transition-transform hover:text-surface',
|
||||
disclosurePlaceholder: 'w-4 shrink-0',
|
||||
|
|
@ -15,7 +15,7 @@ const layerTreeTheme = {
|
|||
renameRow: 'flex w-full items-center gap-1 py-1',
|
||||
renameIcon: 'size-3 shrink-0 opacity-70',
|
||||
renameInput:
|
||||
'min-w-0 flex-1 rounded border border-accent bg-input px-1 py-0 text-xs text-surface outline-none'
|
||||
'min-w-0 flex-1 rounded border border-accent bg-input px-1 py-0 text-[11px] text-surface outline-none'
|
||||
},
|
||||
variants: {
|
||||
selected: {
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ const numberFieldTheme = {
|
|||
slots: {
|
||||
root: [
|
||||
panelFieldBase,
|
||||
'group flex min-h-control flex-1 cursor-ew-resize items-center tabular-nums data-[disabled]:cursor-auto data-[editing]:cursor-auto'
|
||||
'group flex min-h-6 flex-1 cursor-ew-resize items-center text-[11px] tabular-nums data-[disabled]:cursor-auto data-[editing]:cursor-auto'
|
||||
],
|
||||
leading:
|
||||
'flex shrink-0 items-center justify-center self-stretch px-[5px] text-muted select-none [&>*]:pointer-events-none',
|
||||
field:
|
||||
'min-w-0 flex-1 cursor-text border-none bg-transparent pr-1.5 font-[inherit] text-xs text-surface outline-none',
|
||||
display: 'flex flex-1 items-center truncate overflow-hidden text-xs select-none',
|
||||
'min-w-0 flex-1 cursor-text border-none bg-transparent pr-1.5 font-[inherit] text-[11px] text-surface outline-none',
|
||||
display: 'flex flex-1 items-center truncate overflow-hidden text-[11px] select-none',
|
||||
mixed: 'flex-1 text-muted',
|
||||
value: 'flex-1 text-surface',
|
||||
suffix: 'shrink-0 pr-1.5 text-muted'
|
||||
|
|
|
|||
|
|
@ -2,19 +2,19 @@ const pageListTheme = {
|
|||
slots: {
|
||||
panel: 'flex min-h-0 flex-1 flex-col',
|
||||
header: 'flex shrink-0 items-center justify-between px-3 py-1.5',
|
||||
title: 'text-[11px] tracking-wider text-muted uppercase',
|
||||
title: 'text-[11px] font-semibold text-surface',
|
||||
add: 'cursor-pointer rounded border-none bg-transparent px-1 text-base leading-none text-muted hover:bg-hover hover:text-surface focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-panel-focus',
|
||||
body: 'min-h-0 flex-1 overflow-hidden',
|
||||
viewport: 'scrollbar-thin h-full overflow-x-hidden overflow-y-auto px-1 pb-1',
|
||||
row: 'relative cursor-grab active:cursor-grabbing',
|
||||
dropIndicator: 'pointer-events-none absolute inset-x-1 z-10 h-0.5 rounded-full bg-accent',
|
||||
renameRow: 'flex w-full items-center gap-1.5 rounded px-2 py-1',
|
||||
renameRow: 'flex w-full items-center gap-1.5 rounded px-2 py-1.5',
|
||||
icon: 'size-3 shrink-0 opacity-70',
|
||||
renameInput:
|
||||
'min-w-0 flex-1 rounded border border-accent bg-input px-1 py-0 text-xs text-surface outline-none',
|
||||
'min-w-0 flex-1 rounded border border-accent bg-input px-1 py-0 text-[11px] text-surface outline-none',
|
||||
divider: 'my-1 flex cursor-pointer items-center px-2',
|
||||
dividerLine: 'h-px flex-1 bg-border',
|
||||
item: 'flex w-full cursor-pointer items-center gap-1.5 rounded border-none px-2 py-1 text-left text-xs outline-none focus-visible:ring-1 focus-visible:ring-panel-focus',
|
||||
item: 'flex w-full cursor-pointer items-center gap-1.5 rounded border-none px-2 py-1.5 text-left text-[11px] outline-none focus-visible:ring-1 focus-visible:ring-panel-focus',
|
||||
label: 'truncate'
|
||||
},
|
||||
variants: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
slots: {
|
||||
root: 'flex h-control min-w-0 flex-1 items-center overflow-hidden rounded-panel border border-border bg-input shadow-panel-field',
|
||||
root: 'flex h-6 min-w-0 flex-1 items-center overflow-hidden rounded border border-transparent bg-panel-field text-[11px] transition-colors hover:bg-panel-field-hover focus-within:border-panel-focus focus-within:bg-panel-field-hover',
|
||||
preview: 'flex shrink-0 items-center pl-0.5',
|
||||
value: 'flex min-w-0 flex-1 items-center px-1.5',
|
||||
divider: 'h-4 w-px shrink-0 bg-border',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ const panelFieldGroupTheme = {
|
|||
slots: {
|
||||
root: 'min-w-0',
|
||||
label: 'mb-1 block truncate text-[11px] leading-none text-muted',
|
||||
container: 'flex min-w-0 flex-col gap-panel'
|
||||
container: 'flex min-w-0 flex-col gap-1.5'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export const panelFieldBase =
|
||||
'h-control min-w-0 rounded-panel border border-transparent bg-panel-field text-surface outline-none hover:bg-panel-field-hover focus:border-panel-focus focus:bg-panel-field-hover focus-visible:border-panel-focus focus-within:border-panel-focus focus-within:bg-panel-field-hover disabled:cursor-not-allowed disabled:text-muted disabled:opacity-60'
|
||||
'h-6 min-w-0 rounded border border-transparent bg-panel-field text-surface outline-none hover:bg-panel-field-hover focus:border-panel-focus focus:bg-panel-field-hover focus-visible:border-panel-focus focus-within:border-panel-focus focus-within:bg-panel-field-hover disabled:cursor-not-allowed disabled:text-muted disabled:opacity-60'
|
||||
|
||||
export const panelFieldState = {
|
||||
idle: '',
|
||||
|
|
@ -9,4 +9,4 @@ export const panelFieldState = {
|
|||
} as const
|
||||
|
||||
export const panelIconButtonBase =
|
||||
'flex size-control shrink-0 cursor-pointer items-center justify-center rounded-panel border border-transparent bg-transparent text-muted outline-none hover:bg-hover hover:text-surface focus-visible:border-panel-focus disabled:cursor-not-allowed disabled:opacity-50'
|
||||
'flex size-6 shrink-0 cursor-pointer items-center justify-center rounded border border-transparent bg-transparent text-muted outline-none hover:bg-hover hover:text-surface focus-visible:border-panel-focus disabled:cursor-not-allowed disabled:opacity-50'
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
const panelGridTheme = {
|
||||
base: 'grid min-w-0 items-end gap-panel',
|
||||
base: 'grid min-w-0 items-end gap-1.5',
|
||||
variants: {
|
||||
columns: {
|
||||
two: 'grid-cols-2',
|
||||
three: 'grid-cols-3',
|
||||
'two-rail': 'grid-cols-[minmax(0,1fr)_minmax(0,1fr)_var(--spacing-panel-rail)]',
|
||||
appearance: 'grid-cols-[minmax(0,7fr)_minmax(0,5fr)]',
|
||||
'two-rail': 'grid-cols-[minmax(0,1fr)_minmax(0,1fr)_26px]',
|
||||
fill: 'grid-cols-[minmax(0,1fr)]',
|
||||
'fill-rail': 'grid-cols-[minmax(0,1fr)_var(--spacing-panel-rail)]'
|
||||
'fill-rail': 'grid-cols-[minmax(0,1fr)_26px]'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
const panelHeaderTheme = {
|
||||
slots: {
|
||||
root: 'grid min-w-0 grid-cols-[var(--spacing-panel-icon)_minmax(0,1fr)_auto] items-center gap-panel border-b border-border px-panel-x py-panel-y text-surface',
|
||||
icon: 'flex size-panel-icon items-center justify-center text-muted',
|
||||
root: 'grid min-w-0 grid-cols-[14px_minmax(0,1fr)_auto] items-center gap-1.5 border-b border-border px-3 py-2 text-surface',
|
||||
icon: 'flex size-3.5 items-center justify-center text-muted',
|
||||
title: 'min-w-0 truncate text-xs font-semibold text-surface',
|
||||
actions:
|
||||
'flex min-w-0 items-center justify-end gap-0.5 [&_[data-slot=icon-button]]:size-control [&_[data-slot=icon-button]]:rounded-panel'
|
||||
'flex min-w-0 items-center justify-end gap-0.5 [&_[data-slot=icon-button]]:size-6 [&_[data-slot=icon-button]]:rounded'
|
||||
},
|
||||
variants: {
|
||||
component: {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export default {
|
||||
slots: {
|
||||
root: 'group flex min-h-control items-center gap-panel py-0.5',
|
||||
content: 'flex min-w-0 flex-1 items-center gap-panel',
|
||||
root: 'group flex min-h-6 items-center gap-1.5 py-0.5',
|
||||
content: 'flex min-w-0 flex-1 items-center gap-1.5',
|
||||
rail: 'flex shrink-0 items-center gap-0.5',
|
||||
remove:
|
||||
'transition-opacity [@media(hover:hover)]:pointer-events-none [@media(hover:hover)]:opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100'
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export default {
|
||||
base: 'flex h-control w-panel-rail shrink-0 items-center justify-end gap-0.5 [&_[data-slot=icon-button]]:size-control [&_[data-slot=icon-button]]:rounded-panel'
|
||||
base: 'flex h-6 w-[26px] shrink-0 items-center justify-end gap-0.5 [&_[data-slot=icon-button]]:size-6 [&_[data-slot=icon-button]]:rounded'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
const panelSectionTheme = {
|
||||
slots: {
|
||||
root: 'border-b border-border px-panel-x py-panel-y text-surface data-[disabled]:opacity-60',
|
||||
header: 'mb-panel grid min-w-0 items-center gap-panel',
|
||||
root: 'border-b border-border px-3 pb-3 text-surface data-[disabled]:opacity-60',
|
||||
header: 'grid h-8 min-w-0 items-center gap-1.5',
|
||||
title:
|
||||
'min-w-0 cursor-pointer truncate border-0 bg-transparent p-0 text-left text-[11px] leading-none font-semibold text-surface',
|
||||
actions:
|
||||
'flex h-control w-panel-rail shrink-0 items-center justify-end gap-0.5 [&_[data-slot=icon-button]]:size-control [&_[data-slot=icon-button]]:rounded-panel',
|
||||
'flex h-7 w-[26px] shrink-0 items-center justify-end gap-0.5 [&_[data-slot=icon-button]]:size-6 [&_[data-slot=icon-button]]:rounded',
|
||||
body: 'min-w-0 data-[state=closed]:hidden'
|
||||
},
|
||||
variants: {
|
||||
actions: {
|
||||
true: {
|
||||
header: 'grid-cols-[minmax(0,1fr)_var(--spacing-panel-rail)]'
|
||||
header: 'grid-cols-[minmax(0,1fr)_26px]'
|
||||
},
|
||||
false: {
|
||||
header: 'grid-cols-[minmax(0,1fr)]'
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import { panelFieldBase } from './panel/field'
|
||||
|
||||
const segmentedControlTheme = {
|
||||
slots: {
|
||||
root: [panelFieldBase, 'inline-flex items-center gap-0.5 p-0.5'],
|
||||
item: 'flex h-[22px] min-w-0 flex-1 cursor-pointer items-center justify-center gap-1 rounded-sm text-muted outline-none hover:bg-hover hover:text-surface focus-visible:ring-1 focus-visible:ring-panel-focus data-[state=on]:bg-panel-selected-muted data-[state=on]:text-surface data-[state=on]:hover:bg-panel-selected-muted disabled:cursor-not-allowed disabled:opacity-50'
|
||||
root: 'inline-flex items-center gap-0.5 rounded bg-panel-field p-0.5 hover:bg-panel-field-hover',
|
||||
item: 'flex h-[22px] min-w-0 flex-1 cursor-pointer items-center justify-center gap-1 rounded-sm text-muted outline-none hover:bg-hover hover:text-surface focus-visible:ring-1 focus-visible:ring-panel-focus data-[state=on]:bg-hover data-[state=on]:text-surface data-[state=on]:hover:bg-hover disabled:cursor-not-allowed disabled:opacity-50'
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
sm: { item: 'px-1.5 text-[11px]' },
|
||||
md: { item: 'px-2 text-xs' }
|
||||
md: { item: 'px-2 text-[11px]' }
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
|
|||
|
|
@ -2,21 +2,21 @@ import { panelFieldBase } from './panel/field'
|
|||
|
||||
export default {
|
||||
slots: {
|
||||
trigger: [panelFieldBase, 'flex items-center justify-between'],
|
||||
trigger: [panelFieldBase, 'flex items-center justify-between text-[11px]'],
|
||||
value: 'min-w-0 flex-1 truncate text-left',
|
||||
content:
|
||||
'z-[110] min-w-[var(--reka-select-trigger-width)] overflow-hidden border border-border bg-panel',
|
||||
'z-[110] min-w-[var(--reka-select-trigger-width)] overflow-hidden rounded-xl bg-panel text-[11px] shadow-[0_8px_30px_rgb(0_0_0/0.4)]',
|
||||
viewport: '',
|
||||
item: 'relative flex cursor-pointer items-center text-surface outline-none select-none data-[disabled]:pointer-events-none data-[highlighted]:bg-hover data-[disabled]:opacity-50'
|
||||
item: 'relative flex h-6 cursor-pointer items-center text-surface outline-none select-none data-[disabled]:pointer-events-none data-[highlighted]:bg-hover data-[disabled]:opacity-50'
|
||||
},
|
||||
variants: {
|
||||
radius: {
|
||||
md: { content: 'rounded-md' },
|
||||
lg: { content: 'rounded-lg' }
|
||||
lg: { content: 'rounded-xl' }
|
||||
},
|
||||
elevation: {
|
||||
lg: { content: 'shadow-lg' },
|
||||
xl: { content: 'shadow-xl' }
|
||||
lg: { content: 'shadow-[0_8px_30px_rgb(0_0_0/0.4)]' },
|
||||
xl: { content: 'shadow-[0_8px_30px_rgb(0_0_0/0.4)]' }
|
||||
},
|
||||
padding: {
|
||||
none: { content: '' },
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const tabBarTheme = {
|
|||
root: 'scrollbar-none flex h-9 shrink-0 items-end overflow-x-auto border-b border-border bg-canvas',
|
||||
list: 'flex h-full items-end',
|
||||
trigger:
|
||||
'group/tab flex h-full max-w-48 min-w-0 cursor-pointer items-center gap-1.5 border-r border-border px-3 text-xs transition-colors outline-none select-none focus-visible:ring-1 focus-visible:ring-accent',
|
||||
'group/tab flex h-full max-w-48 min-w-0 cursor-pointer items-center gap-1.5 border-r border-border px-3 text-[11px] transition-colors outline-none select-none focus-visible:ring-1 focus-visible:ring-accent',
|
||||
icon: 'size-3 shrink-0 opacity-50',
|
||||
label: 'min-w-0 flex-1 truncate',
|
||||
close:
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.5 KiB |
|
|
@ -32,7 +32,7 @@ test('appearance fields share control height and show variable actions', async (
|
|||
section.getByRole('spinbutton', { name: 'Corner smoothing' })
|
||||
]
|
||||
for (const control of controls) {
|
||||
await expect(control).toHaveCSS('height', '26px')
|
||||
await expect(control).toHaveCSS('height', '24px')
|
||||
}
|
||||
|
||||
const applyVariable = section.getByRole('button', { name: 'Apply variable' }).first()
|
||||
|
|
@ -58,7 +58,7 @@ test('appearance fields share control height and show variable actions', async (
|
|||
|
||||
const fillItem = propertyItems(editor.page, 'fills').first()
|
||||
const paintField = fillItem.locator('[data-slot="paint-field"]')
|
||||
await expect(paintField).toHaveCSS('height', '26px')
|
||||
await expect(paintField).toHaveCSS('height', '24px')
|
||||
const colorBox = expectDefined(
|
||||
await fillItem.getByRole('textbox', { name: 'Fill' }).boundingBox(),
|
||||
'fill color bounds'
|
||||
|
|
|
|||
43
tests/e2e/ui/theme.spec.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
|
||||
|
||||
const editor = useEditorSetup()
|
||||
|
||||
test('rulers follow the active theme', async () => {
|
||||
const { page } = editor
|
||||
|
||||
const readState = () =>
|
||||
page.evaluate(() => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
const style = getComputedStyle(document.documentElement)
|
||||
return {
|
||||
cssTheme: document.documentElement.dataset.theme ?? 'dark',
|
||||
cssRulerBg: style.getPropertyValue('--color-ruler-bg').trim(),
|
||||
storeRulerBg: store?.state.rulerTheme?.background ?? null
|
||||
}
|
||||
})
|
||||
|
||||
// Baseline: dark theme
|
||||
const dark = await readState()
|
||||
expect(dark.cssTheme).toBe('dark')
|
||||
|
||||
// Switch to light via the app's theme API (module state, not raw localStorage,
|
||||
// so the watcher applies it and pushes the ruler theme to the active editor).
|
||||
await page.evaluate(async () => {
|
||||
const themeModule = await import('/src/app/shell/theme.ts')
|
||||
themeModule.useAppTheme().setTheme('light')
|
||||
})
|
||||
await page.waitForFunction(() => document.documentElement.dataset.theme === 'light')
|
||||
|
||||
const light = await readState()
|
||||
expect(light.cssTheme).toBe('light')
|
||||
// Canvas ruler theme must track the light tokens, not stay on the dark fallback.
|
||||
expect(light.cssRulerBg).not.toBe(dark.cssRulerBg)
|
||||
expect(light.storeRulerBg).not.toBeNull()
|
||||
|
||||
// Restore dark so later specs are unaffected.
|
||||
await page.evaluate(async () => {
|
||||
const themeModule = await import('/src/app/shell/theme.ts')
|
||||
themeModule.useAppTheme().setTheme('dark')
|
||||
})
|
||||
await page.waitForFunction(() => document.documentElement.dataset.theme === 'dark')
|
||||
})
|
||||