feat(app): finish Design panel migration
- Rebuild Typography, Variant, Mask, and Page sections with shared labeled fields - Expose semantic empty states across list sections - Replace section test hooks with accessible roles and add focused visual coverage
This commit is contained in:
parent
5371ed11b8
commit
27fdef2ea4
|
|
@ -23,6 +23,7 @@
|
||||||
- Replace the fragmented Vue SDK color-picker model helpers with `useColorModel()`, providing precise Reka bridges, extensible formats, and shared RGB, HSL, HSB, and OkHCL channel behavior.
|
- Replace the fragmented Vue SDK color-picker model helpers with `useColorModel()`, providing precise Reka bridges, extensible formats, and shared RGB, HSL, HSB, and OkHCL channel behavior.
|
||||||
- Add accessible color-channel sliders, binding-aware fill primitives, and keyboard-operable gradient stops while separating fill state from popover composition.
|
- Add accessible color-channel sliders, binding-aware fill primitives, and keyboard-operable gradient stops while separating fill state from popover composition.
|
||||||
- Rebuild Fill, Stroke, Effects, and Export controls with shared compact item rows, semantic actions, binding-aware paint fields, and reversible color-picker edits.
|
- Rebuild Fill, Stroke, Effects, and Export controls with shared compact item rows, semantic actions, binding-aware paint fields, and reversible color-picker edits.
|
||||||
|
- Finish the Design panel migration with compact labeled Typography, Variant, Mask, and Page controls plus semantic empty-section states.
|
||||||
- Upgrade Vue SDK documentation with shared Tailwind demos, source-generated component API tables, and type-aware Twoslash examples in VitePress.
|
- Upgrade Vue SDK documentation with shared Tailwind demos, source-generated component API tables, and type-aware Twoslash examples in VitePress.
|
||||||
- Add desktop image drag-and-drop into the Tauri app window.
|
- Add desktop image drag-and-drop into the Tauri app window.
|
||||||
- Add open-document discovery for live CLI and MCP automation so agents can target the intended document and page.
|
- Add open-document discovery for live CLI and MCP automation so agents can target the intended document and page.
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@
|
||||||
"effects": "Effekte",
|
"effects": "Effekte",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"typography": "Typografie",
|
"typography": "Typografie",
|
||||||
|
"fontFamily": "Schriftfamilie",
|
||||||
|
"fontWeight": "Schriftstärke",
|
||||||
|
"fontSize": "Schriftgröße",
|
||||||
|
"lineHeight": "Zeilenhöhe",
|
||||||
|
"letterSpacing": "Zeichenabstand",
|
||||||
|
"textAlignment": "Textausrichtung",
|
||||||
|
"textFormatting": "Textformatierung",
|
||||||
|
"pageBackground": "Seitenhintergrund",
|
||||||
"variables": "Variablen",
|
"variables": "Variablen",
|
||||||
"constraints": "Einschränkungen",
|
"constraints": "Einschränkungen",
|
||||||
"addFill": "Füllung hinzufügen",
|
"addFill": "Füllung hinzufügen",
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,14 @@
|
||||||
"effects": "Efectos",
|
"effects": "Efectos",
|
||||||
"export": "Exportar",
|
"export": "Exportar",
|
||||||
"typography": "Tipografía",
|
"typography": "Tipografía",
|
||||||
|
"fontFamily": "Familia tipográfica",
|
||||||
|
"fontWeight": "Peso de fuente",
|
||||||
|
"fontSize": "Tamaño de fuente",
|
||||||
|
"lineHeight": "Altura de línea",
|
||||||
|
"letterSpacing": "Espaciado entre letras",
|
||||||
|
"textAlignment": "Alineación del texto",
|
||||||
|
"textFormatting": "Formato de texto",
|
||||||
|
"pageBackground": "Fondo de página",
|
||||||
"variables": "Variables",
|
"variables": "Variables",
|
||||||
"constraints": "Restricciones",
|
"constraints": "Restricciones",
|
||||||
"addFill": "Añadir relleno",
|
"addFill": "Añadir relleno",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@
|
||||||
"effects": "Effets",
|
"effects": "Effets",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"typography": "Typographie",
|
"typography": "Typographie",
|
||||||
|
"fontFamily": "Famille de police",
|
||||||
|
"fontWeight": "Graisse",
|
||||||
|
"fontSize": "Taille de police",
|
||||||
|
"lineHeight": "Hauteur de ligne",
|
||||||
|
"letterSpacing": "Espacement des lettres",
|
||||||
|
"textAlignment": "Alignement du texte",
|
||||||
|
"textFormatting": "Mise en forme du texte",
|
||||||
|
"pageBackground": "Arrière-plan de la page",
|
||||||
"variables": "Variables",
|
"variables": "Variables",
|
||||||
"constraints": "Contraintes",
|
"constraints": "Contraintes",
|
||||||
"addFill": "Ajouter un remplissage",
|
"addFill": "Ajouter un remplissage",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@
|
||||||
"effects": "Effetti",
|
"effects": "Effetti",
|
||||||
"export": "Esporta",
|
"export": "Esporta",
|
||||||
"typography": "Tipografia",
|
"typography": "Tipografia",
|
||||||
|
"fontFamily": "Famiglia di caratteri",
|
||||||
|
"fontWeight": "Peso del carattere",
|
||||||
|
"fontSize": "Dimensione del carattere",
|
||||||
|
"lineHeight": "Altezza riga",
|
||||||
|
"letterSpacing": "Spaziatura lettere",
|
||||||
|
"textAlignment": "Allineamento testo",
|
||||||
|
"textFormatting": "Formattazione testo",
|
||||||
|
"pageBackground": "Sfondo pagina",
|
||||||
"variables": "Variabili",
|
"variables": "Variabili",
|
||||||
"constraints": "Vincoli",
|
"constraints": "Vincoli",
|
||||||
"addFill": "Aggiungi riempimento",
|
"addFill": "Aggiungi riempimento",
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,14 @@
|
||||||
"effects": "エフェクト",
|
"effects": "エフェクト",
|
||||||
"export": "エクスポート",
|
"export": "エクスポート",
|
||||||
"typography": "タイポグラフィ",
|
"typography": "タイポグラフィ",
|
||||||
|
"fontFamily": "フォントファミリー",
|
||||||
|
"fontWeight": "フォントの太さ",
|
||||||
|
"fontSize": "フォントサイズ",
|
||||||
|
"lineHeight": "行の高さ",
|
||||||
|
"letterSpacing": "文字間隔",
|
||||||
|
"textAlignment": "テキストの配置",
|
||||||
|
"textFormatting": "テキストの書式",
|
||||||
|
"pageBackground": "ページの背景",
|
||||||
"variables": "変数",
|
"variables": "変数",
|
||||||
"constraints": "制約",
|
"constraints": "制約",
|
||||||
"addFill": "塗りを追加",
|
"addFill": "塗りを追加",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@
|
||||||
"effects": "Efekty",
|
"effects": "Efekty",
|
||||||
"export": "Eksport",
|
"export": "Eksport",
|
||||||
"typography": "Typografia",
|
"typography": "Typografia",
|
||||||
|
"fontFamily": "Rodzina czcionek",
|
||||||
|
"fontWeight": "Grubość czcionki",
|
||||||
|
"fontSize": "Rozmiar czcionki",
|
||||||
|
"lineHeight": "Wysokość wiersza",
|
||||||
|
"letterSpacing": "Odstępy między literami",
|
||||||
|
"textAlignment": "Wyrównanie tekstu",
|
||||||
|
"textFormatting": "Formatowanie tekstu",
|
||||||
|
"pageBackground": "Tło strony",
|
||||||
"variables": "Zmienne",
|
"variables": "Zmienne",
|
||||||
"constraints": "Ograniczenia",
|
"constraints": "Ograniczenia",
|
||||||
"addFill": "Dodaj wypełnienie",
|
"addFill": "Dodaj wypełnienie",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@
|
||||||
"effects": "Эффекты",
|
"effects": "Эффекты",
|
||||||
"export": "Экспорт",
|
"export": "Экспорт",
|
||||||
"typography": "Типографика",
|
"typography": "Типографика",
|
||||||
|
"fontFamily": "Гарнитура шрифта",
|
||||||
|
"fontWeight": "Насыщенность шрифта",
|
||||||
|
"fontSize": "Размер шрифта",
|
||||||
|
"lineHeight": "Высота строки",
|
||||||
|
"letterSpacing": "Межбуквенный интервал",
|
||||||
|
"textAlignment": "Выравнивание текста",
|
||||||
|
"textFormatting": "Форматирование текста",
|
||||||
|
"pageBackground": "Фон страницы",
|
||||||
"variables": "Переменные",
|
"variables": "Переменные",
|
||||||
"constraints": "Ограничения",
|
"constraints": "Ограничения",
|
||||||
"addFill": "Добавить заливку",
|
"addFill": "Добавить заливку",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@
|
||||||
"effects": "效果",
|
"effects": "效果",
|
||||||
"export": "导出",
|
"export": "导出",
|
||||||
"typography": "排版",
|
"typography": "排版",
|
||||||
|
"fontFamily": "字体",
|
||||||
|
"fontWeight": "字重",
|
||||||
|
"fontSize": "字号",
|
||||||
|
"lineHeight": "行高",
|
||||||
|
"letterSpacing": "字间距",
|
||||||
|
"textAlignment": "文本对齐",
|
||||||
|
"textFormatting": "文本格式",
|
||||||
|
"pageBackground": "页面背景",
|
||||||
"variables": "变量",
|
"variables": "变量",
|
||||||
"constraints": "约束",
|
"constraints": "约束",
|
||||||
"addFill": "添加填充",
|
"addFill": "添加填充",
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,14 @@ export const panelMessageDefaults = {
|
||||||
mask: 'Mask',
|
mask: 'Mask',
|
||||||
export: 'Export',
|
export: 'Export',
|
||||||
typography: 'Typography',
|
typography: 'Typography',
|
||||||
|
fontFamily: 'Font family',
|
||||||
|
fontWeight: 'Font weight',
|
||||||
|
fontSize: 'Font size',
|
||||||
|
lineHeight: 'Line height',
|
||||||
|
letterSpacing: 'Letter spacing',
|
||||||
|
textAlignment: 'Text alignment',
|
||||||
|
textFormatting: 'Text formatting',
|
||||||
|
pageBackground: 'Page background',
|
||||||
variables: 'Variables',
|
variables: 'Variables',
|
||||||
variants: 'Variants',
|
variants: 'Variants',
|
||||||
constraints: 'Constraints',
|
constraints: 'Constraints',
|
||||||
|
|
|
||||||
|
|
@ -88,14 +88,14 @@ const { panels } = useI18n()
|
||||||
class="flex flex-col gap-1 border-b border-border px-3 py-2"
|
class="flex flex-col gap-1 border-b border-border px-3 py-2"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
data-test-id="design-go-to-component"
|
type="button"
|
||||||
class="rounded bg-component/10 px-2 py-1 text-left text-[11px] text-component hover:bg-component/20"
|
class="rounded bg-component/10 px-2 py-1 text-left text-[11px] text-component hover:bg-component/20"
|
||||||
@click="goToMainComponent.run()"
|
@click="goToMainComponent.run()"
|
||||||
>
|
>
|
||||||
{{ panels.goToMainComponent }}
|
{{ panels.goToMainComponent }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
data-test-id="design-detach-instance"
|
type="button"
|
||||||
class="rounded px-2 py-1 text-left text-[11px] text-muted hover:bg-hover"
|
class="rounded px-2 py-1 text-left text-[11px] text-muted hover:bg-hover"
|
||||||
@click="detachInstance.run()"
|
@click="detachInstance.run()"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { WEB_FONT_PROVIDER_IDS } from '@open-pencil/core/text'
|
||||||
|
|
||||||
import type { FontPickerUI } from '@open-pencil/vue'
|
import type { FontPickerUI } from '@open-pencil/vue'
|
||||||
|
|
||||||
|
const { label = 'Font family' } = defineProps<{ label?: string }>()
|
||||||
const modelValue = defineModel<string>({ required: true })
|
const modelValue = defineModel<string>({ required: true })
|
||||||
const emit = defineEmits<{ select: [family: string] }>()
|
const emit = defineEmits<{ select: [family: string] }>()
|
||||||
|
|
||||||
|
|
@ -62,7 +63,12 @@ function loadPreviewFont(family: string, source: string) {
|
||||||
@select="emit('select', $event)"
|
@select="emit('select', $event)"
|
||||||
>
|
>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<button data-test-id="font-picker-trigger" :class="selectCls.trigger">
|
<button
|
||||||
|
type="button"
|
||||||
|
data-test-id="font-picker-trigger"
|
||||||
|
:aria-label="label"
|
||||||
|
:class="selectCls.trigger"
|
||||||
|
>
|
||||||
<span class="truncate">{{ modelValue }}</span>
|
<span class="truncate">{{ modelValue }}</span>
|
||||||
<icon-lucide-chevron-down class="size-3 shrink-0 text-muted" />
|
<icon-lucide-chevron-down class="size-3 shrink-0 text-muted" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ function effectPreview(effect: Effect): Fill {
|
||||||
prop-key="effects"
|
prop-key="effects"
|
||||||
:label="panels.effects"
|
:label="panels.effects"
|
||||||
>
|
>
|
||||||
<PanelSection :label="panels.effects">
|
<PanelSection :label="panels.effects" :empty="!isMixed && items.length === 0">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<IconButton
|
<IconButton
|
||||||
:label="panels.addEffect"
|
:label="panels.addEffect"
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ watch(previewKey, updatePreview, { flush: 'post' })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PanelSection :label="panels.export">
|
<PanelSection :label="panels.export" :empty="activeSettings.length === 0">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<IconButton :label="panels.addExport" @click="addSetting">
|
<IconButton :label="panels.addExport" @click="addSetting">
|
||||||
<icon-lucide-plus class="size-3.5" />
|
<icon-lucide-plus class="size-3.5" />
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ function updateSolidColor(
|
||||||
prop-key="fills"
|
prop-key="fills"
|
||||||
:label="panels.fill"
|
:label="panels.fill"
|
||||||
>
|
>
|
||||||
<PanelSection :label="panels.fill">
|
<PanelSection :label="panels.fill" :empty="!isMixed && items.length === 0">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<IconButton :label="panels.addFill" @click="actions.add({ ...fillCtx.defaultFill })">
|
<IconButton :label="panels.addFill" @click="actions.add({ ...fillCtx.defaultFill })">
|
||||||
<icon-lucide-plus class="size-3.5" />
|
<icon-lucide-plus class="size-3.5" />
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import { computed } from 'vue'
|
||||||
import { useI18n, useMask } from '@open-pencil/vue'
|
import { useI18n, useMask } from '@open-pencil/vue'
|
||||||
|
|
||||||
import AppSelect from '@/components/ui/AppSelect.vue'
|
import AppSelect from '@/components/ui/AppSelect.vue'
|
||||||
|
import PanelFieldGroup from '@/components/ui/panel/PanelFieldGroup.vue'
|
||||||
import PanelSection from '@/components/ui/panel/PanelSection.vue'
|
import PanelSection from '@/components/ui/panel/PanelSection.vue'
|
||||||
import Tip from '@/components/ui/Tip.vue'
|
|
||||||
|
|
||||||
import type { MaskType } from '@open-pencil/scene-graph'
|
import type { MaskType } from '@open-pencil/scene-graph'
|
||||||
|
|
||||||
|
|
@ -25,15 +25,14 @@ const selectedMaskType = computed<MaskType>({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PanelSection v-if="active" :label="panels.mask" data-test-id="mask-section">
|
<PanelSection v-if="active" :label="panels.mask">
|
||||||
<Tip :label="panels.maskType">
|
<PanelFieldGroup :label="panels.maskType">
|
||||||
<AppSelect
|
<AppSelect
|
||||||
class="w-full"
|
|
||||||
:label="panels.maskType"
|
|
||||||
v-model="selectedMaskType"
|
v-model="selectedMaskType"
|
||||||
|
:label="panels.maskType"
|
||||||
:options="maskTypeOptions"
|
:options="maskTypeOptions"
|
||||||
data-test-id="mask-type-select"
|
data-property="mask-type"
|
||||||
/>
|
/>
|
||||||
</Tip>
|
</PanelFieldGroup>
|
||||||
</PanelSection>
|
</PanelSection>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,58 @@
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import { useI18n } from '@open-pencil/vue'
|
import { useI18n } from '@open-pencil/vue'
|
||||||
import ColorInput from '@/components/ColorPicker/ColorInput.vue'
|
|
||||||
import PanelSection from '@/components/ui/panel/PanelSection.vue'
|
|
||||||
|
|
||||||
|
import ColorPicker from '@/components/ColorPicker/ColorPicker.vue'
|
||||||
|
import PaintField from '@/components/properties/paint/PaintField.vue'
|
||||||
|
import PaintValue from '@/components/properties/paint/PaintValue.vue'
|
||||||
|
import FillSwatch from '@/components/ui/FillSwatch.vue'
|
||||||
|
import PanelSection from '@/components/ui/panel/PanelSection.vue'
|
||||||
import { useEditorStore } from '@/app/editor/active-store'
|
import { useEditorStore } from '@/app/editor/active-store'
|
||||||
|
|
||||||
|
import type { Color, Fill } from '@open-pencil/scene-graph'
|
||||||
|
|
||||||
const editor = useEditorStore()
|
const editor = useEditorStore()
|
||||||
const pageColor = computed(() => editor.state.pageColor)
|
const pageColor = computed(() => editor.state.pageColor)
|
||||||
|
const pageFill = computed<Fill>(() => ({
|
||||||
|
type: 'SOLID',
|
||||||
|
color: pageColor.value,
|
||||||
|
opacity: 1,
|
||||||
|
visible: true
|
||||||
|
}))
|
||||||
const { panels } = useI18n()
|
const { panels } = useI18n()
|
||||||
|
|
||||||
|
function updatePageAlpha(alpha: number) {
|
||||||
|
editor.setPageColor({ ...pageColor.value, a: alpha })
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePageColor(color: Color) {
|
||||||
|
editor.setPageColor(color)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PanelSection :label="panels.page ?? 'Page'" data-test-id="page-section">
|
<PanelSection :label="panels.page">
|
||||||
<ColorInput :color="pageColor" editable @update="editor.setPageColor($event)" />
|
<PaintField
|
||||||
|
:opacity="pageColor.a"
|
||||||
|
:opacity-label="panels.opacity"
|
||||||
|
@update:opacity="updatePageAlpha"
|
||||||
|
>
|
||||||
|
<template #preview>
|
||||||
|
<ColorPicker :color="pageColor" @update="updatePageColor">
|
||||||
|
<template #trigger>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:aria-label="panels.pageBackground"
|
||||||
|
class="size-5 shrink-0 cursor-pointer rounded border-0 bg-transparent p-0"
|
||||||
|
>
|
||||||
|
<FillSwatch :fill="pageFill" class="size-full" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</ColorPicker>
|
||||||
|
</template>
|
||||||
|
<template #value>
|
||||||
|
<PaintValue :color="pageColor" :label="panels.pageBackground" @update="updatePageColor" />
|
||||||
|
</template>
|
||||||
|
</PaintField>
|
||||||
</PanelSection>
|
</PanelSection>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ function onToggleSides(activeNode: SceneNode | null) {
|
||||||
prop-key="strokes"
|
prop-key="strokes"
|
||||||
:label="panels.stroke"
|
:label="panels.stroke"
|
||||||
>
|
>
|
||||||
<PanelSection :label="panels.stroke">
|
<PanelSection :label="panels.stroke" :empty="!isMixed && items.length === 0">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<IconButton :label="panels.addStroke" @click="actions.add(strokeCtx.defaultStroke)">
|
<IconButton :label="panels.addStroke" @click="actions.add(strokeCtx.defaultStroke)">
|
||||||
<icon-lucide-plus class="size-3.5" />
|
<icon-lucide-plus class="size-3.5" />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import { TypographyControlsRoot, useI18n } from '@open-pencil/vue'
|
import { TypographyControlsRoot, useI18n } from '@open-pencil/vue'
|
||||||
|
|
||||||
import FontPicker from '@/components/font-picker/FontPicker.vue'
|
import FontPicker from '@/components/font-picker/FontPicker.vue'
|
||||||
|
|
@ -6,27 +8,31 @@ import FontSettingsPopover from '@/components/FontSettings/FontSettingsPopover.v
|
||||||
import VariableNumberField from '@/components/properties/VariableNumberField.vue'
|
import VariableNumberField from '@/components/properties/VariableNumberField.vue'
|
||||||
import AppSelect from '@/components/ui/AppSelect.vue'
|
import AppSelect from '@/components/ui/AppSelect.vue'
|
||||||
import IconButton from '@/components/ui/IconButton.vue'
|
import IconButton from '@/components/ui/IconButton.vue'
|
||||||
import PanelRow from '@/components/ui/panel/PanelRow.vue'
|
import PanelFieldGroup from '@/components/ui/panel/PanelFieldGroup.vue'
|
||||||
|
import PanelGrid from '@/components/ui/panel/PanelGrid.vue'
|
||||||
import PanelSection from '@/components/ui/panel/PanelSection.vue'
|
import PanelSection from '@/components/ui/panel/PanelSection.vue'
|
||||||
|
import SegmentedControl from '@/components/ui/SegmentedControl.vue'
|
||||||
import Tip from '@/components/ui/Tip.vue'
|
import Tip from '@/components/ui/Tip.vue'
|
||||||
import { loadFont } from '@/app/editor/fonts'
|
import { loadFont } from '@/app/editor/fonts'
|
||||||
import { appMenuShortcutLabel } from '@/app/shell/menu/shortcut'
|
import { appMenuShortcutLabel } from '@/app/shell/menu/shortcut'
|
||||||
|
|
||||||
const { panels, menu } = useI18n()
|
const { panels, menu } = useI18n()
|
||||||
const fontLoader = { load: loadFont }
|
const fontLoader = { load: loadFont }
|
||||||
|
const alignmentOptions = computed(() => [
|
||||||
|
{ value: 'LEFT', label: panels.value.alignLeft },
|
||||||
|
{ value: 'CENTER', label: panels.value.alignCenterHorizontally },
|
||||||
|
{ value: 'RIGHT', label: panels.value.alignRight }
|
||||||
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TypographyControlsRoot v-slot="ctx" :font-loader="fontLoader">
|
<TypographyControlsRoot v-slot="ctx" :font-loader="fontLoader">
|
||||||
<PanelSection
|
<PanelSection v-if="ctx.node.value" :label="panels.typography">
|
||||||
v-if="ctx.node.value"
|
<div class="mb-panel flex min-w-0 items-center gap-panel">
|
||||||
:label="panels.typography"
|
|
||||||
data-test-id="typography-section"
|
|
||||||
>
|
|
||||||
<PanelRow class="mb-1.5">
|
|
||||||
<FontPicker
|
<FontPicker
|
||||||
class="min-w-0 flex-1"
|
class="min-w-0 flex-1"
|
||||||
:model-value="ctx.node.value.fontFamily"
|
:model-value="ctx.node.value.fontFamily"
|
||||||
|
:label="panels.fontFamily"
|
||||||
@select="ctx.actions.setFamily"
|
@select="ctx.actions.setFamily"
|
||||||
/>
|
/>
|
||||||
<FontSettingsPopover />
|
<FontSettingsPopover />
|
||||||
|
|
@ -40,64 +46,79 @@ const fontLoader = { load: loadFont }
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<icon-lucide-alert-triangle
|
<icon-lucide-alert-triangle
|
||||||
data-test-id="typography-missing-font"
|
role="img"
|
||||||
|
:aria-label="
|
||||||
|
'Missing font' +
|
||||||
|
(ctx.missingFonts.value.length > 1 ? 's' : '') +
|
||||||
|
': ' +
|
||||||
|
ctx.missingFonts.value.join(', ')
|
||||||
|
"
|
||||||
class="size-3.5 shrink-0 text-[var(--color-warning-action)]"
|
class="size-3.5 shrink-0 text-[var(--color-warning-action)]"
|
||||||
/>
|
/>
|
||||||
</Tip>
|
</Tip>
|
||||||
</PanelRow>
|
</div>
|
||||||
|
|
||||||
<PanelRow cols="two" class="mb-1.5">
|
<PanelGrid columns="two" class="mb-panel">
|
||||||
<AppSelect
|
<PanelFieldGroup :label="panels.fontWeight">
|
||||||
:model-value="ctx.node.value.fontWeight"
|
<AppSelect
|
||||||
:options="ctx.weights"
|
:label="panels.fontWeight"
|
||||||
@update:model-value="ctx.actions.setWeight(+$event)"
|
:model-value="ctx.node.value.fontWeight"
|
||||||
/>
|
:options="ctx.weights"
|
||||||
<VariableNumberField
|
@update:model-value="ctx.actions.setWeight(+$event)"
|
||||||
class="flex-1"
|
/>
|
||||||
:model-value="ctx.node.value.fontSize"
|
</PanelFieldGroup>
|
||||||
:min="1"
|
<PanelFieldGroup :label="panels.fontSize">
|
||||||
:max="1000"
|
<VariableNumberField
|
||||||
:node-id="ctx.node.value.id"
|
:model-value="ctx.node.value.fontSize"
|
||||||
binding-path="fontSize"
|
:aria-label="panels.fontSize"
|
||||||
@update:model-value="ctx.actions.updateProp('fontSize', $event)"
|
:min="1"
|
||||||
@commit="(v: number, p: number) => ctx.actions.commitProp('fontSize', v, p)"
|
:max="1000"
|
||||||
/>
|
:node-id="ctx.node.value.id"
|
||||||
</PanelRow>
|
binding-path="fontSize"
|
||||||
|
@update:model-value="ctx.actions.updateProp('fontSize', $event)"
|
||||||
<PanelRow cols="two" class="mb-1.5">
|
@commit="(v: number, p: number) => ctx.actions.commitProp('fontSize', v, p)"
|
||||||
<VariableNumberField
|
/>
|
||||||
class="flex-1"
|
</PanelFieldGroup>
|
||||||
:model-value="
|
</PanelGrid>
|
||||||
ctx.node.value.lineHeight ?? Math.round((ctx.node.value.fontSize || 14) * 1.2)
|
|
||||||
"
|
<PanelGrid columns="two" class="mb-panel">
|
||||||
:min="0"
|
<PanelFieldGroup :label="panels.lineHeight">
|
||||||
:node-id="ctx.node.value.id"
|
<VariableNumberField
|
||||||
binding-path="lineHeight"
|
:model-value="
|
||||||
@update:model-value="ctx.actions.updateProp('lineHeight', $event)"
|
ctx.node.value.lineHeight ?? Math.round((ctx.node.value.fontSize || 14) * 1.2)
|
||||||
@commit="(v: number, p: number) => ctx.actions.commitProp('lineHeight', v, p)"
|
"
|
||||||
>
|
:aria-label="panels.lineHeight"
|
||||||
<template #icon>
|
:min="0"
|
||||||
<icon-lucide-baseline class="size-3" />
|
:node-id="ctx.node.value.id"
|
||||||
</template>
|
binding-path="lineHeight"
|
||||||
</VariableNumberField>
|
@update:model-value="ctx.actions.updateProp('lineHeight', $event)"
|
||||||
<VariableNumberField
|
@commit="(v: number, p: number) => ctx.actions.commitProp('lineHeight', v, p)"
|
||||||
class="flex-1"
|
>
|
||||||
suffix="%"
|
<template #icon>
|
||||||
:model-value="ctx.node.value.letterSpacing"
|
<icon-lucide-baseline class="size-3" />
|
||||||
:node-id="ctx.node.value.id"
|
</template>
|
||||||
binding-path="letterSpacing"
|
</VariableNumberField>
|
||||||
@update:model-value="ctx.actions.updateProp('letterSpacing', $event)"
|
</PanelFieldGroup>
|
||||||
@commit="(v: number, p: number) => ctx.actions.commitProp('letterSpacing', v, p)"
|
<PanelFieldGroup :label="panels.letterSpacing">
|
||||||
>
|
<VariableNumberField
|
||||||
<template #icon>
|
suffix="%"
|
||||||
<icon-lucide-a-large-small class="size-3" />
|
:model-value="ctx.node.value.letterSpacing"
|
||||||
</template>
|
:aria-label="panels.letterSpacing"
|
||||||
</VariableNumberField>
|
:node-id="ctx.node.value.id"
|
||||||
</PanelRow>
|
binding-path="letterSpacing"
|
||||||
|
@update:model-value="ctx.actions.updateProp('letterSpacing', $event)"
|
||||||
<div class="mb-1.5">
|
@commit="(v: number, p: number) => ctx.actions.commitProp('letterSpacing', v, p)"
|
||||||
<label class="mb-1 block text-[11px] text-muted">{{ panels.direction }}</label>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<icon-lucide-a-large-small class="size-3" />
|
||||||
|
</template>
|
||||||
|
</VariableNumberField>
|
||||||
|
</PanelFieldGroup>
|
||||||
|
</PanelGrid>
|
||||||
|
|
||||||
|
<PanelFieldGroup :label="panels.direction" class="mb-panel">
|
||||||
<AppSelect
|
<AppSelect
|
||||||
|
:label="panels.direction"
|
||||||
:model-value="ctx.node.value.textDirection"
|
:model-value="ctx.node.value.textDirection"
|
||||||
:options="[
|
:options="[
|
||||||
{ value: 'AUTO', label: panels.auto },
|
{ value: 'AUTO', label: panels.auto },
|
||||||
|
|
@ -106,41 +127,29 @@ const fontLoader = { load: loadFont }
|
||||||
]"
|
]"
|
||||||
@update:model-value="ctx.actions.setDirection($event as 'AUTO' | 'LTR' | 'RTL')"
|
@update:model-value="ctx.actions.setDirection($event as 'AUTO' | 'LTR' | 'RTL')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</PanelFieldGroup>
|
||||||
|
|
||||||
<PanelRow class="gap-3">
|
<PanelFieldGroup :label="panels.textAlignment" class="mb-panel">
|
||||||
<PanelRow gap="sm">
|
<SegmentedControl
|
||||||
<IconButton
|
:model-value="ctx.node.value.textAlignHorizontal"
|
||||||
:label="panels.alignLeft"
|
:options="alignmentOptions"
|
||||||
size="md"
|
:label="panels.textAlignment"
|
||||||
:active="ctx.node.value.textAlignHorizontal === 'LEFT'"
|
@change="ctx.actions.align($event as 'LEFT' | 'CENTER' | 'RIGHT')"
|
||||||
@click="ctx.actions.align('LEFT')"
|
>
|
||||||
>
|
<template #option="{ option }">
|
||||||
<icon-lucide-align-left class="size-3.5" />
|
<icon-lucide-align-left v-if="option.value === 'LEFT'" class="size-3.5" />
|
||||||
</IconButton>
|
<icon-lucide-align-center v-else-if="option.value === 'CENTER'" class="size-3.5" />
|
||||||
<IconButton
|
<icon-lucide-align-right v-else class="size-3.5" />
|
||||||
:label="panels.alignCenterHorizontally"
|
</template>
|
||||||
size="md"
|
</SegmentedControl>
|
||||||
:active="ctx.node.value.textAlignHorizontal === 'CENTER'"
|
</PanelFieldGroup>
|
||||||
@click="ctx.actions.align('CENTER')"
|
|
||||||
>
|
<PanelFieldGroup :label="panels.textFormatting" :ui="{ container: 'flex-row gap-panel' }">
|
||||||
<icon-lucide-align-center class="size-3.5" />
|
<div class="flex items-center gap-panel" role="toolbar" :aria-label="panels.textFormatting">
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
:label="panels.alignRight"
|
|
||||||
size="md"
|
|
||||||
:active="ctx.node.value.textAlignHorizontal === 'RIGHT'"
|
|
||||||
@click="ctx.actions.align('RIGHT')"
|
|
||||||
>
|
|
||||||
<icon-lucide-align-right class="size-3.5" />
|
|
||||||
</IconButton>
|
|
||||||
</PanelRow>
|
|
||||||
<PanelRow gap="sm">
|
|
||||||
<IconButton
|
<IconButton
|
||||||
:label="`${menu.bold} (${appMenuShortcutLabel('text.bold')})`"
|
:label="`${menu.bold} (${appMenuShortcutLabel('text.bold')})`"
|
||||||
size="md"
|
size="md"
|
||||||
:active="ctx.activeFormatting.value.includes('bold')"
|
:active="ctx.activeFormatting.value.includes('bold')"
|
||||||
data-test-id="typography-bold-button"
|
|
||||||
@click="ctx.actions.toggleBold"
|
@click="ctx.actions.toggleBold"
|
||||||
>
|
>
|
||||||
<icon-lucide-bold class="size-3.5" />
|
<icon-lucide-bold class="size-3.5" />
|
||||||
|
|
@ -169,8 +178,8 @@ const fontLoader = { load: loadFont }
|
||||||
>
|
>
|
||||||
<icon-lucide-strikethrough class="size-3.5" />
|
<icon-lucide-strikethrough class="size-3.5" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</PanelRow>
|
</div>
|
||||||
</PanelRow>
|
</PanelFieldGroup>
|
||||||
</PanelSection>
|
</PanelSection>
|
||||||
</TypographyControlsRoot>
|
</TypographyControlsRoot>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,9 @@ const { panels } = useI18n()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PanelSection :label="panels.variables" data-test-id="variables-section">
|
<PanelSection :label="panels.variables" :empty="!hasVariables">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<IconButton
|
<IconButton :label="panels.openVariables" @click="emit('openDialog')">
|
||||||
:label="panels.openVariables"
|
|
||||||
data-test-id="variables-section-open"
|
|
||||||
@click="emit('openDialog')"
|
|
||||||
>
|
|
||||||
<icon-lucide-settings-2 class="size-3.5" />
|
<icon-lucide-settings-2 class="size-3.5" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { computed } from 'vue'
|
||||||
import { useI18n, useSelectionState } from '@open-pencil/vue'
|
import { useI18n, useSelectionState } from '@open-pencil/vue'
|
||||||
|
|
||||||
import AppSelect from '@/components/ui/AppSelect.vue'
|
import AppSelect from '@/components/ui/AppSelect.vue'
|
||||||
|
import PanelFieldGroup from '@/components/ui/panel/PanelFieldGroup.vue'
|
||||||
import PanelSection from '@/components/ui/panel/PanelSection.vue'
|
import PanelSection from '@/components/ui/panel/PanelSection.vue'
|
||||||
import { useEditorStore } from '@/app/editor/active-store'
|
import { useEditorStore } from '@/app/editor/active-store'
|
||||||
|
|
||||||
|
|
@ -42,25 +43,21 @@ function switchVariant(propertyName: string, newValue: string) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PanelSection
|
<PanelSection v-if="hasVariants" :label="panels.variants" :ui="{ title: 'text-component' }">
|
||||||
v-if="hasVariants"
|
<div class="flex flex-col gap-panel">
|
||||||
:label="panels.variants"
|
<PanelFieldGroup
|
||||||
data-test-id="variant-section"
|
|
||||||
:ui="{ title: 'text-component' }"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-1.5">
|
|
||||||
<div
|
|
||||||
v-for="[propName, options] in variantOptions"
|
v-for="[propName, options] in variantOptions"
|
||||||
:key="propName"
|
:key="propName"
|
||||||
class="flex flex-col gap-0.5"
|
:label="propName"
|
||||||
>
|
>
|
||||||
<label class="text-[10px] text-muted">{{ propName }}</label>
|
|
||||||
<AppSelect
|
<AppSelect
|
||||||
|
:label="propName"
|
||||||
:model-value="currentValues[propName] ?? ''"
|
:model-value="currentValues[propName] ?? ''"
|
||||||
:options="[...options].map((v) => ({ value: v, label: v }))"
|
:options="[...options].map((value) => ({ value, label: value }))"
|
||||||
|
:data-property="propName"
|
||||||
@update:model-value="switchVariant(propName, $event)"
|
@update:model-value="switchVariant(propName, $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</PanelFieldGroup>
|
||||||
</div>
|
</div>
|
||||||
</PanelSection>
|
</PanelSection>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -106,13 +106,11 @@ test('instance shows INSTANCE type in design panel', async () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('instance has "Go to Main Component" button', async () => {
|
test('instance has "Go to Main Component" button', async () => {
|
||||||
const goToBtn = editor.page.getByTestId('design-go-to-component')
|
await expect(editor.page.getByRole('button', { name: 'Go to Main Component' })).toBeVisible()
|
||||||
await expect(goToBtn).toBeVisible()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('instance has "Detach" button', async () => {
|
test('instance has "Detach" button', async () => {
|
||||||
const detachBtn = editor.page.getByTestId('design-detach-instance')
|
await expect(editor.page.getByRole('button', { name: 'Detach Instance' })).toBeVisible()
|
||||||
await expect(detachBtn).toBeVisible()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('modifying component propagates to instance', async () => {
|
test('modifying component propagates to instance', async () => {
|
||||||
|
|
|
||||||
|
|
@ -173,9 +173,10 @@ test('assets panel groups component sets and inserts the default variant', async
|
||||||
expect(inserted?.width).toBe(132)
|
expect(inserted?.width).toBe(132)
|
||||||
expect(inserted?.childTexts).toEqual(['Secondary'])
|
expect(inserted?.childTexts).toEqual(['Secondary'])
|
||||||
|
|
||||||
await expect(page.getByTestId('variant-section')).toBeVisible()
|
const variantSection = page.getByRole('region', { name: 'Variants' })
|
||||||
|
await expect(variantSection).toBeVisible()
|
||||||
|
|
||||||
await page.getByTestId('variant-section').getByTestId('app-select-trigger').click()
|
await variantSection.getByRole('combobox', { name: 'Type' }).click()
|
||||||
await page.getByRole('option', { name: 'Primary' }).click()
|
await page.getByRole('option', { name: 'Primary' }).click()
|
||||||
|
|
||||||
expectDefined(inserted?.id, 'inserted instance id')
|
expectDefined(inserted?.id, 'inserted instance id')
|
||||||
|
|
|
||||||
|
|
@ -257,15 +257,16 @@ test('mask action toggles mask section and mask type control', async () => {
|
||||||
const maskAction = editor.page.getByTestId('selection-toggle-mask')
|
const maskAction = editor.page.getByTestId('selection-toggle-mask')
|
||||||
await expect(maskAction).toBeVisible()
|
await expect(maskAction).toBeVisible()
|
||||||
|
|
||||||
await expect(editor.page.getByTestId('mask-section')).toHaveCount(0)
|
await expect(propertySection(editor.page, 'Mask')).toHaveCount(0)
|
||||||
await maskAction.click()
|
await maskAction.click()
|
||||||
await editor.canvas.waitForRender()
|
await editor.canvas.waitForRender()
|
||||||
|
|
||||||
let node = await getNode(expectDefined(id, 'selected id'))
|
let node = await getNode(expectDefined(id, 'selected id'))
|
||||||
expect(expectDefined(node, 'node').isMask).toBe(true)
|
expect(expectDefined(node, 'node').isMask).toBe(true)
|
||||||
await expect(editor.page.getByTestId('mask-section')).toBeVisible()
|
const maskSection = propertySection(editor.page, 'Mask')
|
||||||
|
await expect(maskSection).toBeVisible()
|
||||||
|
|
||||||
const maskTypeSelect = editor.page.getByTestId('mask-type-select')
|
const maskTypeSelect = maskSection.getByRole('combobox', { name: 'Mask type' })
|
||||||
await maskTypeSelect.click()
|
await maskTypeSelect.click()
|
||||||
await editor.page.getByRole('option', { name: 'Luminance' }).click()
|
await editor.page.getByRole('option', { name: 'Luminance' }).click()
|
||||||
await editor.canvas.waitForRender()
|
await editor.canvas.waitForRender()
|
||||||
|
|
@ -277,7 +278,7 @@ test('mask action toggles mask section and mask type control', async () => {
|
||||||
await editor.canvas.waitForRender()
|
await editor.canvas.waitForRender()
|
||||||
node = await getNode(expectDefined(id, 'selected id'))
|
node = await getNode(expectDefined(id, 'selected id'))
|
||||||
expect(expectDefined(node, 'node').isMask).toBe(false)
|
expect(expectDefined(node, 'node').isMask).toBe(false)
|
||||||
await expect(editor.page.getByTestId('mask-section')).toHaveCount(0)
|
await expect(propertySection(editor.page, 'Mask')).toHaveCount(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('visibility toggle in appearance section works', async () => {
|
test('visibility toggle in appearance section works', async () => {
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,18 @@ test('font settings popover exposes web font access without desktop-only cache a
|
||||||
const store = window.openPencil?.getStore?.()
|
const store = window.openPencil?.getStore?.()
|
||||||
if (!store) throw new Error('OpenPencil store not initialized')
|
if (!store) throw new Error('OpenPencil store not initialized')
|
||||||
const id = store.createShape('TEXT', 120, 120, 240, 40)
|
const id = store.createShape('TEXT', 120, 120, 240, 40)
|
||||||
store.updateNode(id, { characters: 'Font settings smoke' })
|
store.updateNode(id, {
|
||||||
|
characters: 'Font settings smoke',
|
||||||
|
fontFamily: 'Missing Test Sans'
|
||||||
|
})
|
||||||
store.select([id])
|
store.select([id])
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(page.getByTestId('typography-section')).toBeVisible()
|
const typography = page.getByRole('region', { name: 'Typography' })
|
||||||
|
await expect(typography).toBeVisible()
|
||||||
|
await expect(
|
||||||
|
typography.getByRole('img', { name: /Missing font: Missing Test Sans/ })
|
||||||
|
).toBeVisible()
|
||||||
const fontSettings = page.getByTestId('font-settings-trigger')
|
const fontSettings = page.getByTestId('font-settings-trigger')
|
||||||
await fontSettings.hover()
|
await fontSettings.hover()
|
||||||
await expect(page.locator('[role=tooltip]').filter({ hasText: 'Font settings' })).toBeVisible()
|
await expect(page.locator('[role=tooltip]').filter({ hasText: 'Font settings' })).toBeVisible()
|
||||||
|
|
|
||||||
37
tests/e2e/properties/page-section.spec.ts
Normal file
37
tests/e2e/properties/page-section.spec.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
|
||||||
|
import { propertySection } from '#tests/helpers/properties'
|
||||||
|
|
||||||
|
const editor = useEditorSetup()
|
||||||
|
|
||||||
|
function pageColor() {
|
||||||
|
return editor.page.evaluate(() => {
|
||||||
|
const store = window.openPencil?.getStore?.()
|
||||||
|
if (!store) throw new Error('OpenPencil store not initialized')
|
||||||
|
return store.state.pageColor
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test('page background uses the shared paint field for hex and alpha', async () => {
|
||||||
|
const pageSection = propertySection(editor.page, 'Page')
|
||||||
|
await expect(pageSection).toBeVisible()
|
||||||
|
|
||||||
|
const hex = pageSection.getByRole('textbox', { name: 'Page background' })
|
||||||
|
await hex.fill('336699')
|
||||||
|
await hex.press('Enter')
|
||||||
|
await editor.canvas.waitForRender()
|
||||||
|
|
||||||
|
let color = await pageColor()
|
||||||
|
expect(color.r).toBeCloseTo(0.2, 2)
|
||||||
|
expect(color.g).toBeCloseTo(0.4, 2)
|
||||||
|
expect(color.b).toBeCloseTo(0.6, 2)
|
||||||
|
|
||||||
|
const opacity = pageSection.getByRole('spinbutton', { name: 'Opacity' })
|
||||||
|
await opacity.click()
|
||||||
|
await opacity.fill('50')
|
||||||
|
await opacity.press('Enter')
|
||||||
|
await editor.canvas.waitForRender()
|
||||||
|
|
||||||
|
color = await pageColor()
|
||||||
|
expect(color.a).toBeCloseTo(0.5, 2)
|
||||||
|
editor.canvas.assertNoErrors()
|
||||||
|
})
|
||||||
|
|
@ -40,8 +40,17 @@ test('fill visibility supports repeat click and undo redo', async () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('empty list sections expose semantic empty state', async () => {
|
||||||
|
await expect(propertySection(editor.page, 'Fill')).not.toHaveAttribute('data-empty')
|
||||||
|
await expect(propertySection(editor.page, 'Stroke')).toHaveAttribute('data-empty', '')
|
||||||
|
await expect(propertySection(editor.page, 'Effects')).toHaveAttribute('data-empty', '')
|
||||||
|
await expect(propertySection(editor.page, 'Export')).toHaveAttribute('data-empty', '')
|
||||||
|
})
|
||||||
|
|
||||||
test('stroke visibility supports repeat click and undo redo', async () => {
|
test('stroke visibility supports repeat click and undo redo', async () => {
|
||||||
await propertySection(editor.page, 'Stroke').getByRole('button', { name: 'Add stroke' }).click()
|
const strokeSection = propertySection(editor.page, 'Stroke')
|
||||||
|
await strokeSection.getByRole('button', { name: 'Add stroke' }).click()
|
||||||
|
await expect(strokeSection).not.toHaveAttribute('data-empty')
|
||||||
await editor.canvas.waitForRender()
|
await editor.canvas.waitForRender()
|
||||||
|
|
||||||
const strokeButton = propertyItems(editor.page, 'strokes')
|
const strokeButton = propertyItems(editor.page, 'strokes')
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ test('typography section appears for text node', async () => {
|
||||||
await editor.canvas.click(200, 200)
|
await editor.canvas.click(200, 200)
|
||||||
await editor.canvas.waitForRender()
|
await editor.canvas.waitForRender()
|
||||||
|
|
||||||
const typoSection = editor.page.getByTestId('typography-section')
|
const typoSection = editor.page.getByRole('region', { name: 'Typography' })
|
||||||
await expect(typoSection).toBeVisible()
|
await expect(typoSection).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,34 @@ test('double-click enters text edit mode', async () => {
|
||||||
canvas.assertNoErrors()
|
canvas.assertNoErrors()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('typography controls use compact labeled anatomy', async () => {
|
||||||
|
await canvas.pressKey('Escape')
|
||||||
|
await canvas.waitForRender()
|
||||||
|
await canvas.click(275, 215)
|
||||||
|
await canvas.waitForRender()
|
||||||
|
|
||||||
|
const typography = page.getByRole('region', { name: 'Typography' })
|
||||||
|
await expect(typography.getByRole('button', { name: 'Font family' })).toBeVisible()
|
||||||
|
await expect(typography.getByRole('combobox', { name: 'Font weight' })).toBeVisible()
|
||||||
|
await expect(typography.getByRole('spinbutton', { name: 'Font size' })).toBeVisible()
|
||||||
|
await expect(typography.getByRole('spinbutton', { name: 'Line height' })).toBeVisible()
|
||||||
|
await expect(typography.getByRole('spinbutton', { name: 'Letter spacing' })).toBeVisible()
|
||||||
|
await expect(typography.getByRole('combobox', { name: 'Direction' })).toBeVisible()
|
||||||
|
|
||||||
|
const alignment = typography.getByRole('group', { name: 'Text alignment' })
|
||||||
|
await expect(alignment.getByRole('button', { name: 'Align left' })).toHaveAttribute(
|
||||||
|
'aria-pressed',
|
||||||
|
'true'
|
||||||
|
)
|
||||||
|
await alignment.getByRole('button', { name: 'Align center horizontally' }).click()
|
||||||
|
await canvas.waitForRender()
|
||||||
|
await expect(
|
||||||
|
alignment.getByRole('button', { name: 'Align center horizontally' })
|
||||||
|
).toHaveAttribute('aria-pressed', 'true')
|
||||||
|
|
||||||
|
await expect(typography).toHaveScreenshot('typography-panel.png')
|
||||||
|
})
|
||||||
|
|
||||||
test('bold button toggles fontWeight to 700 then back to 400', async () => {
|
test('bold button toggles fontWeight to 700 then back to 400', async () => {
|
||||||
await canvas.pressKey('Escape')
|
await canvas.pressKey('Escape')
|
||||||
await canvas.waitForRender()
|
await canvas.waitForRender()
|
||||||
|
|
@ -62,7 +90,9 @@ test('bold button toggles fontWeight to 700 then back to 400', async () => {
|
||||||
}, nodeId)
|
}, nodeId)
|
||||||
await canvas.waitForRender()
|
await canvas.waitForRender()
|
||||||
|
|
||||||
const boldBtn = page.getByTestId('typography-bold-button')
|
const boldBtn = page.getByRole('toolbar', { name: 'Text formatting' }).getByRole('button', {
|
||||||
|
name: /^Bold/
|
||||||
|
})
|
||||||
await expect(boldBtn).toBeVisible({ timeout: 3000 })
|
await expect(boldBtn).toBeVisible({ timeout: 3000 })
|
||||||
await page.waitForTimeout(200)
|
await page.waitForTimeout(200)
|
||||||
await boldBtn.click()
|
await boldBtn.click()
|
||||||
|
|
@ -72,7 +102,7 @@ test('bold button toggles fontWeight to 700 then back to 400', async () => {
|
||||||
const bold = await getNodeById(page, nodeId)
|
const bold = await getNodeById(page, nodeId)
|
||||||
expect(bold?.fontWeight).toBe(700)
|
expect(bold?.fontWeight).toBe(700)
|
||||||
|
|
||||||
await page.getByTestId('typography-bold-button').click()
|
await boldBtn.click()
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
await canvas.waitForRender()
|
await canvas.waitForRender()
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -19,10 +19,16 @@ function variableRows() {
|
||||||
return editor.page.getByTestId('variable-row')
|
return editor.page.getByTestId('variable-row')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openVariables() {
|
||||||
|
return editor.page
|
||||||
|
.getByRole('region', { name: 'Variables' })
|
||||||
|
.getByRole('button', { name: 'Open variables' })
|
||||||
|
}
|
||||||
|
|
||||||
test('variables dialog opens', async () => {
|
test('variables dialog opens', async () => {
|
||||||
await createColorVariable('primary-color')
|
await createColorVariable('primary-color')
|
||||||
|
|
||||||
await editor.page.getByTestId('variables-section-open').click()
|
await openVariables().click()
|
||||||
await expect(editor.page.getByTestId('variables-dialog')).toBeVisible()
|
await expect(editor.page.getByTestId('variables-dialog')).toBeVisible()
|
||||||
editor.canvas.assertNoErrors()
|
editor.canvas.assertNoErrors()
|
||||||
})
|
})
|
||||||
|
|
@ -87,7 +93,7 @@ test('color swatch opens color picker', async () => {
|
||||||
// close dialog if open from previous test
|
// close dialog if open from previous test
|
||||||
await editor.page.keyboard.press('Escape')
|
await editor.page.keyboard.press('Escape')
|
||||||
await editor.page.waitForTimeout(200)
|
await editor.page.waitForTimeout(200)
|
||||||
await editor.page.getByTestId('variables-section-open').click()
|
await openVariables().click()
|
||||||
await expect(editor.page.getByTestId('variables-dialog')).toBeVisible({ timeout: 3000 })
|
await expect(editor.page.getByTestId('variables-dialog')).toBeVisible({ timeout: 3000 })
|
||||||
|
|
||||||
const swatch = editor.page
|
const swatch = editor.page
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue