snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 05:09:45 +05:00
commit bcf6ef63ad
7118 changed files with 1236854 additions and 0 deletions

25
examples/index.html Normal file
View File

@@ -0,0 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<link href="/favicon.svg" rel="icon" />
<title>Univer Workbench</title>
</head>
<body>
<header class="workbench-header" lang="en">
<div class="workbench-primary">
<span aria-label="Univer" class="workbench-brand" role="img"></span>
<nav aria-label="Product workbenches">
<a data-example="sheets" href="#sheets">Sheets</a>
<a data-example="docs" href="#docs">Docs</a>
<a data-example="slides" href="#slides">Slides</a>
</nav>
</div>
<div class="workbench-settings" id="workbench-settings"></div>
</header>
<div aria-live="polite" class="workbench-status" hidden id="workbench-status" role="status"></div>
<main id="app"></main>
<script src="/src/main.ts" type="module"></script>
</body>
</html>

57
examples/package.json Normal file
View File

@@ -0,0 +1,57 @@
{
"name": "univer-examples",
"type": "module",
"private": true,
"description": "Low-memory Univer development workbench",
"author": "DreamNum <developer@univer.ai>",
"license": "Apache-2.0",
"scripts": {
"build": "vite build",
"dev": "vite --host 0.0.0.0",
"preview": "vite preview",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/docs": "workspace:*",
"@univerjs/docs-ui": "workspace:*",
"@univerjs/drawing": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "1.38.0",
"@univerjs/preset-docs-core": "workspace:*",
"@univerjs/preset-docs-drawing": "workspace:*",
"@univerjs/preset-docs-hyper-link": "workspace:*",
"@univerjs/preset-docs-thread-comment": "workspace:*",
"@univerjs/preset-sheets-conditional-formatting": "workspace:*",
"@univerjs/preset-sheets-core": "workspace:*",
"@univerjs/preset-sheets-data-validation": "workspace:*",
"@univerjs/preset-sheets-drawing": "workspace:*",
"@univerjs/preset-sheets-filter": "workspace:*",
"@univerjs/preset-sheets-find-replace": "workspace:*",
"@univerjs/preset-sheets-hyper-link": "workspace:*",
"@univerjs/preset-sheets-note": "workspace:*",
"@univerjs/preset-sheets-sort": "workspace:*",
"@univerjs/preset-sheets-table": "workspace:*",
"@univerjs/preset-sheets-thread-comment": "workspace:*",
"@univerjs/presets": "workspace:*",
"@univerjs/slides": "workspace:*",
"@univerjs/slides-ui": "workspace:*",
"@univerjs/themes": "workspace:*",
"@univerjs/ui": "workspace:*",
"react": "19.3.0",
"react-dom": "19.3.0",
"rxjs": "^7.8.2"
},
"devDependencies": {
"@univerjs-infra/shared": "workspace:*",
"postcss": "^8.5.28",
"tailwindcss": "3.4.18",
"tailwindcss-animate": "^1.0.7",
"tsx": "^4.23.13",
"typescript": "^6.0.3",
"vite": "^8.3.0",
"vitest": "^5.0.0"
}
}

View File

@@ -0,0 +1 @@
export { default } from '@univerjs-infra/shared/postcss';

View File

@@ -0,0 +1,13 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.720703 2.21973C0.720703 1.11516 1.61613 0.219727 2.7207 0.219727H14.7207C15.8253 0.219727 16.7207 1.11516 16.7207 2.21973V14.2197C16.7207 15.3243 15.8253 16.2197 14.7207 16.2197H2.7207C1.61613 16.2197 0.720703 15.3243 0.720703 14.2197V2.21973Z" fill="white"/>
<path d="M3.05945 11.7462C3.02958 11.7293 3.00188 11.7073 2.97813 11.68C2.34633 10.9647 2.24522 9.93316 2.72627 9.11354L6.11665 3.33661C6.20481 3.18657 6.39982 3.13512 6.55237 3.22184C6.70493 3.30855 6.7571 3.50032 6.6693 3.65072L3.27892 9.42764C2.93388 10.0156 3.00656 10.7553 3.4599 11.2684C3.5754 11.3993 3.56137 11.5976 3.42824 11.7113C3.32282 11.8016 3.17386 11.8113 3.05945 11.7462ZM2.37151 8.67062L5.84753 2.74762C5.96914 2.54038 6.12997 2.36444 6.32534 2.22483C6.42032 2.15683 6.44155 2.02622 6.37247 1.93268C6.30339 1.83913 6.17026 1.81862 6.07528 1.88626C5.83133 2.06041 5.63057 2.27988 5.47909 2.53822L2.00308 8.46122C1.94443 8.56125 1.97933 8.68933 2.08079 8.7469C2.18226 8.80447 2.3125 8.77029 2.37151 8.67026V8.67062ZM7.54218 11.2741L8.79823 9.13368C8.94503 8.88362 8.85796 8.5634 8.60358 8.41912C8.3492 8.27449 8.02395 8.36048 7.87715 8.61054L6.62109 10.751C6.38614 11.1511 5.86408 11.2889 5.4575 11.0579C5.05057 10.8269 4.91061 10.3131 5.14556 9.91301L7.91061 5.20181C8.05741 4.95175 7.97034 4.63153 7.71596 4.48725C7.46158 4.34261 7.13632 4.4286 6.98953 4.67866L4.22447 9.38986C3.69629 10.2897 4.01075 11.4443 4.92572 11.9639C5.84033 12.4834 7.01435 12.174 7.54218 11.2741ZM14.0132 4.72831C13.88 4.84201 13.866 5.04026 13.9815 5.17122C14.4348 5.6843 14.5075 6.42404 14.1625 7.01195L10.7721 12.7889C10.6839 12.9389 10.7361 13.131 10.889 13.2178C11.0416 13.3045 11.2366 13.253 11.3247 13.103L14.7151 7.32606C15.1962 6.50608 15.0951 5.47489 14.4633 4.75961C14.4395 4.73263 14.4118 4.71068 14.3819 4.69341C14.2672 4.62829 14.1186 4.63836 14.0132 4.72831ZM15.0699 7.76897L11.5939 13.692C11.4723 13.8992 11.3114 14.0752 11.1161 14.2148C11.0211 14.2828 10.9998 14.4134 11.0689 14.5069C11.138 14.6005 11.2711 14.621 11.3661 14.5533C11.6101 14.3792 11.8108 14.1597 11.9623 13.9014L15.4383 7.97837C15.497 7.87835 15.4621 7.75026 15.3606 7.69269C15.2588 7.63476 15.1289 7.6693 15.0699 7.76933V7.76897ZM9.89922 5.16547L8.64316 7.30591C8.49636 7.55597 8.58343 7.87619 8.83781 8.02047C9.09219 8.16511 9.41745 8.07912 9.56424 7.82906L10.8203 5.68861C11.0552 5.28852 11.5773 5.15072 11.9839 5.38171C12.3908 5.6127 12.5308 6.12649 12.2958 6.52658L9.53078 11.2378C9.38398 11.4878 9.47106 11.8081 9.72543 11.9523C9.97981 12.097 10.3051 12.011 10.4519 11.7609L13.2169 7.04973C13.7451 6.14988 13.4306 4.99528 12.5157 4.47573C11.6011 3.95618 10.427 4.26561 9.89922 5.16547Z" fill="url(#paint0_linear_167_3)"/>
<defs>
<linearGradient id="paint0_linear_167_3" x1="3.94409" y1="5.15948" x2="13.6248" y2="11.3617" gradientUnits="userSpaceOnUse">
<stop stop-color="#0B00D0"/>
<stop offset="0.49" stop-color="#0492ED"/>
<stop offset="0.65" stop-color="#029DCE"/>
<stop offset="0.88" stop-color="#00ABA6"/>
<stop offset="1" stop-color="#00B197"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,97 @@
import process from 'node:process';
interface ILocaleModule {
default?: unknown;
}
const LOCALE_SOURCES = {
docs: {
directory: 'locales',
packages: [
'@univerjs/preset-docs-core',
'@univerjs/preset-docs-drawing',
'@univerjs/preset-docs-hyper-link',
'@univerjs/preset-docs-thread-comment',
],
},
sheets: {
directory: 'locales',
packages: [
'@univerjs/preset-sheets-core',
'@univerjs/preset-sheets-drawing',
'@univerjs/preset-sheets-conditional-formatting',
'@univerjs/preset-sheets-data-validation',
'@univerjs/preset-sheets-filter',
'@univerjs/preset-sheets-find-replace',
'@univerjs/preset-sheets-hyper-link',
'@univerjs/preset-sheets-note',
'@univerjs/preset-sheets-sort',
'@univerjs/preset-sheets-table',
'@univerjs/preset-sheets-thread-comment',
],
},
slides: {
directory: 'locale',
packages: [
'@univerjs/design',
'@univerjs/docs-ui',
'@univerjs/slides-ui',
'@univerjs/ui',
],
},
} as const;
const LOCALE_TAGS = new Set([
'ar-SA',
'ca-ES',
'de-DE',
'en-US',
'es-ES',
'fa-IR',
'fr-FR',
'id-ID',
'it-IT',
'ja-JP',
'ko-KR',
'pl-PL',
'pt-BR',
'ru-RU',
'sk-SK',
'vi-VN',
'zh-CN',
'zh-HK',
'zh-TW',
]);
type LocaleProduct = keyof typeof LOCALE_SOURCES;
function isLocaleProduct(value: string | undefined): value is LocaleProduct {
return value !== undefined && Object.hasOwn(LOCALE_SOURCES, value);
}
function unwrapDefault(module: ILocaleModule): unknown {
let value = module.default;
while (
value !== null
&& typeof value === 'object'
&& !Array.isArray(value)
&& Object.keys(value).length === 1
&& Object.hasOwn(value, 'default')
) {
value = (value as { default: unknown }).default;
}
return value;
}
const [product, locale] = process.argv.slice(2);
if (!isLocaleProduct(product) || !locale || !LOCALE_TAGS.has(locale)) {
throw new Error('Unsupported product or locale.');
}
const source = LOCALE_SOURCES[product];
const packs = await Promise.all(source.packages.map(async (packageName) => {
const module = await import(`${packageName}/${source.directory}/${locale}`) as ILocaleModule;
return unwrapDefault(module);
}));
process.stdout.write(JSON.stringify({ packs }));

View File

@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest';
import { loadDevLocale } from '../dev-locale-loader';
describe('loadDevLocale', () => {
it('merges every locale pack returned by the development server', async () => {
const fetchLocale: typeof fetch = async () => new Response(JSON.stringify({
packs: [
{ sheets: { copy: 'Copy' } },
{ ui: { paste: 'Paste' } },
],
}));
await expect(loadDevLocale('sheets', 'enUS', fetchLocale)).resolves.toEqual({
sheets: {
copy: 'Copy',
},
ui: {
paste: 'Paste',
},
});
});
it('reports a failed locale endpoint without applying partial data', async () => {
const fetchLocale: typeof fetch = async () => new Response(
JSON.stringify({ error: 'Locale module failed to load.' }),
{ status: 500 }
);
await expect(loadDevLocale('docs', 'arSA', fetchLocale)).rejects.toThrow(
'Could not load docs locale ar-SA. Locale module failed to load.'
);
});
});

View File

@@ -0,0 +1,395 @@
import type { Univer } from '@univerjs/core';
import type { FUniver } from '@univerjs/core/facade';
import type { IMountedExample, MountExample } from '../mount-example';
import type { IWorkbenchMountOptions } from '../workbench-settings';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createExampleSwitcher, createMountedUniver, createUniverDisposer } from '../mount-example';
afterEach(() => {
vi.unstubAllGlobals();
});
describe('mounted Univer lifecycle', () => {
it('releases an owned Univer mount exactly once', () => {
const univer = { dispose: vi.fn() } as unknown as Univer;
const univerAPI = {} as FUniver;
const host = { replaceChildren: vi.fn() } as unknown as HTMLElement;
vi.stubGlobal('window', { univer, univerAPI });
const dispose = createUniverDisposer(host, univer, univerAPI);
dispose();
dispose();
const workbenchWindow = window as typeof window & { univer?: Univer; univerAPI?: FUniver };
expect(univer.dispose).toHaveBeenCalledOnce();
expect(host.replaceChildren).toHaveBeenCalledOnce();
expect(workbenchWindow.univer).toBeUndefined();
expect(workbenchWindow.univerAPI).toBeUndefined();
});
it('ignores a locale load failure after the mounted example is disposed', async () => {
const univer = { dispose: vi.fn() } as unknown as Univer;
const univerAPI = {
loadLocales: vi.fn(),
setLocale: vi.fn(),
setDirection: vi.fn(),
setTheme: vi.fn(),
toggleDarkMode: vi.fn(),
setRibbonType: vi.fn(),
} as unknown as FUniver;
const host = { replaceChildren: vi.fn() } as unknown as HTMLElement;
const initialSettings = {
locale: 'zhCN',
ribbonType: 'grid',
zoomRatio: 1,
} as IWorkbenchMountOptions;
const nextSettings = {
locale: 'enUS',
ribbonType: 'simple',
zoomRatio: 1.25,
} as IWorkbenchMountOptions;
let rejectLocale!: (error: Error) => void;
const locale = new Promise<never>((_resolve, reject) => {
rejectLocale = reject;
});
const applyProductSettings = vi.fn();
vi.stubGlobal('window', {});
const mounted = createMountedUniver(
host,
univer,
univerAPI,
initialSettings,
() => locale,
applyProductSettings
);
const update = mounted.updateSettings(nextSettings);
mounted.dispose();
rejectLocale(new Error('locale chunk failed'));
await expect(update).resolves.toBeUndefined();
expect(univerAPI.loadLocales).not.toHaveBeenCalled();
expect(univerAPI.setRibbonType).not.toHaveBeenCalled();
expect(applyProductSettings).not.toHaveBeenCalled();
});
it('ignores a product settings failure after the mounted example is disposed', async () => {
const univer = { dispose: vi.fn() } as unknown as Univer;
const univerAPI = {
setDirection: vi.fn(),
setTheme: vi.fn(),
toggleDarkMode: vi.fn(),
setRibbonType: vi.fn(),
} as unknown as FUniver;
const host = { replaceChildren: vi.fn() } as unknown as HTMLElement;
const initialSettings = {
locale: 'zhCN',
ribbonType: 'grid',
zoomRatio: 1,
} as IWorkbenchMountOptions;
const nextSettings = {
...initialSettings,
zoomRatio: 1.25,
} as IWorkbenchMountOptions;
let rejectProductSettings!: (error: Error) => void;
const productSettings = new Promise<never>((_resolve, reject) => {
rejectProductSettings = reject;
});
vi.stubGlobal('window', {});
const mounted = createMountedUniver(
host,
univer,
univerAPI,
initialSettings,
vi.fn(),
() => productSettings
);
const update = mounted.updateSettings(nextSettings);
mounted.dispose();
rejectProductSettings(new Error('render unit disposed'));
await expect(update).resolves.toBeUndefined();
expect(univerAPI.setRibbonType).not.toHaveBeenCalled();
});
it('applies ribbon and product view settings without recreating the Univer instance', async () => {
const univer = { dispose: vi.fn() } as unknown as Univer;
const univerAPI = {
Enum: {
BuiltInUIPart: {
FOOTER: 'footer',
HEADER: 'header',
LEFT_SIDEBAR: 'left-sidebar',
TOOLBAR: 'toolbar',
},
},
setRegion: vi.fn(),
setDirection: vi.fn(),
setTheme: vi.fn(),
toggleDarkMode: vi.fn(),
setRibbonType: vi.fn(),
setUIVisible: vi.fn(),
} as unknown as FUniver;
const host = { replaceChildren: vi.fn() } as unknown as HTMLElement;
const initialSettings = {
locale: 'zhCN',
region: 'auto',
ribbonType: 'grid',
uiChrome: 'full',
zoomRatio: 1,
} as IWorkbenchMountOptions;
const nextSettings = {
...initialSettings,
region: 'frFR',
ribbonType: 'simple',
uiChrome: 'canvas-only',
zoomRatio: 1.25,
} as IWorkbenchMountOptions;
const applyProductSettings = vi.fn();
vi.stubGlobal('window', {});
const mounted = createMountedUniver(
host,
univer,
univerAPI,
initialSettings,
vi.fn(),
applyProductSettings
);
await mounted.updateSettings(nextSettings);
await mounted.updateSettings({ ...nextSettings, darkMode: true });
expect(univerAPI.setRibbonType).toHaveBeenCalledOnce();
expect(univerAPI.setRibbonType).toHaveBeenCalledWith('simple');
expect(univerAPI.setRegion).toHaveBeenCalledOnce();
expect(univerAPI.setRegion).toHaveBeenCalledWith('frFR');
expect(univerAPI.setUIVisible).toHaveBeenCalledTimes(4);
expect(univerAPI.setUIVisible).toHaveBeenNthCalledWith(1, 'header', false);
expect(univerAPI.setUIVisible).toHaveBeenNthCalledWith(2, 'toolbar', false);
expect(univerAPI.setUIVisible).toHaveBeenNthCalledWith(3, 'footer', false);
expect(univerAPI.setUIVisible).toHaveBeenNthCalledWith(4, 'left-sidebar', false);
expect(applyProductSettings).toHaveBeenCalledOnce();
expect(applyProductSettings).toHaveBeenCalledWith(nextSettings);
expect(univer.dispose).not.toHaveBeenCalled();
mounted.dispose();
});
it('does not commit locale or ribbon changes when a combined view update fails', async () => {
const univer = { dispose: vi.fn() } as unknown as Univer;
const univerAPI = {
Enum: {
BuiltInUIPart: {
FOOTER: 'footer',
HEADER: 'header',
LEFT_SIDEBAR: 'left-sidebar',
TOOLBAR: 'toolbar',
},
},
loadLocales: vi.fn(),
setLocale: vi.fn(),
setRegion: vi.fn(),
setDirection: vi.fn(),
setTheme: vi.fn(),
toggleDarkMode: vi.fn(),
setRibbonType: vi.fn(),
setUIVisible: vi.fn(),
} as unknown as FUniver;
const host = { replaceChildren: vi.fn() } as unknown as HTMLElement;
const initialSettings = {
locale: 'zhCN',
region: 'auto',
ribbonType: 'grid',
uiChrome: 'full',
zoomRatio: 1,
} as IWorkbenchMountOptions;
const nextSettings = {
...initialSettings,
locale: 'enUS',
region: 'frFR',
ribbonType: 'simple',
uiChrome: 'canvas-only',
zoomRatio: 1.25,
} as IWorkbenchMountOptions;
const updateError = new Error('zoom failed');
vi.stubGlobal('window', {});
const mounted = createMountedUniver(
host,
univer,
univerAPI,
initialSettings,
() => Promise.resolve({} as Parameters<FUniver['loadLocales']>[1]),
() => Promise.reject(updateError)
);
await expect(mounted.updateSettings(nextSettings)).rejects.toBe(updateError);
expect(univerAPI.loadLocales).not.toHaveBeenCalled();
expect(univerAPI.setLocale).not.toHaveBeenCalled();
expect(univerAPI.setRegion).not.toHaveBeenCalled();
expect(univerAPI.setRibbonType).not.toHaveBeenCalled();
expect(univerAPI.setUIVisible).not.toHaveBeenCalled();
mounted.dispose();
});
});
describe('createExampleSwitcher', () => {
it('mounts only the latest requested example', async () => {
const host = {} as HTMLElement;
const docsDispose = vi.fn();
const docsMount = vi.fn(() => ({ dispose: docsDispose, updateSettings: vi.fn() }));
const sheetsMount = vi.fn(() => ({ dispose: vi.fn(), updateSettings: vi.fn() }));
const options = {} as IWorkbenchMountOptions;
let resolveSheets!: (module: { mount: MountExample }) => void;
const switcher = createExampleSwitcher(host, {
docs: () => Promise.resolve({ mount: docsMount }),
sheets: () => new Promise((resolve) => {
resolveSheets = resolve;
}),
});
const pendingSheets = switcher.open('sheets', options);
await switcher.open('docs', options);
resolveSheets({ mount: sheetsMount });
await pendingSheets;
expect(docsMount).toHaveBeenCalledOnce();
expect(docsMount).toHaveBeenCalledWith(host, options);
expect(sheetsMount).not.toHaveBeenCalled();
switcher.dispose();
switcher.dispose();
expect(docsDispose).toHaveBeenCalledOnce();
});
it('cleans up a stale async mount before mounting the next example', async () => {
const host = {} as HTMLElement;
const options = {} as IWorkbenchMountOptions;
const events: string[] = [];
let resolveSheetsMount!: () => void;
let reportSheetsMountStarted!: () => void;
let reportDocsLoaded!: () => void;
const sheetsMountStarted = new Promise<void>((resolve) => {
reportSheetsMountStarted = resolve;
});
const docsLoaded = new Promise<void>((resolve) => {
reportDocsLoaded = resolve;
});
const sheetsMount = vi.fn(() => new Promise<Awaited<ReturnType<MountExample>>>((resolve) => {
reportSheetsMountStarted();
resolveSheetsMount = () => resolve({
dispose: () => events.push('sheets-dispose'),
updateSettings: vi.fn(),
});
}));
const docsMount = vi.fn(() => {
events.push('docs-mount');
return { dispose: vi.fn(), updateSettings: vi.fn() };
});
const switcher = createExampleSwitcher(host, {
docs: () => {
reportDocsLoaded();
return Promise.resolve({ mount: docsMount });
},
sheets: () => Promise.resolve({ mount: sheetsMount }),
});
const pendingSheets = switcher.open('sheets', options);
await sheetsMountStarted;
const pendingDocs = switcher.open('docs', options);
await docsLoaded;
expect(docsMount).not.toHaveBeenCalled();
resolveSheetsMount();
await Promise.all([pendingSheets, pendingDocs]);
expect(events).toEqual(['sheets-dispose', 'docs-mount']);
switcher.dispose();
});
it('updates the active example without mounting or disposing it again', async () => {
const host = {} as HTMLElement;
const initialSettings = {} as IWorkbenchMountOptions;
const nextSettings = { darkMode: true } as IWorkbenchMountOptions;
const dispose = vi.fn();
const updateSettings = vi.fn();
const mount = vi.fn(() => ({ dispose, updateSettings }));
const switcher = createExampleSwitcher(host, {
sheets: () => Promise.resolve({ mount }),
});
await switcher.open('sheets', initialSettings);
await switcher.updateSettings(nextSettings);
expect(mount).toHaveBeenCalledOnce();
expect(dispose).not.toHaveBeenCalled();
expect(updateSettings).toHaveBeenCalledOnce();
expect(updateSettings).toHaveBeenCalledWith(nextSettings);
switcher.dispose();
expect(dispose).toHaveBeenCalledOnce();
});
it('serializes settings selected while an example is finishing its mount', async () => {
const host = {} as HTMLElement;
const initialSettings = { locale: 'zhCN' } as IWorkbenchMountOptions;
const pendingSettings = { locale: 'frFR' } as IWorkbenchMountOptions;
const latestSettings = { locale: 'enUS' } as IWorkbenchMountOptions;
const updates: IWorkbenchMountOptions[] = [];
let resolveMount!: (mounted: IMountedExample) => void;
let resolvePendingUpdate!: () => void;
let reportMountStarted!: () => void;
let reportPendingUpdateStarted!: () => void;
const mountStarted = new Promise<void>((resolve) => {
reportMountStarted = resolve;
});
const pendingUpdateStarted = new Promise<void>((resolve) => {
reportPendingUpdateStarted = resolve;
});
const updateSettings = vi.fn((options: IWorkbenchMountOptions) => {
updates.push(options);
if (options === pendingSettings) {
reportPendingUpdateStarted();
return new Promise<void>((resolve) => {
resolvePendingUpdate = resolve;
});
}
return Promise.resolve();
});
const mount = vi.fn(() => {
reportMountStarted();
return new Promise<IMountedExample>((resolve) => {
resolveMount = resolve;
});
});
const switcher = createExampleSwitcher(host, {
sheets: () => Promise.resolve({ mount }),
});
const opening = switcher.open('sheets', initialSettings);
await mountStarted;
let pendingUpdateSettled = false;
const pendingUpdate = switcher.updateSettings(pendingSettings);
const trackedPendingUpdate = pendingUpdate.finally(() => {
pendingUpdateSettled = true;
});
await Promise.resolve();
expect(pendingUpdateSettled).toBe(false);
resolveMount({ dispose: vi.fn(), updateSettings });
await pendingUpdateStarted;
const latestUpdate = switcher.updateSettings(latestSettings);
await Promise.resolve();
expect(updates).toEqual([pendingSettings]);
resolvePendingUpdate();
await Promise.all([opening, trackedPendingUpdate, latestUpdate]);
expect(updates).toEqual([pendingSettings, latestSettings]);
switcher.dispose();
});
});

