feat(assets): add new icon files for application branding

- Introduce new icon files in various formats (ICNS, ICO, PNG) for application branding.
- Add logo image for the Electron interface to enhance visual identity.
- Update canvas selection logic to allow programmatic selection from the layer panel.
This commit is contained in:
Kayshen-X 2026-02-21 12:14:02 +08:00
parent 6006c10c24
commit 7273ca49ba
9 changed files with 42 additions and 57 deletions

BIN
build/icon.icns Normal file

Binary file not shown.

BIN
build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

BIN
electron/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View file

@ -5,6 +5,16 @@ import { useCanvasStore } from '@/stores/canvas-store'
import type { FabricObjectWithPenId } from './canvas-object-factory'
import { resolveTargetAtDepth } from './selection-context'
/**
* When true, the next selection event will skip depth-resolution and
* pass through as-is. Used by the layer panel to programmatically select
* children without the handler resolving them back to their parent.
*/
let skipNextDepthResolve = false
export function setSkipNextDepthResolve() {
skipNextDepthResolve = true
}
/**
* Resolve a list of Fabric selected objects to node IDs at the current
* entered-frame depth. If any target falls outside the current context,
@ -52,6 +62,12 @@ export function useCanvasSelection() {
const handleSelection = (e: { selected?: FabricObject[]; e?: unknown }) => {
if (updatingSelection) return
// Programmatic selection from layer panel — skip depth resolution
if (skipNextDepthResolve) {
skipNextDepthResolve = false
return
}
// `selection:updated` payload `selected` may contain only delta objects.
// Always read the full active selection from canvas for accurate multi-select.
const selected = canvas.getActiveObjects()

View file

@ -1,6 +1,7 @@
import { useState, useRef, useCallback } from 'react'
import { useDocumentStore, findNodeInTree } from '@/stores/document-store'
import { useCanvasStore } from '@/stores/canvas-store'
import { setSkipNextDepthResolve } from '@/canvas/use-canvas-selection'
import type { FabricObjectWithPenId } from '@/canvas/canvas-object-factory'
import type { PenNode } from '@/types/pen'
import LayerItem from './layer-item'
@ -154,6 +155,7 @@ export default function LayerPanel() {
(o) => (o as FabricObjectWithPenId).penNodeId === id,
)
if (target) {
setSkipNextDepthResolve()
fabricCanvas.setActiveObject(target)
fabricCanvas.requestRenderAll()
}

View file

@ -82,25 +82,8 @@ NEVER output HTML, CSS, or React code — ONLY PenNode JSON.
NEVER use tools, functions, or external calls. Design everything URSELF in the response.
NEVER say "I will create..." or "Here is the design..." START DIRECTLY WITH <step>.
PROCESS VISUALIZATION (Deep Agent Simulation):
You MUST output your thought process as structured XML steps BEFORE the final JSON.
Follow this EXACT sequence of steps:
1. <step title="Checking guidelines">
[Analyze the request against the Industrial Design System. Quote specific rules you will follow.]
</step>
2. <step title="Getting editor state">
[Simulate checking the context. Mention "pencil-new.pen" and "No reusable components found".]
</step>
3. <step title="Picked a styleguide">
[Briefly summarize the chosen style: "Industrial × Technical Mobile Dashboard". Mention "Space Grotesk", "Roboto Mono", and "Terminal Green".]
</step>
4. <step title="Design">
[The final generation step. Describe the high-level layout structure you are building: "Building layout with sidebar, header, and KPI section..."]
</step>
You may include 1-2 brief <step> tags before the JSON (optional, keep them SHORT one line each).
Start generating JSON as quickly as possible minimize preamble.
When a user asks non-design questions (explain, suggest colors, give advice), respond in text.
@ -138,23 +121,16 @@ export const DESIGN_GENERATOR_PROMPT = `You are a PenNode JSON generation engine
${PEN_NODE_SCHEMA}
OUTPUT FORMAT:
1. Start immediately with <step title="Checking guidelines">...</step>.
2. You may include additional <step ...>...</step> lines.
3. Output progressive JSON blocks in sequence:
- Phase 1 (Structure): one ${BLOCK}json block with root frame + main layout skeleton.
- Phase 2 (Content): one ${BLOCK}json block that uses the SAME IDs and fills content/details.
- Phase 3 (Refine, optional): one ${BLOCK}json block for minor style/spacing fixes.
4. Add a 1-2 sentence summary after the final JSON block.
1. You may include 1-2 brief <step> tags (optional, keep them SHORT one line each).
2. Output a SINGLE ${BLOCK}json code block containing the COMPLETE design as a PenNode JSON array.
3. Add a 1-sentence summary after the JSON block.
CRITICAL RULES:
- Progressive generation is required when possible (at least 2 JSON blocks for UI screens).
- Reuse the same node IDs across phases so partial upsert can update existing nodes.
- Each later phase should be more complete than the previous one.
- After Phase 1, do NOT move already placed sections unless explicitly fixing overlap.
- Keep root frame x/y/width/height stable across phases.
- Output ONE complete JSON block with ALL nodes do NOT split into multiple phases.
- Use a single root frame containing ALL elements as children.
- Keep IDs unique and stable across all phases.
- Keep IDs unique and descriptive.
- DO NOT WRITE ANY INTRODUCTORY TEXT.
- Start generating JSON as quickly as possible minimize preamble.
DO NOT output bullet points, design descriptions, or explanations BEFORE the JSON (except <step> tags).
DO NOT describe what you plan to create just CREATE IT as JSON.

View file

@ -25,7 +25,6 @@ export async function* streamChat(
const controller = new AbortController()
let abortReason: 'hard_timeout' | 'no_text_timeout' | null = null
let hasNonEmptyText = false
let noTextTimeout: ReturnType<typeof setTimeout> | null = null
const clearNoTextTimeout = () => {
@ -35,17 +34,20 @@ export async function* streamChat(
}
}
const resetActivityTimeout = () => {
clearNoTextTimeout()
noTextTimeout = setTimeout(() => {
abortReason = 'no_text_timeout'
controller.abort()
}, noTextTimeoutMs)
}
const hardTimeout = setTimeout(() => {
abortReason = 'hard_timeout'
controller.abort()
}, hardTimeoutMs)
noTextTimeout = setTimeout(() => {
if (!hasNonEmptyText) {
abortReason = 'no_text_timeout'
controller.abort()
}
}, noTextTimeoutMs)
resetActivityTimeout()
try {
const response = await fetch('/api/ai/chat', {
@ -101,15 +103,9 @@ export async function* streamChat(
return
}
// Keep-alive pings from server — reset timeout but don't yield
// Keep-alive pings from server — reset activity timeout but don't yield
if (chunk.type === 'ping') {
clearNoTextTimeout()
noTextTimeout = setTimeout(() => {
if (!hasNonEmptyText) {
abortReason = 'no_text_timeout'
controller.abort()
}
}, noTextTimeoutMs)
resetActivityTimeout()
continue
}
@ -119,8 +115,7 @@ export async function* streamChat(
// Any non-empty content (text or thinking) counts as activity
if ((chunk.type === 'text' || chunk.type === 'thinking') && chunk.content.trim().length > 0) {
hasNonEmptyText = true
clearNoTextTimeout()
resetActivityTimeout()
}
yield chunk
@ -157,14 +152,10 @@ export async function* streamChat(
clearNoTextTimeout()
return
}
if ((chunk.type === 'text' || chunk.type === 'thinking') && chunk.content.trim().length > 0) {
hasNonEmptyText = true
clearNoTextTimeout()
}
clearTimeout(hardTimeout)
clearNoTextTimeout()
yield chunk
if (chunk.type === 'error') {
clearTimeout(hardTimeout)
clearNoTextTimeout()
return
}
} catch {

View file

@ -12,8 +12,8 @@ import {
} from './design-animation'
const DESIGN_STREAM_TIMEOUTS = {
hardTimeoutMs: 300_000,
noTextTimeoutMs: 120_000,
hardTimeoutMs: 180_000,
noTextTimeoutMs: 60_000,
}
// ---------------------------------------------------------------------------