-
diff --git a/src/theme/fill-picker.ts b/src/theme/fill-picker.ts
new file mode 100644
index 000000000..022b0e970
--- /dev/null
+++ b/src/theme/fill-picker.ts
@@ -0,0 +1,31 @@
+const fillPickerTheme = {
+ slots: {
+ tab: 'flex size-6 cursor-pointer items-center justify-center rounded border-none p-0 text-muted transition-colors outline-none focus-visible:ring-1 focus-visible:ring-accent',
+ barStop:
+ 'absolute top-1/2 size-3.5 -translate-x-1/2 -translate-y-1/2 cursor-grab rounded-sm border-2 border-white/60 shadow-sm outline-none data-[dragging]:cursor-grabbing focus-visible:ring-1 focus-visible:ring-accent',
+ listStop: 'flex items-center gap-1 py-0.5'
+ },
+ variants: {
+ active: {
+ true: {
+ tab: 'bg-hover text-surface',
+ barStop: 'border-white',
+ listStop: 'rounded bg-hover/50'
+ },
+ false: {
+ tab: 'hover:bg-hover hover:text-surface'
+ }
+ },
+ dragging: {
+ true: { barStop: 'cursor-grabbing' },
+ false: {}
+ }
+ },
+ defaultVariants: {
+ active: false,
+ dragging: false
+ }
+}
+
+export type FillPickerTheme = typeof fillPickerTheme
+export default fillPickerTheme
diff --git a/src/theme/layout-alignment.ts b/src/theme/layout-alignment.ts
new file mode 100644
index 000000000..0b17915f6
--- /dev/null
+++ b/src/theme/layout-alignment.ts
@@ -0,0 +1,24 @@
+const layoutAlignmentTheme = {
+ slots: {
+ grid: 'grid w-fit grid-cols-3 gap-0.5',
+ cell: 'flex size-6 cursor-pointer items-center justify-center rounded border text-[11px]',
+ dot: 'size-1.5 rounded-full bg-current'
+ },
+ variants: {
+ active: {
+ true: { cell: 'border-accent bg-accent/10 text-accent' },
+ false: { cell: 'border-border text-muted hover:bg-hover hover:text-surface' }
+ },
+ disabled: {
+ true: { cell: 'pointer-events-none opacity-50' },
+ false: {}
+ }
+ },
+ defaultVariants: {
+ active: false,
+ disabled: false
+ }
+}
+
+export type LayoutAlignmentTheme = typeof layoutAlignmentTheme
+export default layoutAlignmentTheme
diff --git a/src/theme/toolbar.ts b/src/theme/toolbar.ts
new file mode 100644
index 000000000..75c2e1272
--- /dev/null
+++ b/src/theme/toolbar.ts
@@ -0,0 +1,79 @@
+const toolbarTheme = {
+ slots: {
+ button:
+ 'flex size-8 cursor-pointer items-center justify-center border-none bg-transparent text-muted transition-colors outline-none focus-visible:ring-1 focus-visible:ring-accent',
+ icon: 'size-4',
+ flyoutGroup: 'flex items-center',
+ flyoutTrigger:
+ 'flex h-8 w-3 cursor-pointer items-center justify-center border-none bg-transparent text-muted transition-colors outline-none focus-visible:ring-1 focus-visible:ring-accent',
+ flyoutTriggerIcon: 'size-2.5',
+ flyoutContent: '',
+ flyoutItem: '',
+ flyoutItemIcon: 'size-3.5',
+ flyoutItemLabel: 'flex-1',
+ navigationAction:
+ 'flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-full border border-border bg-panel text-muted shadow-sm outline-none select-none focus-visible:ring-1 focus-visible:ring-accent disabled:pointer-events-none',
+ navigationIcon: 'size-3.5',
+ action:
+ 'flex size-8 cursor-pointer items-center justify-center rounded-[6px] border-none bg-transparent text-muted transition-colors outline-none select-none active:bg-hover active:text-surface focus-visible:ring-1 focus-visible:ring-accent',
+ actionIcon: 'size-4'
+ },
+ variants: {
+ active: {
+ true: {
+ button: 'bg-accent text-white',
+ flyoutTrigger: 'bg-accent text-white'
+ },
+ false: {}
+ },
+ mobile: {
+ true: {
+ button: 'rounded-[6px] select-none',
+ flyoutTrigger: 'rounded-[6px] select-none'
+ },
+ false: {
+ button: 'rounded-lg',
+ flyoutTrigger: 'rounded-lg'
+ }
+ },
+ disabled: {
+ true: {
+ navigationAction: 'pointer-events-none'
+ },
+ false: {}
+ },
+ subActive: {
+ true: {
+ flyoutItem: 'bg-accent text-white'
+ },
+ false: {}
+ }
+ },
+ compoundVariants: [
+ {
+ active: false,
+ mobile: true,
+ class: {
+ button: 'active:bg-hover',
+ flyoutTrigger: 'active:bg-hover'
+ }
+ },
+ {
+ active: false,
+ mobile: false,
+ class: {
+ button: 'hover:bg-hover hover:text-surface',
+ flyoutTrigger: 'hover:bg-hover hover:text-surface'
+ }
+ }
+ ],
+ defaultVariants: {
+ active: false,
+ mobile: false,
+ disabled: false,
+ subActive: false
+ }
+}
+
+export type ToolbarTheme = typeof toolbarTheme
+export default toolbarTheme
diff --git a/tests/e2e/color-picker/basic.spec.ts b/tests/e2e/color-picker/basic.spec.ts
index 380b25fc5..4b74b90d4 100644
--- a/tests/e2e/color-picker/basic.spec.ts
+++ b/tests/e2e/color-picker/basic.spec.ts
@@ -158,7 +158,9 @@ test('hsb saturation and brightness sliders both affect fill color', async () =>
test('gradient stops support keyboard nudging and removal', async () => {
await openFillPicker()
- await page.getByTestId('fill-picker-tab-gradient').click()
+ const gradientTab = page.getByTestId('fill-picker-tab-gradient')
+ await gradientTab.click()
+ await expect(gradientTab).toHaveAttribute('data-active', 'true')
await page.getByTestId('fill-picker-add-stop').click()
const stops = page.getByTestId('fill-picker-gradient-bar').getByRole('slider')
diff --git a/tests/e2e/editor/auto-layout/basic.spec.ts b/tests/e2e/editor/auto-layout/basic.spec.ts
index 14474bd22..680f03abb 100644
--- a/tests/e2e/editor/auto-layout/basic.spec.ts
+++ b/tests/e2e/editor/auto-layout/basic.spec.ts
@@ -256,6 +256,7 @@ test('alignment grid center sets CENTER alignment', async () => {
const centerCell = page.getByTestId('layout-alignment-grid').locator('button').nth(4)
await centerCell.click()
await canvas.waitForRender()
+ await expect(centerCell).toHaveAttribute('data-active', 'true')
const frame = await getNodeById(page, frameId)
expect(expectDefined(frame, 'frame').primaryAxisAlign).toBe('CENTER')
diff --git a/tests/e2e/toolbar/basic.spec.ts b/tests/e2e/toolbar/basic.spec.ts
index 4f6f0619d..561730a9b 100644
--- a/tests/e2e/toolbar/basic.spec.ts
+++ b/tests/e2e/toolbar/basic.spec.ts
@@ -1,6 +1,10 @@
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
import { getPageChildren } from '#tests/helpers/store'
-import { toolbarFlyoutItemTestId, toolbarFlyoutTestId } from '#tests/helpers/test-ids'
+import {
+ toolbarFlyoutItemTestId,
+ toolbarFlyoutTestId,
+ toolbarToolTestId
+} from '#tests/helpers/test-ids'
const editor = useEditorSetup()
@@ -12,6 +16,10 @@ test('shapes flyout opens', async () => {
test('Polygon tool creates POLYGON node', async () => {
await editor.page.getByTestId(toolbarFlyoutItemTestId('POLYGON')).click()
+ await expect(editor.page.getByTestId(toolbarToolTestId('POLYGON'))).toHaveAttribute(
+ 'data-active',
+ 'true'
+ )
await editor.canvas.drag(300, 200, 400, 300)
await editor.canvas.waitForRender()
diff --git a/tools/architecture/src/steiger-rules/dynamic-tailwind-classes.ts b/tools/architecture/src/steiger-rules/dynamic-tailwind-classes.ts
index 6f91f9eea..41abab28d 100644
--- a/tools/architecture/src/steiger-rules/dynamic-tailwind-classes.ts
+++ b/tools/architecture/src/steiger-rules/dynamic-tailwind-classes.ts
@@ -8,14 +8,7 @@ const DYNAMIC_CLASS_ALLOWLIST = new Set([
'src/components/CollabPanel/CollabAvatarStack.vue:37',
'src/components/CollabPanel/CollabSharePopover.vue:20',
'src/components/MobileHud/MobilePresencePopover.vue:52',
- 'src/components/Toolbar/MobileToolbar.vue:79',
- 'src/components/Toolbar/MobileToolbar.vue:171',
- 'src/components/Toolbar/ToolButton.vue:20',
- 'src/components/Toolbar/ToolFlyout.vue:81',
- 'src/components/Toolbar/ToolFlyout.vue:104',
'src/components/chat/ProviderConnectionTestButton.vue:67',
- 'src/components/fill-picker/GradientEditor.vue:52',
- 'src/components/properties/LayoutSection/FlexControls.vue:225',
'src/components/properties/binding/demo/BindingFieldDemoItem.vue:54',
'src/components/variables/VariablesDialog.vue:269',
'src/components/variables/VariablesDialog.vue:324'
diff --git a/tools/architecture/tests/dynamic-classes.test.ts b/tools/architecture/tests/dynamic-classes.test.ts
index e22d1900b..7631e82c8 100644
--- a/tools/architecture/tests/dynamic-classes.test.ts
+++ b/tools/architecture/tests/dynamic-classes.test.ts
@@ -43,8 +43,10 @@ describe('dynamic Tailwind state classes', () => {
})
test('allows only audited locations inside legacy files', () => {
- const audited = `
${'\n'.repeat(19)}`
- expect(dynamicClassDiagnostics('src/components/Toolbar/ToolButton.vue', audited)).toEqual([])
+ const audited = `
${'\n'.repeat(36)}`
+ expect(
+ dynamicClassDiagnostics('src/components/CollabPanel/CollabAvatarStack.vue', audited)
+ ).toEqual([])
expect(
dynamicClassDiagnostics(
'src/components/LayersPanel.vue',