View File

@@ -0,0 +1,122 @@
import { LOCALE_META, LocaleType } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import {
clearWorkbenchSettings,
getDefaultWorkbenchSettings,
getEffectiveWorkbenchRegion,
getWorkbenchUIChromeVisibility,
readWorkbenchSettings,
WORKBENCH_LOCALE_META,
WORKBENCH_LOCALES,
writeWorkbenchSettings,
} from '../workbench-settings';
function readStoredSettings(value: unknown) {
return readWorkbenchSettings({
getItem: () => JSON.stringify(value),
});
}
describe('readWorkbenchSettings', () => {
it('keeps supported settings and rejects unknown stored values', () => {
expect(readStoredSettings({
locale: 'arSA',
region: 'frFR',
direction: 'rtl',
theme: 'green',
darkMode: true,
ribbonType: 'simple',
uiChrome: 'no-ribbon',
zoomRatio: 1.25,
})).toEqual({
locale: 'arSA',
region: 'frFR',
direction: 'rtl',
theme: 'green',
darkMode: true,
ribbonType: 'simple',
uiChrome: 'no-ribbon',
zoomRatio: 1.25,
});
expect(readStoredSettings({
locale: 'unknown',
region: 'moon',
direction: 'sideways',
theme: 'custom',
darkMode: 'yes',
ribbonType: 'compact',
uiChrome: 'minimal',
zoomRatio: 1.1,
})).toEqual(getDefaultWorkbenchSettings());
expect(readWorkbenchSettings({ getItem: () => '{invalid-json' })).toEqual(getDefaultWorkbenchSettings());
});
it('supports every built-in locale and follows its default direction', () => {
expect(WORKBENCH_LOCALES).toEqual(Object.values(LocaleType));
expect(WORKBENCH_LOCALE_META).toEqual(Object.fromEntries(Object.values(LocaleType).map((locale) => [
locale,
{ direction: LOCALE_META[locale].direction, tag: LOCALE_META[locale].tag },
])));
expect(readStoredSettings({ locale: 'faIR' }).direction).toBe('rtl');
expect(readStoredSettings({ locale: 'frFR' }).direction).toBe('ltr');
expect(readStoredSettings({ locale: 'arSA', direction: 'ltr' }).direction).toBe('ltr');
});
it('persists settings through local storage', () => {
let storedValue: string | null = null;
const storage = {
getItem: () => storedValue,
setItem: (_key: string, value: string) => {
storedValue = value;
},
};
const settings = {
locale: 'enUS',
region: 'frFR',
direction: 'rtl',
theme: 'purple',
darkMode: true,
ribbonType: 'collapsed',
uiChrome: 'canvas-only',
zoomRatio: 1.75,
} as const;
writeWorkbenchSettings(storage, settings);
expect(readWorkbenchSettings(storage)).toEqual(settings);
clearWorkbenchSettings({
removeItem: () => {
storedValue = null;
},
});
expect(readWorkbenchSettings(storage)).toEqual(getDefaultWorkbenchSettings());
});
it('resolves automatic regions from language while preserving explicit regions', () => {
expect(getEffectiveWorkbenchRegion({ locale: 'enUS', region: 'auto' })).toBe('enUS');
expect(getEffectiveWorkbenchRegion({ locale: 'enUS', region: 'frFR' })).toBe('frFR');
});
it('maps UI chrome modes to the parts developers expect to keep visible', () => {
expect(getWorkbenchUIChromeVisibility('full')).toEqual({
footer: true,
header: true,
leftSidebar: true,
toolbar: true,
});
expect(getWorkbenchUIChromeVisibility('no-ribbon')).toEqual({
footer: true,
header: true,
leftSidebar: true,
toolbar: false,
});
expect(getWorkbenchUIChromeVisibility('canvas-only')).toEqual({
footer: false,
header: false,
leftSidebar: false,
toolbar: false,
});
});
});

View File

