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

32
common/debugger/README.md Normal file
View File

@@ -0,0 +1,32 @@
# @univerjs/debugger
> [!CAUTION]
> NEVER use this plugin in your production environment!
## Package Overview
| Package Name | UMD Namespace | Version | License | Downloads | Contains CSS | Contains i18n locales |
| --- | --- | --- | --- | --- | :---: | :---: |
| `@univerjs/debugger` | `UniverDebugger` | [![][npm-version-shield]][npm-version-link] | ![][npm-license-shield] | ![][npm-downloads-shield] | ⭕️ | ❌ |
## Introduction
> This plugin provides a lot of utilities to help you debug Univer.
## Usage
### Installation
```shell
# Using npm
npm install @univerjs/debugger
# Using pnpm
pnpm add @univerjs/debugger
```
<!-- Links -->
[npm-version-shield]: https://img.shields.io/npm/v/@univerjs/debugger?style=flat-square
[npm-version-link]: https://npmjs.com/package/@univerjs/debugger
[npm-license-shield]: https://img.shields.io/npm/l/@univerjs/debugger?style=flat-square
[npm-downloads-shield]: https://img.shields.io/npm/dm/@univerjs/debugger?style=flat-square

View File

@@ -0,0 +1,54 @@
{
"name": "@univerjs/debugger",
"version": "1.0.0-rc.0",
"private": true,
"description": "",
"author": "DreamNum <developer@univer.ai>",
"license": "Apache-2.0",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/univer"
},
"homepage": "https://univer.ai",
"repository": {
"type": "git",
"url": "https://github.com/dream-num/univer"
},
"bugs": {
"url": "https://github.com/dream-num/univer/issues"
},
"keywords": [],
"exports": {
".": "./src/index.ts",
"./*": "./src/*"
},
"main": "./src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"rxjs": ">=7.0.0"
},
"dependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/docs-drawing-ui": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "1.38.0",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-drawing-ui": "workspace:*",
"@univerjs/themes": "workspace:*",
"@univerjs/ui": "workspace:*",
"@univerjs/watermark": "workspace:*"
},
"devDependencies": {
"@univerjs-infra/shared": "workspace:*",
"postcss": "^8.5.28",
"react": "18.3.1",
"rxjs": "^7.8.2",
"tailwindcss": "3.4.18",
"typescript": "^6.0.3",
"vue": "^3.5.42"
}
}

View File

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

View File

@@ -0,0 +1,68 @@
export const FloatButton = () => {
const divStyle = {
width: '100px',
height: '30px',
backgroundColor: '#fff',
border: '1px solid #ccc',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center' as const,
};
const clickHandler = () => {
console.warn('click');
};
return (
<div style={divStyle} onClick={clickHandler}>
FloatButton
</div>
);
};
export const AIButton = () => {
const divStyle = {
width: '80px',
height: '50px',
backgroundColor: '#fff',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center' as const,
borderRadius: '25px',
border: 'none',
color: 'white',
cursor: 'pointer',
transition: 'all 0.3s ease',
background: 'linear-gradient(90deg, #00C9FF 0%, #92FE9D 50%, #00C9FF 100%)',
backgroundSize: '200% auto',
animation: 'gradient 3s linear infinite',
':hover': {
transform: 'translateY(-2px)',
boxShadow: '0 10px 20px rgba(0, 201, 255, 0.3)',
},
};
const clickHandler = () => {
console.warn('click');
};
return (
<button type="button" style={divStyle} onClick={clickHandler}>
AI
<style>
{`
@keyframes gradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 201, 255, 0.3);
}
`}
</style>
</button>
);
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
import { borderClassName, clsx } from '@univerjs/design';
export const RangeLoading = () => {
return (
<div
className={clsx(`
univer-flex univer-size-full univer-origin-top-left univer-items-center univer-justify-center
univer-bg-gray-0
dark:!univer-bg-gray-900
`, borderClassName)}
>
Loading...
</div>
);
};

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { ref } from 'vue';
const state = ref<'sun' | 'moon'>('sun');
function toggleState() {
state.value = state.value === 'sun' ? 'moon' : 'sun';
}
</script>
<template>
<div @click="toggleState">
<span v-if="state === 'sun'">🌞</span>
<span v-if="state === 'moon'">🌚</span>
</div>
</template>

View File

@@ -0,0 +1,47 @@
export class CounterComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const container = document.createElement('div');
container.setAttribute('class', 'counter-container');
const countDisplay = document.createElement('span');
countDisplay.setAttribute('class', 'count-display');
countDisplay.textContent = '0';
const a = document.createElement('a');
a.textContent = '🍔';
a.addEventListener('click', (e) => {
e.preventDefault();
const currentCount = Number.parseInt(countDisplay.textContent || '0', 10);
countDisplay.textContent = (currentCount + 1).toString();
});
const style = document.createElement('style');
style.textContent = `
.counter-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.count-display {
position: relative;
top: 2px;
font-size: 10px;
}
a {
font-size: 10px;
}`;
shadow.appendChild(style);
shadow.appendChild(container);
container.appendChild(countDisplay);
container.appendChild(a);
}
}

View File

@@ -0,0 +1,25 @@
import type { ILanguagePack, LocaleType, UniverInstanceType } from '@univerjs/core';
import type { MenuConfig } from '@univerjs/ui';
export const DEBUGGER_PLUGIN_CONFIG_KEY = 'debugger.config';
export const configSymbol = Symbol(DEBUGGER_PLUGIN_CONFIG_KEY);
export type UniverDebuggerLocaleLoader = (locale: LocaleType) => ILanguagePack | Promise<ILanguagePack>;
export interface IUniverDebuggerConfig {
menu?: MenuConfig;
fab?: boolean;
fabEntryUnitType: UniverInstanceType;
localeLoader: UniverDebuggerLocaleLoader;
performanceMonitor?: {
enabled: boolean;
};
}
export const defaultPluginConfig: Pick<IUniverDebuggerConfig, 'fab' | 'performanceMonitor'> = {
fab: true,
performanceMonitor: {
enabled: true,
},
};

View File

@@ -0,0 +1,44 @@
import { Disposable, Inject } from '@univerjs/core';
import { ComponentManager } from '@univerjs/ui';
import { AIButton, FloatButton } from '../components/FloatButton';
import { ImageDemo } from '../components/Image';
import { RangeLoading } from '../components/RangeLoading';
// @ts-ignore
// import VueComponent from '../components/VueComponent.vue';
// import { CounterComponent } from '../components/WebComponent';
import { WatermarkPanel } from '../views/watermark/WatermarkPanel';
import { WatermarkPanelFooter } from '../views/watermark/WatermarkPanelFooter';
export const WATERMARK_PANEL = 'WATERMARK_PANEL';
export const WATERMARK_PANEL_FOOTER = 'WATERMARK_PANEL_FOOTER';
export class ComponentsController extends Disposable {
constructor(
@Inject(ComponentManager) private readonly _componentManager: ComponentManager
) {
super();
this._registerComponents();
}
private _registerComponents(): void {
([
['ImageDemo', ImageDemo],
['RangeLoading', RangeLoading],
['FloatButton', FloatButton],
['AIButton', AIButton],
[WATERMARK_PANEL, WatermarkPanel],
[WATERMARK_PANEL_FOOTER, WatermarkPanelFooter],
] as const).forEach(([key, component]) => {
this.disposeWithMe(this._componentManager.register(key, component));
});
// this.disposeWithMe(this._componentManager.register('VueComponent', VueComponent, {
// framework: 'vue3',
// }));
// this.disposeWithMe(this._componentManager.register('counter-component', CounterComponent, {
// framework: 'web-component',
// }));
}
}

