From 0071c3254476245ef14df99c0b961baf83151994 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 26 Jul 2026 12:42:50 +0300 Subject: [PATCH] feat(vue): add responsive property grids - Add a headless PropertyGrid primitive with themed app composition - Keep composite paint details full-width and show remove actions consistently - Tighten color picker and Design-panel field sizing without clipping values --- CHANGELOG.md | 2 +- .../programmable/sdk/api/components/index.md | 1 + .../sdk/api/components/property-grid.data.ts | 5 + .../sdk/api/components/property-grid.md | 40 ++++++ packages/vue/src/index.ts | 8 ++ .../PropertyGrid/PropertyGridRoot.vue | 27 ++++ .../vue/src/primitives/PropertyGrid/index.ts | 7 ++ .../vue/src/primitives/PropertyGrid/types.ts | 18 +++ .../color-picker-panel/OkhclChannelSlider.vue | 2 +- .../StandardColorSlider.vue | 2 +- src/components/fill-picker/FillPicker.vue | 2 +- .../properties/AppearanceSection.vue | 40 +++--- src/components/properties/FillSection.vue | 119 +++++++++--------- .../LayoutSection/LayoutGridSection.vue | 2 +- .../LayoutSection/size/SizeControls.vue | 4 +- src/components/properties/PositionSection.vue | 28 +++-- src/components/properties/StrokeSection.vue | 4 +- .../properties/TypographySection.vue | 12 +- .../properties/item-list/PropertyItemRow.vue | 4 + .../properties/paint/PaintField.vue | 4 +- .../shared-style/SharedStyleField.vue | 2 +- .../ui/panel/PanelFoundation.stories.ts | 14 +-- src/components/ui/panel/PanelGrid.vue | 29 +++-- src/components/ui/panel/PanelItemRow.vue | 4 + src/components/ui/panel/PanelRail.vue | 28 ----- src/components/ui/panel/index.ts | 1 - src/theme/color-slider.ts | 4 +- src/theme/paint-field.ts | 8 +- src/theme/panel/grid.ts | 23 ++-- src/theme/panel/item-row.ts | 8 +- src/theme/panel/rail.ts | 3 - tests/e2e/design/panel.spec.ts | 19 ++- ...paint-effects-export-openpencil-darwin.png | Bin 26660 -> 26935 bytes ...-position-appearance-openpencil-darwin.png | Bin 23704 -> 23652 bytes .../single-rectangle-openpencil-darwin.png | Bin 20505 -> 20519 bytes 35 files changed, 287 insertions(+), 187 deletions(-) create mode 100644 packages/docs/programmable/sdk/api/components/property-grid.data.ts create mode 100644 packages/docs/programmable/sdk/api/components/property-grid.md create mode 100644 packages/vue/src/primitives/PropertyGrid/PropertyGridRoot.vue create mode 100644 packages/vue/src/primitives/PropertyGrid/index.ts create mode 100644 packages/vue/src/primitives/PropertyGrid/types.ts delete mode 100644 src/components/ui/panel/PanelRail.vue delete mode 100644 src/theme/panel/rail.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ee04d9f0..0c969b584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ - 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. 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/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/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/LayoutGridSection.vue b/src/components/properties/LayoutSection/LayoutGridSection.vue index b7fa841ee..0a56c2ca3 100644 --- a/src/components/properties/LayoutSection/LayoutGridSection.vue +++ b/src/components/properties/LayoutSection/LayoutGridSection.vue @@ -100,7 +100,7 @@ function isGrid(grid: LayoutGrid): boolean { - + diff --git a/src/components/properties/PositionSection.vue b/src/components/properties/PositionSection.vue index 4b6e66c5c..44de639f5 100644 --- a/src/components/properties/PositionSection.vue +++ b/src/components/properties/PositionSection.vue @@ -79,7 +79,7 @@ function handleAlign( - + - + -
+ - - - - - - - - - -
+
+ + + + + + + + + +
+
diff --git a/src/components/properties/StrokeSection.vue b/src/components/properties/StrokeSection.vue index 607e5b9fe..f9eaedc2f 100644 --- a/src/components/properties/StrokeSection.vue +++ b/src/components/properties/StrokeSection.vue @@ -154,7 +154,7 @@ function onToggleSides(activeNode: SceneNode | null) {