@@ -0,0 +1,40 @@
import type { WorkbenchLocale } from './workbench-settings';
import { mergeLocales } from '@univerjs/core';
import { WORKBENCH_LOCALE_META } from './workbench-settings';
type DevLocaleProduct = 'docs' | 'sheets' | 'slides';
type LocalePack = Record<string, unknown>;
interface IDevLocaleResponse {
error?: unknown;
packs?: unknown;
}
function isLocalePack(value: unknown): value is LocalePack {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
export async function loadDevLocale(
product: DevLocaleProduct,
locale: WorkbenchLocale,
fetchLocale: typeof fetch = fetch
) {
const localeTag = WORKBENCH_LOCALE_META[locale].tag;
const search = new URLSearchParams({ locale: localeTag, product });
const response = await fetchLocale(`/__univer_examples_locale?${search}`, {
headers: { accept: 'application/json' },
});
const payload = await response.json() as IDevLocaleResponse;
if (!response.ok) {
const detail = typeof payload.error === 'string' ? ` ${payload.error}` : '';
throw new Error(`Could not load ${product} locale ${localeTag}.${detail}`);
}
if (!Array.isArray(payload.packs) || !payload.packs.every(isLocalePack)) {
throw new Error(`Received invalid ${product} locale data for ${localeTag}.`);
}
return mergeLocales(...payload.packs);
}

View File

@@ -0,0 +1,155 @@
import type { IDocumentBody } from '@univerjs/core';
import {
BaselineOffset,
BooleanNumber,
CustomRangeType,
DataStreamTreeTokenType,
DocumentBlockRangeType,
HorizontalAlign,
LocaleType,
NamedStyleType,
PositionedObjectLayoutType,
PresetListType,
TextDirection,
validateDocumentStructure,
WrapTextType,
} from '@univerjs/core';
import { getSectionContentWidth } from '@univerjs/docs';
import { DOCS_DRAWING_PLUGIN } from '@univerjs/preset-docs-drawing';
import { describe, expect, it } from 'vitest';
import { readGeneratedSvgSize } from '../../generated-svg';
import { createDocumentFixture } from '../create-document-fixture';
describe('createDocumentFixture', () => {
it('provides valid, discoverable editing, layout and review samples', () => {
const snapshot = createDocumentFixture(LocaleType.EN_US);
const body = snapshot.body!;
const paragraphStyles = body.paragraphs?.flatMap(({ paragraphStyle }) => paragraphStyle ? [paragraphStyle] : []) ?? [];
const bullets = body.paragraphs?.flatMap(({ bullet }) => bullet ? [bullet] : []) ?? [];
const textStyles = body.textRuns?.flatMap(({ ts }) => ts ? [ts] : []) ?? [];
const tableRange = body.tables?.[0];
const table = tableRange ? snapshot.tableSource?.[tableRange.tableId] : undefined;
const multiColumnSection = body.sectionBreaks?.find(({ columnProperties }) => columnProperties?.length === 2);
const columns = multiColumnSection?.columnProperties ?? [];
const commentResource = snapshot.resources?.find(({ name }) => name === 'SHEET_UNIVER_THREAD_COMMENT_PLUGIN');
const drawingResource = snapshot.resources?.find(({ name }) => name === DOCS_DRAWING_PLUGIN);
const comments = commentResource
? JSON.parse(commentResource.data) as Record<
string,
Array<{ children: Array<{ text: IDocumentBody }>; text: IDocumentBody }>
>
: {};
const drawingResourceData = drawingResource
? JSON.parse(drawingResource.data) as { data: unknown; order: unknown }
: undefined;
expect(validateDocumentStructure(snapshot)).toEqual([]);
expect([...new Set(paragraphStyles.map(({ namedStyleType }) => namedStyleType))]).toEqual(expect.arrayContaining([
NamedStyleType.TITLE,
NamedStyleType.HEADING_1,
NamedStyleType.HEADING_2,
]));
expect(textStyles.some(({ bl }) => bl === BooleanNumber.TRUE)).toBe(true);
expect(textStyles.some(({ it }) => it === BooleanNumber.TRUE)).toBe(true);
expect(textStyles.some(({ bg }) => bg != null)).toBe(true);
expect(textStyles.some(({ ul }) => ul != null)).toBe(true);
expect(textStyles.some(({ st }) => st != null)).toBe(true);
expect(textStyles.some(({ va }) => va === BaselineOffset.SUBSCRIPT)).toBe(true);
expect(textStyles.some(({ va }) => va === BaselineOffset.SUPERSCRIPT)).toBe(true);
expect(paragraphStyles.some(({ horizontalAlign }) => horizontalAlign === HorizontalAlign.JUSTIFIED)).toBe(true);
expect(paragraphStyles.some(({ indentFirstLine }) => indentFirstLine != null)).toBe(true);
expect(paragraphStyles.some(({ hanging }) => hanging != null)).toBe(true);
expect(paragraphStyles.some(({ borderLeft, shading }) => borderLeft && shading)).toBe(true);
expect(paragraphStyles.some(({ tabStops }) => tabStops?.length)).toBe(true);
expect([...new Set(bullets.map(({ listType }) => listType))]).toEqual(expect.arrayContaining([
PresetListType.BULLET_LIST,
PresetListType.ORDER_LIST,
PresetListType.CHECK_LIST,
]));
expect(bullets.some(({ nestingLevel }) => nestingLevel > 0)).toBe(true);
expect(body.dataStream).toContain('\t');
expect(body.dataStream).toContain('\f');
expect([...new Set(body.blockRanges?.map(({ blockType }) => blockType))]).toEqual(expect.arrayContaining([
DocumentBlockRangeType.QUOTE,
DocumentBlockRangeType.CALLOUT,
DocumentBlockRangeType.CODE,
]));
expect(columns).toHaveLength(2);
expect(columns.every(({ width }) => width > 0)).toBe(true);
expect(columns.reduce((total, { width, paddingEnd }) => total + width + paddingEnd, 0)).toBeLessThanOrEqual(
getSectionContentWidth(snapshot.documentStyle, multiColumnSection)
);
expect(Object.keys(snapshot.headers ?? {})).not.toHaveLength(0);
expect(Object.keys(snapshot.footers ?? {})).not.toHaveLength(0);
expect(tableRange && body.dataStream[tableRange.startIndex]).toBe(DataStreamTreeTokenType.TABLE_START);
expect(table?.tableRows.some(({ tableCells }) => tableCells.some(({ rowSpan }) => rowSpan === 2))).toBe(true);
expect(table?.tableRows[0].repeatHeaderRow).toBe(BooleanNumber.TRUE);
expect(body.customRanges?.some(({ rangeType }) => rangeType === CustomRangeType.HYPERLINK)).toBe(true);
expect([...new Set(Object.values(snapshot.drawings ?? {}).map(({ layoutType }) => layoutType))]).toEqual(
expect.arrayContaining([PositionedObjectLayoutType.INLINE, PositionedObjectLayoutType.WRAP_SQUARE])
);
expect(Object.values(snapshot.drawings ?? {}).find(
({ layoutType }) => layoutType === PositionedObjectLayoutType.WRAP_SQUARE
)).toMatchObject({
distB: 8,
distL: 12,
distT: 8,
wrapText: WrapTextType.LEFT,
});
expect(Object.values(snapshot.drawings ?? {}).every((drawing) => (
'source' in drawing && typeof drawing.source === 'string' && drawing.source.startsWith('data:image/svg+xml')
))).toBe(true);
Object.values(snapshot.drawings ?? {}).forEach((drawing) => {
if (!('source' in drawing) || typeof drawing.source !== 'string') {
throw new Error(`Expected ${drawing.drawingId} to be an SVG image drawing.`);
}
const intrinsicSize = readGeneratedSvgSize(drawing.source);
const { transform } = drawing;
if (transform?.width == null || transform.height == null) {
throw new Error(`Expected ${drawing.drawingId} to define a render transform.`);
}
expect(drawing.docTransform.size).toEqual(intrinsicSize);
expect(transform.width / transform.height).toBeCloseTo(
intrinsicSize.width / intrinsicSize.height,
8
);
});
expect(body.customBlocks?.map(({ blockId, startIndex }) => ({
blockId,
token: body.dataStream[startIndex],
}))).toEqual(snapshot.drawingsOrder?.map((blockId) => ({
blockId,
token: DataStreamTreeTokenType.CUSTOM_BLOCK,
})));
expect(drawingResourceData).toEqual({
data: snapshot.drawings,
order: snapshot.drawingsOrder,
});
expect(body.customDecorations).toHaveLength(2);
expect(comments.default_doc).toHaveLength(2);
expect(comments.default_doc?.some(({ children }) => children.length > 0)).toBe(true);
expect(comments.default_doc.flatMap((comment) => [comment, ...comment.children]).every(({ text }) => (
text.paragraphs?.some(({ startIndex }) => (
text.dataStream.slice(0, startIndex).replaceAll('\r', '').trim()
)) === true
))).toBe(true);
});
it('creates fresh deterministic snapshots and applies RTL to document sections', () => {
const first = createDocumentFixture(LocaleType.AR_SA, 'rtl');
const second = createDocumentFixture(LocaleType.AR_SA, 'rtl');
const documentSections = first.body?.sectionBreaks?.filter(({ sectionId }) => !sectionId.startsWith('section-table-'));
expect(first.body).not.toBe(second.body);
expect(JSON.stringify(first)).toBe(JSON.stringify(second));
expect(documentSections?.every(({ contentDirection }) => contentDirection === TextDirection.RIGHT_TO_LEFT)).toBe(true);
});
it('persists the selected default zoom in the document snapshot', () => {
expect(createDocumentFixture(LocaleType.EN_US).settings?.zoomRatio).toBe(1);
expect(createDocumentFixture(LocaleType.EN_US, 'ltr', 1.5).settings?.zoomRatio).toBe(1.5);
});
});

View File

@@ -0,0 +1,766 @@
import type {
IDocumentBody,
IDocumentData,
IParagraph,
ISectionBreak,
LocaleType,
} from '@univerjs/core';
import {
BaselineOffset,
BlockType,
BooleanNumber,
ColumnSeparatorType,
CustomDecorationType,
DashStyleType,
DataStreamTreeTokenType,
DocStyleType,
DocumentBlockRangeType,
DocumentFlavor,
DrawingTypeEnum,
getDocsEmptySnapshot,
HorizontalAlign,
ImageSourceType,
NamedStyleType,
PositionedObjectLayoutType,
PresetListType,
RichTextBuilder,
SectionType,
TableLayoutType,
TextDecoration,
TextDirection,
VerticalAlignmentType,
WrapTextType,
} from '@univerjs/core';
import { buildDocTransform, createSectionColumnProperties, docDrawingPositionToTransform } from '@univerjs/docs';
import { genTableSource, getTableColumn } from '@univerjs/docs-ui';
import { DOCS_DRAWING_PLUGIN } from '@univerjs/preset-docs-drawing';
import { createGeneratedSvg } from '../generated-svg';
const BLOCK_END_PLACEHOLDER = '\uE001';
const BLOCK_START_PLACEHOLDER = '\uE000';
const COMMENT_IDS = ['comment-layout', 'comment-review'] as const;
const COMMENT_TARGETS = ['检查段落间距与边框', '检查绘图、链接与批注'] as const;
const DEFAULT_FOOTER_ID = 'footer-default';
const DEFAULT_HEADER_ID = 'header-default';
const DOCUMENT_ID = 'document-01';
const FLOATING_DRAWING_ID = 'drawing-floating-card';
const FIRST_PAGE_HEADER_ID = 'header-first-page';
const INLINE_DRAWING_ID = 'drawing-workflow';
const SECTION_MARKERS = ['[[CAPABILITY_COLUMNS]]', '[[CAPABILITY_TABLE]]'] as const;
const TABLE_ID = 'capability-table';
const FLOW_DRAWING = createGeneratedSvg({
width: 420,
height: 128,
content: [
'<rect width="420" height="128" rx="18" fill="#eef2ff"/>',
'<rect x="24" y="38" width="96" height="52" rx="12" fill="#365cf5"/>',
'<path d="M128 64h23m-9-9 9 9-9 9" fill="none" stroke="#64748b" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>',
'<rect x="162" y="38" width="96" height="52" rx="12" fill="#b45309"/>',
'<path d="M266 64h23m-9-9 9 9-9 9" fill="none" stroke="#64748b" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>',
'<rect x="300" y="38" width="96" height="52" rx="12" fill="#047857"/>',
'<text x="72" y="69" text-anchor="middle" fill="white" font-family="Arial" font-size="14">Edit</text>',
'<text x="210" y="69" text-anchor="middle" fill="white" font-family="Arial" font-size="14">Review</text>',
'<text x="348" y="69" text-anchor="middle" fill="white" font-family="Arial" font-size="14">Ship</text>',
],
});
const FLOATING_DRAWING = createGeneratedSvg({
width: 240,
height: 136,
content: [
'<rect x="2" y="2" width="236" height="132" rx="18" fill="#fff7ed" stroke="#fb923c" stroke-width="3"/>',
'<circle cx="42" cy="42" r="18" fill="#f97316"/>',
'<path d="m34 42 6 6 12-15" fill="none" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>',
'<text x="72" y="48" fill="#9a3412" font-family="Arial" font-size="16" font-weight="700">Wrapped image</text>',
'<text x="120" y="96" text-anchor="middle" fill="#7c2d12" font-family="Arial" font-size="13">Move · resize · arrange</text>',
],
});
interface ITableStreamData {
dataStream: string;
paragraphs: IParagraph[];
sectionBreaks: ISectionBreak[];
textRuns: NonNullable<IDocumentBody['textRuns']>;
}
function addCapabilityMap(builder: RichTextBuilder): RichTextBuilder {
return builder
.paragraph({ align: HorizontalAlign.CENTER, keepNext: true, spaceAfter: 8 })
.span('Univer Docs Capability Workbench', {
bold: true,
color: '#1d4ed8',
fontFamily: 'Inter, Arial, sans-serif',
fontSize: 26,
})
.paragraph({ align: HorizontalAlign.CENTER, spaceAfter: 18 })
.span('Edit every sample. The document is a compact map of public Docs capabilities.', {
color: '#64748b',
fontSize: 11,
italic: true,
})
.paragraph({ keepNext: true, spaceAfter: 6 })
.span('1. Capability map', { bold: true, color: '#0f172a', fontSize: 19 })
.paragraph({ lineHeight: 1.45, firstLineIndent: 20, spaceAfter: 8 })
.text('Use this document to exercise text, paragraph, list, layout, table, drawing, link and review workflows. ')
.link('Open Univer documentation', 'https://docs.univer.ai')
.text(' to verify the hyperlink popup and editing actions.')
.listItem('Text runs: font family, size, color, highlight, bold, italic and decorations', {
type: PresetListType.BULLET_LIST,
listId: 'capability-map',
})
.listItem('Paragraphs: alignment, spacing, indentation, tabs, borders and named styles', {
type: PresetListType.BULLET_LIST,
listId: 'capability-map',
level: 1,
})
.listItem('Document layout: pages, sections, columns, headers, footers and tables', {
type: PresetListType.BULLET_LIST,
listId: 'capability-map',
})
.listItem('Objects and review: inline/floating drawings, links and thread comments', {
type: PresetListType.BULLET_LIST,
listId: 'capability-map',
})
.paragraph({ lineHeight: 1.4, spaceBefore: 10, spaceAfter: 8 })
.span('Status\tOwner\tDue', { bold: true, color: '#334155' })
.paragraph({ lineHeight: 1.4, spaceAfter: 12 })
.text('Editable\tDocs team\tToday');
}
function addTextAndParagraphLab(builder: RichTextBuilder): RichTextBuilder {
return builder.text('\f')
.paragraph({ keepNext: true, spaceAfter: 6 })
.span('2. Text and paragraph laboratory', { bold: true, color: '#0f766e', fontSize: 19 })
.paragraph({ lineHeight: 1.5, spaceAfter: 8 })
.text('Inline formatting: ')
.span('bold', { bold: true })
.text(' · ')
.span('italic', { italic: true })
.text(' · ')
.span('highlight', { background: '#fef08a', color: '#713f12' })
.text(' · ')
.span('underline', { ul: { s: BooleanNumber.TRUE, t: TextDecoration.WAVE } })
.text(' · ')
.span('strike', { st: { s: BooleanNumber.TRUE, t: TextDecoration.SINGLE } })
.text(' · H')
.span('2', { va: BaselineOffset.SUBSCRIPT })
.text('O · x')
.span('2', { va: BaselineOffset.SUPERSCRIPT })
.text(' · ')
.code('pnpm test')
.paragraph({ align: HorizontalAlign.JUSTIFIED, firstLineIndent: 24, lineHeight: 1.55, spaceAfter: 10 })
.text('Justified paragraph — long enough to wrap across lines and expose line spacing, first-line indentation, '
+ 'word wrapping and selection behavior while editing. 中英文混排用于检查字体回退、标点与换行。')
.paragraph({ align: HorizontalAlign.RIGHT, indentStart: 36, indentEnd: 18, spaceAfter: 8 })
.span('Right aligned with leading and trailing indentation', { color: '#7c3aed', italic: true })
.paragraph({ hangingIndent: 24, indentStart: 36, lineHeight: 1.4, spaceAfter: 10 })
.text(`${COMMENT_TARGETS[0]}:这是悬挂缩进段落;选择本句可验证 comment decoration、段落底纹和边框。`)
.listItem('Ordered step one: select a paragraph', {
type: PresetListType.ORDER_LIST,
listId: 'ordered-workflow',
})
.listItem('Nested step: change the list level', {
type: PresetListType.ORDER_LIST,
listId: 'ordered-workflow',
level: 1,
})
.listItem('Ordered step two: switch numbering style', {
type: PresetListType.ORDER_LIST,
listId: 'ordered-workflow',
})
.listItem('Checklist item: verify toolbar state', {
type: PresetListType.CHECK_LIST,
listId: 'review-checklist',
})
.listItem('Checklist item: undo and redo an edit', {
type: PresetListType.CHECK_LIST,
listId: 'review-checklist',
})
.paragraph({ keepNext: true, spaceBefore: 12, spaceAfter: 6 })
.span('Structured blocks', { bold: true, color: '#334155', fontSize: 15 })
.paragraph({ lineHeight: 1.4, indentStart: 20, indentEnd: 20, spaceAfter: 8 })
.text(BLOCK_START_PLACEHOLDER)
.span('“A quote block should remain movable, selectable and distinct from ordinary body text.”', {
color: '#475569',
italic: true,
})
.paragraph()
.text(BLOCK_END_PLACEHOLDER)
.paragraph({ lineHeight: 1.4, indentStart: 20, indentEnd: 20, spaceAfter: 8 })
.text(BLOCK_START_PLACEHOLDER)
.span('Tip: try paragraph alignment, background color and block movement here.', { color: '#075985' })
.paragraph()
.text(BLOCK_END_PLACEHOLDER)
.paragraph({ lineHeight: 1.35, indentStart: 20, indentEnd: 20, spaceAfter: 8 })
.text(BLOCK_START_PLACEHOLDER)
.code('const capability = { editable: true, deterministic: true };')
.paragraph()
.text(BLOCK_END_PLACEHOLDER);
}
function addLayoutAndReview(builder: RichTextBuilder): RichTextBuilder {
return builder.text('\f')
.paragraph({ keepNext: true, spaceAfter: 6 })
.span('3. Layout, drawing and review', { bold: true, color: '#b45309', fontSize: 19 })
.paragraph({ lineHeight: 1.45, spaceAfter: 8 })
.text('This page combines inline and wrapped objects. Select each image to inspect resize, arrange, wrapping and delete actions.')
.paragraph({ align: HorizontalAlign.CENTER, spaceAfter: 8 })
.text('\b')
.paragraph({ lineHeight: 1.45, spaceAfter: 8 })
.text('\b')
.text(`${COMMENT_TARGETS[1]}。The orange card is a floating image with square wrapping; the workflow is inline.`)
.paragraph({ lineHeight: 1.45, spaceAfter: 10 })
.text('Mixed direction sample: English left-to-right · العربية من اليمين إلى اليسار · 中文标点与自动换行。')
.paragraph({ keepNext: true, spaceBefore: 10, spaceAfter: 6 })
.span('Review workflow', { bold: true, color: '#334155', fontSize: 15 })
.listItem('Open the comment panel and reply to the seeded thread', {
type: PresetListType.CHECK_LIST,
listId: 'review-workflow',
})
.listItem('Edit the link, then copy and paste it into another paragraph', {
type: PresetListType.CHECK_LIST,
listId: 'review-workflow',
})
.listItem('Move the floating card and resize the inline workflow', {
type: PresetListType.CHECK_LIST,
listId: 'review-workflow',
})
.paragraph()
.text(SECTION_MARKERS[0]);
}
function addCommandIndexAndTableIntro(builder: RichTextBuilder): RichTextBuilder {
return builder.paragraph({ keepNext: true, spaceAfter: 6 })
.span('4. Two-column command index', { bold: true, color: '#6d28d9', fontSize: 18 })
.paragraph({ lineHeight: 1.35, spaceAfter: 8 })
.span('Edit', { bold: true })
.text(' — select, copy, paste, undo, redo, find and replace.')
.paragraph({ lineHeight: 1.35, spaceAfter: 8 })
.span('Format', { bold: true })
.text(' — text runs, named headings, paragraph spacing, borders and lists.')
.paragraph({ lineHeight: 1.35, spaceAfter: 8 })
.span('Insert', { bold: true })
.text(' — tables, links, images, page breaks, headers and footers.')
.paragraph({ lineHeight: 1.35, spaceAfter: 8 })
.span('Review', { bold: true })
.text(' — comments, replies, resolved state and selection highlights.')
.paragraph({ lineHeight: 1.35, spaceAfter: 8 })
.span('Layout', { bold: true })
.text(' — page size, margins, pagination, sections and columns.')
.paragraph()
.text(SECTION_MARKERS[1])
.paragraph({ keepNext: true, spaceAfter: 6 })
.span('5. Editable capability matrix', { bold: true, color: '#0369a1', fontSize: 19 })
.paragraph({ lineHeight: 1.4, spaceAfter: 8 })
.text('Use the table context menu to insert/delete rows or columns, resize columns and edit the merged cells below.');
}
function createRichText(): RichTextBuilder {
const builder = RichTextBuilder.create();
addCapabilityMap(builder);
addTextAndParagraphLab(builder);
addLayoutAndReview(builder);
return addCommandIndexAndTableIntro(builder);
}
function replaceBlockPlaceholders(body: IDocumentBody): void {
const blockTypes = [
DocumentBlockRangeType.QUOTE,
DocumentBlockRangeType.CALLOUT,
DocumentBlockRangeType.CODE,
];
let searchStart = 0;
body.blockRanges = blockTypes.map((blockType) => {
const startIndex = body.dataStream.indexOf(BLOCK_START_PLACEHOLDER, searchStart);
const endIndex = body.dataStream.indexOf(BLOCK_END_PLACEHOLDER, startIndex + 1);
searchStart = endIndex + 1;
return { blockId: `block-${blockType}`, blockType, startIndex, endIndex };
});
body.dataStream = body.dataStream
.replaceAll(BLOCK_START_PLACEHOLDER, DataStreamTreeTokenType.BLOCK_START)
.replaceAll(BLOCK_END_PLACEHOLDER, DataStreamTreeTokenType.BLOCK_END);
}
function shiftBodyMetadata(body: IDocumentBody, threshold: number, delta: number): void {
const shift = (index: number) => index >= threshold ? index + delta : index;
body.paragraphs?.forEach((paragraph) => {
paragraph.startIndex = shift(paragraph.startIndex);
});
body.sectionBreaks?.forEach((sectionBreak) => {
sectionBreak.startIndex = shift(sectionBreak.startIndex);
});
body.textRuns?.forEach((run) => {
run.st = shift(run.st);
run.ed = shift(run.ed);
});
body.customRanges?.forEach((range) => {
range.startIndex = shift(range.startIndex);
range.endIndex = shift(range.endIndex);
});
body.customBlocks?.forEach((block) => {
block.startIndex = shift(block.startIndex);
});
body.customDecorations?.forEach((decoration) => {
decoration.startIndex = shift(decoration.startIndex);
decoration.endIndex = shift(decoration.endIndex);
});
body.blockRanges?.forEach((range) => {
range.startIndex = shift(range.startIndex);
range.endIndex = shift(range.endIndex);
});
body.tables?.forEach((table) => {
table.startIndex = shift(table.startIndex);
table.endIndex = shift(table.endIndex);
});
}
function replaceSectionMarker(
body: IDocumentBody,
marker: string,
section: Omit<ISectionBreak, 'startIndex'>
): void {
const startIndex = body.dataStream.indexOf(marker);
const endIndex = startIndex + marker.length + DataStreamTreeTokenType.PARAGRAPH.length;
const markerParagraphIndex = endIndex - 1;
const delta = DataStreamTreeTokenType.SECTION_BREAK.length - (endIndex - startIndex);
body.dataStream = body.dataStream.slice(0, startIndex)
+ DataStreamTreeTokenType.SECTION_BREAK
+ body.dataStream.slice(endIndex);
body.paragraphs = body.paragraphs?.filter(({ startIndex: paragraphIndex }) => paragraphIndex !== markerParagraphIndex);
shiftBodyMetadata(body, endIndex, delta);
body.sectionBreaks?.push({ ...section, startIndex });
}
function findParagraph(body: IDocumentBody, text: string): IParagraph {
const textStart = body.dataStream.indexOf(text);
const paragraphs = body.paragraphs ?? [];
const paragraph = paragraphs.find(({ startIndex }, index) => {
const contentStart = index === 0 ? 0 : paragraphs[index - 1].startIndex + 1;
return contentStart <= textStart && startIndex >= textStart + text.length;
});
if (!paragraph) {
throw new Error(`Missing fixture paragraph: ${text}`);
}
return paragraph;
}
function applyParagraphFeatures(body: IDocumentBody): void {
const namedParagraphs = [
['Univer Docs Capability Workbench', 'style-title', NamedStyleType.TITLE],
['1. Capability map', 'style-heading-1', NamedStyleType.HEADING_1],
['2. Text and paragraph laboratory', 'style-heading-1', NamedStyleType.HEADING_1],
['3. Layout, drawing and review', 'style-heading-1', NamedStyleType.HEADING_1],
['4. Two-column command index', 'style-heading-1', NamedStyleType.HEADING_1],
['5. Editable capability matrix', 'style-heading-1', NamedStyleType.HEADING_1],
['Structured blocks', 'style-heading-2', NamedStyleType.HEADING_2],
['Review workflow', 'style-heading-2', NamedStyleType.HEADING_2],
] as const;
namedParagraphs.forEach(([text, styleId, namedStyleType], index) => {
const paragraph = findParagraph(body, text);
paragraph.styleId = styleId;
paragraph.paragraphStyle = {
...paragraph.paragraphStyle,
headingId: `heading-${index + 1}`,
namedStyleType,
};
});
for (const text of ['Status\tOwner\tDue', 'Editable\tDocs team\tToday']) {
const paragraph = findParagraph(body, text);
paragraph.paragraphStyle = {
...paragraph.paragraphStyle,
tabStops: [
{ alignment: 1, offset: 210 },
{ alignment: 1, offset: 390 },
],
};
}
const borderedParagraph = findParagraph(body, `${COMMENT_TARGETS[0]}:`);
borderedParagraph.paragraphStyle = {
...borderedParagraph.paragraphStyle,
borderLeft: { color: { rgb: '#3a60f7' }, dashStyle: DashStyleType.SOLID, padding: 8, width: 3 },
borderBottom: { color: { rgb: '#bfdbfe' }, dashStyle: DashStyleType.SOLID, padding: 5, width: 1 },
shading: { backgroundColor: { rgb: '#eff6ff' } },
};
}
function createDocumentBody(
direction: 'ltr' | 'rtl',
documentStyle: IDocumentData['documentStyle']
): IDocumentBody {
const body = createRichText().getData().body!;
const contentDirection = direction === 'rtl' ? TextDirection.RIGHT_TO_LEFT : TextDirection.LEFT_TO_RIGHT;
replaceBlockPlaceholders(body);
replaceSectionMarker(body, SECTION_MARKERS[0], {
sectionId: 'section-main',
sectionType: SectionType.NEXT_PAGE,
contentDirection,
});
replaceSectionMarker(body, SECTION_MARKERS[1], {
sectionId: 'section-columns',
sectionType: SectionType.NEXT_PAGE,
contentDirection,
columnProperties: createSectionColumnProperties(documentStyle, undefined, 2, 22),
columnSeparatorType: ColumnSeparatorType.BETWEEN_EACH_COLUMN,
});
body.paragraphs?.forEach((paragraph, index) => {
paragraph.paragraphId = `paragraph-${index + 1}`;
});
body.sectionBreaks?.forEach((sectionBreak, index) => {
if (!['section-main', 'section-columns'].includes(sectionBreak.sectionId)) {
sectionBreak.sectionId = `section-${index + 1}`;
}
sectionBreak.contentDirection = contentDirection;
});
body.customRanges?.forEach((range, index) => {
range.rangeId = `link-${index + 1}`;
});
const inlineDrawingStartIndex = body.dataStream.indexOf(DataStreamTreeTokenType.CUSTOM_BLOCK);
body.customBlocks = [
{
blockId: INLINE_DRAWING_ID,
blockType: BlockType.DRAWING,
startIndex: inlineDrawingStartIndex,
},
{
blockId: FLOATING_DRAWING_ID,
blockType: BlockType.DRAWING,
startIndex: body.dataStream.indexOf(DataStreamTreeTokenType.CUSTOM_BLOCK, inlineDrawingStartIndex + 1),
},
];
body.customDecorations = COMMENT_TARGETS.map((target, index) => {
const startIndex = body.dataStream.indexOf(target);
return {
id: COMMENT_IDS[index],
type: CustomDecorationType.COMMENT,
startIndex,
endIndex: startIndex + target.length - 1,
};
});
applyParagraphFeatures(body);
return body;
}
function createTableStream(): ITableStreamData {
const rows = [
['Capability', 'Seeded sample', 'Development action'],
['Text', 'Rich runs + hyperlink', 'Edit styles, copy and paste'],
['Paragraph', 'Lists + tabs + blocks', 'Change indent, spacing and alignment'],
['Layout', 'Pages + sections + columns', 'Inspect header/footer and pagination'],
['Objects', 'Inline + wrapped image', 'Resize, move and arrange'],
['', 'Thread comments', 'Reply, resolve and restore'],
];
const paragraphs: IParagraph[] = [];
const sectionBreaks: ISectionBreak[] = [];
const textRuns: NonNullable<IDocumentBody['textRuns']> = [];
let dataStream: string = DataStreamTreeTokenType.TABLE_START;
rows.forEach((row, rowIndex) => {
dataStream += DataStreamTreeTokenType.TABLE_ROW_START;
row.forEach((text, columnIndex) => {
dataStream += DataStreamTreeTokenType.TABLE_CELL_START;
const textStart = dataStream.length;
dataStream += text;
const paragraphIndex = dataStream.length;
dataStream += DataStreamTreeTokenType.PARAGRAPH;
const sectionIndex = dataStream.length;
dataStream += DataStreamTreeTokenType.SECTION_BREAK + DataStreamTreeTokenType.TABLE_CELL_END;
paragraphs.push({
paragraphId: `paragraph-table-${rowIndex + 1}-${columnIndex + 1}`,
startIndex: paragraphIndex,
paragraphStyle: {
horizontalAlign: rowIndex === 0 ? HorizontalAlign.CENTER : HorizontalAlign.LEFT,
lineSpacing: 1.2,
spaceAbove: { v: 2 },
spaceBelow: { v: 2 },
},
...(rowIndex === 2 && columnIndex === 1
? { bullet: { listId: 'table-check', listType: PresetListType.CHECK_LIST, nestingLevel: 0 } }
: {}),
});
sectionBreaks.push({ sectionId: `section-table-${rowIndex + 1}-${columnIndex + 1}`, startIndex: sectionIndex });
if (text) {
textRuns.push({
st: textStart,
ed: textStart + text.length,
ts: rowIndex === 0
? { bl: BooleanNumber.TRUE, cl: { rgb: '#ffffff' }, fs: 10 }
: { cl: { rgb: '#334155' }, fs: 9 },
});
}
});
dataStream += DataStreamTreeTokenType.TABLE_ROW_END;
});
dataStream += DataStreamTreeTokenType.TABLE_END;
return { dataStream, paragraphs, sectionBreaks, textRuns };
}
function appendCapabilityTable(snapshot: IDocumentData): void {
const body = snapshot.body!;
const tableStream = createTableStream();
const tableStart = body.dataStream.length - 1;
const trailingParagraph = DataStreamTreeTokenType.PARAGRAPH;
const insertedLength = tableStream.dataStream.length + trailingParagraph.length;
const table = genTableSource(6, 3, 600);
shiftBodyMetadata(body, tableStart, insertedLength);
body.dataStream = body.dataStream.slice(0, tableStart)
+ tableStream.dataStream
+ trailingParagraph
+ body.dataStream.slice(tableStart);
body.paragraphs?.push(
...tableStream.paragraphs.map((paragraph) => ({ ...paragraph, startIndex: paragraph.startIndex + tableStart })),
{ paragraphId: 'paragraph-after-table', startIndex: tableStart + tableStream.dataStream.length }
);
body.sectionBreaks?.push(...tableStream.sectionBreaks.map((sectionBreak) => ({
...sectionBreak,
startIndex: sectionBreak.startIndex + tableStart,
})));
body.textRuns?.push(...tableStream.textRuns.map((run) => ({
...run,
st: run.st + tableStart,
ed: run.ed + tableStart,
})));
body.tables?.push({
tableId: TABLE_ID,
startIndex: tableStart,
endIndex: tableStart + tableStream.dataStream.length,
});
table.tableId = TABLE_ID;
table.description = 'Editable Docs capability coverage matrix';
table.layout = TableLayoutType.FIXED;
table.tableColumns = [getTableColumn(120), getTableColumn(190), getTableColumn(290)];
table.tableRows[0].isFirstRow = BooleanNumber.TRUE;
table.tableRows[0].repeatHeaderRow = BooleanNumber.TRUE;
table.tableRows[4].tableCells[0].rowSpan = 2;
table.tableRows[5].tableCells[0].rowSpan = 0;
table.tableRows[5].tableCells[0].columnSpan = 0;
table.tableRows.forEach((row, rowIndex) => {
row.cantSplit = BooleanNumber.TRUE;
row.tableCells.forEach((cell) => {
cell.vAlign = VerticalAlignmentType.CENTER;
cell.backgroundColor = { rgb: rowIndex === 0 ? '#3a60f7' : rowIndex % 2 ? '#f8fafc' : '#eef2ff' };
cell.borderTop = { color: { rgb: '#cbd5e1' }, width: { v: 1 } };
cell.borderBottom = { color: { rgb: '#cbd5e1' }, width: { v: 1 } };
cell.borderLeft = { color: { rgb: '#cbd5e1' }, width: { v: 1 } };
cell.borderRight = { color: { rgb: '#cbd5e1' }, width: { v: 1 } };
});
});
snapshot.tableSource = { [TABLE_ID]: table };
body.paragraphs?.sort((a, b) => a.startIndex - b.startIndex);
body.sectionBreaks?.sort((a, b) => a.startIndex - b.startIndex);
body.textRuns?.sort((a, b) => a.st - b.st);
}
function createSegmentBody(
text: string,
segment: 'header' | 'footer',
direction: 'ltr' | 'rtl',
align: HorizontalAlign
): IDocumentBody {
return {
dataStream: `${text}\r\n`,
paragraphs: [{
paragraphId: `paragraph-${segment}-${text.replaceAll(' ', '-').toLowerCase()}`,
startIndex: text.length,
paragraphStyle: { horizontalAlign: align, lineSpacing: 1 },
}],
sectionBreaks: [{
sectionId: `section-${segment}-${text.replaceAll(' ', '-').toLowerCase()}`,
startIndex: text.length + 1,
contentDirection: direction === 'rtl' ? TextDirection.RIGHT_TO_LEFT : TextDirection.LEFT_TO_RIGHT,
}],
textRuns: [{ st: 0, ed: text.length, ts: { cl: { rgb: '#64748b' }, fs: 9 } }],
};
}
function createDrawings() {
const inlineTransform = buildDocTransform(FLOW_DRAWING.width, FLOW_DRAWING.height);
const floatingTransform = buildDocTransform(FLOATING_DRAWING.width, FLOATING_DRAWING.height, {
left: 390,
top: 90,
});
return {
[INLINE_DRAWING_ID]: {
unitId: DOCUMENT_ID,
subUnitId: DOCUMENT_ID,
drawingId: INLINE_DRAWING_ID,
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
imageSourceType: ImageSourceType.BASE64,
source: FLOW_DRAWING.source,
transform: docDrawingPositionToTransform(inlineTransform),
docTransform: inlineTransform,
behindDoc: BooleanNumber.FALSE,
title: 'Editable review workflow',
description: 'Local SVG showing the edit, review and ship workflow',
layoutType: PositionedObjectLayoutType.INLINE,
wrapText: WrapTextType.BOTH_SIDES,
},
[FLOATING_DRAWING_ID]: {
unitId: DOCUMENT_ID,
subUnitId: DOCUMENT_ID,
drawingId: FLOATING_DRAWING_ID,
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
imageSourceType: ImageSourceType.BASE64,
source: FLOATING_DRAWING.source,
transform: docDrawingPositionToTransform(floatingTransform),
docTransform: floatingTransform,
behindDoc: BooleanNumber.FALSE,
title: 'Floating wrapped image',
description: 'Local SVG for resize, arrange and text wrapping development',
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
wrapText: WrapTextType.LEFT,
distL: 12,
distT: 8,
distB: 8,
},
};
}
function createDrawingResource(
drawings: NonNullable<IDocumentData['drawings']>,
order: string[]
) {
return {
name: DOCS_DRAWING_PLUGIN,
data: JSON.stringify({ data: drawings, order }),
};
}
function createCommentBody(id: string, text: string): IDocumentBody {
const dataStream = `${text}\r\n`;
return {
dataStream,
paragraphs: [{ startIndex: dataStream.length - 2, paragraphId: `${id}-paragraph` }],
sectionBreaks: [{ startIndex: dataStream.length - 1, sectionId: `${id}-section` }],
};
}
function createCommentResource() {
return {
name: 'SHEET_UNIVER_THREAD_COMMENT_PLUGIN',
data: JSON.stringify({
default_doc: COMMENT_IDS.map((id, index) => ({
id,
threadId: id,
ref: COMMENT_TARGETS[index],
dT: `2026/08/19 10:0${index}`,
personId: '',
text: createCommentBody(
id,
index === 0 ? '请核对段落布局和样式。' : '请验证绘图、链接与批注定位。'
),
unitId: DOCUMENT_ID,
subUnitId: 'default_doc',
children: index === 1
? [{
id: 'comment-review-reply',
threadId: id,
parentId: id,
dT: '2026/08/19 10:12',
personId: '',
text: createCommentBody('comment-review-reply', '已加入内联与浮动两种本地 SVG。'),
unitId: DOCUMENT_ID,
subUnitId: 'default_doc',
}]
: [],
})),
}),
};
}
export function createDocumentFixture(
locale: LocaleType,
direction: 'ltr' | 'rtl' = 'ltr',
zoomRatio: number = 1
): IDocumentData {
const snapshot = getDocsEmptySnapshot(
DOCUMENT_ID,
locale,
'Univer Docs Capability Workbench',
DocumentFlavor.TRADITIONAL
);
snapshot.body = createDocumentBody(direction, snapshot.documentStyle);
snapshot.documentStyle = {
...snapshot.documentStyle,
defaultHeaderId: DEFAULT_HEADER_ID,
defaultFooterId: DEFAULT_FOOTER_ID,
firstPageHeaderId: FIRST_PAGE_HEADER_ID,
firstPageFooterId: DEFAULT_FOOTER_ID,
useFirstPageHeaderFooter: BooleanNumber.TRUE,
marginHeader: 24,
marginFooter: 24,
};
snapshot.settings = {
...snapshot.settings,
zoomRatio,
};
snapshot.styles = {
'style-title': {
name: 'Capability Title',
type: DocStyleType.paragraph,
textStyle: { bl: BooleanNumber.TRUE, cl: { rgb: '#1d4ed8' }, fs: 26 },
paragraphStyle: { horizontalAlign: HorizontalAlign.CENTER, keepNext: BooleanNumber.TRUE },
},
'style-heading-1': {
name: 'Capability Heading 1',
type: DocStyleType.paragraph,
textStyle: { bl: BooleanNumber.TRUE, cl: { rgb: '#0f172a' }, fs: 19 },
paragraphStyle: { keepNext: BooleanNumber.TRUE, spaceBelow: { v: 6 } },
},
'style-heading-2': {
name: 'Capability Heading 2',
basedOn: 'style-heading-1',
type: DocStyleType.paragraph,
textStyle: { bl: BooleanNumber.TRUE, cl: { rgb: '#334155' }, fs: 15 },
paragraphStyle: { keepNext: BooleanNumber.TRUE, spaceBelow: { v: 4 } },
},
};
snapshot.headers = {
[FIRST_PAGE_HEADER_ID]: {
headerId: FIRST_PAGE_HEADER_ID,
body: createSegmentBody('UNIVER DOCS · CAPABILITY LAB', 'header', direction, HorizontalAlign.CENTER),
},
[DEFAULT_HEADER_ID]: {
headerId: DEFAULT_HEADER_ID,
body: createSegmentBody('UNIVER DOCS · EDIT · LAYOUT · REVIEW', 'header', direction, HorizontalAlign.RIGHT),
},
};
snapshot.footers = {
[DEFAULT_FOOTER_ID]: {
footerId: DEFAULT_FOOTER_ID,
body: createSegmentBody('DEVELOP · INSPECT · REGRESS', 'footer', direction, HorizontalAlign.CENTER),
},
};
const drawings = createDrawings();
const drawingsOrder = [INLINE_DRAWING_ID, FLOATING_DRAWING_ID];
snapshot.drawings = drawings;
snapshot.drawingsOrder = drawingsOrder;
snapshot.resources = [createDrawingResource(drawings, drawingsOrder), createCommentResource()];
appendCapabilityTable(snapshot);
return snapshot;
}

View File

@@ -0,0 +1,4 @@
@config "../../tailwind.docs.config.ts";
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,6 @@
import type { WorkbenchLocale } from '../workbench-settings';
import { loadDevLocale } from '../dev-locale-loader';
export function loadDocumentLocale(locale: WorkbenchLocale) {
return loadDevLocale('docs', locale);
}

View File

@@ -0,0 +1,33 @@
import type { WorkbenchLocale } from '../workbench-settings';
interface ILocaleModule {
default: Record<string, unknown>;
}
type LocaleLoader = () => Promise<ILocaleModule>;
const DOCUMENT_LOCALE_LOADERS = {
enUS: () => import('./locales/en-US'),
frFR: () => import('./locales/fr-FR'),
zhCN: () => import('./locales/zh-CN'),
ruRU: () => import('./locales/ru-RU'),
zhTW: () => import('./locales/zh-TW'),
zhHK: () => import('./locales/zh-HK'),
viVN: () => import('./locales/vi-VN'),
faIR: () => import('./locales/fa-IR'),
jaJP: () => import('./locales/ja-JP'),
koKR: () => import('./locales/ko-KR'),
esES: () => import('./locales/es-ES'),
caES: () => import('./locales/ca-ES'),
skSK: () => import('./locales/sk-SK'),
ptBR: () => import('./locales/pt-BR'),
deDE: () => import('./locales/de-DE'),
itIT: () => import('./locales/it-IT'),
idID: () => import('./locales/id-ID'),
plPL: () => import('./locales/pl-PL'),
arSA: () => import('./locales/ar-SA'),
} satisfies Record<WorkbenchLocale, LocaleLoader>;
export async function loadDocumentLocale(locale: WorkbenchLocale) {
return (await DOCUMENT_LOCALE_LOADERS[locale]()).default;
}

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/ar-SA';
import drawing from '@univerjs/preset-docs-drawing/locales/ar-SA';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/ar-SA';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/ar-SA';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/ca-ES';
import drawing from '@univerjs/preset-docs-drawing/locales/ca-ES';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/ca-ES';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/ca-ES';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/de-DE';
import drawing from '@univerjs/preset-docs-drawing/locales/de-DE';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/de-DE';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/de-DE';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/en-US';
import drawing from '@univerjs/preset-docs-drawing/locales/en-US';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/en-US';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/en-US';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/es-ES';
import drawing from '@univerjs/preset-docs-drawing/locales/es-ES';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/es-ES';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/es-ES';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/fa-IR';
import drawing from '@univerjs/preset-docs-drawing/locales/fa-IR';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/fa-IR';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/fa-IR';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/fr-FR';
import drawing from '@univerjs/preset-docs-drawing/locales/fr-FR';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/fr-FR';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/fr-FR';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/id-ID';
import drawing from '@univerjs/preset-docs-drawing/locales/id-ID';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/id-ID';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/id-ID';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/it-IT';
import drawing from '@univerjs/preset-docs-drawing/locales/it-IT';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/it-IT';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/it-IT';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/ja-JP';
import drawing from '@univerjs/preset-docs-drawing/locales/ja-JP';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/ja-JP';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/ja-JP';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/ko-KR';
import drawing from '@univerjs/preset-docs-drawing/locales/ko-KR';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/ko-KR';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/ko-KR';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/pl-PL';
import drawing from '@univerjs/preset-docs-drawing/locales/pl-PL';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/pl-PL';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/pl-PL';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/pt-BR';
import drawing from '@univerjs/preset-docs-drawing/locales/pt-BR';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/pt-BR';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/pt-BR';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/ru-RU';
import drawing from '@univerjs/preset-docs-drawing/locales/ru-RU';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/ru-RU';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/ru-RU';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/sk-SK';
import drawing from '@univerjs/preset-docs-drawing/locales/sk-SK';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/sk-SK';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/sk-SK';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/vi-VN';
import drawing from '@univerjs/preset-docs-drawing/locales/vi-VN';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/vi-VN';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/vi-VN';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/zh-CN';
import drawing from '@univerjs/preset-docs-drawing/locales/zh-CN';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/zh-CN';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/zh-CN';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/zh-HK';
import drawing from '@univerjs/preset-docs-drawing/locales/zh-HK';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/zh-HK';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/zh-HK';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import core from '@univerjs/preset-docs-core/locales/zh-TW';
import drawing from '@univerjs/preset-docs-drawing/locales/zh-TW';
import hyperLink from '@univerjs/preset-docs-hyper-link/locales/zh-TW';
import threadComment from '@univerjs/preset-docs-thread-comment/locales/zh-TW';
export default mergeLocales(
core,
drawing,
hyperLink,
threadComment
);

View File

@@ -0,0 +1,95 @@
import type { MountExample } from '../mount-example';
import type { IWorkbenchMountOptions } from '../workbench-settings';
import { UniverDocsLayoutWorkerPlugin } from '@univerjs/docs';
import { SetDocZoomRatioOperation } from '@univerjs/docs-ui';
import { UniverDocsCorePreset } from '@univerjs/preset-docs-core';
import { UniverDocsDrawingPreset } from '@univerjs/preset-docs-drawing';
import { UniverDocsHyperLinkPreset } from '@univerjs/preset-docs-hyper-link';
import { UniverDocsThreadCommentPreset } from '@univerjs/preset-docs-thread-comment';
import { createUniver, LocaleType, UniverInstanceType } from '@univerjs/presets';
import { loadDocumentLocale } from 'virtual:univer-examples-document-locale';
import { applyWorkbenchUIChrome, createMountedUniver } from '../mount-example';
import { getEffectiveWorkbenchRegion } from '../workbench-settings';
import { createDocumentFixture } from './create-document-fixture';
import '@univerjs/ui/facade';
import './global.css';
const UNIVER_LOCALES = {
[LocaleType.ZH_CN]: LocaleType.ZH_CN,
[LocaleType.EN_US]: LocaleType.EN_US,
[LocaleType.AR_SA]: LocaleType.AR_SA,
[LocaleType.FR_FR]: LocaleType.FR_FR,
[LocaleType.RU_RU]: LocaleType.RU_RU,
[LocaleType.ZH_TW]: LocaleType.ZH_TW,
[LocaleType.ZH_HK]: LocaleType.ZH_HK,
[LocaleType.VI_VN]: LocaleType.VI_VN,
[LocaleType.FA_IR]: LocaleType.FA_IR,
[LocaleType.JA_JP]: LocaleType.JA_JP,
[LocaleType.KO_KR]: LocaleType.KO_KR,
[LocaleType.ES_ES]: LocaleType.ES_ES,
[LocaleType.CA_ES]: LocaleType.CA_ES,
[LocaleType.SK_SK]: LocaleType.SK_SK,
[LocaleType.PT_BR]: LocaleType.PT_BR,
[LocaleType.DE_DE]: LocaleType.DE_DE,
[LocaleType.IT_IT]: LocaleType.IT_IT,
[LocaleType.ID_ID]: LocaleType.ID_ID,
[LocaleType.PL_PL]: LocaleType.PL_PL,
} satisfies Record<IWorkbenchMountOptions['locale'], LocaleType>;
export const mount: MountExample = async (host, options) => {
const locale = await loadDocumentLocale(options.locale);
const localeType = UNIVER_LOCALES[options.locale];
const { univer, univerAPI } = createUniver({
locale: localeType,
region: UNIVER_LOCALES[getEffectiveWorkbenchRegion(options)],
locales: {
[options.locale]: locale,
},
theme: options.theme,
darkMode: options.darkMode,
direction: options.direction,
presets: [
UniverDocsCorePreset({ container: host, ribbonType: options.ribbonType }),
UniverDocsDrawingPreset(),
UniverDocsHyperLinkPreset(),
UniverDocsThreadCommentPreset(),
],
});
univer.registerPlugin(UniverDocsLayoutWorkerPlugin, {
workerFactory: () => new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }),
});
try {
applyWorkbenchUIChrome(univerAPI, options.uiChrome);
univer.createUnit(
UniverInstanceType.UNIVER_DOC,
createDocumentFixture(localeType, options.direction, options.zoomRatio)
);
return createMountedUniver(
host,
univer,
univerAPI,
options,
loadDocumentLocale,
async (nextOptions) => {
const document = univerAPI.getActiveDocument();
if (!document) {
return;
}
const applied = await univerAPI.executeCommand(SetDocZoomRatioOperation.id, {
unitId: document.getId(),
zoomRatio: nextOptions.zoomRatio,
});
if (!applied) {
throw new Error(`Failed to apply zoom to document ${document.getId()}.`);
}
}
);
} catch (error) {
univer.dispose();
host.replaceChildren();
throw error;
}
};