View File

@@ -0,0 +1,62 @@
import type { IUniverDebuggerConfig } from '../config/config';
import { Disposable, IConfigService, Inject, Injector } from '@univerjs/core';
import {
BuiltInUIPart,
connectInjector,
IMenuManagerService,
IUIPartsService,
// MenuItemType,
// RibbonStartGroup,
} from '@univerjs/ui';
import { DEBUGGER_PLUGIN_CONFIG_KEY } from '../config/config';
import { Fab } from '../views/Fab';
import { RecordController } from './local-save/record.controller';
export class DebuggerController extends Disposable {
constructor(
@Inject(Injector) private readonly _injector: Injector,
@IConfigService private readonly _configService: IConfigService,
@IUIPartsService protected readonly _uiPartsService: IUIPartsService,
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService
) {
super();
this._initCustomComponents();
// this._menuManagerService.mergeMenu({
// [RibbonStartGroup.OTHERS]: {
// EMOJI: {
// order: 9999,
// menuItemFactory: () => {
// return {
// id: 'EMOJI',
// icon: 'VueComponent',
// type: MenuItemType.BUTTON,
// };
// },
// },
// COUNTER: {
// order: 9999,
// menuItemFactory: () => {
// return {
// id: 'COUNTER',
// icon: 'counter-component',
// type: MenuItemType.BUTTON,
// };
// },
// },
// },
// });
this._injector.add([RecordController]);
}
private _initCustomComponents(): void {
const configs = this._configService.getConfig<IUniverDebuggerConfig>(DEBUGGER_PLUGIN_CONFIG_KEY);
if (configs?.fab) {
this.disposeWithMe(
this._uiPartsService.registerComponent(BuiltInUIPart.GLOBAL, () => connectInjector(Fab, this._injector))
);
}
}
}

View File

@@ -0,0 +1,59 @@
import { CommandType, ICommandService, Inject } from '@univerjs/core';
import { Observable } from 'rxjs';
export class RecordController {
constructor(@Inject(ICommandService) private _commandService: ICommandService) {
// empty
}
record() {
return new Observable<{ type: 'start' } | { type: 'finish'; data: Blob }>((subscribe) => {
navigator.mediaDevices.getDisplayMedia({ video: true }).then((stream) => {
subscribe.next({ type: 'start' });
const mime = MediaRecorder.isTypeSupported('video/webm; codecs=vp9')
? 'video/webm; codecs=vp9'
: 'video/webm';
const mediaRecorder = new MediaRecorder(stream, { mimeType: mime });
const chunks: Blob[] = [];
mediaRecorder.addEventListener('dataavailable', function (e) {
chunks.push(e.data);
});
mediaRecorder.addEventListener('stop', function () {
const blob = new Blob(chunks, { type: chunks[0].type });
subscribe.next({ type: 'finish', data: blob });
subscribe.complete();
});
mediaRecorder.start();
});
});
}
startSaveCommands() {
type SecondsString = string & {};
type CommandString = string & {};
type TypeString = string & {};
type ParamsString = string & {};
const result: Array<[SecondsString, CommandString, TypeString, ParamsString]> = [];
const startTime = performance.now();
const disposable = this._commandService.beforeCommandExecuted((commandInfo) => {
try {
result.push([
String((performance.now() - startTime) / 1000),
commandInfo.id,
String(commandInfo.type || CommandType.COMMAND),
JSON.stringify(commandInfo.params || ''),
]);
} catch (err) {
console.error(`${commandInfo.id} unable to serialize`);
console.error(err);
}
});
return () => {
disposable.dispose();
return result;
};
}
}

View File

@@ -0,0 +1,8 @@
export function createDownloadElement(fileName: string, blob: Blob) {
const a = document.createElement('a');
a.download = fileName;
a.href = window.URL.createObjectURL(blob);
return () => {
a.click();
};
}

View File

@@ -0,0 +1,60 @@
import type { Nullable } from '@univerjs/core';
import type { Subscription } from 'rxjs';
import { Inject, IUniverInstanceService, LifecycleService, LifecycleStages, RxDisposable } from '@univerjs/core';
import { IRenderManagerService } from '@univerjs/engine-render';
import { distinctUntilChanged, filter, take, takeUntil } from 'rxjs';
export class PerformanceMonitorController extends RxDisposable {
private _containerElement: HTMLDivElement | null;
private _currentUnitSub: Nullable<Subscription>;
constructor(
@Inject(LifecycleService) lifecycleService: LifecycleService,
@IUniverInstanceService private readonly _instanceService: IUniverInstanceService,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
) {
super();
lifecycleService.subscribeWithPrevious()
.pipe(
filter((stage) => stage === LifecycleStages.Rendered),
take(1)
)
.subscribe(() => this._listenDocumentTypeChange());
}
override dispose(): void {
super.dispose();
this._disposeCurrentObserver();
}
private _disposeCurrentObserver(): void {
this._currentUnitSub?.unsubscribe();
this._currentUnitSub = null;
}
private _listenDocumentTypeChange() {
this._instanceService.focused$.pipe(takeUntil(this.dispose$), distinctUntilChanged()).subscribe((unitId) => {
this._disposeCurrentObserver();
if (unitId) {
this._listenToRenderer(unitId);
}
});
}
private _listenToRenderer(unitId: string) {
const renderer = this._renderManagerService.getRenderUnitById(unitId);
if (renderer) {
const { engine } = renderer;
this._currentUnitSub = engine.endFrame$.subscribe(() => {
if (!this._containerElement) {
this._containerElement = document.querySelector('[data-u-comp=debugger-fps]');
} else {
this._containerElement.textContent = `FPS: ${Math.round(engine.getFps()).toString()}`;
}
});
}
}
}

View File

@@ -0,0 +1,2 @@
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,4 @@
import './global.css';
export type { IUniverDebuggerConfig, UniverDebuggerLocaleLoader } from './config/config';
export { UniverDebuggerPlugin } from './plugin';

View File

@@ -0,0 +1,66 @@
import type { Dependency } from '@univerjs/core';
import type { IUniverDebuggerConfig } from './config/config';
import { IConfigService, Inject, Injector, merge, Plugin, registerDependencies, touchDependencies } from '@univerjs/core';
import pkg from '../package.json';
import { DEBUGGER_PLUGIN_CONFIG_KEY, defaultPluginConfig } from './config/config';
import { ComponentsController } from './controllers/components.controller';
import { DebuggerController } from './controllers/debugger.controller';
import { PerformanceMonitorController } from './controllers/performance-monitor.controller';
export class UniverDebuggerPlugin extends Plugin {
static override pluginName = 'UNIVER_DEBUGGER_PLUGIN';
static override packageName = pkg.name;
static override version = pkg.version;
private _debuggerController!: DebuggerController;
constructor(
private readonly _config: IUniverDebuggerConfig,
@Inject(Injector) override readonly _injector: Injector,
@IConfigService private readonly _configService: IConfigService
) {
super();
// Manage the plugin configuration.
const { menu, ...rest } = merge(
{},
defaultPluginConfig,
this._config
);
if (menu) {
this._configService.setConfig('menu', menu, { merge: true });
}
this._configService.setConfig(DEBUGGER_PLUGIN_CONFIG_KEY, rest);
}
override onStarting(): void {
const dependencies: Dependency[] = [
[DebuggerController],
[ComponentsController],
];
if (this._config.performanceMonitor?.enabled !== false) {
dependencies.push([PerformanceMonitorController]);
}
registerDependencies(this._injector, dependencies);
}
override onReady(): void {
touchDependencies(this._injector, [
[ComponentsController],
[DebuggerController],
]);
}
override onRendered(): void {
touchDependencies(this._injector, [
[PerformanceMonitorController],
]);
}
getDebuggerController() {
this._debuggerController = this._injector.get(DebuggerController);
return this._debuggerController;
}
}

