@aiquants/markdown-explorer 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/LICENSE +21 -0
  3. package/README.md +606 -2
  4. package/dist/ExplorerSplit-D2plJRRe.cjs +1 -0
  5. package/dist/ExplorerSplit-DB37U7Kh.js +1362 -0
  6. package/dist/MultiLayout-DCPDu1-l.js +909 -0
  7. package/dist/MultiLayout-UyWxs6Xy.cjs +1 -0
  8. package/dist/TreeLayout-BoyFelK2.js +22 -0
  9. package/dist/TreeLayout-CLWbWjOc.cjs +1 -0
  10. package/dist/client/MarkdownExplorer.d.cts +30 -0
  11. package/dist/client/MarkdownExplorer.d.ts +30 -0
  12. package/dist/client/storage.d.cts +11 -0
  13. package/dist/client/storage.d.ts +11 -0
  14. package/dist/core/config.d.cts +77 -0
  15. package/dist/core/config.d.ts +77 -0
  16. package/dist/core/contracts.d.cts +155 -0
  17. package/dist/core/contracts.d.ts +155 -0
  18. package/dist/core/geometry.d.cts +51 -0
  19. package/dist/core/geometry.d.ts +51 -0
  20. package/dist/core/labels.d.cts +180 -0
  21. package/dist/core/labels.d.ts +180 -0
  22. package/dist/core/location.d.cts +82 -0
  23. package/dist/core/location.d.ts +82 -0
  24. package/dist/core/paths.d.cts +17 -0
  25. package/dist/core/paths.d.ts +17 -0
  26. package/dist/core/revalidation.d.cts +2 -0
  27. package/dist/core/revalidation.d.ts +2 -0
  28. package/dist/core/theme.d.cts +6 -0
  29. package/dist/core/theme.d.ts +6 -0
  30. package/dist/core/treeSettings.d.cts +48 -0
  31. package/dist/core/treeSettings.d.ts +48 -0
  32. package/dist/core/viewSettings.d.cts +15 -0
  33. package/dist/core/viewSettings.d.ts +15 -0
  34. package/dist/dataRoute-Bkfwfp1R.js +194 -0
  35. package/dist/dataRoute-CKbdwkQ7.cjs +1 -0
  36. package/dist/index-BfuAu2GV.js +2107 -0
  37. package/dist/index-Cx3-jSna.cjs +3 -0
  38. package/dist/index.cjs +1 -0
  39. package/dist/index.d.cts +12 -0
  40. package/dist/index.d.ts +12 -0
  41. package/dist/index.js +44 -0
  42. package/dist/location-Oo21lTyV.cjs +1 -0
  43. package/dist/location-fVl2R1ee.js +395 -0
  44. package/dist/sanitizeWorker.cjs +1 -0
  45. package/dist/sanitizeWorker.js +281 -0
  46. package/dist/server/anonymous.d.cts +10 -0
  47. package/dist/server/anonymous.d.ts +10 -0
  48. package/dist/server/createMarkdownExplorer.d.cts +12 -0
  49. package/dist/server/createMarkdownExplorer.d.ts +12 -0
  50. package/dist/server/errors.d.cts +26 -0
  51. package/dist/server/errors.d.ts +26 -0
  52. package/dist/server/fileSystemSource.d.cts +38 -0
  53. package/dist/server/fileSystemSource.d.ts +38 -0
  54. package/dist/server/ports.d.cts +107 -0
  55. package/dist/server/ports.d.ts +107 -0
  56. package/dist/server/sanitizeProtocol.d.cts +17 -0
  57. package/dist/server/sanitizeProtocol.d.ts +17 -0
  58. package/dist/server/sanitizer.d.cts +51 -0
  59. package/dist/server/sanitizer.d.ts +51 -0
  60. package/dist/server/workerPool.d.cts +42 -0
  61. package/dist/server/workerPool.d.ts +42 -0
  62. package/dist/server.cjs +1 -0
  63. package/dist/server.d.cts +7 -0
  64. package/dist/server.d.ts +7 -0
  65. package/dist/server.js +1253 -0
  66. package/dist/storage-Bkb2Axdi.cjs +1 -0
  67. package/dist/storage-DA4ItHzl.js +60 -0
  68. package/dist/storage.cjs +1 -0
  69. package/dist/storage.d.cts +1 -0
  70. package/dist/storage.d.ts +1 -0
  71. package/dist/storage.js +4 -0
  72. package/dist/styles/markdown-explorer.css +3 -0
  73. package/docs/behavior/dom-hooks.md +95 -0
  74. package/docs/behavior/layout.md +215 -0
  75. package/docs/behavior/localization.md +48 -0
  76. package/docs/behavior/security.md +138 -0
  77. package/docs/behavior/url-contract.md +135 -0
  78. package/package.json +141 -3
