Code review: svg-export/ folder, restore comments, cleanup

- Move svg-export-{defs,paths}.ts into svg-export/ folder
  (defs.ts, paths.ts, index.ts) instead of prefix-based split
- Remove duplicate getChildren lambda in fig-import.ts importPages
- Remove dead dashPattern local in kiwi-convert.ts nodeChangeToProps
- Restore useful explanatory comments in instance-overrides.ts
  (DSD propagation, direct vs cloned assignments, seed skipping)
- Rename applyEffectOverrides → applyShapeAndEffectOverrides
  (handles polygon props + shadow + blur, not just effects)
- Format src/ with oxfmt
This commit is contained in:
Danila Poyarkov 2026-03-09 15:22:14 +03:00
parent 59981709ba
commit 7f5a388e2b
25 changed files with 194 additions and 75 deletions

110
.pi/context.md Normal file
View file

@ -0,0 +1,110 @@
# Code Context
## Files Retrieved
1. `packages/core/src/tools/analyze.ts` (lines 1-391) - Full file: analyze tools, diff tools, eval tool
2. `packages/core/src/tools/vector.ts` (lines 1-264) - Full file: boolean ops, path ops, viewport, SVG/image export
3. `packages/core/src/tools/ai-adapter.ts` (lines 1-96) - Full file: Vercel AI SDK adapter
4. `packages/core/src/tools/read.ts` (lines 1-183) - Full file: query/read tools
5. `packages/core/src/tools/registry.ts` (lines 1-100) - Full file: ALL_TOOLS array
## analyze.ts — Tool Definitions
| Line | Tool Name |
|------|-----------|
| 128 | `analyze_colors` |
| 197 | `analyze_typography` |
| 251 | `analyze_spacing` |
| 295 | `analyze_clusters` |
| 356 | `diff_create` (under "Diff tools" section) |
| 398 | `diff_show` |
| 449 | `eval` |
**No `describe` tool exists** in analyze.ts or anywhere in the tools directory.
## read.ts — Tool Names
| Tool Name | Description |
|-----------|-------------|
| `get_selection` | Currently selected nodes |
| `get_page_tree` | Node tree of current page |
| `get_node` | Detailed properties by ID |
| `find_nodes` | Find by name/type |
| `get_components` | List components |
| `list_pages` | List all pages |
| `switch_page` | Switch page by name/ID |
| `get_current_page` | Current page name/ID |
| `page_bounds` | Bounding box of all page objects |
| `select_nodes` | Select nodes by ID |
| `list_fonts` | Fonts used on current page |
**Neither `get_jsx` nor `diff_jsx` exist** in read.ts or any other tool file.
## vector.ts — exportImage Tool (line ~225)
- **Default format:** `'PNG'`
- **Default scale:** `1` (min: 0.1, max: 4)
- **Base64 conversion:** Dual-path:
- Node.js: `Buffer.from(data).toString('base64')`
- Browser: `btoa(String.fromCharCode(...data))`
- Returns: `{ mimeType, base64, byteLength }`
- Falls back to error if `figma.exportImage` is unavailable
## ai-adapter.ts — Full Contents
- **`AIAdapterOptions`** interface: `getFigma()`, `onBeforeExecute?`, `onAfterExecute?`, `onFlashNodes?`
- **`toolsToAI()`** function: Converts `ToolDef[]` → Vercel AI SDK `tool()` objects
- Accepts deps: `{ v: valibot, valibotSchema, tool }` (lazy imports)
- Builds valibot schemas from `ParamDef` type declarations
- Wraps each `execute` with before/after hooks and error catching
- If tool `mutates` and returns node IDs, calls `onFlashNodes` for visual feedback
- **`paramToValibot()`**: Maps param types → valibot schemas
- `string``v.string()` or `v.picklist()` if enum
- `number``v.number()` with optional `minValue`/`maxValue` pipes
- `boolean``v.boolean()`
- `color``v.string()` with description
- `string[]``v.array(v.string())` with `minLength(1)`
- Non-required params wrapped in `v.optional()`
- **`extractNodeIds()`**: Extracts IDs from result objects (handles `id`, `selection[]`, `results[]`)
## registry.ts — ALL_TOOLS Array (84 tools total)
**Read (11):** get_selection, get_page_tree, get_node, find_nodes, get_components, list_pages, switch_page, get_current_page, page_bounds, select_nodes, list_fonts
**Create (7):** createShape, render, createComponent, createInstance, createPage, createVector, createSlice
**Modify (20):** setFill, setStroke, setEffects, updateNode, setLayout, setConstraints, setRotation, setOpacity, setRadius, setMinMax, setText, setFont, setFontRange, setTextResize, setVisible, setBlend, setLocked, setStrokeAlign, setTextProperties, setLayoutChild
**Structure (17):** deleteNode, cloneNode, renameNode, reparentNode, groupNodes, ungroupNode, flattenNodes, nodeToComponent, nodeBounds, nodeMove, nodeResize, nodeAncestors, nodeChildren, nodeTree, nodeBindings, nodeReplaceWith, arrangeNodes
**Variables (11):** listVariables, listCollections, getVariable, findVariables, createVariable, setVariable, deleteVariable, bindVariable, getCollection, createCollection, deleteCollection
**Vector & Export (14):** booleanUnion, booleanSubtract, booleanIntersect, booleanExclude, pathGet, pathSet, pathScale, pathFlip, pathMove, viewportGet, viewportSet, viewportZoomToFit, exportSvg, exportImage
**Analyze & Diff (6):** analyzeColors, analyzeTypography, analyzeSpacing, analyzeClusters, diffCreate, diffShow
**Eval (1):** evalCode
## Architecture
```
ToolDef (schema.ts) -- type-safe tool definition with params + execute
├── read.ts -- query tools (selection, find, pages, fonts)
├── create.ts -- shape/component/page creation
├── modify.ts -- property setters
├── structure.ts -- tree operations
├── variables.ts -- variable/collection CRUD
├── vector.ts -- boolean ops, paths, viewport, export
└── analyze.ts -- analysis, diff, eval
registry.ts -- assembles ALL_TOOLS array
ai-adapter.ts -- toolsToAI() → Vercel AI SDK tool() objects
(valibot schemas, before/after hooks, flash nodes)
```
Each `defineTool()` creates a `ToolDef` with typed params. The `registry.ts` collects all into `ALL_TOOLS[]`. The `ai-adapter.ts` converts them to Vercel AI SDK format with valibot validation schemas, injecting `FigmaAPI` at execution time via `getFigma()`.
## Start Here
- **`packages/core/src/tools/registry.ts`** — the central manifest of all 84 tools, organized by domain. Shows the full tool surface and imports from all domain files.
- **`packages/core/src/tools/ai-adapter.ts`** — the bridge between tool definitions and the AI chat system. Key to understanding how tools are exposed to LLMs.