View File

@@ -0,0 +1,116 @@
import type { IDropdownMenuProps } from '@univerjs/design';
import type { IUniverDebuggerConfig } from '../config/config';
import { IConfigService, UniverInstanceType } from '@univerjs/core';
import { borderClassName, clsx, DropdownMenu } from '@univerjs/design';
import { useDependency } from '@univerjs/ui';
import { DEBUGGER_PLUGIN_CONFIG_KEY } from '../config/config';
import { useCellContent } from './use-cell-content';
import { useDarkMode } from './use-dark-mode';
import { useDialog } from './use-dialog';
import { useDispose } from './use-dispose';
import { useEditable } from './use-editable';
import { useFloatingDom } from './use-floating-dom';
import { useLocale } from './use-locale';
import { useMessage } from './use-message';
import { useNotification } from './use-notification';
import { useRTL } from './use-rtl';
import { useSidebar } from './use-sidebar';
import { useSnapshot } from './use-snapshot';
import { useTheme } from './use-theme';
import { useUnits } from './use-units';
import { useUser } from './use-user';
import { useWatermark } from './use-watermark';
export function Fab() {
const configService = useDependency(IConfigService);
const configs = configService.getConfig<IUniverDebuggerConfig>(DEBUGGER_PLUGIN_CONFIG_KEY);
const performanceMonitor = configs?.performanceMonitor;
const fabEntryUnitType = configs?.fabEntryUnitType;
const locale = useLocale();
const rtl = useRTL();
const darkMode = useDarkMode();
const theme = useTheme();
const watermark = useWatermark();
const notification = useNotification();
const message = useMessage();
const dialog = useDialog();
const sidebar = useSidebar();
const floatingDom = useFloatingDom(fabEntryUnitType);
const cellContent = useCellContent(fabEntryUnitType);
const units = useUnits();
const snapshot = useSnapshot();
const editable = useEditable();
const user = useUser();
const dispose = useDispose();
const globalItems = [locale, rtl, darkMode, theme];
const lightweightItems = [
...globalItems,
watermark,
];
const commonDebugItems = [
watermark,
{ type: 'separator' as const },
notification,
message,
dialog,
sidebar,
{ type: 'separator' as const },
snapshot,
editable,
dispose,
];
const sheetItems = [
...globalItems,
watermark,
{ type: 'separator' as const },
notification,
message,
dialog,
sidebar,
{ type: 'separator' as const },
floatingDom,
cellContent,
units,
snapshot,
editable,
user,
dispose,
];
const items: IDropdownMenuProps['items'] = fabEntryUnitType === UniverInstanceType.UNIVER_BASE || fabEntryUnitType === UniverInstanceType.UNIVER_SLIDE
? lightweightItems
: fabEntryUnitType === UniverInstanceType.UNIVER_DOC
? [...globalItems, ...commonDebugItems, floatingDom].filter(Boolean) as IDropdownMenuProps['items']
: sheetItems.filter(Boolean) as IDropdownMenuProps['items'];
return (
<div
data-u-comp="debugger-fab"
className={`
univer-fixed univer-bottom-12 univer-right-8 univer-z-[9999] univer-flex univer-select-none
univer-flex-col univer-items-center univer-gap-1
`}
>
<DropdownMenu align="end" items={items}>
<button
className={clsx(`
univer-flex univer-size-9 univer-cursor-pointer univer-items-center univer-justify-center
univer-rounded-full univer-bg-gray-0 univer-text-base univer-text-gray-900 univer-shadow
univer-outline-none univer-transition-shadow
hover:univer-ring-1 hover:univer-ring-primary-400
dark:!univer-bg-gray-900 dark:!univer-text-gray-200
`, borderClassName)}
type="button"
>
🏗️
</button>
</DropdownMenu>
{performanceMonitor?.enabled && (
<span data-u-comp="debugger-fps" className="univer-text-xs univer-text-gray-400" />
)}
</div>
);
}

View File

@@ -0,0 +1 @@
export const TEST_EDITOR_CONTAINER_COMPONENT = 'TestEditorContainer';

View File

@@ -0,0 +1,28 @@
import { ILogService, IUniverInstanceService, ObjectMatrix, Range, UniverInstanceType } from '@univerjs/core';
import { getSheetCommandTarget, SheetsSelectionsService } from '@univerjs/sheets';
import { useDependency } from '@univerjs/ui';
export function useCellContent(fabEntryUnitType?: UniverInstanceType) {
const logService = useDependency(ILogService);
const selectionManagerService = fabEntryUnitType === UniverInstanceType.UNIVER_SHEET ? useDependency(SheetsSelectionsService) : null;
const univerInstanceService = useDependency(IUniverInstanceService);
const onSelect = () => {
if (!selectionManagerService) return;
const selections = selectionManagerService.getCurrentSelections();
const target = getSheetCommandTarget(univerInstanceService);
const matrix = new ObjectMatrix();
selections.forEach((selection) => {
Range.foreach(selection.range, (row, col) => {
matrix.setValue(row, col, target?.worksheet.getCell(row, col));
});
});
logService.debug('cell-content', matrix);
};
return {
type: 'item' as const,
children: '🗒️ Console cell content',
onSelect,
};
}

View File

@@ -0,0 +1,31 @@
import { ThemeService } from '@univerjs/core';
import { useDependency } from '@univerjs/ui';
import { useEffect } from 'react';
export function useDarkMode() {
const themeService = useDependency(ThemeService);
useEffect(() => {
const darkMode = localStorage.getItem('local.darkMode');
if (darkMode === 'dark') {
document.documentElement.classList.add('univer-dark');
themeService.setDarkMode(true);
} else if (darkMode === 'system') {
themeService.setDarkMode(false);
}
}, []);
const onSelect = () => {
const darkMode = themeService.darkMode ? 'light' : 'dark';
themeService.setDarkMode(darkMode === 'dark');
localStorage.setItem('local.darkMode', darkMode);
};
return {
type: 'item' as const,
children: '🌓 Toggle dark mode',
onSelect,
};
}

View File