View File

@@ -0,0 +1,3 @@
import { startDocsLayoutWorker } from '@univerjs/docs';
startDocsLayoutWorker();

View File

@@ -0,0 +1,47 @@
export interface IGeneratedSvg {
height: number;
source: string;
width: number;
}
interface ICreateGeneratedSvgOptions {
content: readonly string[];
height: number;
width: number;
}
export function createGeneratedSvg({ content, height, width }: ICreateGeneratedSvgOptions): IGeneratedSvg {
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
throw new RangeError('Generated SVG dimensions must be finite positive numbers.');
}
const markup = [
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
...content,
'</svg>',
].join('');
return {
height,
source: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(markup)}`,
width,
};
}
export function readGeneratedSvgSize(source: string): Pick<IGeneratedSvg, 'height' | 'width'> {
const separatorIndex = source.indexOf(',');
if (!source.startsWith('data:image/svg+xml') || separatorIndex < 0) {
throw new TypeError('Expected an SVG data URI.');
}
const markup = decodeURIComponent(source.slice(separatorIndex + 1));
const viewBox = /<svg\b[^>]*\bviewBox="0 0 ([\d.]+) ([\d.]+)"/.exec(markup);
if (!viewBox) {
throw new TypeError('Generated SVG must use a zero-origin numeric viewBox.');
}
return {
height: Number(viewBox[2]),
width: Number(viewBox[1]),
};
}

226
examples/src/global.css Normal file
View File

@@ -0,0 +1,226 @@
html,
body {
height: 100%;
margin: 0;
}
body {
display: flex;
overflow: hidden;
flex-direction: column;
color: #1f2329;
background: #fff;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
}
html.univer-dark body {
color: #f8fafc;
background: #111827;
}
.workbench-header {
--workbench-border: #e2e8f0;
--workbench-muted: #64748b;
--workbench-surface: #fff;
--workbench-surface-hover: #f8fafc;
--workbench-text: #1e293b;
z-index: 10;
isolation: isolate;
display: flex;
min-width: 0;
min-height: 36px;
flex: 0 0 auto;
align-items: center;
gap: 8px;
padding-inline: 12px;
border-bottom: 1px solid var(--workbench-border);
color: var(--workbench-text);
background: var(--workbench-surface);
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
font-size: 13px;
line-height: 1.25;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.workbench-header,
.workbench-header * {
box-sizing: border-box;
}
.workbench-primary,
.workbench-header nav {
display: flex;
align-items: center;
}
.workbench-primary {
min-width: 0;
min-height: 35px;
flex: 1 1 auto;
gap: 16px;
}
.workbench-brand {
display: flex;
width: 68px;
height: 20px;
align-items: center;
flex: 0 0 auto;
background: url('../../docs/img/banner-dark.png') center / contain no-repeat;
}
.workbench-header nav {
min-width: 0;
align-self: stretch;
gap: 2px;
}
.workbench-settings {
display: flex;
flex: 0 0 auto;
align-items: center;
}
.workbench-settings-trigger[data-state='open'] svg {
transform: rotate(180deg);
}
.workbench-settings-trigger svg {
transition: transform 120ms ease-out;
}
.workbench-settings-menu {
width: 240px;
}
.workbench-setting-label {
display: flex;
min-width: 184px;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.workbench-setting-value {
max-width: 112px;
overflow: hidden;
color: var(--univer-gray-400, #7d8698);
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
html.univer-dark .workbench-header {
--workbench-border: #334155;
--workbench-muted: #cbd5e1;
--workbench-surface: #111827;
--workbench-surface-hover: #1e293b;
--workbench-text: #f8fafc;
color-scheme: dark;
}
html.univer-dark .workbench-brand {
background-image: url('../../docs/img/banner-light.png');
}
.workbench-header a {
display: flex;
min-height: 35px;
align-items: center;
padding-inline: 10px;
border-radius: 4px 4px 0 0;
color: var(--workbench-muted);
font-size: 13px;
font-weight: 500;
line-height: 1;
text-decoration: none;
}
.workbench-header a:hover {
color: var(--workbench-text);
background: var(--workbench-surface-hover);
}
.workbench-header a[aria-current='page'] {
color: var(--workbench-accent, #274fee);
background: color-mix(in srgb, var(--workbench-accent, #274fee) 7%, transparent);
font-weight: 600;
box-shadow: inset 0 -2px var(--workbench-accent, #274fee);
}
.workbench-header a:focus-visible,
.workbench-settings-trigger:focus-visible {
outline: 2px solid var(--workbench-accent, #274fee);
outline-offset: -2px;
}
.workbench-status {
position: absolute;
z-index: 20;
top: 44px;
left: 50%;
padding: 8px 12px;
border: 1px solid #c7d2fe;
border-radius: 6px;
color: #312e81;
background: #eef2ff;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
font-size: 12px;
line-height: 1.25;
transform: translateX(-50%);
}
.workbench-status[data-error='true'] {
color: #991b1b;
background: #fee2e2;
}
#app {
isolation: isolate;
width: 100%;
min-width: 0;
min-height: 0;
flex: 1;
}
@media (max-width: 680px) {
.workbench-header {
padding-inline: 8px;
}
.workbench-primary {
gap: 8px;
}
.workbench-header nav {
gap: 0;
}
.workbench-header a {
padding-inline: 8px;
}
}

238
examples/src/main.ts Normal file
View File

@@ -0,0 +1,238 @@
import type { ExampleLoader } from './mount-example';
import type {
WorkbenchDirection,
WorkbenchLocale,
WorkbenchRegion,
WorkbenchRibbonType,
WorkbenchTheme,
WorkbenchUIChromeMode,
WorkbenchZoomRatio,
} from './workbench-settings';
import { createExampleSwitcher } from './mount-example';
import {
clearWorkbenchSettings,
getDefaultWorkbenchSettings,
getWorkbenchTheme,
readWorkbenchSettings,
WORKBENCH_LOCALE_META,
writeWorkbenchSettings,
} from './workbench-settings';
import { renderWorkbenchSettingsControl, unmountWorkbenchSettingsControl } from './workbench-settings-control';
import './global.css';
const loaders = {
docs: () => import('./docs/mount'),
sheets: () => import('./sheets/mount'),
slides: () => import('./slides/mount'),
} satisfies Record<string, ExampleLoader>;
type ExampleName = keyof typeof loaders;
function getRequiredElement<T extends HTMLElement>(selector: string): T {
const element = document.querySelector<T>(selector);
if (!element) {
throw new Error(`Workbench element ${selector} was not found.`);
}
return element;
}
const host = getRequiredElement<HTMLElement>('#app');
const status = getRequiredElement<HTMLElement>('#workbench-status');
const links = [...document.querySelectorAll<HTMLAnchorElement>('[data-example]')];
const settingsContainer = getRequiredElement<HTMLElement>('#workbench-settings');
const switcher = createExampleSwitcher(host, loaders);
let navigation = 0;
let settings = readWorkbenchSettings(localStorage);
let appliedSettings = settings;
let settingsUpdate = 0;
let appliedSettingsUpdate = 0;
let disposed = false;
if (location.search) {
const url = new URL(location.href);
url.search = '';
history.replaceState(null, '', url);
}
function getExampleName(): ExampleName {
const name = location.hash.slice(1);
if (Object.hasOwn(loaders, name)) {
return name as ExampleName;
}
history.replaceState(null, '', '#sheets');
return 'sheets';
}
function showError(error: unknown) {
status.hidden = false;
status.dataset.error = 'true';
status.textContent = error instanceof Error ? error.message : String(error);
}
function getMountOptions(currentSettings = settings) {
return {
...currentSettings,
theme: getWorkbenchTheme(currentSettings.theme),
};
}
function renderWorkbenchSettings(currentSettings = settings) {
const theme = getWorkbenchTheme(currentSettings.theme);
document.documentElement.lang = WORKBENCH_LOCALE_META[currentSettings.locale].tag;
document.documentElement.dir = currentSettings.direction;
document.documentElement.dataset.theme = currentSettings.theme;
document.documentElement.classList.toggle('univer-dark', currentSettings.darkMode);
document.documentElement.style.setProperty('--workbench-accent', theme.primary[600]);
renderWorkbenchSettingsControl(settingsContainer, {
settings: currentSettings,
onLocaleChange,
onRegionChange,
onDirectionChange,
onThemeChange,
onDarkModeChange,
onRibbonTypeChange,
onUIChromeChange,
onZoomRatioChange,
onResetPreferences,
});
}
async function openCurrentExample() {
const request = ++navigation;
const name = getExampleName();
renderWorkbenchSettings();
for (const link of links) {
if (link.dataset.example === name) {
link.setAttribute('aria-current', 'page');
} else {
link.removeAttribute('aria-current');
}
}
document.title = `Univer ${name[0].toUpperCase()}${name.slice(1)} Workbench`;
host.setAttribute('aria-busy', 'true');
status.hidden = false;
delete status.dataset.error;
status.textContent = `Loading ${name}…`;
try {
await switcher.open(name, getMountOptions());
} catch (error) {
if (request === navigation) {
host.removeAttribute('aria-busy');
showError(error);
}
return;
}
if (request === navigation) {
host.removeAttribute('aria-busy');
status.hidden = true;
}
}
const onLocationChange = () => {
openCurrentExample().catch((error) => {
host.removeAttribute('aria-busy');
showError(error);
});
};
async function updateWorkbenchSettings(
nextSettings: typeof settings,
persistSettings: (settings: typeof appliedSettings) => void = (settings) => {
writeWorkbenchSettings(localStorage, settings);
}
) {
const request = ++settingsUpdate;
settings = nextSettings;
try {
await switcher.updateSettings(getMountOptions(nextSettings));
} catch (error) {
if (!disposed && request === settingsUpdate) {
settings = appliedSettings;
renderWorkbenchSettings(appliedSettings);
showError(error);
}
return;
}
if (!disposed && request > appliedSettingsUpdate) {
appliedSettingsUpdate = request;
appliedSettings = nextSettings;
let persistenceError: unknown;
try {
persistSettings(appliedSettings);
} catch (error) {
persistenceError = error;
}
if (request === settingsUpdate) {
renderWorkbenchSettings(appliedSettings);
if (persistenceError) {
const detail = persistenceError instanceof Error ? ` ${persistenceError.message}` : '';
showError(new Error(`Preferences were applied, but could not be saved.${detail}`));
}
}
}
}
async function onLocaleChange(locale: WorkbenchLocale) {
await updateWorkbenchSettings({
...settings,
locale,
direction: WORKBENCH_LOCALE_META[locale].direction,
});
}
async function onDirectionChange(direction: WorkbenchDirection) {
await updateWorkbenchSettings({ ...settings, direction });
}
async function onRegionChange(region: WorkbenchRegion) {
await updateWorkbenchSettings({ ...settings, region });
}
async function onThemeChange(theme: WorkbenchTheme) {
await updateWorkbenchSettings({ ...settings, theme });
}
async function onDarkModeChange(darkMode: boolean) {
await updateWorkbenchSettings({ ...settings, darkMode });
}
async function onRibbonTypeChange(ribbonType: WorkbenchRibbonType) {
await updateWorkbenchSettings({ ...settings, ribbonType });
}
async function onUIChromeChange(uiChrome: WorkbenchUIChromeMode) {
await updateWorkbenchSettings({ ...settings, uiChrome });
}
async function onZoomRatioChange(zoomRatio: WorkbenchZoomRatio) {
await updateWorkbenchSettings({ ...settings, zoomRatio });
}
async function onResetPreferences() {
await updateWorkbenchSettings(getDefaultWorkbenchSettings(), () => {
clearWorkbenchSettings(localStorage);
});
}
window.addEventListener('hashchange', onLocationChange);
onLocationChange();
import.meta.hot?.dispose(() => {
disposed = true;
navigation += 1;
settingsUpdate += 1;
appliedSettingsUpdate = settingsUpdate;
settings = appliedSettings;
window.removeEventListener('hashchange', onLocationChange);
unmountWorkbenchSettingsControl(settingsContainer);
switcher.dispose();
});

View File

@@ -0,0 +1,258 @@
import type { Univer } from '@univerjs/core';
import type { FUniver } from '@univerjs/core/facade';
import type { IWorkbenchMountOptions, WorkbenchLocale, WorkbenchUIChromeMode } from './workbench-settings';
import { getEffectiveWorkbenchRegion, getWorkbenchUIChromeVisibility } from './workbench-settings';
export type DisposeExample = () => void;
export type LoadExampleLocale = (
locale: WorkbenchLocale
) => Promise<Parameters<FUniver['loadLocales']>[1]>;
export type ApplyExampleProductSettings = (options: IWorkbenchMountOptions) => void | Promise<void>;
export interface IMountedExample {
dispose: DisposeExample;
updateSettings: (options: IWorkbenchMountOptions) => Promise<void>;
}
export type MountExample = (
host: HTMLElement,
options: IWorkbenchMountOptions
) => IMountedExample | Promise<IMountedExample>;
export type ExampleLoader = () => Promise<{ mount: MountExample }>;
interface IWorkbenchWindow {
univer?: Univer;
univerAPI?: FUniver;
}
interface IAppliedWorkbenchSettings {
locale: WorkbenchLocale;
region: WorkbenchLocale;
ribbonType: IWorkbenchMountOptions['ribbonType'];
uiChrome: WorkbenchUIChromeMode;
zoomRatio: IWorkbenchMountOptions['zoomRatio'];
}
interface IPendingExampleLocale {
locale: WorkbenchLocale;
pack: Awaited<ReturnType<LoadExampleLocale>>;
}
export function createUniverDisposer(host: HTMLElement, univer: Univer, univerAPI?: FUniver): DisposeExample {
let disposed = false;
const workbenchWindow = window as typeof window & IWorkbenchWindow;
workbenchWindow.univer = univer;
workbenchWindow.univerAPI = univerAPI;
return () => {
if (disposed) {
return;
}
disposed = true;
univer.dispose();
host.replaceChildren();
if (workbenchWindow.univer === univer) {
Reflect.deleteProperty(workbenchWindow, 'univer');
}
if (workbenchWindow.univerAPI === univerAPI) {
Reflect.deleteProperty(workbenchWindow, 'univerAPI');
}
};
}
export function applyWorkbenchUIChrome(univerAPI: FUniver, mode: WorkbenchUIChromeMode): void {
const visibility = getWorkbenchUIChromeVisibility(mode);
const { FOOTER, HEADER, LEFT_SIDEBAR, TOOLBAR } = univerAPI.Enum.BuiltInUIPart;
univerAPI.setUIVisible(HEADER, visibility.header);
univerAPI.setUIVisible(TOOLBAR, visibility.toolbar);
univerAPI.setUIVisible(FOOTER, visibility.footer);
univerAPI.setUIVisible(LEFT_SIDEBAR, visibility.leftSidebar);
}
function commitWorkbenchSettings(
univerAPI: FUniver,
applied: IAppliedWorkbenchSettings,
options: IWorkbenchMountOptions,
pendingLocale?: IPendingExampleLocale
): void {
if (pendingLocale) {
univerAPI.loadLocales(pendingLocale.locale, pendingLocale.pack);
univerAPI.setLocale(pendingLocale.locale);
applied.locale = pendingLocale.locale;
}
const nextRegion = getEffectiveWorkbenchRegion(options);
if (nextRegion !== applied.region) {
univerAPI.setRegion(nextRegion);
applied.region = nextRegion;
}
if (options.ribbonType !== applied.ribbonType) {
univerAPI.setRibbonType(options.ribbonType);
applied.ribbonType = options.ribbonType;
}
if (options.uiChrome !== applied.uiChrome) {
applyWorkbenchUIChrome(univerAPI, options.uiChrome);
applied.uiChrome = options.uiChrome;
}
univerAPI.setDirection(options.direction);
univerAPI.setTheme(options.theme);
univerAPI.toggleDarkMode(options.darkMode);
}
export function createMountedUniver(
host: HTMLElement,
univer: Univer,
univerAPI: FUniver,
initialSettings: IWorkbenchMountOptions,
loadLocale: LoadExampleLocale,
applyProductSettings?: ApplyExampleProductSettings
): IMountedExample {
let disposed = false;
const appliedSettings: IAppliedWorkbenchSettings = {
locale: initialSettings.locale,
region: getEffectiveWorkbenchRegion(initialSettings),
ribbonType: initialSettings.ribbonType,
uiChrome: initialSettings.uiChrome,
zoomRatio: initialSettings.zoomRatio,
};
const disposeUniver = createUniverDisposer(host, univer, univerAPI);
return {
dispose() {
disposed = true;
disposeUniver();
},
async updateSettings(options) {
if (disposed) {
return;
}
let pendingLocale: IPendingExampleLocale | undefined;
if (options.locale !== appliedSettings.locale) {
let localePack: Awaited<ReturnType<LoadExampleLocale>>;
try {
localePack = await loadLocale(options.locale);
} catch (error) {
if (disposed) {
return;
}
throw error;
}
if (disposed) {
return;
}
pendingLocale = { locale: options.locale, pack: localePack };
}
if (disposed) {
return;
}
if (options.zoomRatio !== appliedSettings.zoomRatio) {
try {
await applyProductSettings?.(options);
} catch (error) {
if (disposed) {
return;
}
throw error;
}
if (disposed) {
return;
}
appliedSettings.zoomRatio = options.zoomRatio;
}
commitWorkbenchSettings(univerAPI, appliedSettings, options, pendingLocale);
},
};
}
export function createExampleSwitcher<T extends string>(host: HTMLElement, loaders: Record<T, ExampleLoader>) {
let generation = 0;
let current: IMountedExample | undefined;
let latestSettings: IWorkbenchMountOptions | undefined;
let mountQueue = Promise.resolve();
let pendingOpen: Promise<void> | undefined;
let updateQueue = Promise.resolve();
function enqueueSettingsUpdate(mounted: IMountedExample, options: IWorkbenchMountOptions) {
const updateTask = updateQueue.then(() => mounted.updateSettings(options));
updateQueue = updateTask.catch(() => undefined);
return updateTask;
}
return {
async open(target: T, options: IWorkbenchMountOptions) {
const request = ++generation;
latestSettings = options;
const previous = current;
current = undefined;
previous?.dispose();
const openTask = (async () => {
try {
const { mount } = await loaders[target]();
if (request !== generation) {
return;
}
const mountTask = mountQueue.then(async () => {
if (request !== generation) {
return;
}
const mounted = await mount(host, options);
if (request === generation) {
current = mounted;
if (latestSettings && latestSettings !== options) {
await enqueueSettingsUpdate(mounted, latestSettings);
}
} else {
mounted.dispose();
}
});
mountQueue = mountTask.catch(() => undefined);
await mountTask;
} catch (error) {
if (request === generation) {
throw error;
}
}
})();
pendingOpen = openTask;
try {
await openTask;
} finally {
if (pendingOpen === openTask) {
pendingOpen = undefined;
}
}
},
async updateSettings(options: IWorkbenchMountOptions) {
latestSettings = options;
const mounted = current;
if (!mounted) {
await pendingOpen;
return;
}
await enqueueSettingsUpdate(mounted, options);
},
dispose() {
generation += 1;
const mounted = current;
current = undefined;
mounted?.dispose();
},
};
}

View File

@@ -0,0 +1,66 @@
import type { FUniver } from '@univerjs/core/facade';
import type { IWorkbenchMountOptions } from '../../workbench-settings';
import { describe, expect, it, vi } from 'vitest';
import { applyWorkbookZoom } from '../apply-workbook-zoom';
describe('applyWorkbookZoom', () => {
it('continues a best-effort rollback after apply and rollback commands reject', async () => {
const applyCause = new Error('apply command rejected');
const rollbackCause = new Error('rollback command rejected');
const executeCommand = vi.fn()
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(true)
.mockRejectedValueOnce(applyCause)
.mockResolvedValueOnce(true)
.mockRejectedValueOnce(rollbackCause)
.mockResolvedValueOnce(false);
const worksheets = [
{ getSheetId: () => 'sheet-a', getZoom: () => 1 },
{ getSheetId: () => 'sheet-b', getZoom: () => 1.1 },
{ getSheetId: () => 'sheet-c', getZoom: () => 0.9 },
];
const univerAPI = {
executeCommand,
getActiveWorkbook: () => ({
getId: () => 'workbook-01',
getSheets: () => worksheets,
}),
} as unknown as FUniver;
const options = { zoomRatio: 1.5 } as IWorkbenchMountOptions;
let thrownError: unknown;
try {
await applyWorkbookZoom(univerAPI, options);
} catch (error) {
thrownError = error;
}
expect(executeCommand.mock.calls.map(([, params]) => params)).toEqual([
{ unitId: 'workbook-01', subUnitId: 'sheet-a', zoomRatio: 1.5 },
{ unitId: 'workbook-01', subUnitId: 'sheet-b', zoomRatio: 1.5 },
{ unitId: 'workbook-01', subUnitId: 'sheet-c', zoomRatio: 1.5 },
{ unitId: 'workbook-01', subUnitId: 'sheet-c', zoomRatio: 0.9 },
{ unitId: 'workbook-01', subUnitId: 'sheet-b', zoomRatio: 1.1 },
{ unitId: 'workbook-01', subUnitId: 'sheet-a', zoomRatio: 1 },
]);
expect(thrownError).toBeInstanceOf(AggregateError);
const aggregateError = thrownError as AggregateError;
expect(aggregateError.message).toBe(
'Failed to apply worksheet zoom and fully restore the previous zoom state.'
);
expect(aggregateError.errors).toHaveLength(3);
expect(aggregateError.errors[0]).toMatchObject({
cause: applyCause,
message: 'Failed to apply zoom to worksheet sheet-c.',
});
expect(aggregateError.errors[1]).toMatchObject({
cause: rollbackCause,
message: 'Failed to restore zoom for worksheet sheet-b.',
});
expect(aggregateError.errors[2]).toMatchObject({
message: 'Failed to restore zoom for worksheet sheet-a.',
});
});
});

View File

@@ -0,0 +1,243 @@
import type { ICellData, IDocumentBody, IWorkbookData, IWorksheetData } from '@univerjs/core';
import { BooleanNumber, CustomRangeType, LocaleType } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { readGeneratedSvgSize } from '../../generated-svg';
import { createSheetFixture } from '../create-sheet-fixture';
interface ISheetDrawingAnchorPoint {
column: number;
columnOffset: number;
row: number;
rowOffset: number;
}
interface ISheetDrawingFixture {
sheetTransform: {
from: ISheetDrawingAnchorPoint;
to: ISheetDrawingAnchorPoint;
};
source: string;
transform: {
height: number;
width: number;
};
}
const PRESET_SAMPLES = [
['core', 'Core'],
['conditional-formatting', 'Conditional Formatting'],
['data-validation', 'Data Validation'],
['drawing', 'Drawing'],
['filter', 'Filter'],
['find-replace', 'Find & Replace'],
['hyper-link', 'Hyperlink'],
['note', 'Note'],
['sort', 'Sort'],
['table', 'Table'],
['thread-comment', 'Thread Comment'],
] as const;
function parseResource(fixture: IWorkbookData, name: string): unknown {
const resource = fixture.resources?.find((item) => item.name === name);
if (!resource) {
throw new Error(`Missing fixture resource: ${name}`);
}
return JSON.parse(resource.data) as unknown;
}
function collectCells(fixture: IWorkbookData, sheetId: string): ICellData[] {
const rows = Object.values(fixture.sheets[sheetId].cellData ?? {});
return rows.flatMap((row) => Object.values(row ?? {}) as Array<ICellData | null>).filter(
(cell): cell is ICellData => cell !== null
);
}
function getColumnPosition(sheet: Partial<IWorksheetData>, columnIndex: number): number {
if (sheet.defaultColumnWidth == null) {
throw new Error('Drawing fixture must define a default column width.');
}
let position = 0;
for (let column = 0; column < columnIndex; column++) {
position += sheet.columnData?.[column]?.w ?? sheet.defaultColumnWidth;
}
return position;
}
function getRowPosition(sheet: Partial<IWorksheetData>, rowIndex: number): number {
if (sheet.defaultRowHeight == null) {
throw new Error('Drawing fixture must define a default row height.');
}
let position = 0;
for (let row = 0; row < rowIndex; row++) {
position += sheet.rowData?.[row]?.h ?? sheet.defaultRowHeight;
}
return position;
}
function getDrawingAnchorSize(sheet: Partial<IWorksheetData>, drawing: ISheetDrawingFixture) {
const { from, to } = drawing.sheetTransform;
return {
height: getRowPosition(sheet, to.row) + to.rowOffset - getRowPosition(sheet, from.row) - from.rowOffset,
width: getColumnPosition(sheet, to.column) + to.columnOffset
- getColumnPosition(sheet, from.column) - from.columnOffset,
};
}
describe('createSheetFixture', () => {
it('creates one deterministic, discoverable sample for every registered preset', () => {
const first = createSheetFixture(LocaleType.AR_SA, 'rtl');
const second = createSheetFixture(LocaleType.AR_SA, 'rtl');
const expectedIds = PRESET_SAMPLES.map(([id]) => id);
expect(first.sheetOrder).toEqual(expectedIds);
expect(Object.keys(first.sheets)).toEqual(expectedIds);
expect(PRESET_SAMPLES.map(([id]) => [id, first.sheets[id].name])).toEqual(PRESET_SAMPLES);
expect(Object.values(first.sheets).every(({ rightToLeft }) => rightToLeft === BooleanNumber.TRUE)).toBe(true);
expect(Object.values(first.sheets).every(({ tabColor }) => Boolean(tabColor))).toBe(true);
expect(PRESET_SAMPLES.every(([id]) => {
const guide = first.sheets[id].cellData?.[1]?.[0]?.v;
return typeof guide === 'string'
&& guide.length > 20
&& /edit|open|move|find|select|use|hover/i.test(guide);
})).toBe(true);
expect(first.sheets).not.toBe(second.sheets);
expect(JSON.stringify(first)).toBe(JSON.stringify(second));
expect(JSON.stringify(first).length).toBeLessThan(80_000);
expect(JSON.stringify(first)).not.toMatch(/collaboration/i);
});
it('applies the selected default zoom to every sample sheet', () => {
const defaultFixture = createSheetFixture(LocaleType.EN_US, 'ltr');
const zoomedFixture = createSheetFixture(LocaleType.EN_US, 'ltr', 1.25);
expect(Object.values(defaultFixture.sheets).every(({ zoomRatio }) => zoomRatio === 1)).toBe(true);
expect(Object.values(zoomedFixture.sheets).every(({ zoomRatio }) => zoomRatio === 1.25)).toBe(true);
});
it('provides real core and command-preset interaction targets', () => {
const fixture = createSheetFixture(LocaleType.EN_US, 'ltr');
const core = fixture.sheets.core;
const formulas = collectCells(fixture, 'core').flatMap(({ f }) => f ? [f] : []);
const formulaFunctions = new Set(formulas.flatMap((formula) => formula.match(/[A-Z]+(?=\()/g) ?? []));
const findTargets = collectCells(fixture, 'find-replace')
.map(({ v }) => v)
.filter((value): value is string => typeof value === 'string' && /workbench/i.test(value));
const hyperlink = fixture.sheets['hyper-link'].cellData?.[4]?.[1];
const sortOrder = [4, 5, 6, 7, 8].map((row) => fixture.sheets.sort.cellData?.[row]?.[0]?.v);
expect([...formulaFunctions]).toEqual(expect.arrayContaining([
'SUM',
'SUMIFS',
'IF',
'COUNTIF',
'XLOOKUP',
'TEXTJOIN',
'WORKDAY',
'DATE',
'IFERROR',
'SEQUENCE',
'FILTER',
]));
expect(formulas.some((formula) => formula.includes('TableRevenue'))).toBe(true);
expect(formulas.some((formula) => formula.includes("'Table'!"))).toBe(true);
expect(core.cellData?.[21]?.[5]?.p?.body?.textRuns).toHaveLength(4);
expect(core.mergeData).toContainEqual({ startRow: 25, endRow: 25, startColumn: 0, endColumn: 7 });
expect(core.rowData?.[29]?.hd).toBe(BooleanNumber.TRUE);
expect(core.columnData?.[10]?.hd).toBe(BooleanNumber.TRUE);
expect(core.freeze).toEqual({ xSplit: 0, ySplit: 3, startColumn: 0, startRow: 3 });
expect(core.showGridlines).toBe(BooleanNumber.FALSE);
expect(Object.values(fixture.styles ?? {}).filter((style) => style?.n).length).toBeGreaterThanOrEqual(9);
expect(core.cellData?.[18]?.[5]?.s).toBe('wrapped');
expect(core.cellData?.[18]?.[6]?.s).toBe('rotated');
expect(core.cellData?.[21]?.[3]?.s).toBe('borderDiagonal');
expect(findTargets).toHaveLength(5);
expect(hyperlink?.p?.body?.customRanges?.[0]).toMatchObject({
rangeType: CustomRangeType.HYPERLINK,
properties: { url: 'https://docs.univer.ai' },
});
expect(sortOrder).toEqual(['Gamma', 'Alpha', 'Zeta', 'Beta', 'Delta']);
});
it('binds every resource-backed preset exclusively to its matching sample tab', () => {
const fixture = createSheetFixture(LocaleType.EN_US, 'ltr');
const validation = parseResource(fixture, 'SHEET_DATA_VALIDATION_PLUGIN') as Record<
string,
Array<{ type: string }>
>;
const conditionalFormatting = parseResource(fixture, 'SHEET_CONDITIONAL_FORMATTING_PLUGIN') as Record<
string,
Array<{ rule: { subType?: string; type: string } }>
>;
const drawings = parseResource(fixture, 'SHEET_DRAWING_PLUGIN') as Record<
string,
{ data: Record<string, ISheetDrawingFixture>; order: string[] }
>;
const comments = parseResource(fixture, 'SHEET_UNIVER_THREAD_COMMENT_PLUGIN') as Record<
string,
Array<{ children: Array<{ text: IDocumentBody }>; text: IDocumentBody }>
>;
const filters = parseResource(fixture, 'SHEET_FILTER_PLUGIN') as Record<
string,
{ ref: { endColumn: number; endRow: number; startColumn: number; startRow: number } }
>;
const notes = parseResource(fixture, 'SHEET_NOTE_PLUGIN') as Record<string, unknown>;
const tables = parseResource(fixture, 'SHEET_TABLE_PLUGIN') as Record<
string,
{ tables: Array<{ range: { endColumn: number; endRow: number; startColumn: number; startRow: number } }> }
>;
expect(Object.keys(validation)).toEqual(['data-validation']);
expect(Object.keys(conditionalFormatting)).toEqual(['conditional-formatting']);
expect(Object.keys(drawings)).toEqual(['drawing']);
expect(Object.keys(filters)).toEqual(['filter']);
expect(Object.keys(notes)).toEqual(['note']);
expect(Object.keys(tables)).toEqual(['table']);
expect(Object.keys(comments)).toEqual(['thread-comment']);
expect(new Set(validation['data-validation'].map(({ type }) => type))).toEqual(new Set([
'list',
'textLength',
'whole',
'decimal',
'custom',
'date',
'checkbox',
'listMultiple',
]));
expect(conditionalFormatting['conditional-formatting'].map(({ rule }) => rule.type)).toEqual(expect.arrayContaining([
'dataBar',
'colorScale',
'iconSet',
'highlightCell',
]));
expect(conditionalFormatting['conditional-formatting'].map(({ rule }) => rule.subType)).toEqual(expect.arrayContaining([
'text',
'duplicateValues',
'rank',
]));
expect(filters.filter.ref).toEqual({ startRow: 3, endRow: 9, startColumn: 0, endColumn: 12 });
expect(tables.table.tables[0].range).toEqual({ startRow: 3, endRow: 9, startColumn: 0, endColumn: 12 });
expect(drawings.drawing.order).toHaveLength(2);
expect(Object.values(drawings.drawing.data).every(({ source }) => source.startsWith('data:image/svg+xml')))
.toBe(true);
Object.values(drawings.drawing.data).forEach((drawing) => {
const intrinsicSize = readGeneratedSvgSize(drawing.source);
const anchorSize = getDrawingAnchorSize(fixture.sheets.drawing, drawing);
expect(drawing.transform.width / drawing.transform.height).toBeCloseTo(
intrinsicSize.width / intrinsicSize.height,
8
);
expect(anchorSize).toEqual(intrinsicSize);
});
expect(comments['thread-comment'][0].children).toHaveLength(1);
expect([
comments['thread-comment'][0],
...comments['thread-comment'][0].children,
].every(({ text }) => text.paragraphs?.some(({ startIndex }) => (
text.dataStream.slice(0, startIndex).replaceAll('\r', '').trim()
)) === true)).toBe(true);
});
});

View File

@@ -0,0 +1,72 @@
import type { FUniver } from '@univerjs/core/facade';
import type { IWorkbenchMountOptions } from '../workbench-settings';
import { SetZoomRatioOperation } from '@univerjs/preset-sheets-core';
export async function applyWorkbookZoom(univerAPI: FUniver, options: IWorkbenchMountOptions) {
const workbook = univerAPI.getActiveWorkbook();
if (!workbook) {
return;
}
const worksheetZoomStates = workbook.getSheets().map((worksheet) => ({
worksheetId: worksheet.getSheetId(),
zoomRatio: worksheet.getZoom(),
}));
const attemptedWorksheetZoomStates: typeof worksheetZoomStates = [];
let applyError: Error | undefined;
for (const worksheetZoomState of worksheetZoomStates) {
attemptedWorksheetZoomStates.push(worksheetZoomState);
try {
const applied = await univerAPI.executeCommand(SetZoomRatioOperation.id, {
unitId: workbook.getId(),
subUnitId: worksheetZoomState.worksheetId,
zoomRatio: options.zoomRatio,
});
if (!applied) {
applyError = new Error(`Failed to apply zoom to worksheet ${worksheetZoomState.worksheetId}.`);
break;
}
} catch (error) {
applyError = new Error(`Failed to apply zoom to worksheet ${worksheetZoomState.worksheetId}.`, {
cause: error,
});
break;
}
}
if (!applyError) {
return;
}
const rollbackErrors: Error[] = [];
for (const worksheetZoomState of [...attemptedWorksheetZoomStates].reverse()) {
try {
const restored = await univerAPI.executeCommand(SetZoomRatioOperation.id, {
unitId: workbook.getId(),
subUnitId: worksheetZoomState.worksheetId,
zoomRatio: worksheetZoomState.zoomRatio,
});
if (!restored) {
rollbackErrors.push(new Error(
`Failed to restore zoom for worksheet ${worksheetZoomState.worksheetId}.`
));
}
} catch (error) {
rollbackErrors.push(new Error(
`Failed to restore zoom for worksheet ${worksheetZoomState.worksheetId}.`,
{ cause: error }
));
}
}
if (rollbackErrors.length > 0) {
throw new AggregateError(
[applyError, ...rollbackErrors],
'Failed to apply worksheet zoom and fully restore the previous zoom state.'
);
}
throw applyError;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
@config "../../tailwind.sheets.config.ts";
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,6 @@
import type { WorkbenchLocale } from '../workbench-settings';
import { loadDevLocale } from '../dev-locale-loader';
export function loadSheetLocale(locale: WorkbenchLocale) {
return loadDevLocale('sheets', locale);
}

View File

@@ -0,0 +1,33 @@
import type { WorkbenchLocale } from '../workbench-settings';
interface ILocaleModule {
default: Record<string, unknown>;
}
type LocaleLoader = () => Promise<ILocaleModule>;
const SHEET_LOCALE_LOADERS = {
enUS: () => import('./locales/en-US'),
frFR: () => import('./locales/fr-FR'),
zhCN: () => import('./locales/zh-CN'),
ruRU: () => import('./locales/ru-RU'),
zhTW: () => import('./locales/zh-TW'),
zhHK: () => import('./locales/zh-HK'),
viVN: () => import('./locales/vi-VN'),
faIR: () => import('./locales/fa-IR'),
jaJP: () => import('./locales/ja-JP'),
koKR: () => import('./locales/ko-KR'),
esES: () => import('./locales/es-ES'),
caES: () => import('./locales/ca-ES'),
skSK: () => import('./locales/sk-SK'),
ptBR: () => import('./locales/pt-BR'),
deDE: () => import('./locales/de-DE'),
itIT: () => import('./locales/it-IT'),
idID: () => import('./locales/id-ID'),
plPL: () => import('./locales/pl-PL'),
arSA: () => import('./locales/ar-SA'),
} satisfies Record<WorkbenchLocale, LocaleLoader>;
export async function loadSheetLocale(locale: WorkbenchLocale) {
return (await SHEET_LOCALE_LOADERS[locale]()).default;
}

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/ar-SA';
import core from '@univerjs/preset-sheets-core/locales/ar-SA';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/ar-SA';
import drawing from '@univerjs/preset-sheets-drawing/locales/ar-SA';
import filter from '@univerjs/preset-sheets-filter/locales/ar-SA';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/ar-SA';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/ar-SA';
import note from '@univerjs/preset-sheets-note/locales/ar-SA';
import sort from '@univerjs/preset-sheets-sort/locales/ar-SA';
import table from '@univerjs/preset-sheets-table/locales/ar-SA';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/ar-SA';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/ca-ES';
import core from '@univerjs/preset-sheets-core/locales/ca-ES';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/ca-ES';
import drawing from '@univerjs/preset-sheets-drawing/locales/ca-ES';
import filter from '@univerjs/preset-sheets-filter/locales/ca-ES';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/ca-ES';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/ca-ES';
import note from '@univerjs/preset-sheets-note/locales/ca-ES';
import sort from '@univerjs/preset-sheets-sort/locales/ca-ES';
import table from '@univerjs/preset-sheets-table/locales/ca-ES';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/ca-ES';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/de-DE';
import core from '@univerjs/preset-sheets-core/locales/de-DE';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/de-DE';
import drawing from '@univerjs/preset-sheets-drawing/locales/de-DE';
import filter from '@univerjs/preset-sheets-filter/locales/de-DE';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/de-DE';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/de-DE';
import note from '@univerjs/preset-sheets-note/locales/de-DE';
import sort from '@univerjs/preset-sheets-sort/locales/de-DE';
import table from '@univerjs/preset-sheets-table/locales/de-DE';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/de-DE';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/en-US';
import core from '@univerjs/preset-sheets-core/locales/en-US';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/en-US';
import drawing from '@univerjs/preset-sheets-drawing/locales/en-US';
import filter from '@univerjs/preset-sheets-filter/locales/en-US';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/en-US';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/en-US';
import note from '@univerjs/preset-sheets-note/locales/en-US';
import sort from '@univerjs/preset-sheets-sort/locales/en-US';
import table from '@univerjs/preset-sheets-table/locales/en-US';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/en-US';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/es-ES';
import core from '@univerjs/preset-sheets-core/locales/es-ES';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/es-ES';
import drawing from '@univerjs/preset-sheets-drawing/locales/es-ES';
import filter from '@univerjs/preset-sheets-filter/locales/es-ES';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/es-ES';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/es-ES';
import note from '@univerjs/preset-sheets-note/locales/es-ES';
import sort from '@univerjs/preset-sheets-sort/locales/es-ES';
import table from '@univerjs/preset-sheets-table/locales/es-ES';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/es-ES';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/fa-IR';
import core from '@univerjs/preset-sheets-core/locales/fa-IR';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/fa-IR';
import drawing from '@univerjs/preset-sheets-drawing/locales/fa-IR';
import filter from '@univerjs/preset-sheets-filter/locales/fa-IR';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/fa-IR';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/fa-IR';
import note from '@univerjs/preset-sheets-note/locales/fa-IR';
import sort from '@univerjs/preset-sheets-sort/locales/fa-IR';
import table from '@univerjs/preset-sheets-table/locales/fa-IR';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/fa-IR';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/fr-FR';
import core from '@univerjs/preset-sheets-core/locales/fr-FR';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/fr-FR';
import drawing from '@univerjs/preset-sheets-drawing/locales/fr-FR';
import filter from '@univerjs/preset-sheets-filter/locales/fr-FR';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/fr-FR';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/fr-FR';
import note from '@univerjs/preset-sheets-note/locales/fr-FR';
import sort from '@univerjs/preset-sheets-sort/locales/fr-FR';
import table from '@univerjs/preset-sheets-table/locales/fr-FR';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/fr-FR';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/id-ID';
import core from '@univerjs/preset-sheets-core/locales/id-ID';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/id-ID';
import drawing from '@univerjs/preset-sheets-drawing/locales/id-ID';
import filter from '@univerjs/preset-sheets-filter/locales/id-ID';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/id-ID';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/id-ID';
import note from '@univerjs/preset-sheets-note/locales/id-ID';
import sort from '@univerjs/preset-sheets-sort/locales/id-ID';
import table from '@univerjs/preset-sheets-table/locales/id-ID';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/id-ID';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/it-IT';
import core from '@univerjs/preset-sheets-core/locales/it-IT';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/it-IT';
import drawing from '@univerjs/preset-sheets-drawing/locales/it-IT';
import filter from '@univerjs/preset-sheets-filter/locales/it-IT';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/it-IT';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/it-IT';
import note from '@univerjs/preset-sheets-note/locales/it-IT';
import sort from '@univerjs/preset-sheets-sort/locales/it-IT';
import table from '@univerjs/preset-sheets-table/locales/it-IT';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/it-IT';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/ja-JP';
import core from '@univerjs/preset-sheets-core/locales/ja-JP';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/ja-JP';
import drawing from '@univerjs/preset-sheets-drawing/locales/ja-JP';
import filter from '@univerjs/preset-sheets-filter/locales/ja-JP';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/ja-JP';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/ja-JP';
import note from '@univerjs/preset-sheets-note/locales/ja-JP';
import sort from '@univerjs/preset-sheets-sort/locales/ja-JP';
import table from '@univerjs/preset-sheets-table/locales/ja-JP';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/ja-JP';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/ko-KR';
import core from '@univerjs/preset-sheets-core/locales/ko-KR';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/ko-KR';
import drawing from '@univerjs/preset-sheets-drawing/locales/ko-KR';
import filter from '@univerjs/preset-sheets-filter/locales/ko-KR';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/ko-KR';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/ko-KR';
import note from '@univerjs/preset-sheets-note/locales/ko-KR';
import sort from '@univerjs/preset-sheets-sort/locales/ko-KR';
import table from '@univerjs/preset-sheets-table/locales/ko-KR';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/ko-KR';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/pl-PL';
import core from '@univerjs/preset-sheets-core/locales/pl-PL';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/pl-PL';
import drawing from '@univerjs/preset-sheets-drawing/locales/pl-PL';
import filter from '@univerjs/preset-sheets-filter/locales/pl-PL';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/pl-PL';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/pl-PL';
import note from '@univerjs/preset-sheets-note/locales/pl-PL';
import sort from '@univerjs/preset-sheets-sort/locales/pl-PL';
import table from '@univerjs/preset-sheets-table/locales/pl-PL';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/pl-PL';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/pt-BR';
import core from '@univerjs/preset-sheets-core/locales/pt-BR';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/pt-BR';
import drawing from '@univerjs/preset-sheets-drawing/locales/pt-BR';
import filter from '@univerjs/preset-sheets-filter/locales/pt-BR';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/pt-BR';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/pt-BR';
import note from '@univerjs/preset-sheets-note/locales/pt-BR';
import sort from '@univerjs/preset-sheets-sort/locales/pt-BR';
import table from '@univerjs/preset-sheets-table/locales/pt-BR';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/pt-BR';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/ru-RU';
import core from '@univerjs/preset-sheets-core/locales/ru-RU';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/ru-RU';
import drawing from '@univerjs/preset-sheets-drawing/locales/ru-RU';
import filter from '@univerjs/preset-sheets-filter/locales/ru-RU';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/ru-RU';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/ru-RU';
import note from '@univerjs/preset-sheets-note/locales/ru-RU';
import sort from '@univerjs/preset-sheets-sort/locales/ru-RU';
import table from '@univerjs/preset-sheets-table/locales/ru-RU';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/ru-RU';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/sk-SK';
import core from '@univerjs/preset-sheets-core/locales/sk-SK';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/sk-SK';
import drawing from '@univerjs/preset-sheets-drawing/locales/sk-SK';
import filter from '@univerjs/preset-sheets-filter/locales/sk-SK';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/sk-SK';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/sk-SK';
import note from '@univerjs/preset-sheets-note/locales/sk-SK';
import sort from '@univerjs/preset-sheets-sort/locales/sk-SK';
import table from '@univerjs/preset-sheets-table/locales/sk-SK';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/sk-SK';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/vi-VN';
import core from '@univerjs/preset-sheets-core/locales/vi-VN';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/vi-VN';
import drawing from '@univerjs/preset-sheets-drawing/locales/vi-VN';
import filter from '@univerjs/preset-sheets-filter/locales/vi-VN';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/vi-VN';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/vi-VN';
import note from '@univerjs/preset-sheets-note/locales/vi-VN';
import sort from '@univerjs/preset-sheets-sort/locales/vi-VN';
import table from '@univerjs/preset-sheets-table/locales/vi-VN';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/vi-VN';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/zh-CN';
import core from '@univerjs/preset-sheets-core/locales/zh-CN';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/zh-CN';
import drawing from '@univerjs/preset-sheets-drawing/locales/zh-CN';
import filter from '@univerjs/preset-sheets-filter/locales/zh-CN';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/zh-CN';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/zh-CN';
import note from '@univerjs/preset-sheets-note/locales/zh-CN';
import sort from '@univerjs/preset-sheets-sort/locales/zh-CN';
import table from '@univerjs/preset-sheets-table/locales/zh-CN';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/zh-CN';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/zh-HK';
import core from '@univerjs/preset-sheets-core/locales/zh-HK';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/zh-HK';
import drawing from '@univerjs/preset-sheets-drawing/locales/zh-HK';
import filter from '@univerjs/preset-sheets-filter/locales/zh-HK';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/zh-HK';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/zh-HK';
import note from '@univerjs/preset-sheets-note/locales/zh-HK';
import sort from '@univerjs/preset-sheets-sort/locales/zh-HK';
import table from '@univerjs/preset-sheets-table/locales/zh-HK';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/zh-HK';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,26 @@
import { mergeLocales } from '@univerjs/core';
import conditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/zh-TW';
import core from '@univerjs/preset-sheets-core/locales/zh-TW';
import dataValidation from '@univerjs/preset-sheets-data-validation/locales/zh-TW';
import drawing from '@univerjs/preset-sheets-drawing/locales/zh-TW';
import filter from '@univerjs/preset-sheets-filter/locales/zh-TW';
import findReplace from '@univerjs/preset-sheets-find-replace/locales/zh-TW';
import hyperLink from '@univerjs/preset-sheets-hyper-link/locales/zh-TW';
import note from '@univerjs/preset-sheets-note/locales/zh-TW';
import sort from '@univerjs/preset-sheets-sort/locales/zh-TW';
import table from '@univerjs/preset-sheets-table/locales/zh-TW';
import threadComment from '@univerjs/preset-sheets-thread-comment/locales/zh-TW';
export default mergeLocales(
core,
drawing,
conditionalFormatting,
dataValidation,
filter,
findReplace,
hyperLink,
note,
sort,
table,
threadComment
);

View File

@@ -0,0 +1,90 @@
import type { MountExample } from '../mount-example';
import type { IWorkbenchMountOptions } from '../workbench-settings';
import { LocaleType } from '@univerjs/core';
import { UniverSheetsConditionalFormattingPreset } from '@univerjs/preset-sheets-conditional-formatting';
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core';
import { UniverSheetsDataValidationPreset } from '@univerjs/preset-sheets-data-validation';
import { UniverSheetsDrawingPreset } from '@univerjs/preset-sheets-drawing';
import { UniverSheetsFilterPreset } from '@univerjs/preset-sheets-filter';
import { UniverSheetsFindReplacePreset } from '@univerjs/preset-sheets-find-replace';
import { UniverSheetsHyperLinkPreset } from '@univerjs/preset-sheets-hyper-link';
import { UniverSheetsNotePreset } from '@univerjs/preset-sheets-note';
import { UniverSheetsSortPreset } from '@univerjs/preset-sheets-sort';
import { UniverSheetsTablePreset } from '@univerjs/preset-sheets-table';
import { UniverSheetsThreadCommentPreset } from '@univerjs/preset-sheets-thread-comment';
import { createUniver } from '@univerjs/presets';
import { loadSheetLocale } from 'virtual:univer-examples-sheet-locale';
import { applyWorkbenchUIChrome, createMountedUniver } from '../mount-example';
import { getEffectiveWorkbenchRegion } from '../workbench-settings';
import { applyWorkbookZoom } from './apply-workbook-zoom';
import { createSheetFixture } from './create-sheet-fixture';
import './global.css';
const UNIVER_LOCALES = {
[LocaleType.ZH_CN]: LocaleType.ZH_CN,
[LocaleType.EN_US]: LocaleType.EN_US,
[LocaleType.AR_SA]: LocaleType.AR_SA,
[LocaleType.FR_FR]: LocaleType.FR_FR,
[LocaleType.RU_RU]: LocaleType.RU_RU,
[LocaleType.ZH_TW]: LocaleType.ZH_TW,
[LocaleType.ZH_HK]: LocaleType.ZH_HK,
[LocaleType.VI_VN]: LocaleType.VI_VN,
[LocaleType.FA_IR]: LocaleType.FA_IR,
[LocaleType.JA_JP]: LocaleType.JA_JP,
[LocaleType.KO_KR]: LocaleType.KO_KR,
[LocaleType.ES_ES]: LocaleType.ES_ES,
[LocaleType.CA_ES]: LocaleType.CA_ES,
[LocaleType.SK_SK]: LocaleType.SK_SK,
[LocaleType.PT_BR]: LocaleType.PT_BR,
[LocaleType.DE_DE]: LocaleType.DE_DE,
[LocaleType.IT_IT]: LocaleType.IT_IT,
[LocaleType.ID_ID]: LocaleType.ID_ID,
[LocaleType.PL_PL]: LocaleType.PL_PL,
} satisfies Record<IWorkbenchMountOptions['locale'], LocaleType>;
export const mount: MountExample = async (host, options) => {
const { darkMode, direction, locale, ribbonType, theme, zoomRatio } = options;
const univerLocale = UNIVER_LOCALES[locale];
const localePack = await loadSheetLocale(locale);
const { univer, univerAPI } = createUniver({
locale: univerLocale,
region: UNIVER_LOCALES[getEffectiveWorkbenchRegion(options)],
locales: {
[univerLocale]: localePack,
},
theme,
darkMode,
direction,
presets: [
UniverSheetsCorePreset({ container: host, ribbonType }),
UniverSheetsDrawingPreset(),
UniverSheetsConditionalFormattingPreset(),
UniverSheetsFilterPreset(),
UniverSheetsHyperLinkPreset(),
UniverSheetsDataValidationPreset(),
UniverSheetsFindReplacePreset(),
UniverSheetsNotePreset(),
UniverSheetsSortPreset(),
UniverSheetsTablePreset(),
UniverSheetsThreadCommentPreset(),
],
});
try {
applyWorkbenchUIChrome(univerAPI, options.uiChrome);
univerAPI.createWorkbook(createSheetFixture(univerLocale, direction, zoomRatio));
return createMountedUniver(
host,
univer,
univerAPI,
options,
loadSheetLocale,
(nextOptions) => applyWorkbookZoom(univerAPI, nextOptions)
);
} catch (error) {
univer.dispose();
host.replaceChildren();
throw error;
}
};

View File

@@ -0,0 +1,88 @@
import { PageElementType } from '@univerjs/slides';
import { describe, expect, it } from 'vitest';
import { readGeneratedSvgSize } from '../../generated-svg';
import { createSlideFixture } from '../create-slide-fixture';
function getElementBounds(element: { height?: number; left?: number; top?: number; width?: number }) {
if (element.height == null || element.left == null || element.top == null || element.width == null) {
throw new Error('Fixture element must define complete bounds.');
}
return {
bottom: element.top + element.height,
left: element.left,
right: element.left + element.width,
top: element.top,
};
}
function intersects(
first: ReturnType<typeof getElementBounds>,
second: ReturnType<typeof getElementBounds>
): boolean {
return first.left < second.right && first.right > second.left && first.top < second.bottom && first.bottom > second.top;
}
describe('createSlideFixture', () => {
it('creates fresh offline pages spanning text, shapes and images', () => {
const first = createSlideFixture();
const second = createSlideFixture();
if (!first.body || !second.body) {
throw new Error('Slides workbench fixtures must include a body.');
}
const elements = Object.values(first.body.pages).flatMap(({ pageElements }) => Object.values(pageElements));
const elementTypes = new Set(elements.map(({ type }) => type));
const images = elements.filter(({ type }) => type === PageElementType.IMAGE);
const textBoxes = elements.filter(({ type }) => type === PageElementType.TEXT);
const generatedTextBoxes = textBoxes.filter(({ id }) => id !== 'rich-copy');
expect(elementTypes).toEqual(new Set([PageElementType.IMAGE, PageElementType.SHAPE, PageElementType.TEXT]));
expect(images.every(({ image }) => image?.imageProperties?.contentUrl?.startsWith('data:image/svg+xml'))).toBe(true);
images.forEach(({ height, image, width }) => {
const source = image?.imageProperties?.contentUrl;
if (!source || width == null || height == null) {
throw new Error('Generated slide images must include a source and render dimensions.');
}
const intrinsicSize = readGeneratedSvgSize(source);
expect(width / height).toBeCloseTo(intrinsicSize.width / intrinsicSize.height, 8);
});
expect(generatedTextBoxes.every(({ richText }) => {
const style = richText?.rich?.documentStyle;
return style?.marginBottom === 0 && style.marginLeft === 0 && style.marginRight === 0 && style.marginTop === 0;
})).toBe(true);
expect(textBoxes.every(({ width }) => width != null && width > 144)).toBe(true);
expect(first.body.pages).not.toBe(second.body.pages);
expect(JSON.stringify(first)).toBe(JSON.stringify(second));
});
it('keeps generated images centered in their frames without colliding with foreground elements', () => {
const fixture = createSlideFixture();
const overview = fixture.body?.pages.overview.pageElements;
const media = fixture.body?.pages.media.pageElements;
if (!overview || !media) {
throw new Error('Slides workbench must include overview and media pages.');
}
const overviewFrame = getElementBounds(overview['overview-glow']);
const overviewImage = getElementBounds(overview['overview-image']);
expect((overviewImage.left + overviewImage.right) / 2).toBe((overviewFrame.left + overviewFrame.right) / 2);
expect((overviewImage.top + overviewImage.bottom) / 2).toBe((overviewFrame.top + overviewFrame.bottom) / 2);
const mediaFrame = getElementBounds(media['media-frame']);
const mediaImage = getElementBounds(media['media-image']);
expect([
mediaImage.left - mediaFrame.left,
mediaImage.top - mediaFrame.top,
mediaFrame.right - mediaImage.right,
mediaFrame.bottom - mediaImage.bottom,
]).toEqual([22, 22, 22, 22]);
const collidingForegroundElements = Object.values(media)
.filter(({ id, zIndex }) => id !== 'media-image' && id !== 'media-frame' && zIndex > 1)
.filter((element) => intersects(mediaImage, getElementBounds(element)));
expect(collidingForegroundElements).toEqual([]);
});
});

View File

@@ -0,0 +1,603 @@
import type { IDocumentData, IShapeProperties, IStyleBase } from '@univerjs/core';
import type { IPageElement, ISlideData } from '@univerjs/slides';
import { BooleanNumber, DocumentFlavor, PresetListType } from '@univerjs/core';
import { BasicShapes, PageElementType, PageType } from '@univerjs/slides';
import { createGeneratedSvg } from '../generated-svg';
const INLINE_DASHBOARD_IMAGE = createGeneratedSvg({
width: 480,
height: 280,
content: [
'<rect width="480" height="280" rx="28" fill="#18181b"/>',
'<rect x="28" y="28" width="424" height="46" rx="12" fill="#27272a"/>',
'<circle cx="55" cy="51" r="8" fill="#f97316"/>',
'<circle cx="79" cy="51" r="8" fill="#facc15"/>',
'<circle cx="103" cy="51" r="8" fill="#22c55e"/>',
'<rect x="28" y="96" width="126" height="156" rx="16" fill="#312e81"/>',
'<rect x="174" y="96" width="278" height="70" rx="16" fill="#4338ca"/>',
'<rect x="174" y="186" width="82" height="66" rx="16" fill="#0ea5e9"/>',
'<rect x="272" y="186" width="82" height="66" rx="16" fill="#14b8a6"/>',
'<rect x="370" y="186" width="82" height="66" rx="16" fill="#f97316"/>',
],
});
const WHITE_TEXT: IStyleBase = {
ff: 'Arial',
cl: { rgb: '#f8fafc' },
};
function createTextDocument(id: string, text: string, width: number, height: number, style: IStyleBase): IDocumentData {
const paragraphs: NonNullable<IDocumentData['body']>['paragraphs'] = [];
const textRuns: NonNullable<IDocumentData['body']>['textRuns'] = [];
let dataStream = '';
text.split('\n').forEach((line, index) => {
const startIndex = dataStream.length;
dataStream += line;
if (line.length > 0) {
textRuns.push({ st: startIndex, ed: dataStream.length, ts: style });
}
paragraphs.push({ paragraphId: `${id}-paragraph-${index + 1}`, startIndex: dataStream.length });
dataStream += '\r';
});
const sectionStartIndex = dataStream.length;
dataStream += '\n';
return {
id: `${id}-document`,
body: {
dataStream,
paragraphs,
sectionBreaks: [{ sectionId: `${id}-section`, startIndex: sectionStartIndex }],
textRuns,
},
documentStyle: {
documentFlavor: DocumentFlavor.TRADITIONAL,
pageSize: { height, width },
marginBottom: 0,
marginLeft: 0,
marginRight: 0,
marginTop: 0,
},
};
}
function createText(
id: string,
text: string,
[left, top, width, height]: [number, number, number, number],
richText: IStyleBase,
zIndex = 3,
transform: Partial<Pick<IPageElement, 'angle' | 'flipX' | 'flipY' | 'scaleX' | 'scaleY' | 'skewX' | 'skewY'>> = {}
): IPageElement {
return {
id,
zIndex,
left,
top,
width,
height,
title: id,
description: text,
type: PageElementType.TEXT,
richText: {
rich: createTextDocument(id, text, width, height, richText),
},
...transform,
};
}
function createShape(
id: string,
shapeType: BasicShapes,
[left, top, width, height]: [number, number, number, number],
fill: string,
zIndex = 2,
shapeProperties: Omit<IShapeProperties, 'shapeBackgroundFill'> = {},
transform: Partial<Pick<IPageElement, 'angle' | 'flipX' | 'flipY' | 'scaleX' | 'scaleY' | 'skewX' | 'skewY'>> = {}
): IPageElement {
return {
id,
zIndex,
left,
top,
width,
height,
title: id,
description: `${shapeType} shape`,
type: PageElementType.SHAPE,
shape: {
shapeType,
text: '',
shapeProperties: {
shapeBackgroundFill: { rgb: fill },
...shapeProperties,
},
},
...transform,
};
}
const SLIDE_FIXTURE: ISlideData = {
id: 'slide-workbench',
title: 'Univer Slides workbench',
pageSize: {
width: 960,
height: 540,
},
body: {
pageOrder: ['overview', 'components', 'rich-text', 'media'],
pages: {
overview: {
id: 'overview',
pageType: PageType.SLIDE,
zIndex: 1,
title: 'Overview',
description: 'Cover page with layered text, shapes and an inline image',
pageBackgroundFill: { rgb: '#0f172a' },
pageElements: {
'overview-edge': createShape(
'overview-edge',
BasicShapes.Rect,
[0, 0, 14, 540],
'#6366f1',
1
),
'overview-glow': createShape(
'overview-glow',
BasicShapes.RoundRect,
[615, 100, 290, 330],
'rgba(99, 102, 241, 0.18)',
1,
{ radius: 28 },
{ angle: -5 }
),
'overview-tag': createShape(
'overview-tag',
BasicShapes.RoundRect,
[72, 82, 260, 34],
'#312e81',
2,
{
radius: 17,
outline: {
outlineFill: { rgb: '#818cf8' },
weight: 1,
},
}
),
'overview-tag-label': createText(
'overview-tag-label',
'DEVELOPER WORKBENCH',
[92, 89, 220, 22],
{ ...WHITE_TEXT, fs: 12, bl: BooleanNumber.TRUE },
3
),
'overview-title': createText(
'overview-title',
'Build slides,\ninspect behavior.',
[72, 154, 500, 150],
{ ...WHITE_TEXT, fs: 42, bl: BooleanNumber.TRUE },
3
),
'overview-subtitle': createText(
'overview-subtitle',
'Four compact pages exercise editing, styling, transforms, layers and local media.',
[76, 325, 460, 72],
{ ...WHITE_TEXT, fs: 17, it: BooleanNumber.TRUE, cl: { rgb: '#cbd5e1' } },
3
),
'overview-image': {
id: 'overview-image',
zIndex: 3,
left: 640,
top: 195,
width: 240,
height: 140,
angle: 3,
title: 'Inline dashboard illustration',
description: 'Deterministic SVG data URI without a network dependency',
type: PageElementType.IMAGE,
image: {
imageProperties: {
contentUrl: INLINE_DASHBOARD_IMAGE.source,
},
},
},
'overview-footer': createText(
'overview-footer',
'Univer Slides · local fixture',
[74, 472, 300, 24],
{ ...WHITE_TEXT, fs: 12, cl: { rgb: '#94a3b8' } },
3
),
},
},
components: {
id: 'components',
pageType: PageType.SLIDE,
zIndex: 2,
title: 'Components',
description: 'Shape, outline, typography and transform samples',
pageBackgroundFill: { rgb: '#f8fafc' },
pageElements: {
'components-title': createText(
'components-title',
'Shapes & typography',
[54, 42, 560, 54],
{ ff: 'Arial', fs: 30, bl: BooleanNumber.TRUE, cl: { rgb: '#0f172a' } }
),
'components-rule': createShape(
'components-rule',
BasicShapes.Rect,
[54, 102, 852, 5],
'#6366f1',
1
),
'components-card-1': createShape(
'components-card-1',
BasicShapes.RoundRect,
[54, 148, 252, 286],
'#ffffff',
1,
{
radius: 20,
outline: {
outlineFill: { rgb: '#cbd5e1' },
weight: 2,
},
}
),
'components-card-1-index': createShape(
'components-card-1-index',
BasicShapes.RoundRect,
[78, 174, 58, 48],
'#4f46e5',
2,
{ radius: 14 }
),
'components-card-1-number': createText(
'components-card-1-number',
'01',
[22, 184, 180, 28],
{ ...WHITE_TEXT, fs: 20, bl: BooleanNumber.TRUE }
),
'components-card-1-copy': createText(
'components-card-1-copy',
'Rounded rectangle\nwith a visible outline',
[78, 250, 200, 86],
{ ff: 'Arial', fs: 18, bl: BooleanNumber.TRUE, cl: { rgb: '#1e293b' } }
),
'components-card-2': createShape(
'components-card-2',
BasicShapes.Rect,
[354, 148, 252, 286],
'#ecfeff',
1,
{
outline: {
outlineFill: { rgb: '#06b6d4' },
weight: 2,
},
}
),
'components-card-2-accent': createShape(
'components-card-2-accent',
BasicShapes.Rect,
[406, 195, 150, 92],
'#0891b2',
2,
{},
{ angle: -8, skewX: 4 }
),
'components-card-2-copy': createText(
'components-card-2-copy',
'Rotate + skew',
[394, 310, 180, 34],
{
ff: 'Georgia',
fs: 20,
it: BooleanNumber.TRUE,
ul: { s: BooleanNumber.TRUE },
cl: { rgb: '#155e75' },
}
),
'components-card-3': createShape(
'components-card-3',
BasicShapes.RoundRect,
[654, 148, 252, 286],
'#fff7ed',
1,
{
radius: 40,
outline: {
outlineFill: { rgb: '#fb923c' },
weight: 3,
},
}
),
'components-card-3-layer-1': createShape(
'components-card-3-layer-1',
BasicShapes.RoundRect,
[704, 194, 150, 112],
'#fdba74',
2,
{ radius: 18 },
{ angle: -7 }
),
'components-card-3-layer-2': createShape(
'components-card-3-layer-2',
BasicShapes.RoundRect,
[722, 208, 150, 112],
'rgba(249, 115, 22, 0.78)',
3,
{ radius: 18 },
{ angle: 7 }
),
'components-card-3-copy': createText(
'components-card-3-copy',
'z-index layers',
[694, 350, 176, 32],
{
ff: 'Arial',
fs: 19,
st: { s: BooleanNumber.TRUE },
cl: { rgb: '#9a3412' },
},
4
),
},
},
'rich-text': {
id: 'rich-text',
pageType: PageType.SLIDE,
zIndex: 3,
title: 'Rich text',
description: 'Editable rich text and layered callout samples',
pageBackgroundFill: { rgb: '#fff7ed' },
pageElements: {
'rich-title': createText(
'rich-title',
'Editable rich text',
[56, 42, 520, 54],
{ ff: 'Arial', fs: 30, bl: BooleanNumber.TRUE, cl: { rgb: '#7c2d12' } }
),
'rich-panel': createShape(
'rich-panel',
BasicShapes.RoundRect,
[54, 126, 542, 350],
'#ffffff',
1,
{
radius: 24,
outline: {
outlineFill: { rgb: '#fed7aa' },
weight: 2,
},
}
),
'rich-copy': {
id: 'rich-copy',
zIndex: 2,
left: 84,
top: 158,
width: 480,
height: 280,
title: 'Rich text sample',
description: 'Multiple runs and editable bullet paragraphs',
type: PageElementType.TEXT,
richText: {
rich: {
id: 'slide-rich-copy',
body: {
dataStream:
'Rich text block\rBold, italic, underline and color\rEditable bullet one\rEditable bullet two\r\n',
textRuns: [
{
st: 0,
ed: 14,
ts: {
ff: 'Arial',
fs: 28,
bl: BooleanNumber.TRUE,
cl: { rgb: '#9a3412' },
},
},
{
st: 16,
ed: 20,
ts: { fs: 16, bl: BooleanNumber.TRUE },
},
{
st: 22,
ed: 27,
ts: { fs: 16, it: BooleanNumber.TRUE },
},
{
st: 30,
ed: 38,
ts: { fs: 16, ul: { s: BooleanNumber.TRUE } },
},
{
st: 44,
ed: 48,
ts: { fs: 16, cl: { rgb: '#ea580c' } },
},
{
st: 50,
ed: 88,
ts: { fs: 16, cl: { rgb: '#431407' } },
},
],
paragraphs: [
{ paragraphId: 'rich-heading', startIndex: 15 },
{ paragraphId: 'rich-styles', startIndex: 49 },
{
paragraphId: 'rich-bullet-1',
startIndex: 69,
bullet: {
listType: PresetListType.BULLET_LIST,
listId: 'slide-feature-list',
nestingLevel: 0,
textStyle: { fs: 16 },
},
},
{
paragraphId: 'rich-bullet-2',
startIndex: 89,
bullet: {
listType: PresetListType.BULLET_LIST,
listId: 'slide-feature-list',
nestingLevel: 0,
textStyle: { fs: 16 },
},
},
],
},
documentStyle: {
pageSize: {
width: undefined,
height: undefined,
},
marginTop: 4,
marginBottom: 4,
marginLeft: 4,
marginRight: 4,
},
},
},
},
'rich-callout-back': createShape(
'rich-callout-back',
BasicShapes.RoundRect,
[646, 160, 238, 210],
'#7c2d12',
1,
{ radius: 26 },
{ angle: -4 }
),
'rich-callout-front': createShape(
'rich-callout-front',
BasicShapes.RoundRect,
[664, 178, 238, 210],
'#fb923c',
2,
{ radius: 26 },
{ angle: 4 }
),
'rich-callout-copy': createText(
'rich-callout-copy',
'Double-click\nto edit',
[704, 236, 168, 88],
{ ...WHITE_TEXT, fs: 25, bl: BooleanNumber.TRUE },
3,
{ angle: 4 }
),
'rich-note': createText(
'rich-note',
'The fixture stays small while covering run styles, lists and the Slides text editor bridge.',
[642, 424, 270, 56],
{ ff: 'Arial', fs: 13, it: BooleanNumber.TRUE, cl: { rgb: '#9a3412' } }
),
},
},
media: {
id: 'media',
pageType: PageType.SLIDE,
zIndex: 4,
title: 'Media and layers',
description: 'Inline image, positioning and object samples',
pageBackgroundFill: { rgb: '#111827' },
pageElements: {
'media-title': createText(
'media-title',
'Local media, no network',
[58, 44, 600, 52],
{ ...WHITE_TEXT, fs: 30, bl: BooleanNumber.TRUE }
),
'media-accent': createShape(
'media-accent',
BasicShapes.Rect,
[724, 40, 170, 54],
'#14b8a6',
1,
{},
{ angle: 6 }
),
'media-frame': createShape(
'media-frame',
BasicShapes.RoundRect,
[58, 132, 500, 310],
'#312e81',
1,
{ radius: 28 }
),
'media-image': {
id: 'media-image',
zIndex: 2,
left: 80,
top: 154,
width: 456,
height: 266,
title: 'Inline SVG dashboard',
description: 'Same deterministic data URI at a second size',
type: PageElementType.IMAGE,
image: {
imageProperties: {
contentUrl: INLINE_DASHBOARD_IMAGE.source,
},
},
},
'media-chip-1': createShape(
'media-chip-1',
BasicShapes.RoundRect,
[628, 162, 246, 62],
'#1e293b',
1,
{ radius: 18 }
),
'media-chip-1-copy': createText(
'media-chip-1-copy',
'✓ deterministic fixture',
[652, 181, 200, 28],
{ ...WHITE_TEXT, fs: 16, cl: { rgb: '#5eead4' } }
),
'media-chip-2': createShape(
'media-chip-2',
BasicShapes.RoundRect,
[628, 246, 246, 62],
'#1e293b',
1,
{ radius: 18 }
),
'media-chip-2-copy': createText(
'media-chip-2-copy',
'✓ selectable objects',
[652, 265, 200, 28],
{ ...WHITE_TEXT, fs: 16, cl: { rgb: '#a5b4fc' } }
),
'media-chip-3': createShape(
'media-chip-3',
BasicShapes.RoundRect,
[628, 330, 246, 62],
'#1e293b',
1,
{ radius: 18 }
),
'media-chip-3-copy': createText(
'media-chip-3-copy',
'✓ offline image data',
[652, 349, 200, 28],
{ ...WHITE_TEXT, fs: 16, cl: { rgb: '#fdba74' } }
),
},
},
},
},
};
export function createSlideFixture(): ISlideData {
return structuredClone(SLIDE_FIXTURE);
}

View File

@@ -0,0 +1,4 @@
@config "../../tailwind.slides.config.ts";
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,6 @@
import type { WorkbenchLocale } from '../workbench-settings';
import { loadDevLocale } from '../dev-locale-loader';
export function loadSlideLocale(locale: WorkbenchLocale) {
return loadDevLocale('slides', locale);
}

View File

@@ -0,0 +1,33 @@
import type { WorkbenchLocale } from '../workbench-settings';
interface ILocaleModule {
default: Record<string, unknown>;
}
type LocaleLoader = () => Promise<ILocaleModule>;
const SLIDE_LOCALE_LOADERS = {
enUS: () => import('./locales/en-US'),
frFR: () => import('./locales/fr-FR'),
zhCN: () => import('./locales/zh-CN'),
ruRU: () => import('./locales/ru-RU'),
zhTW: () => import('./locales/zh-TW'),
zhHK: () => import('./locales/zh-HK'),
viVN: () => import('./locales/vi-VN'),
faIR: () => import('./locales/fa-IR'),
jaJP: () => import('./locales/ja-JP'),
koKR: () => import('./locales/ko-KR'),
esES: () => import('./locales/es-ES'),
caES: () => import('./locales/ca-ES'),
skSK: () => import('./locales/sk-SK'),
ptBR: () => import('./locales/pt-BR'),
deDE: () => import('./locales/de-DE'),
itIT: () => import('./locales/it-IT'),
idID: () => import('./locales/id-ID'),
plPL: () => import('./locales/pl-PL'),
arSA: () => import('./locales/ar-SA'),
} satisfies Record<WorkbenchLocale, LocaleLoader>;
export async function loadSlideLocale(locale: WorkbenchLocale) {
return (await SLIDE_LOCALE_LOADERS[locale]()).default;
}

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/ar-SA';
import docsUI from '@univerjs/docs-ui/locale/ar-SA';
import slidesUI from '@univerjs/slides-ui/locale/ar-SA';
import ui from '@univerjs/ui/locale/ar-SA';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/ca-ES';
import docsUI from '@univerjs/docs-ui/locale/ca-ES';
import slidesUI from '@univerjs/slides-ui/locale/ca-ES';
import ui from '@univerjs/ui/locale/ca-ES';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/de-DE';
import docsUI from '@univerjs/docs-ui/locale/de-DE';
import slidesUI from '@univerjs/slides-ui/locale/de-DE';
import ui from '@univerjs/ui/locale/de-DE';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/en-US';
import docsUI from '@univerjs/docs-ui/locale/en-US';
import slidesUI from '@univerjs/slides-ui/locale/en-US';
import ui from '@univerjs/ui/locale/en-US';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/es-ES';
import docsUI from '@univerjs/docs-ui/locale/es-ES';
import slidesUI from '@univerjs/slides-ui/locale/es-ES';
import ui from '@univerjs/ui/locale/es-ES';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/fa-IR';
import docsUI from '@univerjs/docs-ui/locale/fa-IR';
import slidesUI from '@univerjs/slides-ui/locale/fa-IR';
import ui from '@univerjs/ui/locale/fa-IR';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/fr-FR';
import docsUI from '@univerjs/docs-ui/locale/fr-FR';
import slidesUI from '@univerjs/slides-ui/locale/fr-FR';
import ui from '@univerjs/ui/locale/fr-FR';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/id-ID';
import docsUI from '@univerjs/docs-ui/locale/id-ID';
import slidesUI from '@univerjs/slides-ui/locale/id-ID';
import ui from '@univerjs/ui/locale/id-ID';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/it-IT';
import docsUI from '@univerjs/docs-ui/locale/it-IT';
import slidesUI from '@univerjs/slides-ui/locale/it-IT';
import ui from '@univerjs/ui/locale/it-IT';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/ja-JP';
import docsUI from '@univerjs/docs-ui/locale/ja-JP';
import slidesUI from '@univerjs/slides-ui/locale/ja-JP';
import ui from '@univerjs/ui/locale/ja-JP';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/ko-KR';
import docsUI from '@univerjs/docs-ui/locale/ko-KR';
import slidesUI from '@univerjs/slides-ui/locale/ko-KR';
import ui from '@univerjs/ui/locale/ko-KR';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/pl-PL';
import docsUI from '@univerjs/docs-ui/locale/pl-PL';
import slidesUI from '@univerjs/slides-ui/locale/pl-PL';
import ui from '@univerjs/ui/locale/pl-PL';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/pt-BR';
import docsUI from '@univerjs/docs-ui/locale/pt-BR';
import slidesUI from '@univerjs/slides-ui/locale/pt-BR';
import ui from '@univerjs/ui/locale/pt-BR';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/ru-RU';
import docsUI from '@univerjs/docs-ui/locale/ru-RU';
import slidesUI from '@univerjs/slides-ui/locale/ru-RU';
import ui from '@univerjs/ui/locale/ru-RU';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/sk-SK';
import docsUI from '@univerjs/docs-ui/locale/sk-SK';
import slidesUI from '@univerjs/slides-ui/locale/sk-SK';
import ui from '@univerjs/ui/locale/sk-SK';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/vi-VN';
import docsUI from '@univerjs/docs-ui/locale/vi-VN';
import slidesUI from '@univerjs/slides-ui/locale/vi-VN';
import ui from '@univerjs/ui/locale/vi-VN';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/zh-CN';
import docsUI from '@univerjs/docs-ui/locale/zh-CN';
import slidesUI from '@univerjs/slides-ui/locale/zh-CN';
import ui from '@univerjs/ui/locale/zh-CN';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/zh-HK';
import docsUI from '@univerjs/docs-ui/locale/zh-HK';
import slidesUI from '@univerjs/slides-ui/locale/zh-HK';
import ui from '@univerjs/ui/locale/zh-HK';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,12 @@
import { mergeLocales } from '@univerjs/core';
import design from '@univerjs/design/locale/zh-TW';
import docsUI from '@univerjs/docs-ui/locale/zh-TW';
import slidesUI from '@univerjs/slides-ui/locale/zh-TW';
import ui from '@univerjs/ui/locale/zh-TW';
export default mergeLocales(
design,
docsUI,
slidesUI,
ui
);

View File

@@ -0,0 +1,97 @@
import type { MountExample } from '../mount-example';
import type { IWorkbenchMountOptions } from '../workbench-settings';
import { LocaleType, Univer, UniverInstanceType } from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
import { UniverDocsPlugin } from '@univerjs/docs';
import { UniverDocsUIPlugin } from '@univerjs/docs-ui';
import { UniverDrawingPlugin } from '@univerjs/drawing';
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
import { UniverSlidesPlugin } from '@univerjs/slides';
import { UniverSlidesUIPlugin } from '@univerjs/slides-ui';
import { UniverUIPlugin } from '@univerjs/ui';
import { filter, firstValueFrom } from 'rxjs';
import { loadSlideLocale } from 'virtual:univer-examples-slide-locale';
import { applyWorkbenchUIChrome, createMountedUniver } from '../mount-example';
import { getEffectiveWorkbenchRegion } from '../workbench-settings';
import { createSlideFixture } from './create-slide-fixture';
import '@univerjs/ui/facade';
import './global.css';
const UNIVER_LOCALES = {
[LocaleType.ZH_CN]: LocaleType.ZH_CN,
[LocaleType.EN_US]: LocaleType.EN_US,
[LocaleType.AR_SA]: LocaleType.AR_SA,
[LocaleType.FR_FR]: LocaleType.FR_FR,
[LocaleType.RU_RU]: LocaleType.RU_RU,
[LocaleType.ZH_TW]: LocaleType.ZH_TW,
[LocaleType.ZH_HK]: LocaleType.ZH_HK,
[LocaleType.VI_VN]: LocaleType.VI_VN,
[LocaleType.FA_IR]: LocaleType.FA_IR,
[LocaleType.JA_JP]: LocaleType.JA_JP,
[LocaleType.KO_KR]: LocaleType.KO_KR,
[LocaleType.ES_ES]: LocaleType.ES_ES,
[LocaleType.CA_ES]: LocaleType.CA_ES,
[LocaleType.SK_SK]: LocaleType.SK_SK,
[LocaleType.PT_BR]: LocaleType.PT_BR,
[LocaleType.DE_DE]: LocaleType.DE_DE,
[LocaleType.IT_IT]: LocaleType.IT_IT,
[LocaleType.ID_ID]: LocaleType.ID_ID,
[LocaleType.PL_PL]: LocaleType.PL_PL,
} satisfies Record<IWorkbenchMountOptions['locale'], LocaleType>;
export const mount: MountExample = async (host, options) => {
const locale = UNIVER_LOCALES[options.locale];
const messages = await loadSlideLocale(options.locale);
const univer = new Univer({
locale,
region: UNIVER_LOCALES[getEffectiveWorkbenchRegion(options)],
locales: {
[locale]: messages,
},
theme: options.theme,
darkMode: options.darkMode,
direction: options.direction,
});
try {
univer.registerPlugin(UniverRenderEnginePlugin);
univer.registerPlugin(UniverUIPlugin, {
container: host,
ribbonType: options.ribbonType,
});
univer.registerPlugin(UniverDocsPlugin);
univer.registerPlugin(UniverDocsUIPlugin);
univer.registerPlugin(UniverDrawingPlugin);
univer.registerPlugin(UniverSlidesPlugin);
univer.registerPlugin(UniverSlidesUIPlugin);
const univerAPI = FUniver.newAPI(univer);
applyWorkbenchUIChrome(univerAPI, options.uiChrome);
const slideFixture = createSlideFixture();
const slideId = slideFixture.id;
univer.createUnit(UniverInstanceType.UNIVER_SLIDE, slideFixture);
const renderManagerService = univer.__getInjector().get(IRenderManagerService);
const applyProductSettings = async (nextOptions: IWorkbenchMountOptions) => {
const renderUnit = renderManagerService.getRenderUnitById(slideId) ?? await firstValueFrom(
renderManagerService.created$.pipe(filter(({ unitId }) => unitId === slideId))
);
renderUnit.scene.scale(nextOptions.zoomRatio, nextOptions.zoomRatio);
};
await applyProductSettings(options);
return createMountedUniver(
host,
univer,
univerAPI,
options,
loadSlideLocale,
applyProductSettings
);
} catch (error) {
univer.dispose();
host.replaceChildren();
throw error;
}
};

13
examples/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
/// <reference types="vite/client" />
declare module 'virtual:univer-examples-document-locale' {
export const loadDocumentLocale: typeof import('./docs/locale-loader').loadDocumentLocale;
}
declare module 'virtual:univer-examples-sheet-locale' {
export const loadSheetLocale: typeof import('./sheets/locale-loader').loadSheetLocale;
}
declare module 'virtual:univer-examples-slide-locale' {
export const loadSlideLocale: typeof import('./slides/locale-loader').loadSlideLocale;
}

View File

@@ -0,0 +1,265 @@
import type { IDropdownMenuProps } from '@univerjs/design';
import type {
IWorkbenchSettings,
WorkbenchDirection,
WorkbenchLocale,
WorkbenchRegion,
WorkbenchRibbonType,
WorkbenchTheme,
WorkbenchUIChromeMode,
WorkbenchZoomRatio,
} from './workbench-settings';
import { Button, ConfigProvider, DropdownMenu, render, unmount } from '@univerjs/design';
import { MoreDownIcon } from '@univerjs/icons';
import {
WORKBENCH_DIRECTIONS,
WORKBENCH_LOCALE_META,
WORKBENCH_LOCALES,
WORKBENCH_REGIONS,
WORKBENCH_RIBBON_TYPES,
WORKBENCH_THEMES,
WORKBENCH_UI_CHROME_MODES,
WORKBENCH_ZOOM_RATIOS,
} from './workbench-settings';
const LOCALE_LABELS = {
arSA: 'العربية',
caES: 'Català',
deDE: 'Deutsch',
enUS: 'English',
esES: 'Español',
faIR: 'فارسی',
frFR: 'Français',
idID: 'Bahasa Indonesia',
itIT: 'Italiano',
jaJP: '日本語',
koKR: '한국어',
plPL: 'Polski',
ptBR: 'Português (Brasil)',
ruRU: 'Русский',
skSK: 'Slovenčina',
viVN: 'Tiếng Việt',
zhCN: '简体中文',
zhHK: '繁體中文(香港)',
zhTW: '繁體中文',
} satisfies Record<WorkbenchLocale, string>;
const THEME_LABELS = {
blue: 'Blue',
'dark-blue': 'Dark blue',
green: 'Green',
orange: 'Orange',
purple: 'Purple',
red: 'Red',
yellow: 'Yellow',
} satisfies Record<WorkbenchTheme, string>;
const RIBBON_TYPE_LABELS = {
classic: 'Classic',
collapsed: 'Collapsed',
grid: 'Grid',
simple: 'Simple',
} satisfies Record<WorkbenchRibbonType, string>;
const REGION_LABELS = {
arSA: 'Saudi Arabia',
auto: 'Follow language',
caES: 'Catalonia (Spain)',
deDE: 'Germany',
enUS: 'United States',
esES: 'Spain',
faIR: 'Iran',
frFR: 'France',
idID: 'Indonesia',
itIT: 'Italy',
jaJP: 'Japan',
koKR: 'South Korea',
plPL: 'Poland',
ptBR: 'Brazil',
ruRU: 'Russia',
skSK: 'Slovakia',
viVN: 'Vietnam',
zhCN: 'China',
zhHK: 'Hong Kong',
zhTW: 'Taiwan',
} satisfies Record<WorkbenchRegion, string>;
const UI_CHROME_LABELS = {
'canvas-only': 'Canvas only',
full: 'Full',
'no-ribbon': 'No ribbon',
} satisfies Record<WorkbenchUIChromeMode, string>;
interface IWorkbenchSettingsControlProps {
settings: IWorkbenchSettings;
onLocaleChange: (locale: WorkbenchLocale) => Promise<void>;
onRegionChange: (region: WorkbenchRegion) => Promise<void>;
onDirectionChange: (direction: WorkbenchDirection) => Promise<void>;
onThemeChange: (theme: WorkbenchTheme) => Promise<void>;
onDarkModeChange: (darkMode: boolean) => Promise<void>;
onRibbonTypeChange: (ribbonType: WorkbenchRibbonType) => Promise<void>;
onUIChromeChange: (uiChrome: WorkbenchUIChromeMode) => Promise<void>;
onZoomRatioChange: (zoomRatio: WorkbenchZoomRatio) => Promise<void>;
onResetPreferences: () => Promise<void>;
}
function WorkbenchSettingsControl(props: IWorkbenchSettingsControlProps) {
const {
settings,
onDarkModeChange,
onDirectionChange,
onLocaleChange,
onRegionChange,
onResetPreferences,
onRibbonTypeChange,
onThemeChange,
onUIChromeChange,
onZoomRatioChange,
} = props;
function renderSettingLabel(label: string, value: string) {
return (
<span className="workbench-setting-label">
<span>{label}</span>
<span className="workbench-setting-value">{value}</span>
</span>
);
}
const items: IDropdownMenuProps['items'] = [
{
type: 'subItem',
children: renderSettingLabel('Language', LOCALE_LABELS[settings.locale]),
options: [{
type: 'radio',
value: settings.locale,
options: WORKBENCH_LOCALES.map((value) => ({
value,
label: (
<span
dir={WORKBENCH_LOCALE_META[value].direction}
lang={WORKBENCH_LOCALE_META[value].tag}
>
{LOCALE_LABELS[value]}
</span>
),
})),
onSelect: (value) => onLocaleChange(value as WorkbenchLocale),
}],
},
{
type: 'subItem',
children: renderSettingLabel('Region', REGION_LABELS[settings.region]),
options: [{
type: 'radio',
value: settings.region,
options: WORKBENCH_REGIONS.map((value) => ({
value,
label: REGION_LABELS[value],
})),
onSelect: (value) => onRegionChange(value as WorkbenchRegion),
}],
},
{
type: 'subItem',
children: renderSettingLabel('Direction', settings.direction.toUpperCase()),
options: [{
type: 'radio',
value: settings.direction,
options: WORKBENCH_DIRECTIONS.map((value) => ({ value, label: value.toUpperCase() })),
onSelect: (value) => onDirectionChange(value as WorkbenchDirection),
}],
},
{
type: 'subItem',
children: renderSettingLabel('Theme', THEME_LABELS[settings.theme]),
options: [{
type: 'radio',
value: settings.theme,
options: WORKBENCH_THEMES.map((value) => ({ value, label: THEME_LABELS[value] })),
onSelect: (value) => onThemeChange(value as WorkbenchTheme),
}],
},
{
type: 'subItem',
children: renderSettingLabel('Appearance', settings.darkMode ? 'Dark' : 'Light'),
options: [{
type: 'radio',
value: settings.darkMode ? 'dark' : 'light',
options: [
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' },
],
onSelect: (value) => onDarkModeChange(value === 'dark'),
}],
},
{
type: 'subItem',
children: renderSettingLabel('Ribbon', RIBBON_TYPE_LABELS[settings.ribbonType]),
options: [{
type: 'radio',
value: settings.ribbonType,
options: WORKBENCH_RIBBON_TYPES.map((value) => ({ value, label: RIBBON_TYPE_LABELS[value] })),
onSelect: (value) => onRibbonTypeChange(value as WorkbenchRibbonType),
}],
},
{
type: 'subItem',
children: renderSettingLabel('UI chrome', UI_CHROME_LABELS[settings.uiChrome]),
options: [{
type: 'radio',
value: settings.uiChrome,
options: WORKBENCH_UI_CHROME_MODES.map((value) => ({
value,
label: UI_CHROME_LABELS[value],
})),
onSelect: (value) => onUIChromeChange(value as WorkbenchUIChromeMode),
}],
},
{
type: 'subItem',
children: renderSettingLabel('Zoom', `${settings.zoomRatio * 100}%`),
options: [{
type: 'radio',
value: String(settings.zoomRatio),
options: WORKBENCH_ZOOM_RATIOS.map((value) => ({
value: String(value),
label: `${value * 100}%`,
})),
onSelect: (value) => {
const zoomRatio = WORKBENCH_ZOOM_RATIOS.find((item) => String(item) === value);
return zoomRatio == null ? Promise.resolve() : onZoomRatioChange(zoomRatio);
},
}],
},
{ type: 'separator' },
{
type: 'item',
children: 'Reset preferences',
onSelect: onResetPreferences,
},
];
return (
<ConfigProvider direction={settings.direction} mountContainer={document.body}>
<DropdownMenu align="end" className="workbench-settings-menu" items={items} side="bottom">
<Button className="workbench-settings-trigger" size="middle" type="button" variant="text">
Settings
<MoreDownIcon aria-hidden="true" />
</Button>
</DropdownMenu>
</ConfigProvider>
);
}
export function renderWorkbenchSettingsControl(
container: HTMLElement,
props: IWorkbenchSettingsControlProps
) {
render(<WorkbenchSettingsControl {...props} />, container);
}
export function unmountWorkbenchSettingsControl(container: HTMLElement) {
unmount(container);
}

View File

@@ -0,0 +1,196 @@
import type { Theme } from '@univerjs/themes';
import type { RibbonType } from '@univerjs/ui';
import {
blueTheme,
darkBlueTheme,
greenTheme,
orangeTheme,
purpleTheme,
redTheme,
yellowTheme,
} from '@univerjs/themes';
export const WORKBENCH_LOCALES = [
'enUS',
'frFR',
'zhCN',
'ruRU',
'zhTW',
'zhHK',
'viVN',
'faIR',
'jaJP',
'koKR',
'esES',
'caES',
'skSK',
'ptBR',
'deDE',
'itIT',
'idID',
'plPL',
'arSA',
] as const;
export const WORKBENCH_DIRECTIONS = ['ltr', 'rtl'] as const;
export const WORKBENCH_THEMES = ['blue', 'green', 'orange', 'purple', 'red', 'yellow', 'dark-blue'] as const;
export const WORKBENCH_RIBBON_TYPES = ['grid', 'classic', 'collapsed', 'simple'] as const satisfies readonly RibbonType[];
export const WORKBENCH_REGIONS = ['auto', ...WORKBENCH_LOCALES] as const;
export const WORKBENCH_UI_CHROME_MODES = ['full', 'no-ribbon', 'canvas-only'] as const;
export const WORKBENCH_ZOOM_RATIOS = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4] as const;
export const WORKBENCH_SETTINGS_STORAGE_KEY = 'univer.examples.workbench.settings';
export type WorkbenchLocale = (typeof WORKBENCH_LOCALES)[number];
export type WorkbenchDirection = (typeof WORKBENCH_DIRECTIONS)[number];
export type WorkbenchTheme = (typeof WORKBENCH_THEMES)[number];
export type WorkbenchRibbonType = (typeof WORKBENCH_RIBBON_TYPES)[number];
export type WorkbenchRegion = (typeof WORKBENCH_REGIONS)[number];
export type WorkbenchUIChromeMode = (typeof WORKBENCH_UI_CHROME_MODES)[number];
export type WorkbenchZoomRatio = (typeof WORKBENCH_ZOOM_RATIOS)[number];
export interface IWorkbenchSettings {
locale: WorkbenchLocale;
region: WorkbenchRegion;
direction: WorkbenchDirection;
theme: WorkbenchTheme;
darkMode: boolean;
ribbonType: WorkbenchRibbonType;
uiChrome: WorkbenchUIChromeMode;
zoomRatio: WorkbenchZoomRatio;
}
export interface IWorkbenchMountOptions extends Omit<IWorkbenchSettings, 'theme'> {
theme: Theme;
}
export interface IWorkbenchUIChromeVisibility {
footer: boolean;
header: boolean;
leftSidebar: boolean;
toolbar: boolean;
}
export const WORKBENCH_LOCALE_META: Record<WorkbenchLocale, { direction: WorkbenchDirection; tag: string }> = {
arSA: { direction: 'rtl', tag: 'ar-SA' },
caES: { direction: 'ltr', tag: 'ca-ES' },
deDE: { direction: 'ltr', tag: 'de-DE' },
enUS: { direction: 'ltr', tag: 'en-US' },
esES: { direction: 'ltr', tag: 'es-ES' },
faIR: { direction: 'rtl', tag: 'fa-IR' },
frFR: { direction: 'ltr', tag: 'fr-FR' },
idID: { direction: 'ltr', tag: 'id-ID' },
itIT: { direction: 'ltr', tag: 'it-IT' },
jaJP: { direction: 'ltr', tag: 'ja-JP' },
koKR: { direction: 'ltr', tag: 'ko-KR' },
plPL: { direction: 'ltr', tag: 'pl-PL' },
ptBR: { direction: 'ltr', tag: 'pt-BR' },
ruRU: { direction: 'ltr', tag: 'ru-RU' },
skSK: { direction: 'ltr', tag: 'sk-SK' },
viVN: { direction: 'ltr', tag: 'vi-VN' },
zhCN: { direction: 'ltr', tag: 'zh-CN' },
zhHK: { direction: 'ltr', tag: 'zh-HK' },
zhTW: { direction: 'ltr', tag: 'zh-TW' },
};
const THEMES = {
blue: blueTheme,
green: greenTheme,
orange: orangeTheme,
purple: purpleTheme,
red: redTheme,
yellow: yellowTheme,
'dark-blue': darkBlueTheme,
} satisfies Record<WorkbenchTheme, Theme>;
const UI_CHROME_VISIBILITY = {
'canvas-only': {
footer: false,
header: false,
leftSidebar: false,
toolbar: false,
},
full: {
footer: true,
header: true,
leftSidebar: true,
toolbar: true,
},
'no-ribbon': {
footer: true,
header: true,
leftSidebar: true,
toolbar: false,
},
} satisfies Record<WorkbenchUIChromeMode, IWorkbenchUIChromeVisibility>;
function getAllowedValue<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {
return typeof value === 'string' && allowed.includes(value as T) ? value as T : fallback;
}
function getAllowedNumber<T extends number>(value: unknown, allowed: readonly T[], fallback: T): T {
return typeof value === 'number' && allowed.includes(value as T) ? value as T : fallback;
}
export function readWorkbenchSettings(storage: Pick<Storage, 'getItem'>): IWorkbenchSettings {
const defaults = getDefaultWorkbenchSettings();
let value: Record<string, unknown> = {};
try {
const storedValue = storage.getItem(WORKBENCH_SETTINGS_STORAGE_KEY);
const parsedValue: unknown = storedValue ? JSON.parse(storedValue) : {};
if (parsedValue && typeof parsedValue === 'object' && !Array.isArray(parsedValue)) {
value = parsedValue as Record<string, unknown>;
}
} catch {
// Ignore invalid development preferences and fall back to defaults.
}
const locale = getAllowedValue(value.locale, WORKBENCH_LOCALES, defaults.locale);
return {
locale,
region: getAllowedValue(value.region, WORKBENCH_REGIONS, defaults.region),
direction: getAllowedValue(value.direction, WORKBENCH_DIRECTIONS, WORKBENCH_LOCALE_META[locale].direction),
theme: getAllowedValue(value.theme, WORKBENCH_THEMES, defaults.theme),
darkMode: value.darkMode === true,
ribbonType: getAllowedValue(value.ribbonType, WORKBENCH_RIBBON_TYPES, defaults.ribbonType),
uiChrome: getAllowedValue(value.uiChrome, WORKBENCH_UI_CHROME_MODES, defaults.uiChrome),
zoomRatio: getAllowedNumber(value.zoomRatio, WORKBENCH_ZOOM_RATIOS, defaults.zoomRatio),
};
}
export function getDefaultWorkbenchSettings(): IWorkbenchSettings {
return {
locale: 'zhCN',
region: 'auto',
direction: 'ltr',
theme: 'blue',
darkMode: false,
ribbonType: 'grid',
uiChrome: 'full',
zoomRatio: 1,
};
}
export function getEffectiveWorkbenchRegion(
settings: Pick<IWorkbenchSettings, 'locale' | 'region'>
): WorkbenchLocale {
return settings.region === 'auto' ? settings.locale : settings.region;
}
export function getWorkbenchUIChromeVisibility(
mode: WorkbenchUIChromeMode
): IWorkbenchUIChromeVisibility {
return UI_CHROME_VISIBILITY[mode];
}
export function writeWorkbenchSettings(storage: Pick<Storage, 'setItem'>, settings: IWorkbenchSettings): void {
storage.setItem(WORKBENCH_SETTINGS_STORAGE_KEY, JSON.stringify(settings));
}
export function clearWorkbenchSettings(storage: Pick<Storage, 'removeItem'>): void {
storage.removeItem(WORKBENCH_SETTINGS_STORAGE_KEY);
}
export function getWorkbenchTheme(theme: WorkbenchTheme): Theme {
return THEMES[theme];
}

View File

@@ -0,0 +1,51 @@
import type { Config } from 'tailwindcss';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import preset, { createTailwindContent } from '@univerjs-infra/shared/tailwind';
import animate from 'tailwindcss-animate';
const configs = {
docs: [
'../presets/packages/preset-docs-core/tailwind.config.ts',
'../presets/packages/preset-docs-drawing/tailwind.config.ts',
'../presets/packages/preset-docs-hyper-link/tailwind.config.ts',
'../presets/packages/preset-docs-thread-comment/tailwind.config.ts',
],
sheets: [
'../presets/packages/preset-sheets-core/tailwind.config.ts',
'../presets/packages/preset-sheets-conditional-formatting/tailwind.config.ts',
'../presets/packages/preset-sheets-data-validation/tailwind.config.ts',
'../presets/packages/preset-sheets-drawing/tailwind.config.ts',
'../presets/packages/preset-sheets-filter/tailwind.config.ts',
'../presets/packages/preset-sheets-find-replace/tailwind.config.ts',
'../presets/packages/preset-sheets-hyper-link/tailwind.config.ts',
'../presets/packages/preset-sheets-note/tailwind.config.ts',
'../presets/packages/preset-sheets-sort/tailwind.config.ts',
'../presets/packages/preset-sheets-table/tailwind.config.ts',
'../presets/packages/preset-sheets-thread-comment/tailwind.config.ts',
],
slides: [
'../packages/slides-ui/tailwind.config.ts',
],
} as const;
export type ExampleTarget = keyof typeof configs;
export function createTailwindConfig(target: ExampleTarget): Config {
const content = new Set<string>([`./src/${target}/**/*.{js,ts,jsx,tsx}`]);
for (const configUrl of configs[target]) {
const configPath = fileURLToPath(new URL(configUrl, import.meta.url));
for (const pattern of createTailwindContent(configPath, { includeStyleDependencies: true })) {
content.add(path.resolve(path.dirname(configPath), pattern));
}
}
return {
presets: [preset],
content: [...content],
plugins: [animate],
};
}
export default createTailwindConfig('sheets');

View File

@@ -0,0 +1,3 @@
import { createTailwindConfig } from './tailwind.config';
export default createTailwindConfig('docs');

View File

@@ -0,0 +1,3 @@
import { createTailwindConfig } from './tailwind.config';
export default createTailwindConfig('sheets');

View File

@@ -0,0 +1,3 @@
import { createTailwindConfig } from './tailwind.config';
export default createTailwindConfig('slides');

7
examples/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/base",
"compilerOptions": {
"types": ["vite/client"]
},
"include": ["src", "tailwind*.config.ts", "vite.config.ts"]
}

201
examples/vite.config.ts Normal file
View File

@@ -0,0 +1,201 @@
import type { Plugin } from 'vite';
import { spawn } from 'node:child_process';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vite';
const DEV_LOCALE_ENDPOINT = '/__univer_examples_locale';
const DEV_LOCALE_EXPORT_SCRIPT = fileURLToPath(new URL('./scripts/export-locale.mts', import.meta.url));
const DEV_LOCALE_PRODUCTS = new Set(['docs', 'sheets', 'slides']);
const DEV_TSX_IMPORT = fileURLToPath(import.meta.resolve('tsx'));
const DEV_LOCALE_TAGS = new Set([
'ar-SA',
'ca-ES',
'de-DE',
'en-US',
'es-ES',
'fa-IR',
'fr-FR',
'id-ID',
'it-IT',
'ja-JP',
'ko-KR',
'pl-PL',
'pt-BR',
'ru-RU',
'sk-SK',
'vi-VN',
'zh-CN',
'zh-HK',
'zh-TW',
]);
type DevLocaleProduct = 'docs' | 'sheets' | 'slides';
const VIRTUAL_LOCALE_MODULES = {
'virtual:univer-examples-document-locale': {
development: fileURLToPath(new URL('./src/docs/locale-loader.dev.ts', import.meta.url)),
exportName: 'loadDocumentLocale',
production: fileURLToPath(new URL('./src/docs/locale-loader.ts', import.meta.url)),
},
'virtual:univer-examples-sheet-locale': {
development: fileURLToPath(new URL('./src/sheets/locale-loader.dev.ts', import.meta.url)),
exportName: 'loadSheetLocale',
production: fileURLToPath(new URL('./src/sheets/locale-loader.ts', import.meta.url)),
},
'virtual:univer-examples-slide-locale': {
development: fileURLToPath(new URL('./src/slides/locale-loader.dev.ts', import.meta.url)),
exportName: 'loadSlideLocale',
production: fileURLToPath(new URL('./src/slides/locale-loader.ts', import.meta.url)),
},
} as const;
type VirtualLocaleModule = keyof typeof VIRTUAL_LOCALE_MODULES;
function isDevLocaleProduct(value: string | null): value is DevLocaleProduct {
return value !== null && DEV_LOCALE_PRODUCTS.has(value);
}
function isVirtualLocaleModule(value: string): value is VirtualLocaleModule {
return Object.hasOwn(VIRTUAL_LOCALE_MODULES, value);
}
function exportDevLocale(product: DevLocaleProduct, locale: string): Promise<string> {
return new Promise((resolve, reject) => {
const child = spawn(
process.execPath,
['--import', DEV_TSX_IMPORT, DEV_LOCALE_EXPORT_SCRIPT, product, locale],
{
env: { ...process.env, NODE_NO_WARNINGS: '1' },
stdio: ['ignore', 'pipe', 'pipe'],
}
);
let output = '';
let errorOutput = '';
child.stdout.setEncoding('utf8');
child.stdout.on('data', (chunk: string) => {
output += chunk;
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', (chunk: string) => {
errorOutput += chunk;
});
child.on('error', reject);
child.on('close', (code) => {
if (code === 0) {
resolve(output);
return;
}
reject(new Error(errorOutput.trim() || `Locale exporter exited with code ${code}.`));
});
});
}
/**
* bundledDev follows every literal locale import. During development, load the
* requested locale in a short-lived process so the other locale graphs never
* become resident in Vite. Production keeps the regular static locale chunks.
*/
function workbenchLocaleLoader(command: 'build' | 'serve'): Plugin {
const cache = new Map<string, Promise<string>>();
return {
name: 'workbench-locale-loader',
resolveId(source) {
return isVirtualLocaleModule(source) ? `\0${source}` : null;
},
load(id) {
const source = id.startsWith('\0') ? id.slice(1) : id;
if (!isVirtualLocaleModule(source)) {
return null;
}
const localeModule = VIRTUAL_LOCALE_MODULES[source];
const implementation = command === 'serve' ? localeModule.development : localeModule.production;
return `export { ${localeModule.exportName} } from ${JSON.stringify(implementation)};`;
},
configureServer(server) {
server.watcher.on('change', (file) => {
if (/\/src\/locales?\//.test(file)) {
cache.clear();
}
});
server.middlewares.use(async (request, response, next) => {
if (!request.url) {
next();
return;
}
const requestUrl = new URL(request.url, 'http://localhost');
if (requestUrl.pathname !== DEV_LOCALE_ENDPOINT) {
next();
return;
}
const product = requestUrl.searchParams.get('product');
const locale = requestUrl.searchParams.get('locale');
if (!isDevLocaleProduct(product) || !locale || !DEV_LOCALE_TAGS.has(locale)) {
response.statusCode = 400;
response.setHeader('content-type', 'application/json; charset=utf-8');
response.end(JSON.stringify({ error: 'Unsupported product or locale.' }));
return;
}
try {
const cacheKey = `${product}:${locale}`;
let localeData = cache.get(cacheKey);
if (!localeData) {
localeData = exportDevLocale(product, locale).catch((error) => {
cache.delete(cacheKey);
throw error;
});
cache.set(cacheKey, localeData);
}
response.statusCode = 200;
response.setHeader('cache-control', 'no-store');
response.setHeader('content-type', 'application/json; charset=utf-8');
response.end(await localeData);
} catch (error) {
response.statusCode = 500;
response.setHeader('content-type', 'application/json; charset=utf-8');
response.end(JSON.stringify({
error: error instanceof Error ? error.message : String(error),
}));
}
});
},
};
}
function ignoreWorkspaceGlobalCss(): Plugin {
return {
name: 'ignore-workspace-global-css',
enforce: 'pre',
transform: {
filter: { id: /\/packages\/.*\/src\/global\.css$/ },
handler() {
return '';
},
},
};
}
export default defineConfig(({ command, mode }) => ({
build: {
sourcemap: false,
},
experimental: {
bundledDev: command === 'serve' && mode !== 'test',
},
plugins: [workbenchLocaleLoader(command), ignoreWorkspaceGlobalCss()],
preview: {
port: 5173,
strictPort: true,
},
server: {
port: 5173,
strictPort: true,
},
}));