snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 1s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Mobile (push) Has been skipped
Release / Release Docker Image (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-16 04:01:20 +05:00
commit 702cc698a7
9990 changed files with 1073618 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
{
"name": "@blocksuite/affine-widget-edgeless-zoom-toolbar",
"description": "Affine edgeless zoom toolbar widget.",
"type": "module",
"scripts": {
"build": "yarn run -T tsc"
},
"sideEffects": false,
"keywords": [],
"author": "toeverything",
"license": "MIT",
"dependencies": {
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.23",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.23"
},
"exports": {
".": "./src/index.ts",
"./view": "./src/view.ts"
},
"files": [
"src",
"dist",
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.27.5"
}

View File

@@ -0,0 +1,17 @@
import {
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
AffineEdgelessZoomToolbarWidget,
} from '.';
import { MobileZoomRuler } from './mobile-zoom-ruler';
import { ZoomBarToggleButton } from './zoom-bar-toggle-button';
import { EdgelessZoomToolbar } from './zoom-toolbar';
export function effects() {
customElements.define('edgeless-zoom-toolbar', EdgelessZoomToolbar);
customElements.define('zoom-bar-toggle-button', ZoomBarToggleButton);
customElements.define('mobile-zoom-ruler', MobileZoomRuler);
customElements.define(
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
AffineEdgelessZoomToolbarWidget
);
}

View File

@@ -0,0 +1,104 @@
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import type { RootBlockModel } from '@blocksuite/affine-model';
import { IS_MOBILE } from '@blocksuite/global/env';
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import { effect } from '@preact/signals-core';
import { css, html, nothing } from 'lit';
import { state } from 'lit/decorators.js';
import { literal, unsafeStatic } from 'lit/static-html.js';
export const AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET =
'affine-edgeless-zoom-toolbar-widget';
export class AffineEdgelessZoomToolbarWidget extends WidgetComponent<RootBlockModel> {
static override styles = css`
:host {
position: absolute;
bottom: var(--affine-edgeless-zoom-toolbar-bottom, 20px);
left: 12px;
z-index: var(--affine-z-index-popover);
display: flex;
justify-content: center;
pointer-events: none;
-webkit-user-select: none;
user-select: none;
}
mobile-zoom-ruler {
pointer-events: auto;
}
@container viewport (width <= 1200px) {
edgeless-zoom-toolbar {
display: none;
}
}
@container viewport (width > 1200px) {
zoom-bar-toggle-button {
display: none;
}
}
`;
get edgeless() {
return this.block;
}
get gfx() {
return this.std.get(GfxControllerIdentifier);
}
override connectedCallback() {
super.connectedCallback();
this.disposables.add(
effect(() => {
const currentTool = this.gfx.tool.currentToolName$.value;
if (currentTool !== 'frameNavigator') {
this._hide = false;
}
this.requestUpdate();
})
);
}
override firstUpdated() {
const { disposables, std } = this;
const slots = std.get(EdgelessLegacySlotIdentifier);
disposables.add(
slots.navigatorSettingUpdated.subscribe(({ hideToolbar }) => {
if (hideToolbar !== undefined) {
this._hide = hideToolbar;
}
})
);
}
override render() {
if (this._hide) {
return nothing;
}
if (IS_MOBILE) {
return html`<mobile-zoom-ruler .std=${this.std}></mobile-zoom-ruler>`;
}
return html`
<edgeless-zoom-toolbar .std=${this.std}></edgeless-zoom-toolbar>
<zoom-bar-toggle-button .std=${this.std}></zoom-bar-toggle-button>
`;
}
@state()
private accessor _hide = false;
}
export const edgelessZoomToolbarWidget = WidgetViewExtension(
'affine:page',
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
literal`${unsafeStatic(AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET)}`
);

View File

@@ -0,0 +1,139 @@
import { stopPropagation } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/lit';
import { ViewBarIcon } from '@blocksuite/icons/lit';
import type { BlockStdScope } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import { baseTheme } from '@toeverything/theme';
import { css, html, LitElement, unsafeCSS } from 'lit';
import { property } from 'lit/decorators.js';
/**
* Compact zoom indicator for narrow / mobile edgeless viewports.
* Shows the live zoom percentage and a fit-to-screen action in a pill HUD
* anchored to the bottom-left of the canvas.
*/
export class MobileZoomRuler extends WithDisposable(LitElement) {
static override styles = css`
:host {
display: flex;
pointer-events: auto;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
}
.zoom-pill {
display: flex;
align-items: center;
height: 32px;
background: var(--affine-background-overlay-panel-color);
border: 1px solid var(--affine-border-color);
border-radius: 999px;
box-shadow: var(--affine-shadow-1);
overflow: hidden;
}
.zoom-label {
display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
padding: 0 12px;
font-size: 12px;
font-weight: 500;
line-height: 1;
color: var(--affine-text-secondary-color);
white-space: nowrap;
user-select: none;
}
.divider {
width: 1px;
height: 16px;
background: var(--affine-border-color);
flex-shrink: 0;
}
.fit-button {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 100%;
padding: 0;
border: none;
background: transparent;
color: var(--affine-icon-color);
cursor: pointer;
}
.fit-button:hover:not(:disabled) {
background: var(--affine-hover-color);
color: var(--affine-primary-color);
}
.fit-button:disabled {
cursor: not-allowed;
color: var(--affine-text-disable-color);
}
.fit-button svg {
width: 20px;
height: 20px;
}
`;
get gfx() {
return this.std.get(GfxControllerIdentifier);
}
get viewport() {
return this.gfx.viewport;
}
get zoom() {
if (!this.viewport) {
return 1;
}
return this.viewport.zoom;
}
override firstUpdated() {
const { disposables } = this;
const viewport = this.viewport;
if (!viewport) {
return;
}
disposables.add(
viewport.viewportUpdated.subscribe(() => this.requestUpdate())
);
disposables.add(viewport.zoomUpdated.subscribe(() => this.requestUpdate()));
}
override render() {
const formattedZoom = `${Math.round(this.zoom * 100)}%`;
const locked = this.viewport?.locked || this.std.store.readonly;
return html`
<div
class="zoom-pill"
@dblclick=${stopPropagation}
@mousedown=${stopPropagation}
@mouseup=${stopPropagation}
@pointerdown=${stopPropagation}
>
<span class="zoom-label">${formattedZoom}</span>
<span class="divider"></span>
<button
class="fit-button"
aria-label="Fit to screen"
?disabled=${locked}
@click=${() => this.gfx.fitToScreen()}
>
${ViewBarIcon()}
</button>
</div>
`;
}
@property({ attribute: false })
accessor std!: BlockStdScope;
}

View File

@@ -0,0 +1,23 @@
import {
type ViewExtensionContext,
ViewExtensionProvider,
} from '@blocksuite/affine-ext-loader';
import { effects } from './effects';
import { edgelessZoomToolbarWidget } from './index';
export class EdgelessZoomToolbarViewExtension extends ViewExtensionProvider {
override name = 'affine-edgeless-zoom-toolbar-widget';
override effect() {
super.effect();
effects();
}
override setup(context: ViewExtensionContext) {
super.setup(context);
if (this.isEdgeless(context.scope)) {
context.register(edgelessZoomToolbarWidget);
}
}
}

View File

@@ -0,0 +1,113 @@
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import { createLitPortal } from '@blocksuite/affine-components/portal';
import { stopPropagation } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/lit';
import { MoreHorizontalIcon } from '@blocksuite/icons/lit';
import type { BlockStdScope } from '@blocksuite/std';
import { offset } from '@floating-ui/dom';
import { css, html, LitElement, nothing } from 'lit';
import { property, query, state } from 'lit/decorators.js';
export class ZoomBarToggleButton extends WithDisposable(LitElement) {
static override styles = css`
:host {
display: flex;
}
.toggle-button {
display: flex;
position: relative;
}
edgeless-zoom-toolbar {
position: absolute;
bottom: initial;
}
`;
private _abortController: AbortController | null = null;
private _closeZoomMenu() {
if (this._abortController && !this._abortController.signal.aborted) {
this._abortController.abort();
this._abortController = null;
this._showPopper = false;
}
}
private get _slots() {
return this.std.get(EdgelessLegacySlotIdentifier);
}
private _toggleZoomMenu() {
if (this._abortController && !this._abortController.signal.aborted) {
this._closeZoomMenu();
return;
}
this._abortController = new AbortController();
this._abortController.signal.addEventListener('abort', () => {
this._showPopper = false;
});
createLitPortal({
template: html`<edgeless-zoom-toolbar
.std=${this.std}
.layout=${'vertical'}
></edgeless-zoom-toolbar>`,
container: this._toggleButton,
computePosition: {
referenceElement: this._toggleButton,
placement: 'top',
middleware: [offset(4)],
autoUpdate: true,
},
abortController: this._abortController,
closeOnClickAway: true,
});
this._showPopper = true;
}
override disconnectedCallback() {
super.disconnectedCallback();
this._closeZoomMenu();
}
override firstUpdated() {
const { disposables } = this;
disposables.add(
this._slots.readonlyUpdated.subscribe(() => {
this.requestUpdate();
})
);
}
override render() {
if (this.std.store.readonly) {
return nothing;
}
return html`
<div class="toggle-button" @pointerdown=${stopPropagation}>
<edgeless-tool-icon-button
.tooltip=${'Toggle Zoom Tool Bar'}
.tipPosition=${'right'}
.active=${this._showPopper}
.arrow=${false}
.activeMode=${'background'}
.iconContainerPadding=${6}
.iconSize=${'24px'}
@click=${() => this._toggleZoomMenu()}
>
${MoreHorizontalIcon()}
</edgeless-tool-icon-button>
</div>
`;
}
@state()
private accessor _showPopper = false;
@query('.toggle-button')
private accessor _toggleButton!: HTMLElement;
@property({ attribute: false })
accessor std!: BlockStdScope;
}

View File

@@ -0,0 +1,213 @@
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import { stopPropagation } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/lit';
import { MinusIcon, PlusIcon, ViewBarIcon } from '@blocksuite/icons/lit';
import type { BlockStdScope } from '@blocksuite/std';
import {
GfxControllerIdentifier,
ZOOM_MAX,
ZOOM_MIN,
ZOOM_STEP,
} from '@blocksuite/std/gfx';
import { effect } from '@preact/signals-core';
import { baseTheme } from '@toeverything/theme';
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
import { property } from 'lit/decorators.js';
import clamp from 'lodash-es/clamp';
export class EdgelessZoomToolbar extends WithDisposable(LitElement) {
static override styles = css`
:host {
display: flex;
}
.edgeless-zoom-toolbar-container {
display: flex;
align-items: center;
background: transparent;
border-radius: 8px;
fill: currentcolor;
padding: 4px;
}
.edgeless-zoom-toolbar-container.horizantal {
flex-direction: row;
}
.edgeless-zoom-toolbar-container.vertical {
flex-direction: column;
width: 40px;
background-color: var(--affine-background-overlay-panel-color);
box-shadow: var(--affine-shadow-2);
border: 1px solid var(--affine-border-color);
border-radius: 8px;
}
.edgeless-zoom-toolbar-container[level='second'] {
position: absolute;
bottom: 8px;
transform: translateY(-100%);
}
.edgeless-zoom-toolbar-container[hidden] {
display: none;
}
.zoom-percent {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 32px;
border: none;
box-sizing: border-box;
padding: 4px;
color: var(--affine-icon-color);
background-color: transparent;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
font-size: 12px;
font-weight: 500;
text-align: center;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
}
.zoom-percent:hover {
color: var(--affine-primary-color);
background-color: var(--affine-hover-color);
}
.zoom-percent[disabled] {
pointer-events: none;
cursor: not-allowed;
color: var(--affine-text-disable-color);
}
`;
get slots() {
return this.std.get(EdgelessLegacySlotIdentifier);
}
get gfx() {
return this.std.get(GfxControllerIdentifier);
}
get edgelessTool() {
return this.gfx.tool.currentToolOption$.peek();
}
get locked() {
return this.viewport.locked;
}
get viewport() {
return this.gfx.viewport;
}
setZoomByStep = (step: number) => {
this.viewport.smoothZoom(clamp(this.zoom + step, ZOOM_MIN, ZOOM_MAX));
};
get zoom() {
if (!this.viewport) {
console.error('Something went wrong, viewport is not available');
return 1;
}
return this.viewport.zoom;
}
private _isVerticalBar() {
return this.layout === 'vertical';
}
override connectedCallback() {
super.connectedCallback();
this.disposables.add(
effect(() => {
void this.gfx.tool.currentToolName$.value;
this.requestUpdate();
})
);
}
override firstUpdated() {
const { disposables } = this;
disposables.add(
this.viewport.viewportUpdated.subscribe(() => this.requestUpdate())
);
disposables.add(
this.slots.readonlyUpdated.subscribe(() => {
this.requestUpdate();
})
);
}
override render() {
if (this.std.store.readonly) {
return nothing;
}
const formattedZoom = `${Math.round(this.zoom * 100)}%`;
const classes = `edgeless-zoom-toolbar-container ${this.layout}`;
const locked = this.locked;
return html`
<div
class=${classes}
@dblclick=${stopPropagation}
@mousedown=${stopPropagation}
@mouseup=${stopPropagation}
@pointerdown=${stopPropagation}
>
<edgeless-tool-icon-button
.tooltip=${'Fit to screen'}
.tipPosition=${this._isVerticalBar() ? 'right' : 'top-end'}
.arrow=${!this._isVerticalBar()}
@click=${() => this.gfx.fitToScreen()}
.iconContainerPadding=${4}
.iconSize=${'24px'}
.disabled=${locked}
>
${ViewBarIcon()}
</edgeless-tool-icon-button>
<edgeless-tool-icon-button
.tooltip=${'Zoom out'}
.tipPosition=${this._isVerticalBar() ? 'right' : 'top'}
.arrow=${!this._isVerticalBar()}
@click=${() => this.setZoomByStep(-ZOOM_STEP)}
.iconContainerPadding=${4}
.iconSize=${'24px'}
.disabled=${locked}
>
${MinusIcon()}
</edgeless-tool-icon-button>
<button
class="zoom-percent"
@click=${() => this.viewport.smoothZoom(1)}
.disabled=${locked}
>
${formattedZoom}
</button>
<edgeless-tool-icon-button
.tooltip=${'Zoom in'}
.tipPosition=${this._isVerticalBar() ? 'right' : 'top'}
.arrow=${!this._isVerticalBar()}
@click=${() => this.setZoomByStep(ZOOM_STEP)}
.iconContainerPadding=${4}
.iconSize=${'24px'}
.disabled=${locked}
>
${PlusIcon()}
</edgeless-tool-icon-button>
</div>
`;
}
@property({ attribute: false })
accessor layout: 'horizontal' | 'vertical' = 'horizontal';
@property({ attribute: false })
accessor std!: BlockStdScope;
}

View File

@@ -0,0 +1,18 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
},
"include": ["./src"],
"references": [
{ "path": "../../blocks/surface" },
{ "path": "../../components" },
{ "path": "../../ext-loader" },
{ "path": "../../model" },
{ "path": "../../shared" },
{ "path": "../../../framework/global" },
{ "path": "../../../framework/std" }
]
}