@@ -0,0 +1,89 @@
import { IConfirmService } from '@univerjs/core';
import { IDialogService, useDependency } from '@univerjs/ui';
const menu = [
{
label: 'Open dialog',
value: 'dialog',
},
{
label: 'Draggable dialog',
value: 'draggable',
},
{
label: 'Open confirm',
value: 'confirm',
},
];
export function useDialog() {
const dialogService = useDependency(IDialogService);
const confirmService = useDependency(IConfirmService);
const onSelect = (value: string) => {
if (value === 'draggable') {
dialogService.open({
id: 'draggable',
children: { title: 'Draggable Dialog Content' },
title: { title: 'Draggable Dialog' },
draggable: true,
preservePositionOnDestroy: true,
width: 350,
onClose() {
// dialogService.close('draggable');
},
onOpenChange(open: boolean) {
if (!open) {
dialogService.close('draggable');
}
},
});
} else if (value === 'dialog') {
dialogService.open({
id: 'dialog1',
children: { title: 'Dialog Content' },
footer: { title: 'Dialog Footer' },
title: { title: 'Dialog Title' },
draggable: false,
onClose() {
// dialogService.close('dialog1');
},
onOpenChange(open: boolean) {
if (!open) {
dialogService.close('dialog1');
}
},
});
} else if (value === 'confirm') {
confirmService.open({
id: 'confirm1',
children: { title: 'Confirm Content' },
title: { title: 'Confirm Title' },
confirmText: 'hello',
cancelText: 'world',
onClose() {
confirmService.close('confirm1');
},
});
confirmService.open({
id: 'confirm2',
children: { title: 'Confirm2 Content' },
title: { title: 'Confirm2 Title' },
onClose() {
confirmService.close('confirm2');
},
});
}
};
return {
type: 'subItem' as const,
children: '💬 Dialog & Confirm',
options: menu.map((item) => ({
type: 'item' as const,
children: item.label,
onSelect: () => onSelect(item.value),
})),
};
}

View File

@@ -0,0 +1,42 @@
import type { Univer } from '@univerjs/core';
import { IUniverInstanceService } from '@univerjs/core';
import { useDependency } from '@univerjs/ui';
const menu = [
{
label: 'Dispose Univer',
value: 'univer',
},
{
label: 'Dispose current unit',
value: 'unit',
},
];
export function useDispose() {
const univerInstanceService = useDependency(IUniverInstanceService);
const onSelect = (value: string) => {
if (value === 'univer') {
const debuggerWindow = window as typeof window & { univer?: Univer };
debuggerWindow.univer?.dispose();
Reflect.deleteProperty(debuggerWindow, 'univer');
Reflect.deleteProperty(debuggerWindow, 'univerAPI');
} else if (value === 'unit') {
const focused = univerInstanceService.getFocusedUnit();
if (!focused) return false;
return univerInstanceService.disposeUnit(focused.getUnitId());
}
};
return {
type: 'subItem' as const,
children: '🗑️ Dispose',
options: menu.map((item) => ({
type: 'item' as const,
children: item.label,
onSelect: () => onSelect(item.value),
})),
};
}

View File

@@ -0,0 +1,48 @@
import { IPermissionService, IUniverInstanceService } from '@univerjs/core';
import { getSheetCommandTarget, WorkbookEditablePermission, WorksheetEditPermission } from '@univerjs/sheets';
import { useDependency } from '@univerjs/ui';
const menu = [
{
label: 'Change workbook editable',
value: 'univer',
},
{
label: 'Change worksheet editable',
value: 'sheet',
},
];
export function useEditable() {
const univerInstanceService = useDependency(IUniverInstanceService);
const permissionService = useDependency(IPermissionService);
const onSelect = (value: string) => {
const target = getSheetCommandTarget(univerInstanceService);
if (!target) {
return false;
}
const { workbook, worksheet, unitId, subUnitId } = target;
if (!workbook || !worksheet) {
return false;
}
if (value === 'sheet') {
const editable = permissionService.getPermissionPoint(new WorksheetEditPermission(unitId, subUnitId).id);
permissionService.updatePermissionPoint(new WorksheetEditPermission(unitId, subUnitId).id, !editable);
} else {
const unitId = workbook!.getUnitId();
const editable = permissionService.getPermissionPoint(new WorkbookEditablePermission(unitId).id);
permissionService.updatePermissionPoint(new WorkbookEditablePermission(unitId).id, !editable);
}
};
return {
type: 'subItem' as const,
children: '✍️ Editable',
options: menu.map((item) => ({
type: 'item' as const,
children: item.label,
onSelect: () => onSelect(item.value),
})),
};
}

View File

@@ -0,0 +1,50 @@
import { IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { DocFloatDomController } from '@univerjs/docs-drawing-ui';
import { SheetCanvasFloatDomManagerService } from '@univerjs/sheets-drawing-ui';
import { useDependency } from '@univerjs/ui';
export function useFloatingDom(entryUnitType?: UniverInstanceType) {
const univerInstanceService = useDependency(IUniverInstanceService);
const floatDomService = entryUnitType === UniverInstanceType.UNIVER_SHEET ? useDependency(SheetCanvasFloatDomManagerService) : null;
const floatDomController = entryUnitType === UniverInstanceType.UNIVER_DOC ? useDependency(DocFloatDomController) : null;
const onSelect = () => {
if (entryUnitType === UniverInstanceType.UNIVER_SHEET) {
const currentSheet = univerInstanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_SHEET);
if (!currentSheet) return;
floatDomService?.addFloatDomToPosition({
allowTransform: true,
initPosition: {
startX: 200,
endX: 400,
startY: 200,
endY: 400,
},
componentKey: 'ImageDemo',
data: {
aa: '128',
},
});
} else if (entryUnitType === UniverInstanceType.UNIVER_DOC) {
const currentDoc = univerInstanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_DOC);
if (!currentDoc) return;
floatDomController?.insertFloatDom({
allowTransform: true,
componentKey: 'ImageDemo',
data: {
aa: '128',
},
}, {
height: 300,
});
}
};
return {
type: 'item' as const,
children: '☁️ Create floating DOM',
onSelect,
};
}

View File

@@ -0,0 +1,78 @@
import type { IUniverDebuggerConfig } from '../config/config';
import { IConfigService, LocaleService, LocaleType } from '@univerjs/core';
import { useDependency } from '@univerjs/ui';
import { useCallback, useEffect } from 'react';
import { DEBUGGER_PLUGIN_CONFIG_KEY } from '../config/config';
const localeLabels: Record<LocaleType, string> = {
[LocaleType.AR_SA]: 'العربية',
[LocaleType.CA_ES]: 'Català',
[LocaleType.DE_DE]: 'Deutsch',
[LocaleType.EN_US]: 'English',
[LocaleType.ES_ES]: 'Español',
[LocaleType.FA_IR]: 'فارسی',
[LocaleType.FR_FR]: 'Français',
[LocaleType.ID_ID]: 'Bahasa Indonesia',
[LocaleType.IT_IT]: 'Italiano',
[LocaleType.JA_JP]: '日本語',
[LocaleType.KO_KR]: '한국어',
[LocaleType.PL_PL]: 'Polski',
[LocaleType.PT_BR]: 'Português (Brasil)',
[LocaleType.RU_RU]: 'Русский',
[LocaleType.SK_SK]: 'Slovenčina',
[LocaleType.VI_VN]: 'Tiếng Việt',
[LocaleType.ZH_CN]: '简体中文',
[LocaleType.ZH_HK]: '繁體中文(香港)',
[LocaleType.ZH_TW]: '繁體中文(台灣)',
};
export function useLocale() {
const configService = useDependency(IConfigService);
const localeService = useDependency(LocaleService);
const configs = configService.getConfig<IUniverDebuggerConfig>(DEBUGGER_PLUGIN_CONFIG_KEY);
const localeLoader = configs?.localeLoader;
const loadLocales = useCallback(async (value: string) => {
const locale = value as LocaleType;
if (!localeLoader) {
throw new Error('[UniverDebuggerPlugin]: localeLoader is required.');
}
const localePack = await localeLoader(locale);
localeService.load({
[locale]: localePack,
});
}, [localeLoader, localeService]);
useEffect(() => {
const locale = localStorage.getItem('local.locale');
if (locale) {
loadLocales(locale).then(() => {
localeService.setLocale(locale as LocaleType);
});
}
}, [loadLocales, localeService]);
const onSelect = async (value: string) => {
await loadLocales(value);
localeService.setLocale(value as LocaleType);
localStorage.setItem('local.locale', value);
};
return {
type: 'subItem' as const,
children: '🌐 Languages',
options: Object.values(LocaleType).map((locale) => ({
type: 'radio' as const,
value: localeService.getCurrentLocale(),
options: [{
label: localeLabels[locale],
value: locale,
}],
onSelect,
})),
};
}

