Add i18n to Vue SDK with nanostores
nanostores + @nanostores/i18n + @nanostores/vue (~6 KB total). English base translations defined inline in messages.ts, other locales loaded on demand from JSON. Grouped by domain: menu, commands, tools, panels, pages, dialogs. useI18n() composable returns reactive translation objects plus locale controls. Russian locale included as first translation. Browser language auto-detected, user preference persisted in localStorage.
This commit is contained in:
parent
1f851e29cd
commit
2b3219e106
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=="],
|
||||
|
|
|
|||
|
|
@ -304,6 +304,13 @@ export function compressFigData(
|
|||
return compressViaWorker(schemaDeflated, kiwiData, thumbnailPng, metaJson, imageEntries)
|
||||
}
|
||||
return Promise.resolve(
|
||||
compressFigDataSync(schemaDeflated, kiwiData, thumbnailPng, metaJson, imageEntries, figKiwiVersion)
|
||||
compressFigDataSync(
|
||||
schemaDeflated,
|
||||
kiwiData,
|
||||
thumbnailPng,
|
||||
metaJson,
|
||||
imageEntries,
|
||||
figKiwiVersion
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -304,10 +304,10 @@ function resolveTextAutoResize(node: SceneNode, graph: SceneGraph): SceneNode['t
|
|||
if (node.textAutoResize === 'NONE') return 'NONE'
|
||||
const parent = node.parentId ? graph.getNode(node.parentId) : undefined
|
||||
if (
|
||||
parent
|
||||
&& parent.layoutMode !== 'NONE'
|
||||
&& parent.layoutMode !== 'GRID'
|
||||
&& node.layoutPositioning !== 'ABSOLUTE'
|
||||
parent &&
|
||||
parent.layoutMode !== 'NONE' &&
|
||||
parent.layoutMode !== 'GRID' &&
|
||||
node.layoutPositioning !== 'ABSOLUTE'
|
||||
) {
|
||||
return 'NONE'
|
||||
}
|
||||
|
|
@ -418,10 +418,7 @@ function serializeVariableBindings(
|
|||
if (entries.length > 0) nc.variableConsumptionMap = { entries }
|
||||
}
|
||||
|
||||
function computeExportTransform(
|
||||
node: SceneNode,
|
||||
graph: SceneGraph
|
||||
): Matrix {
|
||||
function computeExportTransform(node: SceneNode, graph: SceneGraph): Matrix {
|
||||
const sx = node.flipX ? -1 : 1
|
||||
const cos = Math.cos((node.rotation * Math.PI) / 180)
|
||||
const sin = Math.sin((node.rotation * Math.PI) / 180)
|
||||
|
|
@ -429,15 +426,18 @@ function computeExportTransform(
|
|||
// Auto-layout children should have (0,0) transform — Figma computes
|
||||
// their positions from the layout engine at render time.
|
||||
const parent = node.parentId ? graph.getNode(node.parentId) : undefined
|
||||
const isAutoLayoutChild = parent
|
||||
&& parent.layoutMode !== 'NONE'
|
||||
&& parent.layoutMode !== 'GRID'
|
||||
&& node.layoutPositioning !== 'ABSOLUTE'
|
||||
const isAutoLayoutChild =
|
||||
parent &&
|
||||
parent.layoutMode !== 'NONE' &&
|
||||
parent.layoutMode !== 'GRID' &&
|
||||
node.layoutPositioning !== 'ABSOLUTE'
|
||||
|
||||
return {
|
||||
m00: cos * sx, m01: -sin,
|
||||
m00: cos * sx,
|
||||
m01: -sin,
|
||||
m02: isAutoLayoutChild ? 0 : node.x,
|
||||
m10: sin * sx, m11: cos,
|
||||
m10: sin * sx,
|
||||
m11: cos,
|
||||
m12: isAutoLayoutChild ? 0 : node.y
|
||||
}
|
||||
}
|
||||
|
|
@ -477,7 +477,7 @@ export function sceneNodeToKiwi(
|
|||
size: { x: node.width, y: node.height },
|
||||
transform: computeExportTransform(node, graph),
|
||||
strokeWeight: node.strokes.length > 0 ? node.strokes[0].weight : 1,
|
||||
strokeAlign: node.strokes.length > 0 ? node.strokes[0].align : 'INSIDE',
|
||||
strokeAlign: node.strokes.length > 0 ? node.strokes[0].align : 'INSIDE'
|
||||
}
|
||||
|
||||
if (node.independentStrokeWeights) {
|
||||
|
|
|
|||
|
|
@ -371,9 +371,10 @@ function createDefaultNode(type: NodeType, overrides: Partial<SceneNode> = {}):
|
|||
width: 100,
|
||||
height: 100,
|
||||
rotation: 0,
|
||||
fills: type === 'TEXT'
|
||||
? [{ type: 'SOLID' as const, color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
: [],
|
||||
fills:
|
||||
type === 'TEXT'
|
||||
? [{ type: 'SOLID' as const, color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
: [],
|
||||
strokes: [],
|
||||
effects: [],
|
||||
opacity: 1,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
packages/vue/src/i18n/i18n.ts
Normal file
14
packages/vue/src/i18n/i18n.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { createI18n } from '@nanostores/i18n'
|
||||
|
||||
import { locale } from './locale'
|
||||
|
||||
import type { Locale } from './locale'
|
||||
import type { ComponentsJSON } from '@nanostores/i18n'
|
||||
|
||||
export const i18n = createI18n<Locale, 'en'>(locale, {
|
||||
baseLocale: 'en',
|
||||
async get(code) {
|
||||
const mod = (await import(`../locales/${code}.json`)) as { default: ComponentsJSON }
|
||||
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 } from './locale'
|
||||
export type { Locale } from './locale'
|
||||
export {
|
||||
menuMessages,
|
||||
commandMessages,
|
||||
toolMessages,
|
||||
panelMessages,
|
||||
pageMessages,
|
||||
dialogMessages
|
||||
} from './messages'
|
||||
23
packages/vue/src/i18n/locale.ts
Normal file
23
packages/vue/src/i18n/locale.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
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 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('open-pencil-locale', code)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const saved = localStorage.getItem('open-pencil-locale') as Locale | null
|
||||
if (saved && AVAILABLE_LOCALES.includes(saved)) {
|
||||
localeSetting.set(saved)
|
||||
}
|
||||
}
|
||||
130
packages/vue/src/i18n/messages.ts
Normal file
130
packages/vue/src/i18n/messages.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
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',
|
||||
|
||||
moveToPage: 'Move to page',
|
||||
createInstance: 'Create instance',
|
||||
|
||||
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',
|
||||
design: 'Design',
|
||||
code: 'Code',
|
||||
assets: 'Assets',
|
||||
|
||||
position: 'Position',
|
||||
layout: 'Layout',
|
||||
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',
|
||||
mixed: 'Mixed',
|
||||
|
||||
solid: 'Solid',
|
||||
linearGradient: 'Linear',
|
||||
radialGradient: 'Radial',
|
||||
image: 'Image',
|
||||
|
||||
alignLeft: 'Align left',
|
||||
alignCenter: 'Align center',
|
||||
alignRight: 'Align right',
|
||||
alignTop: 'Align top',
|
||||
alignMiddle: 'Align middle',
|
||||
alignBottom: 'Align bottom'
|
||||
})
|
||||
|
||||
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',
|
||||
search: 'Search…',
|
||||
noResults: 'No results'
|
||||
})
|
||||
46
packages/vue/src/i18n/useI18n.ts
Normal file
46
packages/vue/src/i18n/useI18n.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { useStore } from '@nanostores/vue'
|
||||
|
||||
import { locale, setLocale, AVAILABLE_LOCALES } 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,
|
||||
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 } from './i18n'
|
||||
export type { Locale } from './i18n'
|
||||
export {
|
||||
menuMessages,
|
||||
commandMessages,
|
||||
toolMessages,
|
||||
panelMessages,
|
||||
pageMessages,
|
||||
dialogMessages
|
||||
} from './i18n'
|
||||
|
|
|
|||
112
packages/vue/src/locales/ru.json
Normal file
112
packages/vue/src/locales/ru.json
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"menu": {
|
||||
"file": "Файл",
|
||||
"edit": "Правка",
|
||||
"view": "Вид",
|
||||
"object": "Объект",
|
||||
"arrange": "Порядок",
|
||||
"text": "Текст",
|
||||
"new": "Создать",
|
||||
"open": "Открыть…",
|
||||
"save": "Сохранить",
|
||||
"saveAs": "Сохранить как…",
|
||||
"exportSelection": "Экспорт выделения…",
|
||||
"autosave": "Автосохранение в файл",
|
||||
"copy": "Копировать",
|
||||
"paste": "Вставить",
|
||||
"zoomIn": "Увеличить",
|
||||
"zoomOut": "Уменьшить",
|
||||
"profiler": "Профилирование",
|
||||
"moveToPage": "Переместить на страницу",
|
||||
"createInstance": "Создать экземпляр",
|
||||
"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": "Слои",
|
||||
"design": "Дизайн",
|
||||
"code": "Код",
|
||||
"assets": "Ассеты",
|
||||
"position": "Позиция",
|
||||
"layout": "Раскладка",
|
||||
"appearance": "Внешний вид",
|
||||
"fill": "Заливка",
|
||||
"stroke": "Обводка",
|
||||
"effects": "Эффекты",
|
||||
"export": "Экспорт",
|
||||
"typography": "Типографика",
|
||||
"variables": "Переменные",
|
||||
"constraints": "Ограничения",
|
||||
"addFill": "Добавить заливку",
|
||||
"addStroke": "Добавить обводку",
|
||||
"addEffect": "Добавить эффект",
|
||||
"addExport": "Добавить экспорт",
|
||||
"dropShadow": "Тень",
|
||||
"innerShadow": "Внутренняя тень",
|
||||
"layerBlur": "Размытие слоя",
|
||||
"backgroundBlur": "Размытие фона",
|
||||
"noSelection": "Ничего не выделено",
|
||||
"mixed": "Смешанное",
|
||||
"solid": "Сплошной",
|
||||
"linearGradient": "Линейный",
|
||||
"radialGradient": "Радиальный",
|
||||
"image": "Изображение",
|
||||
"alignLeft": "По левому краю",
|
||||
"alignCenter": "По центру",
|
||||
"alignRight": "По правому краю",
|
||||
"alignTop": "По верхнему краю",
|
||||
"alignMiddle": "По середине",
|
||||
"alignBottom": "По нижнему краю"
|
||||
},
|
||||
"pages": {
|
||||
"newPage": "Новая страница",
|
||||
"rename": "Переименовать",
|
||||
"delete": "Удалить",
|
||||
"pageName": "Страница {number}"
|
||||
},
|
||||
"dialogs": {
|
||||
"cancel": "Отмена",
|
||||
"apply": "Применить",
|
||||
"close": "Закрыть",
|
||||
"ok": "ОК",
|
||||
"search": "Поиск…",
|
||||
"noResults": "Ничего не найдено"
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,8 @@
|
|||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue"],
|
||||
"exclude": ["node_modules", "dist", "example"]
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export function useKeyboard() {
|
|||
})
|
||||
|
||||
// Spacebar hold → temporary Hand tool (Figma-style canvas pan)
|
||||
let toolBeforeSpace: (typeof store.state.activeTool) | null = null
|
||||
let toolBeforeSpace: typeof store.state.activeTool | null = null
|
||||
|
||||
useEventListener(window, 'keydown', (e: KeyboardEvent) => {
|
||||
if (isEditing(e)) return
|
||||
|
|
|
|||
Loading…
Reference in a new issue