Add pageTool plugins with toc and page-index references (#77)
All checks were successful
CI / Auth e2e pack (push) Successful in 4m37s
CI / Import/export fidelity gate (push) Successful in 43s
CI / Lint, typecheck, test (push) Successful in 2m54s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m14s
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m8s
CD / Promote to Int (push) Successful in 9s
All checks were successful
CI / Auth e2e pack (push) Successful in 4m37s
CI / Import/export fidelity gate (push) Successful in 43s
CI / Lint, typecheck, test (push) Successful in 2m54s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m14s
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m8s
CD / Promote to Int (push) Successful in 9s
The read-only widget surface over page/pond data (ADR 0008 extension point `pageTool`): - Host: PageToolsPanel lists the pond's active pageTool surfaces behind disclosures — each sandbox iframe mounts lazily on first open and tears down on close. The same surfaces are insertable as plugin_block embeds (#76's insert picker now offers pageTool points too; the sandbox drives both through the same render lifecycle). - New `ui.scrollToHeading(headingId)` capability: outline ids are derived from the doc and never stamped into the DOM, so the host resolves the id to its heading position via the shared extractOutline and scrolls the matching rendered heading. - `readPond.listPages` now carries label *names* per summary (PagesService.pluginPageSummaries) — the page-index filter chips work on data the viewer could resolve anyway; per-page permission filtering stays in the service as before. - Reference plugins packages/plugins/toc and packages/plugins/page-index: real SDK consumers (createPlugin + windowTransport), bundled with esbuild into the package ZIP; i18n de/en is inlined at build time — the sandbox CSP forbids runtime fetches, the i18n/ files stay the single source. The toc re-fetches its outline on a slow poll, so live heading edits appear once the collab server has re-derived the content cache. - e2e page-tools.spec.ts covers the acceptance criteria: live outline updates after the persistence debounce, heading click scrolls, embedded page-index navigates via ui.openPage, and a label-restricted reader never sees the denied page in the index. - CI: the auth-e2e job now runs the section-styles (missed in #75), plugin-blocks, and page-tools packs, with login-rate-limit resets. - plugins.e2e.db.test clears the plugin registry up front: a local dev DB is shared with the e2e stack, whose installed real `toc` would otherwise collide with the fixture of the same id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
This commit is contained in:
parent
923532f5f7
commit
0003063c39
@ -351,6 +351,33 @@ jobs:
|
||||
E2E_BASE_URL=http://localhost:5173 \
|
||||
pnpm --filter @dorfteich/web exec playwright test e2e/plugin-admin.spec.ts
|
||||
|
||||
# Several contexts per test across these two packs → reset first.
|
||||
- name: Reset login rate limit before plugin feature packs
|
||||
run: |
|
||||
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
|
||||
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
|
||||
|
||||
- name: Run section-styles pack
|
||||
run: |
|
||||
E2E_BASE_URL=http://localhost:5173 \
|
||||
pnpm --filter @dorfteich/web exec playwright test e2e/section-styles.spec.ts
|
||||
|
||||
- name: Run plugin blocks pack
|
||||
run: |
|
||||
E2E_BASE_URL=http://localhost:5173 \
|
||||
pnpm --filter @dorfteich/web exec playwright test e2e/plugin-blocks.spec.ts
|
||||
|
||||
# Owner + admin + restricted viewer contexts → reset first.
|
||||
- name: Reset login rate limit before page-tools pack
|
||||
run: |
|
||||
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
|
||||
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
|
||||
|
||||
- name: Run page-tools pack
|
||||
run: |
|
||||
E2E_BASE_URL=http://localhost:5173 \
|
||||
pnpm --filter @dorfteich/web exec playwright test e2e/page-tools.spec.ts
|
||||
|
||||
- name: Dump server logs on failure
|
||||
if: failure()
|
||||
run: tail -50 /tmp/api.log /tmp/collab.log /tmp/web.log || true
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
PageListItemView,
|
||||
PageStateView,
|
||||
PageView,
|
||||
PluginPageSummary,
|
||||
RepositionPageInput,
|
||||
SidebarSortMode,
|
||||
UpdatePageInput,
|
||||
@ -130,6 +131,26 @@ export class PagesService {
|
||||
}));
|
||||
}
|
||||
|
||||
/** `readPond.listPages` for the plugin API (issue #77): the viewer-readable
|
||||
* pages with their label *names* — pageTool plugins filter on these and
|
||||
* never see ids they could not resolve anyway. */
|
||||
async pluginPageSummaries(user: User, pondId: string): Promise<PluginPageSummary[]> {
|
||||
const [pages, labels] = await Promise.all([
|
||||
this.list(user, pondId),
|
||||
this.prisma.label.findMany({ where: { pondId }, select: { id: true, name: true } }),
|
||||
]);
|
||||
const nameById = new Map(labels.map((label) => [label.id, label.name]));
|
||||
return pages.map((page) => ({
|
||||
id: page.id,
|
||||
title: page.title,
|
||||
slug: page.slug,
|
||||
labels: page.labelIds
|
||||
.map((id) => nameById.get(id))
|
||||
.filter((name): name is string => Boolean(name))
|
||||
.sort(),
|
||||
}));
|
||||
}
|
||||
|
||||
async create(user: User, pondId: string, input: CreatePageInput): Promise<PageView> {
|
||||
const page = await this.insertPage(user, pondId, input.title, emptyPageState());
|
||||
return this.viewOf(page);
|
||||
|
||||
@ -20,15 +20,15 @@ import { PagesService } from './pages.service';
|
||||
export class PluginApiController {
|
||||
constructor(private readonly pages: PagesService) {}
|
||||
|
||||
/** `readPond.listPages` — the pages of a pond the viewer may read. */
|
||||
/** `readPond.listPages` — the pages of a pond the viewer may read, with
|
||||
* label names for pageTool filtering (issue #77). */
|
||||
@Get('ponds/:pondId/pages')
|
||||
@RequiresPondRole('reader', { idParam: 'pondId' }) // the service filters per page
|
||||
async listPages(
|
||||
listPages(
|
||||
@Param('pondId') pondId: string,
|
||||
@Req() request: AuthedRequest,
|
||||
): Promise<PluginPageSummary[]> {
|
||||
const pages = await this.pages.list(request.user!, pondId);
|
||||
return pages.map((page) => ({ id: page.id, title: page.title, slug: page.slug }));
|
||||
return this.pages.pluginPageSummaries(request.user!, pondId);
|
||||
}
|
||||
|
||||
/** `readCurrentPage.getOutline` / `readPond.getPageOutline`. */
|
||||
|
||||
@ -147,6 +147,10 @@ describe.skipIf(!hasTestDb)('plugin API (e2e, issue #74)', () => {
|
||||
[openPageId, secretPageId].sort(),
|
||||
);
|
||||
expect(res.body[0]).toMatchObject({ title: expect.any(String), slug: expect.any(String) });
|
||||
// Label *names* travel with each summary (issue #77, page-index filter).
|
||||
const byId = new Map(res.body.map((p: { id: string; labels: string[] }) => [p.id, p.labels]));
|
||||
expect(byId.get(secretPageId)).toEqual([`secret-${suffix}`]);
|
||||
expect(byId.get(openPageId)).toEqual([]);
|
||||
});
|
||||
|
||||
it('filters listPages for a label-restricted reader and 404s the hidden page', async () => {
|
||||
|
||||
@ -67,6 +67,11 @@ describe.skipIf(!hasTestDb)('plugins install (e2e, issue #71)', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
prisma = createTestPrisma();
|
||||
// A local dev DB is shared with the e2e stack, which installs the real
|
||||
// reference plugins (`toc`, …) — clear the registry up front so the
|
||||
// fixture installs below never collide with a leftover active version.
|
||||
await prisma.pondPlugin.deleteMany({});
|
||||
await prisma.plugin.deleteMany({});
|
||||
app = await createTestApp();
|
||||
storage = app.get(PluginStorageService);
|
||||
watcher = app.get(PluginWatcherService);
|
||||
|
||||
233
apps/web/e2e/page-tools.spec.ts
Normal file
233
apps/web/e2e/page-tools.spec.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import type { BrowserContext, Page } from '@playwright/test';
|
||||
|
||||
import { contextForUser } from './helpers';
|
||||
|
||||
/**
|
||||
* pageTool plugins end to end (issue #77): installs the real `toc` and
|
||||
* `page-index` reference packages (built by `pnpm build` into
|
||||
* packages/plugins/<id>/dist/<id>-<version>.zip) and drives the acceptance
|
||||
* criteria — live outline updates after the persistence debounce, scroll on
|
||||
* heading click, permission-filtered page index, navigation via `ui.openPage`,
|
||||
* and both surfaces working as panel tools and embedded blocks.
|
||||
*/
|
||||
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173';
|
||||
|
||||
/** Generous ceiling over collab persistence debounce (2 s) + the toc's 5 s poll. */
|
||||
const OUTLINE_TIMEOUT = 20000;
|
||||
|
||||
function referenceZip(id: string): Buffer {
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
return readFileSync(join(here, `../../../packages/plugins/${id}/dist/${id}-1.0.0.zip`));
|
||||
}
|
||||
|
||||
async function installAsRequired(admin: BrowserContext, id: string): Promise<void> {
|
||||
await admin.request.patch(`/api/v1/admin/plugins/${id}/mode`, { data: { mode: 'disabled' } });
|
||||
await admin.request.delete(`/api/v1/admin/plugins/${id}`);
|
||||
const installed = await admin.request.post('/api/v1/admin/plugins', {
|
||||
multipart: {
|
||||
file: { name: `${id}.zip`, mimeType: 'application/zip', buffer: referenceZip(id) },
|
||||
},
|
||||
});
|
||||
expect(installed.status(), await installed.text()).toBe(201);
|
||||
const mode = await admin.request.patch(`/api/v1/admin/plugins/${id}/mode`, {
|
||||
data: { mode: 'required' },
|
||||
});
|
||||
expect(mode.status(), await mode.text()).toBe(200);
|
||||
}
|
||||
|
||||
async function personalPond(context: BrowserContext): Promise<{ id: string; slug: string }> {
|
||||
const ponds = await context.request.get('/api/v1/ponds');
|
||||
const pond = (await ponds.json()).find((p: { type: string }) => p.type === 'personal');
|
||||
return { id: pond.id, slug: pond.slug };
|
||||
}
|
||||
|
||||
async function createPage(
|
||||
context: BrowserContext,
|
||||
pondId: string,
|
||||
title: string,
|
||||
): Promise<{ id: string; slug: string }> {
|
||||
const created = await context.request.post(`/api/v1/ponds/${pondId}/pages`, {
|
||||
data: { title },
|
||||
});
|
||||
return created.json();
|
||||
}
|
||||
|
||||
async function openEditor(context: BrowserContext, pondSlug: string, slug: string): Promise<Page> {
|
||||
const page = await context.newPage();
|
||||
await page.goto(`/p/${pondSlug}/${slug}`);
|
||||
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
||||
await expect(page.locator('.ProseMirror')).toHaveAttribute('contenteditable', 'true');
|
||||
await expect(page.locator('.editor-connection')).toHaveAttribute('data-status', 'connected', {
|
||||
timeout: 15000,
|
||||
});
|
||||
return page;
|
||||
}
|
||||
|
||||
/** Opens the page-tools panel and expands one tool, returning its frame body. */
|
||||
async function openTool(page: Page, toolKey: string) {
|
||||
await page.locator('.editor-shell__page-tools-toggle').click();
|
||||
await page.locator(`.page-tools__tool[data-tool="${toolKey}"] .page-tools__toggle`).click();
|
||||
const host = page.locator(`.page-tools__tool[data-tool="${toolKey}"] .plugin-frame-host`);
|
||||
await expect(host).toHaveAttribute('data-state', 'ready', { timeout: 10000 });
|
||||
return page.frameLocator(`.page-tools__tool[data-tool="${toolKey}"] iframe`).locator('body');
|
||||
}
|
||||
|
||||
test('toc panel: outline appears, follows live edits, and click scrolls', async ({ browser }) => {
|
||||
const admin = await contextForUser(browser, BASE_URL, 'fixture-admin');
|
||||
await installAsRequired(admin, 'toc');
|
||||
const pond = await personalPond(admin);
|
||||
const created = await createPage(admin, pond.id, `E2E Toc ${Date.now()}`);
|
||||
|
||||
const page = await openEditor(admin, pond.slug, created.slug);
|
||||
const editor = page.locator('.ProseMirror');
|
||||
await editor.click();
|
||||
await page.keyboard.type('# Alpha heading');
|
||||
await page.keyboard.press('Enter');
|
||||
// Enough body text that the second heading sits below the fold.
|
||||
for (let i = 0; i < 40; i += 1) {
|
||||
await page.keyboard.type(`filler paragraph ${i}`);
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
await page.keyboard.type('## Omega heading');
|
||||
|
||||
const toc = await openTool(page, 'toc/toc');
|
||||
await expect(toc).toContainText('Alpha heading', { timeout: OUTLINE_TIMEOUT });
|
||||
await expect(toc).toContainText('Omega heading', { timeout: OUTLINE_TIMEOUT });
|
||||
|
||||
// Live heading edits arrive after the persistence debounce + poll.
|
||||
await editor.click();
|
||||
await page.keyboard.press('ControlOrMeta+End');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.type('## Freshly added');
|
||||
await expect(toc).toContainText('Freshly added', { timeout: OUTLINE_TIMEOUT });
|
||||
|
||||
// Clicking an entry scrolls the host content to that heading. Typing left
|
||||
// the view at the bottom — first jump to the top heading, then to Omega.
|
||||
const alpha = editor.locator('h1', { hasText: 'Alpha heading' });
|
||||
const omega = editor.locator('h2', { hasText: 'Omega heading' });
|
||||
await toc.locator('a', { hasText: 'Alpha heading' }).click();
|
||||
await expect(alpha).toBeInViewport({ timeout: 5000 });
|
||||
await expect(omega).not.toBeInViewport();
|
||||
await toc.locator('a', { hasText: 'Omega heading' }).click();
|
||||
await expect(omega).toBeInViewport({ timeout: 5000 });
|
||||
|
||||
// The same surface also works embedded as a plugin_block (issue #77 AC).
|
||||
await editor.click();
|
||||
await page.locator('.editor-toolbar__block-select').selectOption('toc/toc');
|
||||
await expect(page.locator('.plugin-block .plugin-block__surface')).toHaveAttribute(
|
||||
'data-state',
|
||||
'ready',
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
await expect(page.frameLocator('.plugin-block iframe').locator('body')).toContainText(
|
||||
'Alpha heading',
|
||||
{ timeout: OUTLINE_TIMEOUT },
|
||||
);
|
||||
|
||||
await admin.close();
|
||||
});
|
||||
|
||||
test('page-index as embedded block lists pages and navigates on click', async ({ browser }) => {
|
||||
const admin = await contextForUser(browser, BASE_URL, 'fixture-admin');
|
||||
await installAsRequired(admin, 'page-index');
|
||||
const pond = await personalPond(admin);
|
||||
const stamp = Date.now();
|
||||
const target = await createPage(admin, pond.id, `E2E Index Target ${stamp}`);
|
||||
const home = await createPage(admin, pond.id, `E2E Index Home ${stamp}`);
|
||||
|
||||
const page = await openEditor(admin, pond.slug, home.slug);
|
||||
// Embed the pageTool as a plugin_block via the insert menu (issue #77 AC).
|
||||
await page.locator('.editor-toolbar__block-select').selectOption('page-index/page-index');
|
||||
const host = page.locator('.plugin-block .plugin-block__surface');
|
||||
await expect(host).toHaveAttribute('data-state', 'ready', { timeout: 10000 });
|
||||
|
||||
const frame = page.frameLocator('.plugin-block iframe').locator('body');
|
||||
await expect(frame.locator('a', { hasText: `E2E Index Target ${stamp}` })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// `ui.openPage` navigates the host to the clicked page.
|
||||
await frame.locator('a', { hasText: `E2E Index Target ${stamp}` }).click();
|
||||
await page.waitForURL(`**/p/${pond.slug}/${target.slug}`, { timeout: 10000 });
|
||||
|
||||
await admin.close();
|
||||
});
|
||||
|
||||
test('page-index respects the viewer permissions (label-restricted reader)', async ({
|
||||
browser,
|
||||
}) => {
|
||||
const owner = await contextForUser(browser, BASE_URL, 'fixture-user');
|
||||
const admin = await contextForUser(browser, BASE_URL, 'fixture-admin');
|
||||
const viewer = await contextForUser(browser, BASE_URL, 'fixture-viewer');
|
||||
await installAsRequired(admin, 'page-index');
|
||||
|
||||
// The owner's pond: an open page, a secret-labeled page, and a reader who
|
||||
// may read the pond but is denied the secret label (permissions.md).
|
||||
const pond = await personalPond(owner);
|
||||
const stamp = Date.now();
|
||||
const open = await createPage(owner, pond.id, `E2E Perm Open ${stamp}`);
|
||||
const secret = await createPage(owner, pond.id, `E2E Perm Secret ${stamp}`);
|
||||
const label = await (
|
||||
await owner.request.post(`/api/v1/ponds/${pond.id}/labels`, {
|
||||
data: { name: `e2e-secret-${stamp}` },
|
||||
})
|
||||
).json();
|
||||
await owner.request.post(`/api/v1/pages/${secret.id}/labels`, {
|
||||
data: { labelId: label.id },
|
||||
});
|
||||
const viewerId = ((await (await viewer.request.get('/api/v1/auth/me')).json()) as { id: string })
|
||||
.id;
|
||||
const allow = await owner.request.post(`/api/v1/ponds/${pond.id}/grants`, {
|
||||
data: {
|
||||
subjectType: 'user',
|
||||
subjectId: viewerId,
|
||||
role: 'reader',
|
||||
scopeType: 'pond',
|
||||
effect: 'allow',
|
||||
},
|
||||
});
|
||||
// 409 = the pond-wide reader grant survived an earlier run — same effect.
|
||||
expect([201, 409], await allow.text()).toContain(allow.status());
|
||||
const deny = await owner.request.post(`/api/v1/ponds/${pond.id}/grants`, {
|
||||
data: {
|
||||
subjectType: 'user',
|
||||
subjectId: viewerId,
|
||||
role: 'reader',
|
||||
scopeType: 'label',
|
||||
scopeId: label.id,
|
||||
effect: 'deny',
|
||||
},
|
||||
});
|
||||
expect(deny.status(), await deny.text()).toBe(201);
|
||||
|
||||
// The restricted reader's page index shows the open page, never the secret.
|
||||
const page = await viewer.newPage();
|
||||
await page.goto(`/p/${pond.slug}/${open.slug}`);
|
||||
await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 15000 });
|
||||
const index = await openTool(page, 'page-index/page-index');
|
||||
await expect(index.locator('a', { hasText: `E2E Perm Open ${stamp}` })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(index.locator('a', { hasText: `E2E Perm Secret ${stamp}` })).toHaveCount(0);
|
||||
|
||||
// The owner sees both, and can narrow by the label filter chip.
|
||||
const ownerPage = await owner.newPage();
|
||||
await ownerPage.goto(`/p/${pond.slug}/${open.slug}`);
|
||||
await expect(ownerPage.locator('.ProseMirror')).toBeVisible({ timeout: 15000 });
|
||||
const ownerIndex = await openTool(ownerPage, 'page-index/page-index');
|
||||
await expect(ownerIndex.locator('a', { hasText: `E2E Perm Secret ${stamp}` })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await ownerIndex.locator(`button[data-filter="e2e-secret-${stamp}"]`).click();
|
||||
await expect(ownerIndex.locator('a', { hasText: `E2E Perm Open ${stamp}` })).toHaveCount(0);
|
||||
await expect(ownerIndex.locator('a', { hasText: `E2E Perm Secret ${stamp}` })).toBeVisible();
|
||||
|
||||
await owner.close();
|
||||
await admin.close();
|
||||
await viewer.close();
|
||||
});
|
||||
@ -12,6 +12,8 @@ export interface PluginBlockScope {
|
||||
pondId?: string;
|
||||
/** Navigate to a page (backs the `ui.openPage` capability). */
|
||||
openPage?: (pageId: string) => void;
|
||||
/** Scroll the content to a heading by outline id (`ui.scrollToHeading`, #77). */
|
||||
scrollToHeading?: (headingId: string) => void;
|
||||
}
|
||||
|
||||
export const PluginBlockContext = createContext<PluginBlockScope>({});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DEFAULT_FONTS } from '@dorfteich/shared';
|
||||
import { DEFAULT_FONTS, extractOutline } from '@dorfteich/shared';
|
||||
import type { PageListItemView, PageStateView, PondView } from '@dorfteich/shared';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Collaboration } from '@tiptap/extension-collaboration';
|
||||
@ -29,6 +29,7 @@ import { useForceSidebarHidden } from '../layout/sidebar-chrome';
|
||||
import { ApiError, apiDelete, apiGet, apiGetText, apiPatch } from '../lib/api';
|
||||
import { recallPage, rememberPage } from '../offline/page-cache';
|
||||
import { PluginBlockContext } from '../editor/plugin-block-context';
|
||||
import { hasPageTools, PageToolsPanel } from '../plugins/PageToolsPanel';
|
||||
import { SectionStyleSheets } from '../plugins/SectionStyleSheets';
|
||||
import {
|
||||
pluginBlockOptions,
|
||||
@ -67,6 +68,7 @@ function PageEditor({
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [showAttachments, setShowAttachments] = useState(false);
|
||||
const [showPageTools, setShowPageTools] = useState(false);
|
||||
|
||||
// Created and destroyed within the same effect (not `useMemo` + a separate
|
||||
// cleanup effect): React StrictMode's dev-only mount→cleanup→remount would
|
||||
@ -151,8 +153,18 @@ function PageEditor({
|
||||
const target = (pondPagesData ?? []).find((p) => p.id === pageId);
|
||||
if (target) navigate(`/p/${pondSlug}/${target.slug}`);
|
||||
},
|
||||
// Outline ids are derived from the doc (extractOutline), never stamped
|
||||
// into the DOM — so resolve the id to its heading *position* and scroll
|
||||
// the matching rendered heading (#77).
|
||||
scrollToHeading: (headingId: string) => {
|
||||
if (!editor) return;
|
||||
const index = extractOutline(editor.state.doc).findIndex((entry) => entry.id === headingId);
|
||||
if (index < 0) return;
|
||||
const headings = editor.view.dom.querySelectorAll('h1, h2, h3, h4');
|
||||
headings[index]?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
},
|
||||
}),
|
||||
[page.id, page.pondId, pondPagesData, pondSlug, navigate],
|
||||
[page.id, page.pondId, pondPagesData, pondSlug, navigate, editor],
|
||||
);
|
||||
const blockInserts = useMemo(() => pluginBlockOptions(pondPlugins.data), [pondPlugins.data]);
|
||||
|
||||
@ -175,7 +187,20 @@ function PageEditor({
|
||||
>
|
||||
{t('files:title')}
|
||||
</button>
|
||||
{hasPageTools(pondPlugins.data) && (
|
||||
<button
|
||||
type="button"
|
||||
className="button editor-shell__page-tools-toggle"
|
||||
aria-expanded={showPageTools}
|
||||
onClick={() => setShowPageTools((open) => !open)}
|
||||
>
|
||||
{t('plugins:tools.title')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{showPageTools && (
|
||||
<PageToolsPanel plugins={pondPlugins.data} context={pluginBlockScope} />
|
||||
)}
|
||||
{showAttachments && (
|
||||
<AttachmentsPanel
|
||||
pageId={page.id}
|
||||
|
||||
81
apps/web/src/plugins/PageToolsPanel.tsx
Normal file
81
apps/web/src/plugins/PageToolsPanel.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { PluginView } from '@dorfteich/shared';
|
||||
|
||||
import { type PluginHostContext } from './host-capabilities';
|
||||
import { PluginFrame } from './PluginFrame';
|
||||
|
||||
/** One pageTool surface: a plugin + the extension point it exposes. */
|
||||
interface PageToolEntry {
|
||||
plugin: PluginView;
|
||||
pointId: string;
|
||||
title: Record<string, string>;
|
||||
}
|
||||
|
||||
function toolEntries(plugins: PluginView[] | undefined): PageToolEntry[] {
|
||||
return (plugins ?? [])
|
||||
.filter((plugin) => plugin.kind === 'code')
|
||||
.flatMap((plugin) =>
|
||||
plugin.extensionPoints
|
||||
.filter((point) => point.type === 'pageTool')
|
||||
.map((point) => ({ plugin, pointId: point.id, title: point.title })),
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether the pond has any pageTool surface — gates the panel's toggle. */
|
||||
export function hasPageTools(plugins: PluginView[] | undefined): boolean {
|
||||
return toolEntries(plugins).length > 0;
|
||||
}
|
||||
|
||||
function titleFor(entry: PageToolEntry, language: string): string {
|
||||
const base = language.split('-')[0] ?? language;
|
||||
return entry.title[base] ?? entry.title.en ?? entry.pointId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The page's tools panel (issue #77): every active `pageTool` plugin surface
|
||||
* of the pond, each behind a disclosure — its sandbox iframe mounts only when
|
||||
* opened for the first time (lazy, ADR 0008 "pageTool widgets are
|
||||
* lazy-loaded") and unmounts when closed, tearing the frame down.
|
||||
*/
|
||||
export function PageToolsPanel({
|
||||
plugins,
|
||||
context,
|
||||
}: {
|
||||
plugins: PluginView[] | undefined;
|
||||
context: PluginHostContext;
|
||||
}): React.JSX.Element | null {
|
||||
const { i18n } = useTranslation('plugins');
|
||||
const [open, setOpen] = useState<Record<string, boolean>>({});
|
||||
|
||||
const entries = toolEntries(plugins);
|
||||
if (entries.length === 0) return null;
|
||||
|
||||
return (
|
||||
<aside className="page-tools">
|
||||
{entries.map((entry) => {
|
||||
const key = `${entry.plugin.id}/${entry.pointId}`;
|
||||
const isOpen = open[key] === true;
|
||||
return (
|
||||
<section key={key} className="page-tools__tool" data-tool={key}>
|
||||
<button
|
||||
type="button"
|
||||
className="page-tools__toggle"
|
||||
aria-expanded={isOpen}
|
||||
onClick={() => setOpen((state) => ({ ...state, [key]: !isOpen }))}
|
||||
>
|
||||
{titleFor(entry, i18n.language)}
|
||||
</button>
|
||||
{isOpen && (
|
||||
<PluginFrame
|
||||
plugin={entry.plugin}
|
||||
extensionPointId={entry.pointId}
|
||||
context={context}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@ -21,6 +21,9 @@ export interface PluginHostContext {
|
||||
openPage?: (pageId: string) => void;
|
||||
/** Show a localized toast (the `ui.toast` capability). */
|
||||
toast?: (messageKey: string) => void;
|
||||
/** Scroll the page content to a heading by its outline id (`ui.scrollToHeading`,
|
||||
* issue #77 — the TOC reference plugin's click target). */
|
||||
scrollToHeading?: (headingId: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -62,6 +65,9 @@ export function buildHostCapabilities(context: PluginHostContext): HostCapabilit
|
||||
toast: (params) => {
|
||||
if (typeof params === 'string') context.toast?.(params);
|
||||
},
|
||||
scrollToHeading: (params) => {
|
||||
if (typeof params === 'string' && params.length > 0) context.scrollToHeading?.(params);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -39,7 +39,9 @@ export function sectionStyleOptions(plugins: PluginView[] | undefined): SectionS
|
||||
}
|
||||
|
||||
/** The block types the active `code` plugins offer for insertion (issue #76),
|
||||
* flattened for the editor's insert menu. */
|
||||
* flattened for the editor's insert menu. `pageTool` surfaces are insertable
|
||||
* too (issue #77: "embeddable as a plugin_block") — the sandbox drives both
|
||||
* through the same render lifecycle. */
|
||||
export interface PluginBlockOption {
|
||||
pluginId: string;
|
||||
blockType: string;
|
||||
@ -51,7 +53,7 @@ export function pluginBlockOptions(plugins: PluginView[] | undefined): PluginBlo
|
||||
.filter((plugin) => plugin.kind === 'code')
|
||||
.flatMap((plugin) =>
|
||||
plugin.extensionPoints
|
||||
.filter((point) => point.type === 'block')
|
||||
.filter((point) => point.type === 'block' || point.type === 'pageTool')
|
||||
.map((point) => ({ pluginId: plugin.id, blockType: point.id, title: point.title })),
|
||||
);
|
||||
}
|
||||
|
||||
@ -610,6 +610,34 @@ button {
|
||||
outline-color: var(--color-border);
|
||||
}
|
||||
|
||||
/* The page-tools panel (issue #77): active pageTool plugin surfaces behind
|
||||
* disclosures; each iframe mounts lazily on first open. */
|
||||
.page-tools {
|
||||
margin: var(--space-2) var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.page-tools__tool + .page-tools__tool {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.page-tools__toggle {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: none;
|
||||
background: var(--color-bg-subtle);
|
||||
color: var(--color-text);
|
||||
text-align: left;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-tools__tool .plugin-frame-host {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
}
|
||||
|
||||
/* A plugin-owned block (issue #76): a framed island in the content column.
|
||||
* The sandbox iframe sizes itself via ui.resize; the bar carries the plugin
|
||||
* name and the edit affordance. */
|
||||
|
||||
@ -91,7 +91,7 @@ person looking at it could.
|
||||
| `readPond` | `listPages()`, `getPageOutline(pageId)`, `getPageContent(pageId)` |
|
||||
| `readBlock` | `getBlock(pageId, blockId)` — cross-page block embedding |
|
||||
| `blockData` | `getData()` / `setData(data)` for the plugin's own block instance (writes go through the editor as a normal document change — requires the viewer to have write permission) |
|
||||
| `ui` | `resize(height)`, `openPage(pageId)` (host navigates), `toast(msgKey)` |
|
||||
| `ui` | `resize(height)`, `openPage(pageId)` (host navigates), `toast(msgKey)`, `scrollToHeading(headingId)` (host scrolls to an outline entry, #77) |
|
||||
|
||||
## Lifecycle & administration
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ export const CAPABILITY_METHODS = {
|
||||
readPond: ['listPages', 'getPageOutline', 'getPageContent'],
|
||||
readBlock: ['getBlock'],
|
||||
blockData: ['getData', 'setData'],
|
||||
ui: ['resize', 'openPage', 'toast'],
|
||||
ui: ['resize', 'openPage', 'toast', 'scrollToHeading'],
|
||||
} as const satisfies Record<Capability, readonly string[]>;
|
||||
|
||||
/** Every host method name across all capabilities. */
|
||||
|
||||
@ -25,6 +25,9 @@ export interface PageSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
/** Names of the page's labels — the page-index reference plugin filters on
|
||||
* these (issue #77). Only pages the viewer may read arrive here at all. */
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -55,6 +58,8 @@ export interface PluginHost {
|
||||
resize: (height: number) => Promise<void>;
|
||||
openPage: (pageId: string) => Promise<void>;
|
||||
toast: (messageKey: string) => Promise<void>;
|
||||
/** Scroll the host page to a heading by its outline id (issue #77). */
|
||||
scrollToHeading: (headingId: string) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
34
packages/plugins/page-index/build.mjs
Normal file
34
packages/plugins/page-index/build.mjs
Normal file
@ -0,0 +1,34 @@
|
||||
// Builds the installable plugin (plugin-architecture.md §Package format):
|
||||
// bundles src/plugin.ts (SDK + i18n inlined — the sandbox CSP forbids runtime
|
||||
// fetches) into plugin.js as a single ES module, then packs the ZIP for the
|
||||
// admin upload / dropzone watcher.
|
||||
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { build } from 'esbuild';
|
||||
import { zipSync } from 'fflate';
|
||||
|
||||
const root = dirname(fileURLToPath(import.meta.url));
|
||||
const manifest = JSON.parse(readFileSync(join(root, 'manifest.json'), 'utf8'));
|
||||
|
||||
mkdirSync(join(root, 'dist'), { recursive: true });
|
||||
await build({
|
||||
entryPoints: [join(root, 'src/plugin.ts')],
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
outfile: join(root, 'dist/plugin.js'),
|
||||
minify: true,
|
||||
});
|
||||
|
||||
const files = {
|
||||
'manifest.json': readFileSync(join(root, 'manifest.json')),
|
||||
'plugin.js': readFileSync(join(root, 'dist/plugin.js')),
|
||||
};
|
||||
for (const name of readdirSync(join(root, 'i18n'))) {
|
||||
files[`i18n/${name}`] = readFileSync(join(root, 'i18n', name));
|
||||
}
|
||||
|
||||
const target = join(root, 'dist', `${manifest.id}-${manifest.version}.zip`);
|
||||
writeFileSync(target, zipSync(files));
|
||||
console.log(`wrote ${target}`);
|
||||
5
packages/plugins/page-index/i18n/de.json
Normal file
5
packages/plugins/page-index/i18n/de.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"empty": "Keine Seiten gefunden.",
|
||||
"all": "Alle",
|
||||
"error": "Der Seitenindex konnte nicht geladen werden."
|
||||
}
|
||||
5
packages/plugins/page-index/i18n/en.json
Normal file
5
packages/plugins/page-index/i18n/en.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"empty": "No pages found.",
|
||||
"all": "All",
|
||||
"error": "The page index could not be loaded."
|
||||
}
|
||||
18
packages/plugins/page-index/manifest.json
Normal file
18
packages/plugins/page-index/manifest.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "page-index",
|
||||
"name": "Page Index",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": "1",
|
||||
"kind": "code",
|
||||
"extensionPoints": [
|
||||
{
|
||||
"type": "pageTool",
|
||||
"id": "page-index",
|
||||
"title": { "de": "Seitenindex", "en": "Page index" }
|
||||
}
|
||||
],
|
||||
"permissions": ["readPond", "ui"],
|
||||
"fallback": { "type": "text", "value": "[Page index]" },
|
||||
"license": "MIT",
|
||||
"i18n": { "de": "i18n/de.json", "en": "i18n/en.json" }
|
||||
}
|
||||
20
packages/plugins/page-index/package.json
Normal file
20
packages/plugins/page-index/package.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@dorfteich/plugin-page-index",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Reference pageTool plugin: label-filtered page list of the pond (issue #77)",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "node build.mjs",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dorfteich/plugin-sdk": "workspace:*",
|
||||
"@types/node": "^26.1.0",
|
||||
"esbuild": "^0.24.0",
|
||||
"fflate": "^0.8.2",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
104
packages/plugins/page-index/src/plugin.ts
Normal file
104
packages/plugins/page-index/src/plugin.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { createPlugin, windowTransport, type PageSummary } from '@dorfteich/plugin-sdk';
|
||||
|
||||
import de from '../i18n/de.json';
|
||||
import en from '../i18n/en.json';
|
||||
|
||||
/**
|
||||
* Page-index reference plugin (issue #77, plugin-architecture.md §Reference
|
||||
* plugins): lists the pond's pages the viewer may read (`readPond` — the
|
||||
* server filters per page, so permissions are inherently respected), with
|
||||
* label chips to filter by. Clicking a page navigates via `ui.openPage`.
|
||||
*
|
||||
* Strings are bundled (the sandbox CSP blocks runtime fetches); the files in
|
||||
* `i18n/` are the single source and are inlined at build time.
|
||||
*/
|
||||
const STRINGS: Record<string, Record<string, string>> = { de, en };
|
||||
|
||||
function labelFor(locale: string, key: string): string {
|
||||
const base = locale.split('-')[0] ?? locale;
|
||||
return STRINGS[base]?.[key] ?? STRINGS.en?.[key] ?? key;
|
||||
}
|
||||
|
||||
const { host } = createPlugin({
|
||||
transport: windowTransport({
|
||||
// Window.postMessage's overloads don't structurally match the transport's
|
||||
// minimal shape; this adapter pins the sandbox-safe wildcard origin.
|
||||
target: { postMessage: (message) => window.parent.postMessage(message, '*') },
|
||||
source: window,
|
||||
}),
|
||||
onRender: (context) => void render(context.locale),
|
||||
});
|
||||
|
||||
/** The active label filter; `null` shows every readable page. */
|
||||
let activeLabel: string | null = null;
|
||||
|
||||
async function render(locale: string): Promise<void> {
|
||||
let pages: PageSummary[];
|
||||
try {
|
||||
pages = await host.readPond.listPages();
|
||||
} catch {
|
||||
document.body.textContent = labelFor(locale, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
document.body.textContent = '';
|
||||
document.body.className = 'dt-page-index';
|
||||
|
||||
const labels = [...new Set(pages.flatMap((page) => page.labels))].sort();
|
||||
if (labels.length > 0) {
|
||||
document.body.appendChild(buildFilterBar(labels, locale));
|
||||
}
|
||||
|
||||
const shown = activeLabel ? pages.filter((page) => page.labels.includes(activeLabel!)) : pages;
|
||||
if (shown.length === 0) {
|
||||
const empty = document.createElement('p');
|
||||
empty.textContent = labelFor(locale, 'empty');
|
||||
document.body.appendChild(empty);
|
||||
} else {
|
||||
const list = document.createElement('ul');
|
||||
for (const page of shown) {
|
||||
const item = document.createElement('li');
|
||||
const link = document.createElement('a');
|
||||
link.href = '#';
|
||||
link.textContent = page.title;
|
||||
link.dataset.pageId = page.id;
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
void host.ui.openPage(page.id);
|
||||
});
|
||||
item.appendChild(link);
|
||||
if (page.labels.length > 0) {
|
||||
const chips = document.createElement('span');
|
||||
chips.textContent = ` (${page.labels.join(', ')})`;
|
||||
item.appendChild(chips);
|
||||
}
|
||||
list.appendChild(item);
|
||||
}
|
||||
document.body.appendChild(list);
|
||||
}
|
||||
|
||||
void host.ui.resize(document.body.scrollHeight + 16);
|
||||
}
|
||||
|
||||
function buildFilterBar(labels: string[], locale: string): HTMLElement {
|
||||
const bar = document.createElement('p');
|
||||
bar.className = 'dt-page-index__filters';
|
||||
const options: Array<{ value: string | null; text: string }> = [
|
||||
{ value: null, text: labelFor(locale, 'all') },
|
||||
...labels.map((label) => ({ value: label, text: label })),
|
||||
];
|
||||
for (const option of options) {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.textContent = option.text;
|
||||
button.dataset.filter = option.value ?? '';
|
||||
button.style.fontWeight = option.value === activeLabel ? 'bold' : 'normal';
|
||||
button.addEventListener('click', () => {
|
||||
activeLabel = option.value;
|
||||
void render(locale);
|
||||
});
|
||||
bar.appendChild(button);
|
||||
bar.appendChild(document.createTextNode(' '));
|
||||
}
|
||||
return bar;
|
||||
}
|
||||
11
packages/plugins/page-index/tsconfig.json
Normal file
11
packages/plugins/page-index/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true,
|
||||
"lib": ["ES2022", "DOM"]
|
||||
},
|
||||
"include": ["src", "*.ts"]
|
||||
}
|
||||
34
packages/plugins/toc/build.mjs
Normal file
34
packages/plugins/toc/build.mjs
Normal file
@ -0,0 +1,34 @@
|
||||
// Builds the installable plugin (plugin-architecture.md §Package format):
|
||||
// bundles src/plugin.ts (SDK + i18n inlined — the sandbox CSP forbids runtime
|
||||
// fetches) into plugin.js as a single ES module, then packs the ZIP for the
|
||||
// admin upload / dropzone watcher.
|
||||
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { build } from 'esbuild';
|
||||
import { zipSync } from 'fflate';
|
||||
|
||||
const root = dirname(fileURLToPath(import.meta.url));
|
||||
const manifest = JSON.parse(readFileSync(join(root, 'manifest.json'), 'utf8'));
|
||||
|
||||
mkdirSync(join(root, 'dist'), { recursive: true });
|
||||
await build({
|
||||
entryPoints: [join(root, 'src/plugin.ts')],
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
outfile: join(root, 'dist/plugin.js'),
|
||||
minify: true,
|
||||
});
|
||||
|
||||
const files = {
|
||||
'manifest.json': readFileSync(join(root, 'manifest.json')),
|
||||
'plugin.js': readFileSync(join(root, 'dist/plugin.js')),
|
||||
};
|
||||
for (const name of readdirSync(join(root, 'i18n'))) {
|
||||
files[`i18n/${name}`] = readFileSync(join(root, 'i18n', name));
|
||||
}
|
||||
|
||||
const target = join(root, 'dist', `${manifest.id}-${manifest.version}.zip`);
|
||||
writeFileSync(target, zipSync(files));
|
||||
console.log(`wrote ${target}`);
|
||||
4
packages/plugins/toc/i18n/de.json
Normal file
4
packages/plugins/toc/i18n/de.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"empty": "Diese Seite hat noch keine Überschriften.",
|
||||
"error": "Das Inhaltsverzeichnis konnte nicht geladen werden."
|
||||
}
|
||||
4
packages/plugins/toc/i18n/en.json
Normal file
4
packages/plugins/toc/i18n/en.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"empty": "This page has no headings yet.",
|
||||
"error": "The table of contents could not be loaded."
|
||||
}
|
||||
18
packages/plugins/toc/manifest.json
Normal file
18
packages/plugins/toc/manifest.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "toc",
|
||||
"name": "Table of Contents",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": "1",
|
||||
"kind": "code",
|
||||
"extensionPoints": [
|
||||
{
|
||||
"type": "pageTool",
|
||||
"id": "toc",
|
||||
"title": { "de": "Inhaltsverzeichnis", "en": "Table of contents" }
|
||||
}
|
||||
],
|
||||
"permissions": ["readCurrentPage", "ui"],
|
||||
"fallback": { "type": "text", "value": "[Table of contents]" },
|
||||
"license": "MIT",
|
||||
"i18n": { "de": "i18n/de.json", "en": "i18n/en.json" }
|
||||
}
|
||||
20
packages/plugins/toc/package.json
Normal file
20
packages/plugins/toc/package.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@dorfteich/plugin-toc",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Reference pageTool plugin: table of contents from the current page's outline (issue #77)",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "node build.mjs",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dorfteich/plugin-sdk": "workspace:*",
|
||||
"@types/node": "^26.1.0",
|
||||
"esbuild": "^0.24.0",
|
||||
"fflate": "^0.8.2",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
92
packages/plugins/toc/src/plugin.ts
Normal file
92
packages/plugins/toc/src/plugin.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { createPlugin, windowTransport, type OutlineEntry } from '@dorfteich/plugin-sdk';
|
||||
|
||||
import de from '../i18n/de.json';
|
||||
import en from '../i18n/en.json';
|
||||
|
||||
/**
|
||||
* Table-of-contents reference plugin (issue #77, plugin-architecture.md
|
||||
* §Reference plugins): renders the current page's heading outline via the
|
||||
* `readCurrentPage` capability; clicking an entry scrolls the host page to
|
||||
* the heading (`ui.scrollToHeading`). The outline is re-fetched on a slow
|
||||
* poll while the surface is mounted, so live heading edits appear once the
|
||||
* server has re-derived the content cache (persistence debounce).
|
||||
*
|
||||
* Strings are bundled (the sandbox CSP blocks runtime fetches); the files in
|
||||
* `i18n/` are the single source and are inlined at build time.
|
||||
*/
|
||||
const STRINGS: Record<string, Record<string, string>> = { de, en };
|
||||
const REFRESH_MS = 5000;
|
||||
|
||||
function labelFor(locale: string, key: string): string {
|
||||
const base = locale.split('-')[0] ?? locale;
|
||||
return STRINGS[base]?.[key] ?? STRINGS.en?.[key] ?? key;
|
||||
}
|
||||
|
||||
const { host } = createPlugin({
|
||||
transport: windowTransport({
|
||||
// Window.postMessage's overloads don't structurally match the transport's
|
||||
// minimal shape; this adapter pins the sandbox-safe wildcard origin.
|
||||
target: { postMessage: (message) => window.parent.postMessage(message, '*') },
|
||||
source: window,
|
||||
}),
|
||||
onRender: (context) => start(context.locale),
|
||||
onDestroy: () => stop(),
|
||||
});
|
||||
|
||||
let timer: number | null = null;
|
||||
let lastOutlineJson = '';
|
||||
|
||||
function start(locale: string): void {
|
||||
stop();
|
||||
void refresh(locale, true);
|
||||
timer = window.setInterval(() => void refresh(locale, false), REFRESH_MS);
|
||||
}
|
||||
|
||||
function stop(): void {
|
||||
if (timer !== null) window.clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
|
||||
async function refresh(locale: string, force: boolean): Promise<void> {
|
||||
let outline: OutlineEntry[];
|
||||
try {
|
||||
outline = await host.readCurrentPage.getOutline();
|
||||
} catch {
|
||||
document.body.textContent = labelFor(locale, 'error');
|
||||
return;
|
||||
}
|
||||
const json = JSON.stringify(outline);
|
||||
if (!force && json === lastOutlineJson) return;
|
||||
lastOutlineJson = json;
|
||||
render(outline, locale);
|
||||
}
|
||||
|
||||
function render(outline: OutlineEntry[], locale: string): void {
|
||||
document.body.textContent = '';
|
||||
document.body.className = 'dt-toc';
|
||||
|
||||
if (outline.length === 0) {
|
||||
const empty = document.createElement('p');
|
||||
empty.textContent = labelFor(locale, 'empty');
|
||||
document.body.appendChild(empty);
|
||||
} else {
|
||||
const list = document.createElement('ul');
|
||||
for (const entry of outline) {
|
||||
const item = document.createElement('li');
|
||||
item.style.marginLeft = `${(entry.level - 1) * 1}rem`;
|
||||
const link = document.createElement('a');
|
||||
link.href = '#';
|
||||
link.textContent = entry.text;
|
||||
link.dataset.headingId = entry.id;
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
void host.ui.scrollToHeading(entry.id);
|
||||
});
|
||||
item.appendChild(link);
|
||||
list.appendChild(item);
|
||||
}
|
||||
document.body.appendChild(list);
|
||||
}
|
||||
|
||||
void host.ui.resize(document.body.scrollHeight + 16);
|
||||
}
|
||||
11
packages/plugins/toc/tsconfig.json
Normal file
11
packages/plugins/toc/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true,
|
||||
"lib": ["ES2022", "DOM"]
|
||||
},
|
||||
"include": ["src", "*.ts"]
|
||||
}
|
||||
@ -8,6 +8,9 @@
|
||||
"done": "Fertig",
|
||||
"inactive": "Das Plugin „{{name}}“ ist für diesen Teich nicht aktiv."
|
||||
},
|
||||
"tools": {
|
||||
"title": "Seiten-Werkzeuge"
|
||||
},
|
||||
"preview": {
|
||||
"loading": "Plugins werden geladen …",
|
||||
"loadFailed": "Die Plugin-Liste konnte nicht geladen werden.",
|
||||
|
||||
@ -8,6 +8,9 @@
|
||||
"done": "Done",
|
||||
"inactive": "The plugin “{{name}}” is not active for this pond."
|
||||
},
|
||||
"tools": {
|
||||
"title": "Page tools"
|
||||
},
|
||||
"preview": {
|
||||
"loading": "Loading plugins …",
|
||||
"loadFailed": "The plugin list could not be loaded.",
|
||||
|
||||
@ -108,6 +108,8 @@ export interface PluginPageSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
/** Label names, for pageTool filtering (issue #77). */
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
export interface PluginPageMeta {
|
||||
|
||||
300
pnpm-lock.yaml
generated
300
pnpm-lock.yaml
generated
@ -337,6 +337,27 @@ importers:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.6(@types/node@26.1.0)(jiti@2.7.0)(jsdom@26.1.0)(terser@5.48.0)(tsx@4.23.0)
|
||||
|
||||
packages/plugins/page-index:
|
||||
devDependencies:
|
||||
'@dorfteich/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugin-sdk
|
||||
'@types/node':
|
||||
specifier: ^26.1.0
|
||||
version: 26.1.0
|
||||
esbuild:
|
||||
specifier: ^0.24.0
|
||||
version: 0.24.2
|
||||
fflate:
|
||||
specifier: ^0.8.2
|
||||
version: 0.8.3
|
||||
typescript:
|
||||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.6(@types/node@26.1.0)(jiti@2.7.0)(jsdom@26.1.0)(terser@5.48.0)(tsx@4.23.0)
|
||||
|
||||
packages/plugins/section-styles-basic:
|
||||
devDependencies:
|
||||
'@dorfteich/plugin-sdk':
|
||||
@ -355,6 +376,27 @@ importers:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.6(@types/node@26.1.0)(jiti@2.7.0)(jsdom@26.1.0)(terser@5.48.0)(tsx@4.23.0)
|
||||
|
||||
packages/plugins/toc:
|
||||
devDependencies:
|
||||
'@dorfteich/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugin-sdk
|
||||
'@types/node':
|
||||
specifier: ^26.1.0
|
||||
version: 26.1.0
|
||||
esbuild:
|
||||
specifier: ^0.24.0
|
||||
version: 0.24.2
|
||||
fflate:
|
||||
specifier: ^0.8.2
|
||||
version: 0.8.3
|
||||
typescript:
|
||||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.6(@types/node@26.1.0)(jiti@2.7.0)(jsdom@26.1.0)(terser@5.48.0)(tsx@4.23.0)
|
||||
|
||||
packages/shared:
|
||||
dependencies:
|
||||
markdown-it:
|
||||
@ -979,6 +1021,12 @@ packages:
|
||||
'@epic-web/invariant@1.0.0':
|
||||
resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.24.2':
|
||||
resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.12':
|
||||
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
|
||||
engines: {node: '>=18'}
|
||||
@ -997,6 +1045,12 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1015,6 +1069,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.24.2':
|
||||
resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.25.12':
|
||||
resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1033,6 +1093,12 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.24.2':
|
||||
resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.25.12':
|
||||
resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1051,6 +1117,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1069,6 +1141,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.24.2':
|
||||
resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.25.12':
|
||||
resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1087,6 +1165,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1105,6 +1189,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.24.2':
|
||||
resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.12':
|
||||
resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1123,6 +1213,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1141,6 +1237,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.24.2':
|
||||
resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.25.12':
|
||||
resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1159,6 +1261,12 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.24.2':
|
||||
resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.25.12':
|
||||
resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1177,6 +1285,12 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.24.2':
|
||||
resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.25.12':
|
||||
resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1195,6 +1309,12 @@ packages:
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.24.2':
|
||||
resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.12':
|
||||
resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1213,6 +1333,12 @@ packages:
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.24.2':
|
||||
resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.12':
|
||||
resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1231,6 +1357,12 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.24.2':
|
||||
resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.12':
|
||||
resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1249,6 +1381,12 @@ packages:
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.24.2':
|
||||
resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.25.12':
|
||||
resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1267,6 +1405,12 @@ packages:
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.24.2':
|
||||
resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.25.12':
|
||||
resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1285,6 +1429,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1303,6 +1453,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.24.2':
|
||||
resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.12':
|
||||
resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1321,6 +1477,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1339,6 +1501,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.24.2':
|
||||
resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.12':
|
||||
resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1375,6 +1543,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.24.2':
|
||||
resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/sunos-x64@0.25.12':
|
||||
resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1393,6 +1567,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1411,6 +1591,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.24.2':
|
||||
resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.25.12':
|
||||
resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1429,6 +1615,12 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.24.2':
|
||||
resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.25.12':
|
||||
resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
|
||||
engines: {node: '>=18'}
|
||||
@ -3212,6 +3404,11 @@ packages:
|
||||
resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
esbuild@0.24.2:
|
||||
resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esbuild@0.25.12:
|
||||
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -6338,6 +6535,9 @@ snapshots:
|
||||
|
||||
'@epic-web/invariant@1.0.0': {}
|
||||
|
||||
'@esbuild/aix-ppc64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6347,6 +6547,9 @@ snapshots:
|
||||
'@esbuild/aix-ppc64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6356,6 +6559,9 @@ snapshots:
|
||||
'@esbuild/android-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6365,6 +6571,9 @@ snapshots:
|
||||
'@esbuild/android-arm@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6374,6 +6583,9 @@ snapshots:
|
||||
'@esbuild/android-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6383,6 +6595,9 @@ snapshots:
|
||||
'@esbuild/darwin-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6392,6 +6607,9 @@ snapshots:
|
||||
'@esbuild/darwin-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6401,6 +6619,9 @@ snapshots:
|
||||
'@esbuild/freebsd-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6410,6 +6631,9 @@ snapshots:
|
||||
'@esbuild/freebsd-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6419,6 +6643,9 @@ snapshots:
|
||||
'@esbuild/linux-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6428,6 +6655,9 @@ snapshots:
|
||||
'@esbuild/linux-arm@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6437,6 +6667,9 @@ snapshots:
|
||||
'@esbuild/linux-ia32@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6446,6 +6679,9 @@ snapshots:
|
||||
'@esbuild/linux-loong64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6455,6 +6691,9 @@ snapshots:
|
||||
'@esbuild/linux-mips64el@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6464,6 +6703,9 @@ snapshots:
|
||||
'@esbuild/linux-ppc64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6473,6 +6715,9 @@ snapshots:
|
||||
'@esbuild/linux-riscv64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6482,6 +6727,9 @@ snapshots:
|
||||
'@esbuild/linux-s390x@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6491,6 +6739,9 @@ snapshots:
|
||||
'@esbuild/linux-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6500,6 +6751,9 @@ snapshots:
|
||||
'@esbuild/netbsd-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6509,6 +6763,9 @@ snapshots:
|
||||
'@esbuild/netbsd-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6518,6 +6775,9 @@ snapshots:
|
||||
'@esbuild/openbsd-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6536,6 +6796,9 @@ snapshots:
|
||||
'@esbuild/openharmony-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6545,6 +6808,9 @@ snapshots:
|
||||
'@esbuild/sunos-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6554,6 +6820,9 @@ snapshots:
|
||||
'@esbuild/win32-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -6563,6 +6832,9 @@ snapshots:
|
||||
'@esbuild/win32-ia32@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
@ -8401,6 +8673,34 @@ snapshots:
|
||||
is-date-object: 1.1.0
|
||||
is-symbol: 1.1.1
|
||||
|
||||
esbuild@0.24.2:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.24.2
|
||||
'@esbuild/android-arm': 0.24.2
|
||||
'@esbuild/android-arm64': 0.24.2
|
||||
'@esbuild/android-x64': 0.24.2
|
||||
'@esbuild/darwin-arm64': 0.24.2
|
||||
'@esbuild/darwin-x64': 0.24.2
|
||||
'@esbuild/freebsd-arm64': 0.24.2
|
||||
'@esbuild/freebsd-x64': 0.24.2
|
||||
'@esbuild/linux-arm': 0.24.2
|
||||
'@esbuild/linux-arm64': 0.24.2
|
||||
'@esbuild/linux-ia32': 0.24.2
|
||||
'@esbuild/linux-loong64': 0.24.2
|
||||
'@esbuild/linux-mips64el': 0.24.2
|
||||
'@esbuild/linux-ppc64': 0.24.2
|
||||
'@esbuild/linux-riscv64': 0.24.2
|
||||
'@esbuild/linux-s390x': 0.24.2
|
||||
'@esbuild/linux-x64': 0.24.2
|
||||
'@esbuild/netbsd-arm64': 0.24.2
|
||||
'@esbuild/netbsd-x64': 0.24.2
|
||||
'@esbuild/openbsd-arm64': 0.24.2
|
||||
'@esbuild/openbsd-x64': 0.24.2
|
||||
'@esbuild/sunos-x64': 0.24.2
|
||||
'@esbuild/win32-arm64': 0.24.2
|
||||
'@esbuild/win32-ia32': 0.24.2
|
||||
'@esbuild/win32-x64': 0.24.2
|
||||
|
||||
esbuild@0.25.12:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.12
|
||||
|
||||
Loading…
Reference in New Issue
Block a user