View File

@@ -0,0 +1,43 @@
import { MessageType } from '@univerjs/design';
import { IMessageService, useDependency } from '@univerjs/ui';
const menu = [
{
label: 'Open message',
value: '',
},
{
label: 'Open loading message',
value: 'loading',
},
];
export function useMessage() {
const messageService = useDependency(IMessageService);
const onSelect = (value: string) => {
if (value === 'loading') {
messageService.show({
type: MessageType.Loading,
content: 'Loading message',
duration: 3000,
});
} else {
messageService.show({
type: MessageType.Success,
content: 'Demo message',
duration: 1500,
});
}
};
return {
type: 'subItem' as const,
children: '✉️ Message',
options: menu.map((item) => ({
type: 'item' as const,
children: item.label,
onSelect: () => onSelect(item.value),
})),
};
}

View File

@@ -0,0 +1,43 @@
import type { INotificationOptions } from '@univerjs/ui';
import { INotificationService, useDependency } from '@univerjs/ui';
const menu = [
{
label: 'Notification success',
value: 'success',
},
{
label: 'Notification info',
value: 'info',
},
{
label: 'Notification warning',
value: 'warning',
},
{
label: 'Notification error',
value: 'error',
},
];
export function useNotification() {
const notificationService = useDependency(INotificationService);
const onSelect = (value: string) => {
notificationService.show({
type: value as INotificationOptions['type'],
content: 'Lorem Ipusm dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
title: 'Notification Title',
});
};
return {
type: 'subItem' as const,
children: '🔔 Notification',
options: menu.map((item) => ({
type: 'item' as const,
children: item.label,
onSelect: () => onSelect(item.value),
})),
};
}

View File

@@ -0,0 +1,29 @@
import { LocaleService } from '@univerjs/core';
import { useDependency } from '@univerjs/ui';
import { useEffect } from 'react';
export function useRTL() {
const localeService = useDependency(LocaleService);
useEffect(() => {
const dir = localStorage.getItem('local.direction');
if (dir === 'rtl' || dir === 'ltr') {
localeService.setDirection(dir);
}
}, [localeService]);
const onSelect = () => {
const current = localeService.getDirection();
const nextDirection = current === 'rtl' ? 'ltr' : 'rtl';
localeService.setDirection(nextDirection);
localStorage.setItem('local.direction', nextDirection);
};
return {
type: 'item' as const,
children: '↔️ Toggle RTL',
onSelect,
};
}

View File

@@ -0,0 +1,41 @@
import { ISidebarService, useDependency } from '@univerjs/ui';
import { TEST_EDITOR_CONTAINER_COMPONENT } from './test-editor/component-name';
const menu = [
{
label: 'Open sidebar',
value: 'open',
},
{
label: 'Close sidebar',
value: 'close',
},
];
export function useSidebar() {
const sidebarService = useDependency(ISidebarService);
const onSelect = (value: string) => {
if (value === 'open') {
sidebarService.open({
header: { title: 'Sidebar title' },
children: { label: TEST_EDITOR_CONTAINER_COMPONENT },
footer: { title: 'Sidebar Footer' },
onClose: () => {
},
});
} else if (value === 'close') {
sidebarService.close();
}
};
return {
type: 'subItem' as const,
children: '🧩 Sidebar',
options: menu.map((item) => ({
type: 'item' as const,
children: item.label,
onSelect: () => onSelect(item.value),
})),
};
}

View File

@@ -0,0 +1,128 @@
/* eslint-disable node/prefer-global/process */
import type { DocumentDataModel, IStyleData, IWorkbookData, Workbook } from '@univerjs/core';
import { IResourceLoaderService, IUniverInstanceService, ObjectMatrix, UniverInstanceType } from '@univerjs/core';
import { ILocalFileService, useDependency } from '@univerjs/ui';
import { RecordController } from '../controllers/local-save/record.controller';
const menu = [
{
label: 'Save workbook',
value: 'workbook',
},
{
label: 'Save worksheet',
value: 'sheet',
},
{
label: 'Record',
value: 'record',
},
{
label: 'Load snapshot',
value: 'load',
},
];
export interface ISaveSnapshotParams {
value: 'sheet' | 'workbook' | 'record';
}
const filterStyle = (workbookData: IWorkbookData) => {
const sheets = workbookData.sheets;
const cacheStyle: Record<string, IStyleData> = {};
Object.keys(sheets).forEach((sheetId) => {
const sheet = sheets[sheetId];
new ObjectMatrix(sheet.cellData).forValue((_r, _c, value) => {
const s = value?.s;
if (s && typeof s === 'string') {
const style = workbookData.styles[s];
if (style) {
cacheStyle[s] = style;
}
}
});
});
workbookData.styles = cacheStyle;
return workbookData;
};
export function useSnapshot() {
const univerInstanceService = useDependency(IUniverInstanceService);
const resourceLoaderService = useDependency(IResourceLoaderService);
const localFileService = useDependency(ILocalFileService);
const recordController = useDependency(RecordController);
const onSelect = async (value: string) => {
const preName = new Date().toLocaleString();
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
if (!workbook) {
const doc = univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC)!;
const snapshot = resourceLoaderService.saveUnit(doc.getUnitId());
if (process.env.NODE_ENV === 'production') {
const gitHash = process.env.GIT_COMMIT_HASH;
const gitBranch = process.env.GIT_REF_NAME;
const buildTime = process.env.BUILD_TIME;
(snapshot as any).__env__ = { gitHash, gitBranch, buildTime };
}
const text = JSON.stringify(snapshot, null, 2);
localFileService.downloadFile(new Blob([text]), `${preName} snapshot.json`);
return true;
}
const worksheet = workbook.getActiveSheet();
if (!worksheet) {
return false;
}
const snapshot = resourceLoaderService.saveUnit<ReturnType<typeof workbook.getSnapshot>>(workbook.getUnitId())!;
if (process.env.NODE_ENV === 'production') {
const gitHash = process.env.GIT_COMMIT_HASH;
const gitBranch = process.env.GIT_REF_NAME;
const buildTime = process.env.BUILD_TIME;
(snapshot as any).__env__ = { gitHash, gitBranch, buildTime };
}
if (value === 'sheet') {
const sheetId = worksheet.getSheetId();
const sheet = snapshot.sheets[sheetId];
snapshot.sheets = { [sheetId]: sheet };
snapshot.sheetOrder = [sheetId];
const text = JSON.stringify(filterStyle(snapshot), null, 2);
localFileService.downloadFile(new Blob([text]), `${preName} snapshot.json`);
} else if (value === 'workbook') {
const text = JSON.stringify(filterStyle(snapshot), null, 2);
localFileService.downloadFile(new Blob([text]), `${preName} snapshot.json`);
} else if (value === 'record') {
let endCommands = () => [];
recordController.record().subscribe((v) => {
if (v.type === 'start') {
endCommands = recordController.startSaveCommands() as any;
}
if (v.type === 'finish') {
const commands = endCommands();
localFileService.downloadFile(v.data, `${preName} video.webm`);
localFileService.downloadFile(new Blob([JSON.stringify(commands, null, 2)]), `${preName} commands.json`);
}
});
} else if (value === 'load') {
const snapshotFile = await localFileService.openFile({ multiple: false, accept: '.json' });
if (snapshotFile.length !== 1) return false;
const text = await snapshotFile[0].text();
univerInstanceService.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, JSON.parse(text));
return true;
}
};
return {
type: 'subItem' as const,
children: '📷 Snapshot',
options: menu.map((item) => ({
type: 'item' as const,
children: item.label,
onSelect: () => onSelect(item.value),
})),
};
}

