refactor(app): theme toolbar editing states
- Share typed toolbar slots across desktop, mobile, flyout, and action controls\n- Theme alignment cells, fill tabs, and gradient stop states\n- Expose semantic active and disabled state and retire migrated allowlist entries
This commit is contained in:
parent
3e750383c0
commit
69e39cc59e
|
|
@ -58,6 +58,7 @@ function activeKeyForTool(tool: EditorToolDef) {
|
|||
:data-test-id="toolbarToolTestId(tool.key)"
|
||||
:icon="toolIcons[tool.key]"
|
||||
:active="active || isActive(tool)"
|
||||
:ui="ui"
|
||||
@click="actions.select"
|
||||
/>
|
||||
</Tip>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { tv } from 'tailwind-variants'
|
||||
import { AnimatePresence, motion } from 'motion-v'
|
||||
|
||||
import IconChevronLeft from '~icons/lucide/chevron-left'
|
||||
|
|
@ -7,6 +8,7 @@ import IconChevronRight from '~icons/lucide/chevron-right'
|
|||
import ToolButton from '@/components/Toolbar/ToolButton.vue'
|
||||
import ToolFlyout from '@/components/Toolbar/ToolFlyout.vue'
|
||||
import ToolbarActionGroup from '@/components/Toolbar/ToolbarActionGroup.vue'
|
||||
import toolbarTheme from '@/theme/toolbar'
|
||||
import { toolbarToolTestId, ToolbarItem } from '@open-pencil/vue'
|
||||
|
||||
import type { Tool } from '@open-pencil/vue'
|
||||
|
|
@ -46,6 +48,9 @@ const {
|
|||
arrangeActions: ToolbarActionItem[]
|
||||
}>()
|
||||
|
||||
const toolbar = tv(toolbarTheme)
|
||||
const styles = toolbar()
|
||||
|
||||
const emit = defineEmits<{
|
||||
setTool: [tool: Tool]
|
||||
prev: []
|
||||
|
|
@ -62,6 +67,10 @@ const slideVariants = {
|
|||
function activeKeyForTool(tool: EditorToolDef) {
|
||||
return tool.flyout?.includes(activeTool) ? activeTool : tool.key
|
||||
}
|
||||
|
||||
function navigationClass(disabled: boolean) {
|
||||
return toolbar({ disabled }).navigationAction({ class: ui?.navigationAction })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -75,13 +84,14 @@ function activeKeyForTool(tool: EditorToolDef) {
|
|||
>
|
||||
<motion.button
|
||||
data-test-id="mobile-toolbar-prev"
|
||||
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-full border border-border bg-panel shadow-sm select-none"
|
||||
:class="hasPrev ? 'text-muted' : 'pointer-events-none'"
|
||||
:disabled="!hasPrev"
|
||||
:data-disabled="!hasPrev || undefined"
|
||||
:class="navigationClass(!hasPrev)"
|
||||
:animate="{ opacity: hasPrev ? 1 : 0 }"
|
||||
:transition="{ duration: 0.15 }"
|
||||
@click="emit('prev')"
|
||||
>
|
||||
<IconChevronLeft class="size-3.5" />
|
||||
<IconChevronLeft :class="styles.navigationIcon({ class: ui?.navigationIcon })" />
|
||||
</motion.button>
|
||||
|
||||
<motion.div
|
||||
|
|
@ -121,6 +131,7 @@ function activeKeyForTool(tool: EditorToolDef) {
|
|||
:data-test-id="toolbarToolTestId(tool.key, true)"
|
||||
:icon="toolIcons[tool.key]"
|
||||
:active="active || activeKeyForTool(tool) === activeTool"
|
||||
:ui="ui"
|
||||
@click="actions.select"
|
||||
/>
|
||||
</ToolbarItem>
|
||||
|
|
@ -140,6 +151,7 @@ function activeKeyForTool(tool: EditorToolDef) {
|
|||
>
|
||||
<ToolbarActionGroup
|
||||
:actions="editActions"
|
||||
:ui="ui"
|
||||
test-prefix="mobile-toolbar"
|
||||
@action="emit('action', $event)"
|
||||
/>
|
||||
|
|
@ -158,6 +170,7 @@ function activeKeyForTool(tool: EditorToolDef) {
|
|||
>
|
||||
<ToolbarActionGroup
|
||||
:actions="arrangeActions"
|
||||
:ui="ui"
|
||||
test-prefix="mobile-toolbar"
|
||||
@action="emit('action', $event)"
|
||||
/>
|
||||
|
|
@ -167,13 +180,14 @@ function activeKeyForTool(tool: EditorToolDef) {
|
|||
|
||||
<motion.button
|
||||
data-test-id="mobile-toolbar-next"
|
||||
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-full border border-border bg-panel shadow-sm select-none"
|
||||
:class="hasNext ? 'text-muted' : 'pointer-events-none'"
|
||||
:disabled="!hasNext"
|
||||
:data-disabled="!hasNext || undefined"
|
||||
:class="navigationClass(!hasNext)"
|
||||
:animate="{ opacity: hasNext ? 1 : 0 }"
|
||||
:transition="{ duration: 0.15 }"
|
||||
@click="emit('next')"
|
||||
>
|
||||
<IconChevronRight class="size-3.5" />
|
||||
<IconChevronRight :class="styles.navigationIcon({ class: ui?.navigationIcon })" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { tv } from 'tailwind-variants'
|
||||
|
||||
import toolbarTheme from '@/theme/toolbar'
|
||||
|
||||
import type { Component } from 'vue'
|
||||
import type { ToolbarUI } from '@/components/Toolbar/types'
|
||||
|
||||
interface ToolButtonProps {
|
||||
icon: Component
|
||||
active?: boolean
|
||||
mobile?: boolean
|
||||
ui?: ToolbarUI
|
||||
}
|
||||
|
||||
const { icon, active = false, mobile = false } = defineProps<ToolButtonProps>()
|
||||
const { icon, active = false, mobile = false, ui } = defineProps<ToolButtonProps>()
|
||||
const toolbar = tv(toolbarTheme)
|
||||
const styles = computed(() => toolbar({ active, mobile }))
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: []
|
||||
|
|
@ -16,17 +25,11 @@ const emit = defineEmits<{
|
|||
|
||||
<template>
|
||||
<button
|
||||
class="flex size-8 cursor-pointer items-center justify-center border-none transition-colors"
|
||||
:class="[
|
||||
mobile ? 'rounded-[6px] select-none' : 'rounded-lg',
|
||||
active
|
||||
? 'bg-accent text-white'
|
||||
: mobile
|
||||
? 'bg-transparent text-muted active:bg-hover'
|
||||
: 'bg-transparent text-muted hover:bg-hover hover:text-surface'
|
||||
]"
|
||||
:data-active="active || undefined"
|
||||
:data-mobile="mobile || undefined"
|
||||
:class="styles.button({ class: ui?.button })"
|
||||
@click="emit('click')"
|
||||
>
|
||||
<component :is="icon" class="size-4" />
|
||||
<component :is="icon" :class="styles.icon({ class: ui?.icon })" />
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { tv } from 'tailwind-variants'
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
|
|
@ -11,6 +13,7 @@ import IconChevronDown from '~icons/lucide/chevron-down'
|
|||
|
||||
import AppShortcutText from '@/components/ui/AppShortcutText.vue'
|
||||
import { menu } from '@/components/ui/menu'
|
||||
import toolbarTheme from '@/theme/toolbar'
|
||||
import ToolButton from '@/components/Toolbar/ToolButton.vue'
|
||||
import {
|
||||
toolbarFlyoutItemTestId,
|
||||
|
|
@ -42,6 +45,10 @@ const {
|
|||
mobile?: boolean
|
||||
}>()
|
||||
|
||||
const toolbar = tv(toolbarTheme)
|
||||
const triggerActive = computed(() => isActiveTool(activeKeyForTool()))
|
||||
const styles = computed(() => toolbar({ active: triggerActive.value, mobile }))
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [tool: Tool]
|
||||
}>()
|
||||
|
|
@ -59,16 +66,21 @@ function isActiveTool(key: Tool) {
|
|||
function activeKeyForTool() {
|
||||
return tool.flyout?.includes(activeTool) ? activeTool : tool.key
|
||||
}
|
||||
|
||||
function flyoutItemClass(subActive: boolean) {
|
||||
return menu().item({ class: toolbar({ subActive }).flyoutItem({ class: ui?.flyoutItem }) })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<div :class="styles.flyoutGroup({ class: ui?.flyoutGroup })">
|
||||
<slot :label="`${toolLabels[activeKeyForTool()]} (${tool.shortcut})`">
|
||||
<ToolButton
|
||||
:data-test-id="toolbarToolTestId(activeKeyForTool(), mobile)"
|
||||
:icon="toolIcons[activeKeyForTool()]"
|
||||
:active="isActiveTool(activeKeyForTool())"
|
||||
:active="triggerActive"
|
||||
:mobile="mobile"
|
||||
:ui="ui"
|
||||
@click="emit('select', activeKeyForTool())"
|
||||
/>
|
||||
</slot>
|
||||
|
|
@ -77,22 +89,21 @@ function activeKeyForTool() {
|
|||
<DropdownMenuTrigger as-child>
|
||||
<button
|
||||
v-test-id="toolbarFlyoutTestId(tool.key, mobile)"
|
||||
class="flex h-8 w-3 cursor-pointer items-center justify-center border-none transition-colors"
|
||||
:class="[
|
||||
mobile ? 'rounded-[6px] select-none' : 'rounded-lg',
|
||||
isActiveTool(activeKeyForTool())
|
||||
? 'bg-accent text-white'
|
||||
: mobile
|
||||
? 'bg-transparent text-muted active:bg-hover'
|
||||
: 'bg-transparent text-muted hover:bg-hover hover:text-surface'
|
||||
]"
|
||||
:data-active="triggerActive || undefined"
|
||||
:data-mobile="mobile || undefined"
|
||||
:class="styles.flyoutTrigger({ class: ui?.flyoutTrigger })"
|
||||
>
|
||||
<IconChevronDown class="size-2.5" />
|
||||
<IconChevronDown :class="styles.flyoutTriggerIcon({ class: ui?.flyoutTriggerIcon })" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent side="top" :side-offset="8" align="start" :class="ui?.flyoutContent">
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
:side-offset="8"
|
||||
align="start"
|
||||
:class="styles.flyoutContent({ class: ui?.flyoutContent })"
|
||||
>
|
||||
<ToolbarItem
|
||||
v-for="sub in tool.flyout"
|
||||
:key="sub"
|
||||
|
|
@ -101,11 +112,17 @@ function activeKeyForTool() {
|
|||
>
|
||||
<DropdownMenuItem
|
||||
v-test-id="toolbarFlyoutItemTestId(sub, mobile)"
|
||||
:class="menu().item({ class: subActive ? 'bg-accent text-white' : undefined })"
|
||||
:data-active="subActive || undefined"
|
||||
:class="flyoutItemClass(subActive)"
|
||||
@select="actions.select"
|
||||
>
|
||||
<component :is="toolIcons[sub]" class="size-3.5" />
|
||||
<span class="flex-1">{{ toolLabels[sub] }}</span>
|
||||
<component
|
||||
:is="toolIcons[sub]"
|
||||
:class="styles.flyoutItemIcon({ class: ui?.flyoutItemIcon })"
|
||||
/>
|
||||
<span :class="styles.flyoutItemLabel({ class: ui?.flyoutItemLabel })">
|
||||
{{ toolLabels[sub] }}
|
||||
</span>
|
||||
<AppShortcutText v-if="!mobile && toolShortcuts[sub]">
|
||||
{{ toolShortcuts[sub] }}
|
||||
</AppShortcutText>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import { tv } from 'tailwind-variants'
|
||||
|
||||
import toolbarTheme from '@/theme/toolbar'
|
||||
import { vTestId } from '@open-pencil/vue'
|
||||
|
||||
import type { ToolbarActionItem } from '@/components/Toolbar/types'
|
||||
import type { ToolbarActionItem, ToolbarUI } from '@/components/Toolbar/types'
|
||||
|
||||
const { actions, testPrefix } = defineProps<{
|
||||
const { actions, testPrefix, ui } = defineProps<{
|
||||
actions: ToolbarActionItem[]
|
||||
testPrefix: string
|
||||
ui?: ToolbarUI
|
||||
}>()
|
||||
|
||||
const styles = tv(toolbarTheme)()
|
||||
|
||||
const emit = defineEmits<{
|
||||
action: [item: ToolbarActionItem]
|
||||
}>()
|
||||
|
|
@ -18,9 +24,9 @@ const emit = defineEmits<{
|
|||
v-for="item in actions"
|
||||
:key="item.label"
|
||||
v-test-id="`${testPrefix}-${item.label.toLowerCase()}`"
|
||||
class="flex size-8 cursor-pointer items-center justify-center rounded-[6px] border-none bg-transparent text-muted transition-colors select-none active:bg-hover active:text-surface"
|
||||
:class="styles.action({ class: ui?.action })"
|
||||
@click="emit('action', item)"
|
||||
>
|
||||
<component :is="item.icon" class="size-4" />
|
||||
<component :is="item.icon" :class="styles.actionIcon({ class: ui?.actionIcon })" />
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@ import type { Component } from 'vue'
|
|||
|
||||
import type { Tool } from '@open-pencil/vue'
|
||||
|
||||
import type { ComponentUI } from '@/components/ui/types'
|
||||
import type { ToolbarTheme } from '@/theme/toolbar'
|
||||
|
||||
export interface ToolbarActionItem {
|
||||
icon: Component
|
||||
label: string
|
||||
action: () => void
|
||||
}
|
||||
|
||||
export interface ToolbarUI {
|
||||
flyoutContent?: string
|
||||
}
|
||||
export type ToolbarUI = ComponentUI<ToolbarTheme>
|
||||
|
||||
export type ToolLabels = Record<Tool, string>
|
||||
export type ToolIconMap = Record<Tool, Component>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { tv } from 'tailwind-variants'
|
||||
import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui'
|
||||
|
||||
import { applySolidFillColor, FillRoot, useI18n } from '@open-pencil/vue'
|
||||
|
|
@ -10,18 +10,15 @@ import ImageFillPicker from '@/components/fill-picker/ImageFillPicker.vue'
|
|||
import FillSwatch from '@/components/ui/FillSwatch.vue'
|
||||
import Tip from '@/components/ui/Tip.vue'
|
||||
import { usePopoverUI } from '@/components/ui/popover'
|
||||
import fillPickerTheme from '@/theme/fill-picker'
|
||||
|
||||
import type { Fill } from '@open-pencil/scene-graph'
|
||||
import type { OkHCLControls } from '@open-pencil/vue'
|
||||
|
||||
const TAB_BASE =
|
||||
'flex size-6 cursor-pointer items-center justify-center rounded border-none p-0 transition-colors'
|
||||
const fillPicker = tv(fillPickerTheme)
|
||||
|
||||
function tabClass(active: boolean) {
|
||||
return twMerge(
|
||||
TAB_BASE,
|
||||
active ? 'bg-hover text-surface' : 'text-muted hover:bg-hover hover:text-surface'
|
||||
)
|
||||
return fillPicker({ active }).tab()
|
||||
}
|
||||
|
||||
const {
|
||||
|
|
@ -77,6 +74,7 @@ function cancelFromEscape(event: KeyboardEvent) {
|
|||
<div class="mb-2 flex items-center gap-0.5">
|
||||
<Tip :label="panels.solid">
|
||||
<button
|
||||
:data-active="root.category === 'SOLID' || undefined"
|
||||
:class="tabClass(root.category === 'SOLID')"
|
||||
data-test-id="fill-picker-tab-solid"
|
||||
@click="root.actions.toSolid"
|
||||
|
|
@ -86,6 +84,7 @@ function cancelFromEscape(event: KeyboardEvent) {
|
|||
</Tip>
|
||||
<Tip :label="panels.linearGradient">
|
||||
<button
|
||||
:data-active="root.category === 'GRADIENT' || undefined"
|
||||
:class="tabClass(root.category === 'GRADIENT')"
|
||||
data-test-id="fill-picker-tab-gradient"
|
||||
@click="root.actions.toGradient"
|
||||
|
|
@ -95,6 +94,7 @@ function cancelFromEscape(event: KeyboardEvent) {
|
|||
</Tip>
|
||||
<Tip :label="panels.image">
|
||||
<button
|
||||
:data-active="root.category === 'IMAGE' || undefined"
|
||||
:class="tabClass(root.category === 'IMAGE')"
|
||||
data-test-id="fill-picker-tab-image"
|
||||
@click="root.actions.toImage"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { tv } from 'tailwind-variants'
|
||||
|
||||
import AppSelect from '@/components/ui/AppSelect.vue'
|
||||
import Tip from '@/components/ui/Tip.vue'
|
||||
import ColorPickerPanel from '@/components/color-picker-panel/ColorPickerPanel.vue'
|
||||
import NumberField from '@/components/inputs/NumberField.vue'
|
||||
import fillPickerTheme from '@/theme/fill-picker'
|
||||
import { colorToCSS } from '@open-pencil/core/color'
|
||||
import {
|
||||
GradientEditorRoot,
|
||||
|
|
@ -17,6 +20,15 @@ import type { Fill } from '@open-pencil/scene-graph'
|
|||
const { fill } = defineProps<{ fill: Fill }>()
|
||||
const emit = defineEmits<{ update: [fill: Fill] }>()
|
||||
const { panels } = useI18n()
|
||||
const fillPicker = tv(fillPickerTheme)
|
||||
|
||||
function barStopClass(active: boolean, dragging: boolean) {
|
||||
return fillPicker({ active, dragging }).barStop()
|
||||
}
|
||||
|
||||
function listStopClass(active: boolean) {
|
||||
return fillPicker({ active }).listStop()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -48,8 +60,7 @@ const { panels } = useI18n()
|
|||
:active="idx === bar.activeStopIndex"
|
||||
:dragging="idx === bar.draggingIndex"
|
||||
:removable="bar.stops.length > 2"
|
||||
class="absolute top-1/2 size-3.5 -translate-x-1/2 -translate-y-1/2 cursor-grab rounded-sm border-2 shadow-sm data-[selected]:border-white data-[dragging]:cursor-grabbing"
|
||||
:class="idx === bar.activeStopIndex ? 'border-white' : 'border-white/60'"
|
||||
:class="barStopClass(idx === bar.activeStopIndex, idx === bar.draggingIndex)"
|
||||
:style="{ left: `${stop.position * 100}%`, background: colorToCSS(stop.color) }"
|
||||
@select="root.actions.selectStop"
|
||||
@update-position="root.actions.updateStopPosition"
|
||||
|
|
@ -79,7 +90,7 @@ const { panels } = useI18n()
|
|||
:active="idx === root.activeStopIndex"
|
||||
:removable="root.stops.length > 2"
|
||||
:interactive="false"
|
||||
class="flex items-center gap-1 py-0.5 data-[selected]:rounded data-[selected]:bg-hover/50"
|
||||
:class="listStopClass(idx === root.activeStopIndex)"
|
||||
@select="root.actions.selectStop"
|
||||
@update-position="root.actions.updateStopPosition"
|
||||
@update-color="root.actions.updateStopColor"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { tv } from 'tailwind-variants'
|
||||
import {
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
|
|
@ -12,6 +13,7 @@ import {
|
|||
} from 'reka-ui'
|
||||
|
||||
import AppSelect from '@/components/ui/AppSelect.vue'
|
||||
import layoutAlignmentTheme from '@/theme/layout-alignment'
|
||||
|
||||
import VariableNumberField from '@/components/properties/VariableNumberField.vue'
|
||||
import ClipContentControl from '@/components/properties/LayoutSection/ClipContentControl.vue'
|
||||
|
|
@ -22,6 +24,8 @@ import { useI18n, useLayoutControlsContext } from '@open-pencil/vue'
|
|||
import type { LayoutDirection, LayoutAlign } from '@open-pencil/scene-graph'
|
||||
|
||||
const ctx = useLayoutControlsContext()
|
||||
const layoutAlignment = tv(layoutAlignmentTheme)
|
||||
const alignmentStyles = layoutAlignment()
|
||||
const gapFieldRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const { panels } = useI18n()
|
||||
|
|
@ -40,6 +44,10 @@ function isAlignmentActive(primary: LayoutAlign, counter: string) {
|
|||
return ctx.node.primaryAxisAlign === 'SPACE_BETWEEN' && ctx.node.counterAxisAlign === counter
|
||||
return ctx.node.primaryAxisAlign === primary && ctx.node.counterAxisAlign === counter
|
||||
}
|
||||
|
||||
function alignmentCellClass(primary: LayoutAlign, counter: string) {
|
||||
return layoutAlignment({ active: isAlignmentActive(primary, counter) }).cell()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -217,19 +225,15 @@ function isAlignmentActive(primary: LayoutAlign, counter: string) {
|
|||
|
||||
<div class="mt-2">
|
||||
<label class="mb-1 block text-[11px] text-muted">{{ panels.alignment }}</label>
|
||||
<div data-test-id="layout-alignment-grid" class="grid w-fit grid-cols-3 gap-0.5">
|
||||
<div data-test-id="layout-alignment-grid" :class="alignmentStyles.grid()">
|
||||
<button
|
||||
v-for="cell in ctx.alignGrid"
|
||||
:key="`${cell.primary}-${cell.counter}`"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded border text-[11px]"
|
||||
:class="
|
||||
isAlignmentActive(cell.primary, cell.counter)
|
||||
? 'border-accent bg-accent/10 text-accent'
|
||||
: 'border-border text-muted hover:bg-hover hover:text-surface'
|
||||
"
|
||||
:data-active="isAlignmentActive(cell.primary, cell.counter) || undefined"
|
||||
:class="alignmentCellClass(cell.primary, cell.counter)"
|
||||
@click="ctx.setAlignment(ctx.gapAuto ? 'SPACE_BETWEEN' : cell.primary, cell.counter)"
|
||||
>
|
||||
<span class="size-1.5 rounded-full bg-current" />
|
||||
<span :class="alignmentStyles.dot()" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
31
src/theme/fill-picker.ts
Normal file
31
src/theme/fill-picker.ts
Normal file
|
|
@ -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
|
||||
24
src/theme/layout-alignment.ts
Normal file
24
src/theme/layout-alignment.ts
Normal file
|
|
@ -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
|
||||
79
src/theme/toolbar.ts
Normal file
79
src/theme/toolbar.ts
Normal file
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -43,8 +43,10 @@ describe('dynamic Tailwind state classes', () => {
|
|||
})
|
||||
|
||||
test('allows only audited locations inside legacy files', () => {
|
||||
const audited = `<template>${'\n'.repeat(19)}<button :class="active ? 'bg-hover' : 'text-muted'" /></template>`
|
||||
expect(dynamicClassDiagnostics('src/components/Toolbar/ToolButton.vue', audited)).toEqual([])
|
||||
const audited = `<template>${'\n'.repeat(36)}<button :class="active ? 'bg-hover' : 'text-muted'" /></template>`
|
||||
expect(
|
||||
dynamicClassDiagnostics('src/components/CollabPanel/CollabAvatarStack.vue', audited)
|
||||
).toEqual([])
|
||||
expect(
|
||||
dynamicClassDiagnostics(
|
||||
'src/components/LayersPanel.vue',
|
||||
|
|
|
|||
Loading…
Reference in a new issue