Merge pull request #683 from open-pencil/release-ui-polish

fix(ui): align Home controls and theme the Code editor
This commit is contained in:
Danila Poyarkov 2026-09-13 19:08:25 +03:00 committed by GitHub
commit 4e9ef085a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 124 additions and 30 deletions

View file

@ -215,6 +215,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.

View file

@ -24,7 +24,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.
@ -36,6 +36,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.

View file

@ -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:*",

View file

@ -91,6 +91,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:*",

View file

@ -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())
</script>

View file

@ -39,27 +39,32 @@ watch(
</script>
<template>
<div class="mb-4 flex flex-col gap-3 sm:mb-6 sm:flex-row sm:items-center">
<div class="mb-4 flex flex-col gap-3 md:mb-6 md:flex-row md:items-center">
<AppInput
ref="searchInput"
v-model="query"
type="search"
name="file-search"
autocomplete="off"
class="sm:flex-1"
class="md:flex-1"
:density="isMobile ? 'comfortable' : 'compact'"
:placeholder="isMobile ? files.searchFiles : files.searchRecentAndStorageFiles"
:aria-label="files.searchFiles"
>
<template #leading><icon-lucide-search class="size-4" /></template>
</AppInput>
<div class="grid grid-cols-2 gap-2 sm:contents">
<AppButton size="lg" variant="outline" data-test-id="home-open-file" @click="openFileDialog">
<div class="grid grid-cols-2 gap-2 md:contents">
<AppButton
:size="isMobile ? 'lg' : 'md'"
variant="outline"
data-test-id="home-open-file"
@click="openFileDialog"
>
<template #leading><icon-lucide-folder-open class="size-3.5" /></template>
{{ menu.open }}
</AppButton>
<AppButton
size="lg"
:size="isMobile ? 'lg' : 'md'"
color="primary"
variant="solid"
data-test-id="home-new-document"

99
src/theme/code/editor.ts Normal file
View file

@ -0,0 +1,99 @@
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'
import { EditorView } from '@codemirror/view'
import { tags } from '@lezer/highlight'
export function codeEditorTheme(dark: boolean) {
const palette = dark
? {
keyword: '#c4a7e7',
tag: '#82aaff',
string: '#a3d9a5',
number: '#e5bc8b',
comment: '#969eaa'
}
: {
keyword: '#7947a7',
tag: '#145ca4',
string: '#276b3b',
number: '#965212',
comment: '#69727f'
}
return [
EditorView.theme(
{
'&': {
height: '100%',
backgroundColor: 'var(--color-panel)',
color: 'var(--color-surface)',
fontSize: '12px'
},
'&.cm-focused': { outline: 'none' },
'.cm-scroller': { overflow: 'auto', fontFamily: 'var(--font-mono)', lineHeight: '1.65' },
'.cm-content': { padding: '10px 0', caretColor: 'var(--color-accent)' },
'.cm-line': { padding: '0 8px' },
'.cm-gutters': {
backgroundColor: 'var(--color-panel)',
color: 'var(--color-muted)',
border: 'none'
},
'.cm-lineNumbers .cm-gutterElement': { padding: '0 4px 0 8px', minWidth: '24px' },
'.cm-foldGutter .cm-gutterElement': { padding: '0 2px' },
'.cm-activeLine': {
backgroundColor: 'color-mix(in srgb, var(--color-hover) 45%, transparent)'
},
'.cm-activeLineGutter': {
backgroundColor: 'var(--color-hover)',
color: 'var(--color-surface)'
},
'.cm-cursor, .cm-dropCursor': { borderLeftColor: 'var(--color-accent)' },
'.cm-selectionBackground, &.cm-focused .cm-selectionBackground': {
backgroundColor: 'color-mix(in srgb, var(--color-accent) 22%, transparent)'
},
'&.cm-focused .cm-matchingBracket': {
backgroundColor: 'var(--color-hover)',
outline: '1px solid var(--color-border)'
},
'.cm-tooltip, .cm-panels': {
backgroundColor: 'var(--color-panel)',
color: 'var(--color-surface)',
border: '1px solid var(--color-border)'
},
'.cm-tooltip': { borderRadius: '6px', overflow: 'hidden' },
'.cm-tooltip-autocomplete > ul > li[aria-selected]': {
backgroundColor: 'var(--color-hover)',
color: 'var(--color-surface)'
},
'.cm-searchMatch': {
backgroundColor: 'color-mix(in srgb, var(--color-accent) 20%, transparent)',
outline: '1px solid var(--color-accent)'
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: 'color-mix(in srgb, var(--color-accent) 35%, transparent)'
},
'.cm-textfield, .cm-button': {
background: 'var(--color-input)',
color: 'var(--color-surface)',
border: '1px solid var(--color-border)',
borderRadius: '4px',
font: 'inherit'
}
},
{ dark }
),
syntaxHighlighting(
HighlightStyle.define([
{ tag: tags.keyword, color: palette.keyword },
{ tag: [tags.tagName, tags.typeName, tags.className], color: palette.tag },
{ tag: [tags.string, tags.attributeValue], color: palette.string },
{ tag: [tags.number, tags.bool, tags.null], color: palette.number },
{ tag: tags.comment, color: palette.comment, fontStyle: 'italic' },
{
tag: [tags.attributeName, tags.propertyName, tags.variableName],
color: 'var(--color-surface)'
},
{ tag: [tags.punctuation, tags.operator], color: 'var(--color-muted)' }
])
)
]
}