View File

@@ -0,0 +1,77 @@
import { ThemeService } from '@univerjs/core';
import {
blueTheme,
darkBlueTheme,
greenTheme,
orangeTheme,
purpleTheme,
redTheme,
yellowTheme,
} from '@univerjs/themes';
import { useDependency } from '@univerjs/ui';
import { useEffect } from 'react';
const themes = [
{
label: '🟢',
value: greenTheme,
},
{
label: '🔵',
value: blueTheme,
},
{
label: '🔵🌑',
value: darkBlueTheme,
},
{
label: '🟠',
value: orangeTheme,
},
{
label: '🔴',
value: redTheme,
},
{
label: '🟣',
value: purpleTheme,
},
{
label: '🟡',
value: yellowTheme,
},
];
export function useTheme() {
const themeService = useDependency(ThemeService);
useEffect(() => {
const themeKey = localStorage.getItem('local.theme');
const theme = themes.find((theme) => theme.label === themeKey);
if (theme) {
themeService.setTheme(theme.value);
}
}, [themeService]);
const onSelect = (value: string) => {
localStorage.setItem('local.theme', value);
const theme = themes.find((theme) => theme.label === value);
if (theme) {
themeService.setTheme(theme.value);
}
};
return {
type: 'subItem' as const,
children: '🎨 Themes',
options: themes.map((theme) => ({
type: 'item' as const,
children: theme.label,
onSelect: () => onSelect(theme.label),
})),
};
}

View File