@@ -0,0 +1,215 @@
1
+ # Layout
2
+
3
+ This page is part of the host-facing contract of `@aiquants/markdown-explorer`; its summary is in the [README](../../README.md#layout). The proportions below are defined in the package's geometry module, and the stylesheet's tokens mirror its Fibonacci scales, control sizes and reading measure (a unit test compares them). The constants a host may need are exported from the package entry: `GOLDEN_RATIO`, `EXPLORER_DEFAULT_PERCENT`, `EXPLORER_MULTI_DEFAULT_PERCENT`, `EXPLORER_MAX_PERCENT`, `EXPLORER_MIN_REM` and `COLUMN_MIN_MEASURE`.
4
+
5
+ ## Sizing
6
+
7
+ The explorer fills its container's block size, so the container needs a definite block size: for example `height: 100dvh`, or a flex column of definite height whose item has `flex: 1` and `min-height: 0` (in a fresh React Router app, with `html` and `body` at `height: 100%`).
8
+ A container whose block size is auto or only a minimum (`html` and `body` at their default auto height included) does not size the explorer: the areas that fill the leftover height collapse to 0 px, or the explorer grows with its content, depending on the host's structure.
9
+ With `lockDocumentScroll` the explorer must fill a container of definite block size inside the viewport: while the page is locked it logs one `console.error` per mount — checked at mount and whenever the explorer's root or the window is resized — naming either the overflow (the explorer ends more than 1 px below the viewport) or the collapse (its root, the document region of the tree or single view, or the explorer's tree area is rendered but less than 1 px tall), and what to give the container.
10
+ Only rendered parts are measured: a part with no layout box (`display: none` on it or an ancestor — a closed sheet, an explorer panel the reader collapsed, an explorer the host hides) is hidden, not collapsed, and is never reported.
11
+
12
+ ## The split
13
+
14
+ The tree and multi-panel views split the frame into the explorer and the content with a resizable handle.
15
+
16
+ | Quantity | Value | Why |
17
+ | --- | --- | --- |
18
+ | Explorer, tree view | 100/φ³ ≈ 23.607 % | The document gets 1 − 1/φ³ = 2φ times the explorer (1 + 2φ = φ³) |
19
+ | Explorer, multi-panel view | 100/φ⁴ ≈ 14.590 % | One golden step further back: the panels are the subject |
20
+ | Explorer maximum | 100/φ² ≈ 38.197 % | The golden section; the content keeps at least 1/φ |
21
+ | Explorer minimum | 13rem: 13 times the root font size (208 px at the browsers' default 16 px root) | Fibonacci 13; it follows the reader's font size |
22
+ | Sheet threshold | 13rem × φ² (≈ 544.6 px at a 16 px root) | Below it the minimum explorer would pass the golden section, so the explorer leaves the split and opens as a sheet |
23
+ | Handle | A band as wide as the minimum pointer target: 24 px under a fine pointer, 44 px under a coarse one (`(pointer: coarse)`); a 5 × 34 px grip around a 2 × 21 px bar | WCAG 2.2 target size (minimum for a fine pointer, enhanced for a coarse one). The band itself is the target: a wider invisible hit area would cover the tree's scroll bar beside it. The grip is Fibonacci |
24
+
25
+ - The root font size is measured in the browser (through a 1rem probe, so a text-only zoom is followed as well); on the server and in the first paint it is assumed to be 16 px.
26
+ - The server cannot know the pointer, so the server render and the hydration render assume a fine one; under a coarse pointer the handle widens right after hydration.
27
+ - At 1920 px, with a 16 px root and a fine pointer, the 24 px handle leaves 1896 px to the explorer and the content. The tree view gives the explorer 1896/φ³ ≈ 447.6 px. The multi-panel view gives it 1896/φ⁴ ≈ 276.6 px and the stage ≈ 1619.4 px; less 13 px of padding at each side, the column track is ≈ 1593.4 px,
28
+ so the stage shows ⌊(1593.4 + 13) / (384 + 13)⌋ = 4 columns of (1593.4 − 3 × 13) / 4 ≈ 388.6 px, each wider than the 40ch measure (≈ 384 px with a common UI font, whose `0` is about 0.6em wide). At 1280 px the multi-panel explorer stays at its 13rem floor (1256/φ⁴ ≈ 183.3 px is less than 208 px) and the stage shows two columns.
29
+ - The reader's width is remembered per view, as a percentage of the split, in the display-settings cookie (see [URL contract](url-contract.md#display-settings)). It is written only after the reader moves the handle — at the end of a drag, or when the arrow keys are released (or the handle loses focus) after they moved it, 233 ms later in both cases — never because the window was resized, so a narrow window does not overwrite the width chosen on a wide one.
30
+ - The handle is a focusable `separator` (named by `resizeExplorer`) from `@aiquants/resize-panels`: Left and Right move it by 10 px (50 px with Shift), pressing past the minimum collapses the explorer, and the opposite arrow opens it again at its previous width. Only keys that moved the handle by the time they were released count as the reader's choice.
31
+
32
+ ## The sheet on narrow frames
33
+
34
+ When the frame is narrower than 13rem × φ² — a phone, or a narrow window — the explorer leaves the split:
35
+
36
+ - The content takes the whole width under a bar with a visible toggle (named by `showExplorer`, a 44 px target under every pointer, with `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`). It stays mounted across the change, so the document keeps its scroll position and focus.
37
+ - The toggle opens the explorer as a modal sheet over the content: a native `dialog` named by `explorerRegion`, at most 21rem wide (13rem × φ ≈ 21rem) and leaving 3.4375rem of the content visible beside it (55 px at 16 px text, 110 px at 200 %). Focus moves to its close button (named by `hideExplorer`), and the rest of the page is inert while it is open — the explorer's root live region included, so the sheet holds a live region of its own that speaks the explorer's announcements while it is open (see [Announcements](#announcements)).
38
+ - Escape, the close button, a press on the backdrop that also started there (a press that starts inside the sheet and is released over the backdrop does not close it) and any navigation that keeps the tree's source (choosing a document, opening a selection) close it, and focus returns to the toggle. Switching the source keeps it open.
39
+ So does a pick while the reader gathers documents (Selection set to Multiple in the tree view): a click, Enter or Space adds the document to the selection tray (or takes it out) and shows it behind the sheet, and the sheet stays open with focus on the row, so several documents can be gathered with the tray in view; opening the selection as panels leaves the tree view, and Escape, the close button or the backdrop close the sheet.
40
+ - The mode follows the frame: widening it past the threshold brings the split back (and the reader's remembered width with it). The explorer is rebuilt in its new place, but what the reader set up there survives the move in both directions: the documents gathered in the selection tray, and the tree settings, which the explorer keeps in memory (browser storage only saves them), so the rebuilt explorer draws them from its first frame — never the defaults in between — even when the browser cannot write its storage.
41
+ - Focus on the explorer's side (the tree, the handle, the toggle or the open sheet) is handed over when the mode changes: to the sheet's toggle when the frame narrows past the threshold; when it widens past it, once the rebuilt tree has drawn its rows, to the tree row the reader was on (found by its path in the rebuilt tree), else the tree's tab-stop row, else the tree's stand-in stop (see [The tree](#the-tree)), which brings that row into view and hands it the focus, else the handle. The row is remembered from the reader's own focus moves only, so a round trip (split, sheet, split) without touching anything returns to the same row, while a later focus move by the reader (opening the sheet, say) replaces it. A row the rebuilt tree has not rendered (far down a long tree) falls back to the tab-stop row. The row that takes the focus becomes the tree's only Tab stop, so Shift+Tab from it leaves the tree at once. Focus elsewhere stays where it is.
42
+
43
+ ## The tree
44
+
45
+ - **One Tab stop.** The tree is one stop in the Tab order: the row the reader last focused — with the keyboard, a press or a script — while it holds the focus or as long as the shown document has not changed since, else the shown document's row, else the first row (`@aiquants/directory-tree`'s roving tab index). Shift+Tab from any row leaves the tree, and Tab comes back to the same row. While that row is outside the tree's render window (far down a long tree, or at a large text size), the tree puts a stand-in stop in its place, outside the `tree` role (`data-directory-tree-tab-stop`): Tab lands on it, and it brings the row into view and hands it the focus, so Tab still enters the tree on that row.
46
+ - **Rows only.** The `tree` role holds the rows and nothing else; the scroll bar and its buttons sit outside it and are no Tab stops (see [Rhythm](#rhythm)).
47
+ - **The shown document's row.** In the tree view, with Selection set to Single or Multiple, the shown document's row carries `aria-current="page"` (none with Selection Off, and none in the multi view, whose open panels are the selected rows). While the reader gathers documents (Multiple), the gathered rows carry `aria-selected="true"` and the current row moves with the shown document alone.
48
+ On load, and whenever the shown document changes while the focus is outside the tree (a document link, Back or Forward), its row is brought into the tree's view by the least distance, without moving the focus, and kept there whenever the tree's area changes size (the explorer's layout settling, the tray growing or shrinking, the sheet opening on a tree it hid) until the reader presses, wheels, touches, types or moves the focus inside the tree. A row the tree does not list (inside a collapsed folder) is left alone, and a document opened from the tree does not move the tree: the pressed row stays where it was, also when it was only partly visible.
49
+ - **Loading follows what is shown open.** A lazy folder's listing is asked for while some tree shows the folder open, whatever opened it, at most 4 at a time per explorer. Bulk work — Expand all, a remembered expansion restored on load, a recursive double click — uses at most 3 of them, so a folder the reader opens (a click, Enter or ArrowRight) or retries goes ahead of every queued bulk listing and starts at once on the slot kept for the reader, unless another folder the reader opened is using it (a listing in flight is never interrupted).
50
+ Each source has its own queues and the sources are served in turn, so one source's bulk work never holds back another's.
51
+ Queues follow the order on screen (the tree's sort order), not the source's listing order.
52
+ A listing no tree wants any more is cancelled when the current task ends — a queued one is never sent, one in flight is aborted and frees its slot at once — and the folder goes back to not loaded: no failure, no Try again, no announcement and no loading pulse (`data-aqmx-children="loading"` is left only on folders some tree still wants).
53
+ That happens when the reader collapses the folder or one of its ancestors, uses Collapse all (at the click), switches the source, closes the sheet or collapses the explorer panel, when the explorer or the pane unmounts, and when a refresh, a seed or a reader change replaces the tree (a refresh lists the folders still shown open again). Opening the folder again asks for it again. Nothing is listed while the explorer is mounted but hidden (a closed sheet, a collapsed explorer panel), not even on its first render.
54
+ - **Expand all.** Each press opens every folder and keeps opening the folders inside as their listings land, until the tree is fully open or the press's budget (`expandAll` in the configuration) runs out: at most `maxFolders` listings (500 by default), and a folder whose contents are not known yet only down to `maxDepth` levels (16 by default, the top level being 1). Folders whose contents are known — loaded, or listed by the source up front — always open, as do folders loading or failed.
55
+ A press walks the tree level by level, siblings in the tree's order, so its bulk listings run roughly level by level (a listing that lands early can let a deeper level start while a slower, shallower one is still in flight).
56
+ A folder the reader closes during a press stays closed, and the press does not walk into it. The folders a press opened join the tree's remembered expansion (per source, in browser storage), so a reload lists them again as bulk work, without opening anything beyond them and without an announcement. Hiding the explorer pauses a running press, which continues once the explorer is shown again;
57
+ Collapse all, a source switch and a tree with no rows to show (failed, empty or cleared) end it silently. The tree is not dimmed while a press runs.
58
+ - **Progress line.** While the source has listings queued or in flight, a line floats at the bottom of the tree area, over the rows and clear of the scroll buttons and the scroll bar, without moving the tree (`markdown-explorer-tree-progress`): "Loading 12 folders…", or "Opening all folders (40 opened, 12 loading)…" during a press. After a press stopped at a limit it shows the loading count alone while listings remain and nothing once none does; it never shows the stop sentence, which would cover the last rows until the next press (see the stop notice below).
59
+ It counts the source's listings whichever rows are drawn, so a reader scrolled away from the loading folders still sees them counted. It appears after `--aqmx-delay-progress` (233 ms), so a single fast listing never flashes it, and it stays the same line while a remembered expansion loads level by level (a listing that lands lets the folders inside it be asked for within the same task), so it does not blink between levels. It is not a live region (see [Announcements](#announcements)).
60
+ - **Stop notice.** A press that stopped at a limit leaves its stop sentence — the text announced at the stop — in a notice in the explorer's body, just above the tree area, below the header, a failed refresh's alert and the host's notices (`markdown-explorer-expand-all-stop`, in the notices' info colours). It sits in the pane's flow, so the tree moves down by its height when the press stops and back when the notice clears, and it never covers a row.
61
+ It stays until the next press, Collapse all, the pane having no rows to show (a failed, empty or cleared tree), or unmount (a source or view switch). It is not a live region, so the stop is spoken once, and it takes no focus.
62
+ - **Loop rows.** A folder met again among its own ancestors — a shortcut inside its own target, or two folders holding shortcuts to each other — is shown as a loop row named "Name (already open above)" (`loopFolderName`), in italics, instead of being listed again, so nothing recurses forever.
63
+ It never expands (its glyph stays closed), it is never selected, remembered or listed, a double click on a folder above it counts the folders without it (when all of those are open, the double click collapses them), and activating it (a click, Enter or ArrowRight) scrolls to the folder it loops back to, which is open above it, and moves the focus there. It carries `data-aqmx-loop` and no `data-aqmx-path`.
64
+
65
+ ## The multi-panel stage
66
+
67
+ Documents in the multi-panel view sit in `multiView.columns` logical columns (4 by default).
68
+
69
+ - **Visible columns.** A column is never narrower than the reading measure `40ch` (Bringhurst's lower bound for multi-column text) at the root font size. With A the inline size of the column track, g the column gap and m the measure in pixels, the stage shows n = clamp(⌊(A + g) / (m + g)⌋, 1, N) columns. Text is never scaled to fit: when the stage narrows, fewer columns are shown.
70
+ - **Hysteresis.** A width hovering at a threshold does not flip the count: it grows only once one more column fits with a margin of g/φ (A ≥ (n + 1)m + ng + g/φ) and shrinks only once the current count no longer fits (A < nm + (n − 1)g). The count is frozen while a panel is maximized and while the reader drags the explorer's handle.
71
+ - **Projection.** When fewer columns are visible than exist, visible column i shows the logical columns ⌊iN/n⌋ to ⌊(i + 1)N/n⌋ − 1, stacked in order. Reading order is kept, and the original arrangement returns unchanged as soon as the width allows.
72
+ - **Rearranging while projected.** When the reader drags or moves panels while fewer columns are visible, the new arrangement is mapped back onto the logical columns by dynamic programming: the cut points inside each visible column are chosen so that the largest number of panels keeps its previous logical column (the earliest cuts win a tie). Rearranging is idempotent: applying the arrangement that is already shown changes nothing.
73
+ - **Moving without a drag.** Each panel's header (drawn by `@aiquants/drag-drop-panels`) has four move buttons — Move to the previous column, Move to the next column, Move up and Move down (`panelMovePreviousColumn`, `panelMoveNextColumn`, `panelMoveUp`, `panelMoveDown`); a move that is impossible from where the panel sits is `aria-disabled` and stays focusable. The header's buttons show while the panel is hovered or one of them holds the focus, and always on a device that cannot hover (touch), so panels move with them on a touch screen as well.
74
+ The same moves are keyboard shortcuts while the focus is in the panel: Alt + Shift + ← / → / ↑ / ↓ (Alt + ← / → alone stay the browser's Back / Forward). A held chord moves once, and the shortcuts are ignored in text inputs and editable content, with Ctrl or ⌘, while a panel is maximized and during a drag. A move to another visible column keeps the panel's position among that column's shown panels (its end when the column has fewer); up and down swap the panel with the neighbouring shown panel, and hidden panels keep their place.
75
+ The layout computes a move on the visible columns the stage shows and reports it as their next arrangement; the stage maps it back like a drop, which refreshes the column memory, and the layout then shows exactly the arrangement it asked for, so it announces the move from its own live region (`panelMoved`). The focus stays where it was: when a move to another column rebuilt the panel, the element that held the focus — the move button, or the link or frame the shortcut was pressed in — takes it back without scrolling, as after any rebuild (see Reading position below), and the layout then brings it into view by the least distance. A shortcut the panel cannot follow (Alt + Shift + ← in the first column, for example) changes nothing and is announced as unavailable (`panelMoveUnavailable`). No moves while a panel is maximized.
76
+ - **Touch and pen drag.** A long-press on a panel's header — its title, or anywhere outside its buttons — lifts the panel once the finger or pen has rested there for 500 ms within 10 px (`@aiquants/drag-drop-panels`' `DEFAULT_TOUCH_DRAG_CONFIG`); until then nothing is held back, so a swipe that starts on a header scrolls the stage and a tap stays a tap, and no drag state shows. A long-press on a header button starts nothing (the press belongs to the button), and a finger on a panel's body scrolls it as on any page.
77
+ Once lifted, the stage does not scroll under the finger, a copy of the panel follows it, the drop placeholder nearest to the finger in the column under it is the target, and the stage scrolls while the finger rests near its top or bottom edge (the target follows as the stage moves). Releasing over a target moves the panel exactly like a mouse drop on that placeholder — mapped back like any rearrangement, written to the URL and the column memory — and the layout announces the move (`panelMoved`). The browser cancelling the touch, Escape, the finger leaving the layout, or a release where the panel would stay where it is changes nothing. The layout announces the lift and every cancel too (`panelDragStarted`, `panelDragCancelled`). The stage wires nothing for this: the layout runs the drag and asks for the drop through the same `onColumnPanelsChange` as the move buttons.
78
+ - **Panel height.** A panel is as tall as its document, between stage/φ³ and stage/φ: no single panel takes more than the golden share of the stage, none shrinks below the next golden step, and a longer document scrolls inside its panel. A panel alone in its column keeps the same cap: the band below it is where a dragged panel is dropped after it and where a newly opened panel appears, so neither resizes the panel being read; maximize gives one document the whole viewport.
79
+ The bounds hold the whole panel, header included, so the body's floor is added to the header: the panel's scroll area keeps at least three lines of text at the reader's root font size (each `1rem` × φ, never less than the viewer's own line at the default size), and where stage/φ minus the header — which wraps onto more lines at narrow widths and large text sizes — would leave less, the panel's largest height is its header plus that floor, even beyond stage/φ (a phone width with 200 % text, a 400 % zoom).
80
+ The header is measured per panel, so the bounds follow it as it wraps again. The smallest height needs no floor: a panel at stage/φ³ shows a document shorter than that whole.
81
+ - **Document refresh in a panel.** `@aiquants/drag-drop-panels` draws the panel's header and takes no content of the explorer's into it, so a panel's Refresh document (see [Busy and unavailable controls](#busy-and-unavailable-controls)) sits in a thin bar that is the first row of the panel's body, inside its scroll area, at the bar's inline end.
82
+ The bar is pinned to the top of the scroll area (sticky, on the panel's surface) while the document scrolls under it, so the reader reaches the button from anywhere in the document — with a pointer or with Tab — without the panel scrolling, and the refresh keeps the place they were reading. The frame measures the bar, a failure notice it shows included, and the document's elements add its block size to their block-start scroll margin, so a fragment's target and a focused element land below the bar, never under it (a scroll padding on the area would count the bar's own button as covered, and focusing it would scroll the panel); the reading position a rebuilt panel brings back is taken below the bar. It keeps a focus ring's extent around the button, so the button's ring is drawn whole inside the scroll area.
83
+ The panel's height bounds count the bar with the header, so the body's floor of three lines of text lies below it. A double-click on the button is two presses and never maximizes the panel; a double-click elsewhere on the body still does.
84
+ - **Stage spacing.** The stage keeps 21 px above and below its columns and 13 px at the sides, and 13 px between columns (Fibonacci, in rem: `--aqmx-space-7` and `--aqmx-space-6`), set through drag-drop-panels' neutral properties (`--aqdd-stage-padding-block`, `--aqdd-stage-padding-inline`, `--aqdd-column-gap`), which the stage binds to the explorer's tokens. The column plan reads the actual gap and padding, so it always matches what is drawn.
85
+ - **Reading position.** When the column count changes, every panel is rebuilt at its new width; the element that was at the top of a panel (and how far it was scrolled past) is brought back to the same place, so the reader keeps their line even though the text wraps differently. The keyboard focus survives the rebuild too: focus that was anywhere inside the panel (a link in its document, a button in its header) returns to the same element without scrolling (an element that held the focus only through a temporary `tabindex="-1"`, such as the first heading or the fragment target focused after a followed link, takes it back the same way), and when that element is gone (the rebuild drew something else in its place), to the panel's frame — never to the page body. Zoom in and Zoom out stay focusable when they cannot act (see [The document region](#the-document-region)). An image keeps its zoom across the rebuild: the panel remembers the zoom, with the size its levels are relative to, and the natural size the browser reported (forgotten when the panel moves to another document or closes), so the rebuilt viewer draws the same size and its zoom buttons take the focus back. The frame that receives the focus in the multi view's hand-offs (closing or hiding a panel, opening documents in panels, and the fallback above) is a `group` named after the panel's document, so a screen reader names the document; the document region inside stays the panel's only landmark. Rebuilding a panel requests nothing: its document is reused as it is. A panel that follows a link starts the new document at its top (or at the link's fragment), and brings that start into the stage's view with the panel when the stage had scrolled the panel's header out of it (see [The document region](#the-document-region)); a remembered position is only ever restored into the document it was taken in.
86
+ - **Panel headers.** A panel's header (drawn by `@aiquants/drag-drop-panels` and named in the explorer's language, see [Localization](localization.md)) holds the panel's title and, after it, the custom drag mode's badge and the buttons: the four moves, the drag-mode toggle, Hide (absent while the panel is maximized), Maximize or Restore, and Close. It keeps every button wholly inside the panel at any width and text size, a phone width with 200 % text included, and keeps the title readable: when the title would get less than the width of a few characters (6em) beside the badge and the buttons, they wrap to a second line of the header, at its end, and the title takes the whole first line; a title longer than its line is cut with an ellipsis.
87
+ The stage binds the header's colours to the explorer's tokens through drag-drop-panels' neutral properties, so they follow `theme` rather than a host's `dark` class: the panel's surface, border and title are `--aqmx-surface`, `--aqmx-border` and `--aqmx-text`; the icons are `--aqmx-text-muted` (`--aqmx-text` over `--aqmx-surface-sunken` on hover); the badge and the pressed custom-mode toggle are `--aqmx-warning-text` on `--aqmx-warning-surface` — at least 3:1 for the icons and 4.5:1 for the title and the badge in both colour schemes. The bar of hidden panels, an empty column's notice and a hidden panel's stand-in during a drag take the same surface, border and text. The bar's restore buttons are `--aqmx-surface` on `--aqmx-accent` (`--aqmx-info-text` while hovered), and the drop placeholders of a drag draw their text, icon and dashed border in `--aqmx-accent` on `--aqmx-surface`, the placeholder a drop would land on on `--aqmx-accent-surface` — at least 4.5:1 for that text and 3:1 for the dashed border against the stage in both colour schemes. The whole header, on one line or two, stays the panel's drag handle (a long-press outside its buttons for touch and pen), and a double-click on it outside its buttons toggles maximize.
88
+ - **Focus and announcements.** Closing or hiding the panel that holds the keyboard focus moves the focus to the next panel in reading order (column by column, top to bottom), else to the previous one, else to the stage; when the focus is elsewhere (in the tree, for example), it stays there. Showing a panel from the list of hidden panels, or following a link to a document open in another panel, moves the focus to that panel; while a panel is maximized, such a link maximizes the target panel. Opening, closing, hiding, showing, maximizing and restoring a panel are announced (`panelOpened`, `panelClosed`, `panelHidden`, `panelShown`, `panelMaximized`, `panelRestored`). A maximize or a restore is announced once the layout has changed, whichever way it came — the header's button, Escape, a double-click on the body, or a link followed while a panel is maximized (after `panelAlreadyOpen`); a maximize that changes nothing is not. Every restore brings the restored panel into the stage's view by the least distance: a panel maximized where it sat stays put, while one maximized from far down the stage — through a URL's `maximized` (a reload or a shared link) or by a link followed while another panel was maximized — scrolls into view, and a focus that survived inside it (the header's restore button, a link in the document) is then brought into view inside the panel's scroll area by the least distance — the area keeps its scroll offset while the text wraps again in the narrower column, so a link the maximized panel showed would otherwise land below it. A restore that removes the element holding the focus — the viewer's toolbar, its table of contents and their links exist only while the panel is maximized — then gives the focus to the restored panel's frame, without scrolling, instead of dropping it to the page body. A panel opened or closed from the explorer is announced once the URL it wrote has rendered: in the sheet that navigation closes the sheet, so the root's region speaks it instead of the closing sheet's; when several toggles render as one navigation (a fast double-click), each is still announced, in order. Picking a hidden panel's document shows the panel without changing the URL (hidden panels live in browser storage), so the view closes the sheet itself and the root's region speaks `panelShown` once it has closed: every pick that changes the stage closes the sheet. A panel that a pick opened or showed is then brought into the stage's view by the least distance, without moving the focus (a pick in the split leaves the focus on the tree row, so the reader keeps picking): in a crowded stage it would otherwise sit below the fold, where the pick changes nothing the reader sees and a second click on the same row closes a panel they never saw. A panel is always brought into view whole — its header with the title and buttons, and its body — and kept there, with the focus a restore left in it, while the stage settles around it (its own document arriving, an image loading in a panel above it once the scroll brought that panel near, the restored document's text wrapping again), until the reader's next key, press, wheel or touch anywhere on the page, or until the stage, the panel's scroll area or the page is scrolled to a place these reveals did not leave it at (assistive technology's browse mode, the browser's find bar or a script moved it).
89
+ - **Maximized panels.** A maximized panel covers the stage and the explorer, and everything of the explorer it covers is inert until it is restored: the explorer makes its own side inert — the explorer and its handle (or, in sheet mode, the sheet's bar) — and drag-drop-panels everything else it renders — the other panels, the bar of hidden panels, the drop placeholders and its announcer. Tab and Shift+Tab therefore never land on the explorer's hidden controls, and Enter or Space never acts on one. The host's own chrome is not the explorer's to make inert: what the panel covers of it stays focusable, so a host binds `--aqdd-maximize-*` to keep its chrome uncovered (the demo binds `--aqdd-maximize-top` to its header) or makes it inert while the maximized panel's frame carries `data-aqmx-maximized="true"` (see the README's "Load the styles").
90
+ Escape restores the panel only when nothing else used the key: drag-drop-panels ignores an Escape another handler marked handled (`preventDefault()`), one that belongs to an input method's composition, one pressed inside an open dialog, and one pressed while a popover the browser closes on Escape is open. Every Escape the explorer handles is marked so — closing the sheet or the tree settings menu (it also stops there, so the explorer's outer handlers never see it) — and so is every Escape the document viewer handles: in a maximized panel, the first Escape in the viewer's open table of contents, style or alignment menu closes the menu, returns the focus to its button and keeps the panel maximized, and the next Escape restores the panel. This holds in framework mode as well, where React's root listener sits on the document itself: the mark, not the propagation, is what the page's later listeners read, so a host's own Escape listener on the document honours it by checking `defaultPrevented`.
91
+ - **Panel limit.** A pick beyond `multiView.maxPanels` opens nothing; the announcer says `panelLimit` once and a visible notice appears for three seconds where the reader made the pick. While the explorer's sheet is open (it stays open after the pick) the notice sits inside it, so it is not inert and presses on it land on it (outside the modal dialog it would be inert and let presses through to the sheet's controls). It spans the sheet's header from its top edge and always shows its whole text, wrapped around the close button, which stays above it (cut out of the notice with the sheet's surface) and pressable: when the text fits beside the button on one line the notice covers exactly the header's band and no control, and only text that needs more lines — a narrow sheet, enlarged text or a longer language — spills below the header over the top of the sheet's body for the three seconds. It never takes space in flow (in flow it would grow the header and push every tree row, the focused one included, down for three seconds and back up again, under the reader's pointer), so the header keeps its size and nothing in the sheet moves while it is shown or when it goes. It is never cut short: the announcer speaks only to screen-reader users, so a notice cut to fit one line would lose the limit for a reader at a phone width or with enlarged text. Otherwise it is at the stage's block-start, inline-end corner in the top layer, above a maximized panel; if the sheet closes or opens while it is shown, it moves there or back. Wherever it is, it ends before its three seconds when the focus moves onto an element it is painted over — the notice is the topmost element at one of nine points spread over the focused element's box, measured in the frame after the focus moved (in the sheet, a control below the header that Shift+Tab reaches under a notice of more lines; at the stage's corner, a panel's header button): the announcer has already spoken it, and a focused control never stays hidden under it. A control painted above it keeps it: the sheet's close button, which the notice's box contains, never ends it, so closing the sheet with that button moves the notice to the stage's corner. Browsers without the Popover API draw it in place at that corner, without the top layer. The notice itself is no live region, so the limit is spoken once.
92
+ - **Wheel.** Over a panel whose document is longer than the panel, a plain wheel scrolls the stage while the stage can still move in that direction; once it cannot (the columns fit the screen, or the stage is at its end), the panel itself scrolls. Ctrl/⌘ + wheel always scrolls the panel, and Shift + wheel and horizontal gestures are left to the browser. Over a panel whose document fits, every wheel is left to the browser. A maximized panel scrolls as usual.
93
+ - **Column memory.** A document keeps its column after it is closed: reopening it puts it back where it was. The memory holds the last 256 documents.
94
+ - **Placement of a new panel.** A document opened for the first time goes to the visible group with the fewest panels (the leftmost on a tie), and inside that group to its least-filled logical column.
95
+
96
+ ## The document region
97
+
98
+ - In the tree and single views the document region fills its pane and scrolls inside it; in a multi-view panel it takes the document's own height, and the panel's scroll area scrolls it.
99
+ - A document replaced in place by a newer copy of itself — Refresh document, Refresh tree, or the background revalidation of a document opened again more than 55 s after it was fetched — keeps its scroller and the reader's scroll offset: the new content is reconciled into the shown one (nothing is mounted again), so the scroller stays the same element.
100
+ Where the browser supports scroll anchoring it also holds the reader's line when something above it changes size (a refresh's failure notice appearing above a panel's document). An offset beyond the end of a document that became shorter stops at its new end.
101
+ - A fragment (a followed link's `#section`, a link to a section of the shown document, or a page's URL hash) follows one rule: the element the fragment indicates in the pane (see [Document links](url-contract.md#document-links)) is brought to the start of the pane's own scroller, and every other scroller moves only as far as needed to show it, in both axes.
102
+ The pane's own scroller is the panel's scroll area in the multi view, whatever its range. In a page view it is the viewer's scroll container when the document scrolls inside it. When the explorer grows with its document instead (its document region is taller than the scroller around it, which a host's own styles can arrange), it is the scroller that scrolls the explorer: a host's scroll container, the body when the root's `overflow` keeps the body's its own, or the page.
103
+ A page view of a definite height whose document fits has no such scroller: nothing outside the viewer moves while the element is already in view. The scrollers around the pane's scroller — the multi view's stage, a host's scroll container, the page — and the scroll containers inside the document that hold the element (a wide table that scrolls sideways) only bring it into view by the least distance.
104
+ The alignment is the browser's own, so a scroller's `scroll-padding` (any length, `min()`, `max()` and `clamp()` included) and the element's `scroll-margin` are honoured, and a host's `scroll-behavior: smooth` does not change where the element lands.
105
+ In a page view whose document scrolls in the viewer, the viewer's own scroll padding is the band its floating toolbar covers (see the toolbar below), so the element lands below the buttons. When an element no taller than the scrollport already sits at the start of its scroller, the least-distance alignment alone keeps it there (without scroll padding nothing moves) or brings it below the scroll padding;
106
+ every other element — one taller than the scrollport included, which the least-distance alignment leaves where it is because both of its edges lie outside what the padding leaves — is moved past and back within one task, which Chromium reports as `scroll` and `scrollend` even where the position ends unchanged.
107
+ Every element of a shown document has a scroll margin on every side as wide as the focus ring's extent (`--aqmx-focus-ring-width` plus `--aqmx-focus-ring-offset`, 4 px at a 16 px root font size), so an element brought to an edge of a scroller — the start, by a fragment, or any edge, by the browser when the element takes the focus — shows its whole focus ring: a fragment's element lands that far below the start of the pane's own scroller (below its scroll padding — in a page view's viewer, the toolbar's band).
108
+ A document shown in place of another, or the first one a pane shows, is aligned once its viewer has measured its toolbar and reserved that band (within the same task, before the next paint): the band's padding, added later, would push the element down by its depth where the page or a panel's scroll area scrolls the document.
109
+ A footnote reference the reader has scrolled to within that distance of the edge of a sideways-scrolling table keeps that side of its ring cut when it takes the focus: Chromium does not scroll an element that is already visible, scroll margin or not, and a ring drawn inside the reference would cover its glyph.
110
+ As the browser does for a fragment of its own, what hides the element is revealed first: a closed `details` around it opens (unless the element is in its summary) and a `hidden="until-found"` ancestor gets a `beforematch` event and is shown. An element that still has no box (inside `display: none`) does not move its pane.
111
+ In the multi view the panel then comes into the stage's view with the element, its header first, and both stay there while the stage settles, as for a panel brought into view (see [The multi-panel stage](#the-multi-panel-stage)): the stage moves only when part of the panel is out of its view, and never to take the element to the stage's start.
112
+ - A fragment that names no element of the pane but HTML's top of the document — the empty fragment (`#`), or one that decodes to `top` in any ASCII case (`#top`, `#TOP`) — takes the pane's own scroller to its beginning in both axes at once, and the scrollers around it show that scroller by the least distance (in the multi view the panel comes into the stage's view); the element the fragment indicates — an element with the id `top`, or an `<a name="top">` — is still that fragment's element, as above.
113
+ As in HTML, the top of the document is no element: the focus and the sequential focus navigation starting point stay on the link the reader activated (Chromium leaves them there too).
114
+ In a page view such a link (`#top` or `#`, indicating no element) is also the browser's own fragment navigation, which scrolls the page itself to its top first (HTML applies the top of the document to the viewport; a page that does not scroll, such as one with `lockDocumentScroll`, stays where it is); the explorer's rule above then moves the page from there only as far as it needs to show the pane's scroller — except when the explorer follows the jump during `popstate`, before the browser's own scroll (a link to the hash the URL already has, below), where the page ends at its top.
115
+ A link to `#top` is followed through the URL's hash like any fragment-only link; a link to the empty fragment leaves no fragment in React Router's location (which, like `location.hash`, reads an empty fragment as none), so the explorer follows it from the click, once the browser's own navigation is done, and Back or Forward to such an entry leaves the pane where it is.
116
+ - How a link reaches a section. A fragment-only link (`#section`) is the browser's own fragment navigation in a page view: the browser scrolls the element to the start of every scroller it sits in, the page included, and the explorer then applies the rule above as the URL's hash changes, for every history entry: React Router gives every entry the browser makes itself (each fragment-only link) one shared key, so the explorer tells navigations apart by the location object React Router makes for each.
117
+ A fragment-only link to the hash the URL already has makes the browser replace the current entry and fire `popstate`; Chromium keeps the entry's state, so React Router reports a pop to the same entry with the same key. When React Router's last navigation was a push or a replace (the reader came by a link, such as a section link of the shown document), that pop is a new location, which the explorer follows as it is reported — during `popstate`, before the browser's own scroll, whose alignment then stands;
118
+ when it was already a pop (Back, Forward, the page load, or an earlier such click), React Router's location stays the same, and the explorer follows the jump from the click, once the browser's own navigation is done.
119
+ A document link to a section of the shown document (`./this.md#section`) is, in a page view, a router navigation like any other document link (one history entry), and the explorer scrolls by the rule above; a host's React Router `<ScrollRestoration>` scrolls the element with the hash's id into view itself, page-wide and before the explorer does, on a router navigation that carries a hash to a location it has saved no window offset for (a new history entry), whenever that element is already in the page; on Back or Forward to an entry it saved an offset for it restores the window's offset instead (see the README's hosting notes). In a multi-view panel both kinds of link only scroll the panel, by the rule above, and the URL keeps no hash.
120
+ An entry of the viewer's table of contents is the same jump as a link to its heading's section. In a page view it is a router navigation to the shown document with the heading's hash (`follow-link`: one history entry, as for a section document link, `<ScrollRestoration>` included), or, when the URL's hash already names that heading, the jump alone without a history entry, as the browser's own navigation to the hash the URL already has replaces the entry; in a multi-view panel it is the jump alone and the URL keeps no hash.
121
+ The jump follows the rule above: a closed `details` around the heading opens, the scroll is the explorer's instant one (the viewer's own smooth scroll never runs, so it never animates under `prefers-reduced-motion: reduce`), and the focus moves to the heading as below.
122
+ An unpinned table of contents closes; a modified or middle click on an entry is left to the browser, which opens the entry's href, the heading's own address (in a panel, the panel's document in the tree view; see [URL contract](url-contract.md#table-of-contents)); the entry of a heading without an id is text, not a link, and does nothing.
123
+ After a jump within the shown document — either kind of link or a table-of-contents entry, in any view, or a page's hash changed by Back or Forward — the focus moves to the element without scrolling again (a heading takes it through a temporary `tabindex="-1"`) when it was inside the document region, or fell from it to the page while the reader stayed with the region — the browser's own fragment navigation takes it off the link when the element takes no focus, and a press on the document's text drops it to the page too —, as after a link to another document's section,
124
+ so reading and the next Tab or Shift+Tab continue from the section; focus outside the region (a page load, a page loaded with a hash included) and focus the reader moved away from it (see below) are never taken.
125
+ - The region is the containing block of the viewer's toolbar and of any fixed element of a document, and clips them, so the toolbar sits at the region's top-right; in a panel the panel's frame clips instead, at the same top edge, and the toolbar, attached to the top of the document, scrolls away with it. The explorer sets the viewer's `--aqmd-toolbar-inset-block-start` on the region to a focus ring's extent (`--aqmx-focus-ring-width` plus `--aqmx-focus-ring-offset`), so the buttons sit that far below the edge and a focused button's ring is drawn whole.
126
+ The toolbar's buttons — alignment, style and table of contents — form one row in that visual and Tab order, spaced in rem, so at any text size they never overlap and each stays at least 24 × 24 CSS px.
127
+ The document's column fills its pane (the viewer's `[data-aqmd-content]` takes the track's full width): the explorer gives the viewer a `width` or `maxWidth` only where a single-view embed asks for one (`?width=` / `?maxWidth=`), each capped at the pane's width (`min(<value>, 100%)`, so a width wider than the pane never makes every line scroll sideways), so the alignment button, which places only a column narrower than its track, shows only there; the tree view, a single view without those parameters and a maximized panel show the style and table-of-contents buttons.
128
+ A Mermaid gantt chart is drawn at the width of its slot (and again, after a short debounce, when a split or the window changes that width), so it keeps its natural height.
129
+ Where the toolbar covers the inline extent of the document's content — in the tree view and a maximized panel at every width, where the content spans the pane, and in the single view unless its `width` or `maxWidth` leaves room beside the content — the viewer reserves the depth it covers as the top padding and the `scroll-padding-block-start` of its scroll container and publishes it as `--aqmd-toolbar-block-overlap` on `[data-aqmd-scroll-container]`: the first line and every fragment's element start below the buttons. Where the toolbar covers none of the content the band is `0px`.
130
+ An open, unpinned menu of the toolbar (the table of contents, the style or the alignment menu) closes on Escape while the focus is inside it and returns the focus to its button; the viewer marks that keydown handled (`preventDefault()`). In a panel every part of the document (failure callout, image, media) takes its content height, and an embedded PDF is 1 : √2.
131
+ - A file header (the toolbar of an image or a medium: its name, the zoom controls, Open in a new tab and Download) wraps its controls onto more lines instead of letting any of them run out of the pane or the panel at a narrow width or a large text size; the name takes what the controls leave on the first line and is cut with an ellipsis.
132
+ - An image opens fitted to the pane (Fit pressed). Zoom in and Zoom out step through the levels 1/φ², 1/φ, 1, φ and φ² (≈ 38.2 %, 61.8 %, 100 %, 161.8 % and 261.8 %) of a base size — the image's natural size, or for an image without one the fitted size described below — and the level shown is a percentage of that base; from the fitted size they go to the nearest level above or below it. A zoom button that cannot act stays focusable: before the image is measured, at the ends of the levels, and — while an image with a natural size is fitted — Zoom out when the fitted size is already at or below the smallest level, it is `aria-disabled`, dims its icon and ignores activation. While such an image is fitted, Zoom out's availability follows the pane's size: a pane that grows or shrinks can make it available or unavailable.
133
+ An image without a natural size (an SVG whose root has no absolute `width` and `height`, such as one with only a `viewBox` or with `width="100%"`) is recognized when the browser reports a natural width or height of 0 for it, or when Fit enlarges it, which Fit never does to an image that has one. Its levels are relative to its fitted size instead: the size it was fitted to when the reader leaves Fit, kept until Fit is pressed again. From Fit, Zoom in goes to φ and Zoom out to 1/φ of that size, and the level shown is a percentage of it.
134
+ Chromium reports the default object size (300 × 150, fitted to the SVG's aspect ratio) as such an SVG's natural size, so in a pane narrower than that size Fit does not enlarge it, and its levels stay relative to that default size (see the README's known limitations).
135
+ The level shown beside the buttons (Fit, or a percentage) is an `output`, a polite status: assistive technology reads the new level when it changes, and the focus stays on the button that changed it.
136
+ - The region is not focusable, so the keyboard scrolls the document the reader clicked in: after a click in a document's text, PageDown, the arrow keys, Space, Home and End scroll it.
137
+ - When the content that holds focus is replaced while focus is inside the region — the shown document replaced by another (a document link followed with the keyboard or a click, or a back or forward navigation), a failure replaced by its document after Try again, or a background revalidation that changes the document and removes the focused element — focus moves into the replacement instead of falling to the page: to the fragment's target, else its first heading, else the element that scrolls it (the viewer's scroll container, or a failure's callout area), else its first control (an image or a medium). A revalidation that changes nothing, or keeps the focused element, leaves focus where it is.
138
+ Moving the focus never scrolls; a heading or scroller that takes no focus gets `tabindex="-1"` until focus moves elsewhere. When another document replaced the one that held the focus, the element the focus moved to is then brought into view by the least distance in the scrollers around the pane — in the multi view with its panel, as for a fragment — so a panel whose header the stage had scrolled out of view shows its new document's first heading; a revalidation or a Try again in the same document keeps the reader's position.
139
+ The region itself never takes focus, and focus outside the region, focus that was never in it, and focus the reader moved away from it are never taken. The reader moves it away by moving it to an element or into a frame outside the region, or by pressing anywhere outside the region — whenever the press happens, also after the focus had already fallen to the page, and whether or not the press moves the focus — unless the focus stays inside the region (a press whose default action the page prevented).
140
+ Focus that fell from the region to the page while the reader stayed with it (a press on the document's text, the browser's own fragment navigation, the focused content removed, the window losing the focus to another window or application) counts as inside.
141
+ So does focus in a frame inside the region (a PDF document's frame, an embedded frame of `iframeHosts`), also when it entered the frame straight from outside the region (a click from the tree, Tab), and a press outside the region moves it away from there as well.
142
+
143
+ ## Switching views
144
+
145
+ The tree and multi-panel views are separate parts of the page, so the control a reader uses to switch between them leaves with the old view. Switching with the explorer's own controls keeps the keyboard focus in the explorer instead of dropping it to the page: the header's link to the other view hands the focus to the new view's link (the same place in the same header), or — below the sheet threshold, where that link sits in the new view's closed sheet — to the sheet's toggle; the selection tray's Open in panels hands it to the first panel it opened. The hand-over happens once the new view has rendered, also while its code is still loading. Whether the reader was still on the control is decided after the switch removed it, so the hand-over works whether or not the browser fires `focusout` when it removes the focused element (Chromium does). Focus that was anywhere else, that the reader let go of before the switch, or that the reader moved meanwhile stays where it is.
146
+
147
+ ## Busy and unavailable controls
148
+
149
+ A header button that is unavailable never takes the `disabled` attribute, which would drop the focus to the page body: Refresh tree is `aria-disabled="true"` and `aria-busy="true"` while the tree reloads (its icon is dimmed, and spins unless motion is reduced) and ignores activation; Expand all is `aria-disabled` until the tree is shown, and `aria-disabled` and `aria-busy` while a press runs (its icon is dimmed but does not spin), when it ignores activation; Collapse all is `aria-disabled` until the tree area is mounted, and ends a running press and cancels the tree's listings. The focus stays on the button throughout. In the selection tray, removing the focused document moves the focus to the next document's remove button (else the previous one); removing the last document or clearing the tray moves it to the tree's tab-stop row (its stand-in stop while that row is outside the tree's render window, and the tree settings button while the tree shows no rows).
150
+
151
+ Refresh tree carries its label as visible text next to its icon (`refreshTreeLabel`, «Refresh tree» / 「ツリーを更新」), which is also its accessible name; its tooltip says what it reloads (`refreshTree`).
152
+ It has the server reload the source's tree and shows the tree the server answers (unless a tree that loaded meanwhile, by a page load or a background fetch, began later: the later one stays, and the refresh still counts as done), then reads the documents again past the host's caches (the shown ones at once — a document in the background, a failure in the foreground, one still loading as soon as its request settles — and the others when next opened), whether or not the tree reloaded. A reloaded tree is announced politely (`treeRefreshed`).
153
+ A reload that fails keeps the tree on screen and shows an alert first in the explorer's body (`treeRefreshFailed` with the failure's title, `data-testid="markdown-explorer-refresh-failure"`), until the next refresh starts and only while the pane shows that source. Switching to another source after a refresh fetches that source's tree again in the background (it may share the storage that changed).
154
+
155
+ **Document refresh.** Every view reads one shown document alone again past the host's caches with Refresh document, apart from Refresh tree (which also lists every expanded folder again).
156
+ The tree and single views show it in a thin bar above the document region, at the bar's inline end, as a labelled button (`refreshDocumentLabel`, «Refresh document» / 「文書を更新」, which is also its accessible name; its tooltip is `refreshDocument`), and the tree view's empty state, which has no document, has no bar.
157
+ A multi-view panel shows it icon-only in a bar pinned to the top of the panel's scroll area (see [The multi-panel stage](#the-multi-panel-stage)), named after the panel's document (`refreshDocumentNamed`, «Refresh a.md» / 「a.md を更新」, its accessible name and its tooltip). The document stays on screen while it reloads.
158
+ A loaded document is replaced in place only when it changed, keeping its scroller and the reader's offset (see [The document region](#the-document-region)); a shown failure, a document the page loader gave up on and one not cached yet are fetched in the foreground. Each press is announced once. A document reloaded in the background is announced as reloaded (`documentRefreshed`); one fetched in the foreground is shown by the pane, so in the tree and single views the pane alone announces it — as shown (`documentShown`) or as a failure (`documentFailed`) — and in a panel, whose pane announces no document it shows, the control announces a document as reloaded and leaves a failure to the pane.
159
+ A reload that answers a failure keeps the document and shows a notice inside the bar, below its button row (`documentRefreshFailed` with the failure's title, `data-testid="markdown-explorer-document-refresh-failure"`), while the copy it kept stays on screen: a newer answer that replaces or confirms that copy (Refresh tree, the background revalidation of a document opened again more than 55 s after it was fetched, a fetch after the cache dropped it), a move to another document (the notice does not come back on return) and that control's next refresh each take it down; the notice is no live region, and the announcer speaks the same text (see [Announcements](#announcements)). Like Refresh tree, the button is `aria-disabled` and ignores activation while it runs (also `aria-busy`, its icon spinning unless motion is reduced) and while the document loads, and never takes `disabled`.
160
+ Each page view's control and each panel's control keeps its own state: refreshing one panel never makes another panel busy, shows another panel's notice or requests another panel's document. An outcome that arrives after its control moved to another document (a URL change, a link followed inside a panel) or went away (a panel closed or rebuilt) is not reported, and a refresh that a newer request for the same document replaced — Refresh tree reading the shown documents again meanwhile, for example — ends silently while that request updates the document.
161
+ A folder whose children failed to load shows Try again in its row, and the button leaves the row while the children load again: activating it while it holds the focus (Enter, Space or a click) first moves the focus to the folder's row, without scrolling, so the focus stays in the tree whether the retry succeeds or fails again.
162
+ The failed row's label never pushes Try again out of the row. Rows are single lines of a virtual list, so when the row is too narrow — a long folder name, a deep folder, large text or a longer language — the label's parts give way in this order: the failure text first (hidden, it stays Try again's description), then the folder's name, cut with an ellipsis, down to 3em (or its whole width when it is shorter), then Try again's text, cut with an ellipsis, down to the button's icon (the button keeps its accessible name), and only then the name below 3em. The icon-only Try again is 1.5rem wide (24 px at 16 px text) and as tall as the row, and it stays wholly visible, focus ring included, in every row whose name box is at least that wide plus a 0.3125rem gap: in the sheet at 320 px with 200 % text, two levels below a top-level folder, Try again is the icon alone and the name keeps about one character before its ellipsis. Try again's focus ring is drawn inside the button, so the tree's name box, which hides what overflows it, never cuts it, and moving the focus to Try again does not scroll the row's name out of view.
163
+ The row's accessible name, which `@aiquants/directory-tree` sets, leaves the failure text out, so every failed load is announced once through the explorer's announcer (from the sheet's region while the sheet is open; see [Announcements](#announcements)), naming the folder (`foldersLoadFailedNamed`): an expansion, a remembered expansion restored on load, Expand all and Try again alike, a retry that fails again included; a retry that succeeds is not announced. Folders whose loads fail within the same animation frame — Expand all or a restored expansion against a server that is down — are named together in one sentence ("a, concepts, and images could not be loaded"). Only failures in the tree the explorer shows are announced, and redrawing the rows never repeats an announcement. Try again is described by the failure text (`aria-describedby`), so a reader who tabs to it hears why it is there.
164
+
165
+ ## Announcements
166
+
167
+ The explorer's own messages — failed folder loads, the start and end of an Expand all press, tree setting changes, the document shown, a document that failed, panel actions other than moves and the panel limit — go through one announcer per reader, which speaks them through a live region (`role="status"`, polite and atomic). Every message posted before the next animation frame is spoken: the region takes them together in that frame as one text — two or more joined as separate sentences (`announcementSequence`) — so neither of two failures, nor a failure and the document shown in the same frame, is dropped. They are spoken in the order posted, except that the items posted through the same list label within a frame (the folders whose children failed, `foldersLoadFailedNamed`) merge into one sentence in the place of the first item, ahead of any message posted between them: folder a failing, then the document shown, then folder b failing in one frame is spoken "a and b could not be loaded. Showing x.md." The region is emptied when the first message of a frame is posted, so the same text posted again is spoken again, and a later frame replaces the text.
168
+
169
+ An Expand all press is announced once when it starts listing (`expandAllStarted`; a press over folders whose contents are all known skips it) and once at its end: `expandAllDone` (the folders it opened, and how many could not be loaded — those are also named by `foldersLoadFailedNamed` — or that every folder is open, which it does not claim when the reader closed a folder during the press), `expandAllFolderLimit` (inviting another press, in the words of the Expand all button's own label) or `expandAllDepthLimit`.
170
+ Collapse all, a source switch and a cancelled listing say nothing, a press paused by hiding the explorer says nothing until it ends, and nothing is announced per row: neither the progress line nor the stop notice is a live region, and a loop row's name says what it is.
171
+
172
+ The announcer speaks through the explorer root's region (`markdown-explorer-announcer`) while no sheet is open. The sheet's modal dialog hides that region from assistive technology, so the sheet holds a region of its own (`markdown-explorer-sheet-announcer`), present and empty from the start, which takes the announcements while the sheet is open: a failed folder, a setting changed, a document shown or failed while gathering or a panel limit reached in the sheet is spoken from inside it. A frame goes to the region that holds the announcements when the frame is published, and a change of holder empties both regions, so nothing is spoken twice when the sheet opens or closes, and nothing spoken in the sheet is repeated by the root's region once it closes.
173
+
174
+ Each reader has an announcer of their own: when another reader signs in, the rebuilt explorer starts with empty regions, and nothing told to the previous reader (a folder or document name), nor a message the previous reader's work posted just before the switch, reaches it.
175
+
176
+ Three other kinds of live region speak for themselves, outside the announcer and its frames: each image viewer's zoom level is an `output`, a polite status (see [The document region](#the-document-region)); the failure callout of a tree that cannot be loaded is `role="alert"`, which assistive technology speaks at once (a host's `renderTreeFailure` view replaces that callout, so it must announce itself); and a panel move, and a touch or pen drag's lift and cancel, are spoken by the multi view's drag-and-drop layout from its own polite region (`data-aqdd-announcer`), in the explorer's words (`panelMoved`, `panelMoveUnavailable`, `panelDragStarted`, `panelDragCancelled`; see [The multi-panel stage](#the-multi-panel-stage)). Moves and touch drags start in the panels' headers, which the open sheet makes inert, so that region is never hidden while a move can be made.
177
+ A document's failure callout is no alert in any view — tree, single or a multi-view panel: a document pane may sit behind the open sheet, whose modal dialog hides every alert outside it, so the announcer speaks the failure (`documentFailed`, the document's name and the failure's title) from the region the reader can hear. It is spoken once for each failed attempt: a Try again or a reload that fails again is spoken again, while a re-render, a panel rebuilt by a change of the column count or a move, a document visited again or a panel opened again is not — the last two show the cached failure until the fetch they start settles, and only that fetch's answer is spoken (`documentFailed` again, or `documentShown` in a page view; a panel says nothing when its document loads). Nor is a cached failure that no pane showed spoken when opening its document fetches it again — one a dwell prefetch fetched more than 55 s earlier, or any failure after the tree's reload button marked the cache stale: the pane shows it until the new fetch settles, and only the new answer is spoken. A panel's failure is spoken once the URL the multi view wrote has rendered, so a document opened from the sheet that fails is spoken by the root's region after `panelOpened`, once the sheet has closed.
178
+ The notice of a document refresh that failed is no alert either, as the open sheet would hide it from a document behind it: the announcer speaks it (`documentRefreshFailed`) once for each failed refresh.
179
+
180
+ ## Held keys
181
+
182
+ A held key repeats its keydown, and the browser turns every repeated Enter into one more activation of whatever holds the focus by then. Where an activation hands the focus to another control, one press would run on through every control that receives the focus; where it keeps the focus, it would repeat its change, and a popup would flip open and closed once per repeat. So one guard at the explorer's root ignores every repeated Enter keydown inside the explorer: it cancels the keydown, so nothing is activated, and stops it there, before any control inside sees it. The first press always acts.
183
+ The guard covers the controls the explorer renders — the selection tray's remove buttons (each removal focuses the next one), Clear the selection and Open in panels; the header's link to the other view (the new view's link receives the focus), Reload, Expand all and Collapse all, and the tree settings button and its rows (opening the menu focuses its first row, and a row would cycle its value); a folder's Try again;
184
+ the sheet's toggle and close button (each focuses the other); an image's Fit, and a file's Open in a new tab and Download; a notice's action link; and a failure's Reload the page and Try again —
185
+ and the controls other packages render inside it, some of which ignore repeats on their own as well: the tree's rows (`@aiquants/directory-tree`), so holding Enter never flips a folder or a gathered document back and forth, and a press that hands the focus to a row (removing the last gathered document, clearing the selection, or a folder's Try again) does not go on to press that row; a panel header's buttons (`@aiquants/drag-drop-panels`: the moves, the drag-mode toggle, Hide, Maximize or Restore, and Close), whose Alt + Shift + arrow shortcuts also move a panel once per held chord;
186
+ the document viewer's toolbar and its popups (`@aiquants/markdown`: the table of contents, the display style and alignment menus, and their pin buttons); and the controls inside a document (links, `<details>` summaries, a diagram's buttons).
187
+ Because the repeated keydowns stop at the explorer's root, keydown listeners the host attaches outside the explorer for the bubbling phase (on `document`, for example) do not receive them either; the host's chrome from `renderFrame` is outside the root and keeps the browser's own handling.
188
+ A control inside the explorer opts in to the key repeat with `data-aqmx-key-repeat="allow"` on itself or on an element around it: a press of Enter that began on the control then keeps activating it once per repeat, as long as the focus stays on it. The opt-in follows where the press began, not where its repeats land: a press that began on another control and handed the focus to an opted-in one — following a document link to an image, or a failure's Try again that comes back as an image, both of which focus the image's Zoom out — does not run on into it, and neither does a press that began outside the explorer. Releasing Enter, or moving the focus off the control, ends the press's claim; the next press starts afresh. The explorer sets it on Zoom in and Zoom out, which keep stepping while a press begun on them is held: each repeat is one bounded step that keeps the focus, and they stop at the ends of the levels.
189
+ Space needs no such rule on buttons, which act when the key is released, and a tree row, which acts on the Space keydown, ignores its repeats itself (`@aiquants/directory-tree`): a held Space toggles a folder or a gathered document once.
190
+
191
+ ## Rhythm
192
+
193
+ - Spacing is the Fibonacci scale 1, 2, 3, 5, 8, 13, 21, 34, 55 px, written in rem (`--aqmx-space-1` to `--aqmx-space-9`), so it scales with the reader's font size.
194
+ - Header rows, icon buttons, menu items, tree rows and the retry button of a folder that failed to load are one control high: 34 px (2.125rem), and 44 px (2.75rem) under a coarse pointer, the WCAG 2.2 enhanced target size. The tree measures `--aqmx-control` at run time, so its rows follow the pointer, the root font size and a host's token override. The sheet's toggle and close button are 44 px under every pointer, and the resize handle is a 24 px band under a fine pointer (see [The split](#the-split)).
195
+ - The tree's virtual scrolling renders 13 rows beyond each edge and reports scrolling at most every 13 ms. Its 8 px scroll bar does not grow under a coarse pointer (the wheel, touch drag and the tree's arrow keys scroll the same list), and its arrow buttons are not Tab stops. The arrow keys, Home and End keep the focused row wholly visible: when the focus moves onto a row outside the tree's window, the tree scrolls by exactly the distance needed.
196
+ - The explorer's header wraps rather than cutting a control off: the source switch keeps at least 9rem and the icon buttons wrap below it, so every control stays reachable at the minimum width, under a coarse pointer and at large font sizes.
197
+ A labelled button that is wider on its own than its line — Refresh tree in the header, Refresh document in its bar — shrinks, down to one control wide, and ends its label with an ellipsis (its icon stays whole), so it never pushes the explorer pane or the bar sideways; the cut is visual only, and the button's accessible name stays its whole label. The tree settings menu opens below the header.
198
+ - Durations are 89, 144, 233 and 377 ms (`--aqmx-duration-1` to `--aqmx-duration-4`); with `prefers-reduced-motion: reduce` every explorer duration is 0, the explorer's resize handle and the tree's fade while an expansion is pending (a transition `@aiquants/directory-tree` sets on the tree's root element) included. The drag-and-drop panels follow the setting as well: `@aiquants/drag-drop-panels` leaves out its own transitions and its reorder animation.
199
+ - Line heights are 1 + 1/φ² ≈ 1.382 for the interface and φ ≈ 1.618 for the explorer's own prose (callouts and empty states). A document's text keeps the line height that `@aiquants/markdown` and the document style set (not φ).
200
+ - The type scale is 13, 16 and 21 px (0.8125, 1 and 1.3125rem) for body text, titles and display text: 13 and 21 are consecutive Fibonacci numbers (their ratio ≈ φ), and 16 sits near their geometric mean. Tree rows use `@aiquants/directory-tree`'s own type size, and a document's text that of its document style.
201
+ - Loading skeleton lines are 61.8 %, 100 % and 85.4 % wide (1/φ, 1 and 1 − 1/φ⁴).
202
+
203
+ ## Colour
204
+
205
+ Text colours reach WCAG 2.2's 4.5:1 against every surface they are drawn on, in both colour schemes; a unit test reads the shipped tokens and checks every pair. The scheme follows the `theme` prop (`data-aqmx-theme` on the root), not the operating system, so a host with its own theme switch stays in control.
206
+
207
+ Tree rows take every colour from the tokens, whatever the host's own dark variant: rest rows `--aqmx-text` on the surface; hovered and selected rows `--aqmx-text` on `--aqmx-tree-row-tint` (a selected row also has a 2 px bar in its text colour); the shown document and documents open as panels `--aqmx-accent` on `--aqmx-tree-row-accent-tint`.
208
+ The two row tints are translucent: with the tree lines on, `@aiquants/directory-tree` draws the connector lines and the expand glyphs on a canvas under the rows, so an opaque row would hide them, and a hovered folder would lose its glyph.
209
+ Every text and focus-ring pair is checked against each tint composited over `--aqmx-surface`, the opaque surface the explorer pane paints under the tree in the split and in the sheet alike. Lines and glyphs use `--aqmx-tree-line`, which reaches 3:1 against every row state, composited the same way, in both schemes (the glyph shows whether a folder is expanded).
210
+ The tree draws them in the colour the browser resolves for the token on a hidden probe, never in the token's text, because a canvas resolves a system colour once and keeps it. The colour is read at run time and again when `theme` changes, when forced colors turn on or off and when the preferred colour scheme changes (which is how the browser reports a switch between a light and a dark forced palette), so a host's override recolours them; without the stylesheet no lines are drawn.
211
+ With the tree lines turned off (a tree setting), the tree draws each expand glyph in its row instead, in `--aqmx-tree-line` as well: the explorer passes the token as the glyph's own colour (`@aiquants/directory-tree`'s `expandIconStyle`), so it follows `theme`, not a host's `dark` class.
212
+
213
+ Under forced colors (Windows High Contrast) the explorer draws its states in system colours: the shown document's row, rows open as panels, gathered rows, a checked tree setting, the open tree settings button and the open sheet toggle get `Highlight` with `HighlightText` (`forced-color-adjust: none` on that element only); focus rings, the resize handle's included, are `Highlight`; an unavailable header button is `GrayText`; the selection tray's buttons get a `ButtonText` border. Shown and gathered rows look the same there.
214
+ The tree's lines and expand glyphs are drawn in `CanvasText` of the palette in force, since forced colors never recolour a canvas; switching to a high-contrast theme of the other lightness with the page open redraws them in the new palette. An expand glyph drawn in its row (tree lines off) takes the palette's text colour, like any text. A hovered row keeps its tint's transparency (forced colors keep a background's alpha), so they stay visible on it.
215
+ A row in `Highlight` stays opaque, so the system colour pair is exact, and covers its own line segment; only document rows take those states, so no expand glyph is ever covered. In the document pane the pressed Fit button is drawn in `Highlight` with `HighlightText` and an unavailable zoom button in `GrayText`; in the multi view focus rings and a maximized panel's outline are `Highlight`, and drag-drop-panels draws the pressed custom-mode toggle in `Highlight` with `HighlightText` and an unavailable move button in `GrayText`. The loading cues, which are otherwise only a background, keep a system colour of their own (`forced-color-adjust: none` on them): the progress line over a document being replaced is `Highlight`, and every loading skeleton — the tree's, the multi stage's before its layout is measured, and that of a pane waiting for its first document — is `GrayText`.
@@ -0,0 +1,48 @@
1
+ # Localization
2
+
3
+ This page is part of the host-facing contract of `@aiquants/markdown-explorer`; its summary is in the [README](../../README.md#localization).
4
+
5
+ ## Languages
6
+
7
+ - `locale` is the UI language, `"en"` (the default) or `"ja"` — never a BCP 47 region tag. `"ja-JP"`, `"EN"` and any other value throw a `RangeError` on the first render, so a mistyped locale is found in development instead of silently showing English.
8
+ - The catalogs are `MARKDOWN_EXPLORER_LABEL_CATALOGS.en` and `MARKDOWN_EXPLORER_LABEL_CATALOGS.ja`, frozen objects with the same keys; `MARKDOWN_EXPLORER_LABEL_KEYS` lists the keys in declaration order.
9
+ - The explorer passes its document-viewer strings to `@aiquants/markdown`, which names every control and status text of the viewer with them, so the viewer follows the same language:
10
+ its status texts (`viewerLoading`, `viewerErrorPrefix`, `viewerErrorTitle`, `viewerMissingImage`), its toolbar's buttons (`viewerSelectAlign`, `viewerSelectStyle`, `viewerOpenToc`), the alignment and style menus (`viewerAlignTitle`, `viewerAlignLeft`, `viewerAlignCenter`, `viewerAlignRight`, `viewerPinAlign`, `viewerUnpinAlign`, `viewerStyleTitle`, `viewerPinStyle`, `viewerUnpinStyle`), the table of contents (`viewerTocTitle`, which also names its navigation, `viewerPinToc`, `viewerUnpinToc`)
11
+ and every Mermaid diagram's controls (`viewerMermaidInteractionMode` for the mouse-interaction menu and its button, `viewerMermaidModeNone`, `viewerMermaidModeZoom`, `viewerMermaidModePan`, `viewerMermaidModePanShift`, `viewerMermaidModeBoth`, `viewerMermaidModeBothShift` for its modes, `viewerMermaidZoomIn`, `viewerMermaidZoomOut`, `viewerMermaidResetView` and `viewerMermaidRetry`), and the titles of embedded frames (`viewerEmbeddedVideo` and `viewerEmbeddedPost` after a provider's name, as in "YouTube: embedded video", and `viewerEmbeddedFrame` before the host of an iframe written in a document without a title).
12
+ A label the viewer receives carries no `lang` of its own, so it is read in the page's language. Where the viewer's own built-in label is in a catalog's language, the catalog repeats it word for word (the English catalog the viewer's English labels, such as `Display Style` and `Zoom In`; the Japanese catalog its Japanese ones, such as `目次を開く`).
13
+ - The explorer passes `treeEntryKindDirectory` and `treeEntryKindFile` (the kind words in the rows' accessible names) and `treeScrollUp`, `treeScrollDown`, `treeScrollToTop` and `treeScrollToBottom` (the tree's scroll bar) to `@aiquants/directory-tree`.
14
+ - The explorer passes every string the multi view's drag-and-drop layout shows to `@aiquants/drag-drop-panels`: the panel headers' buttons (`panelSwitchToCustomDrag`, `panelSwitchToNormalDrag`, `panelMovePreviousColumn`, `panelMoveNextColumn`, `panelMoveUp`, `panelMoveDown`, `panelHide`, `panelMaximize`, `panelRestore`, `panelClose`) and the custom drag mode's badge (`panelCustomDragBadge`), the move announcements (`panelMoved`, `panelMoveUnavailable`), a touch or pen drag's lift and cancel announcements (`panelDragStarted`, `panelDragCancelled`), the bar of hidden panels (`hiddenPanelsTitle`), a hidden panel's stand-in while a panel is dragged (`hiddenPanelBadge`, `hiddenPanelNotice`), the drop placeholders (`panelDropHere`), an empty column's text (`multiColumnEmpty`) and the name of each column's region (`multiColumnRegion`).
15
+ The stage never lets the reader resize its columns, so the layout renders no column resize handle and the catalog has no key for that handle's name. A label the layout receives carries no `lang` of its own, so it is read in the page's language, like the explorer's other strings.
16
+ - `treeRegion` names the tree itself, apart from `explorerRegion`, which names the explorer's landmark (and the sheet on narrow frames).
17
+ - `refreshTreeLabel` is the visible text of the explorer's Refresh tree button and `refreshDocumentLabel` that of the page views' (tree and single) Refresh document button, each also its button's accessible name; `refreshTree` and `refreshDocument` are their tooltips.
18
+ A multi-view panel's Refresh document is icon-only, and `refreshDocumentNamed(name)` (the panel title) is its accessible name and its tooltip. A successful reload is announced with `treeRefreshed` or `documentRefreshed(name)`. A failed one is shown with `treeRefreshFailed(reason)` (an alert in the explorer) or `documentRefreshFailed(reason)` (a notice, which the announcer also speaks), where `reason` is the failure's title (the matching `failure…Title` label).
19
+ - `switchToTree` and `switchToMulti` name their destination with the README's view names ("Open in the tree view" / "ツリービューで開く", "Open in the multi-panel view" / "マルチビューで開く"); no label names the single view, which no control inside the explorer opens.
20
+
21
+ ## Overrides
22
+
23
+ `labels` overlays single keys onto the catalog of `locale`:
24
+
25
+ ```tsx
26
+ const HANDBOOK_LABELS = {
27
+ explorerRegion: "Handbook",
28
+ selectionTitle: (count: number) => `${String(count)} picked`,
29
+ } satisfies MarkdownExplorerLabelOverrides
30
+
31
+ <MarkdownExplorer data={data} config={explorerConfig} storageNamespace="docs" locale="en" labels={HANDBOOK_LABELS} />
32
+ ```
33
+
34
+ - Strings that embed a value are functions — `foldersLoadFailedNamed(names)` (the announcement of the folders whose children failed to load within one frame, named in one sentence with the language's list format;
35
+ `folderLoadFailed` is the text shown in a row), `announcementSequence(messages)` (the live region's text for two or more messages announced within one frame, as separate sentences), `selectionTitle(count)`, `openSelectionInMulti(count)`, `removeFromSelection(name)`, `expandIconSizeValue(pixels)`, `settingAnnouncement(setting, value)`, `documentShown(name)`, `foldersLoading(count)` (the tree's progress line: the source's folder listings queued or in flight), `expandAllProgress(opened, loading)` (the progress line during an Expand all press), `expandAllDone(opened, failed, complete)` (the end of a press that no limit stopped; `complete` is whether every folder is open at the end, false when the reader closed one during the press, so the sentence then claims nothing about the rest; nothing opened, nothing failed and complete means every folder was already open), `expandAllFolderLimit(opened, limit, expandAll)` (a press stopped at `expandAll.maxFolders`; `expandAll` is the Expand all button's own label, so a host that renames the button keeps the invitation to press it again consistent), `expandAllDepthLimit(opened, depth)` (a press that left folders deeper than `expandAll.maxDepth` closed), `loopFolderName(name)` (the name of a loop row, a folder met again among its own ancestors), `documentFailed(name, title)` (a document that could not be shown:
36
+ the path's last segment and the failure's title — the text of the matching `failure…Title` label, not the reason's code), `imageZoomLevel(percent)`, `treeRefreshFailed(reason)`, `documentRefreshed(name)`, `documentRefreshFailed(reason)`, `refreshDocumentNamed(name)` (a multi-view panel's icon-only Refresh document: its accessible name and tooltip, `name` being the panel title), `panelLimit(max)`, `panelOpened(name)`, `panelClosed(name)`, `panelAlreadyOpen(name)`, `panelHidden(name)`, `panelShown(name)`, `panelMaximized(name)`, `panelRestored(name)` — so each language can place the value where its grammar needs it.
37
+ - The labels `@aiquants/drag-drop-panels` fills itself are templates instead, strings whose `{name}` placeholders mark where each value goes: `panelMoved` (`{title}` the panel's name, `{column}` its visible column, `{position}` its place among that column's shown panels), `panelMoveUnavailable` (`{title}`, and `{move}` the name of the move, one of the `panelMove…` labels), `panelDragStarted` and `panelDragCancelled` (`{title}`) and `multiColumnRegion` (`{column}`); columns and positions count from 1. `MARKDOWN_EXPLORER_LABEL_TEMPLATES` lists each template's placeholders, and braces in every other label are literal text.
38
+ - `labels` must be a plain object. An unknown key throws a `RangeError` that lists the valid keys, a value of the wrong type (a string where a function is expected, or the reverse) throws a `TypeError`, and a blank string throws a `RangeError`, because a blank name is neither shown nor announced. A template that uses a placeholder its key does not offer (`{name}` for `{title}`, say) throws a `RangeError` naming the placeholders it accepts, because a placeholder that is never filled would be read out as it is. A key set to `undefined` keeps the catalog's string.
39
+ - The overrides are compared by value, key by key: writing `labels={{ … }}` inline does not re-render the explorer on every render of the host, but a function created inline is a new value each time — hoist it, or memoize the object.
40
+
41
+ ## Formatting
42
+
43
+ Counts and percentages are formatted by the label functions themselves, so a host that needs grouping or another numeral system overrides the function. The tree keeps the order its source returns unless the configuration's `defaultTreeSettings` sets a sort order or the reader picks one in the tree settings; sorted entries are compared with `Intl.Collator("en", { numeric: true, sensitivity: "variant" })` — numeric-aware and independent of the UI language, so a source sorts the same for every reader.
44
+
45
+ ## Strings outside the catalog
46
+
47
+ - Names of sources (`label`), notices and document styles come from the host and are shown as given.
48
+ - The empty-list and horizontal-scroll strings of `@aiquants/directory-tree` are never shown by the explorer, so the catalog has no keys for them.