- Model imported fill, stroke, text, effect, and grid style definitions and references - Add SDK and app selectors with mixed-selection batching, detach-on-edit, and undo restoration - Preserve style definitions through .fig export and extracted subgraphs - Document the shared-style composable and update compatibility coverage
189 lines
7.3 KiB
Vue
189 lines
7.3 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
|
|
import { TypographyControlsRoot, useI18n } from '@open-pencil/vue'
|
|
|
|
import FontPicker from '@/components/font-picker/FontPicker.vue'
|
|
import FontSettingsPopover from '@/components/FontSettings/FontSettingsPopover.vue'
|
|
import SharedStyleField from '@/components/properties/shared-style/SharedStyleField.vue'
|
|
import VariableNumberField from '@/components/properties/VariableNumberField.vue'
|
|
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 PanelSection from '@/components/ui/panel/PanelSection.vue'
|
|
import SegmentedControl from '@/components/ui/SegmentedControl.vue'
|
|
import Tip from '@/components/ui/Tip.vue'
|
|
import { loadFont } from '@/app/editor/fonts'
|
|
import { appMenuShortcutLabel } from '@/app/shell/menu/shortcut'
|
|
|
|
const { panels, menu } = useI18n()
|
|
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>
|
|
|
|
<template>
|
|
<TypographyControlsRoot v-slot="ctx" :font-loader="fontLoader">
|
|
<PanelSection v-if="ctx.node.value" :label="panels.typography">
|
|
<SharedStyleField kind="text" :label="panels.textStyle" />
|
|
|
|
<div class="mb-panel flex min-w-0 items-center gap-panel">
|
|
<FontPicker
|
|
class="min-w-0 flex-1"
|
|
:model-value="ctx.node.value.fontFamily"
|
|
:label="panels.fontFamily"
|
|
@select="ctx.actions.setFamily"
|
|
/>
|
|
<FontSettingsPopover />
|
|
<Tip
|
|
v-if="ctx.hasMissingFonts.value"
|
|
:label="
|
|
'Missing font' +
|
|
(ctx.missingFonts.value.length > 1 ? 's' : '') +
|
|
': ' +
|
|
ctx.missingFonts.value.join(', ')
|
|
"
|
|
>
|
|
<icon-lucide-alert-triangle
|
|
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)]"
|
|
/>
|
|
</Tip>
|
|
</div>
|
|
|
|
<PanelGrid columns="two" class="mb-panel">
|
|
<PanelFieldGroup :label="panels.fontWeight">
|
|
<AppSelect
|
|
:label="panels.fontWeight"
|
|
:model-value="ctx.node.value.fontWeight"
|
|
:options="ctx.weights"
|
|
@update:model-value="ctx.actions.setWeight(+$event)"
|
|
/>
|
|
</PanelFieldGroup>
|
|
<PanelFieldGroup :label="panels.fontSize">
|
|
<VariableNumberField
|
|
:model-value="ctx.node.value.fontSize"
|
|
:aria-label="panels.fontSize"
|
|
:min="1"
|
|
:max="1000"
|
|
:node-id="ctx.node.value.id"
|
|
binding-path="fontSize"
|
|
@update:model-value="ctx.actions.updateProp('fontSize', $event)"
|
|
@commit="(v: number, p: number) => ctx.actions.commitProp('fontSize', v, p)"
|
|
/>
|
|
</PanelFieldGroup>
|
|
</PanelGrid>
|
|
|
|
<PanelGrid columns="two" class="mb-panel">
|
|
<PanelFieldGroup :label="panels.lineHeight">
|
|
<VariableNumberField
|
|
:model-value="
|
|
ctx.node.value.lineHeight ?? Math.round((ctx.node.value.fontSize || 14) * 1.2)
|
|
"
|
|
:aria-label="panels.lineHeight"
|
|
:min="0"
|
|
:node-id="ctx.node.value.id"
|
|
binding-path="lineHeight"
|
|
@update:model-value="ctx.actions.updateProp('lineHeight', $event)"
|
|
@commit="(v: number, p: number) => ctx.actions.commitProp('lineHeight', v, p)"
|
|
>
|
|
<template #icon>
|
|
<icon-lucide-baseline class="size-3" />
|
|
</template>
|
|
</VariableNumberField>
|
|
</PanelFieldGroup>
|
|
<PanelFieldGroup :label="panels.letterSpacing">
|
|
<VariableNumberField
|
|
suffix="%"
|
|
:model-value="ctx.node.value.letterSpacing"
|
|
:aria-label="panels.letterSpacing"
|
|
:node-id="ctx.node.value.id"
|
|
binding-path="letterSpacing"
|
|
@update:model-value="ctx.actions.updateProp('letterSpacing', $event)"
|
|
@commit="(v: number, p: number) => ctx.actions.commitProp('letterSpacing', v, p)"
|
|
>
|
|
<template #icon>
|
|
<icon-lucide-a-large-small class="size-3" />
|
|
</template>
|
|
</VariableNumberField>
|
|
</PanelFieldGroup>
|
|
</PanelGrid>
|
|
|
|
<PanelFieldGroup :label="panels.direction" class="mb-panel">
|
|
<AppSelect
|
|
:label="panels.direction"
|
|
:model-value="ctx.node.value.textDirection"
|
|
:options="[
|
|
{ value: 'AUTO', label: panels.auto },
|
|
{ value: 'LTR', label: 'LTR' },
|
|
{ value: 'RTL', label: 'RTL' }
|
|
]"
|
|
@update:model-value="ctx.actions.setDirection($event as 'AUTO' | 'LTR' | 'RTL')"
|
|
/>
|
|
</PanelFieldGroup>
|
|
|
|
<PanelFieldGroup :label="panels.textAlignment" class="mb-panel">
|
|
<SegmentedControl
|
|
:model-value="ctx.node.value.textAlignHorizontal"
|
|
:options="alignmentOptions"
|
|
:label="panels.textAlignment"
|
|
@change="ctx.actions.align($event as 'LEFT' | 'CENTER' | 'RIGHT')"
|
|
>
|
|
<template #option="{ option }">
|
|
<icon-lucide-align-left v-if="option.value === 'LEFT'" class="size-3.5" />
|
|
<icon-lucide-align-center v-else-if="option.value === 'CENTER'" class="size-3.5" />
|
|
<icon-lucide-align-right v-else class="size-3.5" />
|
|
</template>
|
|
</SegmentedControl>
|
|
</PanelFieldGroup>
|
|
|
|
<PanelFieldGroup :label="panels.textFormatting" :ui="{ container: 'flex-row gap-panel' }">
|
|
<div class="flex items-center gap-panel" role="toolbar" :aria-label="panels.textFormatting">
|
|
<IconButton
|
|
:label="`${menu.bold} (${appMenuShortcutLabel('text.bold')})`"
|
|
size="md"
|
|
:active="ctx.activeFormatting.value.includes('bold')"
|
|
@click="ctx.actions.toggleBold"
|
|
>
|
|
<icon-lucide-bold class="size-3.5" />
|
|
</IconButton>
|
|
<IconButton
|
|
:label="`${menu.italic} (${appMenuShortcutLabel('text.italic')})`"
|
|
size="md"
|
|
:active="ctx.activeFormatting.value.includes('italic')"
|
|
@click="ctx.actions.toggleItalic"
|
|
>
|
|
<icon-lucide-italic class="size-3.5" />
|
|
</IconButton>
|
|
<IconButton
|
|
:label="`${menu.underline} (${appMenuShortcutLabel('text.underline')})`"
|
|
size="md"
|
|
:active="ctx.activeFormatting.value.includes('underline')"
|
|
@click="ctx.actions.toggleDecoration('UNDERLINE')"
|
|
>
|
|
<icon-lucide-underline class="size-3.5" />
|
|
</IconButton>
|
|
<IconButton
|
|
:label="menu.strikethrough"
|
|
size="md"
|
|
:active="ctx.activeFormatting.value.includes('strikethrough')"
|
|
@click="ctx.actions.toggleDecoration('STRIKETHROUGH')"
|
|
>
|
|
<icon-lucide-strikethrough class="size-3.5" />
|
|
</IconButton>
|
|
</div>
|
|
</PanelFieldGroup>
|
|
</PanelSection>
|
|
</TypographyControlsRoot>
|
|
</template>
|