@@ -0,0 +1,50 @@
import type { Workbook } from '@univerjs/core';
import { IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { useDependency, useObservable } from '@univerjs/ui';
import { useEffect, useState } from 'react';
const defaultMenu = [
{
label: 'Create another sheet',
value: 'create',
},
];
export function useUnits() {
const univerInstanceService = useDependency(IUniverInstanceService);
const [menu, setMenu] = useState<{ label: string; value: string }[]>([...defaultMenu]);
const unitAdded = useObservable(univerInstanceService.unitAdded$);
const unitDisposed = useObservable(univerInstanceService.unitDisposed$);
useEffect(() => {
const sheets = univerInstanceService.getAllUnitsForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
const options = sheets.map((sheet) => ({
label: sheet.name || sheet.getUnitId(),
value: sheet.getUnitId(),
}));
setMenu([
...defaultMenu,
...(options as any[]),
]);
}, [unitAdded, unitDisposed]);
const onSelect = (value: string) => {
if (value === 'create') {
univerInstanceService.createUnit(UniverInstanceType.UNIVER_SHEET, {});
} else {
if (!univerInstanceService.getUnit(value)) return false;
univerInstanceService.setCurrentUnitForType(value);
}
};
return {
type: 'subItem' as const,
children: '🪸 Units',
options: menu.map((item) => ({
type: 'item' as const,
children: item.label,
onSelect: () => onSelect(item.value),
})),
};
}

View File

@@ -0,0 +1,54 @@
import type { Workbook } from '@univerjs/core';
import { createDefaultUser, IPermissionService, IUniverInstanceService, UniverInstanceType, UserManagerService } from '@univerjs/core';
import { WorkbookManageCollaboratorPermission } from '@univerjs/sheets';
import { useDependency } from '@univerjs/ui';
enum UnitRole {
Reader = 0,
Editor = 1,
Owner = 2,
UNRECOGNIZED = -1,
}
const menu = [
{
label: 'Owner',
value: UnitRole.Owner,
},
{
label: 'Editor',
value: UnitRole.Editor,
},
{
label: 'Reader',
value: UnitRole.Reader,
},
];
export function useUser() {
const univerInstanceService = useDependency(IUniverInstanceService);
const userManagerService = useDependency(UserManagerService);
const permissionService = useDependency(IPermissionService);
const onSelect = (value: UnitRole) => {
userManagerService.setCurrentUser(createDefaultUser(value));
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const unitId = workbook.getUnitId();
if (value === UnitRole.Owner) {
permissionService.updatePermissionPoint(new WorkbookManageCollaboratorPermission(unitId).id, true);
} else {
permissionService.updatePermissionPoint(new WorkbookManageCollaboratorPermission(unitId).id, false);
}
};
return {
type: 'subItem' as const,
children: '👥 Change user\'s role',
options: menu.map((item) => ({
type: 'item' as const,
children: item.label,
onSelect: () => onSelect(item.value),
})),
};
}

View File

@@ -0,0 +1,22 @@
import { ISidebarService, useDependency } from '@univerjs/ui';
import { WATERMARK_PANEL, WATERMARK_PANEL_FOOTER } from '../controllers/components.controller';
export function useWatermark() {
const sidebarService = useDependency(ISidebarService);
const onSelect = () => {
sidebarService.open({
header: { title: 'Watermark' },
children: { label: WATERMARK_PANEL },
footer: { label: WATERMARK_PANEL_FOOTER },
onClose: () => { },
width: 330,
});
};
return {
type: 'item' as const,
children: '🌊 Watermark Settings',
onSelect,
};
}

View File

@@ -0,0 +1,224 @@
import type { IImageWatermarkConfig } from '@univerjs/engine-render';
import { Button, Checkbox, InputNumber } from '@univerjs/design';
import { ILocalFileService, useDependency } from '@univerjs/ui';
import { WATERMARK_IMAGE_ALLOW_IMAGE_LIST } from '@univerjs/watermark';
interface IWatermarkImageSettingProps {
config?: IImageWatermarkConfig;
onChange: (config: IImageWatermarkConfig) => void;
}
export function WatermarkImageSetting({ config, onChange }: IWatermarkImageSettingProps) {
const fileOpenService = useDependency(ILocalFileService);
if (!config) return null;
const handleUpdateImageUrl = async () => {
const files = await fileOpenService.openFile({
multiple: false,
accept: WATERMARK_IMAGE_ALLOW_IMAGE_LIST.map((image) => `.${image.replace('image/', '')}`).join(','),
});
const fileLength = files.length;
if (fileLength === 0) {
return false;
}
const file = files[0];
const reader = new FileReader();
reader.onload = function (event) {
if (event.target?.result) {
const base64String = event.target.result;
const img = new Image();
img.onload = function () {
onChange({ ...config, url: base64String as string, width: Math.max(20, img.width), height: Math.max(img.height, 20), originRatio: img.width / img.height });
};
img.src = base64String as string;
}
};
reader.readAsDataURL(file);
};
return (
<div className="univer-grid univer-gap-2">
<div className="univer-text-gray-400">Image</div>
<div className="univer-mb-4 univer-grid univer-gap-1">
<Button
className="univer-mb-2"
onClick={handleUpdateImageUrl}
>
{config.url ? 'Replace Image' : 'Upload Image'}
</Button>
<div className="univer-flex univer-gap-2 univer-text-center">
<div className="univer-grid univer-flex-1 univer-gap-1">
<div>Opacity</div>
<InputNumber
className="univer-box-border univer-h-7"
value={config.opacity}
max={1}
min={0}
step={0.05}
onChange={(val) => {
if (val != null) {
onChange({ ...config, opacity: Number.parseFloat(val.toString()) });
}
}}
/>
</div>
<div className="univer-grid univer-flex-1 univer-gap-1">
<div>Keep Ratio</div>
<Checkbox
className="univer-justify-center univer-self-baseline"
checked={config.maintainAspectRatio}
onChange={(val) => {
if (val === true) {
onChange({ ...config, maintainAspectRatio: val as boolean, height: Math.round(config.width / config.originRatio) });
} else {
onChange({ ...config, maintainAspectRatio: val as boolean });
}
}}
/>
</div>
</div>
</div>
<div className="univer-grid univer-gap-2 univer-text-center">
<div className="univer-flex univer-gap-2">
<div className="univer-grid univer-flex-1 univer-gap-1">
<div>Width</div>
<InputNumber
className="univer-box-border univer-h-7"
value={config.width}
min={20}
onChange={(val) => {
if (val != null) {
const newWidth = Math.max(20, Number.parseInt(val.toString()));
if (config.maintainAspectRatio) {
onChange({ ...config, width: newWidth, height: Math.round(newWidth / config.originRatio) });
} else {
onChange({ ...config, width: newWidth });
}
}
}}
/>
</div>
<div className="univer-grid univer-flex-1 univer-gap-1">
<div>Height</div>
<InputNumber
className="univer-box-border univer-h-7"
value={config.height}
min={20}
onChange={(val) => {
if (val != null) {
const newHeight = Math.max(20, Number.parseInt(val.toString()));
if (config.maintainAspectRatio) {
onChange({ ...config, height: newHeight, width: Math.round(newHeight * config.originRatio) });
} else {
onChange({ ...config, height: Number.parseInt(val.toString()) });
}
}
}}
/>
</div>
</div>
</div>
<div className="univer-text-gray-400">Layout Settings</div>
<div className="univer-grid univer-gap-2 univer-text-center">
<div className="univer-flex univer-gap-2">
<div className="univer-grid univer-flex-1 univer-gap-1">
<div>Rotate</div>
<InputNumber
className="univer-box-border univer-h-7"
value={config.rotate}
max={360}
min={-360}
onChange={(val) => {
if (val != null) {
onChange({ ...config, rotate: Number.parseInt(val.toString()) });
}
}}
/>
</div>
<div className="univer-grid univer-flex-1 univer-gap-1">
<div>Repeat</div>
<Checkbox
className="univer-justify-center univer-self-baseline"
checked={config.repeat}
onChange={(val) => onChange({ ...config, repeat: val as boolean })}
/>
</div>
</div>
<div className="univer-flex univer-gap-2">
<div className="univer-grid univer-flex-1 univer-gap-1">
<div>Horizontal Spacing</div>
<InputNumber
className="univer-box-border univer-h-7"
value={config.spacingX}
min={0}
onChange={(val) => {
if (val != null) {
onChange({ ...config, spacingX: Number.parseInt(val.toString()) });
}
}}
/>
</div>
<div className="univer-grid univer-flex-1 univer-gap-1">
<div>Vertical Spacing</div>
<InputNumber
className="univer-box-border univer-h-7"
value={config.spacingY}
min={0}
onChange={(val) => {
if (val != null) {
onChange({ ...config, spacingY: Number.parseInt(val.toString()) });
}
}}
/>
</div>
</div>
<div className="univer-flex univer-gap-2">
<div className="univer-grid univer-flex-1 univer-gap-1">
<div>Horizontal Start Position</div>
<InputNumber
className="univer-box-border univer-h-7"
value={config.x}
min={0}
onChange={(val) => {
if (val != null) {
onChange({ ...config, x: Number.parseInt(val.toString()) });
}
}}
/>
</div>
<div className="univer-grid univer-flex-1 univer-gap-1">
<div>Vertical Start Position</div>
<InputNumber
className="univer-box-border univer-h-7"
value={config.y}
min={0}
onChange={(val) => {
if (val != null) {
onChange({ ...config, y: Number.parseInt(val.toString()) });
}
}}
/>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,65 @@
import type { IWatermarkConfig, IWatermarkConfigWithType } from '@univerjs/engine-render';
import { ILocalStorageService } from '@univerjs/core';
import { Select } from '@univerjs/design';
import { IWatermarkTypeEnum, UNIVER_WATERMARK_STORAGE_KEY } from '@univerjs/engine-render';
import { useDependency, useObservable } from '@univerjs/ui';
import { WatermarkImageBaseConfig, WatermarkService, WatermarkTextBaseConfig } from '@univerjs/watermark';
import { useCallback, useEffect, useState } from 'react';
import { WatermarkImageSetting } from './WatermarkImageSetting';
import { WatermarkTextSetting } from './WatermarkTextSetting';
export function WatermarkPanel() {
const [watermarkType, setWatermarkType] = useState<IWatermarkTypeEnum>(IWatermarkTypeEnum.Text);
const [config, setConfig] = useState<IWatermarkConfig>();
const watermarkService = useDependency(WatermarkService);
const localStorageService = useDependency(ILocalStorageService);
const _refresh = useObservable(watermarkService.refresh$);
function handleConfigChange(config: IWatermarkConfig, type?: IWatermarkTypeEnum) {
setConfig(config);
watermarkService.updateWatermarkConfig({ type: type ?? watermarkType, config });
}
const getWatermarkConfig = useCallback(async () => {
const watermarkConfig = await localStorageService.getItem<IWatermarkConfigWithType>(UNIVER_WATERMARK_STORAGE_KEY);
if (watermarkConfig) {
setWatermarkType(watermarkConfig.type);
setConfig(watermarkConfig.config);
} else {
setConfig({ text: WatermarkTextBaseConfig });
}
}, []);
useEffect(() => {
getWatermarkConfig();
}, [_refresh, getWatermarkConfig]);
return (
<div className="univer-grid univer-gap-3 univer-text-sm">
{/* Watermark type */}
<div className="univer-grid univer-gap-2">
<div className="univer-text-gray-400">Type</div>
<Select
value={watermarkType}
options={[
{ label: 'Text', value: IWatermarkTypeEnum.Text },
{ label: 'Image', value: IWatermarkTypeEnum.Image },
]}
onChange={(v) => {
setWatermarkType(v as IWatermarkTypeEnum);
if (v === IWatermarkTypeEnum.Text) {
handleConfigChange({ text: WatermarkTextBaseConfig }, IWatermarkTypeEnum.Text);
} else if (v === IWatermarkTypeEnum.Image) {
handleConfigChange({ image: WatermarkImageBaseConfig }, IWatermarkTypeEnum.Image);
}
}}
/>
</div>
<div className="univer-grid univer-gap-2">
{watermarkType === IWatermarkTypeEnum.Text && <WatermarkTextSetting config={config?.text} onChange={(v) => handleConfigChange({ text: v })} />}
{watermarkType === IWatermarkTypeEnum.Image && <WatermarkImageSetting config={config?.image} onChange={(v) => handleConfigChange({ image: v })} />}
</div>
</div>
);
};

View File

@@ -0,0 +1,57 @@
import { Button } from '@univerjs/design';
import { IWatermarkTypeEnum } from '@univerjs/engine-render';
import { IClipboardInterfaceService, ISidebarService, useDependency } from '@univerjs/ui';
import { WatermarkService, WatermarkTextBaseConfig } from '@univerjs/watermark';
export function WatermarkPanelFooter() {
const sidebarService = useDependency(ISidebarService);
const watermarkService = useDependency(WatermarkService);
const clipboardService = useDependency(IClipboardInterfaceService);
return (
<div className="univer-flex univer-items-center univer-justify-between">
<a
className="univer-text-sm univer-text-primary-600 univer-underline"
onClick={() => {
watermarkService.updateWatermarkConfig({
type: IWatermarkTypeEnum.Text,
config: { text: WatermarkTextBaseConfig },
});
watermarkService.refresh();
}}
>
Cancel Watermark
</a>
<div className="univer-flex univer-items-center univer-gap-2">
<Button
onClick={async () => {
const watermarkConfig = await watermarkService.getWatermarkConfig();
let config;
if (watermarkConfig?.type === IWatermarkTypeEnum.Text) {
config = watermarkConfig.config.text;
} else if (watermarkConfig?.type === IWatermarkTypeEnum.Image) {
config = watermarkConfig.config.image;
}
clipboardService.writeText(JSON.stringify(config));
}}
>
Copy Config
</Button>
<Button
onClick={async () => {
const watermarkConfig = await watermarkService.getWatermarkConfig();
if (watermarkConfig?.type === IWatermarkTypeEnum.Text && !watermarkConfig.config.text?.content) {
watermarkService.deleteWatermarkConfig();
} else if (watermarkConfig?.type === IWatermarkTypeEnum.Image && !watermarkConfig.config.image?.url) {
watermarkService.deleteWatermarkConfig();
}
sidebarService.close();
}}
>
Close Panel
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,205 @@
import type { ITextWatermarkConfig } from '@univerjs/engine-render';
import { Checkbox, clsx, ColorPicker, Dropdown, Input, InputNumber, Select } from '@univerjs/design';
import { BoldIcon, FontColorDoubleIcon, ItalicIcon } from '@univerjs/icons';
interface IWatermarkTextSettingProps {
config?: ITextWatermarkConfig;
onChange: (config: ITextWatermarkConfig) => void;
}
export function WatermarkTextSetting(props: IWatermarkTextSettingProps) {
const { config, onChange } = props;
if (!config) return null;
return (
<div className="univer-grid univer-gap-2">
<div className="univer-text-gray-400">Style Settings</div>
<div className="univer-mb-4 univer-grid univer-gap-1">
<div>Content</div>
<Input
value={config.content}
onChange={(val) => onChange({ ...config, content: val })}
placeholder="Enter text"
/>
</div>
<div className="univer-grid univer-gap-4">
<div className="univer-flex univer-gap-2">
<div className="univer-grid univer-gap-1">
<div>Font Size</div>
<InputNumber
value={config.fontSize}
max={72}
min={12}
onChange={(val) => {
if (val != null) {
onChange({ ...config, fontSize: Number.parseInt(val.toString()) });
}
}}
/>
</div>
<div className="univer-grid univer-gap-1">
<div>Direction</div>
<Select
value={config.direction}
options={[
{ label: 'Left to Right', value: 'ltr' },
{ label: 'Right to Left', value: 'rtl' },
]}
onChange={(v) => onChange({ ...config, direction: v as 'ltr' | 'rtl' })}
/>
</div>
<div className="univer-grid univer-gap-1">
<div>Opacity</div>
<InputNumber
max={1}
min={0}
step={0.05}
value={config.opacity}
onChange={(val) => {
if (val != null) {
onChange({ ...config, opacity: Number.parseFloat(val.toString()) });
}
}}
/>
</div>
</div>
<div
className={`
univer-flex univer-justify-around univer-gap-4
[&_a]:univer-flex [&_a]:univer-size-6 [&_a]:univer-items-center [&_a]:univer-justify-center
[&_a]:univer-rounded
`}
>
<Dropdown
overlay={(
<div className="univer-rounded-lg univer-p-4">
<ColorPicker value={config.color} onChange={(val) => onChange({ ...config, color: val })} />
</div>
)}
>
<a
className={`
hover:univer-bg-gray-100
dark:hover:!univer-bg-gray-700
`}
>
<FontColorDoubleIcon extend={{ colorChannel1: config.color ?? '#2c53f1' }} />
</a>
</Dropdown>
<a
className={clsx(`
hover:univer-bg-gray-100
dark:hover:!univer-bg-gray-700
`, {
'univer-bg-gray-200 dark:!univer-bg-gray-600': config.bold,
})}
onClick={() => { onChange({ ...config, bold: !config.bold }); }}
>
<BoldIcon />
</a>
<a
className={clsx(`
hover:univer-bg-gray-100
dark:hover:!univer-bg-gray-700
`, {
'univer-bg-gray-200 dark:!univer-bg-gray-600': config.italic,
})}
onClick={() => { onChange({ ...config, italic: !config.italic }); }}
>
<ItalicIcon />
</a>
</div>
</div>
{/* Layout */}
<div className="univer-text-gray-400">Layout Settings</div>
<div className="univer-grid univer-gap-2 univer-text-center">
<div className="univer-flex univer-gap-2">
<div className="univer-grid univer-flex-1 univer-gap-1">
<div>Rotate</div>
<InputNumber
value={config.rotate}
max={360}
min={-360}
onChange={(val) => {
if (val != null) {
onChange({ ...config, rotate: Number.parseInt(val.toString()) });
}
}}
/>
</div>
<div className="univer-grid univer-flex-1 univer-gap-1">
<div>Repeat</div>
<Checkbox
className="univer-justify-center univer-self-baseline"
checked={config.repeat}
onChange={(val) => onChange({ ...config, repeat: val as boolean })}
/>
</div>
</div>
<div className="univer-flex univer-gap-2">
<div className="univer-grid univer-gap-1">
<div>Horizontal Spacing</div>
<InputNumber
value={config.spacingX}
min={0}
onChange={(val) => {
if (val != null) {
onChange({ ...config, spacingX: Number.parseInt(val.toString()) });
}
}}
/>
</div>
<div className="univer-grid univer-gap-1">
<div>Vertical Spacing</div>
<InputNumber
value={config.spacingY}
min={0}
onChange={(val) => {
if (val != null) {
onChange({ ...config, spacingY: Number.parseInt(val.toString()) });
}
}}
/>
</div>
</div>
<div className="univer-flex univer-gap-2">
<div className="univer-grid univer-gap-1">
<div>Horizontal Start Position</div>
<InputNumber
value={config.x}
min={0}
onChange={(val) => {
if (val != null) {
onChange({ ...config, x: Number.parseInt(val.toString()) });
}
}}
/>
</div>
<div className="univer-grid univer-gap-1">
<div>Vertical Start Position</div>
<InputNumber
value={config.y}
min={0}
onChange={(val) => {
if (val != null) {
onChange({ ...config, y: Number.parseInt(val.toString()) });
}
}}
/>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,11 @@
import type { Config } from 'tailwindcss';
import preset from '@univerjs-infra/shared/tailwind';
const config: Config = {
presets: [preset],
content: [
'./src/**/*.{js,ts,jsx,tsx}',
],
};
export default config;

View File

@@ -0,0 +1,8 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/base",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"]
}