diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a145fc29..54ee3084b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,10 +3,6 @@ name: CI on: pull_request: branches: [master] - paths-ignore: - - 'packages/docs/**' - - 'openspec/**' - - '*.md' permissions: contents: read @@ -17,8 +13,52 @@ concurrency: cancel-in-progress: true jobs: + changes: + name: Classify changes + runs-on: ubuntu-latest + timeout-minutes: 3 + outputs: + scope: ${{ steps.classify.outputs.scope }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + + - name: Fetch comparison base + env: + CI_BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: git fetch --no-tags --depth=1 origin "$CI_BASE_SHA" + + - name: Select validation scope + id: classify + env: + CI_BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: bun tools/ci/src/classify.ts + + documentation: + name: Documentation + needs: changes + if: needs.changes.outputs.scope == 'docs' + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: ./.github/actions/setup-bun + - name: Build documentation type dependencies + run: bun run build:packages + - name: Validate documentation and generated references + run: bun run check:docs + - name: Build documentation and check examples + run: bun run docs:build + source-quality: name: Code quality + needs: changes + if: needs.changes.outputs.scope == 'code' timeout-minutes: 10 runs-on: ubuntu-latest steps: @@ -45,6 +85,8 @@ jobs: package-quality: name: Package integrity + needs: changes + if: needs.changes.outputs.scope == 'code' timeout-minutes: 10 runs-on: ubuntu-latest steps: @@ -68,6 +110,8 @@ jobs: repository-quality: name: Repository hygiene + needs: changes + if: needs.changes.outputs.scope == 'code' timeout-minutes: 10 runs-on: ubuntu-latest steps: @@ -97,6 +141,8 @@ jobs: storybook: name: Component workshop + needs: changes + if: needs.changes.outputs.scope == 'code' timeout-minutes: 10 runs-on: ubuntu-latest steps: @@ -114,6 +160,8 @@ jobs: native-test-contracts: name: Native app contracts + needs: changes + if: needs.changes.outputs.scope == 'code' timeout-minutes: 8 runs-on: ubuntu-24.04 container: @@ -142,6 +190,8 @@ jobs: run: cargo check --manifest-path desktop/Cargo.toml --features native-test unit-tests: + needs: changes + if: needs.changes.outputs.scope == 'code' timeout-minutes: 10 runs-on: ubuntu-latest strategy: @@ -172,3 +222,19 @@ jobs: bun test "${test_files[@]}" env: BUN_HEAVY_TESTS: 'false' + + result: + name: CI result + needs: [changes, documentation, source-quality, package-quality, repository-quality, storybook, native-test-contracts, unit-tests] + if: always() + runs-on: ubuntu-latest + timeout-minutes: 3 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + - name: Require successful checks for the selected scope + env: + CI_NEEDS: ${{ toJSON(needs) }} + run: bun tools/ci/src/gate.ts diff --git a/AGENTS.md b/AGENTS.md index 61c6c6403..cfd73d917 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,6 +86,8 @@ For releases, update versions in the root and publishable package manifests plus App/docs production workflows run on `v*` tags or `workflow_dispatch`, not ordinary `master` pushes. `ci.yml` and `heavy-tests.yml` define validation gates. +PR CI always classifies changed paths through `tools/ci/`. Docs-only changes run documentation integrity/reference checks and the docs build, not engine, browser, Storybook, or native suites. Runtime prompt Markdown, executable examples, configuration, and unknown paths require code validation. The aggregate `CI result` gate requires successful classification and every applicable job; failures, cancellations, and unexpected skips cannot pass. Do not restore workflow-level path filtering on required CI. + ## Documentation - `CHANGELOG.md` — curated user-facing changes by version; `Unreleased` stays first. @@ -217,6 +219,7 @@ Keep responsibilities distinct: engine tests cover state contracts, Playwright b - Colocate `ComponentName.stories.ts` with `ComponentName.vue`. Multipart composition stories may use a descriptive family name. Preserve explicit Storybook titles and exported story names during file moves; keep default playgrounds static and give interaction flows named stories. Use deterministic fixtures and colocated Vue demos for substantial markup. - `src/components/ui/**` is store-free app design-system code; feature controls stay in their domain. - SDK property primitives remain controlled/editor-agnostic. Compose property rows from `PanelGrid`, `PanelFieldGroup`, `PanelItemRow`, and `PropertyItemRow`; use `BindableValue`, `FillRoot`, and `FillSwatch` rather than rebuilding binding/picker infrastructure. +- Do not add automated tests or snapshot baselines for simple CSS-only UI changes, including spacing, sizing, colors, and responsive breakpoints. Verify these visually instead. Keep automated coverage focused on behavior and contracts; the separate canvas-renderer visual coverage requirement still applies. - Prefer accessible role/name, label, then text in tests. Use scoped `data-slot` anatomy or semantic attributes (`data-property`, `data-command`, `data-node-id`) when needed; reserve `data-test-id` for integration boundaries and never add test-hook props. - Use Reka UI primitives and typed Tailwind Variants themes under `src/theme/**`; merge per-instance `ui` slot overrides, expose `class` for single-root components, and do not add one-off class props. Use `UI` casing in type names. - Bind visual state through semantic `data-*` attributes; Steiger rejects template-time `use*UI()`, visual-state utility branches, and raw SVG app icons. diff --git a/CHANGELOG.md b/CHANGELOG.md index 726d52611..8c0f763a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ - Add local AI usage and technical diagnostics, including token telemetry, provider/model summaries, recent failures, configurable retention, export, and clear controls. (#588) - Import, render, edit, resize, select, and export Figma text-on-path layers while preserving their curved glyph layout. - Show temporary Figma-style distance measurements between selected and Option/Alt-hovered layers. (#491) -- Edit Design JSX and HTML/CSS previews in CodeMirror, with Tailwind viewing, completion, diagnostics, bounded execution, and session-level undo. (#130) +- Edit Design JSX and HTML/CSS previews in CodeMirror, with theme-aware highlighting, Tailwind viewing, completion, diagnostics, bounded execution, and session-level undo. (#130) - Set provider-specific reasoning effort on supported AI model profiles. (#454) - Show unavailable or substituted document fonts with affected-layer selection and retry actions, and expose font fidelity through the Figma API and MCP tooling. (#503) - Add reusable remote MCP connections for ACP agents, with Streamable HTTP endpoints and credential-backed bearer tokens. @@ -37,6 +37,7 @@ ### Changed +- Use compact desktop Home search actions with consistent responsive layout and control sizing. - Keep applied and available Effect styles concise, and collapse equal independent corner fields when all four use the same variable. - Keep pixel-grid rounding invisible while showing alignment guides only for real geometry, objects, and canvas/layout guides. - Copy selections with embedded images into Figma while preserving typed geometry, text sizing, images, components, variables, modes, and shared styles for lossless in-app paste. diff --git a/bun.lock b/bun.lock index 4bf207b48..9fcc9b0bb 100644 --- a/bun.lock +++ b/bun.lock @@ -23,6 +23,7 @@ "@codemirror/search": "^6.7.1", "@codemirror/state": "^6.7.1", "@codemirror/view": "^6.43.9", + "@lezer/highlight": "^1.2.3", "@nanostores/i18n": "^1.3.3", "@nanostores/vue": "^1.1.0", "@open-pencil/cli": "workspace:*", diff --git a/package.json b/package.json index bab7702dd..817bab30b 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "@codemirror/search": "^6.7.1", "@codemirror/state": "^6.7.1", "@codemirror/view": "^6.43.9", + "@lezer/highlight": "^1.2.3", "@nanostores/i18n": "^1.3.3", "@nanostores/vue": "^1.1.0", "@open-pencil/cli": "workspace:*", diff --git a/packages/core/src/design-jsx/tree.ts b/packages/core/src/design-jsx/tree.ts index cadc5e708..3d50fd647 100644 --- a/packages/core/src/design-jsx/tree.ts +++ b/packages/core/src/design-jsx/tree.ts @@ -95,6 +95,7 @@ export type StyleProps = { gap?: number | DesignVariable wrap?: boolean rowGap?: number | DesignVariable + columnGap?: number | DesignVariable justify?: 'start' | 'end' | 'center' | 'between' justifyContent?: 'start' | 'end' | 'center' | 'between' items?: 'start' | 'end' | 'center' | 'stretch' @@ -145,6 +146,8 @@ export type StyleProps = { size?: number | DesignVariable fontSize?: number | DesignVariable + lineHeight?: number | DesignVariable + letterSpacing?: number | DesignVariable font?: string fontFamily?: string weight?: number | 'bold' | 'medium' | 'normal' diff --git a/packages/docs/programmable/sdk/api/components/bindable-value.md b/packages/docs/programmable/sdk/api/components/bindable-value.md index 5f5475456..bd256fd9f 100644 --- a/packages/docs/programmable/sdk/api/components/bindable-value.md +++ b/packages/docs/programmable/sdk/api/components/bindable-value.md @@ -49,6 +49,7 @@ const variable: Variable = { const values = new Map([[variable.id, 16]]) const bindings = new Map() const getBindingId = (target: BindingTarget) => bindings.get(`${target.nodeId}:${target.path}`) +const resolve: BindingProvider['resolve'] = id => values.get(id) const provider: BindingProvider = { listVariables: () => [variable], @@ -59,9 +60,12 @@ const provider: BindingProvider = { const ids = new Set(targets.map(getBindingId)) if (ids.size === 0 || (ids.size === 1 && ids.has(undefined))) return 'unbound' if (ids.size > 1) return 'mixed' - return ids.has(variable.id) ? 'bound' : 'unresolved' + if (!ids.has(variable.id)) return 'unresolved' + const resolved = targets.map(target => resolve(variable.id, target)) + if (resolved.some(value => value === undefined)) return 'unresolved' + return new Set(resolved).size > 1 ? 'mixed' : 'bound' }, - resolve: id => values.get(id), + resolve, bind: (target: BindingTarget, variableId) => { bindings.set(`${target.nodeId}:${target.path}`, variableId) }, diff --git a/src/components/code-editor/CodeEditor.vue b/src/components/code-editor/CodeEditor.vue index 255587611..7a7ef4298 100644 --- a/src/components/code-editor/CodeEditor.vue +++ b/src/components/code-editor/CodeEditor.vue @@ -3,14 +3,7 @@ import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirro import { defaultKeymap, history, historyKeymap, redo, undo } from '@codemirror/commands' import { html } from '@codemirror/lang-html' import { javascript } from '@codemirror/lang-javascript' -import { - bracketMatching, - defaultHighlightStyle, - foldGutter, - foldKeymap, - indentOnInput, - syntaxHighlighting -} from '@codemirror/language' +import { bracketMatching, foldGutter, foldKeymap, indentOnInput } from '@codemirror/language' import { lintKeymap } from '@codemirror/lint' import { searchKeymap } from '@codemirror/search' import { Compartment, EditorState, Transaction, type Extension } from '@codemirror/state' @@ -25,8 +18,10 @@ import { } from '@codemirror/view' import { onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue' +import { resolvedAppTheme } from '@/app/shell/theme' import { designJSXExtensions } from '@/components/code-editor/extensions' import type { CodeEditorLanguage } from '@/components/code-editor/types' +import { codeEditorTheme } from '@/theme/code/editor' const { modelValue, @@ -48,6 +43,7 @@ const host = useTemplateRef('host') const languageCompartment = new Compartment() const editableCompartment = new Compartment() const labelCompartment = new Compartment() +const themeCompartment = new Compartment() let editor: EditorView | undefined let externalUpdate = false @@ -78,7 +74,7 @@ onMounted(() => { drawSelection(), EditorState.allowMultipleSelections.of(true), indentOnInput(), - syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + themeCompartment.of(codeEditorTheme(resolvedAppTheme.value === 'dark')), bracketMatching(), closeBrackets(), highlightActiveLine(), @@ -97,21 +93,7 @@ onMounted(() => { editableCompartment.of(editableExtensions(readOnly)), labelCompartment.of(EditorView.contentAttributes.of({ 'aria-label': label })), EditorView.lineWrapping, - EditorView.theme({ - '&': { height: '100%', backgroundColor: 'transparent', color: 'var(--color-surface)' }, - '.cm-scroller': { overflow: 'auto', fontFamily: 'var(--font-mono)' }, - '.cm-content': { padding: '12px 0', caretColor: 'var(--color-accent)' }, - '.cm-line': { padding: '0 12px' }, - '.cm-gutters': { - backgroundColor: 'transparent', - color: 'color-mix(in srgb, var(--color-muted) 45%, transparent)', - border: 'none' - }, - '&.cm-focused': { outline: 'none' }, - '.cm-selectionBackground, &.cm-focused .cm-selectionBackground': { - backgroundColor: 'color-mix(in srgb, var(--color-accent) 22%, transparent)' - } - }), + EditorView.updateListener.of((update) => { if (!update.docChanged || externalUpdate) return emit('update:modelValue', update.state.doc.toString()) @@ -155,6 +137,10 @@ watch( }) ) +watch(resolvedAppTheme, (theme) => + editor?.dispatch({ effects: themeCompartment.reconfigure(codeEditorTheme(theme === 'dark')) }) +) + onBeforeUnmount(() => editor?.destroy()) diff --git a/src/components/home/search/HomeSearchActions.vue b/src/components/home/search/HomeSearchActions.vue index e78d3d9b8..a9ac4e919 100644 --- a/src/components/home/search/HomeSearchActions.vue +++ b/src/components/home/search/HomeSearchActions.vue @@ -39,27 +39,32 @@ watch(