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.
This commit is contained in:
Danila Poyarkov 2026-07-18 10:56:24 +03:00
commit 11de539087
57 changed files with 191 additions and 133 deletions

View file

@ -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);
}

View file

@ -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)

View file

@ -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 {

View file

@ -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">

View file

@ -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>

View file

@ -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',

View file

@ -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')"

View file

@ -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>

View file

@ -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 }}

View file

@ -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>

View file

@ -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"

View file

@ -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')"
>

View file

@ -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 })" />

View file

@ -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="%"

View file

@ -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">

View file

@ -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"

View file

@ -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"

View file

@ -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>

View file

@ -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="°"

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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 &&

View file

@ -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'"

View file

@ -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)"
>

View file

@ -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')"

View file

@ -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>

View file

@ -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'
}

View file

@ -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: {

View file

@ -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>

View file

@ -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: ''

View file

@ -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)]'
}
})

View file

@ -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,

View file

@ -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: {

View file

@ -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:

View file

@ -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: {

View file

@ -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
},

View file

@ -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: {

View file

@ -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'

View file

@ -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: {

View file

@ -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',

View file

@ -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'
}
}

View file

@ -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'

View file

@ -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: {

View file

@ -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: {

View file

@ -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'

View file

@ -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'
}

View file

@ -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)]'

View file

@ -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: {

View file

@ -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: '' },

View file

@ -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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View file

@ -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'

View 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')
})