diff --git a/CHANGELOG.md b/CHANGELOG.md index ae014662e..0c969b584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/packages/docs/programmable/sdk/api/components/index.md b/packages/docs/programmable/sdk/api/components/index.md index a642c2e36..c2d97d90f 100644 --- a/packages/docs/programmable/sdk/api/components/index.md +++ b/packages/docs/programmable/sdk/api/components/index.md @@ -23,6 +23,7 @@ description: Component reference for headless Vue primitives in @open-pencil/vue + diff --git a/packages/docs/programmable/sdk/api/components/property-grid.data.ts b/packages/docs/programmable/sdk/api/components/property-grid.data.ts new file mode 100644 index 000000000..d242e2890 --- /dev/null +++ b/packages/docs/programmable/sdk/api/components/property-grid.data.ts @@ -0,0 +1,5 @@ +import { defineComponentMetaLoader } from '#docs/sdk/component-meta' + +const sources = ['packages/vue/src/primitives/PropertyGrid/PropertyGridRoot.vue'] + +export default defineComponentMetaLoader(sources) diff --git a/packages/docs/programmable/sdk/api/components/property-grid.md b/packages/docs/programmable/sdk/api/components/property-grid.md new file mode 100644 index 000000000..14374349f --- /dev/null +++ b/packages/docs/programmable/sdk/api/components/property-grid.md @@ -0,0 +1,40 @@ +--- +title: PropertyGrid +description: Headless field-grid and action-rail anatomy for property panels. +--- + + + +# 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 + + + +``` + +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 + + diff --git a/packages/vue/src/i18n/locales/de/panels.json b/packages/vue/src/i18n/locales/de/panels.json index f4f5d4897..928f01ee8 100644 --- a/packages/vue/src/i18n/locales/de/panels.json +++ b/packages/vue/src/i18n/locales/de/panels.json @@ -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", diff --git a/packages/vue/src/i18n/locales/es/panels.json b/packages/vue/src/i18n/locales/es/panels.json index 0f4a4da43..e36693967 100644 --- a/packages/vue/src/i18n/locales/es/panels.json +++ b/packages/vue/src/i18n/locales/es/panels.json @@ -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", diff --git a/packages/vue/src/i18n/locales/fr/panels.json b/packages/vue/src/i18n/locales/fr/panels.json index 279a041ba..c5a8c9ff6 100644 --- a/packages/vue/src/i18n/locales/fr/panels.json +++ b/packages/vue/src/i18n/locales/fr/panels.json @@ -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", diff --git a/packages/vue/src/i18n/locales/it/panels.json b/packages/vue/src/i18n/locales/it/panels.json index b5e90139f..f60e55c5d 100644 --- a/packages/vue/src/i18n/locales/it/panels.json +++ b/packages/vue/src/i18n/locales/it/panels.json @@ -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", diff --git a/packages/vue/src/i18n/locales/ja/panels.json b/packages/vue/src/i18n/locales/ja/panels.json index b7b6232a8..53694589d 100644 --- a/packages/vue/src/i18n/locales/ja/panels.json +++ b/packages/vue/src/i18n/locales/ja/panels.json @@ -142,12 +142,14 @@ "multiplayerCursors": "他の参加者のカーソル", "direction": "方向", "flow": "フロー", + "freeform": "フリーフォーム", + "dimensions": "寸法", "layoutHorizontal": "横方向レイアウト", "layoutVertical": "縦方向レイアウト", "layoutGrid": "グリッドレイアウト", - "layoutGrids": "レイアウトグリッド", - "addLayoutGrid": "レイアウトグリッドを追加", - "removeLayoutGrid": "レイアウトグリッドを削除", + "layoutGrids": "レイアウトガイド", + "addLayoutGrid": "レイアウトガイドを追加", + "removeLayoutGrid": "レイアウトガイドを削除", "gridGrid": "グリッド", "gridColumns": "列", "gridRows": "行", diff --git a/packages/vue/src/i18n/locales/pl/panels.json b/packages/vue/src/i18n/locales/pl/panels.json index f304a15b9..165a3ec02 100644 --- a/packages/vue/src/i18n/locales/pl/panels.json +++ b/packages/vue/src/i18n/locales/pl/panels.json @@ -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", diff --git a/packages/vue/src/i18n/locales/ru/panels.json b/packages/vue/src/i18n/locales/ru/panels.json index dbb63f1d8..768a786f4 100644 --- a/packages/vue/src/i18n/locales/ru/panels.json +++ b/packages/vue/src/i18n/locales/ru/panels.json @@ -128,12 +128,14 @@ "multiplayerCursors": "Курсоры участников", "direction": "Направление", "flow": "Поток", + "freeform": "Свободная", + "dimensions": "Размеры", "layoutHorizontal": "Горизонтальная раскладка", "layoutVertical": "Вертикальная раскладка", "layoutGrid": "Сетка", - "layoutGrids": "Сетки макета", - "addLayoutGrid": "Добавить сетку макета", - "removeLayoutGrid": "Удалить сетку макета", + "layoutGrids": "Направляющие макета", + "addLayoutGrid": "Добавить направляющую макета", + "removeLayoutGrid": "Удалить направляющую макета", "gridGrid": "Сетка", "gridColumns": "Столбцы", "gridRows": "Строки", diff --git a/packages/vue/src/i18n/locales/zh-cn/panels.json b/packages/vue/src/i18n/locales/zh-cn/panels.json index 4fc301825..533f2443e 100644 --- a/packages/vue/src/i18n/locales/zh-cn/panels.json +++ b/packages/vue/src/i18n/locales/zh-cn/panels.json @@ -128,12 +128,14 @@ "multiplayerCursors": "协作光标", "direction": "方向", "flow": "流向", + "freeform": "自由布局", + "dimensions": "尺寸", "layoutHorizontal": "水平布局", "layoutVertical": "垂直布局", "layoutGrid": "网格布局", - "layoutGrids": "布局网格", - "addLayoutGrid": "添加布局网格", - "removeLayoutGrid": "移除布局网格", + "layoutGrids": "布局参考线", + "addLayoutGrid": "添加布局参考线", + "removeLayoutGrid": "移除布局参考线", "gridGrid": "网格", "gridColumns": "列", "gridRows": "行", diff --git a/packages/vue/src/i18n/messages/panels.ts b/packages/vue/src/i18n/messages/panels.ts index 1ed49b6d6..496efc84a 100644 --- a/packages/vue/src/i18n/messages/panels.ts +++ b/packages/vue/src/i18n/messages/panels.ts @@ -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', diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 2c9187e35..c82e631ec 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -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, diff --git a/packages/vue/src/primitives/PropertyGrid/PropertyGridRoot.vue b/packages/vue/src/primitives/PropertyGrid/PropertyGridRoot.vue new file mode 100644 index 000000000..78454f7eb --- /dev/null +++ b/packages/vue/src/primitives/PropertyGrid/PropertyGridRoot.vue @@ -0,0 +1,27 @@ + + + diff --git a/packages/vue/src/primitives/PropertyGrid/index.ts b/packages/vue/src/primitives/PropertyGrid/index.ts new file mode 100644 index 000000000..cda77b067 --- /dev/null +++ b/packages/vue/src/primitives/PropertyGrid/index.ts @@ -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' diff --git a/packages/vue/src/primitives/PropertyGrid/types.ts b/packages/vue/src/primitives/PropertyGrid/types.ts new file mode 100644 index 000000000..ace20d98c --- /dev/null +++ b/packages/vue/src/primitives/PropertyGrid/types.ts @@ -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[] +} diff --git a/src/components/DesignPanel.vue b/src/components/DesignPanel.vue index 0d6d8028f..083d53e53 100644 --- a/src/components/DesignPanel.vue +++ b/src/components/DesignPanel.vue @@ -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() @@ -131,6 +136,7 @@ const { panels } = useI18n() + diff --git a/src/components/color-picker-panel/OkhclChannelSlider.vue b/src/components/color-picker-panel/OkhclChannelSlider.vue index f1d608206..07f0b36cd 100644 --- a/src/components/color-picker-panel/OkhclChannelSlider.vue +++ b/src/components/color-picker-panel/OkhclChannelSlider.vue @@ -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)" /> diff --git a/src/components/color-picker-panel/StandardColorSlider.vue b/src/components/color-picker-panel/StandardColorSlider.vue index 7080df110..590ebb80e 100644 --- a/src/components/color-picker-panel/StandardColorSlider.vue +++ b/src/components/color-picker-panel/StandardColorSlider.vue @@ -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)" /> diff --git a/src/components/fill-picker/FillPicker.vue b/src/components/fill-picker/FillPicker.vue index 28c5b42d2..ecc3b78c9 100644 --- a/src/components/fill-picker/FillPicker.vue +++ b/src/components/fill-picker/FillPicker.vue @@ -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" > - + - + - +
- +
- - - - - - + - + -
- + - - + - + - - - + + + + +
diff --git a/src/components/properties/LayoutSection/AutoLayoutControls.vue b/src/components/properties/LayoutSection/AutoLayoutControls.vue index f81daaa8d..aa4c7b8a3 100644 --- a/src/components/properties/LayoutSection/AutoLayoutControls.vue +++ b/src/components/properties/LayoutSection/AutoLayoutControls.vue @@ -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>(() => [ - { mode: 'HORIZONTAL', label: panels.value.layoutHorizontal }, - { mode: 'VERTICAL', label: panels.value.layoutVertical }, - { mode: 'GRID', label: panels.value.layoutGrid } +const layoutModes = computed>(() => [ + { 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) +} diff --git a/src/components/properties/LayoutSection/FlexControls.vue b/src/components/properties/LayoutSection/FlexControls.vue index 68e49654c..5969a23b9 100644 --- a/src/components/properties/LayoutSection/FlexControls.vue +++ b/src/components/properties/LayoutSection/FlexControls.vue @@ -52,7 +52,7 @@ function alignmentCellClass(primary: LayoutAlign, counter: string) {