View file

@ -161,8 +161,6 @@ function importPages(
created: Set<string>, created: Set<string>,
createSceneNode: (ncId: string, graphParentId: string) => void createSceneNode: (ncId: string, graphParentId: string) => void
): void { ): void {
const getChildren = (ncId: string): string[] => childrenMap.get(ncId) ?? []
let docId: string | null = null let docId: string | null = null
for (const [id, nc] of changeMap) { for (const [id, nc] of changeMap) {
if (nc.type === 'DOCUMENT' || id === '0:0') { if (nc.type === 'DOCUMENT' || id === '0:0') {
@ -172,14 +170,14 @@ function importPages(
} }
if (docId) { if (docId) {
for (const canvasId of getChildren(docId)) { for (const canvasId of childrenMap.get(docId) ?? []) {
const canvasNc = changeMap.get(canvasId) const canvasNc = changeMap.get(canvasId)
if (!canvasNc) continue if (!canvasNc) continue
if (canvasNc.type === 'CANVAS') { if (canvasNc.type === 'CANVAS') {
const page = graph.addPage(canvasNc.name ?? 'Page') const page = graph.addPage(canvasNc.name ?? 'Page')
if (canvasNc.internalOnly) page.internalOnly = true if (canvasNc.internalOnly) page.internalOnly = true
created.add(canvasId) created.add(canvasId)
for (const childId of getChildren(canvasId)) { for (const childId of childrenMap.get(canvasId) ?? []) {
createSceneNode(childId, page.id) createSceneNode(childId, page.id)
} }
} else { } else {

View file

@ -270,6 +270,7 @@ export function populateAndApplyOverrides(
) { ) {
for (const node of graph.getAllNodes()) { for (const node of graph.getAllNodes()) {
if (node.type !== 'INSTANCE') continue if (node.type !== 'INSTANCE') continue
// Apply assignments from the instance's own kiwi data first
const ownFigmaId = nodeIdToGuid.get(node.id) const ownFigmaId = nodeIdToGuid.get(node.id)
if (ownFigmaId) { if (ownFigmaId) {
const ownAssignments = assignmentSources.get(ownFigmaId) const ownAssignments = assignmentSources.get(ownFigmaId)
@ -278,6 +279,8 @@ export function populateAndApplyOverrides(
} }
} }
// Also apply from cloned instance sources — after population, cloned
// instances have componentId pointing to the original kiwi node
if (!node.componentId) continue if (!node.componentId) continue
const sourceFigmaId = nodeIdToGuid.get(node.componentId) const sourceFigmaId = nodeIdToGuid.get(node.componentId)
if (!sourceFigmaId) continue if (!sourceFigmaId) continue
@ -459,6 +462,9 @@ export function populateAndApplyOverrides(
return { dsdModified, dsdSizeSet } return { dsdModified, dsdSizeSet }
} }
// Propagate DSD changes through clone chains — each clone should match
// its source for size/position/geometry. Nodes whose size was explicitly
// set by DSD keep their own values; others inherit from their source.
function propagateDsdChanges(dsdModified: Set<string>, dsdSizeSet: Set<string>) { function propagateDsdChanges(dsdModified: Set<string>, dsdSizeSet: Set<string>) {
if (dsdModified.size === 0) return if (dsdModified.size === 0) return
@ -643,6 +649,7 @@ export function populateAndApplyOverrides(
const node = graph.getNode(cloneId) const node = graph.getNode(cloneId)
if (!node) continue if (!node) continue
// Don't overwrite nodes directly targeted by symbolOverrides
if (seeds.has(cloneId)) { if (seeds.has(cloneId)) {
syncQueue.push(cloneId) syncQueue.push(cloneId)
continue continue

View file

@ -557,8 +557,6 @@ export function nodeChangeToProps(
let nodeType = mapNodeType(nc.type) let nodeType = mapNodeType(nc.type)
if (nodeType === 'FRAME' && isComponentSet(nc)) nodeType = 'COMPONENT_SET' if (nodeType === 'FRAME' && isComponentSet(nc)) nodeType = 'COMPONENT_SET'
const dashPattern = nc.dashPattern ?? []
return { return {
nodeType, nodeType,
name: nc.name ?? nodeType, name: nc.name ?? nodeType,
@ -568,7 +566,7 @@ export function nodeChangeToProps(
locked: nc.locked ?? false, locked: nc.locked ?? false,
blendMode: (nc.blendMode as Fill['blendMode']) ?? 'PASS_THROUGH', blendMode: (nc.blendMode as Fill['blendMode']) ?? 'PASS_THROUGH',
fills: convertFills(nc.fillPaints), fills: convertFills(nc.fillPaints),
strokes: convertStrokes(nc.strokePaints, nc.strokeWeight, nc.strokeAlign, nc.strokeCap, nc.strokeJoin, dashPattern), strokes: convertStrokes(nc.strokePaints, nc.strokeWeight, nc.strokeAlign, nc.strokeCap, nc.strokeJoin, nc.dashPattern ?? []),
effects: convertEffects(nc.effects), effects: convertEffects(nc.effects),
...convertCornerProps(nc), ...convertCornerProps(nc),
...convertTextProps(nc), ...convertTextProps(nc),

View file

@ -274,7 +274,7 @@ function applyTextOverrides(props: Record<string, unknown>, o: Partial<SceneNode
: 'HEIGHT' : 'HEIGHT'
} }
function applyEffectOverrides(props: Record<string, unknown>, o: Partial<SceneNode>): void { function applyShapeAndEffectOverrides(props: Record<string, unknown>, o: Partial<SceneNode>): void {
if (props.points !== undefined) o.pointCount = props.points as number if (props.points !== undefined) o.pointCount = props.points as number
if (props.innerRadius !== undefined) o.starInnerRadius = props.innerRadius as number if (props.innerRadius !== undefined) o.starInnerRadius = props.innerRadius as number
if (props.pointCount !== undefined) o.pointCount = props.pointCount as number if (props.pointCount !== undefined) o.pointCount = props.pointCount as number
@ -321,7 +321,7 @@ function propsToOverrides(props: Record<string, unknown>, isText: boolean): Part
applyVisualOverrides(props, o) applyVisualOverrides(props, o)
applyLayoutOverrides(props, o, w, h) applyLayoutOverrides(props, o, w, h)
if (isText) applyTextOverrides(props, o) if (isText) applyTextOverrides(props, o)
applyEffectOverrides(props, o) applyShapeAndEffectOverrides(props, o)
return o return o
} }

View file

@ -1,11 +1,11 @@
import { colorToHex, colorToHex8 } from './color' import { colorToHex, colorToHex8 } from '../color'
import { round } from './svg-export-paths' import { round } from './paths'
import { svg } from './svg-node' import { svg } from '../svg-node'
import type { SVGNode } from './svg-node' import type { SVGNode } from '../svg-node'
import type { SceneGraph, SceneNode, Fill, Effect } from './scene-graph' import type { SceneGraph, SceneNode, Fill, Effect } from '../scene-graph'
import type { Color } from './types' import type { Color } from '../types'
export interface SVGExportContext { export interface SVGExportContext {
defs: SVGNode[] defs: SVGNode[]

View file

@ -1,4 +1,4 @@
import { computeContentBounds } from './render-image' import { computeContentBounds } from '../render-image'
import { import {
round, round,
geometryBlobToSVGPath, geometryBlobToSVGPath,
@ -7,7 +7,7 @@ import {
hasRadius, hasRadius,
roundedRectPath, roundedRectPath,
arcPath arcPath
} from './svg-export-paths' } from './paths'
import { import {
nextDefId, nextDefId,
formatColor, formatColor,
@ -16,15 +16,15 @@ import {
SVG_STROKE_CAP, SVG_STROKE_CAP,
SVG_STROKE_JOIN, SVG_STROKE_JOIN,
SVG_BLEND_MODE SVG_BLEND_MODE
} from './svg-export-defs' } from './defs'
export { geometryBlobToSVGPath, vectorNetworkToSVGPaths } from './svg-export-paths' export { geometryBlobToSVGPath, vectorNetworkToSVGPaths } from './paths'
import { svg, renderSVGNode } from './svg-node' import { svg, renderSVGNode } from '../svg-node'
import type { SVGNode } from './svg-node' import type { SVGNode } from '../svg-node'
import type { SceneGraph, SceneNode, Fill, Stroke, CharacterStyleOverride } from './scene-graph' import type { SceneGraph, SceneNode, Fill, Stroke, CharacterStyleOverride } from '../scene-graph'
import type { SVGExportContext } from './svg-export-defs' import type { SVGExportContext } from './defs'
// --- Node rendering --- // --- Node rendering ---

View file

@ -1,4 +1,4 @@
import type { SceneNode, VectorNetwork, VectorSegment, VectorVertex } from './scene-graph' import type { SceneNode, VectorNetwork, VectorSegment, VectorVertex } from '../scene-graph'
const CMD_CLOSE = 0 const CMD_CLOSE = 0
const CMD_MOVE_TO = 1 const CMD_MOVE_TO = 1

View file

@ -237,7 +237,7 @@ export const exportSvg = defineTool({
} }
}, },
execute: async (figma, args) => { execute: async (figma, args) => {
const { renderNodesToSVG } = await import('../svg-export.js') const { renderNodesToSVG } = await import('../svg-export/index.js')
const pageId = figma.currentPageId const pageId = figma.currentPageId
const ids = const ids =
args.ids && args.ids.length > 0 args.ids && args.ids.length > 0

View file

@ -101,7 +101,9 @@ export function startAutomationBridge(server: ViteServer) {
authToken = null authToken = null
ws.on('message', (raw) => { ws.on('message', (raw) => {
handleBrowserMessage(typeof raw === 'string' ? raw : Buffer.from(raw as Buffer).toString('utf-8')) handleBrowserMessage(
typeof raw === 'string' ? raw : Buffer.from(raw as Buffer).toString('utf-8')
)
}) })
ws.on('close', () => { ws.on('close', () => {

View file

@ -122,7 +122,9 @@ export function connectAutomation(getStore: () => EditorStore) {
return { ok: true, result: { jsx } } return { ok: true, result: { jsx } }
} }
const commandHandlers: Partial<Record<string, (store: EditorStore, args: unknown) => Promise<unknown>>> = { const commandHandlers: Partial<
Record<string, (store: EditorStore, args: unknown) => Promise<unknown>>
> = {
eval: handleEval, eval: handleEval,
tool: handleTool, tool: handleTool,
export: handleExport, export: handleExport,

View file

@ -31,7 +31,9 @@ function handleSubmit(text: string) {
const c = ensureChat() const c = ensureChat()
if (c) chat.value = markRaw(c) if (c) chat.value = markRaw(c)
} }
chat.value?.sendMessage({ text }).catch(() => { /* user-facing error handled by UI */ }) chat.value?.sendMessage({ text }).catch(() => {
/* user-facing error handled by UI */
})
} }
function handleStop() { function handleStop() {

View file

@ -12,7 +12,10 @@ const store = useEditorStore()
const collab = useCollabInjected() const collab = useCollabInjected()
const canvasRef = ref<HTMLCanvasElement | null>(null) const canvasRef = ref<HTMLCanvasElement | null>(null)
const { hitTestSectionTitle, hitTestComponentLabel, hitTestFrameTitle } = useCanvas(canvasRef, store) const { hitTestSectionTitle, hitTestComponentLabel, hitTestFrameTitle } = useCanvas(
canvasRef,
store
)
const { cursorOverride } = useCanvasInput( const { cursorOverride } = useCanvasInput(
canvasRef, canvasRef,
store, store,

View file

@ -87,9 +87,7 @@ function onColorUpdate(color: Color) {
function setCategory(cat: FillCategory) { function setCategory(cat: FillCategory) {
if (cat === fillCategory.value) return if (cat === fillCategory.value) return
if (cat === 'SOLID') { if (cat === 'SOLID') {
const color = fill.gradientStops?.length const color = fill.gradientStops?.length ? { ...fill.gradientStops[0].color } : fill.color
? { ...fill.gradientStops[0].color }
: fill.color
emit('update', { ...fill, type: 'SOLID', color }) emit('update', { ...fill, type: 'SOLID', color })
} else if (cat === 'GRADIENT') { } else if (cat === 'GRADIENT') {
const type: GradientSubtype = 'GRADIENT_LINEAR' const type: GradientSubtype = 'GRADIENT_LINEAR'

View file

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from 'vue' import { computed, nextTick, onMounted, ref, watch } from 'vue'
import type { import type { ListboxFilter } from 'reka-ui'
ListboxFilter} from 'reka-ui';
import { import {
ListboxContent, ListboxContent,
ListboxItem, ListboxItem,

View file

@ -348,10 +348,7 @@ function updateDropTarget(ev: PointerEvent) {
<icon-lucide-chevron-right class="size-3" /> <icon-lucide-chevron-right class="size-3" />
</span> </span>
<span v-else class="w-4 shrink-0" /> <span v-else class="w-4 shrink-0" />
<component <component :is="nodeIcon(item.value)" class="size-3 shrink-0 opacity-70" />
:is="nodeIcon(item.value)"
class="size-3 shrink-0 opacity-70"
/>
<input <input
:ref=" :ref="
(el) => { (el) => {

View file

@ -151,9 +151,7 @@ const onlineCount = computed(() => collabPeers.length + 1)
v-for="peer in collabPeers" v-for="peer in collabPeers"
:key="peer.clientId" :key="peer.clientId"
class="flex cursor-pointer items-center gap-2 rounded-md px-0.5 py-0.5 select-none active:bg-hover" class="flex cursor-pointer items-center gap-2 rounded-md px-0.5 py-0.5 select-none active:bg-hover"
@click=" @click="emit('follow', followingPeer === peer.clientId ? null : peer.clientId)"
emit('follow', followingPeer === peer.clientId ? null : peer.clientId)
"
> >
<div <div
class="flex size-7 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white" class="flex size-7 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white"

View file

@ -2,7 +2,17 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useEventListener } from '@vueuse/core' import { useEventListener } from '@vueuse/core'
const { modelValue, min = -Infinity, max = Infinity, step = 1, icon, label, suffix, sensitivity = 1, placeholder = 'Mixed' } = defineProps<{ const {
modelValue,
min = -Infinity,
max = Infinity,
step = 1,
icon,
label,
suffix,
sensitivity = 1,
placeholder = 'Mixed'
} = defineProps<{
modelValue: number | symbol modelValue: number | symbol
min?: number min?: number
max?: number max?: number

View file

@ -17,8 +17,12 @@ function toolDisplayName(part: ToolPart): string {
} }
function hasErrorOutput(part: ToolPart): boolean { function hasErrorOutput(part: ToolPart): boolean {
return part.state === 'output-available' && return (
typeof part.output === 'object' && part.output !== null && 'error' in part.output part.state === 'output-available' &&
typeof part.output === 'object' &&
part.output !== null &&
'error' in part.output
)
} }
function toolState(part: ToolPart): 'pending' | 'done' | 'error' { function toolState(part: ToolPart): 'pending' | 'done' | 'error' {
@ -42,10 +46,7 @@ function partKey(part: UIMessagePart, index: number): string {
<template v-if="message.role === 'assistant'"> <template v-if="message.role === 'assistant'">
<template v-for="(part, i) in message.parts" :key="partKey(part, i)"> <template v-for="(part, i) in message.parts" :key="partKey(part, i)">
<!-- Tool call --> <!-- Tool call -->
<div <div v-if="isToolUIPart(part)" class="rounded-lg border border-border bg-canvas p-2">
v-if="isToolUIPart(part)"
class="rounded-lg border border-border bg-canvas p-2"
>
<CollapsibleRoot> <CollapsibleRoot>
<CollapsibleTrigger <CollapsibleTrigger
class="flex w-full items-center gap-2 rounded px-1 py-0.5 hover:bg-hover" class="flex w-full items-center gap-2 rounded px-1 py-0.5 hover:bg-hover"
@ -62,10 +63,7 @@ function partKey(part: UIMessagePart, index: number): string {
v-if="toolState(part) === 'pending'" v-if="toolState(part) === 'pending'"
class="size-3 animate-spin" class="size-3 animate-spin"
/> />
<icon-lucide-check <icon-lucide-check v-else-if="toolState(part) === 'done'" class="size-3" />
v-else-if="toolState(part) === 'done'"
class="size-3"
/>
<icon-lucide-triangle-alert v-else class="size-3" /> <icon-lucide-triangle-alert v-else class="size-3" />
</div> </div>
<span class="text-[11px] text-surface"> <span class="text-[11px] text-surface">
@ -106,11 +104,7 @@ function partKey(part: UIMessagePart, index: number): string {
data-test-id="chat-text-bubble" data-test-id="chat-text-bubble"
class="rounded-xl rounded-tl-md bg-hover px-3 py-2 text-xs leading-relaxed text-surface" class="rounded-xl rounded-tl-md bg-hover px-3 py-2 text-xs leading-relaxed text-surface"
> >
<Markdown <Markdown :content="part.text" :mermaid="false" class="chat-markdown" />
:content="part.text"
:mermaid="false"
class="chat-markdown"
/>
</div> </div>
</template> </template>
</template> </template>
@ -121,7 +115,12 @@ function partKey(part: UIMessagePart, index: number): string {
data-test-id="chat-text-bubble" data-test-id="chat-text-bubble"
class="whitespace-pre-wrap rounded-xl rounded-br-md bg-accent px-3 py-2 text-xs leading-relaxed text-white" class="whitespace-pre-wrap rounded-xl rounded-br-md bg-accent px-3 py-2 text-xs leading-relaxed text-white"
> >
{{ message.parts.filter(isTextUIPart).map((p) => p.text).join('') }} {{
message.parts
.filter(isTextUIPart)
.map((p) => p.text)
.join('')
}}
</div> </div>
</div> </div>
</div> </div>

View file

@ -26,10 +26,7 @@ function save() {
</script> </script>
<template> <template>
<div <div data-test-id="provider-setup" class="flex flex-1 flex-col items-center justify-center px-6">
data-test-id="provider-setup"
class="flex flex-1 flex-col items-center justify-center px-6"
>
<icon-lucide-sparkles class="mb-3 size-7 text-muted" /> <icon-lucide-sparkles class="mb-3 size-7 text-muted" />
<p class="mb-5 text-center text-xs text-muted">Connect an AI provider to start chatting.</p> <p class="mb-5 text-center text-xs text-muted">Connect an AI provider to start chatting.</p>

View file

@ -97,18 +97,14 @@ function commitUniformPadding(_value: number, previous: number) {
} }
const widthSizingOptions = computed(() => { const widthSizingOptions = computed(() => {
const options: { value: LayoutSizing; label: string }[] = [ const options: { value: LayoutSizing; label: string }[] = [{ value: 'FIXED', label: 'Fixed' }]
{ value: 'FIXED', label: 'Fixed' }
]
if (isFlex.value) options.push({ value: 'HUG', label: 'Hug' }) if (isFlex.value) options.push({ value: 'HUG', label: 'Hug' })
if (isInAutoLayout.value || isFlex.value) options.push({ value: 'FILL', label: 'Fill' }) if (isInAutoLayout.value || isFlex.value) options.push({ value: 'FILL', label: 'Fill' })
return options return options
}) })
const heightSizingOptions = computed(() => { const heightSizingOptions = computed(() => {
const options: { value: LayoutSizing; label: string }[] = [ const options: { value: LayoutSizing; label: string }[] = [{ value: 'FIXED', label: 'Fixed' }]
{ value: 'FIXED', label: 'Fixed' }
]
if (isFlex.value) options.push({ value: 'HUG', label: 'Hug' }) if (isFlex.value) options.push({ value: 'HUG', label: 'Hug' })
if (isInAutoLayout.value || isFlex.value) options.push({ value: 'FILL', label: 'Fill' }) if (isInAutoLayout.value || isFlex.value) options.push({ value: 'FILL', label: 'Fill' })
return options return options
@ -439,10 +435,15 @@ function trackLabel(track: GridTrack): string {
/> />
<button <button
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded border border-border bg-transparent text-muted hover:bg-hover hover:text-surface" class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded border border-border bg-transparent text-muted hover:bg-hover hover:text-surface"
:title="showIndividualPadding || !hasUniformPadding() ? 'Uniform padding' : 'Per-side padding'" :title="
showIndividualPadding || !hasUniformPadding() ? 'Uniform padding' : 'Per-side padding'
"
@click="showIndividualPadding = !showIndividualPadding" @click="showIndividualPadding = !showIndividualPadding"
> >
<icon-lucide-minus v-if="showIndividualPadding || !hasUniformPadding()" class="size-3" /> <icon-lucide-minus
v-if="showIndividualPadding || !hasUniformPadding()"
class="size-3"
/>
<icon-lucide-plus v-else class="size-3" /> <icon-lucide-plus v-else class="size-3" />
</button> </button>
</div> </div>

View file

@ -119,7 +119,7 @@ export function useCanvas(canvasRef: Ref<HTMLCanvasElement | null>, store: Edito
} }
} }
const glCtx = (canvas.getContext('webgl2') ?? null) const glCtx = canvas.getContext('webgl2') ?? null
renderer = new SkiaRenderer(ck, surface, glCtx) renderer = new SkiaRenderer(ck, surface, glCtx)
store.setCanvasKit(ck, renderer) store.setCanvasKit(ck, renderer)
void renderer.loadFonts().then(() => renderNow()) void renderer.loadFonts().then(() => renderNow())

View file

@ -15,8 +15,6 @@ import { AI_PROVIDERS, DEFAULT_AI_MODEL, DEFAULT_AI_PROVIDER } from '@open-penci
import type { AIProviderID } from '@open-pencil/core' import type { AIProviderID } from '@open-pencil/core'
import type { LanguageModel, UIMessage } from 'ai' import type { LanguageModel, UIMessage } from 'ai'
const STORAGE_PREFIX = 'open-pencil:' const STORAGE_PREFIX = 'open-pencil:'
const LEGACY_KEY_STORAGE = `${STORAGE_PREFIX}openrouter-api-key` const LEGACY_KEY_STORAGE = `${STORAGE_PREFIX}openrouter-api-key`

View file

@ -1,5 +1,5 @@
import { nextTick, ref, type Ref } from 'vue'
import { onClickOutside } from '@vueuse/core' import { onClickOutside } from '@vueuse/core'
import { nextTick, ref, type Ref } from 'vue'
export function useInlineRename<T extends string>(onCommit: (id: T, newName: string) => void) { export function useInlineRename<T extends string>(onCommit: (id: T, newName: string) => void) {
const editingId = ref<T | null>(null) const editingId = ref<T | null>(null)

View file

@ -14,7 +14,7 @@ export async function openFileDialog() {
}) })
if (!path) return if (!path) return
const bytes = await readFile(path) const bytes = await readFile(path)
const file = new File([bytes], (path).split('/').pop() ?? 'file.fig') const file = new File([bytes], path.split('/').pop() ?? 'file.fig')
await openFileInNewTab(file, undefined, path) await openFileInNewTab(file, undefined, path)
return return
} }