Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
8e3eeed
Add candle, dusk, night and ambient sky tokens
XrxcGH Oct 3, 2026
305b8cc
Add small line drawings for the desk-by-the-window look
XrxcGH Oct 3, 2026
c993009
Paint window light and an evening sky behind the page
XrxcGH Oct 3, 2026
f6e3d8e
Give the setup steps the desk, an ink stroke and even chips
XrxcGH Oct 3, 2026
b065864
Warm the theme toggle and show the sky in the theme cards
XrxcGH Oct 3, 2026
94417bd
Add a bookshelf, a footer plant and a green saved cloud to the tree
XrxcGH Oct 3, 2026
cbb890b
Show the notebook color as a dot in the palette and breadcrumb
XrxcGH Oct 3, 2026
218684f
Draw the empty page and Trash, and put Trash on a page card
XrxcGH Oct 3, 2026
379983d
Give Settings, About a window, the logo and the ink stroke
XrxcGH Oct 3, 2026
491e4d3
Document the warm accents, ambient light and drawings in the guide
XrxcGH Oct 3, 2026
7ac448f
Test that the drawings stay calm and out of the way
XrxcGH Oct 3, 2026
708756f
Stand the leaning book and the desk things on their lines
XrxcGH Oct 3, 2026
99e60de
Keep the window's stars and sun clear of the plant and bars
XrxcGH Oct 3, 2026
89ebf28
Line the drawings up with the text beside them
XrxcGH Oct 3, 2026
f0208fb
Hang the palette's notebook dot in front of the title
XrxcGH Oct 3, 2026
c35590d
Keep the theme toggle's fill off the window edge
XrxcGH Oct 3, 2026
32368bc
Seat the candle in its dish and keep the plant off the sill and bar
XrxcGH Oct 3, 2026
8085726
Move the night star out of the moon and lower the day sun
XrxcGH Oct 3, 2026
c65d69d
Set the notebook dot 5 px from its name in the crumb and palette
XrxcGH Oct 3, 2026
4c59bcf
Pin the welcome title and center only the desk scene
XrxcGH Oct 3, 2026
aad3c4d
Give the window one more unit so its sill line is not clipped
XrxcGH Oct 3, 2026
76f8c77
Keep the evening stars clear of the page card's top edge
XrxcGH Oct 3, 2026
241bf7e
Fix the cascade layer order in the built app
XrxcGH Oct 3, 2026
48dfe9c
Keep the shortcuts dialog's Close button in short windows
XrxcGH Oct 3, 2026
d4aec42
Let the shortcut capture field take the empty actions cell
XrxcGH Oct 3, 2026
27ea902
Keep the command palette clear of a short window's bottom edge
XrxcGH Oct 3, 2026
e4e5e92
Leave room for focus rings at the edges of scrollers
XrxcGH Oct 3, 2026
a25cfaa
Give disabled buttons a border only where enabled ones have one
XrxcGH Oct 3, 2026
3df17d1
Make the setup card tall enough for step 3 at 800 px
XrxcGH Oct 3, 2026
48473c9
Center the theme cards under the setup step's heading
XrxcGH Oct 3, 2026
a6d78da
Line up Settings' switches with their headings and each other
XrxcGH Oct 3, 2026
989bf94
Line up the Find by shortcut switch with the filter's input
XrxcGH Oct 3, 2026
b6d185c
Space the shortcut tables on the Settings page as in the dialog
XrxcGH Oct 3, 2026
dbd2e0b
Size the Updates section's heading like the other Settings headings
XrxcGH Oct 3, 2026
ec4a538
Give the text and interface size choices equal widths
XrxcGH Oct 3, 2026
3553a5d
Drop the browser's paragraph margins from empty states and help
XrxcGH Oct 3, 2026
440dc97
Pad the Move to filter like every other text field
XrxcGH Oct 3, 2026
c9a368a
Separate the app path from the read-only folder warning
XrxcGH Oct 3, 2026
b628746
Word the delete-notebook confirmation for empty notebooks
XrxcGH Oct 3, 2026
e069527
Move a shortcut row's Change and Reset buttons into their own cell
XrxcGH Oct 3, 2026
16965de
Keep this round's comments and border test within the repo checks
XrxcGH Oct 3, 2026
2a8eb05
Check geometry and focus rings on every registered screen
XrxcGH Oct 3, 2026
eba7d8e
Keep the shortcut tables' hidden heads inside the Settings scroller
XrxcGH Oct 3, 2026
477f17b
Give the shortcut tables' keys column the room
XrxcGH Oct 3, 2026
6a91dfa
Split the eight text sizes four and four when they don't fit
XrxcGH Oct 3, 2026
ed0af1c
Give the cards of one choice one width
XrxcGH Oct 3, 2026
5b956b8
Line the shortcut list's switch and tables up at the end edge
XrxcGH Oct 3, 2026
096582f
Say "No results." in the palette when nothing matches
XrxcGH Oct 3, 2026
bff20bb
Keep the Settings section list in view while a section scrolls
XrxcGH Oct 3, 2026
599036d
Let the theme cards hug their content and keep setup's row steady
XrxcGH Oct 3, 2026
b8d3eaf
Let the setup theme cards fill the step's column
XrxcGH Oct 3, 2026
d0f5ca1
Start sibling section and section group names at one edge
XrxcGH Oct 3, 2026
2a357cd
Space the update popover's buttons like a dialog's
XrxcGH Oct 3, 2026
403f4b7
Drop tooltips that only repeat a visible label
XrxcGH Oct 3, 2026
23d9f0b
Keep "or" with the chord it introduces when keys wrap
XrxcGH Oct 3, 2026
2a66e3f
Run the tree's shortcuts in the notebooks drawer
XrxcGH Oct 3, 2026
06fb74d
Catch spilled cells, lone and ragged choices in the geometry checks
XrxcGH Oct 3, 2026
8b17c60
Record the choice card, tooltip, and empty palette rules in BRAND.md
XrxcGH Oct 3, 2026
29cb72c
Keep this round's comments and geometry measure within the repo checks
XrxcGH Oct 3, 2026
adcd681
Color the drawings with five soft tints instead of outlines
XrxcGH Oct 3, 2026
e91d184
Grow the drawings and fill them with more of the desk
XrxcGH Oct 3, 2026
98d020f
Brighten the ambient canvas: window light by day, dusk at night
XrxcGH Oct 3, 2026
09a1037
Widen the desk's light patch and keep setup to the pool of light
XrxcGH Oct 3, 2026
9f81f7c
Drop the cross-bar shadows from the desk's light patch
XrxcGH Oct 3, 2026
dc77a5a
Keep every evening star above the page card's top edge
XrxcGH Oct 3, 2026
667027c
Fix the palette dot check and fit the start-up CSS budget
XrxcGH Oct 3, 2026
97410fd
Wait for the palette and a settings section to be ready before measuring
XrxcGH Oct 3, 2026
67f64bf
Merge origin/main into brand-personality
XrxcGH Oct 3, 2026
694e3a6
Raise the start-up bundle limits to fit the brand work
XrxcGH Oct 3, 2026
aec1988
Keep the editing page an opaque sheet over the ambient canvas
XrxcGH Oct 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/bundle-budget.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"startup": { "jsKb": 180, "cssKb": 15 },
"startup": { "jsKb": 185, "cssKb": 18 },
"after": { "within": "features/page/", "entry": "features/page/PageBody" },
"lazyKb": {
"editor/markdown/worker": 45,
Expand Down
21 changes: 21 additions & 0 deletions app/scripts/build-tokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,27 @@ describe('buildCss', () => {
expect(css).toContain('[data-theme-scope="light"],\n:root[data-page-color="paper"] [data-region="page"] {');
});

it('writes the warm accents and the ambient sky in both themes and in the scoped blocks', () => {
expect(blockOf(':root')).toContain('--color-accent-candle: #9A5410;');
expect(blockOf(':root')).toContain('--color-accent-night-subtle: #E8EAF3;');
expect(blockOf(':root')).toContain('--color-ambient-canvas-top: #FFF0D4;');
expect(blockOf(':root[data-theme="dark"]')).toContain('--color-accent-dusk: #EBA0B2;');
expect(blockOf(':root[data-theme="dark"]')).toContain('--color-ambient-canvas-bottom: #33243A;');
expect(blockOf('[data-theme-scope="dark"]')).toContain('--color-ambient-spark: #E7D9B0;');
expect(css).toContain('[data-theme-scope="light"],\n:root[data-page-color="paper"] [data-region="page"] {');
});

it('flattens the ambient wash and the stars to Canvas under a Windows contrast theme', () => {
const forced = css.slice(css.indexOf('@media (forced-colors: active)'));
['canvas-top', 'canvas-bottom', 'glow', 'spark'].forEach((name) =>
expect(forced).toContain(`--color-ambient-${name}: Canvas;`),
);
['candle', 'dusk', 'night'].forEach((name) => {
expect(forced).toContain(`--color-accent-${name}: CanvasText;`);
expect(forced).toContain(`--color-accent-${name}-subtle: Canvas;`);
});
});

it('reduces motion for the in-app setting as for the system one', () => {
expect(blockOf(':root[data-motion="reduce"]')).toContain('--motion-duration-base: 100ms;');
expect(blockOf(':root[data-motion="reduce"]')).toContain('--motion-duration-spring: 100ms;');
Expand Down
32 changes: 32 additions & 0 deletions app/scripts/layer-order.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import { LAYERS_CSS, layerOrderStatement, withLayerOrder } from './layer-order.ts';

describe('layerOrderStatement', () => {
it('reads the order from styles/layers.css', () => {
expect(layerOrderStatement(readFileSync(LAYERS_CSS, 'utf8'))).toBe('@layer tokens,base,components,states,forced;');
});

it('skips a block @layer and finds the order statement', () => {
const css = '@layer base { a { color: red; } }\n@layer one,\n two;';
expect(layerOrderStatement(css)).toBe('@layer one,two;');
});

it('throws when the file has no order statement, so the build fails instead of shipping the wrong order', () => {
expect(() => layerOrderStatement('@layer base { a { color: red; } }')).toThrow(/no @layer order/);
});
});

describe('withLayerOrder', () => {
const statement = '@layer tokens,base,components,states,forced;';

it('puts the order in front of a sheet that opens with a later layer', () => {
const css = '@layer components{._row_1{outline-offset:-2px}}';
expect(withLayerOrder(css, statement)).toBe(`${statement}${css}`);
});

it('leaves a sheet that already starts with the order as it is', () => {
const css = `${statement}@layer base{a{color:red}}`;
expect(withLayerOrder(css, statement)).toBe(css);
});
});
43 changes: 43 additions & 0 deletions app/scripts/layer-order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// The app's style sheets rely on one cascade layer order (styles/layers.css, ARCHITECTURE.md section 4.6). A browser
// fixes that order the first time it meets each layer name. A build splits the CSS into one file per chunk. It links
// the shared chunks' files before the entry's own, so a sheet that opens with @layer components loaded before
// layers.css. Base rules then beat components, states, and forced colors.
//
// This build plugin starts every emitted style sheet with the order statement from layers.css. The order is then
// the same whichever file loads first, including sheets that lazy chunks add later. Repeating the statement once
// the order is set changes nothing.

import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import type { Plugin } from 'vite';

/** The style sheet that declares the layer order. */
export const LAYERS_CSS = join(import.meta.dirname, '..', 'src', 'styles', 'layers.css');

/** The `@layer a, b, c;` order statement in `css`, on one line. Throws when there is none. */
export function layerOrderStatement(css: string): string {
const match = /@layer\s+[\w-]+(?:\s*,\s*[\w-]+)+\s*;/.exec(css);
if (!match) throw new Error('styles/layers.css has no @layer order statement.');
return match[0].replace(/\s*,\s*/g, ',').replace(/\s+/g, ' ');
}

/** `css` with the order statement in front, unless it already starts with it. */
export function withLayerOrder(css: string, statement: string): string {
return css.startsWith(statement) ? css : `${statement}${css}`;
}

export function layerOrder(source = LAYERS_CSS): Plugin {
return {
name: 'opennote:layer-order',
apply: 'build',
enforce: 'post',
generateBundle(_options, bundle) {
const statement = layerOrderStatement(readFileSync(source, 'utf8'));
for (const file of Object.values(bundle)) {
if (file.type !== 'asset' || !file.fileName.endsWith('.css')) continue;
const css = typeof file.source === 'string' ? file.source : new TextDecoder().decode(file.source);
file.source = withLayerOrder(css, statement);
}
},
};
}
23 changes: 21 additions & 2 deletions app/src/commands/dispatcher.scopes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { createMemoryNotesService } from '../services/notes/memory';
import { pushLayer } from '../state/layers';
import { resetStores } from '../state/store';
import { createTestPlatform } from '../test/platform';
import { activeScopes, commandForKey, installDispatcher, reachesCommandsInText } from './dispatcher';
import { chord, configureCommands, defineCommand } from './registry';
import { activeScopes, commandForKey, installDispatcher, layerScopes, reachesCommandsInText } from './dispatcher';
import { chord, configureCommands, defineCommand, focusZone } from './registry';
import type { CommandDef } from './types';

const stops: (() => void)[] = [];
Expand Down Expand Up @@ -124,6 +124,25 @@ describe('text fields, dialogs, repeats, and flags', () => {
expect(run(document.body, 'n', 'KeyN', 'ctrl alt')).toBe('test.newPage');
});

it('run the commands of the scopes a modal layer holds, such as the tree in the notebooks drawer', () => {
add('rename', { keys: [chord('Ctrl+Alt+E')], scope: 'tree' });
add('newPage', { keys: [chord('Ctrl+Alt+N')], scope: 'workspace' });
const pop = pushLayer({ id: 'drawer', kind: 'drawer', modal: true, close() {} });
const row = element(
'<div role="dialog"><div data-region="notebooks"><div data-scope="tree notebooksTree">' +
'<div role="treeitem" id="row"></div></div></div></div>',
'#row',
);
expect([...layerScopes(row)].sort()).toEqual(['notebooksTree', 'tree', 'workspace']);
expect(focusZone(row)).toBe('notebooks');
expect(run(row, 'e', 'KeyE', 'ctrl alt')).toBe('test.rename');
expect(run(row, 'n', 'KeyN', 'ctrl alt')).toBe('test.newPage');
// A global command still needs to allow the modal layer.
add('global', { keys: [chord('Ctrl+Alt+G')] });
expect(run(row, 'g', 'KeyG', 'ctrl alt')).toBeNull();
pop();
});

it('repeat only commands that allow it, skip flags that are off, and leave key capture alone', () => {
add('toggle', { keys: [chord('Ctrl+Shift+D')] });
add('move', { keys: [chord('Ctrl+Shift+Up')], allowRepeat: true });
Expand Down
45 changes: 34 additions & 11 deletions app/src/commands/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
// 2. The active scopes come from the focused element's data-scope attributes and the view.
// 3. The most specific command wins. One whose `when` or `enabled` is false lets the key go on.
// 4. Text fields take only chords with Ctrl or Alt, or function keys, and keep their own editing chords.
// 5. While a modal layer is open, only commands that allow it run, and those of the layer's own scope.
// 5. While a modal layer is open, only commands that allow it run, those of a modal scope, and those of a scope the
// layer holds itself, such as the tree inside the notebooks drawer.
// 6. Held keys repeat only commands that allow it. The default is prevented only when a command runs.

import { isEnabled } from '../app/flags';
import { getLocation } from '../app/location';
import { topLayer } from '../state/layers';
import { chordMatches, keyKind, loadKeyboardLayout, parseChord, primaryChord } from './chords';
import type { KeyInput } from './chords';
import { SCOPE_PARENT, commandsForChord, scopeDepth } from './keymap';
import { SCOPE_PARENT, commandsForChord, scopeChain, scopeDepth } from './keymap';
import type { AnyCommand } from './keymap';
import { commandContext, executeCommand } from './registry';
import type { Chord, KeyScope } from './types';
Expand Down Expand Up @@ -47,19 +48,39 @@ export function reachesCommandsInText(chord: Chord): boolean {
const VIEWS_WITH_WORKSPACE = new Set(['workspace', 'trash']);
const MODAL_SCOPES = new Set<KeyScope>(['palette', 'dialog']);

/** The scopes active for a focused element: global, the workspace while it shows, and every data-scope around it. */
export function activeScopes(element: Element | null, view: string = getLocation().view): Set<KeyScope> {
const scopes = new Set<KeyScope>(['global']);
if (VIEWS_WITH_WORKSPACE.has(view)) scopes.add('workspace');
const DIALOG = '[role="dialog"], dialog';

/** The scopes the data-scope attributes on and around an element name, up to the edge of `within` if given. */
function scopesAround(element: Element | null, within?: Element): KeyScope[] {
const found: KeyScope[] = [];
for (let at = element?.closest('[data-scope]'); at; at = at.parentElement?.closest('[data-scope]')) {
if (within && !within.contains(at)) break;
for (const token of (at.getAttribute('data-scope') ?? '').split(/\s+/)) {
if (token in SCOPE_PARENT) scopes.add(token as KeyScope);
if (token in SCOPE_PARENT) found.push(token as KeyScope);
}
}
if (!scopes.has('palette') && element?.closest('[role="dialog"], dialog')) scopes.add('dialog');
return found;
}

/** The scopes active for a focused element: global, the workspace while it shows, and every data-scope around it. */
export function activeScopes(element: Element | null, view: string = getLocation().view): Set<KeyScope> {
const scopes = new Set<KeyScope>(['global', ...scopesAround(element)]);
if (VIEWS_WITH_WORKSPACE.has(view)) scopes.add('workspace');
if (!scopes.has('palette') && element?.closest(DIALOG)) scopes.add('dialog');
return scopes;
}

/**
* The scopes the dialog around a focused element holds itself, with the scopes they sit in short of global. The
* notebooks drawer is a modal dialog with the notebooks tree inside, so the tree's commands and the workspace's,
* which the tree's menus list with their shortcuts, still run there.
*/
export function layerScopes(element: Element | null): Set<KeyScope> {
const layer = element?.closest(DIALOG);
const held = layer ? scopesAround(element, layer) : [];
return new Set(held.flatMap(scopeChain).filter((scope) => scope !== 'global'));
}

function bySpecificity(a: AnyCommand, b: AnyCommand): number {
const depth = scopeDepth(b.scope) - scopeDepth(a.scope);
if (depth !== 0) return depth;
Expand All @@ -71,10 +92,11 @@ interface Press {
readonly repeat: boolean;
}

function allowed(def: AnyCommand, press: Press, textInput: boolean): boolean {
function allowed(def: AnyCommand, press: Press, textInput: boolean, held: Set<KeyScope>): boolean {
if (press.repeat && !def.allowRepeat) return false;
if (textInput && !def.allowInTextInput) return false;
if (topLayer()?.modal && !def.allowInModal && !MODAL_SCOPES.has(def.scope ?? 'global')) return false;
const scope = def.scope ?? 'global';
if (topLayer()?.modal && !def.allowInModal && !MODAL_SCOPES.has(scope) && !held.has(scope)) return false;
const ctx = commandContext('keyboard');
return (!def.when || def.when(ctx)) && (!def.enabled || def.enabled(ctx));
}
Expand All @@ -84,13 +106,14 @@ export function commandForKey(event: KeyInput & Press): AnyCommand | null {
const { target } = event;
if (target?.closest('[data-key-capture]')) return null;
const scopes = activeScopes(target);
const held = layerScopes(target);
const textInput = isTextInput(target);
for (const { chord } of chordMatches(event)) {
if (textInput && !reachesCommandsInText(chord)) continue;
const candidates = commandsForChord(chord)
.filter((def) => (!def.flag || isEnabled(def.flag)) && scopes.has(def.scope ?? 'global'))
.sort(bySpecificity);
const def = candidates.find((candidate) => allowed(candidate, event, textInput));
const def = candidates.find((candidate) => allowed(candidate, event, textInput, held));
if (def) return def;
}
return null;
Expand Down
7 changes: 5 additions & 2 deletions app/src/commands/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ export function focusZone(element: Element | null = document.activeElement): Foc
if (!element || element === document.body) return 'none';
if (element.matches('input, textarea, select, [contenteditable=""], [contenteditable="true"]')) return 'textInput';
if (element.closest('[data-scope~="palette"]')) return 'palette';
if (element.closest('[role="dialog"], dialog')) return 'dialog';
const region = element.closest('[data-region]')?.getAttribute('data-region') as FocusZone | undefined;
const dialog = element.closest('[role="dialog"], dialog');
const regionElement = element.closest('[data-region]');
// A region a dialog holds, such as the notebooks in the drawer, is still that region.
if (dialog && !(regionElement && dialog.contains(regionElement))) return 'dialog';
const region = regionElement?.getAttribute('data-region') as FocusZone | undefined;
return region && REGIONS.includes(region) ? region : 'none';
}

Expand Down
30 changes: 0 additions & 30 deletions app/src/features/page/EmptyPageArt.tsx

This file was deleted.

32 changes: 7 additions & 25 deletions app/src/features/page/PageView.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,33 +48,19 @@
text-align: center;
}

.art {
inline-size: min(12rem, 60%);
block-size: auto;
fill: none;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
/* The drawing above the "no page" sentence: a little company, with the sentence below it keeping the meaning. */
.page .art {
margin-block-start: var(--space-5);
}

.artFrame {
stroke: var(--color-border-control);
}

.artSky {
stroke: var(--color-status-warning);
}

.artCandle {
stroke: var(--color-accent-clay);
}

.artPlant {
stroke: var(--color-accent-primary);
.page .art + .note {
margin-block-start: var(--space-4);
}

/* With the editor on, the page fills the region and its viewport scrolls (ARCHITECTURE.md section 5.1). */
.editing {
/* The sheet stays opaque paper over the ambient canvas (BRAND.md section 7). */
background: var(--color-surface-page);
display: flex;
flex-direction: column;
block-size: 100%;
Expand All @@ -94,9 +80,5 @@
.title::after {
background: CanvasText;
}

.art g {
stroke: CanvasText;
}
}
}
9 changes: 9 additions & 0 deletions app/src/features/page/PageView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ describe('the page view', () => {
expect(screen.getByText('Choose a page to see it here.')).toBeTruthy();
});

it('puts a small drawing above the prompt, hidden from screen readers, and none on an open page', async () => {
const { container } = await renderApp();
const prompt = await screen.findByText('Choose a page to see it here.');
const art = container.querySelector('article svg[viewBox="0 0 110 54"]');
expect(art?.getAttribute('aria-hidden')).toBe('true');
expect(art?.getAttribute('focusable')).toBe('false');
expect((art as Node).compareDocumentPosition(prompt) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});

it('shows the open page as a heading that Enter in the tree moves focus to', async () => {
await renderApp();
const notebooks = await screen.findByRole('tree', { name: 'Notebooks' });
Expand Down
5 changes: 2 additions & 3 deletions app/src/features/page/PageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import type { NodeId, NodeSummary } from '../../services/notes';
import { registerRegionMain } from '../../shell/regions';
import { formatDate } from '../../strings/format';
import { t } from '../../strings/t';
import { ProgressBar, useDelayedFlag } from '../../ui';
import { EmptyArt, ProgressBar, useDelayedFlag } from '../../ui';
import { titleOf, useTreeNode } from '../tree';
import { EmptyPageArt } from './EmptyPageArt';
import { TitleSlot, useTitleBand } from './title/TitleSlot';
import styles from './PageView.module.css';
import { usePageZoom } from './zoom';
Expand Down Expand Up @@ -80,7 +79,7 @@ export function PageView() {
{page && !editing && <p className={styles.note}>{t('tree.page.empty')}</p>}
{!page && !loading && (
<div className={styles.empty}>
<EmptyPageArt />
<EmptyArt kind="page" className={styles.art} />
<p className={styles.note}>{t('tree.page.none')}</p>
</div>
)}
Expand Down
Loading
Loading