Merge branch 'figma-layout-hierarchy'
|
|
@ -19,12 +19,13 @@
|
|||
- Drag with the Text tool to create a fixed-size text box, or click to create auto-width text.
|
||||
- Target a specific open document and page from live CLI and MCP automation, including sessions with multiple documents.
|
||||
- Test OpenAI-compatible provider connections from AI settings with clearer setup errors.
|
||||
- Build custom property panels with new Vue SDK number fields, bindable values, property sections, segmented controls, property lists, color models, fill controls, and gradient primitives.
|
||||
- Build custom property panels with new Vue SDK number fields, bindable values, property sections, responsive property grids, segmented controls, property lists, color models, fill controls, and gradient primitives.
|
||||
- Connect local MCP clients through automatically discovered private Unix sockets on macOS and Linux, with localhost TCP fallback. (#338)
|
||||
- Create centered frames from current Figma-style device and asset presets, or resize selected frames from the Design panel while preserving their names.
|
||||
|
||||
### Changed
|
||||
|
||||
- Choose Freeform, vertical, horizontal, or grid flow directly from the contextual Layout section, with sizing grouped alongside it and current Layout guide terminology.
|
||||
- Redesign the editor chrome and Design panel with denser aligned controls, clearer selection and section states, improved menus and overlays, consistent light/dark theming, and better keyboard and screen-reader behavior.
|
||||
- Scale the Layers panel to documents with thousands of nodes through virtualized rows, faster incremental updates, stable expansion, range selection, and scroll-to-selection.
|
||||
- Resolve fonts before text appears, with language-aware CJK and Arabic fallback, character-specific remote subsets, and more reliable rendering as fonts load.
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ description: Component reference for headless Vue primitives in @open-pencil/vue
|
|||
|
||||
<SdkCardGroup>
|
||||
<SdkCard title="PropertySection" to="/programmable/sdk/api/components/property-section" description="Collapsible property-section anatomy and empty states." />
|
||||
<SdkCard title="PropertyGrid" to="/programmable/sdk/api/components/property-grid" description="Responsive field-grid and action-rail anatomy." />
|
||||
<SdkCard title="SegmentedControl" to="/programmable/sdk/api/components/segmented-control" description="Accessible selection and action-only segment groups." />
|
||||
<SdkCard title="PropertyListRoot" to="/programmable/sdk/api/components/property-list-root" description="Headless property list primitive." />
|
||||
<SdkCard title="PropertyListItem" to="/programmable/sdk/api/components/property-list-item" description="Single fills, strokes, or effects row primitive." />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { defineComponentMetaLoader } from '#docs/sdk/component-meta'
|
||||
|
||||
const sources = ['packages/vue/src/primitives/PropertyGrid/PropertyGridRoot.vue']
|
||||
|
||||
export default defineComponentMetaLoader(sources)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
title: PropertyGrid
|
||||
description: Headless field-grid and action-rail anatomy for property panels.
|
||||
---
|
||||
|
||||
<script setup lang="ts">
|
||||
import { data } from './property-grid.data'
|
||||
</script>
|
||||
|
||||
# PropertyGrid
|
||||
|
||||
`PropertyGridRoot` separates responsive property fields from optional intrinsic-width actions. It exposes structural data attributes without imposing column widths, gaps, or presentation.
|
||||
|
||||
```vue twoslash
|
||||
<script setup lang="ts">
|
||||
import { PropertyGridRoot } from '@open-pencil/vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PropertyGridRoot :columns="2">
|
||||
<label>
|
||||
Width
|
||||
<input type="number" />
|
||||
</label>
|
||||
<label>
|
||||
Height
|
||||
<input type="number" />
|
||||
</label>
|
||||
<template #actions>
|
||||
<button type="button" aria-label="Constrain proportions">Link</button>
|
||||
</template>
|
||||
</PropertyGridRoot>
|
||||
</template>
|
||||
```
|
||||
|
||||
Themes can target `data-slot="fields"`, `data-slot="actions"`, `data-columns`, and `data-distribution`. The `wide-first` distribution is semantic; consumers choose its exact ratio.
|
||||
|
||||
## Generated API reference
|
||||
|
||||
<SdkComponentAPI :components="data.components" />
|
||||
|
|
@ -128,12 +128,14 @@
|
|||
"multiplayerCursors": "Cursor anderer Teilnehmer",
|
||||
"direction": "Richtung",
|
||||
"flow": "Fluss",
|
||||
"freeform": "Freiform",
|
||||
"dimensions": "Abmessungen",
|
||||
"layoutHorizontal": "Horizontales Layout",
|
||||
"layoutVertical": "Vertikales Layout",
|
||||
"layoutGrid": "Rasterlayout",
|
||||
"layoutGrids": "Layout-Raster",
|
||||
"addLayoutGrid": "Layout-Raster hinzufügen",
|
||||
"removeLayoutGrid": "Layout-Raster entfernen",
|
||||
"layoutGrids": "Layout-Hilfslinien",
|
||||
"addLayoutGrid": "Layout-Hilfslinie hinzufügen",
|
||||
"removeLayoutGrid": "Layout-Hilfslinie entfernen",
|
||||
"gridGrid": "Raster",
|
||||
"gridColumns": "Spalten",
|
||||
"gridRows": "Zeilen",
|
||||
|
|
|
|||
|
|
@ -142,12 +142,14 @@
|
|||
"multiplayerCursors": "Cursores de otros participantes",
|
||||
"direction": "Dirección",
|
||||
"flow": "Flujo",
|
||||
"freeform": "Forma libre",
|
||||
"dimensions": "Dimensiones",
|
||||
"layoutHorizontal": "Diseño horizontal",
|
||||
"layoutVertical": "Diseño vertical",
|
||||
"layoutGrid": "Diseño de cuadrícula",
|
||||
"layoutGrids": "Cuadrículas de diseño",
|
||||
"addLayoutGrid": "Añadir cuadrícula de diseño",
|
||||
"removeLayoutGrid": "Eliminar cuadrícula de diseño",
|
||||
"layoutGrids": "Guías de diseño",
|
||||
"addLayoutGrid": "Añadir guía de diseño",
|
||||
"removeLayoutGrid": "Eliminar guía de diseño",
|
||||
"gridGrid": "Cuadrícula",
|
||||
"gridColumns": "Columnas",
|
||||
"gridRows": "Filas",
|
||||
|
|
|
|||
|
|
@ -128,12 +128,14 @@
|
|||
"multiplayerCursors": "Curseurs des participants",
|
||||
"direction": "Direction",
|
||||
"flow": "Flux",
|
||||
"freeform": "Forme libre",
|
||||
"dimensions": "Dimensions",
|
||||
"layoutHorizontal": "Disposition horizontale",
|
||||
"layoutVertical": "Disposition verticale",
|
||||
"layoutGrid": "Disposition en grille",
|
||||
"layoutGrids": "Grilles de mise en page",
|
||||
"addLayoutGrid": "Ajouter une grille de mise en page",
|
||||
"removeLayoutGrid": "Supprimer la grille de mise en page",
|
||||
"layoutGrids": "Repères de mise en page",
|
||||
"addLayoutGrid": "Ajouter un repère de mise en page",
|
||||
"removeLayoutGrid": "Supprimer le repère de mise en page",
|
||||
"gridGrid": "Grille",
|
||||
"gridColumns": "Colonnes",
|
||||
"gridRows": "Rangées",
|
||||
|
|
|
|||
|
|
@ -128,12 +128,14 @@
|
|||
"multiplayerCursors": "Cursori degli altri partecipanti",
|
||||
"direction": "Direzione",
|
||||
"flow": "Flusso",
|
||||
"freeform": "Forma libera",
|
||||
"dimensions": "Dimensioni",
|
||||
"layoutHorizontal": "Layout orizzontale",
|
||||
"layoutVertical": "Layout verticale",
|
||||
"layoutGrid": "Layout a griglia",
|
||||
"layoutGrids": "Griglie di layout",
|
||||
"addLayoutGrid": "Aggiungi griglia di layout",
|
||||
"removeLayoutGrid": "Rimuovi griglia di layout",
|
||||
"layoutGrids": "Guide di layout",
|
||||
"addLayoutGrid": "Aggiungi guida di layout",
|
||||
"removeLayoutGrid": "Rimuovi guida di layout",
|
||||
"gridGrid": "Griglia",
|
||||
"gridColumns": "Colonne",
|
||||
"gridRows": "Righe",
|
||||
|
|
|
|||
|
|
@ -142,12 +142,14 @@
|
|||
"multiplayerCursors": "他の参加者のカーソル",
|
||||
"direction": "方向",
|
||||
"flow": "フロー",
|
||||
"freeform": "フリーフォーム",
|
||||
"dimensions": "寸法",
|
||||
"layoutHorizontal": "横方向レイアウト",
|
||||
"layoutVertical": "縦方向レイアウト",
|
||||
"layoutGrid": "グリッドレイアウト",
|
||||
"layoutGrids": "レイアウトグリッド",
|
||||
"addLayoutGrid": "レイアウトグリッドを追加",
|
||||
"removeLayoutGrid": "レイアウトグリッドを削除",
|
||||
"layoutGrids": "レイアウトガイド",
|
||||
"addLayoutGrid": "レイアウトガイドを追加",
|
||||
"removeLayoutGrid": "レイアウトガイドを削除",
|
||||
"gridGrid": "グリッド",
|
||||
"gridColumns": "列",
|
||||
"gridRows": "行",
|
||||
|
|
|
|||
|
|
@ -128,12 +128,14 @@
|
|||
"multiplayerCursors": "Kursory innych uczestników",
|
||||
"direction": "Kierunek",
|
||||
"flow": "Przepływ",
|
||||
"freeform": "Swobodny",
|
||||
"dimensions": "Wymiary",
|
||||
"layoutHorizontal": "Układ poziomy",
|
||||
"layoutVertical": "Układ pionowy",
|
||||
"layoutGrid": "Układ siatki",
|
||||
"layoutGrids": "Siatki układu",
|
||||
"addLayoutGrid": "Dodaj siatkę układu",
|
||||
"removeLayoutGrid": "Usuń siatkę układu",
|
||||
"layoutGrids": "Prowadnice układu",
|
||||
"addLayoutGrid": "Dodaj prowadnicę układu",
|
||||
"removeLayoutGrid": "Usuń prowadnicę układu",
|
||||
"gridGrid": "Siatka",
|
||||
"gridColumns": "Kolumny",
|
||||
"gridRows": "Wiersze",
|
||||
|
|
|
|||
|
|
@ -128,12 +128,14 @@
|
|||
"multiplayerCursors": "Курсоры участников",
|
||||
"direction": "Направление",
|
||||
"flow": "Поток",
|
||||
"freeform": "Свободная",
|
||||
"dimensions": "Размеры",
|
||||
"layoutHorizontal": "Горизонтальная раскладка",
|
||||
"layoutVertical": "Вертикальная раскладка",
|
||||
"layoutGrid": "Сетка",
|
||||
"layoutGrids": "Сетки макета",
|
||||
"addLayoutGrid": "Добавить сетку макета",
|
||||
"removeLayoutGrid": "Удалить сетку макета",
|
||||
"layoutGrids": "Направляющие макета",
|
||||
"addLayoutGrid": "Добавить направляющую макета",
|
||||
"removeLayoutGrid": "Удалить направляющую макета",
|
||||
"gridGrid": "Сетка",
|
||||
"gridColumns": "Столбцы",
|
||||
"gridRows": "Строки",
|
||||
|
|
|
|||
|
|
@ -128,12 +128,14 @@
|
|||
"multiplayerCursors": "协作光标",
|
||||
"direction": "方向",
|
||||
"flow": "流向",
|
||||
"freeform": "自由布局",
|
||||
"dimensions": "尺寸",
|
||||
"layoutHorizontal": "水平布局",
|
||||
"layoutVertical": "垂直布局",
|
||||
"layoutGrid": "网格布局",
|
||||
"layoutGrids": "布局网格",
|
||||
"addLayoutGrid": "添加布局网格",
|
||||
"removeLayoutGrid": "移除布局网格",
|
||||
"layoutGrids": "布局参考线",
|
||||
"addLayoutGrid": "添加布局参考线",
|
||||
"removeLayoutGrid": "移除布局参考线",
|
||||
"gridGrid": "网格",
|
||||
"gridColumns": "列",
|
||||
"gridRows": "行",
|
||||
|
|
|
|||
|
|
@ -231,6 +231,8 @@ export const panelMessageDefaults = {
|
|||
multiplayerCursors: 'Multiplayer cursors',
|
||||
direction: 'Direction',
|
||||
flow: 'Flow',
|
||||
freeform: 'Freeform',
|
||||
dimensions: 'Dimensions',
|
||||
layoutHorizontal: 'Horizontal layout',
|
||||
layoutVertical: 'Vertical layout',
|
||||
layoutGrid: 'Grid layout',
|
||||
|
|
@ -266,9 +268,9 @@ export const panelMessageDefaults = {
|
|||
resizeAutoWidth: 'Auto width',
|
||||
resizeAutoHeight: 'Auto height',
|
||||
resizeFixed: 'Fixed size',
|
||||
layoutGrids: 'Layout grid',
|
||||
addLayoutGrid: 'Add layout grid',
|
||||
removeLayoutGrid: 'Remove layout grid',
|
||||
layoutGrids: 'Layout guide',
|
||||
addLayoutGrid: 'Add layout guide',
|
||||
removeLayoutGrid: 'Remove layout guide',
|
||||
gridColumns: 'Columns',
|
||||
gridRows: 'Rows',
|
||||
gridGrid: 'Grid',
|
||||
|
|
|
|||
|
|
@ -246,6 +246,14 @@ export type {
|
|||
PropertyListRootSlotProps,
|
||||
PropertyListRootSlots
|
||||
} from '#vue/primitives/PropertyList'
|
||||
export { PropertyGridRoot } from '#vue/primitives/PropertyGrid'
|
||||
export type {
|
||||
PropertyGridColumns,
|
||||
PropertyGridDistribution,
|
||||
PropertyGridRootProps,
|
||||
PropertyGridRootSlots
|
||||
} from '#vue/primitives/PropertyGrid'
|
||||
|
||||
export {
|
||||
PropertySectionRoot,
|
||||
PropertySectionHeader,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import type {
|
||||
PropertyGridRootProps,
|
||||
PropertyGridRootSlots
|
||||
} from '#vue/primitives/PropertyGrid/types'
|
||||
|
||||
const { columns = 1, distribution = 'equal' } = defineProps<PropertyGridRootProps>()
|
||||
defineSlots<PropertyGridRootSlots>()
|
||||
defineOptions({ inheritAttrs: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-bind="$attrs"
|
||||
data-slot="root"
|
||||
data-property-grid
|
||||
:data-columns="columns"
|
||||
:data-distribution="distribution"
|
||||
>
|
||||
<div data-slot="fields">
|
||||
<slot />
|
||||
</div>
|
||||
<div v-if="$slots.actions" data-slot="actions">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
7
packages/vue/src/primitives/PropertyGrid/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export { default as PropertyGridRoot } from '#vue/primitives/PropertyGrid/PropertyGridRoot.vue'
|
||||
export type {
|
||||
PropertyGridColumns,
|
||||
PropertyGridDistribution,
|
||||
PropertyGridRootProps,
|
||||
PropertyGridRootSlots
|
||||
} from '#vue/primitives/PropertyGrid/types'
|
||||
18
packages/vue/src/primitives/PropertyGrid/types.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import type { VNode } from 'vue'
|
||||
|
||||
export type PropertyGridColumns = 1 | 2 | 3
|
||||
export type PropertyGridDistribution = 'equal' | 'wide-first'
|
||||
|
||||
export interface PropertyGridRootProps {
|
||||
/** Number of field columns. @default 1 */
|
||||
columns?: PropertyGridColumns
|
||||
/** Relative distribution of field columns. @default 'equal' */
|
||||
distribution?: PropertyGridDistribution
|
||||
}
|
||||
|
||||
export interface PropertyGridRootSlots {
|
||||
/** Property fields arranged by the consumer's visual theme. */
|
||||
default(): VNode[]
|
||||
/** Optional intrinsic-width controls kept separate from the field grid. */
|
||||
actions?(): VNode[]
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import AppearanceSection from './properties/AppearanceSection.vue'
|
|||
import EffectsSection from './properties/EffectsSection.vue'
|
||||
import ExportSection from './properties/ExportSection.vue'
|
||||
import FillSection from './properties/FillSection.vue'
|
||||
import LayoutGridSection from './properties/LayoutSection/LayoutGridSection.vue'
|
||||
import LayoutSection from './properties/LayoutSection/LayoutSection.vue'
|
||||
import MaskSection from './properties/MaskSection.vue'
|
||||
import PageSection from './properties/PageSection.vue'
|
||||
|
|
@ -39,6 +40,10 @@ const isComponentType = computed(() => {
|
|||
return type ? COMPONENT_TYPES.has(type) : false
|
||||
})
|
||||
const selectedIcon = computed(() => (node.value ? nodeIcon(node.value) : undefined))
|
||||
const supportsLayoutGuides = computed(() => {
|
||||
const type = node.value?.type
|
||||
return type === 'FRAME' || type === 'COMPONENT' || type === 'COMPONENT_SET' || type === 'INSTANCE'
|
||||
})
|
||||
const { panels } = useI18n()
|
||||
</script>
|
||||
|
||||
|
|
@ -131,6 +136,7 @@ const { panels } = useI18n()
|
|||
<TypographySection v-if="node.type === 'TEXT'" />
|
||||
<FillSection />
|
||||
<StrokeSection />
|
||||
<LayoutGridSection v-if="supportsLayoutGuides" />
|
||||
<EffectsSection />
|
||||
|
||||
<ExportSection />
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ const styles = useColorSliderUI(
|
|||
:max="displayMax"
|
||||
:step="displayStep"
|
||||
:suffix="suffix"
|
||||
:ui="{ leading: 'hidden' }"
|
||||
:ui="{ leading: 'hidden', field: 'pl-1.5', display: 'pl-1.5' }"
|
||||
@update:model-value="emit('updateDisplay', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ const styles = useColorSliderUI(
|
|||
:max="numberMax"
|
||||
:step="numberStep"
|
||||
:suffix="suffix"
|
||||
:ui="{ leading: 'hidden' }"
|
||||
:ui="{ leading: 'hidden', field: 'pl-1.5', display: 'pl-1.5' }"
|
||||
@update:model-value="emit('updateNumber', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ function cancelFromEscape(event: KeyboardEvent) {
|
|||
type="button"
|
||||
:aria-label="panels.fill"
|
||||
data-test-id="fill-picker-swatch"
|
||||
class="size-5 shrink-0 cursor-pointer rounded border-0 bg-transparent p-0"
|
||||
class="size-4 shrink-0 cursor-pointer rounded-sm border-0 bg-transparent p-0"
|
||||
>
|
||||
<FillSwatch :fill="fill" class="size-full" v-slot="swatch">
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import AppSelect from '@/components/ui/AppSelect.vue'
|
|||
import IconButton from '@/components/ui/IconButton.vue'
|
||||
import PanelFieldGroup from '@/components/ui/panel/PanelFieldGroup.vue'
|
||||
import PanelGrid from '@/components/ui/panel/PanelGrid.vue'
|
||||
import PanelRail from '@/components/ui/panel/PanelRail.vue'
|
||||
import PanelSection from '@/components/ui/panel/PanelSection.vue'
|
||||
|
||||
import type { BlendMode } from '@open-pencil/scene-graph'
|
||||
|
|
@ -55,7 +54,7 @@ function blendModeOptions(value: BlendMode | typeof MIXED) {
|
|||
</IconButton>
|
||||
</template>
|
||||
|
||||
<PanelGrid columns="appearance">
|
||||
<PanelGrid :columns="2" distribution="wide-first">
|
||||
<PanelFieldGroup :label="panels.blendMode">
|
||||
<AppSelect
|
||||
:model-value="blendModeValue === MIXED ? 'MIXED' : blendModeValue"
|
||||
|
|
@ -103,11 +102,7 @@ function blendModeOptions(value: BlendMode | typeof MIXED) {
|
|||
</PanelFieldGroup>
|
||||
</PanelGrid>
|
||||
|
||||
<PanelGrid
|
||||
v-if="hasCornerRadius && !showIndependentCorners"
|
||||
columns="fill-rail"
|
||||
class="mt-1.5"
|
||||
>
|
||||
<PanelGrid v-if="hasCornerRadius && !showIndependentCorners" :columns="2" class="mt-1.5">
|
||||
<PanelFieldGroup :label="panels.radius">
|
||||
<VariableNumberField
|
||||
v-if="node && !isMulti"
|
||||
|
|
@ -137,7 +132,7 @@ function blendModeOptions(value: BlendMode | typeof MIXED) {
|
|||
</template>
|
||||
</NumberField>
|
||||
</PanelFieldGroup>
|
||||
<PanelRail>
|
||||
<div class="flex h-6 items-center justify-end">
|
||||
<IconButton
|
||||
:label="panels.independentCornerRadii"
|
||||
size="md"
|
||||
|
|
@ -146,13 +141,13 @@ function blendModeOptions(value: BlendMode | typeof MIXED) {
|
|||
>
|
||||
<icon-lucide-square-round-corner class="size-3" />
|
||||
</IconButton>
|
||||
</PanelRail>
|
||||
</div>
|
||||
</PanelGrid>
|
||||
|
||||
<PanelGrid
|
||||
v-else-if="hasCornerRadius && !isMulti && node"
|
||||
columns="two-rail"
|
||||
class="mt-1.5"
|
||||
:columns="2"
|
||||
class="mt-1.5 [&>[data-slot=actions]]:self-start"
|
||||
data-corner-grid
|
||||
>
|
||||
<VariableNumberField
|
||||
|
|
@ -173,16 +168,6 @@ function blendModeOptions(value: BlendMode | typeof MIXED) {
|
|||
@update:model-value="actions.updateCornerProp('topRightRadius', $event)"
|
||||
@commit="(v: number, p: number) => actions.commitCornerProp('topRightRadius', v, p)"
|
||||
/>
|
||||
<PanelRail>
|
||||
<IconButton
|
||||
:label="panels.independentCornerRadii"
|
||||
size="md"
|
||||
active
|
||||
@click="actions.toggleIndependentCorners"
|
||||
>
|
||||
<icon-lucide-square-round-corner class="size-3" />
|
||||
</IconButton>
|
||||
</PanelRail>
|
||||
<VariableNumberField
|
||||
label="BL"
|
||||
:model-value="node.bottomLeftRadius"
|
||||
|
|
@ -201,10 +186,19 @@ function blendModeOptions(value: BlendMode | typeof MIXED) {
|
|||
@update:model-value="actions.updateCornerProp('bottomRightRadius', $event)"
|
||||
@commit="(v: number, p: number) => actions.commitCornerProp('bottomRightRadius', v, p)"
|
||||
/>
|
||||
<PanelRail />
|
||||
<template #actions>
|
||||
<IconButton
|
||||
:label="panels.independentCornerRadii"
|
||||
size="md"
|
||||
active
|
||||
@click="actions.toggleIndependentCorners"
|
||||
>
|
||||
<icon-lucide-square-round-corner class="size-3" />
|
||||
</IconButton>
|
||||
</template>
|
||||
</PanelGrid>
|
||||
|
||||
<PanelGrid v-if="hasCornerRadius" columns="fill" class="mt-1.5">
|
||||
<PanelGrid v-if="hasCornerRadius" :columns="2" class="mt-1.5">
|
||||
<PanelFieldGroup :label="panels.cornerSmoothing">
|
||||
<NumberField
|
||||
suffix="%"
|
||||
|
|
|
|||
|
|
@ -93,67 +93,68 @@ function updateSolidColor(
|
|||
:visibility-label="panels.toggleVisibility"
|
||||
:remove-label="panels.removeFill"
|
||||
>
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||
<BindableValueRoot
|
||||
v-slot="binding"
|
||||
:provider="colorProvider"
|
||||
:targets="paintBindingTargets(selectedNodeIds, 'fills', index)"
|
||||
:value="fill.color"
|
||||
batch-label="Change fill color"
|
||||
<BindableValueRoot
|
||||
v-slot="binding"
|
||||
:provider="colorProvider"
|
||||
:targets="paintBindingTargets(selectedNodeIds, 'fills', index)"
|
||||
:value="fill.color"
|
||||
batch-label="Change fill color"
|
||||
>
|
||||
<PaintField
|
||||
class="w-full flex-none"
|
||||
:opacity="fill.opacity"
|
||||
:opacity-label="panels.opacity"
|
||||
@update:opacity="actions.patch(index, { opacity: $event })"
|
||||
>
|
||||
<PaintField
|
||||
class="w-full flex-none"
|
||||
:opacity="fill.opacity"
|
||||
:opacity-label="panels.opacity"
|
||||
@update:opacity="actions.patch(index, { opacity: $event })"
|
||||
>
|
||||
<template #preview>
|
||||
<FillPicker
|
||||
:fill="displayFill(fill, binding.resolvedValue)"
|
||||
:okhcl="createFillOkhclAdapter(okhcl, activeNode, index)"
|
||||
@update="
|
||||
updatePickerFill(binding.actions, flush, $event, (next) =>
|
||||
actions.update(index, next)
|
||||
)
|
||||
"
|
||||
@open-change="!$event && commitPaintMutation(binding.actions)"
|
||||
@cancel="cancelPaintMutation(binding.actions)"
|
||||
/>
|
||||
</template>
|
||||
<template #preview>
|
||||
<FillPicker
|
||||
:fill="displayFill(fill, binding.resolvedValue)"
|
||||
:okhcl="createFillOkhclAdapter(okhcl, activeNode, index)"
|
||||
@update="
|
||||
updatePickerFill(binding.actions, flush, $event, (next) =>
|
||||
actions.update(index, next)
|
||||
)
|
||||
"
|
||||
@open-change="!$event && commitPaintMutation(binding.actions)"
|
||||
@cancel="cancelPaintMutation(binding.actions)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #value>
|
||||
<PaintValue
|
||||
v-if="fill.type === 'SOLID'"
|
||||
:color="fill.color"
|
||||
:resolved-color="binding.resolvedValue"
|
||||
:variable-name="binding.variable?.name"
|
||||
:label="panels.fill"
|
||||
@update="
|
||||
updateSolidColor(binding.actions, flush, fill, $event, (next) =>
|
||||
actions.update(index, next)
|
||||
)
|
||||
"
|
||||
/>
|
||||
<span v-else class="min-w-0 flex-1 truncate font-mono text-xs text-surface">
|
||||
{{ fillLabel(fill) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #value>
|
||||
<PaintValue
|
||||
v-if="fill.type === 'SOLID'"
|
||||
:color="fill.color"
|
||||
:resolved-color="binding.resolvedValue"
|
||||
:variable-name="binding.variable?.name"
|
||||
:label="panels.fill"
|
||||
@update="
|
||||
updateSolidColor(binding.actions, flush, fill, $event, (next) =>
|
||||
actions.update(index, next)
|
||||
)
|
||||
"
|
||||
/>
|
||||
<span v-else class="min-w-0 flex-1 truncate font-mono text-xs text-surface">
|
||||
{{ fillLabel(fill) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-if="fill.type === 'SOLID'" #binding>
|
||||
<VariableBindingPicker
|
||||
:trigger-label="panels.applyVariable"
|
||||
:search-placeholder="dialogs.search"
|
||||
:empty-label="panels.noVariablesFound"
|
||||
:detach-label="panels.detachVariable"
|
||||
:create-label="
|
||||
panels.createColorVariable({ value: `#${colorToHexRaw(fill.color)}` })
|
||||
"
|
||||
:create-name-placeholder="panels.variableName"
|
||||
:create-submit-label="panels.create"
|
||||
/>
|
||||
</template>
|
||||
</PaintField>
|
||||
</BindableValueRoot>
|
||||
<template v-if="fill.type === 'SOLID'" #binding>
|
||||
<VariableBindingPicker
|
||||
:trigger-label="panels.applyVariable"
|
||||
:search-placeholder="dialogs.search"
|
||||
:empty-label="panels.noVariablesFound"
|
||||
:detach-label="panels.detachVariable"
|
||||
:create-label="
|
||||
panels.createColorVariable({ value: `#${colorToHexRaw(fill.color)}` })
|
||||
"
|
||||
:create-name-placeholder="panels.variableName"
|
||||
:create-submit-label="panels.create"
|
||||
/>
|
||||
</template>
|
||||
</PaintField>
|
||||
</BindableValueRoot>
|
||||
|
||||
<template #details>
|
||||
<PanelFieldGroup :label="panels.blendMode">
|
||||
<AppSelect
|
||||
:model-value="fill.blendMode ?? 'NORMAL'"
|
||||
|
|
@ -167,7 +168,7 @@ function updateSolidColor(
|
|||
"
|
||||
/>
|
||||
</PanelFieldGroup>
|
||||
</div>
|
||||
</template>
|
||||
</PropertyItemRow>
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
|
|
|||
|
|
@ -4,46 +4,56 @@ import { computed } from 'vue'
|
|||
import { useI18n, useLayoutControlsContext } from '@open-pencil/vue'
|
||||
|
||||
import IconButton from '@/components/ui/IconButton.vue'
|
||||
import SegmentedControl from '@/components/ui/SegmentedControl.vue'
|
||||
import Tip from '@/components/ui/Tip.vue'
|
||||
|
||||
import type { LayoutMode } from '@open-pencil/scene-graph'
|
||||
|
||||
const ctx = useLayoutControlsContext()
|
||||
const { panels } = useI18n()
|
||||
|
||||
const layoutModes = computed<Array<{ mode: LayoutMode; label: string }>>(() => [
|
||||
{ mode: 'HORIZONTAL', label: panels.value.layoutHorizontal },
|
||||
{ mode: 'VERTICAL', label: panels.value.layoutVertical },
|
||||
{ mode: 'GRID', label: panels.value.layoutGrid }
|
||||
const layoutModes = computed<Array<{ value: LayoutMode; label: string }>>(() => [
|
||||
{ value: 'NONE', label: panels.value.freeform },
|
||||
{ value: 'VERTICAL', label: panels.value.layoutVertical },
|
||||
{ value: 'HORIZONTAL', label: panels.value.layoutHorizontal },
|
||||
{ value: 'GRID', label: panels.value.layoutGrid }
|
||||
])
|
||||
|
||||
function setLayoutMode(mode: string) {
|
||||
ctx.editor.setLayoutMode(ctx.node.id, mode as LayoutMode)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="ctx.node.layoutMode !== 'NONE'"
|
||||
class="flex items-center gap-1"
|
||||
role="toolbar"
|
||||
:aria-label="panels.flow"
|
||||
>
|
||||
<IconButton
|
||||
v-for="direction in layoutModes"
|
||||
:key="direction.mode"
|
||||
:label="direction.label"
|
||||
size="md"
|
||||
:active="direction.mode === 'GRID' ? ctx.isGrid : ctx.node.layoutMode === direction.mode"
|
||||
@click="ctx.editor.setLayoutMode(ctx.node.id, direction.mode)"
|
||||
>
|
||||
<icon-lucide-arrow-right v-if="direction.mode === 'HORIZONTAL'" class="size-3.5" />
|
||||
<icon-lucide-arrow-down v-else-if="direction.mode === 'VERTICAL'" class="size-3.5" />
|
||||
<icon-lucide-layout-grid v-else class="size-3.5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-if="ctx.isFlex"
|
||||
:label="panels.layoutWrap"
|
||||
size="md"
|
||||
:active="ctx.node.layoutWrap === 'WRAP'"
|
||||
@click="ctx.updateProp('layoutWrap', ctx.node.layoutWrap === 'WRAP' ? 'NO_WRAP' : 'WRAP')"
|
||||
>
|
||||
<icon-lucide-wrap-text class="size-3.5" />
|
||||
</IconButton>
|
||||
<div>
|
||||
<label class="mb-1 block text-[11px] text-muted">{{ panels.flow }}</label>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<SegmentedControl
|
||||
:model-value="ctx.node.layoutMode"
|
||||
:options="layoutModes"
|
||||
:label="panels.flow"
|
||||
:ui="{ root: 'flex min-w-0 flex-1' }"
|
||||
@change="setLayoutMode"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<Tip :label="option.label" class="flex items-center justify-center">
|
||||
<icon-lucide-move v-if="option.value === 'NONE'" class="size-3.5" />
|
||||
<icon-lucide-rows-2 v-else-if="option.value === 'VERTICAL'" class="size-3.5" />
|
||||
<icon-lucide-columns-2 v-else-if="option.value === 'HORIZONTAL'" class="size-3.5" />
|
||||
<icon-lucide-layout-grid v-else class="size-3.5" />
|
||||
</Tip>
|
||||
</template>
|
||||
</SegmentedControl>
|
||||
|
||||
<IconButton
|
||||
v-if="ctx.isFlex"
|
||||
:label="panels.layoutWrap"
|
||||
size="md"
|
||||
:active="ctx.node.layoutWrap === 'WRAP'"
|
||||
@click="ctx.updateProp('layoutWrap', ctx.node.layoutWrap === 'WRAP' ? 'NO_WRAP' : 'WRAP')"
|
||||
>
|
||||
<icon-lucide-wrap-text class="size-3.5" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ function alignmentCellClass(primary: LayoutAlign, counter: string) {
|
|||
|
||||
<template>
|
||||
<div class="mt-2">
|
||||
<label class="mb-1 block text-[11px] text-muted">{{ panels.flow }}</label>
|
||||
<label class="mb-1 block text-[11px] text-muted">{{ panels.direction }}</label>
|
||||
<AppSelect
|
||||
:model-value="ctx.layoutDirection"
|
||||
:options="[
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n, useLayoutControlsContext, useSceneComputed } from '@open-pencil/vue'
|
||||
import { useEditor, useI18n, useSceneComputed } from '@open-pencil/vue'
|
||||
|
||||
import NumberField from '@/components/inputs/NumberField.vue'
|
||||
import IconButton from '@/components/ui/IconButton.vue'
|
||||
|
|
@ -10,13 +10,14 @@ import PanelFieldGroup from '@/components/ui/panel/PanelFieldGroup.vue'
|
|||
import PanelGrid from '@/components/ui/panel/PanelGrid.vue'
|
||||
import PanelItemRow from '@/components/ui/panel/PanelItemRow.vue'
|
||||
import PanelSection from '@/components/ui/panel/PanelSection.vue'
|
||||
import SharedStyleField from '@/components/properties/shared-style/SharedStyleField.vue'
|
||||
|
||||
import type { LayoutGrid } from '@open-pencil/scene-graph'
|
||||
|
||||
const ctx = useLayoutControlsContext()
|
||||
const editor = useEditor()
|
||||
const { panels } = useI18n()
|
||||
|
||||
const selectedNode = useSceneComputed(() => ctx.editor.getSelectedNode() ?? null)
|
||||
const selectedNode = useSceneComputed(() => editor.getSelectedNode() ?? null)
|
||||
const grids = computed<LayoutGrid[]>(() => selectedNode.value?.layoutGrids ?? [])
|
||||
|
||||
const patternOptions = computed(() => [
|
||||
|
|
@ -41,21 +42,21 @@ function defaultGrid(): LayoutGrid {
|
|||
function commit(next: LayoutGrid[], label: string) {
|
||||
const node = selectedNode.value
|
||||
if (!node) return
|
||||
ctx.editor.updateNodeWithUndo(node.id, { layoutGrids: next }, label)
|
||||
editor.updateNodeWithUndo(node.id, { layoutGrids: next }, label)
|
||||
}
|
||||
|
||||
function add() {
|
||||
commit([...grids.value, defaultGrid()], 'Add layout grid')
|
||||
commit([...grids.value, defaultGrid()], 'Add layout guide')
|
||||
}
|
||||
|
||||
function remove(index: number) {
|
||||
commit(
|
||||
grids.value.filter((_, i) => i !== index),
|
||||
'Remove layout grid'
|
||||
'Remove layout guide'
|
||||
)
|
||||
}
|
||||
|
||||
function patch(index: number, changes: Partial<LayoutGrid>, label = 'Edit layout grid') {
|
||||
function patch(index: number, changes: Partial<LayoutGrid>, label = 'Edit layout guide') {
|
||||
commit(
|
||||
grids.value.map((grid, i) => (i === index ? { ...grid, ...changes } : grid)),
|
||||
label
|
||||
|
|
@ -74,6 +75,7 @@ function isGrid(grid: LayoutGrid): boolean {
|
|||
|
||||
<template>
|
||||
<PanelSection :label="panels.layoutGrids" :empty="grids.length === 0">
|
||||
<SharedStyleField kind="grid" :label="panels.gridStyle" />
|
||||
<template #actions>
|
||||
<IconButton :label="panels.addLayoutGrid" @click="add">
|
||||
<icon-lucide-plus class="size-3.5" />
|
||||
|
|
@ -98,7 +100,7 @@ function isGrid(grid: LayoutGrid): boolean {
|
|||
</Tip>
|
||||
</template>
|
||||
</SegmentedControl>
|
||||
<PanelGrid columns="two">
|
||||
<PanelGrid :columns="2">
|
||||
<PanelFieldGroup :label="panels.gridCount">
|
||||
<NumberField
|
||||
:model-value="grid.count ?? grid.numSections ?? 1"
|
||||
|
|
@ -136,7 +138,7 @@ function isGrid(grid: LayoutGrid): boolean {
|
|||
<IconButton
|
||||
:label="panels.toggleVisibility"
|
||||
:active="grid.visible === false"
|
||||
@click="patch(index, { visible: grid.visible === false }, 'Toggle layout grid')"
|
||||
@click="patch(index, { visible: grid.visible === false }, 'Toggle layout guide')"
|
||||
>
|
||||
<icon-lucide-eye-off v-if="grid.visible === false" class="size-3.5" />
|
||||
<icon-lucide-eye v-else class="size-3.5" />
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@ import AutoLayoutControls from '@/components/properties/LayoutSection/AutoLayout
|
|||
import ClipContentControl from '@/components/properties/LayoutSection/ClipContentControl.vue'
|
||||
import FlexControls from '@/components/properties/LayoutSection/FlexControls.vue'
|
||||
import GridControls from '@/components/properties/LayoutSection/GridControls.vue'
|
||||
import LayoutGridSection from '@/components/properties/LayoutSection/LayoutGridSection.vue'
|
||||
import IconButton from '@/components/ui/IconButton.vue'
|
||||
import PaddingControls from '@/components/properties/LayoutSection/PaddingControls.vue'
|
||||
import SizeControls from '@/components/properties/LayoutSection/size/SizeControls.vue'
|
||||
import TextResizingControl from '@/components/properties/LayoutSection/TextResizingControl.vue'
|
||||
import SharedStyleField from '@/components/properties/shared-style/SharedStyleField.vue'
|
||||
import PanelSection from '@/components/ui/panel/PanelSection.vue'
|
||||
|
||||
const { panels } = useI18n()
|
||||
|
|
@ -21,45 +19,41 @@ const CONTAINER_TYPES = ['FRAME', 'COMPONENT', 'COMPONENT_SET', 'INSTANCE']
|
|||
<template>
|
||||
<LayoutControlsRoot v-slot="ctx">
|
||||
<template v-if="ctx.node">
|
||||
<PanelSection :label="panels.layout">
|
||||
<SharedStyleField kind="grid" :label="panels.gridStyle" />
|
||||
<PanelSection :label="ctx.node.layoutMode === 'NONE' ? panels.layout : panels.autoLayout">
|
||||
<template v-if="CONTAINER_TYPES.includes(ctx.node.type)" #actions>
|
||||
<IconButton
|
||||
:label="ctx.node.layoutMode === 'NONE' ? panels.addAutoLayout : panels.removeAutoLayout"
|
||||
size="md"
|
||||
:active="ctx.node.layoutMode !== 'NONE'"
|
||||
class="data-[state=on]:bg-accent/15"
|
||||
@click="
|
||||
ctx.editor.setLayoutMode(
|
||||
ctx.node.id,
|
||||
ctx.node.layoutMode === 'NONE' ? 'VERTICAL' : 'NONE'
|
||||
)
|
||||
"
|
||||
>
|
||||
<icon-lucide-layout-panel-top class="size-3.5" />
|
||||
</IconButton>
|
||||
</template>
|
||||
|
||||
<AutoLayoutControls v-if="CONTAINER_TYPES.includes(ctx.node.type)" />
|
||||
<div class="mt-2 mb-1 text-[11px] text-muted">{{ panels.dimensions }}</div>
|
||||
<TextResizingControl v-if="ctx.node.type === 'TEXT'" />
|
||||
<SizeControls />
|
||||
<ClipContentControl
|
||||
v-if="CONTAINER_TYPES.includes(ctx.node.type) && ctx.node.layoutMode === 'NONE'"
|
||||
/>
|
||||
|
||||
<template v-if="CONTAINER_TYPES.includes(ctx.node.type) && ctx.node.layoutMode !== 'NONE'">
|
||||
<FlexControls v-if="ctx.isFlex" />
|
||||
<template v-if="ctx.isGrid">
|
||||
<GridControls />
|
||||
<PaddingControls />
|
||||
<ClipContentControl />
|
||||
</template>
|
||||
</template>
|
||||
</PanelSection>
|
||||
|
||||
<template v-if="CONTAINER_TYPES.includes(ctx.node.type)">
|
||||
<PanelSection :label="panels.autoLayout">
|
||||
<template #actions>
|
||||
<IconButton
|
||||
v-if="ctx.node.layoutMode === 'NONE'"
|
||||
:label="panels.addAutoLayout"
|
||||
@click="ctx.editor.setLayoutMode(ctx.node.id, 'VERTICAL')"
|
||||
>
|
||||
<icon-lucide-plus class="size-3.5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-else
|
||||
:label="panels.removeAutoLayout"
|
||||
@click="ctx.editor.setLayoutMode(ctx.node.id, 'NONE')"
|
||||
>
|
||||
<icon-lucide-minus class="size-3.5" />
|
||||
</IconButton>
|
||||
</template>
|
||||
|
||||
<AutoLayoutControls />
|
||||
|
||||
<template v-if="ctx.node.layoutMode !== 'NONE'">
|
||||
<FlexControls v-if="ctx.isFlex" />
|
||||
<template v-if="ctx.isGrid">
|
||||
<GridControls />
|
||||
<PaddingControls />
|
||||
<ClipContentControl />
|
||||
</template>
|
||||
</template>
|
||||
</PanelSection>
|
||||
|
||||
<LayoutGridSection />
|
||||
</template>
|
||||
</template>
|
||||
</LayoutControlsRoot>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -48,12 +48,12 @@ const visibleSizeLimits = computed(() =>
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<PanelGrid columns="two">
|
||||
<PanelGrid :columns="2">
|
||||
<SizeAxisField axis="width" icon="W" :label="panels.width" />
|
||||
<SizeAxisField axis="height" icon="H" :label="panels.height" />
|
||||
</PanelGrid>
|
||||
|
||||
<PanelGrid v-if="visibleSizeLimits.length" columns="two" class="mt-1.5">
|
||||
<PanelGrid v-if="visibleSizeLimits.length" :columns="2" class="mt-1.5">
|
||||
<SizeLimitField v-for="item in visibleSizeLimits" :key="item.prop" :item="item" />
|
||||
</PanelGrid>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ function handleAlign(
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<PanelGrid columns="two">
|
||||
<PanelGrid :columns="2">
|
||||
<Tip :label="panels.xAxis">
|
||||
<NumberField
|
||||
icon="X"
|
||||
|
|
@ -102,7 +102,7 @@ function handleAlign(
|
|||
</Tip>
|
||||
</PanelGrid>
|
||||
|
||||
<PanelGrid v-if="isMulti" columns="two" class="mt-1.5">
|
||||
<PanelGrid v-if="isMulti" :columns="2" class="mt-1.5">
|
||||
<Tip :label="panels.width">
|
||||
<NumberField
|
||||
icon="W"
|
||||
|
|
@ -127,7 +127,7 @@ function handleAlign(
|
|||
</Tip>
|
||||
</PanelGrid>
|
||||
|
||||
<div class="mt-1.5 grid grid-cols-[minmax(0,1fr)_repeat(3,24px)] gap-0.5">
|
||||
<PanelGrid :columns="2" class="mt-1.5">
|
||||
<Tip :label="panels.rotation">
|
||||
<NumberField
|
||||
suffix="°"
|
||||
|
|
@ -144,16 +144,18 @@ function handleAlign(
|
|||
</template>
|
||||
</NumberField>
|
||||
</Tip>
|
||||
<IconButton :label="panels.flipHorizontal" size="md" @click="actions.flip('horizontal')">
|
||||
<icon-lucide-flip-horizontal-2 class="size-3.5" />
|
||||
</IconButton>
|
||||
<IconButton :label="panels.flipVertical" size="md" @click="actions.flip('vertical')">
|
||||
<icon-lucide-flip-vertical-2 class="size-3.5" />
|
||||
</IconButton>
|
||||
<IconButton :label="panels.rotate90" size="md" @click="actions.rotate(90)">
|
||||
<icon-lucide-rotate-cw-square class="size-3.5" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class="flex h-6 items-center justify-end gap-0.5">
|
||||
<IconButton :label="panels.flipHorizontal" size="md" @click="actions.flip('horizontal')">
|
||||
<icon-lucide-flip-horizontal-2 class="size-3.5" />
|
||||
</IconButton>
|
||||
<IconButton :label="panels.flipVertical" size="md" @click="actions.flip('vertical')">
|
||||
<icon-lucide-flip-vertical-2 class="size-3.5" />
|
||||
</IconButton>
|
||||
<IconButton :label="panels.rotate90" size="md" @click="actions.rotate(90)">
|
||||
<icon-lucide-rotate-cw-square class="size-3.5" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</PanelGrid>
|
||||
</PanelSection>
|
||||
</PositionControlsRoot>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ function onToggleSides(activeNode: SceneNode | null) {
|
|||
<button
|
||||
type="button"
|
||||
:aria-label="panels.stroke"
|
||||
class="size-5 shrink-0 cursor-pointer rounded border-0 bg-transparent p-0"
|
||||
class="size-4 shrink-0 cursor-pointer rounded-sm border-0 bg-transparent p-0"
|
||||
>
|
||||
<FillSwatch
|
||||
:fill="strokePreview(stroke, binding.resolvedValue ?? stroke.color)"
|
||||
|
|
@ -266,7 +266,7 @@ function onToggleSides(activeNode: SceneNode | null) {
|
|||
</template>
|
||||
</div>
|
||||
|
||||
<PanelGrid v-if="advancedActive" columns="three" class="mt-1.5">
|
||||
<PanelGrid v-if="advancedActive" :columns="3" class="mt-1.5">
|
||||
<PanelFieldGroup :label="panels.strokeCap">
|
||||
<SegmentedControl
|
||||
:model-value="cap === MIXED ? 'MIXED' : cap"
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
</Tip>
|
||||
</div>
|
||||
|
||||
<PanelGrid columns="two" class="mb-3">
|
||||
<PanelGrid :columns="2" class="mb-3">
|
||||
<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-3">
|
||||
<PanelGrid :columns="2" class="mb-3">
|
||||
<PanelFieldGroup :label="panels.lineHeight">
|
||||
<VariableNumberField
|
||||
:model-value="
|
||||
|
|
@ -196,7 +196,11 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
</SegmentedControl>
|
||||
</PanelFieldGroup>
|
||||
|
||||
<PanelFieldGroup :label="panels.textFormatting" class="mb-3" :ui="{ container: 'flex-row gap-1.5' }">
|
||||
<PanelFieldGroup
|
||||
:label="panels.textFormatting"
|
||||
class="mb-3"
|
||||
:ui="{ container: 'flex-row gap-1.5' }"
|
||||
>
|
||||
<div
|
||||
class="inline-flex items-center gap-0.5 rounded bg-panel-field p-0.5 hover:bg-panel-field-hover"
|
||||
role="toolbar"
|
||||
|
|
@ -237,7 +241,7 @@ function featureEnabled(features: Array<{ tag: string; enabled: boolean }>, tag:
|
|||
</div>
|
||||
</PanelFieldGroup>
|
||||
|
||||
<PanelGrid columns="two" class="mb-3">
|
||||
<PanelGrid :columns="2" class="mb-3">
|
||||
<PanelFieldGroup :label="panels.textCase">
|
||||
<AppSelect
|
||||
:label="panels.textCase"
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ const emit = defineEmits<{
|
|||
defineSlots<{
|
||||
default(props: PropertyListItemSlotProps<K>): VNode[]
|
||||
rail?(props: PropertyListItemSlotProps<K>): VNode[]
|
||||
details?(props: PropertyListItemSlotProps<K>): VNode[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
|
|
@ -47,6 +48,9 @@ defineSlots<{
|
|||
>
|
||||
<PanelItemRow>
|
||||
<slot v-bind="item" />
|
||||
<template v-if="$slots.details" #details>
|
||||
<slot name="details" v-bind="item" />
|
||||
</template>
|
||||
<template #rail="{ removeClass }">
|
||||
<slot name="rail" v-bind="item" />
|
||||
<Tip v-if="showVisibility" :label="visibilityLabel">
|
||||
|
|
|
|||
|
|
@ -56,7 +56,9 @@ const styles = computed(() => tv(paintFieldTheme)())
|
|||
:max="100"
|
||||
:ui="{
|
||||
root: 'h-full rounded-none border-0 bg-transparent shadow-none',
|
||||
leading: 'hidden'
|
||||
leading: 'hidden',
|
||||
field: 'pl-1.5',
|
||||
display: 'pl-1.5'
|
||||
}"
|
||||
data-property="opacity"
|
||||
@update:model-value="emit('update:opacity', Math.max(0, Math.min(1, $event / 100)))"
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ function update(value: string) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<PanelGrid v-if="visible" columns="fill" class="mb-1.5">
|
||||
<PanelGrid v-if="visible" class="mb-1.5">
|
||||
<PanelFieldGroup :label="label">
|
||||
<AppSelect
|
||||
:model-value="styleId === MIXED ? 'MIXED' : (styleId ?? 'NONE')"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import SegmentedControl from '@/components/ui/SegmentedControl.vue'
|
|||
import PanelFieldGroup from './PanelFieldGroup.vue'
|
||||
import PanelGrid from './PanelGrid.vue'
|
||||
import PanelHeader from './PanelHeader.vue'
|
||||
import PanelRail from './PanelRail.vue'
|
||||
import PanelSection from './PanelSection.vue'
|
||||
|
||||
const meta = {
|
||||
|
|
@ -47,7 +46,6 @@ export const StateMatrix: Story = {
|
|||
PanelFieldGroup,
|
||||
PanelGrid,
|
||||
PanelHeader,
|
||||
PanelRail,
|
||||
PanelSection,
|
||||
RotateIcon,
|
||||
SegmentedControl,
|
||||
|
|
@ -98,30 +96,30 @@ export const StateMatrix: Story = {
|
|||
<template #actions>
|
||||
<IconButton label="Reset layout"><RotateIcon class="size-3.5" /></IconButton>
|
||||
</template>
|
||||
<PanelGrid columns="two-rail">
|
||||
<PanelGrid :columns="2">
|
||||
<PanelFieldGroup label="Width">
|
||||
<AppInput v-model="width" tone="panel" data-story-control data-state="idle" aria-label="Width" />
|
||||
</PanelFieldGroup>
|
||||
<PanelFieldGroup label="Height">
|
||||
<AppInput v-model="height" tone="panel" data-story-control data-state="focus" aria-label="Height" />
|
||||
</PanelFieldGroup>
|
||||
<PanelRail>
|
||||
<template #actions>
|
||||
<IconButton label="Constrain proportions" size="md"><LinkIcon class="size-3.5" /></IconButton>
|
||||
</PanelRail>
|
||||
</template>
|
||||
</PanelGrid>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection label="Appearance">
|
||||
<PanelGrid columns="two-rail">
|
||||
<PanelGrid :columns="2">
|
||||
<PanelFieldGroup label="Blend mode">
|
||||
<AppSelect v-model="blendMode" :options="blendModes" data-story-control aria-label="Blend mode" />
|
||||
</PanelFieldGroup>
|
||||
<PanelFieldGroup label="Opacity">
|
||||
<AppInput v-model="mixed" tone="panel" state="mixed" readonly data-story-control aria-label="Mixed opacity" />
|
||||
</PanelFieldGroup>
|
||||
<PanelRail>
|
||||
<template #actions>
|
||||
<IconButton label="Toggle visibility"><EyeIcon class="size-3.5" /></IconButton>
|
||||
</PanelRail>
|
||||
</template>
|
||||
</PanelGrid>
|
||||
</PanelSection>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +1,36 @@
|
|||
<script lang="ts">
|
||||
import type { VNode } from 'vue'
|
||||
import type { PropertyGridRootSlots } from '@open-pencil/vue'
|
||||
import type { ClassValue } from 'tailwind-variants'
|
||||
|
||||
import type { PanelGridTheme } from '@/theme/panel/grid'
|
||||
type PanelGridColumns = keyof PanelGridTheme['variants']['columns']
|
||||
|
||||
export interface PanelGridProps {
|
||||
columns?: PanelGridColumns
|
||||
columns?: 1 | 2 | 3
|
||||
distribution?: 'equal' | 'wide-first'
|
||||
class?: ClassValue
|
||||
}
|
||||
|
||||
export interface PanelGridSlots {
|
||||
default(): VNode[]
|
||||
}
|
||||
export type PanelGridSlots = PropertyGridRootSlots
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropertyGridRoot } from '@open-pencil/vue'
|
||||
import { tv } from 'tailwind-variants'
|
||||
|
||||
import theme from '@/theme/panel/grid'
|
||||
|
||||
const { columns = 'two-rail', class: className } = defineProps<PanelGridProps>()
|
||||
const { columns = 1, distribution = 'equal', class: className } = defineProps<PanelGridProps>()
|
||||
defineSlots<PanelGridSlots>()
|
||||
const panelGrid = tv(theme)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="root"
|
||||
data-panel-grid
|
||||
:data-columns="columns"
|
||||
:class="panelGrid({ columns, class: className })"
|
||||
<PropertyGridRoot
|
||||
:columns="columns"
|
||||
:distribution="distribution"
|
||||
:class="panelGrid({ columns, distribution, class: className })"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
<template v-if="$slots.actions" #actions>
|
||||
<slot name="actions" />
|
||||
</template>
|
||||
</PropertyGridRoot>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export interface PanelItemRowProps {
|
|||
export interface PanelItemRowSlots {
|
||||
default(): VNode[]
|
||||
rail?(props: { removeClass: string }): VNode[]
|
||||
details?(): VNode[]
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -37,5 +38,8 @@ const styles = computed(() => tv(itemRowTheme)())
|
|||
<div v-if="$slots.rail" :class="styles.rail({ class: ui?.rail })" data-slot="rail">
|
||||
<slot name="rail" :remove-class="styles.remove({ class: ui?.remove })" />
|
||||
</div>
|
||||
<div v-if="$slots.details" :class="styles.details({ class: ui?.details })" data-slot="details">
|
||||
<slot name="details" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { VNode } from 'vue'
|
||||
import type { ClassValue } from 'tailwind-variants'
|
||||
|
||||
export interface PanelRailProps {
|
||||
class?: ClassValue
|
||||
}
|
||||
|
||||
export interface PanelRailSlots {
|
||||
default(): VNode[]
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { tv } from 'tailwind-variants'
|
||||
|
||||
import theme from '@/theme/panel/rail'
|
||||
|
||||
const { class: className } = defineProps<PanelRailProps>()
|
||||
defineSlots<PanelRailSlots>()
|
||||
const panelRail = tv(theme)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-slot="root" data-panel-rail :class="panelRail({ class: className })">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -2,5 +2,4 @@ export { default as PanelFieldGroup } from './PanelFieldGroup.vue'
|
|||
export { default as PanelGrid } from './PanelGrid.vue'
|
||||
export { default as PanelHeader } from './PanelHeader.vue'
|
||||
export { default as PanelItemRow } from './PanelItemRow.vue'
|
||||
export { default as PanelRail } from './PanelRail.vue'
|
||||
export { default as PanelSection } from './PanelSection.vue'
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import { CHECKERBOARD_BACKGROUND } from '@/theme/checkerboard'
|
|||
export default {
|
||||
slots: {
|
||||
root: 'flex items-center gap-2',
|
||||
label: 'w-4 shrink-0 text-[10px] font-medium text-muted',
|
||||
label: 'w-7 shrink-0 text-[10px] font-medium text-muted',
|
||||
slider: 'relative flex h-3 flex-1 touch-none items-center rounded-md select-none',
|
||||
track: 'absolute inset-0 overflow-hidden rounded-md',
|
||||
thumb:
|
||||
'block size-3.5 rounded-full border-2 border-white shadow-sm outline-none ring-offset-1 focus-visible:ring-2 focus-visible:ring-primary',
|
||||
input: 'w-14 shrink-0'
|
||||
input: 'w-14 flex-none shrink-0'
|
||||
},
|
||||
variants: {
|
||||
checkerboard: {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
export default {
|
||||
slots: {
|
||||
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',
|
||||
opacity: 'h-full w-14 shrink-0',
|
||||
preview: 'flex shrink-0 items-center pl-1',
|
||||
value: 'flex min-w-0 flex-1 items-center pl-1.5 pr-1',
|
||||
divider: 'h-4 w-px shrink-0 bg-muted/40',
|
||||
opacity: 'h-full w-12 flex-none shrink-0',
|
||||
binding: 'flex shrink-0 items-center pr-0.5'
|
||||
}
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -1,17 +1,24 @@
|
|||
const panelGridTheme = {
|
||||
base: 'grid min-w-0 items-end gap-1.5',
|
||||
base: [
|
||||
'flex min-w-0 items-end gap-1.5',
|
||||
'[&>[data-slot=fields]]:grid [&>[data-slot=fields]]:min-w-0 [&>[data-slot=fields]]:flex-1 [&>[data-slot=fields]]:items-end [&>[data-slot=fields]]:gap-1.5',
|
||||
'[&>[data-slot=actions]]:flex [&>[data-slot=actions]]:h-6 [&>[data-slot=actions]]:min-w-[26px] [&>[data-slot=actions]]:shrink-0 [&>[data-slot=actions]]:items-center [&>[data-slot=actions]]:justify-end [&>[data-slot=actions]]:gap-0.5',
|
||||
'[&>[data-slot=actions]_[data-slot=icon-button]]:size-6 [&>[data-slot=actions]_[data-slot=icon-button]]:rounded'
|
||||
],
|
||||
variants: {
|
||||
columns: {
|
||||
two: 'grid-cols-2',
|
||||
three: 'grid-cols-3',
|
||||
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)_26px]'
|
||||
1: '[&>[data-slot=fields]]:grid-cols-1',
|
||||
2: '[&>[data-slot=fields]]:grid-cols-2',
|
||||
3: '[&>[data-slot=fields]]:grid-cols-3'
|
||||
},
|
||||
distribution: {
|
||||
equal: '',
|
||||
'wide-first': '[&>[data-slot=fields]]:grid-cols-[minmax(0,7fr)_minmax(0,5fr)]'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
columns: 'two-rail' as const
|
||||
columns: 1 as const,
|
||||
distribution: 'equal' as const
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
export default {
|
||||
slots: {
|
||||
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',
|
||||
root: 'group grid min-h-6 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-1.5 gap-y-1.5 py-0.5',
|
||||
content: 'flex min-w-0 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'
|
||||
details: 'col-span-2 min-w-0',
|
||||
remove: ''
|
||||
}
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export default {
|
||||
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'
|
||||
}
|
||||
|
|
@ -89,9 +89,19 @@ test('fill section appears with default fill', async () => {
|
|||
await expect(fillItems.first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('fill item shows color swatch', async () => {
|
||||
test('fill item shows color swatch and full hex value', async () => {
|
||||
const swatch = fillSection().getByTestId('fill-picker-swatch').first()
|
||||
await expect(swatch).toBeVisible()
|
||||
|
||||
const fillItem = propertyItems(editor.page, 'fills').first()
|
||||
await expect(fillItem.getByRole('button', { name: 'Remove fill' })).toBeVisible()
|
||||
const hex = fillItem.locator('[data-property="color-hex"]')
|
||||
const metrics = await hex.evaluate((element) => ({
|
||||
fits: element.scrollWidth <= element.clientWidth,
|
||||
clientWidth: element.clientWidth,
|
||||
scrollWidth: element.scrollWidth
|
||||
}))
|
||||
expect(metrics.fits, JSON.stringify(metrics)).toBe(true)
|
||||
})
|
||||
|
||||
test('clicking color area changes fill color', async () => {
|
||||
|
|
@ -129,6 +139,9 @@ test('adding a stroke creates stroke section item', async () => {
|
|||
|
||||
const strokeItems = propertyItems(editor.page, 'strokes')
|
||||
await expect(strokeItems.first()).toBeVisible()
|
||||
await expect(strokeItems.first().getByRole('button', { name: 'Remove stroke' })).toBeVisible()
|
||||
const hex = strokeItems.first().locator('[data-property="color-hex"]')
|
||||
expect(await hex.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true)
|
||||
|
||||
const id = await getSelectedId()
|
||||
const node = await getNode(expectDefined(id, 'selected id'))
|
||||
|
|
@ -148,7 +161,7 @@ test('adding an effect creates effect item', async () => {
|
|||
expect(expectDefined(node, 'node node').effects.length).toBe(1)
|
||||
})
|
||||
|
||||
test('effect settings expand semantically and row remove reveals on hover', async () => {
|
||||
test('effect settings expand semantically and row remove stays visible', async () => {
|
||||
const effectItem = propertyItems(editor.page, 'effects').first()
|
||||
const expand = effectItem.locator('[data-property="effect-expand"]')
|
||||
await expect(expand).toHaveAttribute('aria-expanded', 'false')
|
||||
|
|
@ -160,8 +173,6 @@ test('effect settings expand semantically and row remove reveals on hover', asyn
|
|||
await expect(remove).toHaveCSS('opacity', '1')
|
||||
await editor.page.getByRole('tab', { name: 'Design' }).focus()
|
||||
await editor.page.mouse.move(0, 0)
|
||||
await expect(remove).toHaveCSS('opacity', '0')
|
||||
await effectItem.hover()
|
||||
await expect(remove).toHaveCSS('opacity', '1')
|
||||
})
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
|
|
@ -50,7 +50,8 @@ test('Shift+A wraps selection in auto-layout frame', async () => {
|
|||
expect(expectDefined(node, 'node').childIds.length).toBe(2)
|
||||
|
||||
frameId = expectDefined(node, 'node').id
|
||||
await expect(propertySection(page, 'Layout')).toHaveScreenshot('layout-size-controls.png')
|
||||
await expect(propertySection(page, 'Auto layout')).toHaveScreenshot('layout-size-controls.png')
|
||||
await expect(propertySection(page, 'Layout')).toHaveCount(0)
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
|
|
@ -158,7 +159,7 @@ test('padding controls set horizontal and vertical padding pairs', async () => {
|
|||
test('size dropdown adds and removes min width', async () => {
|
||||
await selectFrame()
|
||||
|
||||
const layout = propertySection(page, 'Layout')
|
||||
const layout = propertySection(page, 'Auto layout')
|
||||
await propertyField(page, 'width').getByRole('combobox', { name: 'Width' }).click()
|
||||
await page.getByRole('option', { name: 'Add min width' }).click()
|
||||
await canvas.waitForRender()
|
||||
|
|
@ -264,15 +265,36 @@ test('alignment grid center sets CENTER alignment', async () => {
|
|||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
test('remove auto-layout sets layoutMode to NONE', async () => {
|
||||
test('Freeform disables auto layout and a flow option restores it', async () => {
|
||||
await selectFrame()
|
||||
|
||||
await propertySection(page, 'Auto layout')
|
||||
.getByRole('button', { name: 'Remove auto layout' })
|
||||
.click()
|
||||
await propertySection(page, 'Auto layout').getByRole('button', { name: 'Freeform' }).click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const frame = await getNodeById(page, frameId)
|
||||
let frame = await getNodeById(page, frameId)
|
||||
expect(expectDefined(frame, 'frame').layoutMode).toBe('NONE')
|
||||
const layout = propertySection(page, 'Layout')
|
||||
await expect(layout).toBeVisible()
|
||||
await expect(layout.getByRole('button', { name: 'Add auto layout' })).toHaveAttribute(
|
||||
'data-state',
|
||||
'off'
|
||||
)
|
||||
await expect(layout.getByText('Flow', { exact: true })).toBeVisible()
|
||||
await expect(layout.getByRole('checkbox', { name: 'Clip content' })).toBeVisible()
|
||||
await expect(layout.getByRole('button', { name: 'Freeform' })).toHaveAttribute('data-state', 'on')
|
||||
await expect(propertySection(page, 'Auto layout')).toHaveCount(0)
|
||||
await expect(layout).toHaveScreenshot('layout-freeform-controls.png')
|
||||
|
||||
await layout.getByRole('button', { name: 'Vertical layout' }).click()
|
||||
await canvas.waitForRender()
|
||||
frame = await getNodeById(page, frameId)
|
||||
expect(expectDefined(frame, 'frame').layoutMode).toBe('VERTICAL')
|
||||
const autoLayout = propertySection(page, 'Auto layout')
|
||||
await expect(autoLayout).toBeVisible()
|
||||
await expect(autoLayout.getByRole('button', { name: 'Remove auto layout' })).toHaveAttribute(
|
||||
'data-state',
|
||||
'on'
|
||||
)
|
||||
await expect(propertySection(page, 'Layout')).toHaveCount(0)
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 6 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
|
@ -1,7 +1,7 @@
|
|||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||
import { propertySection } from '#tests/helpers/properties'
|
||||
import { propertyItems, propertySection } from '#tests/helpers/properties'
|
||||
|
||||
let page: Page
|
||||
let canvas: CanvasHelper
|
||||
|
|
@ -114,7 +114,7 @@ test('applies fill, stroke, effect, and grid styles from local definitions', asy
|
|||
await chooseStyle('Fill', 'Fill style', 'Brand/Primary')
|
||||
await chooseStyle('Stroke', 'Stroke style', 'Brand/Primary')
|
||||
await chooseStyle('Effects', 'Effect style', 'Effects/Card')
|
||||
await chooseStyle('Layout', 'Grid style', 'Grid/12 columns')
|
||||
await chooseStyle('Layout guide', 'Grid style', 'Grid/12 columns')
|
||||
|
||||
expect(await targetStyles()).toMatchObject({
|
||||
fillStyleId: '1:100',
|
||||
|
|
@ -128,6 +128,7 @@ test('applies fill, stroke, effect, and grid styles from local definitions', asy
|
|||
})
|
||||
|
||||
test('manual paint edits detach the style and undo restores the reference', async () => {
|
||||
await propertyItems(page, 'fills').first().hover()
|
||||
await propertySection(page, 'Fill').getByRole('button', { name: 'Remove fill' }).click()
|
||||
await canvas.waitForRender()
|
||||
expect((await targetStyles())?.fillStyleId).toBeNull()
|
||||
|
|
|
|||