Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ jobs:
packages/ui/components/PlanHeaderMenu.mobile.test.tsx
packages/review-editor/components/ReviewHeaderMenu.mobile.test.tsx
packages/editor/App.editModeShortcut.test.tsx
packages/ui/components/blocks/AlertBlock.title.test.tsx
packages/ui/components/blocks/AlertBlock.seam.test.tsx

opencode-v2:
name: OpenCode 2 installed package
Expand Down
6 changes: 3 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/ui/HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -678,8 +678,8 @@ Additive only, but required: `@plannotator/ui` 0.32.0 imports the new `@plannota

## Publishing & versioning

- The current pair is `@plannotator/ui` `0.38.1` on `@plannotator/core` `0.25.2`. **Do not consume ui 0.38.0**: it imports `@plannotator/core/token-hover` (the hover-card trigger settings, #1462) but pins core 0.25.1, which never exported that subpath, so it fails to compile in any consumer; 0.38.1 is the same UI pinning core 0.25.2, which publishes `./token-hover`, the rotated `guide-viewer-manifest` pin, and the `config-types` hover fields (core 0.25.2 is the first core publish since 0.25.1 even though those changes landed over several releases; the package smoke now diffs the UI's core imports against the registry so an unpublished core subpath fails preflight instead of the consumer). UI 0.38.1 also aligns `@codemirror/state` to `^6.7.2` beside `@codemirror/view ^6.43.10`, so a consumer can no longer resolve two state copies. UI 0.38.0 also renders a GitHub alert's bold-only first body line as its title on the icon row (an emoji on that line becomes the icon; `<!-- icon: name -->` is stripped and resolved through the new `alertIconRenderer` seam, null by default; grammar in `utils/alertTitle`, importable by a host editor so it writes the bytes the reader parses; a fenced code block inside an alert body still renders as text, deferred because nesting a `CodeBlock` inside a block interacts with the positional annotation anchors and needs its own design). UI 0.38.0 carries the whole unified decision-control stack: the internal primitives (`DecisionControl`, `utils/decisionSpec`, `hooks/useDismissablePopover` — not host-supported surface, see the unsupported list; `useDismissablePopover` also replaced the hand-rolled dismissal inside `ActionMenu`/`ApproveDropdown`, both likewise unsupported) plus one blessed-barrel addition: `decisionControlShortcuts` on `@plannotator/ui/shortcuts` (pure scope data, fetch-free, same contract as the other scopes). The removal of `ToolbarButtons`' platform-mode `muted` prop is internal — `ToolbarButtons` is not host-supported surface. UI 0.37.0 added the Viewer-owned document-header seam (a new public API, hence the minor bump; 0.36.1 was reserved for it but never published) while retaining the `hideQuickLabel` and `StickyHeaderLane` seams from the 0.35.x and 0.36.0 releases; core 0.25.1 publishes the `annotation-threads` subpath already used by `AnnotationPanel` and `utils/parser`, and UI pins that corrected core exactly.
- Recent pairs, for the consumer's install matrix: ui 0.32.0 on core 0.25.0 (lockstep, `html-anchor`), ui 0.33.0 and ui 0.34.0 on core 0.25.0 (ui only), and ui 0.35.2, ui 0.36.0, and ui 0.37.0 on core 0.25.1 (0.36.1 was never published), and ui 0.38.1 on core 0.25.2. Do not consume ui 0.35.0 externally because its published manifest contains `workspace:*`; do not consume ui 0.35.1 because its exact core 0.25.0 dependency lacks the `annotation-threads` export. Do not consume ui 0.38.0 because its exact core 0.25.1 dependency lacks the `token-hover` export.
- The current pair is `@plannotator/ui` `0.38.2` on `@plannotator/core` `0.25.2`. UI 0.38.2 keeps the type word in a titled alert's accessible name through a visually hidden `sr-only` span before the title instead of an `aria-label` on the title row (naming a generic `div` is prohibited by ARIA and WebKit drops it, so VoiceOver on Safari read only the bold title in 0.38.1), and loosens the React peer back to `^19.2.3` (0.38.1 declared `^19.2.8` only because the dependency batch moved it; nothing in the package needs a newer API). **Do not consume ui 0.38.0**: it imports `@plannotator/core/token-hover` (the hover-card trigger settings, #1462) but pins core 0.25.1, which never exported that subpath, so it fails to compile in any consumer; 0.38.1 is the same UI pinning core 0.25.2, which publishes `./token-hover`, the rotated `guide-viewer-manifest` pin, and the `config-types` hover fields (core 0.25.2 is the first core publish since 0.25.1 even though those changes landed over several releases; the package smoke now diffs the UI's core imports against the registry so an unpublished core subpath fails preflight instead of the consumer). UI 0.38.1 also aligns `@codemirror/state` to `^6.7.2` beside `@codemirror/view ^6.43.10`, so a consumer can no longer resolve two state copies. UI 0.38.0 also renders a GitHub alert's bold-only first body line as its title on the icon row (an emoji on that line becomes the icon; `<!-- icon: name -->` is stripped and resolved through the new `alertIconRenderer` seam, null by default; grammar in `utils/alertTitle`, importable by a host editor so it writes the bytes the reader parses; a fenced code block inside an alert body still renders as text, deferred because nesting a `CodeBlock` inside a block interacts with the positional annotation anchors and needs its own design). UI 0.38.0 carries the whole unified decision-control stack: the internal primitives (`DecisionControl`, `utils/decisionSpec`, `hooks/useDismissablePopover` — not host-supported surface, see the unsupported list; `useDismissablePopover` also replaced the hand-rolled dismissal inside `ActionMenu`/`ApproveDropdown`, both likewise unsupported) plus one blessed-barrel addition: `decisionControlShortcuts` on `@plannotator/ui/shortcuts` (pure scope data, fetch-free, same contract as the other scopes). The removal of `ToolbarButtons`' platform-mode `muted` prop is internal — `ToolbarButtons` is not host-supported surface. UI 0.37.0 added the Viewer-owned document-header seam (a new public API, hence the minor bump; 0.36.1 was reserved for it but never published) while retaining the `hideQuickLabel` and `StickyHeaderLane` seams from the 0.35.x and 0.36.0 releases; core 0.25.1 publishes the `annotation-threads` subpath already used by `AnnotationPanel` and `utils/parser`, and UI pins that corrected core exactly.
- Recent pairs, for the consumer's install matrix: ui 0.32.0 on core 0.25.0 (lockstep, `html-anchor`), ui 0.33.0 and ui 0.34.0 on core 0.25.0 (ui only), and ui 0.35.2, ui 0.36.0, and ui 0.37.0 on core 0.25.1 (0.36.1 was never published), and ui 0.38.1 and ui 0.38.2 on core 0.25.2. Do not consume ui 0.35.0 externally because its published manifest contains `workspace:*`; do not consume ui 0.35.1 because its exact core 0.25.0 dependency lacks the `annotation-threads` export. Do not consume ui 0.38.0 because its exact core 0.25.1 dependency lacks the `token-hover` export.
- When both packages change, **publish `core` first**: ui 0.32.0 imports the `@plannotator/core/html-anchor` subpath, which no earlier published core (0.24.0 and before) has, just as ui 0.29.0 needed core 0.23.0 for `@plannotator/core/annotatable`. Bump core, update UI's exact core dependency to the same new version, and run `bun install` so `bun.lock` records the new workspace versions before packing either package.
- The HTML annotation seams also changed the guides.show viewer **stylesheet** (five utility rules from `HtmlSurfaceControls`; the viewer JS is unchanged), so `packages/core/guide-viewer-manifest.ts` now pins a CSS hash that exists on guides.show only after the deploy workflow has published this build's `/v1/` assets. A guide exported from this build before that deploy would pin a stylesheet the host does not serve yet: **deploy guides.show before any release that ships this manifest.**
- UI declares the already published core version exactly in its source manifest. Do not replace it with `workspace:*`: direct publication can preserve that protocol and make the package impossible to install outside this repository. Bun links the local core workspace whenever its version matches the exact dependency. Before publishing, run `bun run --cwd packages/ui smoke:package`; it checks the source and packed manifests, required tarball subpaths, local Bun linking, and a real pnpm install in an external temporary consumer. When both packages change, publish **`core` first, then `ui`**.
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ npm install @plannotator/ui @plannotator/core
- `@plannotator/core` — pure utils + types, zero deps, browser-safe (CI enforces no `node:` imports). Published.
- `@plannotator/ui` — React components/hooks + theme + `configure()`. Depends on an exact published `@plannotator/core` version. Published.
- `@plannotator/shared`, `@plannotator/ai` — stay private to the monorepo; `shared` re-exports `core`'s modules via shims so Plannotator's internals are untouched.
- Currently `@plannotator/ui` 0.38.1 depends exactly on `@plannotator/core` 0.25.2. `core` is bumped only when something under `packages/core` changes, so `ui` can advance alone. Keep the published core version exact in `packages/ui/package.json`; do not use a `workspace:` protocol there, because a directly published manifest must remain installable outside this monorepo. Bun still links the matching local workspace during development. When both packages change, publish `core` first, then build and publish the UI tarball. See HANDOFF.md "Publishing & versioning" for the verification command.
- Currently `@plannotator/ui` 0.38.2 depends exactly on `@plannotator/core` 0.25.2. `core` is bumped only when something under `packages/core` changes, so `ui` can advance alone. Keep the published core version exact in `packages/ui/package.json`; do not use a `workspace:` protocol there, because a directly published manifest must remain installable outside this monorepo. Bun still links the matching local workspace during development. When both packages change, publish `core` first, then build and publish the UI tarball. See HANDOFF.md "Publishing & versioning" for the verification command.

## The one rule

Expand Down
2 changes: 1 addition & 1 deletion packages/ui/components/blocks/AlertBlock.seam.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('AlertIconRenderer seam', () => {
const row = await render(TITLED);
expect(row.querySelector('svg')).not.toBeNull();
expect(row.querySelector('[data-host-icon]')).toBeNull();
expect(row.textContent).toBe('Browser quirks');
expect(row.textContent).toBe('Tip: Browser quirks');
});

test.skipIf(!hasDom)('a registered renderer receives the name and its node takes the icon slot', async () => {
Expand Down
20 changes: 12 additions & 8 deletions packages/ui/components/blocks/AlertBlock.title.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,15 @@ async function render(kind: 'note' | 'tip' | 'warning' | 'caution' | 'important'
}

const titleRow = (el: HTMLElement) => el.querySelector<HTMLElement>('.alert-title')!;
const srOnly = (el: HTMLElement) => el.querySelector<HTMLElement>('.alert-title .sr-only')?.textContent ?? null;
const paragraphs = (el: HTMLElement) => Array.from(el.querySelectorAll('p')).map((p) => p.textContent);

describe('AlertBlock title line', () => {
test.skipIf(!hasDom)('control: a plain body keeps the type word, the type icon, no indent wrapper', async () => {
const el = await render('caution', 'ML auto-detection has no protobuf model.');
expect(titleRow(el).textContent).toBe('Caution');
expect(titleRow(el).querySelector('svg')).not.toBeNull();
expect(titleRow(el).getAttribute('aria-label')).toBeNull();
expect(srOnly(el)).toBeNull();
expect(el.querySelector('.alert-body')).toBeNull();
expect(paragraphs(el)).toEqual(['ML auto-detection has no protobuf model.']);
});
Expand All @@ -53,9 +54,12 @@ describe('AlertBlock title line', () => {

test.skipIf(!hasDom)('B: a bold-only first line becomes the title in place of the type word', async () => {
const el = await render('important', '**Read before you deploy**\n\nThe env still names D1.');
expect(titleRow(el).textContent).toBe('Read before you deploy');
// Visible text is the title alone; the accessible name keeps the type word
// through a visually hidden span (an aria-label on the generic div is
// prohibited by ARIA and dropped by WebKit, so VoiceOver lost the type word).
expect(titleRow(el).textContent).toBe('Important: Read before you deploy');
expect(srOnly(el)).toBe('Important: ');
expect(titleRow(el).querySelector('svg')).not.toBeNull(); // type icon stays
expect(titleRow(el).getAttribute('aria-label')).toBe('Important: Read before you deploy');
expect(paragraphs(el)).toEqual(['The env still names D1.']); // no bold paragraph any more
expect(el.querySelector('.alert-body')?.className).toContain('pl-6');
});
Expand All @@ -66,8 +70,8 @@ describe('AlertBlock title line', () => {
expect(row.querySelector('svg')).toBeNull();
expect(row.querySelector('.alert-emoji')?.textContent).toBe('🧭');
expect(row.querySelector('.alert-emoji')?.getAttribute('aria-hidden')).toBe('true');
expect(row.textContent).toBe('🧭Browser quirks');
expect(row.getAttribute('aria-label')).toBe('Tip: Browser quirks');
expect(row.textContent).toBe('🧭Tip: Browser quirks');
expect(srOnly(el)).toBe('Tip: ');
expect(paragraphs(el)).toEqual(['There are caret bugs.']);
});

Expand All @@ -76,21 +80,21 @@ describe('AlertBlock title line', () => {
expect(el.textContent).not.toContain('icon:');
expect(el.textContent).not.toContain('<!--');
expect(titleRow(el).querySelector('svg')).not.toBeNull();
expect(titleRow(el).textContent).toBe('Browser quirks');
expect(titleRow(el).textContent).toBe('Tip: Browser quirks');
expect(paragraphs(el)).toEqual(['The sentence.']);
});

test.skipIf(!hasDom)('E: an emoji alone keeps the type word as the title', async () => {
const el = await render('warning', '🚧\n\ntot.page is bound to the old D1.');
expect(titleRow(el).querySelector('.alert-emoji')?.textContent).toBe('🚧');
expect(titleRow(el).textContent).toBe('🚧Warning');
expect(titleRow(el).getAttribute('aria-label')).toBeNull();
expect(srOnly(el)).toBeNull();
expect(paragraphs(el)).toEqual(['tot.page is bound to the old D1.']);
});

test.skipIf(!hasDom)('a title line with no body renders no body wrapper', async () => {
const el = await render('note', '**Just a title**');
expect(titleRow(el).textContent).toBe('Just a title');
expect(titleRow(el).textContent).toBe('Note: Just a title');
expect(el.querySelector('.alert-body')).toBeNull();
expect(paragraphs(el)).toEqual([]);
});
Expand Down
20 changes: 13 additions & 7 deletions packages/ui/components/blocks/AlertBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,11 @@ export const AlertBlock: React.FC<AlertBlockProps> = ({
// takes the icon slot. See utils/alertTitle for the exact grammar. A body
// with no such line renders exactly as before.
const titleLine = parseAlertTitleLine(body);
const hostIcon = titleLine?.icon && alertIconRenderer ? alertIconRenderer(titleLine.icon) : null;
// The emoji wins outright: the host renderer is consulted only for a title
// line that carries an icon comment and no emoji (the seam contract).
const icon = titleLine?.emoji
? <span aria-hidden="true" className="alert-emoji text-base leading-none">{titleLine.emoji}</span>
: hostIcon ?? <Icon kind={kind} />;
: (titleLine?.icon && alertIconRenderer ? alertIconRenderer(titleLine.icon) : null) ?? <Icon kind={kind} />;

return (
<div
Expand All @@ -82,14 +83,19 @@ export const AlertBlock: React.FC<AlertBlockProps> = ({
data-block-type="alert"
data-alert-kind={kind}
>
<div
className="alert-title flex items-center gap-2 font-semibold mb-1"
aria-label={titleLine?.title ? `${TITLE[kind]}: ${titleLine.title}` : undefined}
>
<div className="alert-title flex items-center gap-2 font-semibold mb-1">
{icon}
<span>
{titleLine?.title
? <InlineMarkdown text={titleLine.title} {...proseProps} />
? (
<>
{/* The type word stays part of the accessible name. A visually
hidden span is read by every engine; an aria-label on this
generic div is prohibited by ARIA and dropped by WebKit. */}
<span className="sr-only">{TITLE[kind]}: </span>
<InlineMarkdown text={titleLine.title} {...proseProps} />
</>
)
: TITLE[kind]}
</span>
</div>
Expand Down
6 changes: 3 additions & 3 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plannotator/ui",
"version": "0.38.1",
"version": "0.38.2",
"type": "module",
"exports": {
"./components/*": "./components/*.tsx",
Expand Down Expand Up @@ -93,8 +93,8 @@
"unique-username-generator": "^1.5.1"
},
"peerDependencies": {
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
Expand Down