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:
parent
59981709ba
commit
7f5a388e2b
110
.pi/context.md
Normal file
110
.pi/context.md
Normal 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.
|
||||
|
|
@ -161,8 +161,6 @@ function importPages(
|
|||
created: Set<string>,
|
||||
createSceneNode: (ncId: string, graphParentId: string) => void
|
||||
): void {
|
||||
const getChildren = (ncId: string): string[] => childrenMap.get(ncId) ?? []
|
||||
|
||||
let docId: string | null = null
|
||||
for (const [id, nc] of changeMap) {
|
||||
if (nc.type === 'DOCUMENT' || id === '0:0') {
|
||||
|
|
@ -172,14 +170,14 @@ function importPages(
|
|||
}
|
||||
|
||||
if (docId) {
|
||||
for (const canvasId of getChildren(docId)) {
|
||||
for (const canvasId of childrenMap.get(docId) ?? []) {
|
||||
const canvasNc = changeMap.get(canvasId)
|
||||
if (!canvasNc) continue
|
||||
if (canvasNc.type === 'CANVAS') {
|
||||
const page = graph.addPage(canvasNc.name ?? 'Page')
|
||||
if (canvasNc.internalOnly) page.internalOnly = true
|
||||
created.add(canvasId)
|
||||
for (const childId of getChildren(canvasId)) {
|
||||
for (const childId of childrenMap.get(canvasId) ?? []) {
|
||||
createSceneNode(childId, page.id)
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -270,6 +270,7 @@ export function populateAndApplyOverrides(
|
|||
) {
|
||||
for (const node of graph.getAllNodes()) {
|
||||
if (node.type !== 'INSTANCE') continue
|
||||
// Apply assignments from the instance's own kiwi data first
|
||||
const ownFigmaId = nodeIdToGuid.get(node.id)
|
||||
if (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
|
||||
const sourceFigmaId = nodeIdToGuid.get(node.componentId)
|
||||
if (!sourceFigmaId) continue
|
||||
|
|
@ -459,6 +462,9 @@ export function populateAndApplyOverrides(
|
|||
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>) {
|
||||
if (dsdModified.size === 0) return
|
||||
|
||||
|
|
@ -643,6 +649,7 @@ export function populateAndApplyOverrides(
|
|||
const node = graph.getNode(cloneId)
|
||||
if (!node) continue
|
||||
|
||||
// Don't overwrite nodes directly targeted by symbolOverrides
|
||||
if (seeds.has(cloneId)) {
|
||||
syncQueue.push(cloneId)
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -557,8 +557,6 @@ export function nodeChangeToProps(
|
|||
let nodeType = mapNodeType(nc.type)
|
||||
if (nodeType === 'FRAME' && isComponentSet(nc)) nodeType = 'COMPONENT_SET'
|
||||
|
||||
const dashPattern = nc.dashPattern ?? []
|
||||
|
||||
return {
|
||||
nodeType,
|
||||
name: nc.name ?? nodeType,
|
||||
|
|
@ -568,7 +566,7 @@ export function nodeChangeToProps(
|
|||
locked: nc.locked ?? false,
|
||||
blendMode: (nc.blendMode as Fill['blendMode']) ?? 'PASS_THROUGH',
|
||||
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),
|
||||
...convertCornerProps(nc),
|
||||
...convertTextProps(nc),
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ function applyTextOverrides(props: Record<string, unknown>, o: Partial<SceneNode
|
|||
: '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.innerRadius !== undefined) o.starInnerRadius = props.innerRadius 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)
|
||||
applyLayoutOverrides(props, o, w, h)
|
||||
if (isText) applyTextOverrides(props, o)
|
||||
applyEffectOverrides(props, o)
|
||||
applyShapeAndEffectOverrides(props, o)
|
||||
|
||||
return o
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { colorToHex, colorToHex8 } from './color'
|
||||
import { round } from './svg-export-paths'
|
||||
import { colorToHex, colorToHex8 } from '../color'
|
||||
import { round } from './paths'
|
||||
|
||||
import { svg } from './svg-node'
|
||||
import { svg } from '../svg-node'
|
||||
|
||||
import type { SVGNode } from './svg-node'
|
||||
import type { SceneGraph, SceneNode, Fill, Effect } from './scene-graph'
|
||||
import type { Color } from './types'
|
||||
import type { SVGNode } from '../svg-node'
|
||||
import type { SceneGraph, SceneNode, Fill, Effect } from '../scene-graph'
|
||||
import type { Color } from '../types'
|
||||
|
||||
export interface SVGExportContext {
|
||||
defs: SVGNode[]
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { computeContentBounds } from './render-image'
|
||||
import { computeContentBounds } from '../render-image'
|
||||
import {
|
||||
round,
|
||||
geometryBlobToSVGPath,
|
||||
|
|
@ -7,7 +7,7 @@ import {
|
|||
hasRadius,
|
||||
roundedRectPath,
|
||||
arcPath
|
||||
} from './svg-export-paths'
|
||||
} from './paths'
|
||||
import {
|
||||
nextDefId,
|
||||
formatColor,
|
||||
|
|
@ -16,15 +16,15 @@ import {
|
|||
SVG_STROKE_CAP,
|
||||
SVG_STROKE_JOIN,
|
||||
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 { SceneGraph, SceneNode, Fill, Stroke, CharacterStyleOverride } from './scene-graph'
|
||||
import type { SVGExportContext } from './svg-export-defs'
|
||||
import type { SVGNode } from '../svg-node'
|
||||
import type { SceneGraph, SceneNode, Fill, Stroke, CharacterStyleOverride } from '../scene-graph'
|
||||
import type { SVGExportContext } from './defs'
|
||||
|
||||
// --- Node rendering ---
|
||||
|
||||
|
|
@ -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_MOVE_TO = 1
|
||||
|
|
@ -237,7 +237,7 @@ export const exportSvg = defineTool({
|
|||
}
|
||||
},
|
||||
execute: async (figma, args) => {
|
||||
const { renderNodesToSVG } = await import('../svg-export.js')
|
||||
const { renderNodesToSVG } = await import('../svg-export/index.js')
|
||||
const pageId = figma.currentPageId
|
||||
const ids =
|
||||
args.ids && args.ids.length > 0
|
||||
|
|
|
|||
|
|
@ -101,7 +101,9 @@ export function startAutomationBridge(server: ViteServer) {
|
|||
authToken = null
|
||||
|
||||
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', () => {
|
||||
|
|
|
|||
|
|
@ -122,7 +122,9 @@ export function connectAutomation(getStore: () => EditorStore) {
|
|||
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,
|
||||
tool: handleTool,
|
||||
export: handleExport,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ function handleSubmit(text: string) {
|
|||
const c = ensureChat()
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ const store = useEditorStore()
|
|||
const collab = useCollabInjected()
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
|
||||
const { hitTestSectionTitle, hitTestComponentLabel, hitTestFrameTitle } = useCanvas(canvasRef, store)
|
||||
const { hitTestSectionTitle, hitTestComponentLabel, hitTestFrameTitle } = useCanvas(
|
||||
canvasRef,
|
||||
store
|
||||
)
|
||||
const { cursorOverride } = useCanvasInput(
|
||||
canvasRef,
|
||||
store,
|
||||
|
|
|
|||
|
|
@ -87,9 +87,7 @@ function onColorUpdate(color: Color) {
|
|||
function setCategory(cat: FillCategory) {
|
||||
if (cat === fillCategory.value) return
|
||||
if (cat === 'SOLID') {
|
||||
const color = fill.gradientStops?.length
|
||||
? { ...fill.gradientStops[0].color }
|
||||
: fill.color
|
||||
const color = fill.gradientStops?.length ? { ...fill.gradientStops[0].color } : fill.color
|
||||
emit('update', { ...fill, type: 'SOLID', color })
|
||||
} else if (cat === 'GRADIENT') {
|
||||
const type: GradientSubtype = 'GRADIENT_LINEAR'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import type {
|
||||
ListboxFilter} from 'reka-ui';
|
||||
import type { ListboxFilter } from 'reka-ui'
|
||||
import {
|
||||
ListboxContent,
|
||||
ListboxItem,
|
||||
|
|
|
|||
|
|
@ -348,10 +348,7 @@ function updateDropTarget(ev: PointerEvent) {
|
|||
<icon-lucide-chevron-right class="size-3" />
|
||||
</span>
|
||||
<span v-else class="w-4 shrink-0" />
|
||||
<component
|
||||
:is="nodeIcon(item.value)"
|
||||
class="size-3 shrink-0 opacity-70"
|
||||
/>
|
||||
<component :is="nodeIcon(item.value)" class="size-3 shrink-0 opacity-70" />
|
||||
<input
|
||||
:ref="
|
||||
(el) => {
|
||||
|
|
|
|||
|
|
@ -151,9 +151,7 @@ const onlineCount = computed(() => collabPeers.length + 1)
|
|||
v-for="peer in collabPeers"
|
||||
:key="peer.clientId"
|
||||
class="flex cursor-pointer items-center gap-2 rounded-md px-0.5 py-0.5 select-none active:bg-hover"
|
||||
@click="
|
||||
emit('follow', followingPeer === peer.clientId ? null : peer.clientId)
|
||||
"
|
||||
@click="emit('follow', followingPeer === peer.clientId ? null : peer.clientId)"
|
||||
>
|
||||
<div
|
||||
class="flex size-7 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,17 @@
|
|||
import { ref, computed } from 'vue'
|
||||
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
|
||||
min?: number
|
||||
max?: number
|
||||
|
|
|
|||
|
|
@ -17,8 +17,12 @@ function toolDisplayName(part: ToolPart): string {
|
|||
}
|
||||
|
||||
function hasErrorOutput(part: ToolPart): boolean {
|
||||
return part.state === 'output-available' &&
|
||||
typeof part.output === 'object' && part.output !== null && 'error' in part.output
|
||||
return (
|
||||
part.state === 'output-available' &&
|
||||
typeof part.output === 'object' &&
|
||||
part.output !== null &&
|
||||
'error' in part.output
|
||||
)
|
||||
}
|
||||
|
||||
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-for="(part, i) in message.parts" :key="partKey(part, i)">
|
||||
<!-- Tool call -->
|
||||
<div
|
||||
v-if="isToolUIPart(part)"
|
||||
class="rounded-lg border border-border bg-canvas p-2"
|
||||
>
|
||||
<div v-if="isToolUIPart(part)" class="rounded-lg border border-border bg-canvas p-2">
|
||||
<CollapsibleRoot>
|
||||
<CollapsibleTrigger
|
||||
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'"
|
||||
class="size-3 animate-spin"
|
||||
/>
|
||||
<icon-lucide-check
|
||||
v-else-if="toolState(part) === 'done'"
|
||||
class="size-3"
|
||||
/>
|
||||
<icon-lucide-check v-else-if="toolState(part) === 'done'" class="size-3" />
|
||||
<icon-lucide-triangle-alert v-else class="size-3" />
|
||||
</div>
|
||||
<span class="text-[11px] text-surface">
|
||||
|
|
@ -106,11 +104,7 @@ function partKey(part: UIMessagePart, index: number): string {
|
|||
data-test-id="chat-text-bubble"
|
||||
class="rounded-xl rounded-tl-md bg-hover px-3 py-2 text-xs leading-relaxed text-surface"
|
||||
>
|
||||
<Markdown
|
||||
:content="part.text"
|
||||
:mermaid="false"
|
||||
class="chat-markdown"
|
||||
/>
|
||||
<Markdown :content="part.text" :mermaid="false" class="chat-markdown" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
|
@ -121,7 +115,12 @@ function partKey(part: UIMessagePart, index: number): string {
|
|||
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"
|
||||
>
|
||||
{{ message.parts.filter(isTextUIPart).map((p) => p.text).join('') }}
|
||||
{{
|
||||
message.parts
|
||||
.filter(isTextUIPart)
|
||||
.map((p) => p.text)
|
||||
.join('')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,10 +26,7 @@ function save() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-test-id="provider-setup"
|
||||
class="flex flex-1 flex-col items-center justify-center px-6"
|
||||
>
|
||||
<div 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" />
|
||||
<p class="mb-5 text-center text-xs text-muted">Connect an AI provider to start chatting.</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -97,18 +97,14 @@ function commitUniformPadding(_value: number, previous: number) {
|
|||
}
|
||||
|
||||
const widthSizingOptions = computed(() => {
|
||||
const options: { value: LayoutSizing; label: string }[] = [
|
||||
{ value: 'FIXED', label: 'Fixed' }
|
||||
]
|
||||
const options: { value: LayoutSizing; label: string }[] = [{ value: 'FIXED', label: 'Fixed' }]
|
||||
if (isFlex.value) options.push({ value: 'HUG', label: 'Hug' })
|
||||
if (isInAutoLayout.value || isFlex.value) options.push({ value: 'FILL', label: 'Fill' })
|
||||
return options
|
||||
})
|
||||
|
||||
const heightSizingOptions = computed(() => {
|
||||
const options: { value: LayoutSizing; label: string }[] = [
|
||||
{ value: 'FIXED', label: 'Fixed' }
|
||||
]
|
||||
const options: { value: LayoutSizing; label: string }[] = [{ value: 'FIXED', label: 'Fixed' }]
|
||||
if (isFlex.value) options.push({ value: 'HUG', label: 'Hug' })
|
||||
if (isInAutoLayout.value || isFlex.value) options.push({ value: 'FILL', label: 'Fill' })
|
||||
return options
|
||||
|
|
@ -439,10 +435,15 @@ function trackLabel(track: GridTrack): string {
|
|||
/>
|
||||
<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"
|
||||
:title="showIndividualPadding || !hasUniformPadding() ? 'Uniform padding' : 'Per-side padding'"
|
||||
:title="
|
||||
showIndividualPadding || !hasUniformPadding() ? 'Uniform padding' : 'Per-side padding'
|
||||
"
|
||||
@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" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
store.setCanvasKit(ck, renderer)
|
||||
void renderer.loadFonts().then(() => renderNow())
|
||||
|
|
|
|||
|
|
@ -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 { LanguageModel, UIMessage } from 'ai'
|
||||
|
||||
|
||||
|
||||
const STORAGE_PREFIX = 'open-pencil:'
|
||||
const LEGACY_KEY_STORAGE = `${STORAGE_PREFIX}openrouter-api-key`
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { nextTick, ref, type Ref } from 'vue'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { nextTick, ref, type Ref } from 'vue'
|
||||
|
||||
export function useInlineRename<T extends string>(onCommit: (id: T, newName: string) => void) {
|
||||
const editingId = ref<T | null>(null)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export async function openFileDialog() {
|
|||
})
|
||||
if (!path) return
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue