Merge branch 'up_master' into feat/vector-editor
# Conflicts: # CHANGELOG.md # src/composables/use-keyboard.ts
This commit is contained in:
commit
6ad25297df
|
|
@ -9,6 +9,8 @@
|
|||
- Nested container navigation — each double-click goes one level deeper
|
||||
- Dashed border around entered container for visual feedback
|
||||
- Layer panel click syncs canvas scope automatically
|
||||
- Vue SDK internationalization primitives — `useI18n()`, locale detection, persisted locale selection, lazy-loaded locale JSON files, and exported locale metadata for custom editor shells
|
||||
- App language picker in the menu bar — switch UI locale without reloading
|
||||
- Added a vector curve editor and improved drawing experience with the pen tool
|
||||
- Resume pen drawing from existing open path endpoints — click an endpoint to continue the curve
|
||||
- Close open paths by dragging one endpoint to the other
|
||||
|
|
@ -16,6 +18,13 @@
|
|||
### Fixes
|
||||
|
||||
- Fix imported `.fig` file open and page-switch regressions — loaded documents now keep graph/store state in sync, remap imported canvas/page children correctly, and recompute imported auto-layout descendants when switching pages
|
||||
- Fix first canvas render happening before fonts load — wait for fonts before the initial draw to avoid Safari and text measurement glitches
|
||||
- Preserve `fig-kiwi` version on `.fig` roundtrip — imports keep the original header version instead of rewriting everything to a hardcoded value; new files default to version 101
|
||||
- Normalize auto-layout text export for Figma — text children inside auto-layout frames now serialize with `NONE` auto-resize to match Figma behavior and avoid overflow on reimport
|
||||
- Fix keyboard editing regressions after the refactor — canvas shortcuts no longer fire while editing text, and Delete/Backspace no longer delete nodes during text entry
|
||||
- Fix MCP page switching persistence — `switch_page` now survives across tool calls in the same session
|
||||
- Improve CJK font fallback coverage — load multiple Google Fonts for broader Han/Japanese/Korean text support
|
||||
- Normalize more visible UI strings for localized app chrome — menus, panels, variables dialog, code panel, chat setup, and editor controls now respect the selected locale instead of falling back to English in common flows
|
||||
- Fix imported text rendering in browser and headless export — preserve stored bounds until fonts are ready, restore missing font-loaded guards, use natural width for `WIDTH_AND_HEIGHT` text, and clip text to node bounds
|
||||
- Fix browser/headless rendering mismatch for imported toolbar/instance content by correcting runtime imported layout recomputation instead of diverging browser rendering behavior
|
||||
- Fix `set_layout` tool not defaulting to HUG sizing when enabling auto-layout — frames now shrink/grow to fit children instead of keeping fixed dimensions
|
||||
|
|
|
|||
9
bun.lock
9
bun.lock
|
|
@ -153,7 +153,10 @@
|
|||
"name": "@open-pencil/vue",
|
||||
"version": "0.10.0",
|
||||
"dependencies": {
|
||||
"@nanostores/i18n": "^1.2.2",
|
||||
"@nanostores/vue": "^1.1.0",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"nanostores": "^1.2.0",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@open-pencil/core": "workspace:*",
|
||||
|
|
@ -676,6 +679,10 @@
|
|||
|
||||
"@multiformats/multiaddr-to-uri": ["@multiformats/multiaddr-to-uri@11.0.2", "", { "dependencies": { "@multiformats/multiaddr": "^12.3.0" } }, "sha512-SiLFD54zeOJ0qMgo9xv1Tl9O5YktDKAVDP4q4hL16mSq4O4sfFNagNADz8eAofxd6TfQUzGQ3TkRRG9IY2uHRg=="],
|
||||
|
||||
"@nanostores/i18n": ["@nanostores/i18n@1.2.2", "", { "peerDependencies": { "nanostores": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^1.0.0" } }, "sha512-5LLxl95+ZI46MrM/Kn7YjORKsD7+Xy2tgjZ7/oDT/BGPEiaBM9lK89/afeK+BqaQL0Xd9Xaa5MPuuVSyWAo+/w=="],
|
||||
|
||||
"@nanostores/vue": ["@nanostores/vue@1.1.0", "", { "peerDependencies": { "@nanostores/logger": "^0.4.0 || ^1.0.0", "@vue/devtools-api": ">=7.6.2", "nanostores": "^0.11.3 || ^1.0.0", "vue": ">=3.3.1" }, "optionalPeers": ["@nanostores/logger", "@vue/devtools-api"] }, "sha512-14eY8/YA0AbtWkAjk8zHKxbjcuPubF+M7y9afQzg2b5VqRJcashwA0100vprau13BTBSynUmU6ZsU6x03YidjA=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
|
||||
|
|
@ -1976,6 +1983,8 @@
|
|||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"nanostores": ["nanostores@1.2.0", "", {}, "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^14.2.1"
|
||||
"@nanostores/i18n": "^1.2.2",
|
||||
"@nanostores/vue": "^1.1.0",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"nanostores": "^1.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { useStore } from '@nanostores/vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useEditor } from '@open-pencil/vue/context/editorContext'
|
||||
import { commandMessages } from '@open-pencil/vue/i18n'
|
||||
import { usePageList } from '@open-pencil/vue/PageList/usePageList'
|
||||
import { useSelectionCapabilities } from '@open-pencil/vue/selection/useSelectionCapabilities'
|
||||
import { useSelectionState } from '@open-pencil/vue/selection/useSelectionState'
|
||||
|
|
@ -76,64 +78,86 @@ export function useEditorCommands() {
|
|||
const capabilities = useSelectionCapabilities()
|
||||
const { pages } = usePageList()
|
||||
|
||||
const t = useStore(commandMessages)
|
||||
|
||||
const commands: Record<EditorCommandId, EditorCommand> = {
|
||||
'edit.undo': {
|
||||
id: 'edit.undo',
|
||||
label: 'Undo',
|
||||
get label() {
|
||||
return t.value.undo
|
||||
},
|
||||
enabled: capabilities.canUndo,
|
||||
run: () => editor.undoAction()
|
||||
},
|
||||
'edit.redo': {
|
||||
id: 'edit.redo',
|
||||
label: 'Redo',
|
||||
get label() {
|
||||
return t.value.redo
|
||||
},
|
||||
enabled: capabilities.canRedo,
|
||||
run: () => editor.redoAction()
|
||||
},
|
||||
'selection.selectAll': {
|
||||
id: 'selection.selectAll',
|
||||
label: 'Select all',
|
||||
get label() {
|
||||
return t.value.selectAll
|
||||
},
|
||||
enabled: capabilities.canSelectAll,
|
||||
run: () => editor.selectAll()
|
||||
},
|
||||
'selection.duplicate': {
|
||||
id: 'selection.duplicate',
|
||||
label: 'Duplicate',
|
||||
get label() {
|
||||
return t.value.duplicate
|
||||
},
|
||||
enabled: capabilities.canDuplicate,
|
||||
run: () => editor.duplicateSelected()
|
||||
},
|
||||
'selection.delete': {
|
||||
id: 'selection.delete',
|
||||
label: 'Delete',
|
||||
get label() {
|
||||
return t.value.delete
|
||||
},
|
||||
enabled: capabilities.canDelete,
|
||||
run: () => editor.deleteSelected()
|
||||
},
|
||||
'selection.group': {
|
||||
id: 'selection.group',
|
||||
label: 'Group',
|
||||
get label() {
|
||||
return t.value.group
|
||||
},
|
||||
enabled: capabilities.canGroup,
|
||||
run: () => editor.groupSelected()
|
||||
},
|
||||
'selection.ungroup': {
|
||||
id: 'selection.ungroup',
|
||||
label: 'Ungroup',
|
||||
get label() {
|
||||
return t.value.ungroup
|
||||
},
|
||||
enabled: capabilities.canUngroup,
|
||||
run: () => editor.ungroupSelected()
|
||||
},
|
||||
'selection.createComponent': {
|
||||
id: 'selection.createComponent',
|
||||
label: 'Create component',
|
||||
get label() {
|
||||
return t.value.createComponent
|
||||
},
|
||||
enabled: capabilities.canCreateComponent,
|
||||
run: () => editor.createComponentFromSelection()
|
||||
},
|
||||
'selection.createComponentSet': {
|
||||
id: 'selection.createComponentSet',
|
||||
label: 'Create component set',
|
||||
get label() {
|
||||
return t.value.createComponentSet
|
||||
},
|
||||
enabled: capabilities.canCreateComponentSet,
|
||||
run: () => editor.createComponentSetFromComponents()
|
||||
},
|
||||
'selection.createInstance': {
|
||||
id: 'selection.createInstance',
|
||||
label: 'Create instance',
|
||||
get label() {
|
||||
return t.value.createInstance
|
||||
},
|
||||
enabled: capabilities.canCreateInstance,
|
||||
run: () => {
|
||||
const node = selection.selectedNode.value
|
||||
|
|
@ -142,49 +166,65 @@ export function useEditorCommands() {
|
|||
},
|
||||
'selection.detachInstance': {
|
||||
id: 'selection.detachInstance',
|
||||
label: 'Detach instance',
|
||||
get label() {
|
||||
return t.value.detachInstance
|
||||
},
|
||||
enabled: capabilities.canDetachInstance,
|
||||
run: () => editor.detachInstance()
|
||||
},
|
||||
'selection.goToMainComponent': {
|
||||
id: 'selection.goToMainComponent',
|
||||
label: 'Go to main component',
|
||||
get label() {
|
||||
return t.value.goToMainComponent
|
||||
},
|
||||
enabled: capabilities.canGoToMainComponent,
|
||||
run: () => editor.goToMainComponent()
|
||||
},
|
||||
'selection.wrapInAutoLayout': {
|
||||
id: 'selection.wrapInAutoLayout',
|
||||
label: 'Add auto layout',
|
||||
get label() {
|
||||
return t.value.addAutoLayout
|
||||
},
|
||||
enabled: capabilities.canWrapInAutoLayout,
|
||||
run: () => editor.wrapInAutoLayout()
|
||||
},
|
||||
'selection.bringToFront': {
|
||||
id: 'selection.bringToFront',
|
||||
label: 'Bring to front',
|
||||
get label() {
|
||||
return t.value.bringToFront
|
||||
},
|
||||
enabled: capabilities.canBringToFront,
|
||||
run: () => editor.bringToFront()
|
||||
},
|
||||
'selection.sendToBack': {
|
||||
id: 'selection.sendToBack',
|
||||
label: 'Send to back',
|
||||
get label() {
|
||||
return t.value.sendToBack
|
||||
},
|
||||
enabled: capabilities.canSendToBack,
|
||||
run: () => editor.sendToBack()
|
||||
},
|
||||
'selection.toggleVisibility': {
|
||||
id: 'selection.toggleVisibility',
|
||||
label: 'Toggle visibility',
|
||||
get label() {
|
||||
return t.value.toggleVisibility
|
||||
},
|
||||
enabled: capabilities.canToggleVisibility,
|
||||
run: () => editor.toggleVisibility()
|
||||
},
|
||||
'selection.toggleLock': {
|
||||
id: 'selection.toggleLock',
|
||||
label: 'Toggle lock',
|
||||
get label() {
|
||||
return t.value.toggleLock
|
||||
},
|
||||
enabled: capabilities.canToggleLock,
|
||||
run: () => editor.toggleLock()
|
||||
},
|
||||
'selection.moveToPage': {
|
||||
id: 'selection.moveToPage',
|
||||
label: 'Move to page',
|
||||
get label() {
|
||||
return t.value.moveToPage
|
||||
},
|
||||
enabled: capabilities.canMoveToPage,
|
||||
run: () => {
|
||||
const targetPage = otherPages.value[0]
|
||||
|
|
@ -193,19 +233,25 @@ export function useEditorCommands() {
|
|||
},
|
||||
'view.zoom100': {
|
||||
id: 'view.zoom100',
|
||||
label: 'Zoom to 100%',
|
||||
get label() {
|
||||
return t.value.zoomTo100
|
||||
},
|
||||
enabled: computed(() => true),
|
||||
run: () => editor.zoomTo100()
|
||||
},
|
||||
'view.zoomFit': {
|
||||
id: 'view.zoomFit',
|
||||
label: 'Zoom to fit',
|
||||
get label() {
|
||||
return t.value.zoomToFit
|
||||
},
|
||||
enabled: computed(() => true),
|
||||
run: () => editor.zoomToFit()
|
||||
},
|
||||
'view.zoomSelection': {
|
||||
id: 'view.zoomSelection',
|
||||
label: 'Zoom to selection',
|
||||
get label() {
|
||||
return t.value.zoomToSelection
|
||||
},
|
||||
enabled: capabilities.canZoomToSelection,
|
||||
run: () => editor.zoomToSelection()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { useStore } from '@nanostores/vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useEditorCommands } from '@open-pencil/vue/commands/useEditorCommands'
|
||||
import { useEditor } from '@open-pencil/vue/context/editorContext'
|
||||
import { menuMessages } from '@open-pencil/vue/i18n'
|
||||
import { useSelectionState } from '@open-pencil/vue/selection/useSelectionState'
|
||||
|
||||
/**
|
||||
|
|
@ -37,6 +39,8 @@ export function useMenuModel() {
|
|||
const { hasSelection, isGroup, isInstance, isComponent, canCreateComponentSet, selectedNode } =
|
||||
useSelectionState()
|
||||
|
||||
const t = useStore(menuMessages)
|
||||
|
||||
const editMenu = computed<MenuEntry[]>(() => [
|
||||
commandMenuItem('edit.undo'),
|
||||
commandMenuItem('edit.redo'),
|
||||
|
|
@ -68,10 +72,10 @@ export function useMenuModel() {
|
|||
const arrangeMenu = computed<MenuEntry[]>(() => [commandMenuItem('selection.wrapInAutoLayout')])
|
||||
|
||||
const appMenu = computed(() => [
|
||||
{ label: 'Edit', items: editMenu.value },
|
||||
{ label: 'View', items: viewMenu.value },
|
||||
{ label: 'Object', items: objectMenu.value },
|
||||
{ label: 'Arrange', items: arrangeMenu.value }
|
||||
{ label: t.value.edit, items: editMenu.value },
|
||||
{ label: t.value.view, items: viewMenu.value },
|
||||
{ label: t.value.object, items: objectMenu.value },
|
||||
{ label: t.value.arrange, items: arrangeMenu.value }
|
||||
])
|
||||
|
||||
const canvasMenu = computed<MenuEntry[]>(() => {
|
||||
|
|
@ -81,11 +85,11 @@ export function useMenuModel() {
|
|||
}))
|
||||
|
||||
return [
|
||||
{ ...commandMenuItem('selection.duplicate', '⌘D'), label: 'Duplicate' },
|
||||
commandMenuItem('selection.duplicate', '⌘D'),
|
||||
commandMenuItem('selection.delete', '⌫'),
|
||||
{ separator: true },
|
||||
...(moveToPageSubmenu.length > 0 && hasSelection.value
|
||||
? [{ label: 'Move to page', sub: moveToPageSubmenu } satisfies MenuActionNode]
|
||||
? [{ label: t.value.moveToPage, sub: moveToPageSubmenu } satisfies MenuActionNode]
|
||||
: []),
|
||||
commandMenuItem('selection.bringToFront', ']'),
|
||||
commandMenuItem('selection.sendToBack', '['),
|
||||
|
|
@ -101,7 +105,7 @@ export function useMenuModel() {
|
|||
...(isComponent.value && selectedNode.value
|
||||
? [
|
||||
{
|
||||
label: 'Create instance',
|
||||
label: t.value.createInstance,
|
||||
action: () => commandMenuItem('selection.createInstance').action?.(),
|
||||
disabled: !commandMenuItem('selection.createInstance').disabled
|
||||
} satisfies MenuActionNode
|
||||
|
|
@ -120,8 +124,8 @@ export function useMenuModel() {
|
|||
})
|
||||
|
||||
const selectionLabelMenu = computed(() => ({
|
||||
visibility: (editor.getSelectedNode()?.visible ?? true) ? 'Hide' : 'Show',
|
||||
lock: (editor.getSelectedNode()?.locked ?? false) ? 'Unlock' : 'Lock'
|
||||
visibility: (editor.getSelectedNode()?.visible ?? true) ? t.value.hide : t.value.show,
|
||||
lock: (editor.getSelectedNode()?.locked ?? false) ? t.value.unlock : t.value.lock
|
||||
}))
|
||||
|
||||
return {
|
||||
|
|
|
|||
23
packages/vue/src/global.d.ts
vendored
Normal file
23
packages/vue/src/global.d.ts
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
interface FontData {
|
||||
family: string
|
||||
fullName: string
|
||||
postscriptName: string
|
||||
style: string
|
||||
blob(): Promise<Blob>
|
||||
}
|
||||
|
||||
interface Window {
|
||||
queryLocalFonts?(): Promise<FontData[]>
|
||||
}
|
||||
|
||||
interface GestureEvent extends UIEvent {
|
||||
scale: number
|
||||
rotation: number
|
||||
clientX: number
|
||||
clientY: number
|
||||
}
|
||||
|
||||
declare module '*?raw' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
24
packages/vue/src/i18n/i18n.ts
Normal file
24
packages/vue/src/i18n/i18n.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { createI18n } from '@nanostores/i18n'
|
||||
|
||||
import { locale } from './locale'
|
||||
|
||||
import type { Locale } from './locale'
|
||||
import type { ComponentsJSON } from '@nanostores/i18n'
|
||||
|
||||
const localeLoaders: Record<Exclude<Locale, 'en'>, () => Promise<{ default: ComponentsJSON }>> = {
|
||||
de: () => import('../locales/de.json'),
|
||||
es: () => import('../locales/es.json'),
|
||||
fr: () => import('../locales/fr.json'),
|
||||
it: () => import('../locales/it.json'),
|
||||
pl: () => import('../locales/pl.json'),
|
||||
ru: () => import('../locales/ru.json')
|
||||
}
|
||||
|
||||
export const i18n = createI18n<Locale, 'en'>(locale, {
|
||||
baseLocale: 'en',
|
||||
async get(code) {
|
||||
if (code === 'en') return {}
|
||||
const mod = await localeLoaders[code]()
|
||||
return mod.default
|
||||
}
|
||||
})
|
||||
11
packages/vue/src/i18n/index.ts
Normal file
11
packages/vue/src/i18n/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export { useI18n } from './useI18n'
|
||||
export { locale, localeSetting, setLocale, AVAILABLE_LOCALES, LOCALE_LABELS } from './locale'
|
||||
export type { Locale } from './locale'
|
||||
export {
|
||||
menuMessages,
|
||||
commandMessages,
|
||||
toolMessages,
|
||||
panelMessages,
|
||||
pageMessages,
|
||||
dialogMessages
|
||||
} from './messages'
|
||||
35
packages/vue/src/i18n/locale.ts
Normal file
35
packages/vue/src/i18n/locale.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { browser, localeFrom } from '@nanostores/i18n'
|
||||
import { atom } from 'nanostores'
|
||||
|
||||
export const AVAILABLE_LOCALES = ['en', 'de', 'es', 'fr', 'it', 'pl', 'ru'] as const
|
||||
export type Locale = (typeof AVAILABLE_LOCALES)[number]
|
||||
|
||||
export const LOCALE_LABELS: Record<Locale, string> = {
|
||||
en: 'English',
|
||||
de: 'Deutsch',
|
||||
es: 'Español',
|
||||
fr: 'Français',
|
||||
it: 'Italiano',
|
||||
pl: 'Polski',
|
||||
ru: 'Русский'
|
||||
}
|
||||
|
||||
const LOCALE_STORAGE_KEY = 'open-pencil-locale'
|
||||
|
||||
export const localeSetting = atom<Locale | undefined>(undefined)
|
||||
|
||||
export const locale = localeFrom(localeSetting, browser({ available: AVAILABLE_LOCALES }))
|
||||
|
||||
export function setLocale(code: Locale) {
|
||||
localeSetting.set(code)
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(LOCALE_STORAGE_KEY, code)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const saved = localStorage.getItem(LOCALE_STORAGE_KEY) as Locale | null
|
||||
if (saved && AVAILABLE_LOCALES.includes(saved)) {
|
||||
localeSetting.set(saved)
|
||||
}
|
||||
}
|
||||
195
packages/vue/src/i18n/messages.ts
Normal file
195
packages/vue/src/i18n/messages.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import { params } from '@nanostores/i18n'
|
||||
|
||||
import { i18n } from './i18n'
|
||||
|
||||
export const menuMessages = i18n('menu', {
|
||||
file: 'File',
|
||||
edit: 'Edit',
|
||||
view: 'View',
|
||||
object: 'Object',
|
||||
arrange: 'Arrange',
|
||||
text: 'Text',
|
||||
|
||||
new: 'New',
|
||||
open: 'Open…',
|
||||
save: 'Save',
|
||||
saveAs: 'Save as…',
|
||||
exportSelection: 'Export selection…',
|
||||
autosave: 'Auto-save to local file',
|
||||
|
||||
copy: 'Copy',
|
||||
paste: 'Paste',
|
||||
|
||||
zoomIn: 'Zoom in',
|
||||
zoomOut: 'Zoom out',
|
||||
profiler: 'Performance profiler',
|
||||
language: 'Language',
|
||||
|
||||
moveToPage: 'Move to page',
|
||||
createInstance: 'Create instance',
|
||||
hide: 'Hide',
|
||||
show: 'Show',
|
||||
lock: 'Lock',
|
||||
unlock: 'Unlock',
|
||||
cut: 'Cut',
|
||||
front: 'Front',
|
||||
back: 'Back',
|
||||
toggleUI: 'Toggle UI',
|
||||
|
||||
bold: 'Bold',
|
||||
italic: 'Italic',
|
||||
underline: 'Underline'
|
||||
})
|
||||
|
||||
export const commandMessages = i18n('commands', {
|
||||
undo: 'Undo',
|
||||
redo: 'Redo',
|
||||
selectAll: 'Select all',
|
||||
duplicate: 'Duplicate',
|
||||
delete: 'Delete',
|
||||
group: 'Group',
|
||||
ungroup: 'Ungroup',
|
||||
createComponent: 'Create component',
|
||||
createComponentSet: 'Create component set',
|
||||
createInstance: 'Create instance',
|
||||
detachInstance: 'Detach instance',
|
||||
goToMainComponent: 'Go to main component',
|
||||
addAutoLayout: 'Add auto layout',
|
||||
bringToFront: 'Bring to front',
|
||||
sendToBack: 'Send to back',
|
||||
toggleVisibility: 'Toggle visibility',
|
||||
toggleLock: 'Toggle lock',
|
||||
moveToPage: 'Move to page',
|
||||
zoomTo100: 'Zoom to 100%',
|
||||
zoomToFit: 'Zoom to fit',
|
||||
zoomToSelection: 'Zoom to selection'
|
||||
})
|
||||
|
||||
export const toolMessages = i18n('tools', {
|
||||
move: 'Move',
|
||||
frame: 'Frame',
|
||||
section: 'Section',
|
||||
rectangle: 'Rectangle',
|
||||
ellipse: 'Ellipse',
|
||||
line: 'Line',
|
||||
polygon: 'Polygon',
|
||||
star: 'Star',
|
||||
pen: 'Pen',
|
||||
text: 'Text',
|
||||
hand: 'Hand'
|
||||
})
|
||||
|
||||
export const panelMessages = i18n('panels', {
|
||||
layers: 'Layers',
|
||||
pages: 'Pages',
|
||||
design: 'Design',
|
||||
code: 'Code',
|
||||
ai: 'AI',
|
||||
assets: 'Assets',
|
||||
|
||||
page: 'Page',
|
||||
position: 'Position',
|
||||
layout: 'Layout',
|
||||
autoLayout: 'Auto layout',
|
||||
alignment: 'Alignment',
|
||||
appearance: 'Appearance',
|
||||
fill: 'Fill',
|
||||
stroke: 'Stroke',
|
||||
effects: 'Effects',
|
||||
export: 'Export',
|
||||
typography: 'Typography',
|
||||
variables: 'Variables',
|
||||
constraints: 'Constraints',
|
||||
|
||||
addFill: 'Add fill',
|
||||
addStroke: 'Add stroke',
|
||||
addEffect: 'Add effect',
|
||||
addExport: 'Add export',
|
||||
|
||||
dropShadow: 'Drop shadow',
|
||||
innerShadow: 'Inner shadow',
|
||||
layerBlur: 'Layer blur',
|
||||
backgroundBlur: 'Background blur',
|
||||
|
||||
noSelection: 'No selection',
|
||||
noLocalVariables: 'No local variables',
|
||||
openVariables: 'Open variables',
|
||||
addPage: 'Add page',
|
||||
toggleVisibility: 'Toggle visibility',
|
||||
independentCornerRadii: 'Independent corner radii',
|
||||
detachVariable: 'Detach variable',
|
||||
applyVariable: 'Apply variable',
|
||||
noVariablesFound: 'No variables found',
|
||||
addAutoLayout: 'Add auto layout (Shift+A)',
|
||||
removeAutoLayout: 'Remove auto layout',
|
||||
alignLeft: 'Align left',
|
||||
alignCenterHorizontally: 'Align center horizontally',
|
||||
alignRight: 'Align right',
|
||||
alignTop: 'Align top',
|
||||
alignCenterVertically: 'Align center vertically',
|
||||
alignBottom: 'Align bottom',
|
||||
flipHorizontal: 'Flip horizontal',
|
||||
flipVertical: 'Flip vertical',
|
||||
rotate90: 'Rotate 90°',
|
||||
mixedFillsHelp: 'Click + to replace mixed fills',
|
||||
mixedStrokesHelp: 'Click + to replace mixed strokes',
|
||||
mixedEffectsHelp: 'Click + to replace mixed effects',
|
||||
strokeSides: 'Stroke sides',
|
||||
mixed: 'Mixed',
|
||||
layersCount: params('{count} layers'),
|
||||
goToMainComponent: 'Go to Main Component',
|
||||
detachInstance: 'Detach Instance',
|
||||
|
||||
solid: 'Solid',
|
||||
linearGradient: 'Linear',
|
||||
radialGradient: 'Radial',
|
||||
image: 'Image',
|
||||
stops: 'Stops',
|
||||
addStop: 'Add stop',
|
||||
|
||||
alignCenter: 'Align center',
|
||||
alignMiddle: 'Align middle'
|
||||
})
|
||||
|
||||
export const pageMessages = i18n('pages', {
|
||||
newPage: 'New page',
|
||||
rename: 'Rename',
|
||||
delete: 'Delete',
|
||||
pageName: params('Page {number}')
|
||||
})
|
||||
|
||||
export const dialogMessages = i18n('dialogs', {
|
||||
cancel: 'Cancel',
|
||||
apply: 'Apply',
|
||||
close: 'Close',
|
||||
ok: 'OK',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
createCollection: 'Create collection',
|
||||
localVariables: 'Local variables',
|
||||
noVariableCollections: 'No variable collections',
|
||||
selectLayerForJSX: 'Select a layer to see its JSX code',
|
||||
connectAIProvider: 'Connect an AI provider to start chatting.',
|
||||
connect: 'Connect',
|
||||
getAPIKey: params('Get an {provider} API key →'),
|
||||
oneKeyManyModels: 'One key for 100+ models from all providers.',
|
||||
describeChange: 'Describe a change…',
|
||||
describeCreateOrChange: 'Describe what you want to create or change.',
|
||||
stopGenerating: 'Stop generating',
|
||||
sendMessage: 'Send message',
|
||||
baseURLPlaceholder: 'Base URL (e.g. http://localhost:11434/v1)',
|
||||
modelIDPlaceholder: 'Model ID (e.g. llama-3.3-70b)',
|
||||
yourName: 'Your name',
|
||||
enterYourName: 'Enter your name',
|
||||
shareThisFile: 'Share this file',
|
||||
joinRoom: 'Join room',
|
||||
join: 'Join',
|
||||
roomLink: 'Room link',
|
||||
joinCollaboration: 'Join collaboration',
|
||||
orJoinRoom: 'or join a room',
|
||||
pasteRoomLinkOrId: 'Paste room link or ID',
|
||||
connected: 'Connected',
|
||||
search: 'Search…',
|
||||
noResults: 'No results',
|
||||
share: 'Share'
|
||||
})
|
||||
47
packages/vue/src/i18n/useI18n.ts
Normal file
47
packages/vue/src/i18n/useI18n.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { useStore } from '@nanostores/vue'
|
||||
|
||||
import { locale, setLocale, AVAILABLE_LOCALES, LOCALE_LABELS } from './locale'
|
||||
import {
|
||||
menuMessages,
|
||||
commandMessages,
|
||||
toolMessages,
|
||||
panelMessages,
|
||||
pageMessages,
|
||||
dialogMessages
|
||||
} from './messages'
|
||||
|
||||
import type { Locale } from './locale'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
/**
|
||||
* Reactive i18n composable for OpenPencil Vue components.
|
||||
*
|
||||
* Returns reactive translation objects grouped by domain, plus locale
|
||||
* controls. All values update automatically when the locale changes.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup>
|
||||
* const { menu, commands, locale, setLocale } = useI18n()
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <button>{{ menu.save }}</button>
|
||||
* <span>{{ commands.undo }}</span>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useI18n() {
|
||||
return {
|
||||
menu: useStore(menuMessages),
|
||||
commands: useStore(commandMessages),
|
||||
tools: useStore(toolMessages),
|
||||
panels: useStore(panelMessages),
|
||||
pages: useStore(pageMessages),
|
||||
dialogs: useStore(dialogMessages),
|
||||
locale: useStore(locale) as Ref<Locale>,
|
||||
availableLocales: AVAILABLE_LOCALES,
|
||||
localeLabels: LOCALE_LABELS,
|
||||
setLocale
|
||||
}
|
||||
}
|
||||
|
|
@ -86,3 +86,16 @@ export type { ScrubInputContext } from './ScrubInput'
|
|||
export { TypographyControlsRoot } from './TypographyControls'
|
||||
export { ToolbarRoot, ToolbarItem, useToolbar } from './Toolbar'
|
||||
export type { ToolbarContext } from './Toolbar'
|
||||
|
||||
/** Internationalization. */
|
||||
export { useI18n } from './i18n'
|
||||
export { locale, localeSetting, setLocale, AVAILABLE_LOCALES, LOCALE_LABELS } from './i18n'
|
||||
export type { Locale } from './i18n'
|
||||
export {
|
||||
menuMessages,
|
||||
commandMessages,
|
||||
toolMessages,
|
||||
panelMessages,
|
||||
pageMessages,
|
||||
dialogMessages
|
||||
} from './i18n'
|
||||
|
|
|
|||
177
packages/vue/src/locales/de.json
Normal file
177
packages/vue/src/locales/de.json
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
{
|
||||
"menu": {
|
||||
"file": "Datei",
|
||||
"edit": "Bearbeiten",
|
||||
"view": "Ansicht",
|
||||
"object": "Objekt",
|
||||
"arrange": "Anordnen",
|
||||
"text": "Text",
|
||||
"new": "Neu",
|
||||
"open": "Öffnen…",
|
||||
"save": "Speichern",
|
||||
"saveAs": "Speichern unter…",
|
||||
"exportSelection": "Auswahl exportieren…",
|
||||
"autosave": "Automatisch in Datei speichern",
|
||||
"copy": "Kopieren",
|
||||
"paste": "Einfügen",
|
||||
"zoomIn": "Vergrößern",
|
||||
"zoomOut": "Verkleinern",
|
||||
"profiler": "Leistungsprofil",
|
||||
"language": "Sprache",
|
||||
"moveToPage": "Auf Seite verschieben",
|
||||
"createInstance": "Instanz erstellen",
|
||||
"hide": "Ausblenden",
|
||||
"show": "Einblenden",
|
||||
"lock": "Sperren",
|
||||
"unlock": "Entsperren",
|
||||
"cut": "Ausschneiden",
|
||||
"front": "Nach vorn",
|
||||
"back": "Nach hinten",
|
||||
"toggleUI": "UI umschalten",
|
||||
"bold": "Fett",
|
||||
"italic": "Kursiv",
|
||||
"underline": "Unterstrichen"
|
||||
},
|
||||
"commands": {
|
||||
"undo": "Rückgängig",
|
||||
"redo": "Wiederholen",
|
||||
"selectAll": "Alles auswählen",
|
||||
"duplicate": "Duplizieren",
|
||||
"delete": "Löschen",
|
||||
"group": "Gruppieren",
|
||||
"ungroup": "Gruppierung aufheben",
|
||||
"createComponent": "Komponente erstellen",
|
||||
"createComponentSet": "Komponentensatz erstellen",
|
||||
"createInstance": "Instanz erstellen",
|
||||
"detachInstance": "Instanz lösen",
|
||||
"goToMainComponent": "Zur Hauptkomponente",
|
||||
"addAutoLayout": "Auto-Layout hinzufügen",
|
||||
"bringToFront": "In den Vordergrund",
|
||||
"sendToBack": "In den Hintergrund",
|
||||
"toggleVisibility": "Sichtbarkeit",
|
||||
"toggleLock": "Sperren",
|
||||
"moveToPage": "Auf Seite verschieben",
|
||||
"zoomTo100": "Zoom 100 %",
|
||||
"zoomToFit": "Einpassen",
|
||||
"zoomToSelection": "Auswahl einpassen"
|
||||
},
|
||||
"tools": {
|
||||
"move": "Verschieben",
|
||||
"frame": "Rahmen",
|
||||
"section": "Abschnitt",
|
||||
"rectangle": "Rechteck",
|
||||
"ellipse": "Ellipse",
|
||||
"line": "Linie",
|
||||
"polygon": "Polygon",
|
||||
"star": "Stern",
|
||||
"pen": "Stift",
|
||||
"text": "Text",
|
||||
"hand": "Hand"
|
||||
},
|
||||
"panels": {
|
||||
"layers": "Ebenen",
|
||||
"pages": "Seiten",
|
||||
"design": "Design",
|
||||
"code": "Code",
|
||||
"ai": "KI",
|
||||
"assets": "Elemente",
|
||||
"page": "Seite",
|
||||
"position": "Position",
|
||||
"layout": "Layout",
|
||||
"autoLayout": "Auto-Layout",
|
||||
"alignment": "Ausrichtung",
|
||||
"appearance": "Darstellung",
|
||||
"fill": "Füllung",
|
||||
"stroke": "Kontur",
|
||||
"effects": "Effekte",
|
||||
"export": "Export",
|
||||
"typography": "Typografie",
|
||||
"variables": "Variablen",
|
||||
"constraints": "Einschränkungen",
|
||||
"addFill": "Füllung hinzufügen",
|
||||
"addStroke": "Kontur hinzufügen",
|
||||
"addEffect": "Effekt hinzufügen",
|
||||
"addExport": "Export hinzufügen",
|
||||
"dropShadow": "Schlagschatten",
|
||||
"innerShadow": "Innerer Schatten",
|
||||
"layerBlur": "Ebenenunschärfe",
|
||||
"backgroundBlur": "Hintergrundunschärfe",
|
||||
"noSelection": "Keine Auswahl",
|
||||
"noLocalVariables": "Keine lokalen Variablen",
|
||||
"openVariables": "Variablen öffnen",
|
||||
"addPage": "Seite hinzufügen",
|
||||
"toggleVisibility": "Sichtbarkeit umschalten",
|
||||
"independentCornerRadii": "Unabhängige Eckenradien",
|
||||
"detachVariable": "Variable lösen",
|
||||
"applyVariable": "Variable anwenden",
|
||||
"noVariablesFound": "Keine Variablen gefunden",
|
||||
"addAutoLayout": "Auto-Layout hinzufügen (Shift+A)",
|
||||
"removeAutoLayout": "Auto-Layout entfernen",
|
||||
"mixed": "Gemischt",
|
||||
"layersCount": "{count} Ebenen",
|
||||
"goToMainComponent": "Zur Hauptkomponente",
|
||||
"detachInstance": "Instanz lösen",
|
||||
"solid": "Vollton",
|
||||
"linearGradient": "Linear",
|
||||
"radialGradient": "Radial",
|
||||
"image": "Bild",
|
||||
"stops": "Stopps",
|
||||
"addStop": "Stopp hinzufügen",
|
||||
"alignLeft": "Links ausrichten",
|
||||
"alignCenter": "Zentrieren",
|
||||
"alignRight": "Rechts ausrichten",
|
||||
"alignTop": "Oben ausrichten",
|
||||
"alignMiddle": "Mittig ausrichten",
|
||||
"alignBottom": "Unten ausrichten",
|
||||
"alignCenterHorizontally": "Horizontal zentrieren",
|
||||
"alignCenterVertically": "Vertikal zentrieren",
|
||||
"flipHorizontal": "Horizontal spiegeln",
|
||||
"flipVertical": "Vertikal spiegeln",
|
||||
"rotate90": "90° drehen",
|
||||
"mixedFillsHelp": "Klicke auf +, um gemischte Füllungen zu ersetzen",
|
||||
"mixedStrokesHelp": "Klicke auf +, um gemischte Konturen zu ersetzen",
|
||||
"mixedEffectsHelp": "Klicke auf +, um gemischte Effekte zu ersetzen",
|
||||
"strokeSides": "Konturseiten"
|
||||
},
|
||||
"pages": {
|
||||
"newPage": "Neue Seite",
|
||||
"rename": "Umbenennen",
|
||||
"delete": "Löschen",
|
||||
"pageName": "Seite {number}"
|
||||
},
|
||||
"dialogs": {
|
||||
"cancel": "Abbrechen",
|
||||
"apply": "Anwenden",
|
||||
"close": "Schließen",
|
||||
"ok": "OK",
|
||||
"copy": "Kopieren",
|
||||
"copied": "Kopiert",
|
||||
"createCollection": "Sammlung erstellen",
|
||||
"localVariables": "Lokale Variablen",
|
||||
"noVariableCollections": "Keine Variablensammlungen",
|
||||
"selectLayerForJSX": "Wähle eine Ebene, um ihren JSX-Code zu sehen",
|
||||
"connectAIProvider": "Verbinde einen KI-Anbieter, um den Chat zu starten.",
|
||||
"connect": "Verbinden",
|
||||
"getAPIKey": "Einen {provider}-API-Schlüssel erhalten →",
|
||||
"oneKeyManyModels": "Ein Schlüssel für über 100 Modelle aller Anbieter.",
|
||||
"describeChange": "Beschreibe eine Änderung…",
|
||||
"describeCreateOrChange": "Beschreibe, was du erstellen oder ändern möchtest.",
|
||||
"stopGenerating": "Generierung stoppen",
|
||||
"sendMessage": "Nachricht senden",
|
||||
"baseURLPlaceholder": "Base URL (z. B. http://localhost:11434/v1)",
|
||||
"modelIDPlaceholder": "Model ID (z. B. llama-3.3-70b)",
|
||||
"yourName": "Dein Name",
|
||||
"enterYourName": "Deinen Namen eingeben",
|
||||
"shareThisFile": "Diese Datei teilen",
|
||||
"joinRoom": "Raum beitreten",
|
||||
"join": "Beitreten",
|
||||
"roomLink": "Raumlink",
|
||||
"joinCollaboration": "Zur Zusammenarbeit beitreten",
|
||||
"orJoinRoom": "oder einem Raum beitreten",
|
||||
"pasteRoomLinkOrId": "Raumlink oder ID einfügen",
|
||||
"connected": "Verbunden",
|
||||
"search": "Suchen…",
|
||||
"noResults": "Keine Ergebnisse",
|
||||
"share": "Teilen"
|
||||
}
|
||||
}
|
||||
177
packages/vue/src/locales/es.json
Normal file
177
packages/vue/src/locales/es.json
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
{
|
||||
"menu": {
|
||||
"file": "Archivo",
|
||||
"edit": "Editar",
|
||||
"view": "Ver",
|
||||
"object": "Objeto",
|
||||
"arrange": "Organizar",
|
||||
"text": "Texto",
|
||||
"new": "Nuevo",
|
||||
"open": "Abrir…",
|
||||
"save": "Guardar",
|
||||
"saveAs": "Guardar como…",
|
||||
"exportSelection": "Exportar selección…",
|
||||
"autosave": "Guardado automático en archivo",
|
||||
"copy": "Copiar",
|
||||
"paste": "Pegar",
|
||||
"zoomIn": "Ampliar",
|
||||
"zoomOut": "Reducir",
|
||||
"profiler": "Perfil de rendimiento",
|
||||
"language": "Idioma",
|
||||
"moveToPage": "Mover a página",
|
||||
"createInstance": "Crear instancia",
|
||||
"hide": "Ocultar",
|
||||
"show": "Mostrar",
|
||||
"lock": "Bloquear",
|
||||
"unlock": "Desbloquear",
|
||||
"cut": "Cortar",
|
||||
"front": "Al frente",
|
||||
"back": "Al fondo",
|
||||
"toggleUI": "Alternar interfaz",
|
||||
"bold": "Negrita",
|
||||
"italic": "Cursiva",
|
||||
"underline": "Subrayado"
|
||||
},
|
||||
"commands": {
|
||||
"undo": "Deshacer",
|
||||
"redo": "Rehacer",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"duplicate": "Duplicar",
|
||||
"delete": "Eliminar",
|
||||
"group": "Agrupar",
|
||||
"ungroup": "Desagrupar",
|
||||
"createComponent": "Crear componente",
|
||||
"createComponentSet": "Crear conjunto de componentes",
|
||||
"createInstance": "Crear instancia",
|
||||
"detachInstance": "Separar instancia",
|
||||
"goToMainComponent": "Ir al componente principal",
|
||||
"addAutoLayout": "Añadir auto-layout",
|
||||
"bringToFront": "Traer al frente",
|
||||
"sendToBack": "Enviar al fondo",
|
||||
"toggleVisibility": "Visibilidad",
|
||||
"toggleLock": "Bloqueo",
|
||||
"moveToPage": "Mover a página",
|
||||
"zoomTo100": "Zoom 100 %",
|
||||
"zoomToFit": "Ajustar a pantalla",
|
||||
"zoomToSelection": "Zoom a selección"
|
||||
},
|
||||
"tools": {
|
||||
"move": "Mover",
|
||||
"frame": "Marco",
|
||||
"section": "Sección",
|
||||
"rectangle": "Rectángulo",
|
||||
"ellipse": "Elipse",
|
||||
"line": "Línea",
|
||||
"polygon": "Polígono",
|
||||
"star": "Estrella",
|
||||
"pen": "Pluma",
|
||||
"text": "Texto",
|
||||
"hand": "Mano"
|
||||
},
|
||||
"panels": {
|
||||
"layers": "Capas",
|
||||
"pages": "Páginas",
|
||||
"design": "Diseño",
|
||||
"code": "Código",
|
||||
"ai": "IA",
|
||||
"assets": "Recursos",
|
||||
"page": "Página",
|
||||
"position": "Posición",
|
||||
"layout": "Diseño",
|
||||
"autoLayout": "Auto-layout",
|
||||
"alignment": "Alineación",
|
||||
"appearance": "Apariencia",
|
||||
"fill": "Relleno",
|
||||
"stroke": "Trazo",
|
||||
"effects": "Efectos",
|
||||
"export": "Exportar",
|
||||
"typography": "Tipografía",
|
||||
"variables": "Variables",
|
||||
"constraints": "Restricciones",
|
||||
"addFill": "Añadir relleno",
|
||||
"addStroke": "Añadir trazo",
|
||||
"addEffect": "Añadir efecto",
|
||||
"addExport": "Añadir exportación",
|
||||
"dropShadow": "Sombra",
|
||||
"innerShadow": "Sombra interior",
|
||||
"layerBlur": "Desenfoque de capa",
|
||||
"backgroundBlur": "Desenfoque de fondo",
|
||||
"noSelection": "Sin selección",
|
||||
"noLocalVariables": "Sin variables locales",
|
||||
"openVariables": "Abrir variables",
|
||||
"addPage": "Añadir página",
|
||||
"toggleVisibility": "Alternar visibilidad",
|
||||
"independentCornerRadii": "Radios de esquina independientes",
|
||||
"detachVariable": "Desvincular variable",
|
||||
"applyVariable": "Aplicar variable",
|
||||
"noVariablesFound": "No se encontraron variables",
|
||||
"addAutoLayout": "Añadir auto-layout (Shift+A)",
|
||||
"removeAutoLayout": "Quitar auto-layout",
|
||||
"mixed": "Mixto",
|
||||
"layersCount": "{count} capas",
|
||||
"goToMainComponent": "Ir al componente principal",
|
||||
"detachInstance": "Separar instancia",
|
||||
"solid": "Sólido",
|
||||
"linearGradient": "Lineal",
|
||||
"radialGradient": "Radial",
|
||||
"image": "Imagen",
|
||||
"stops": "Paradas",
|
||||
"addStop": "Añadir parada",
|
||||
"alignLeft": "Alinear a la izquierda",
|
||||
"alignCenter": "Centrar",
|
||||
"alignRight": "Alinear a la derecha",
|
||||
"alignTop": "Alinear arriba",
|
||||
"alignMiddle": "Centrar verticalmente",
|
||||
"alignBottom": "Alinear abajo",
|
||||
"alignCenterHorizontally": "Centrar horizontalmente",
|
||||
"alignCenterVertically": "Centrar verticalmente",
|
||||
"flipHorizontal": "Voltear horizontalmente",
|
||||
"flipVertical": "Voltear verticalmente",
|
||||
"rotate90": "Girar 90°",
|
||||
"mixedFillsHelp": "Haz clic en + para reemplazar rellenos mixtos",
|
||||
"mixedStrokesHelp": "Haz clic en + para reemplazar trazos mixtos",
|
||||
"mixedEffectsHelp": "Haz clic en + para reemplazar efectos mixtos",
|
||||
"strokeSides": "Lados del trazo"
|
||||
},
|
||||
"pages": {
|
||||
"newPage": "Nueva página",
|
||||
"rename": "Renombrar",
|
||||
"delete": "Eliminar",
|
||||
"pageName": "Página {number}"
|
||||
},
|
||||
"dialogs": {
|
||||
"cancel": "Cancelar",
|
||||
"apply": "Aplicar",
|
||||
"close": "Cerrar",
|
||||
"ok": "Aceptar",
|
||||
"copy": "Copiar",
|
||||
"copied": "Copiado",
|
||||
"createCollection": "Crear colección",
|
||||
"localVariables": "Variables locales",
|
||||
"noVariableCollections": "No hay colecciones de variables",
|
||||
"selectLayerForJSX": "Selecciona una capa para ver su código JSX",
|
||||
"connectAIProvider": "Conecta un proveedor de IA para empezar a chatear.",
|
||||
"connect": "Conectar",
|
||||
"getAPIKey": "Obtener una clave API de {provider} →",
|
||||
"oneKeyManyModels": "Una clave para más de 100 modelos de todos los proveedores.",
|
||||
"describeChange": "Describe un cambio…",
|
||||
"describeCreateOrChange": "Describe lo que quieres crear o cambiar.",
|
||||
"stopGenerating": "Detener generación",
|
||||
"sendMessage": "Enviar mensaje",
|
||||
"baseURLPlaceholder": "Base URL (p. ej. http://localhost:11434/v1)",
|
||||
"modelIDPlaceholder": "Model ID (p. ej. llama-3.3-70b)",
|
||||
"yourName": "Tu nombre",
|
||||
"enterYourName": "Introduce tu nombre",
|
||||
"shareThisFile": "Compartir este archivo",
|
||||
"joinRoom": "Unirse a la sala",
|
||||
"join": "Unirse",
|
||||
"roomLink": "Enlace de la sala",
|
||||
"joinCollaboration": "Unirse a la colaboración",
|
||||
"orJoinRoom": "o unirse a una sala",
|
||||
"pasteRoomLinkOrId": "Pega el enlace o ID de la sala",
|
||||
"connected": "Conectado",
|
||||
"search": "Buscar…",
|
||||
"noResults": "Sin resultados",
|
||||
"share": "Compartir"
|
||||
}
|
||||
}
|
||||
177
packages/vue/src/locales/fr.json
Normal file
177
packages/vue/src/locales/fr.json
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
{
|
||||
"menu": {
|
||||
"file": "Fichier",
|
||||
"edit": "Édition",
|
||||
"view": "Affichage",
|
||||
"object": "Objet",
|
||||
"arrange": "Disposition",
|
||||
"text": "Texte",
|
||||
"new": "Nouveau",
|
||||
"open": "Ouvrir…",
|
||||
"save": "Enregistrer",
|
||||
"saveAs": "Enregistrer sous…",
|
||||
"exportSelection": "Exporter la sélection…",
|
||||
"autosave": "Enregistrement automatique",
|
||||
"copy": "Copier",
|
||||
"paste": "Coller",
|
||||
"zoomIn": "Zoom avant",
|
||||
"zoomOut": "Zoom arrière",
|
||||
"profiler": "Profileur de performance",
|
||||
"language": "Langue",
|
||||
"moveToPage": "Déplacer vers la page",
|
||||
"createInstance": "Créer une instance",
|
||||
"hide": "Masquer",
|
||||
"show": "Afficher",
|
||||
"lock": "Verrouiller",
|
||||
"unlock": "Déverrouiller",
|
||||
"cut": "Couper",
|
||||
"front": "Premier plan",
|
||||
"back": "Arrière-plan",
|
||||
"toggleUI": "Basculer l’interface",
|
||||
"bold": "Gras",
|
||||
"italic": "Italique",
|
||||
"underline": "Souligné"
|
||||
},
|
||||
"commands": {
|
||||
"undo": "Annuler",
|
||||
"redo": "Rétablir",
|
||||
"selectAll": "Tout sélectionner",
|
||||
"duplicate": "Dupliquer",
|
||||
"delete": "Supprimer",
|
||||
"group": "Grouper",
|
||||
"ungroup": "Dégrouper",
|
||||
"createComponent": "Créer un composant",
|
||||
"createComponentSet": "Créer un jeu de composants",
|
||||
"createInstance": "Créer une instance",
|
||||
"detachInstance": "Détacher l'instance",
|
||||
"goToMainComponent": "Aller au composant principal",
|
||||
"addAutoLayout": "Ajouter un auto-layout",
|
||||
"bringToFront": "Mettre au premier plan",
|
||||
"sendToBack": "Mettre en arrière-plan",
|
||||
"toggleVisibility": "Visibilité",
|
||||
"toggleLock": "Verrouillage",
|
||||
"moveToPage": "Déplacer vers la page",
|
||||
"zoomTo100": "Zoom 100 %",
|
||||
"zoomToFit": "Ajuster à l'écran",
|
||||
"zoomToSelection": "Zoom sur la sélection"
|
||||
},
|
||||
"tools": {
|
||||
"move": "Déplacer",
|
||||
"frame": "Cadre",
|
||||
"section": "Section",
|
||||
"rectangle": "Rectangle",
|
||||
"ellipse": "Ellipse",
|
||||
"line": "Ligne",
|
||||
"polygon": "Polygone",
|
||||
"star": "Étoile",
|
||||
"pen": "Plume",
|
||||
"text": "Texte",
|
||||
"hand": "Main"
|
||||
},
|
||||
"panels": {
|
||||
"layers": "Calques",
|
||||
"pages": "Pages",
|
||||
"design": "Design",
|
||||
"code": "Code",
|
||||
"ai": "IA",
|
||||
"assets": "Ressources",
|
||||
"page": "Page",
|
||||
"position": "Position",
|
||||
"layout": "Disposition",
|
||||
"autoLayout": "Auto-layout",
|
||||
"alignment": "Alignement",
|
||||
"appearance": "Apparence",
|
||||
"fill": "Remplissage",
|
||||
"stroke": "Contour",
|
||||
"effects": "Effets",
|
||||
"export": "Export",
|
||||
"typography": "Typographie",
|
||||
"variables": "Variables",
|
||||
"constraints": "Contraintes",
|
||||
"addFill": "Ajouter un remplissage",
|
||||
"addStroke": "Ajouter un contour",
|
||||
"addEffect": "Ajouter un effet",
|
||||
"addExport": "Ajouter un export",
|
||||
"dropShadow": "Ombre portée",
|
||||
"innerShadow": "Ombre interne",
|
||||
"layerBlur": "Flou de calque",
|
||||
"backgroundBlur": "Flou d'arrière-plan",
|
||||
"noSelection": "Aucune sélection",
|
||||
"noLocalVariables": "Aucune variable locale",
|
||||
"openVariables": "Ouvrir les variables",
|
||||
"addPage": "Ajouter une page",
|
||||
"toggleVisibility": "Basculer la visibilité",
|
||||
"independentCornerRadii": "Rayons de coin indépendants",
|
||||
"detachVariable": "Détacher la variable",
|
||||
"applyVariable": "Appliquer la variable",
|
||||
"noVariablesFound": "Aucune variable trouvée",
|
||||
"addAutoLayout": "Ajouter un auto-layout (Shift+A)",
|
||||
"removeAutoLayout": "Retirer l'auto-layout",
|
||||
"mixed": "Mixte",
|
||||
"layersCount": "{count} calques",
|
||||
"goToMainComponent": "Aller au composant principal",
|
||||
"detachInstance": "Détacher l'instance",
|
||||
"solid": "Uni",
|
||||
"linearGradient": "Linéaire",
|
||||
"radialGradient": "Radial",
|
||||
"image": "Image",
|
||||
"stops": "Arrêts",
|
||||
"addStop": "Ajouter un arrêt",
|
||||
"alignLeft": "Aligner à gauche",
|
||||
"alignCenter": "Centrer",
|
||||
"alignRight": "Aligner à droite",
|
||||
"alignTop": "Aligner en haut",
|
||||
"alignMiddle": "Centrer verticalement",
|
||||
"alignBottom": "Aligner en bas",
|
||||
"alignCenterHorizontally": "Centrer horizontalement",
|
||||
"alignCenterVertically": "Centrer verticalement",
|
||||
"flipHorizontal": "Retourner horizontalement",
|
||||
"flipVertical": "Retourner verticalement",
|
||||
"rotate90": "Pivoter de 90°",
|
||||
"mixedFillsHelp": "Cliquez sur + pour remplacer les remplissages mixtes",
|
||||
"mixedStrokesHelp": "Cliquez sur + pour remplacer les contours mixtes",
|
||||
"mixedEffectsHelp": "Cliquez sur + pour remplacer les effets mixtes",
|
||||
"strokeSides": "Côtés du contour"
|
||||
},
|
||||
"pages": {
|
||||
"newPage": "Nouvelle page",
|
||||
"rename": "Renommer",
|
||||
"delete": "Supprimer",
|
||||
"pageName": "Page {number}"
|
||||
},
|
||||
"dialogs": {
|
||||
"cancel": "Annuler",
|
||||
"apply": "Appliquer",
|
||||
"close": "Fermer",
|
||||
"ok": "OK",
|
||||
"copy": "Copier",
|
||||
"copied": "Copié",
|
||||
"createCollection": "Créer une collection",
|
||||
"localVariables": "Variables locales",
|
||||
"noVariableCollections": "Aucune collection de variables",
|
||||
"selectLayerForJSX": "Sélectionnez un calque pour voir son code JSX",
|
||||
"connectAIProvider": "Connectez un fournisseur d’IA pour commencer à discuter.",
|
||||
"connect": "Connecter",
|
||||
"getAPIKey": "Obtenir une clé API {provider} →",
|
||||
"oneKeyManyModels": "Une clé pour plus de 100 modèles de tous les fournisseurs.",
|
||||
"describeChange": "Décrivez un changement…",
|
||||
"describeCreateOrChange": "Décrivez ce que vous voulez créer ou modifier.",
|
||||
"stopGenerating": "Arrêter la génération",
|
||||
"sendMessage": "Envoyer le message",
|
||||
"baseURLPlaceholder": "Base URL (ex. http://localhost:11434/v1)",
|
||||
"modelIDPlaceholder": "Model ID (ex. llama-3.3-70b)",
|
||||
"yourName": "Votre nom",
|
||||
"enterYourName": "Entrez votre nom",
|
||||
"shareThisFile": "Partager ce fichier",
|
||||
"joinRoom": "Rejoindre la salle",
|
||||
"join": "Rejoindre",
|
||||
"roomLink": "Lien de la salle",
|
||||
"joinCollaboration": "Rejoindre la collaboration",
|
||||
"orJoinRoom": "ou rejoindre une salle",
|
||||
"pasteRoomLinkOrId": "Collez le lien ou l’ID de la salle",
|
||||
"connected": "Connecté",
|
||||
"search": "Rechercher…",
|
||||
"noResults": "Aucun résultat",
|
||||
"share": "Partager"
|
||||
}
|
||||
}
|
||||
177
packages/vue/src/locales/it.json
Normal file
177
packages/vue/src/locales/it.json
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
{
|
||||
"menu": {
|
||||
"file": "File",
|
||||
"edit": "Modifica",
|
||||
"view": "Visualizza",
|
||||
"object": "Oggetto",
|
||||
"arrange": "Disponi",
|
||||
"text": "Testo",
|
||||
"new": "Nuovo",
|
||||
"open": "Apri…",
|
||||
"save": "Salva",
|
||||
"saveAs": "Salva con nome…",
|
||||
"exportSelection": "Esporta selezione…",
|
||||
"autosave": "Salvataggio automatico su file",
|
||||
"copy": "Copia",
|
||||
"paste": "Incolla",
|
||||
"zoomIn": "Ingrandisci",
|
||||
"zoomOut": "Riduci",
|
||||
"profiler": "Profilo prestazioni",
|
||||
"language": "Lingua",
|
||||
"moveToPage": "Sposta nella pagina",
|
||||
"createInstance": "Crea istanza",
|
||||
"hide": "Nascondi",
|
||||
"show": "Mostra",
|
||||
"lock": "Blocca",
|
||||
"unlock": "Sblocca",
|
||||
"cut": "Taglia",
|
||||
"front": "Davanti",
|
||||
"back": "Dietro",
|
||||
"toggleUI": "Mostra/nascondi interfaccia",
|
||||
"bold": "Grassetto",
|
||||
"italic": "Corsivo",
|
||||
"underline": "Sottolineato"
|
||||
},
|
||||
"commands": {
|
||||
"undo": "Annulla",
|
||||
"redo": "Ripeti",
|
||||
"selectAll": "Seleziona tutto",
|
||||
"duplicate": "Duplica",
|
||||
"delete": "Elimina",
|
||||
"group": "Raggruppa",
|
||||
"ungroup": "Separa",
|
||||
"createComponent": "Crea componente",
|
||||
"createComponentSet": "Crea set di componenti",
|
||||
"createInstance": "Crea istanza",
|
||||
"detachInstance": "Scollega istanza",
|
||||
"goToMainComponent": "Vai al componente principale",
|
||||
"addAutoLayout": "Aggiungi auto-layout",
|
||||
"bringToFront": "Porta in primo piano",
|
||||
"sendToBack": "Porta in secondo piano",
|
||||
"toggleVisibility": "Visibilità",
|
||||
"toggleLock": "Blocco",
|
||||
"moveToPage": "Sposta nella pagina",
|
||||
"zoomTo100": "Zoom 100%",
|
||||
"zoomToFit": "Adatta allo schermo",
|
||||
"zoomToSelection": "Zoom sulla selezione"
|
||||
},
|
||||
"tools": {
|
||||
"move": "Sposta",
|
||||
"frame": "Cornice",
|
||||
"section": "Sezione",
|
||||
"rectangle": "Rettangolo",
|
||||
"ellipse": "Ellisse",
|
||||
"line": "Linea",
|
||||
"polygon": "Poligono",
|
||||
"star": "Stella",
|
||||
"pen": "Penna",
|
||||
"text": "Testo",
|
||||
"hand": "Mano"
|
||||
},
|
||||
"panels": {
|
||||
"layers": "Livelli",
|
||||
"pages": "Pagine",
|
||||
"design": "Design",
|
||||
"code": "Codice",
|
||||
"ai": "AI",
|
||||
"assets": "Risorse",
|
||||
"page": "Pagina",
|
||||
"position": "Posizione",
|
||||
"layout": "Layout",
|
||||
"autoLayout": "Auto-layout",
|
||||
"alignment": "Allineamento",
|
||||
"appearance": "Aspetto",
|
||||
"fill": "Riempimento",
|
||||
"stroke": "Contorno",
|
||||
"effects": "Effetti",
|
||||
"export": "Esporta",
|
||||
"typography": "Tipografia",
|
||||
"variables": "Variabili",
|
||||
"constraints": "Vincoli",
|
||||
"addFill": "Aggiungi riempimento",
|
||||
"addStroke": "Aggiungi contorno",
|
||||
"addEffect": "Aggiungi effetto",
|
||||
"addExport": "Aggiungi esportazione",
|
||||
"dropShadow": "Ombra esterna",
|
||||
"innerShadow": "Ombra interna",
|
||||
"layerBlur": "Sfocatura livello",
|
||||
"backgroundBlur": "Sfocatura sfondo",
|
||||
"noSelection": "Nessuna selezione",
|
||||
"noLocalVariables": "Nessuna variabile locale",
|
||||
"openVariables": "Apri variabili",
|
||||
"addPage": "Aggiungi pagina",
|
||||
"toggleVisibility": "Attiva/disattiva visibilità",
|
||||
"independentCornerRadii": "Raggi angolo indipendenti",
|
||||
"detachVariable": "Scollega variabile",
|
||||
"applyVariable": "Applica variabile",
|
||||
"noVariablesFound": "Nessuna variabile trovata",
|
||||
"addAutoLayout": "Aggiungi auto-layout (Shift+A)",
|
||||
"removeAutoLayout": "Rimuovi auto-layout",
|
||||
"mixed": "Misto",
|
||||
"layersCount": "{count} livelli",
|
||||
"goToMainComponent": "Vai al componente principale",
|
||||
"detachInstance": "Scollega istanza",
|
||||
"solid": "Pieno",
|
||||
"linearGradient": "Lineare",
|
||||
"radialGradient": "Radiale",
|
||||
"image": "Immagine",
|
||||
"stops": "Punti",
|
||||
"addStop": "Aggiungi punto",
|
||||
"alignLeft": "Allinea a sinistra",
|
||||
"alignCenter": "Centra",
|
||||
"alignRight": "Allinea a destra",
|
||||
"alignTop": "Allinea in alto",
|
||||
"alignMiddle": "Centra verticalmente",
|
||||
"alignBottom": "Allinea in basso",
|
||||
"alignCenterHorizontally": "Centra orizzontalmente",
|
||||
"alignCenterVertically": "Centra verticalmente",
|
||||
"flipHorizontal": "Rifletti orizzontalmente",
|
||||
"flipVertical": "Rifletti verticalmente",
|
||||
"rotate90": "Ruota di 90°",
|
||||
"mixedFillsHelp": "Fai clic su + per sostituire i riempimenti misti",
|
||||
"mixedStrokesHelp": "Fai clic su + per sostituire i contorni misti",
|
||||
"mixedEffectsHelp": "Fai clic su + per sostituire gli effetti misti",
|
||||
"strokeSides": "Lati del contorno"
|
||||
},
|
||||
"pages": {
|
||||
"newPage": "Nuova pagina",
|
||||
"rename": "Rinomina",
|
||||
"delete": "Elimina",
|
||||
"pageName": "Pagina {number}"
|
||||
},
|
||||
"dialogs": {
|
||||
"cancel": "Annulla",
|
||||
"apply": "Applica",
|
||||
"close": "Chiudi",
|
||||
"ok": "OK",
|
||||
"copy": "Copia",
|
||||
"copied": "Copiato",
|
||||
"createCollection": "Crea raccolta",
|
||||
"localVariables": "Variabili locali",
|
||||
"noVariableCollections": "Nessuna raccolta di variabili",
|
||||
"selectLayerForJSX": "Seleziona un livello per vedere il suo codice JSX",
|
||||
"connectAIProvider": "Collega un provider AI per iniziare a chattare.",
|
||||
"connect": "Connetti",
|
||||
"getAPIKey": "Ottieni una chiave API {provider} →",
|
||||
"oneKeyManyModels": "Una chiave per oltre 100 modelli di tutti i provider.",
|
||||
"describeChange": "Descrivi una modifica…",
|
||||
"describeCreateOrChange": "Descrivi cosa vuoi creare o cambiare.",
|
||||
"stopGenerating": "Interrompi generazione",
|
||||
"sendMessage": "Invia messaggio",
|
||||
"baseURLPlaceholder": "Base URL (es. http://localhost:11434/v1)",
|
||||
"modelIDPlaceholder": "Model ID (es. llama-3.3-70b)",
|
||||
"yourName": "Il tuo nome",
|
||||
"enterYourName": "Inserisci il tuo nome",
|
||||
"shareThisFile": "Condividi questo file",
|
||||
"joinRoom": "Entra nella stanza",
|
||||
"join": "Entra",
|
||||
"roomLink": "Link della stanza",
|
||||
"joinCollaboration": "Partecipa alla collaborazione",
|
||||
"orJoinRoom": "o entra in una stanza",
|
||||
"pasteRoomLinkOrId": "Incolla il link o l'ID della stanza",
|
||||
"connected": "Connesso",
|
||||
"search": "Cerca…",
|
||||
"noResults": "Nessun risultato",
|
||||
"share": "Condividi"
|
||||
}
|
||||
}
|
||||
177
packages/vue/src/locales/pl.json
Normal file
177
packages/vue/src/locales/pl.json
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
{
|
||||
"menu": {
|
||||
"file": "Plik",
|
||||
"edit": "Edycja",
|
||||
"view": "Widok",
|
||||
"object": "Obiekt",
|
||||
"arrange": "Rozmieszczenie",
|
||||
"text": "Tekst",
|
||||
"new": "Nowy",
|
||||
"open": "Otwórz…",
|
||||
"save": "Zapisz",
|
||||
"saveAs": "Zapisz jako…",
|
||||
"exportSelection": "Eksportuj zaznaczenie…",
|
||||
"autosave": "Automatyczny zapis do pliku",
|
||||
"copy": "Kopiuj",
|
||||
"paste": "Wklej",
|
||||
"zoomIn": "Powiększ",
|
||||
"zoomOut": "Pomniejsz",
|
||||
"profiler": "Profiler wydajności",
|
||||
"language": "Język",
|
||||
"moveToPage": "Przenieś na stronę",
|
||||
"createInstance": "Utwórz instancję",
|
||||
"hide": "Ukryj",
|
||||
"show": "Pokaż",
|
||||
"lock": "Zablokuj",
|
||||
"unlock": "Odblokuj",
|
||||
"cut": "Wytnij",
|
||||
"front": "Do przodu",
|
||||
"back": "Do tyłu",
|
||||
"toggleUI": "Przełącz interfejs",
|
||||
"bold": "Pogrubienie",
|
||||
"italic": "Kursywa",
|
||||
"underline": "Podkreślenie"
|
||||
},
|
||||
"commands": {
|
||||
"undo": "Cofnij",
|
||||
"redo": "Ponów",
|
||||
"selectAll": "Zaznacz wszystko",
|
||||
"duplicate": "Duplikuj",
|
||||
"delete": "Usuń",
|
||||
"group": "Grupuj",
|
||||
"ungroup": "Rozgrupuj",
|
||||
"createComponent": "Utwórz komponent",
|
||||
"createComponentSet": "Utwórz zestaw komponentów",
|
||||
"createInstance": "Utwórz instancję",
|
||||
"detachInstance": "Odłącz instancję",
|
||||
"goToMainComponent": "Przejdź do komponentu",
|
||||
"addAutoLayout": "Dodaj auto-layout",
|
||||
"bringToFront": "Przenieś na wierzch",
|
||||
"sendToBack": "Przenieś na spód",
|
||||
"toggleVisibility": "Widoczność",
|
||||
"toggleLock": "Blokada",
|
||||
"moveToPage": "Przenieś na stronę",
|
||||
"zoomTo100": "Zoom 100%",
|
||||
"zoomToFit": "Dopasuj do ekranu",
|
||||
"zoomToSelection": "Zoom do zaznaczenia"
|
||||
},
|
||||
"tools": {
|
||||
"move": "Przesuń",
|
||||
"frame": "Ramka",
|
||||
"section": "Sekcja",
|
||||
"rectangle": "Prostokąt",
|
||||
"ellipse": "Elipsa",
|
||||
"line": "Linia",
|
||||
"polygon": "Wielokąt",
|
||||
"star": "Gwiazda",
|
||||
"pen": "Pióro",
|
||||
"text": "Tekst",
|
||||
"hand": "Dłoń"
|
||||
},
|
||||
"panels": {
|
||||
"layers": "Warstwy",
|
||||
"pages": "Strony",
|
||||
"design": "Design",
|
||||
"code": "Kod",
|
||||
"ai": "AI",
|
||||
"assets": "Zasoby",
|
||||
"page": "Strona",
|
||||
"position": "Pozycja",
|
||||
"layout": "Układ",
|
||||
"autoLayout": "Auto-layout",
|
||||
"alignment": "Wyrównanie",
|
||||
"appearance": "Wygląd",
|
||||
"fill": "Wypełnienie",
|
||||
"stroke": "Obrys",
|
||||
"effects": "Efekty",
|
||||
"export": "Eksport",
|
||||
"typography": "Typografia",
|
||||
"variables": "Zmienne",
|
||||
"constraints": "Ograniczenia",
|
||||
"addFill": "Dodaj wypełnienie",
|
||||
"addStroke": "Dodaj obrys",
|
||||
"addEffect": "Dodaj efekt",
|
||||
"addExport": "Dodaj eksport",
|
||||
"dropShadow": "Cień",
|
||||
"innerShadow": "Cień wewnętrzny",
|
||||
"layerBlur": "Rozmycie warstwy",
|
||||
"backgroundBlur": "Rozmycie tła",
|
||||
"noSelection": "Brak zaznaczenia",
|
||||
"noLocalVariables": "Brak lokalnych zmiennych",
|
||||
"openVariables": "Otwórz zmienne",
|
||||
"addPage": "Dodaj stronę",
|
||||
"toggleVisibility": "Przełącz widoczność",
|
||||
"independentCornerRadii": "Niezależne promienie rogów",
|
||||
"detachVariable": "Odłącz zmienną",
|
||||
"applyVariable": "Zastosuj zmienną",
|
||||
"noVariablesFound": "Nie znaleziono zmiennych",
|
||||
"addAutoLayout": "Dodaj auto-layout (Shift+A)",
|
||||
"removeAutoLayout": "Usuń auto-layout",
|
||||
"mixed": "Mieszane",
|
||||
"layersCount": "{count} warstw",
|
||||
"goToMainComponent": "Przejdź do głównego komponentu",
|
||||
"detachInstance": "Odłącz instancję",
|
||||
"solid": "Jednolity",
|
||||
"linearGradient": "Liniowy",
|
||||
"radialGradient": "Promieniowy",
|
||||
"image": "Obraz",
|
||||
"stops": "Punkty",
|
||||
"addStop": "Dodaj punkt",
|
||||
"alignLeft": "Wyrównaj do lewej",
|
||||
"alignCenter": "Wyśrodkuj",
|
||||
"alignRight": "Wyrównaj do prawej",
|
||||
"alignTop": "Wyrównaj do góry",
|
||||
"alignMiddle": "Wyśrodkuj w pionie",
|
||||
"alignBottom": "Wyrównaj do dołu",
|
||||
"alignCenterHorizontally": "Wyśrodkuj poziomo",
|
||||
"alignCenterVertically": "Wyśrodkuj pionowo",
|
||||
"flipHorizontal": "Odbij poziomo",
|
||||
"flipVertical": "Odbij pionowo",
|
||||
"rotate90": "Obróć o 90°",
|
||||
"mixedFillsHelp": "Kliknij +, aby zastąpić mieszane wypełnienia",
|
||||
"mixedStrokesHelp": "Kliknij +, aby zastąpić mieszane obrysy",
|
||||
"mixedEffectsHelp": "Kliknij +, aby zastąpić mieszane efekty",
|
||||
"strokeSides": "Boki obrysu"
|
||||
},
|
||||
"pages": {
|
||||
"newPage": "Nowa strona",
|
||||
"rename": "Zmień nazwę",
|
||||
"delete": "Usuń",
|
||||
"pageName": "Strona {number}"
|
||||
},
|
||||
"dialogs": {
|
||||
"cancel": "Anuluj",
|
||||
"apply": "Zastosuj",
|
||||
"close": "Zamknij",
|
||||
"ok": "OK",
|
||||
"copy": "Kopiuj",
|
||||
"copied": "Skopiowano",
|
||||
"createCollection": "Utwórz kolekcję",
|
||||
"localVariables": "Zmienne lokalne",
|
||||
"noVariableCollections": "Brak kolekcji zmiennych",
|
||||
"selectLayerForJSX": "Wybierz warstwę, aby zobaczyć jej kod JSX",
|
||||
"connectAIProvider": "Połącz dostawcę AI, aby rozpocząć czat.",
|
||||
"connect": "Połącz",
|
||||
"getAPIKey": "Pobierz klucz API {provider} →",
|
||||
"oneKeyManyModels": "Jeden klucz do ponad 100 modeli od wszystkich dostawców.",
|
||||
"describeChange": "Opisz zmianę…",
|
||||
"describeCreateOrChange": "Opisz, co chcesz utworzyć lub zmienić.",
|
||||
"stopGenerating": "Zatrzymaj generowanie",
|
||||
"sendMessage": "Wyślij wiadomość",
|
||||
"baseURLPlaceholder": "Base URL (np. http://localhost:11434/v1)",
|
||||
"modelIDPlaceholder": "Model ID (np. llama-3.3-70b)",
|
||||
"yourName": "Twoje imię",
|
||||
"enterYourName": "Wpisz swoje imię",
|
||||
"shareThisFile": "Udostępnij ten plik",
|
||||
"joinRoom": "Dołącz do pokoju",
|
||||
"join": "Dołącz",
|
||||
"roomLink": "Link do pokoju",
|
||||
"joinCollaboration": "Dołącz do współpracy",
|
||||
"orJoinRoom": "lub dołącz do pokoju",
|
||||
"pasteRoomLinkOrId": "Wklej link lub ID pokoju",
|
||||
"connected": "Połączono",
|
||||
"search": "Szukaj…",
|
||||
"noResults": "Brak wyników",
|
||||
"share": "Udostępnij"
|
||||
}
|
||||
}
|
||||
181
packages/vue/src/locales/ru.json
Normal file
181
packages/vue/src/locales/ru.json
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
{
|
||||
"menu": {
|
||||
"file": "Файл",
|
||||
"edit": "Правка",
|
||||
"view": "Вид",
|
||||
"object": "Объект",
|
||||
"arrange": "Порядок",
|
||||
"text": "Текст",
|
||||
"new": "Создать",
|
||||
"open": "Открыть…",
|
||||
"save": "Сохранить",
|
||||
"saveAs": "Сохранить как…",
|
||||
"exportSelection": "Экспорт выделения…",
|
||||
"autosave": "Автосохранение в файл",
|
||||
"copy": "Копировать",
|
||||
"paste": "Вставить",
|
||||
"zoomIn": "Увеличить",
|
||||
"zoomOut": "Уменьшить",
|
||||
"profiler": "Профилирование",
|
||||
"language": "Язык",
|
||||
"moveToPage": "Переместить на страницу",
|
||||
"createInstance": "Создать экземпляр",
|
||||
"hide": "Скрыть",
|
||||
"show": "Показать",
|
||||
"lock": "Заблокировать",
|
||||
"unlock": "Разблокировать",
|
||||
"cut": "Вырезать",
|
||||
"front": "Вперёд",
|
||||
"back": "Назад",
|
||||
"toggleUI": "Переключить интерфейс",
|
||||
"bold": "Жирный",
|
||||
"italic": "Курсив",
|
||||
"underline": "Подчёркнутый"
|
||||
},
|
||||
"commands": {
|
||||
"undo": "Отменить",
|
||||
"redo": "Повторить",
|
||||
"selectAll": "Выделить всё",
|
||||
"duplicate": "Дублировать",
|
||||
"delete": "Удалить",
|
||||
"group": "Сгруппировать",
|
||||
"ungroup": "Разгруппировать",
|
||||
"createComponent": "Создать компонент",
|
||||
"createComponentSet": "Создать набор компонентов",
|
||||
"createInstance": "Создать экземпляр",
|
||||
"detachInstance": "Отвязать экземпляр",
|
||||
"goToMainComponent": "Перейти к компоненту",
|
||||
"addAutoLayout": "Добавить авто-раскладку",
|
||||
"bringToFront": "На передний план",
|
||||
"sendToBack": "На задний план",
|
||||
"toggleVisibility": "Видимость",
|
||||
"toggleLock": "Блокировка",
|
||||
"moveToPage": "Переместить на страницу",
|
||||
"zoomTo100": "Масштаб 100%",
|
||||
"zoomToFit": "Вписать в экран",
|
||||
"zoomToSelection": "Масштаб по выделению"
|
||||
},
|
||||
"tools": {
|
||||
"move": "Перемещение",
|
||||
"frame": "Фрейм",
|
||||
"section": "Секция",
|
||||
"rectangle": "Прямоугольник",
|
||||
"ellipse": "Эллипс",
|
||||
"line": "Линия",
|
||||
"polygon": "Многоугольник",
|
||||
"star": "Звезда",
|
||||
"pen": "Перо",
|
||||
"text": "Текст",
|
||||
"hand": "Рука"
|
||||
},
|
||||
"panels": {
|
||||
"layers": "Слои",
|
||||
"pages": "Страницы",
|
||||
"design": "Дизайн",
|
||||
"code": "Код",
|
||||
"ai": "AI",
|
||||
"assets": "Ассеты",
|
||||
"page": "Страница",
|
||||
"position": "Позиция",
|
||||
"layout": "Раскладка",
|
||||
"autoLayout": "Автораскладка",
|
||||
"alignment": "Выравнивание",
|
||||
"appearance": "Внешний вид",
|
||||
"fill": "Заливка",
|
||||
"stroke": "Обводка",
|
||||
"effects": "Эффекты",
|
||||
"export": "Экспорт",
|
||||
"typography": "Типографика",
|
||||
"variables": "Переменные",
|
||||
"constraints": "Ограничения",
|
||||
"addFill": "Добавить заливку",
|
||||
"addStroke": "Добавить обводку",
|
||||
"addEffect": "Добавить эффект",
|
||||
"addExport": "Добавить экспорт",
|
||||
"dropShadow": "Тень",
|
||||
"innerShadow": "Внутренняя тень",
|
||||
"layerBlur": "Размытие слоя",
|
||||
"backgroundBlur": "Размытие фона",
|
||||
"noSelection": "Ничего не выделено",
|
||||
"noLocalVariables": "Нет локальных переменных",
|
||||
"openVariables": "Открыть переменные",
|
||||
"addPage": "Добавить страницу",
|
||||
"toggleVisibility": "Переключить видимость",
|
||||
"independentCornerRadii": "Независимые радиусы углов",
|
||||
"detachVariable": "Отвязать переменную",
|
||||
"applyVariable": "Применить переменную",
|
||||
"noVariablesFound": "Переменные не найдены",
|
||||
"addAutoLayout": "Добавить автораскладку (Shift+A)",
|
||||
"removeAutoLayout": "Убрать автораскладку",
|
||||
"alignLeft": "Выровнять по левому краю",
|
||||
"alignCenterHorizontally": "Выровнять по центру по горизонтали",
|
||||
"alignRight": "Выровнять по правому краю",
|
||||
"alignTop": "Выровнять по верхнему краю",
|
||||
"alignCenterVertically": "Выровнять по центру по вертикали",
|
||||
"alignBottom": "Выровнять по нижнему краю",
|
||||
"flipHorizontal": "Отразить по горизонтали",
|
||||
"flipVertical": "Отразить по вертикали",
|
||||
"rotate90": "Повернуть на 90°",
|
||||
"mixedFillsHelp": "Нажмите +, чтобы заменить смешанные заливки",
|
||||
"mixedStrokesHelp": "Нажмите +, чтобы заменить смешанные обводки",
|
||||
"mixedEffectsHelp": "Нажмите +, чтобы заменить смешанные эффекты",
|
||||
"strokeSides": "Стороны обводки",
|
||||
"mixed": "Смешанное",
|
||||
"layersCount": "{count} слоёв",
|
||||
"goToMainComponent": "Перейти к главному компоненту",
|
||||
"detachInstance": "Отвязать экземпляр",
|
||||
"solid": "Сплошной",
|
||||
"linearGradient": "Линейный",
|
||||
"radialGradient": "Радиальный",
|
||||
"image": "Изображение",
|
||||
"stops": "Точки",
|
||||
"addStop": "Добавить точку",
|
||||
"alignLeft": "По левому краю",
|
||||
"alignCenter": "По центру",
|
||||
"alignRight": "По правому краю",
|
||||
"alignTop": "По верхнему краю",
|
||||
"alignMiddle": "По середине",
|
||||
"alignBottom": "По нижнему краю"
|
||||
},
|
||||
"pages": {
|
||||
"newPage": "Новая страница",
|
||||
"rename": "Переименовать",
|
||||
"delete": "Удалить",
|
||||
"pageName": "Страница {number}"
|
||||
},
|
||||
"dialogs": {
|
||||
"cancel": "Отмена",
|
||||
"apply": "Применить",
|
||||
"close": "Закрыть",
|
||||
"ok": "ОК",
|
||||
"copy": "Копировать",
|
||||
"copied": "Скопировано",
|
||||
"createCollection": "Создать коллекцию",
|
||||
"localVariables": "Локальные переменные",
|
||||
"noVariableCollections": "Нет коллекций переменных",
|
||||
"selectLayerForJSX": "Выберите слой, чтобы увидеть его JSX-код",
|
||||
"connectAIProvider": "Подключите AI-провайдера, чтобы начать чат.",
|
||||
"connect": "Подключить",
|
||||
"getAPIKey": "Получить API-ключ {provider} →",
|
||||
"oneKeyManyModels": "Один ключ для 100+ моделей от разных провайдеров.",
|
||||
"describeChange": "Опишите изменение…",
|
||||
"describeCreateOrChange": "Опишите, что вы хотите создать или изменить.",
|
||||
"stopGenerating": "Остановить генерацию",
|
||||
"sendMessage": "Отправить сообщение",
|
||||
"baseURLPlaceholder": "Base URL (например, http://localhost:11434/v1)",
|
||||
"modelIDPlaceholder": "Model ID (например, llama-3.3-70b)",
|
||||
"yourName": "Ваше имя",
|
||||
"enterYourName": "Введите ваше имя",
|
||||
"shareThisFile": "Поделиться файлом",
|
||||
"joinRoom": "Войти в комнату",
|
||||
"join": "Войти",
|
||||
"roomLink": "Ссылка на комнату",
|
||||
"joinCollaboration": "Присоединиться к совместной работе",
|
||||
"orJoinRoom": "или войти в комнату",
|
||||
"pasteRoomLinkOrId": "Вставьте ссылку на комнату или ID",
|
||||
"connected": "Подключено",
|
||||
"search": "Поиск…",
|
||||
"noResults": "Ничего не найдено",
|
||||
"share": "Поделиться"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,19 @@
|
|||
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)
|
||||
export interface InlineRenameState<T extends string> {
|
||||
editingId: Ref<T | null>
|
||||
start: (id: T, currentName: string) => void
|
||||
focusInput: (input: HTMLInputElement | null) => Promise<void>
|
||||
commit: (id: T, input: HTMLInputElement) => void
|
||||
cancel: () => void
|
||||
onKeydown: (e: KeyboardEvent) => void
|
||||
}
|
||||
|
||||
export function useInlineRename<T extends string>(
|
||||
onCommit: (id: T, newName: string) => void
|
||||
): InlineRenameState<T> {
|
||||
const editingId: Ref<T | null> = ref(null)
|
||||
const inputRef: Ref<HTMLInputElement | null> = ref(null)
|
||||
let originalName = ''
|
||||
let cleanupOutsideClick: (() => void) | undefined
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"target": "ES2022",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
|
|
@ -8,10 +9,18 @@
|
|||
"declaration": true,
|
||||
"declarationDir": "./dist",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"rootDir": "..",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@open-pencil/vue": ["./src/index.ts"],
|
||||
"@open-pencil/vue/*": ["./src/*"],
|
||||
"@open-pencil/core": ["../core/src/index.ts"],
|
||||
"@open-pencil/core/*": ["../core/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue"],
|
||||
"include": ["src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts", "../core/src/global.d.ts"],
|
||||
"exclude": ["node_modules", "dist", "example"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import IconChevronRight from '~icons/lucide/chevron-right'
|
|||
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useInlineRename } from '@open-pencil/vue'
|
||||
import { useI18n, useInlineRename } from '@open-pencil/vue'
|
||||
import { useMenuUI } from '@/components/ui/menu'
|
||||
import { IS_TAURI } from '@/constants'
|
||||
import { useAppMenu } from '@/composables/use-app-menu'
|
||||
|
|
@ -46,6 +46,7 @@ function commitRename(input: HTMLInputElement) {
|
|||
|
||||
const isMac = navigator.platform.includes('Mac')
|
||||
const mod = isMac ? '⌘' : 'Ctrl+'
|
||||
const { menu: t } = useI18n()
|
||||
|
||||
interface MenuAction {
|
||||
separator?: false
|
||||
|
|
@ -91,7 +92,7 @@ const subMenuCls = useMenuUI({ content: 'min-w-44' })
|
|||
@dblclick="startRename"
|
||||
>{{ store.state.documentName }}</span
|
||||
>
|
||||
<Tip label="Toggle UI (⌘\)">
|
||||
<Tip :label="`${t.toggleUI} (${mod}\\)`">
|
||||
<button
|
||||
data-test-id="app-toggle-ui"
|
||||
class="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded text-muted transition-colors hover:bg-hover hover:text-surface"
|
||||
|
|
@ -133,6 +134,17 @@ const subMenuCls = useMenuUI({ content: 'min-w-44' })
|
|||
v-if="'separator' in sub && sub.separator"
|
||||
:class="menuCls.separator"
|
||||
/>
|
||||
<MenubarCheckboxItem
|
||||
v-else-if="'onCheckedChange' in sub && sub.onCheckedChange"
|
||||
:model-value="'checked' in sub ? sub.checked : undefined"
|
||||
:class="menuCls.item"
|
||||
@update:model-value="sub.onCheckedChange?.($event as boolean)"
|
||||
>
|
||||
<span class="flex-1">{{ 'label' in sub ? sub.label : '' }}</span>
|
||||
<MenubarItemIndicator class="text-surface">
|
||||
<icon-lucide-check class="size-3.5" />
|
||||
</MenubarItemIndicator>
|
||||
</MenubarCheckboxItem>
|
||||
<MenubarItem
|
||||
v-else
|
||||
:class="menuCls.item"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import ChatInput from '@/components/chat/ChatInput.vue'
|
|||
import ChatMessage from '@/components/chat/ChatMessage.vue'
|
||||
import ProviderSetup from '@/components/chat/ProviderSetup.vue'
|
||||
import { useAIChat } from '@/composables/use-chat'
|
||||
import { useI18n } from '@open-pencil/vue'
|
||||
|
||||
import type { Chat } from '@ai-sdk/vue'
|
||||
import type { UIMessage } from 'ai'
|
||||
|
|
@ -17,6 +18,7 @@ import type { UIMessage } from 'ai'
|
|||
const IS_DEV = import.meta.env.DEV
|
||||
|
||||
const { isConfigured, ensureChat, resetChat } = useAIChat()
|
||||
const { dialogs } = useI18n()
|
||||
|
||||
const chat = ref<Chat<UIMessage> | null>(null)
|
||||
|
||||
|
|
@ -120,7 +122,7 @@ function handleClearChat() {
|
|||
class="flex h-full flex-col items-center justify-center gap-3 text-muted"
|
||||
>
|
||||
<icon-lucide-message-circle class="size-8 opacity-50" />
|
||||
<p class="text-center text-xs">Describe what you want to create or change.</p>
|
||||
<p class="text-center text-xs">{{ dialogs.describeCreateOrChange }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { useClipboard } from '@vueuse/core'
|
|||
import { computed, ref } from 'vue'
|
||||
|
||||
import { selectionToJSX } from '@open-pencil/core'
|
||||
import { useSceneComputed } from '@open-pencil/vue'
|
||||
import { useI18n, useSceneComputed } from '@open-pencil/vue'
|
||||
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ import type { JSXFormat } from '@open-pencil/core'
|
|||
|
||||
const store = useEditorStore()
|
||||
const { copy, copied } = useClipboard({ copiedDuring: 2000 })
|
||||
const { dialogs } = useI18n()
|
||||
const jsxFormat = ref<JSXFormat>('openpencil')
|
||||
|
||||
function toggleFormat() {
|
||||
|
|
@ -44,7 +45,7 @@ function copyCode() {
|
|||
data-test-id="code-panel-empty"
|
||||
class="flex flex-1 items-center justify-center px-4 text-center"
|
||||
>
|
||||
<span class="text-xs text-muted">Select a layer to see its JSX code</span>
|
||||
<span class="text-xs text-muted">{{ dialogs.selectLayerForJSX }}</span>
|
||||
</div>
|
||||
|
||||
<div v-else data-test-id="code-panel" class="flex min-h-0 flex-1 flex-col">
|
||||
|
|
@ -69,7 +70,7 @@ function copyCode() {
|
|||
>
|
||||
<icon-lucide-check v-if="copied" class="size-3 text-green-400" />
|
||||
<icon-lucide-copy v-else class="size-3" />
|
||||
{{ copied ? 'Copied' : 'Copy' }}
|
||||
{{ copied ? dialogs.copied : dialogs.copy }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ import { usePopoverUI } from '@/components/ui/popover'
|
|||
import { DEFAULT_COLLAB_STATE, useCollabInjected } from '@/composables/use-collab'
|
||||
import { toast } from '@/utils/toast'
|
||||
import { initials } from '@/utils/text'
|
||||
import { useI18n } from '@open-pencil/vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const cls = usePopoverUI({ content: 'z-50 w-72 p-3' })
|
||||
const collab = useCollabInjected()
|
||||
const { copy, copied } = useClipboard({ copiedDuring: 2000 })
|
||||
const { dialogs } = useI18n()
|
||||
|
||||
const joinInput = ref('')
|
||||
const nameDraft = ref(collab?.state.value.localName ?? '')
|
||||
|
|
@ -120,7 +122,7 @@ function onDisconnect() {
|
|||
"
|
||||
>
|
||||
<icon-lucide-share-2 class="size-3.5" />
|
||||
{{ state.connected ? 'Connected' : isJoining ? 'Join room' : 'Share' }}
|
||||
{{ state.connected ? dialogs.connected : isJoining ? dialogs.joinRoom : dialogs.share }}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
|
||||
|
|
@ -134,7 +136,7 @@ function onDisconnect() {
|
|||
>
|
||||
<!-- Connected state -->
|
||||
<template v-if="state.connected">
|
||||
<div class="mb-3 text-xs font-medium text-surface">Room link</div>
|
||||
<div class="mb-3 text-xs font-medium text-surface">{{ dialogs.roomLink }}</div>
|
||||
<div class="mb-3 flex items-center gap-1.5">
|
||||
<input
|
||||
:value="shareUrl"
|
||||
|
|
@ -169,18 +171,18 @@ function onDisconnect() {
|
|||
|
||||
<!-- Joining via /share/ link -->
|
||||
<template v-else-if="isJoining">
|
||||
<div class="mb-1 text-xs font-medium text-surface">Join collaboration</div>
|
||||
<div class="mb-1 text-xs font-medium text-surface">{{ dialogs.joinCollaboration }}</div>
|
||||
<div class="mb-3 text-[11px] text-muted">
|
||||
Someone shared this file with you. Enter your name to join.
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="mb-1 block text-xs text-muted">Your name</label>
|
||||
<label class="mb-1 block text-xs text-muted">{{ dialogs.yourName }}</label>
|
||||
<input
|
||||
v-model="nameDraft"
|
||||
data-test-id="collab-name-input"
|
||||
class="w-full rounded border border-border bg-input px-2 py-1 text-xs text-surface"
|
||||
placeholder="Enter your name"
|
||||
:placeholder="dialogs.enterYourName"
|
||||
autofocus
|
||||
@keydown.enter="onJoin"
|
||||
/>
|
||||
|
|
@ -200,12 +202,12 @@ function onDisconnect() {
|
|||
<!-- Not connected: share or join -->
|
||||
<template v-else>
|
||||
<div class="mb-3">
|
||||
<label class="mb-1 block text-xs text-muted">Your name</label>
|
||||
<label class="mb-1 block text-xs text-muted">{{ dialogs.yourName }}</label>
|
||||
<input
|
||||
v-model="nameDraft"
|
||||
data-test-id="collab-name-input"
|
||||
class="w-full rounded border border-border bg-input px-2 py-1 text-xs text-surface"
|
||||
placeholder="Enter your name"
|
||||
:placeholder="dialogs.enterYourName"
|
||||
@keydown.enter="onShare"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -217,12 +219,12 @@ function onDisconnect() {
|
|||
@click="onShare"
|
||||
>
|
||||
<icon-lucide-share-2 class="size-3.5" />
|
||||
Share this file
|
||||
{{ dialogs.shareThisFile }}
|
||||
</button>
|
||||
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<div class="h-px flex-1 bg-border" />
|
||||
<span class="text-[11px] text-muted">or join a room</span>
|
||||
<span class="text-[11px] text-muted">{{ dialogs.orJoinRoom }}</span>
|
||||
<div class="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
|
||||
|
|
@ -231,7 +233,7 @@ function onDisconnect() {
|
|||
v-model="joinInput"
|
||||
data-test-id="collab-join-input"
|
||||
class="min-w-0 flex-1 rounded border border-border bg-input px-2 py-1 text-xs text-surface"
|
||||
placeholder="Paste room link or ID"
|
||||
:placeholder="dialogs.pasteRoomLinkOrId"
|
||||
@keydown.enter="onJoin"
|
||||
/>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useSelectionState, useEditorCommands } from '@open-pencil/vue'
|
||||
import { useI18n, useSelectionState, useEditorCommands } from '@open-pencil/vue'
|
||||
|
||||
import VariablesDialog from './VariablesDialog.vue'
|
||||
import AppearanceSection from './properties/AppearanceSection.vue'
|
||||
|
|
@ -24,6 +24,7 @@ const isComponentType = computed(() => {
|
|||
const t = node.value?.type
|
||||
return t === 'COMPONENT' || t === 'COMPONENT_SET' || t === 'INSTANCE'
|
||||
})
|
||||
const { panels } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -37,8 +38,8 @@ const isComponentType = computed(() => {
|
|||
data-test-id="design-multi-header"
|
||||
class="flex items-center gap-1.5 border-b border-border px-3 py-2"
|
||||
>
|
||||
<span class="text-[11px] text-muted">Mixed</span>
|
||||
<span class="text-xs font-semibold">{{ multiCount }} layers</span>
|
||||
<span class="text-[11px] text-muted">{{ panels.mixed }}</span>
|
||||
<span class="text-xs font-semibold">{{ panels.layersCount({ count: String(multiCount) }) }}</span>
|
||||
</div>
|
||||
<PositionSection />
|
||||
<AppearanceSection />
|
||||
|
|
@ -73,14 +74,14 @@ const isComponentType = computed(() => {
|
|||
class="rounded bg-component/10 px-2 py-1 text-left text-[11px] text-component hover:bg-component/20"
|
||||
@click="goToMainComponent.run()"
|
||||
>
|
||||
Go to Main Component
|
||||
{{ panels.goToMainComponent }}
|
||||
</button>
|
||||
<button
|
||||
data-test-id="design-detach-instance"
|
||||
class="rounded px-2 py-1 text-left text-[11px] text-muted hover:bg-hover"
|
||||
@click="detachInstance.run()"
|
||||
>
|
||||
Detach Instance
|
||||
{{ panels.detachInstance }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import { FillPickerRoot } from '@open-pencil/vue'
|
||||
import { FillPickerRoot, useI18n } from '@open-pencil/vue'
|
||||
|
||||
import GradientEditor from './GradientEditor.vue'
|
||||
import HsvColorArea from './HsvColorArea.vue'
|
||||
|
|
@ -24,6 +24,7 @@ function tabClass(active: boolean) {
|
|||
const { fill } = defineProps<{ fill: Fill }>()
|
||||
const emit = defineEmits<{ update: [fill: Fill] }>()
|
||||
const cls = usePopoverUI({ content: 'w-60 p-2' })
|
||||
const { panels } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -35,7 +36,7 @@ const cls = usePopoverUI({ content: 'w-60 p-2' })
|
|||
>
|
||||
<template #default="{ fill: currentFill, category, toSolid, toGradient, toImage, update }">
|
||||
<div class="mb-2 flex items-center gap-0.5">
|
||||
<Tip label="Solid">
|
||||
<Tip :label="panels.solid">
|
||||
<button
|
||||
:class="tabClass(category === 'SOLID')"
|
||||
data-test-id="fill-picker-tab-solid"
|
||||
|
|
@ -44,7 +45,7 @@ const cls = usePopoverUI({ content: 'w-60 p-2' })
|
|||
<icon-lucide-square class="size-3.5" />
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip label="Gradient">
|
||||
<Tip :label="panels.linearGradient">
|
||||
<button
|
||||
:class="tabClass(category === 'GRADIENT')"
|
||||
data-test-id="fill-picker-tab-gradient"
|
||||
|
|
@ -53,7 +54,7 @@ const cls = usePopoverUI({ content: 'w-60 p-2' })
|
|||
<icon-lucide-blend class="size-3.5" />
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip label="Image">
|
||||
<Tip :label="panels.image">
|
||||
<button
|
||||
:class="tabClass(category === 'IMAGE')"
|
||||
data-test-id="fill-picker-tab-image"
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import Tip from './ui/Tip.vue'
|
|||
import HsvColorArea from './HsvColorArea.vue'
|
||||
import ScrubInput from './ScrubInput.vue'
|
||||
import { colorToCSS } from '@open-pencil/core'
|
||||
import { GradientEditorRoot, GradientEditorBar, GradientEditorStop } from '@open-pencil/vue'
|
||||
import { GradientEditorRoot, GradientEditorBar, GradientEditorStop, useI18n } from '@open-pencil/vue'
|
||||
|
||||
import type { Fill } from '@open-pencil/core'
|
||||
|
||||
const { fill } = defineProps<{ fill: Fill }>()
|
||||
const emit = defineEmits<{ update: [fill: Fill] }>()
|
||||
const { panels } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -52,8 +53,8 @@ const emit = defineEmits<{ update: [fill: Fill] }>()
|
|||
|
||||
<div class="mb-2">
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<span class="text-[11px] text-muted">Stops</span>
|
||||
<Tip label="Add stop">
|
||||
<span class="text-[11px] text-muted">{{ panels.stops }}</span>
|
||||
<Tip :label="panels.addStop">
|
||||
<button
|
||||
class="flex size-4 cursor-pointer items-center justify-center rounded border-none bg-transparent p-0 text-muted hover:text-surface"
|
||||
data-test-id="fill-picker-add-stop"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { TreeRoot, TreeItem, ContextMenuRoot, ContextMenuTrigger, ContextMenuPortal } from 'reka-ui'
|
||||
|
||||
import { LayerTreeRoot, LayerTreeItem, useInlineRename, useLayerDrag } from '@open-pencil/vue'
|
||||
import {
|
||||
LayerTreeRoot,
|
||||
LayerTreeItem,
|
||||
useI18n,
|
||||
useInlineRename,
|
||||
useLayerDrag
|
||||
} from '@open-pencil/vue'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { nodeIcon, COMPONENT_TYPES } from '@/utils/layer-icons'
|
||||
import CanvasMenu from './CanvasMenu.vue'
|
||||
|
|
@ -10,6 +16,7 @@ import Tip from './ui/Tip.vue'
|
|||
const INDENT = 16
|
||||
const store = useEditorStore()
|
||||
const rename = useInlineRename((id, name) => store.renameNode(id, name))
|
||||
const { menu: t } = useI18n()
|
||||
const { draggingId, instruction, instructionTargetId } = useLayerDrag(store, INDENT)
|
||||
|
||||
function onLayerRightClick(e: MouseEvent) {
|
||||
|
|
@ -142,7 +149,7 @@ function onLayerRightClick(e: MouseEvent) {
|
|||
!node.locked && node.visible ? 'opacity-0 group-hover/row:opacity-100' : ''
|
||||
"
|
||||
>
|
||||
<Tip :label="node.locked ? 'Unlock' : 'Lock'">
|
||||
<Tip :label="node.locked ? t.unlock : t.lock">
|
||||
<span
|
||||
class="flex size-4 items-center justify-center rounded hover:bg-white/15"
|
||||
@pointerdown.stop
|
||||
|
|
@ -160,7 +167,7 @@ function onLayerRightClick(e: MouseEvent) {
|
|||
/>
|
||||
</span>
|
||||
</Tip>
|
||||
<Tip :label="node.visible ? 'Hide' : 'Show'">
|
||||
<Tip :label="node.visible ? t.hide : t.show">
|
||||
<span
|
||||
class="flex size-4 items-center justify-center rounded hover:bg-white/15"
|
||||
@pointerdown.stop
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from 'reka-ui'
|
||||
|
||||
import { useI18n } from '@open-pencil/vue'
|
||||
|
||||
import AppMenu from './AppMenu.vue'
|
||||
import LayerTree from './LayerTree.vue'
|
||||
import PagesPanel from './PagesPanel.vue'
|
||||
|
||||
const { panels } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -32,7 +36,7 @@ import PagesPanel from './PagesPanel.vue'
|
|||
data-test-id="layers-header"
|
||||
class="shrink-0 px-3 py-2 text-[11px] tracking-wider text-muted uppercase"
|
||||
>
|
||||
Layers
|
||||
{{ panels.layers }}
|
||||
</header>
|
||||
<LayerTree data-test-id="layers-tree" />
|
||||
</SplitterPanel>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { toast } from '@/utils/toast'
|
|||
import { useEditorStore } from '@/stores/editor'
|
||||
import Tip from '@/components/ui/Tip.vue'
|
||||
import { colorToCSS } from '@open-pencil/core'
|
||||
import { useEditorCommands } from '@open-pencil/vue'
|
||||
import { useEditorCommands, useI18n } from '@open-pencil/vue'
|
||||
import { toolIcons } from '@/utils/tools'
|
||||
import { initials } from '@/utils/text'
|
||||
|
||||
|
|
@ -37,6 +37,7 @@ const router = useRouter()
|
|||
const collab = useCollabInjected()
|
||||
const store = useEditorStore()
|
||||
const { copy } = useClipboard()
|
||||
const { dialogs } = useI18n()
|
||||
|
||||
const collabState = computed(() => collab?.state.value ?? DEFAULT_COLLAB_STATE)
|
||||
const collabPeers = computed(() => collab?.remotePeers.value ?? [])
|
||||
|
|
@ -217,7 +218,7 @@ const onlineCount = computed(() => collabPeers.value.length + 1)
|
|||
@click="onShare"
|
||||
>
|
||||
<icon-lucide-share-2 class="size-3.5 text-surface" />
|
||||
<span class="text-xs text-surface">Share</span>
|
||||
<span class="text-xs text-surface">{{ dialogs.share }}</span>
|
||||
</button>
|
||||
|
||||
<DropdownMenuRoot>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { PageListRoot, useInlineRename } from '@open-pencil/vue'
|
||||
import { PageListRoot, useI18n, useInlineRename } from '@open-pencil/vue'
|
||||
|
||||
import Tip from '@/components/ui/Tip.vue'
|
||||
|
||||
const pageInputRefs = new Map<string, HTMLInputElement>()
|
||||
const activeRenameId = ref<string | null>(null)
|
||||
const rename = useInlineRename((id, name) => pageActions.value?.renamePage(id, name))
|
||||
const { panels } = useI18n()
|
||||
|
||||
const pageActions = ref<{
|
||||
renamePage: (pageId: string, name: string) => void
|
||||
|
|
@ -46,9 +47,9 @@ function handlePageDblClick(
|
|||
<div data-test-id="pages-panel" class="flex min-h-0 flex-1 flex-col">
|
||||
<div class="flex shrink-0 items-center justify-between px-3 py-1.5">
|
||||
<span data-test-id="pages-header" class="text-[11px] tracking-wider text-muted uppercase"
|
||||
>Pages</span
|
||||
>{{ panels.pages }}</span
|
||||
>
|
||||
<Tip label="Add page">
|
||||
<Tip :label="panels.addPage">
|
||||
<button
|
||||
data-test-id="pages-add"
|
||||
class="cursor-pointer rounded border-none bg-transparent px-1 text-base leading-none text-muted hover:bg-hover hover:text-surface"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'reka-ui'
|
||||
|
||||
import { useI18n } from '@open-pencil/vue'
|
||||
import { useAIChat } from '@/composables/use-chat'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
|
||||
|
|
@ -10,6 +11,7 @@ import DesignPanel from './DesignPanel.vue'
|
|||
|
||||
const store = useEditorStore()
|
||||
const { activeTab } = useAIChat()
|
||||
const { panels } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -25,7 +27,7 @@ const { activeTab } = useAIChat()
|
|||
data-test-id="properties-tab-design"
|
||||
class="rounded px-2.5 py-1 text-xs text-muted hover:text-surface data-[state=active]:font-semibold data-[state=active]:text-surface"
|
||||
>
|
||||
Design
|
||||
{{ panels.design }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="code"
|
||||
|
|
@ -33,7 +35,7 @@ const { activeTab } = useAIChat()
|
|||
class="flex items-center gap-1 rounded px-2.5 py-1 text-xs text-muted hover:text-surface data-[state=active]:font-semibold data-[state=active]:text-surface"
|
||||
>
|
||||
<icon-lucide-code class="size-3" />
|
||||
Code
|
||||
{{ panels.code }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="ai"
|
||||
|
|
@ -41,7 +43,7 @@ const { activeTab } = useAIChat()
|
|||
class="flex items-center gap-1 rounded px-2.5 py-1 text-xs text-muted hover:text-surface data-[state=active]:font-semibold data-[state=active]:text-surface"
|
||||
>
|
||||
<icon-lucide-sparkles class="size-3" />
|
||||
AI
|
||||
{{ panels.ai }}
|
||||
</TabsTrigger>
|
||||
<span
|
||||
v-if="activeTab === 'design'"
|
||||
|
|
|
|||
|
|
@ -31,9 +31,12 @@ import {
|
|||
ToolbarRoot,
|
||||
ToolbarItem,
|
||||
useEditorCommands,
|
||||
useI18n,
|
||||
useToolbarState,
|
||||
useViewportKind
|
||||
} from '@open-pencil/vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { Component } from 'vue'
|
||||
import type { Tool } from '@open-pencil/vue'
|
||||
|
||||
|
|
@ -41,20 +44,21 @@ const store = useEditorStore()
|
|||
const { isMobile } = useViewportKind()
|
||||
const { getCommand } = useEditorCommands()
|
||||
const { showActionToast } = useActionToast()
|
||||
const { menu: t, tools: toolTexts } = useI18n()
|
||||
|
||||
const toolLabels: Record<Tool, string> = {
|
||||
SELECT: 'Move',
|
||||
FRAME: 'Frame',
|
||||
SECTION: 'Section',
|
||||
RECTANGLE: 'Rectangle',
|
||||
ELLIPSE: 'Ellipse',
|
||||
LINE: 'Line',
|
||||
POLYGON: 'Polygon',
|
||||
STAR: 'Star',
|
||||
PEN: 'Pen',
|
||||
TEXT: 'Text',
|
||||
HAND: 'Hand'
|
||||
}
|
||||
const toolLabels = computed<Record<Tool, string>>(() => ({
|
||||
SELECT: toolTexts.value.move,
|
||||
FRAME: toolTexts.value.frame,
|
||||
SECTION: toolTexts.value.section,
|
||||
RECTANGLE: toolTexts.value.rectangle,
|
||||
ELLIPSE: toolTexts.value.ellipse,
|
||||
LINE: toolTexts.value.line,
|
||||
POLYGON: toolTexts.value.polygon,
|
||||
STAR: toolTexts.value.star,
|
||||
PEN: toolTexts.value.pen,
|
||||
TEXT: toolTexts.value.text,
|
||||
HAND: toolTexts.value.hand
|
||||
}))
|
||||
|
||||
const toolShortcuts: Record<Tool, string> = {
|
||||
SELECT: 'V',
|
||||
|
|
@ -78,29 +82,49 @@ interface ActionItem {
|
|||
|
||||
const flyoutMenuCls = useMenuUI({ content: 'min-w-32' })
|
||||
|
||||
const editActions: ActionItem[] = [
|
||||
{ icon: IconCopy, label: 'Copy', action: () => store.mobileCopy() },
|
||||
{ icon: IconClipboard, label: 'Paste', action: () => store.mobilePaste() },
|
||||
{ icon: IconScissors, label: 'Cut', action: () => store.mobileCut() },
|
||||
{ icon: IconCopyPlus, label: 'Duplicate', action: () => getCommand('selection.duplicate').run() },
|
||||
{ icon: IconTrash2, label: 'Delete', action: () => getCommand('selection.delete').run() }
|
||||
]
|
||||
const editActions = computed<ActionItem[]>(() => [
|
||||
{ icon: IconCopy, label: t.value.copy, action: () => store.mobileCopy() },
|
||||
{ icon: IconClipboard, label: t.value.paste, action: () => store.mobilePaste() },
|
||||
{ icon: IconScissors, label: t.value.cut, action: () => store.mobileCut() },
|
||||
{
|
||||
icon: IconCopyPlus,
|
||||
label: getCommand('selection.duplicate').label,
|
||||
action: () => getCommand('selection.duplicate').run()
|
||||
},
|
||||
{
|
||||
icon: IconTrash2,
|
||||
label: getCommand('selection.delete').label,
|
||||
action: () => getCommand('selection.delete').run()
|
||||
}
|
||||
])
|
||||
|
||||
const arrangeActions: ActionItem[] = [
|
||||
const arrangeActions = computed<ActionItem[]>(() => [
|
||||
{
|
||||
icon: IconArrowUpToLine,
|
||||
label: 'Front',
|
||||
label: t.value.front,
|
||||
action: () => getCommand('selection.bringToFront').run()
|
||||
},
|
||||
{
|
||||
icon: IconArrowDownToLine,
|
||||
label: 'Back',
|
||||
label: t.value.back,
|
||||
action: () => getCommand('selection.sendToBack').run()
|
||||
},
|
||||
{ icon: IconGroup, label: 'Group', action: () => getCommand('selection.group').run() },
|
||||
{ icon: IconUngroup, label: 'Ungroup', action: () => getCommand('selection.ungroup').run() },
|
||||
{ icon: IconLock, label: 'Lock', action: () => getCommand('selection.toggleLock').run() }
|
||||
]
|
||||
{
|
||||
icon: IconGroup,
|
||||
label: getCommand('selection.group').label,
|
||||
action: () => getCommand('selection.group').run()
|
||||
},
|
||||
{
|
||||
icon: IconUngroup,
|
||||
label: getCommand('selection.ungroup').label,
|
||||
action: () => getCommand('selection.ungroup').run()
|
||||
},
|
||||
{
|
||||
icon: IconLock,
|
||||
label: t.value.lock,
|
||||
action: () => getCommand('selection.toggleLock').run()
|
||||
}
|
||||
])
|
||||
|
||||
const {
|
||||
mobileCategory,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from 'reka-ui'
|
||||
import { FlexRender } from '@tanstack/vue-table'
|
||||
|
||||
import { useVariablesEditor } from '@open-pencil/vue'
|
||||
import { useI18n, useVariablesEditor } from '@open-pencil/vue'
|
||||
|
||||
import IconHash from '~icons/lucide/hash'
|
||||
import IconPalette from '~icons/lucide/palette'
|
||||
|
|
@ -35,6 +35,8 @@ const variableTypeIcons: Record<string, Component> = {
|
|||
BOOLEAN: IconToggleLeft
|
||||
}
|
||||
|
||||
const { dialogs } = useI18n()
|
||||
|
||||
const ctx = useVariablesEditor({
|
||||
colorInput: ColorInput,
|
||||
icons: variableTypeIcons,
|
||||
|
|
@ -50,7 +52,7 @@ const ctx = useVariablesEditor({
|
|||
<DialogContent data-test-id="variables-dialog" :class="cls.content">
|
||||
<div v-if="!ctx.hasCollections" class="flex flex-1 flex-col">
|
||||
<div class="flex shrink-0 items-center justify-between border-b border-border px-4 py-3">
|
||||
<DialogTitle class="text-sm font-semibold text-surface">Local variables</DialogTitle>
|
||||
<DialogTitle class="text-sm font-semibold text-surface">{{ dialogs.localVariables }}</DialogTitle>
|
||||
<DialogClose
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded border-none bg-transparent text-muted hover:bg-hover hover:text-surface"
|
||||
>
|
||||
|
|
@ -59,13 +61,13 @@ const ctx = useVariablesEditor({
|
|||
</div>
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-muted">No variable collections</p>
|
||||
<p class="text-sm text-muted">{{ dialogs.noVariableCollections }}</p>
|
||||
<button
|
||||
data-test-id="variables-create-collection"
|
||||
class="mt-2 cursor-pointer rounded bg-hover px-3 py-1.5 text-xs text-surface hover:bg-border"
|
||||
@click="ctx.addCollection"
|
||||
>
|
||||
Create collection
|
||||
{{ dialogs.createCollection }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -107,10 +109,10 @@ const ctx = useVariablesEditor({
|
|||
v-model="ctx.searchTerm.value"
|
||||
data-test-id="variables-search-input"
|
||||
class="w-24 border-none bg-transparent text-xs text-surface outline-none placeholder:text-muted"
|
||||
placeholder="Search"
|
||||
:placeholder="dialogs.search"
|
||||
/>
|
||||
</div>
|
||||
<Tip label="Add collection">
|
||||
<Tip :label="dialogs.createCollection">
|
||||
<button
|
||||
data-test-id="variables-add-collection"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded border-none bg-transparent text-muted hover:bg-hover hover:text-surface"
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ import Tip from '@/components/ui/Tip.vue'
|
|||
import { uiButton } from '@/components/ui/button'
|
||||
import { uiInput } from '@/components/ui/input'
|
||||
import { useAIChat } from '@/composables/use-chat'
|
||||
import { useI18n } from '@open-pencil/vue'
|
||||
|
||||
import { ACP_AGENTS } from '@open-pencil/core'
|
||||
|
||||
const { providerID, providerDef, modelID, customModelID } = useAIChat()
|
||||
const { dialogs } = useI18n()
|
||||
|
||||
const { status } = defineProps<{
|
||||
status: 'ready' | 'submitted' | 'streaming' | 'error'
|
||||
|
|
@ -83,14 +85,14 @@ function handleSubmit(e: Event) {
|
|||
v-model="input"
|
||||
type="text"
|
||||
data-test-id="chat-input"
|
||||
placeholder="Describe a change…"
|
||||
:placeholder="dialogs.describeChange"
|
||||
:class="uiInput({ ui: { base: 'min-w-0 flex-1 placeholder:text-muted' } })"
|
||||
:disabled="isStreaming"
|
||||
@paste.stop
|
||||
@copy.stop
|
||||
@cut.stop
|
||||
/>
|
||||
<Tip v-if="isStreaming" label="Stop generating">
|
||||
<Tip v-if="isStreaming" :label="dialogs.stopGenerating">
|
||||
<button
|
||||
type="button"
|
||||
data-test-id="chat-stop-button"
|
||||
|
|
@ -107,7 +109,7 @@ function handleSubmit(e: Event) {
|
|||
<icon-lucide-square class="size-3" />
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip v-else label="Send message">
|
||||
<Tip v-else :label="dialogs.sendMessage">
|
||||
<button
|
||||
type="submit"
|
||||
data-test-id="chat-send-button"
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import ProviderSelectField from '@/components/chat/ProviderSelectField.vue'
|
|||
import { uiInput } from '@/components/ui/input'
|
||||
import { useAIChat } from '@/composables/use-chat'
|
||||
import { ACP_AGENTS } from '@open-pencil/core'
|
||||
import { useI18n } from '@open-pencil/vue'
|
||||
|
||||
const { providerID, providerDef, setAPIKey, customBaseURL, customModelID } = useAIChat()
|
||||
const { dialogs } = useI18n()
|
||||
|
||||
const isACP = computed(() => providerID.value.startsWith('acp:'))
|
||||
const acpAgent = computed(() => {
|
||||
|
|
@ -36,7 +38,7 @@ function save() {
|
|||
<template>
|
||||
<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>
|
||||
<p class="mb-5 text-center text-xs text-muted">{{ dialogs.connectAIProvider }}</p>
|
||||
|
||||
<form v-if="!isACP" class="flex w-full flex-col gap-2" @submit.prevent="save">
|
||||
<ProviderSelectField test-id="provider-selector" />
|
||||
|
|
@ -47,7 +49,7 @@ function save() {
|
|||
v-model="baseURLInput"
|
||||
type="text"
|
||||
data-test-id="provider-base-url"
|
||||
placeholder="Base URL (e.g. http://localhost:11434/v1)"
|
||||
:placeholder="dialogs.baseURLPlaceholder"
|
||||
:class="uiInput()"
|
||||
/>
|
||||
|
||||
|
|
@ -57,7 +59,7 @@ function save() {
|
|||
v-model="customModelInput"
|
||||
type="text"
|
||||
data-test-id="provider-custom-model"
|
||||
placeholder="Model ID (e.g. llama-3.3-70b)"
|
||||
:placeholder="dialogs.modelIDPlaceholder"
|
||||
:class="uiInput()"
|
||||
/>
|
||||
|
||||
|
|
@ -75,7 +77,7 @@ function save() {
|
|||
class="mt-1 w-full rounded bg-accent py-1.5 text-xs font-medium text-white hover:bg-accent/90"
|
||||
:disabled="!keyInput.trim()"
|
||||
>
|
||||
Connect
|
||||
{{ dialogs.connect }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
|
@ -99,14 +101,14 @@ function save() {
|
|||
data-test-id="api-key-get-link"
|
||||
class="mt-2.5 text-[10px] text-muted underline hover:text-surface"
|
||||
>
|
||||
Get an {{ providerDef.name }} API key →
|
||||
{{ dialogs.getAPIKey({ provider: providerDef.name }) }}
|
||||
</a>
|
||||
|
||||
<p
|
||||
v-if="providerID === 'openrouter'"
|
||||
class="mt-3 text-center text-[10px] leading-relaxed text-muted/50"
|
||||
>
|
||||
One key for 100+ models from all providers.
|
||||
{{ dialogs.oneKeyManyModels }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { AppearanceControlsRoot } from '@open-pencil/vue'
|
||||
import { AppearanceControlsRoot, useI18n } from '@open-pencil/vue'
|
||||
|
||||
import ScrubInput from '@/components/ScrubInput.vue'
|
||||
import Tip from '@/components/ui/Tip.vue'
|
||||
import { sectionWrapper } from '@/components/ui/section'
|
||||
|
||||
const { panels } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -27,8 +29,8 @@ import { sectionWrapper } from '@/components/ui/section'
|
|||
>
|
||||
<div v-if="active" data-test-id="appearance-section" :class="sectionWrapper()">
|
||||
<div class="mb-1.5 flex items-center justify-between">
|
||||
<label class="text-[11px] text-muted">Appearance</label>
|
||||
<Tip label="Toggle visibility">
|
||||
<label class="text-[11px] text-muted">{{ panels.appearance }}</label>
|
||||
<Tip :label="panels.toggleVisibility">
|
||||
<button
|
||||
data-test-id="appearance-visibility"
|
||||
class="flex cursor-pointer items-center justify-center rounded border-none bg-transparent p-0.5 text-muted hover:bg-hover hover:text-surface"
|
||||
|
|
@ -67,7 +69,7 @@ import { sectionWrapper } from '@/components/ui/section'
|
|||
<icon-lucide-radius class="size-3" />
|
||||
</template>
|
||||
</ScrubInput>
|
||||
<Tip label="Independent corner radii">
|
||||
<Tip :label="panels.independentCornerRadii">
|
||||
<button
|
||||
data-test-id="independent-corners-toggle"
|
||||
class="flex size-[26px] shrink-0 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
|
|
|
|||
|
|
@ -4,24 +4,25 @@ import ColorInput from '@/components/ColorInput.vue'
|
|||
import ScrubInput from '@/components/ScrubInput.vue'
|
||||
import { iconButton } from '@/components/ui/icon-button'
|
||||
import { sectionLabel, sectionWrapper } from '@/components/ui/section'
|
||||
import { PropertyListRoot, useEffectsControls } from '@open-pencil/vue'
|
||||
import { PropertyListRoot, useEffectsControls, useI18n } from '@open-pencil/vue'
|
||||
|
||||
import { colorToCSS } from '@open-pencil/core'
|
||||
|
||||
import type { Effect } from '@open-pencil/core'
|
||||
|
||||
const effectsCtx = useEffectsControls()
|
||||
const { panels } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PropertyListRoot
|
||||
v-slot="{ items, isMixed, activeNode, patch, add, remove, toggleVisibility }"
|
||||
prop-key="effects"
|
||||
label="Effects"
|
||||
:label="panels.effects"
|
||||
>
|
||||
<div data-test-id="effects-section" :class="sectionWrapper()">
|
||||
<div class="flex items-center justify-between">
|
||||
<label :class="sectionLabel()">Effects</label>
|
||||
<label :class="sectionLabel()">{{ panels.effects }}</label>
|
||||
<button
|
||||
data-test-id="effects-section-add"
|
||||
:class="iconButton()"
|
||||
|
|
@ -31,7 +32,7 @@ const effectsCtx = useEffectsControls()
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="isMixed" class="text-[11px] text-muted">Click + to replace mixed effects</p>
|
||||
<p v-if="isMixed" class="text-[11px] text-muted">{{ panels.mixedEffectsHelp }}</p>
|
||||
|
||||
<div
|
||||
v-for="(effect, i) in items as Effect[]"
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@ import AppSelect from '@/components/ui/AppSelect.vue'
|
|||
import { iconButton } from '@/components/ui/icon-button'
|
||||
import { sectionLabel, sectionWrapper } from '@/components/ui/section'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { useExport } from '@open-pencil/vue'
|
||||
import { useExport, useI18n } from '@open-pencil/vue'
|
||||
|
||||
import type { ExportFormat } from '@open-pencil/core'
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
const { panels } = useI18n()
|
||||
const { settings, nodeName, addSetting, removeSetting, updateScale, updateFormat } = useExport()
|
||||
|
||||
const SCALE_OPTIONS = [0.5, 0.75, 1, 1.5, 2, 3, 4].map((s) => ({ value: s, label: `${s}x` }))
|
||||
|
|
@ -72,7 +73,7 @@ onScopeDispose(() => {
|
|||
<template>
|
||||
<div data-test-id="export-section" :class="sectionWrapper()">
|
||||
<div class="flex items-center justify-between">
|
||||
<label :class="sectionLabel()">Export</label>
|
||||
<label :class="sectionLabel()">{{ panels.export }}</label>
|
||||
<button data-test-id="export-section-add" :class="iconButton()" @click="addSetting">+</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from 'reka-ui'
|
||||
|
||||
import { colorToCSS, colorToHexRaw } from '@open-pencil/core'
|
||||
import { PropertyListRoot, useFillControls } from '@open-pencil/vue'
|
||||
import { PropertyListRoot, useFillControls, useI18n } from '@open-pencil/vue'
|
||||
|
||||
import FillPicker from '@/components/FillPicker.vue'
|
||||
import ScrubInput from '@/components/ScrubInput.vue'
|
||||
|
|
@ -23,17 +23,18 @@ import { sectionLabel, sectionWrapper } from '@/components/ui/section'
|
|||
import type { Fill, Variable } from '@open-pencil/core'
|
||||
|
||||
const fillCtx = useFillControls()
|
||||
const { panels, dialogs } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PropertyListRoot
|
||||
v-slot="{ items, isMixed, activeNode, add, remove, update, patch, toggleVisibility }"
|
||||
prop-key="fills"
|
||||
label="Fill"
|
||||
:label="panels.fill"
|
||||
>
|
||||
<div data-test-id="fill-section" :class="sectionWrapper()">
|
||||
<div class="flex items-center justify-between">
|
||||
<label :class="sectionLabel()">Fill</label>
|
||||
<label :class="sectionLabel()">{{ panels.fill }}</label>
|
||||
<button
|
||||
data-test-id="fill-section-add"
|
||||
:class="iconButton()"
|
||||
|
|
@ -42,7 +43,7 @@ const fillCtx = useFillControls()
|
|||
+
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="isMixed" class="text-[11px] text-muted">Click + to replace mixed fills</p>
|
||||
<p v-if="isMixed" class="text-[11px] text-muted">{{ panels.mixedFillsHelp }}</p>
|
||||
<div
|
||||
v-for="(fill, i) in items as Fill[]"
|
||||
:key="i"
|
||||
|
|
@ -58,7 +59,7 @@ const fillCtx = useFillControls()
|
|||
>
|
||||
{{ fillCtx.getBoundVariable(activeNode.id, i)!.name }}
|
||||
</span>
|
||||
<Tip label="Detach variable">
|
||||
<Tip :label="panels.detachVariable">
|
||||
<button
|
||||
data-test-id="fill-unbind-variable"
|
||||
class="cursor-pointer border-none bg-transparent p-0 text-violet-400 hover:text-surface"
|
||||
|
|
@ -95,7 +96,7 @@ const fillCtx = useFillControls()
|
|||
!fillCtx.getBoundVariable(activeNode.id, i)
|
||||
"
|
||||
>
|
||||
<Tip label="Apply variable">
|
||||
<Tip :label="panels.applyVariable">
|
||||
<PopoverTrigger
|
||||
class="cursor-pointer border-none bg-transparent p-0 text-muted hover:text-surface"
|
||||
>
|
||||
|
|
@ -115,13 +116,13 @@ const fillCtx = useFillControls()
|
|||
>
|
||||
<ComboboxInput
|
||||
:model-value="fillCtx.searchTerm.value"
|
||||
placeholder="Search variables…"
|
||||
:placeholder="dialogs.search"
|
||||
class="w-full border-b border-border bg-transparent px-2 py-1.5 text-[11px] text-surface outline-none placeholder:text-muted"
|
||||
@update:model-value="fillCtx.searchTerm.value = String($event)"
|
||||
/>
|
||||
<ComboboxContent class="max-h-48 overflow-y-auto p-1">
|
||||
<ComboboxEmpty class="px-2 py-3 text-center text-[11px] text-muted"
|
||||
>No variables found</ComboboxEmpty
|
||||
>{{ panels.noVariablesFound }}</ComboboxEmpty
|
||||
>
|
||||
<ComboboxItem
|
||||
v-for="v in fillCtx.filteredVariables.value"
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import { LayoutControlsRoot } from '@open-pencil/vue'
|
||||
import { LayoutControlsRoot, useI18n } from '@open-pencil/vue'
|
||||
|
||||
import AppSelect from '@/components/ui/AppSelect.vue'
|
||||
import ScrubInput from '@/components/ScrubInput.vue'
|
||||
import Tip from '@/components/ui/Tip.vue'
|
||||
import { sectionWrapper } from '@/components/ui/section'
|
||||
|
||||
const { panels } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutControlsRoot v-slot="ctx">
|
||||
<template v-if="ctx.node">
|
||||
<div data-test-id="layout-section" :class="sectionWrapper()">
|
||||
<label class="mb-1.5 block text-[11px] text-muted">Layout</label>
|
||||
<label class="mb-1.5 block text-[11px] text-muted">{{ panels.layout }}</label>
|
||||
<div class="flex gap-1.5">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1">
|
||||
<ScrubInput
|
||||
|
|
@ -49,8 +51,8 @@ import { sectionWrapper } from '@/components/ui/section'
|
|||
<template v-if="ctx.node.type === 'FRAME'">
|
||||
<div :class="sectionWrapper()">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="mb-1.5 block text-[11px] text-muted">Auto layout</label>
|
||||
<Tip v-if="ctx.node.layoutMode === 'NONE'" label="Add auto layout (Shift+A)">
|
||||
<label class="mb-1.5 block text-[11px] text-muted">{{ panels.autoLayout }}</label>
|
||||
<Tip v-if="ctx.node.layoutMode === 'NONE'" :label="panels.addAutoLayout">
|
||||
<button
|
||||
class="cursor-pointer rounded border-none bg-transparent px-1 text-base leading-none text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="layout-add-auto"
|
||||
|
|
@ -59,7 +61,7 @@ import { sectionWrapper } from '@/components/ui/section'
|
|||
+
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip v-else label="Remove auto layout">
|
||||
<Tip v-else :label="panels.removeAutoLayout">
|
||||
<button
|
||||
class="cursor-pointer rounded border-none bg-transparent px-1 text-base leading-none text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="layout-remove-auto"
|
||||
|
|
@ -238,7 +240,7 @@ import { sectionWrapper } from '@/components/ui/section'
|
|||
</template>
|
||||
|
||||
<div v-if="ctx.isFlex" class="mt-2">
|
||||
<label class="mb-1 block text-[11px] text-muted">Alignment</label>
|
||||
<label class="mb-1 block text-[11px] text-muted">{{ panels.alignment }}</label>
|
||||
<div data-test-id="layout-alignment-grid" class="grid w-fit grid-cols-3 gap-0.5">
|
||||
<button
|
||||
v-for="cell in ctx.alignGrid"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useI18n } from '@open-pencil/vue'
|
||||
import ColorInput from '@/components/ColorInput.vue'
|
||||
import { sectionWrapper } from '@/components/ui/section'
|
||||
|
||||
|
|
@ -8,11 +9,12 @@ import { useEditorStore } from '@/stores/editor'
|
|||
|
||||
const editor = useEditorStore()
|
||||
const pageColor = computed(() => editor.state.pageColor)
|
||||
const { panels } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-test-id="page-section" :class="sectionWrapper()">
|
||||
<label class="mb-1.5 block text-[11px] text-muted">Page</label>
|
||||
<label class="mb-1.5 block text-[11px] text-muted">{{ panels.page ?? 'Page' }}</label>
|
||||
<ColorInput :color="pageColor" editable @update="editor.setPageColor($event)" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import ScrubInput from '@/components/ScrubInput.vue'
|
|||
import Tip from '@/components/ui/Tip.vue'
|
||||
import { iconButton } from '@/components/ui/icon-button'
|
||||
import { sectionWrapper } from '@/components/ui/section'
|
||||
import { PositionControlsRoot } from '@open-pencil/vue'
|
||||
import { PositionControlsRoot, useI18n } from '@open-pencil/vue'
|
||||
|
||||
const { panels } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -24,11 +26,11 @@ import { PositionControlsRoot } from '@open-pencil/vue'
|
|||
}"
|
||||
>
|
||||
<div v-if="active" data-test-id="position-section" :class="sectionWrapper()">
|
||||
<label class="mb-1.5 block text-[11px] text-muted">Position</label>
|
||||
<label class="mb-1.5 block text-[11px] text-muted">{{ panels.position }}</label>
|
||||
|
||||
<div class="mb-1.5 flex gap-2">
|
||||
<div class="flex gap-0.5">
|
||||
<Tip label="Align left">
|
||||
<Tip :label="panels.alignLeft">
|
||||
<button
|
||||
:class="iconButton({ size: 'md' })"
|
||||
data-test-id="position-align-left"
|
||||
|
|
@ -37,7 +39,7 @@ import { PositionControlsRoot } from '@open-pencil/vue'
|
|||
<icon-lucide-align-horizontal-justify-start class="size-3.5" />
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip label="Align center horizontally">
|
||||
<Tip :label="panels.alignCenterHorizontally">
|
||||
<button
|
||||
:class="iconButton({ size: 'md' })"
|
||||
data-test-id="position-align-center-h"
|
||||
|
|
@ -46,7 +48,7 @@ import { PositionControlsRoot } from '@open-pencil/vue'
|
|||
<icon-lucide-align-horizontal-justify-center class="size-3.5" />
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip label="Align right">
|
||||
<Tip :label="panels.alignRight">
|
||||
<button
|
||||
:class="iconButton({ size: 'md' })"
|
||||
data-test-id="position-align-right"
|
||||
|
|
@ -57,7 +59,7 @@ import { PositionControlsRoot } from '@open-pencil/vue'
|
|||
</Tip>
|
||||
</div>
|
||||
<div class="flex gap-0.5">
|
||||
<Tip label="Align top">
|
||||
<Tip :label="panels.alignTop">
|
||||
<button
|
||||
:class="iconButton({ size: 'md' })"
|
||||
data-test-id="position-align-top"
|
||||
|
|
@ -66,7 +68,7 @@ import { PositionControlsRoot } from '@open-pencil/vue'
|
|||
<icon-lucide-align-vertical-justify-start class="size-3.5" />
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip label="Align center vertically">
|
||||
<Tip :label="panels.alignCenterVertically">
|
||||
<button
|
||||
:class="iconButton({ size: 'md' })"
|
||||
data-test-id="position-align-center-v"
|
||||
|
|
@ -75,7 +77,7 @@ import { PositionControlsRoot } from '@open-pencil/vue'
|
|||
<icon-lucide-align-vertical-justify-center class="size-3.5" />
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip label="Align bottom">
|
||||
<Tip :label="panels.alignBottom">
|
||||
<button
|
||||
:class="iconButton({ size: 'md' })"
|
||||
data-test-id="position-align-bottom"
|
||||
|
|
@ -133,7 +135,7 @@ import { PositionControlsRoot } from '@open-pencil/vue'
|
|||
<icon-lucide-rotate-ccw class="size-3" />
|
||||
</template>
|
||||
</ScrubInput>
|
||||
<Tip label="Flip horizontal">
|
||||
<Tip :label="panels.flipHorizontal">
|
||||
<button
|
||||
:class="iconButton({ size: 'md', ui: { base: 'shrink-0' } })"
|
||||
data-test-id="position-flip-horizontal"
|
||||
|
|
@ -142,7 +144,7 @@ import { PositionControlsRoot } from '@open-pencil/vue'
|
|||
<icon-lucide-flip-horizontal class="size-3.5" />
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip label="Flip vertical">
|
||||
<Tip :label="panels.flipVertical">
|
||||
<button
|
||||
:class="iconButton({ size: 'md', ui: { base: 'shrink-0' } })"
|
||||
data-test-id="position-flip-vertical"
|
||||
|
|
@ -151,7 +153,7 @@ import { PositionControlsRoot } from '@open-pencil/vue'
|
|||
<icon-lucide-flip-vertical class="size-3.5" />
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip label="Rotate 90°">
|
||||
<Tip :label="panels.rotate90">
|
||||
<button
|
||||
:class="iconButton({ size: 'md', ui: { base: 'shrink-0' } })"
|
||||
data-test-id="position-rotate-90"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
DropdownMenuTrigger
|
||||
} from 'reka-ui'
|
||||
|
||||
import { PropertyListRoot, useStrokeControls } from '@open-pencil/vue'
|
||||
import { PropertyListRoot, useStrokeControls, useI18n } from '@open-pencil/vue'
|
||||
|
||||
import AppSelect from '@/components/ui/AppSelect.vue'
|
||||
import ColorInput from '@/components/ColorInput.vue'
|
||||
|
|
@ -20,6 +20,7 @@ import { sectionLabel, sectionWrapper } from '@/components/ui/section'
|
|||
import type { SceneNode, Stroke } from '@open-pencil/core'
|
||||
|
||||
const strokeCtx = useStrokeControls()
|
||||
const { panels } = useI18n()
|
||||
const sideMenuCls = useMenuUI({
|
||||
content: 'min-w-[140px] rounded-md p-0.5',
|
||||
item: 'relative px-2'
|
||||
|
|
@ -30,11 +31,11 @@ const sideMenuCls = useMenuUI({
|
|||
<PropertyListRoot
|
||||
v-slot="{ items, isMixed, activeNode, add, remove, patch, toggleVisibility }"
|
||||
prop-key="strokes"
|
||||
label="Stroke"
|
||||
:label="panels.stroke"
|
||||
>
|
||||
<div data-test-id="stroke-section" :class="sectionWrapper()">
|
||||
<div class="flex items-center justify-between">
|
||||
<label :class="sectionLabel()">Stroke</label>
|
||||
<label :class="sectionLabel()">{{ panels.stroke }}</label>
|
||||
<button
|
||||
data-test-id="stroke-section-add"
|
||||
:class="iconButton()"
|
||||
|
|
@ -44,7 +45,7 @@ const sideMenuCls = useMenuUI({
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="isMixed" class="text-[11px] text-muted">Click + to replace mixed strokes</p>
|
||||
<p v-if="isMixed" class="text-[11px] text-muted">{{ panels.mixedStrokesHelp }}</p>
|
||||
|
||||
<div
|
||||
v-for="(stroke, i) in items as Stroke[]"
|
||||
|
|
@ -101,7 +102,7 @@ const sideMenuCls = useMenuUI({
|
|||
</template>
|
||||
</ScrubInput>
|
||||
<DropdownMenuRoot v-model:open="strokeCtx.sideMenuOpen.value">
|
||||
<Tip label="Stroke sides">
|
||||
<Tip :label="panels.strokeSides">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<button
|
||||
class="flex size-[26px] shrink-0 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ToggleGroupItem, ToggleGroupRoot } from 'reka-ui'
|
||||
|
||||
import { TypographyControlsRoot } from '@open-pencil/vue'
|
||||
import { TypographyControlsRoot, useI18n } from '@open-pencil/vue'
|
||||
|
||||
import FontPicker from '@/components/FontPicker.vue'
|
||||
import ScrubInput from '@/components/ScrubInput.vue'
|
||||
|
|
@ -9,12 +9,14 @@ import AppSelect from '@/components/ui/AppSelect.vue'
|
|||
import Tip from '@/components/ui/Tip.vue'
|
||||
import { sectionWrapper } from '@/components/ui/section'
|
||||
import { loadFont } from '@/engine/fonts'
|
||||
|
||||
const { panels } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TypographyControlsRoot v-slot="ctx" :load-font="loadFont">
|
||||
<div v-if="ctx.node.value" data-test-id="typography-section" :class="sectionWrapper()">
|
||||
<label class="mb-1.5 block text-[11px] text-muted">Typography</label>
|
||||
<label class="mb-1.5 block text-[11px] text-muted">{{ panels.typography }}</label>
|
||||
|
||||
<div class="mb-1.5 flex items-center gap-1.5">
|
||||
<FontPicker
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { computed } from 'vue'
|
|||
|
||||
import Tip from '@/components/ui/Tip.vue'
|
||||
import { sectionWrapper } from '@/components/ui/section'
|
||||
import { useSceneComputed } from '@open-pencil/vue'
|
||||
import { useI18n, useSceneComputed } from '@open-pencil/vue'
|
||||
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
|
||||
|
|
@ -19,13 +19,14 @@ const variableCount = useSceneComputed(() => {
|
|||
return editor.getVariableCount()
|
||||
})
|
||||
const hasVariables = computed(() => variableCount.value > 0)
|
||||
const { panels } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-test-id="variables-section" :class="sectionWrapper()">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-[11px] font-medium text-surface">Variables</label>
|
||||
<Tip label="Open variables">
|
||||
<label class="text-[11px] font-medium text-surface">{{ panels.variables }}</label>
|
||||
<Tip :label="panels.openVariables">
|
||||
<button
|
||||
data-test-id="variables-section-open"
|
||||
class="flex size-5 cursor-pointer items-center justify-center rounded border-none bg-transparent text-muted hover:bg-hover hover:text-surface"
|
||||
|
|
@ -36,9 +37,8 @@ const hasVariables = computed(() => variableCount.value > 0)
|
|||
</Tip>
|
||||
</div>
|
||||
<div v-if="hasVariables" class="mt-1 text-[11px] text-muted">
|
||||
{{ variableCount }} variable{{ variableCount !== 1 ? 's' : '' }} in
|
||||
{{ collectionCount }} collection{{ collectionCount !== 1 ? 's' : '' }}
|
||||
{{ variableCount }} / {{ collectionCount }}
|
||||
</div>
|
||||
<div v-else class="mt-1 text-[11px] text-muted">No local variables</div>
|
||||
<div v-else class="mt-1 text-[11px] text-muted">{{ panels.noLocalVariables }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,139 +2,154 @@ import { computed } from 'vue'
|
|||
|
||||
import { openFileDialog } from '@/composables/use-menu'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { useEditorCommands, useMenuModel } from '@open-pencil/vue'
|
||||
import { useEditorCommands, useI18n } from '@open-pencil/vue'
|
||||
|
||||
import type { MenuEntry } from '@open-pencil/vue'
|
||||
|
||||
export interface AppMenuGroup {
|
||||
label: string
|
||||
items: MenuEntry[]
|
||||
}
|
||||
|
||||
export function useAppMenu(mod: string) {
|
||||
const store = useEditorStore()
|
||||
const { menuItem: commandMenuItem } = useEditorCommands()
|
||||
const { appMenu } = useMenuModel()
|
||||
const { menu: t, panels: p, locale, availableLocales, localeLabels, setLocale } = useI18n()
|
||||
|
||||
const textMenu = [
|
||||
{ label: 'Bold', shortcut: `${mod}B` },
|
||||
{ label: 'Italic', shortcut: `${mod}I` },
|
||||
{ label: 'Underline', shortcut: `${mod}U` }
|
||||
]
|
||||
|
||||
const fileMenu = [
|
||||
{
|
||||
label: 'New',
|
||||
shortcut: `${mod}N`,
|
||||
action: () => import('@/stores/tabs').then((m) => m.createTab())
|
||||
},
|
||||
{ label: 'Open…', shortcut: `${mod}O`, action: () => void openFileDialog() },
|
||||
{ separator: true as const },
|
||||
{ label: 'Save', shortcut: `${mod}S`, action: () => void store.saveFigFile() },
|
||||
{ label: 'Save as…', shortcut: `${mod}⇧S`, action: () => void store.saveFigFileAs() },
|
||||
{ separator: true as const },
|
||||
{
|
||||
label: 'Export selection…',
|
||||
shortcut: `${mod}⇧E`,
|
||||
action: () => void store.exportSelection(1, 'PNG'),
|
||||
disabled: store.state.selectedIds.size === 0
|
||||
},
|
||||
{ separator: true as const },
|
||||
{
|
||||
label: 'Auto-save to local file',
|
||||
get checked() {
|
||||
return store.state.autosaveEnabled
|
||||
},
|
||||
onCheckedChange: (value: boolean) => {
|
||||
store.state.autosaveEnabled = value
|
||||
const languageMenu = computed<MenuEntry[]>(() =>
|
||||
availableLocales.map((code) => ({
|
||||
label: localeLabels[code],
|
||||
checked: locale.value === code,
|
||||
onCheckedChange: (checked: boolean) => {
|
||||
if (checked) setLocale(code)
|
||||
}
|
||||
}))
|
||||
)
|
||||
|
||||
const topMenus = computed<AppMenuGroup[]>(() => [
|
||||
{
|
||||
label: t.value.file,
|
||||
items: [
|
||||
{
|
||||
label: t.value.new,
|
||||
shortcut: `${mod}N`,
|
||||
action: () => {
|
||||
void import('@/stores/tabs').then((m) => m.createTab())
|
||||
}
|
||||
},
|
||||
{ label: t.value.open, shortcut: `${mod}O`, action: () => void openFileDialog() },
|
||||
{ separator: true as const },
|
||||
{ label: t.value.save, shortcut: `${mod}S`, action: () => void store.saveFigFile() },
|
||||
{
|
||||
label: t.value.saveAs,
|
||||
shortcut: `${mod}⇧S`,
|
||||
action: () => void store.saveFigFileAs()
|
||||
},
|
||||
{ separator: true as const },
|
||||
{
|
||||
label: t.value.exportSelection,
|
||||
shortcut: `${mod}⇧E`,
|
||||
action: () => {
|
||||
void store.exportSelection(1, 'PNG')
|
||||
},
|
||||
disabled: store.state.selectedIds.size === 0
|
||||
},
|
||||
{ separator: true as const },
|
||||
{
|
||||
label: t.value.autosave,
|
||||
get checked() {
|
||||
return store.state.autosaveEnabled
|
||||
},
|
||||
onCheckedChange: (value: boolean) => {
|
||||
store.state.autosaveEnabled = value
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: t.value.edit,
|
||||
items: [
|
||||
commandMenuItem('edit.undo', `${mod}Z`),
|
||||
commandMenuItem('edit.redo', `${mod}⇧Z`),
|
||||
{ separator: true as const },
|
||||
{ label: t.value.copy, shortcut: `${mod}C` },
|
||||
{ label: t.value.paste, shortcut: `${mod}V` },
|
||||
commandMenuItem('selection.duplicate', `${mod}D`),
|
||||
commandMenuItem('selection.delete', '⌫'),
|
||||
{ separator: true as const },
|
||||
commandMenuItem('selection.selectAll', `${mod}A`)
|
||||
]
|
||||
},
|
||||
{
|
||||
label: t.value.view,
|
||||
items: [
|
||||
commandMenuItem('view.zoom100', `${mod}0`),
|
||||
commandMenuItem('view.zoomFit', `${mod}1`),
|
||||
commandMenuItem('view.zoomSelection', `${mod}2`),
|
||||
{
|
||||
label: t.value.zoomIn,
|
||||
shortcut: `${mod}=`,
|
||||
action: () => store.applyZoom(-100, window.innerWidth / 2, window.innerHeight / 2)
|
||||
},
|
||||
{
|
||||
label: t.value.zoomOut,
|
||||
shortcut: `${mod}-`,
|
||||
action: () => store.applyZoom(100, window.innerWidth / 2, window.innerHeight / 2)
|
||||
},
|
||||
{ separator: true as const },
|
||||
{
|
||||
label: t.value.profiler,
|
||||
get checked() {
|
||||
return store.renderer?.profiler.hudVisible ?? false
|
||||
},
|
||||
onCheckedChange: () => {
|
||||
store.toggleProfiler()
|
||||
}
|
||||
},
|
||||
{ separator: true as const },
|
||||
{
|
||||
label: t.value.language,
|
||||
sub: languageMenu.value
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: t.value.object,
|
||||
items: [
|
||||
commandMenuItem('selection.group', `${mod}G`),
|
||||
commandMenuItem('selection.ungroup', `${mod}⇧G`),
|
||||
{ separator: true as const },
|
||||
commandMenuItem('selection.createComponent', `${mod}⌥K`),
|
||||
commandMenuItem('selection.createComponentSet'),
|
||||
commandMenuItem('selection.detachInstance'),
|
||||
{ separator: true as const },
|
||||
commandMenuItem('selection.bringToFront', ']'),
|
||||
commandMenuItem('selection.sendToBack', '[')
|
||||
]
|
||||
},
|
||||
{
|
||||
label: t.value.arrange,
|
||||
items: [
|
||||
commandMenuItem('selection.wrapInAutoLayout', '⇧A'),
|
||||
{ separator: true as const },
|
||||
{ label: p.value.alignLeft, shortcut: '⌥A' },
|
||||
{ label: p.value.alignCenter, shortcut: '⌥H' },
|
||||
{ label: p.value.alignRight, shortcut: '⌥D' },
|
||||
{ separator: true as const },
|
||||
{ label: p.value.alignTop, shortcut: '⌥W' },
|
||||
{ label: p.value.alignMiddle, shortcut: '⌥V' },
|
||||
{ label: p.value.alignBottom, shortcut: '⌥S' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: t.value.text,
|
||||
items: [
|
||||
{ label: t.value.bold, shortcut: `${mod}B` },
|
||||
{ label: t.value.italic, shortcut: `${mod}I` },
|
||||
{ label: t.value.underline, shortcut: `${mod}U` }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const topMenus = computed(() => [
|
||||
{ label: 'File', items: fileMenu },
|
||||
...appMenu.value.map((menu) => {
|
||||
if (menu.label === 'View') {
|
||||
return {
|
||||
label: 'View',
|
||||
items: [
|
||||
commandMenuItem('view.zoom100', `${mod}0`),
|
||||
commandMenuItem('view.zoomFit', `${mod}1`),
|
||||
commandMenuItem('view.zoomSelection', `${mod}2`),
|
||||
{
|
||||
label: 'Zoom in',
|
||||
shortcut: `${mod}=`,
|
||||
action: () => store.applyZoom(-100, window.innerWidth / 2, window.innerHeight / 2)
|
||||
},
|
||||
{
|
||||
label: 'Zoom out',
|
||||
shortcut: `${mod}-`,
|
||||
action: () => store.applyZoom(100, window.innerWidth / 2, window.innerHeight / 2)
|
||||
},
|
||||
{ separator: true as const },
|
||||
{
|
||||
label: 'Performance profiler',
|
||||
get checked() {
|
||||
return store.renderer?.profiler.hudVisible ?? false
|
||||
},
|
||||
onCheckedChange: () => {
|
||||
store.toggleProfiler()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if (menu.label === 'Edit') {
|
||||
return {
|
||||
label: 'Edit',
|
||||
items: [
|
||||
commandMenuItem('edit.undo', `${mod}Z`),
|
||||
commandMenuItem('edit.redo', `${mod}⇧Z`),
|
||||
{ separator: true as const },
|
||||
{ label: 'Copy', shortcut: `${mod}C` },
|
||||
{ label: 'Paste', shortcut: `${mod}V` },
|
||||
commandMenuItem('selection.duplicate', `${mod}D`),
|
||||
commandMenuItem('selection.delete', '⌫'),
|
||||
{ separator: true as const },
|
||||
commandMenuItem('selection.selectAll', `${mod}A`)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if (menu.label === 'Object') {
|
||||
return {
|
||||
label: 'Object',
|
||||
items: [
|
||||
commandMenuItem('selection.group', `${mod}G`),
|
||||
commandMenuItem('selection.ungroup', `${mod}⇧G`),
|
||||
{ separator: true as const },
|
||||
commandMenuItem('selection.createComponent', `${mod}⌥K`),
|
||||
commandMenuItem('selection.createComponentSet'),
|
||||
commandMenuItem('selection.detachInstance'),
|
||||
{ separator: true as const },
|
||||
commandMenuItem('selection.bringToFront', ']'),
|
||||
commandMenuItem('selection.sendToBack', '[')
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if (menu.label === 'Arrange') {
|
||||
return {
|
||||
label: 'Arrange',
|
||||
items: [
|
||||
commandMenuItem('selection.wrapInAutoLayout', '⇧A'),
|
||||
{ separator: true as const },
|
||||
{ label: 'Align left', shortcut: '⌥A' },
|
||||
{ label: 'Align center', shortcut: '⌥H' },
|
||||
{ label: 'Align right', shortcut: '⌥D' },
|
||||
{ separator: true as const },
|
||||
{ label: 'Align top', shortcut: '⌥W' },
|
||||
{ label: 'Align middle', shortcut: '⌥V' },
|
||||
{ label: 'Align bottom', shortcut: '⌥S' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return menu
|
||||
}),
|
||||
{ label: 'Text', items: textMenu }
|
||||
])
|
||||
|
||||
return {
|
||||
topMenus
|
||||
}
|
||||
return { topMenus }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
|
||||
import { openFileDialog } from './use-menu'
|
||||
|
||||
import type { Tool } from '@open-pencil/core/editor'
|
||||
import type { ComputedRef } from 'vue'
|
||||
|
||||
type NodeEditKeyboardMethods = Partial<{
|
||||
|
|
@ -102,20 +101,12 @@ export function useKeyboard() {
|
|||
if (html) store.pasteFromHTML(html, cursorPos)
|
||||
})
|
||||
|
||||
// Space hold-to-pan: temporarily switch to HAND while Space is held,
|
||||
// without losing object/node selections
|
||||
let toolBeforeSpace: Tool | null = null
|
||||
// Spacebar hold → temporary Hand tool (Figma-style canvas pan)
|
||||
let toolBeforeSpace: (typeof store.state.activeTool) | null = null
|
||||
|
||||
useEventListener(window, 'keydown', (e: KeyboardEvent) => {
|
||||
if (isEditing(e)) return
|
||||
if (
|
||||
e.code === 'Space' &&
|
||||
!e.metaKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.repeat &&
|
||||
toolBeforeSpace === null
|
||||
) {
|
||||
if (e.code === 'Space' && !e.metaKey && !e.ctrlKey && !e.altKey && !e.repeat && toolBeforeSpace === null) {
|
||||
if (store.state.activeTool !== 'HAND') {
|
||||
toolBeforeSpace = store.state.activeTool
|
||||
store.setTool('HAND')
|
||||
|
|
@ -131,6 +122,7 @@ export function useKeyboard() {
|
|||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
const keys = useMagicKeys({
|
||||
passive: false,
|
||||
onEventFired(e) {
|
||||
|
|
@ -141,7 +133,7 @@ export function useKeyboard() {
|
|||
if (!e.metaKey && !e.ctrlKey && !e.altKey) {
|
||||
// Space is handled by hold-to-pan above
|
||||
if (e.code === 'Space') return
|
||||
const tool = TOOL_SHORTCUTS[e.code]
|
||||
const tool = TOOL_SHORTCUTS[e.key.toLowerCase()]
|
||||
if (tool) {
|
||||
// Permanent tool switch cancels space-hold
|
||||
toolBeforeSpace = null
|
||||
|
|
@ -246,8 +238,8 @@ export function useKeyboard() {
|
|||
)
|
||||
}
|
||||
|
||||
whenever(plain('bracketright'), () => store.bringToFront())
|
||||
whenever(plain('bracketleft'), () => store.sendToBack())
|
||||
whenever(plain('bracketright'), () => runCommand('selection.bringToFront'))
|
||||
whenever(plain('bracketleft'), () => runCommand('selection.sendToBack'))
|
||||
whenever(plain('backspace'), () => {
|
||||
if (
|
||||
store.state.nodeEditState &&
|
||||
|
|
|
|||
Loading…
Reference in a new issue