@aiquants/markdown-explorer 0.1.0 → 0.3.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.
- package/CHANGELOG.md +53 -1
- package/README.md +53 -16
- package/dist/{ExplorerSplit-DwEFmvQM.js → ExplorerSplit-B7pmzR8t.js} +3 -3
- package/dist/{ExplorerSplit-BESGFcu6.cjs → ExplorerSplit-Bi3yU6NG.cjs} +1 -1
- package/dist/MultiLayout-BxeXOLX1.js +909 -0
- package/dist/MultiLayout-DtFrFRmS.cjs +1 -0
- package/dist/TreeLayout-BQUfVNlA.cjs +1 -0
- package/dist/TreeLayout-D6hCSihW.js +22 -0
- package/dist/core/config.d.cts +8 -0
- package/dist/core/config.d.ts +8 -0
- package/dist/core/labels.d.cts +6 -1
- package/dist/core/labels.d.ts +6 -1
- package/dist/{dataRoute-CKbdwkQ7.cjs → dataRoute-B9Wk-G7A.cjs} +1 -1
- package/dist/{dataRoute-Bkfwfp1R.js → dataRoute-zUX-oR9j.js} +39 -38
- package/dist/{index-BgkZ9ATV.js → index-BJTmp0lR.js} +947 -901
- package/dist/index-DkiGlM7C.cjs +3 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +13 -13
- package/dist/location-DfYTzfFS.cjs +1 -0
- package/dist/{location-Cb2Ak55B.js → location-V5BKeWNs.js} +189 -162
- package/dist/sanitizeWorker.cjs +1 -1
- package/dist/sanitizeWorker.js +196 -206
- package/dist/server/anonymous.d.cts +2 -2
- package/dist/server/anonymous.d.ts +2 -2
- package/dist/server/ports.d.cts +4 -1
- package/dist/server/ports.d.ts +4 -1
- package/dist/server.cjs +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +631 -603
- package/dist/styles/markdown-explorer.css +1 -1
- package/docs/behavior/dom-hooks.md +4 -4
- package/docs/behavior/layout.md +21 -9
- package/docs/behavior/localization.md +4 -3
- package/docs/behavior/security.md +23 -5
- package/docs/behavior/url-contract.md +8 -2
- package/package.json +5 -5
- package/dist/MultiLayout-DxPiARsv.js +0 -896
- package/dist/MultiLayout-r_qx1Mfz.cjs +0 -1
- package/dist/TreeLayout-9-20bbSM.cjs +0 -1
- package/dist/TreeLayout-CW7lgV_e.js +0 -19
- package/dist/index-y9tVvDW7.cjs +0 -3
- package/dist/location-DWHQYntR.cjs +0 -1
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
|
|
2
2
|
/*! @aiquants/markdown-explorer — component styles, layered in `components`. The layer order is pinned first (theme, base, components, utilities, as Tailwind CSS declares it), so a Tailwind host's preflight in `base` cannot override the explorer when this sheet is the first to name a layer. */
|
|
3
|
-
@layer theme,base;@layer components{.aqmx-root{--aqmx-space-1:.0625rem;--aqmx-space-2:.125rem;--aqmx-space-3:.1875rem;--aqmx-space-4:.3125rem;--aqmx-space-5:.5rem;--aqmx-space-6:.8125rem;--aqmx-space-7:1.3125rem;--aqmx-space-8:2.125rem;--aqmx-space-9:3.4375rem;--aqmx-control:2.125rem;--aqmx-focus-ring-width:var(--aqmx-space-2);--aqmx-focus-ring-offset:var(--aqmx-space-2);--aqmx-icon:1rem;--aqmx-radius-s:.3125rem;--aqmx-radius-m:.5rem;--aqmx-radius-l:.8125rem;--aqmx-duration-1:89ms;--aqmx-duration-2:.144s;--aqmx-duration-3:.233s;--aqmx-duration-4:.377s;--aqmx-delay-progress:.233s;--aqmx-ease:cubic-bezier(.2, 0, 0, 1);--aqmx-font-size-body:.8125rem;--aqmx-font-size-title:1rem;--aqmx-font-size-display:1.3125rem;--aqmx-line-height-compact:1.382;--aqmx-line-height-prose:1.618;--aqmx-column-measure:40ch;--aqmx-surface:#fff;--aqmx-surface-raised:#f8fafc;--aqmx-surface-sunken:#f1f5f9;--aqmx-border:#cbd5e1;--aqmx-border-strong:#94a3b8;--aqmx-text:#0f172a;--aqmx-text-muted:#475569;--aqmx-accent:#1d4ed8;--aqmx-accent-surface:#dbeafe;--aqmx-tree-line:#64748b;--aqmx-tree-row-tint:#0f172a0f;--aqmx-tree-row-accent-tint:#2563eb29;--aqmx-focus:#2563eb;--aqmx-info-surface:#eff6ff;--aqmx-info-border:#93c5fd;--aqmx-info-text:#1e3a8a;--aqmx-warning-surface:#fffbeb;--aqmx-warning-border:#fcd34d;--aqmx-warning-text:#78350f;--aqmx-danger-surface:#fef2f2;--aqmx-danger-border:#fca5a5;--aqmx-danger-text:#7f1d1d;--aqmx-shadow:0 .0625rem .1875rem #0f172a21, 0 .3125rem .8125rem #0f172a14}.aqmx-root[data-aqmx-theme=dark]{--aqmx-surface:#0f172a;--aqmx-surface-raised:#1e293b;--aqmx-surface-sunken:#020617;--aqmx-border:#334155;--aqmx-border-strong:#64748b;--aqmx-text:#e2e8f0;--aqmx-text-muted:#94a3b8;--aqmx-accent:#93c5fd;--aqmx-accent-surface:#1e3a8a;--aqmx-tree-line:#94a3b8;--aqmx-tree-row-tint:#e2e8f00f;--aqmx-tree-row-accent-tint:#2563eb70;--aqmx-focus:#60a5fa;--aqmx-info-surface:#172554;--aqmx-info-border:#1d4ed8;--aqmx-info-text:#bfdbfe;--aqmx-warning-surface:#422006;--aqmx-warning-border:#a16207;--aqmx-warning-text:#fde68a;--aqmx-danger-surface:#450a0a;--aqmx-danger-border:#b91c1c;--aqmx-danger-text:#fecaca;--aqmx-shadow:0 .0625rem .1875rem #0000008c, 0 .3125rem .8125rem #00000057}@media (pointer:coarse){.aqmx-root{--aqmx-control:2.75rem}}@media (forced-colors:active){.aqmx-root.aqmx-root[data-aqmx-theme]{--aqmx-tree-line:CanvasText}}@media (prefers-reduced-motion:reduce){.aqmx-root{--aqmx-duration-1:0s;--aqmx-duration-2:0s;--aqmx-duration-3:0s;--aqmx-duration-4:0s}}.aqmx-root{block-size:100%;min-block-size:0;inline-size:100%;min-inline-size:0;color:var(--aqmx-text);background:var(--aqmx-surface);font-size:var(--aqmx-font-size-body);line-height:var(--aqmx-line-height-compact);flex-direction:column;display:flex;position:relative}.aqmx-root *,.aqmx-root :before,.aqmx-root :after{box-sizing:border-box}.aqmx-icon-button{inline-size:var(--aqmx-control);block-size:var(--aqmx-control);border-radius:var(--aqmx-radius-m);color:var(--aqmx-text-muted);cursor:pointer;transition:background-color var(--aqmx-duration-2) var(--aqmx-ease), color var(--aqmx-duration-2) var(--aqmx-ease);background:0 0;border:0;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.aqmx-icon-button:hover{color:var(--aqmx-text);background:var(--aqmx-surface-sunken)}.aqmx-icon-button[aria-pressed=true],.aqmx-icon-button[aria-expanded=true]{color:var(--aqmx-accent);background:var(--aqmx-accent-surface)}.aqmx-icon-button:disabled{opacity:.382;cursor:not-allowed}.aqmx-icon-button[aria-disabled=true]{cursor:not-allowed}.aqmx-icon-button[aria-disabled=true]:hover{color:var(--aqmx-text-muted);background:0 0}.aqmx-icon-button>svg{inline-size:var(--aqmx-icon);block-size:var(--aqmx-icon)}.aqmx-icon-button[aria-disabled=true]>svg{opacity:.382}.aqmx-root :focus-visible{outline:var(--aqmx-focus-ring-width) solid var(--aqmx-focus);outline-offset:var(--aqmx-focus-ring-offset)}.aqmx-callout{gap:var(--aqmx-space-5);padding:var(--aqmx-space-6);border:var(--aqmx-space-1) solid var(--aqmx-info-border);border-radius:var(--aqmx-radius-l);color:var(--aqmx-info-text);background:var(--aqmx-info-surface);line-height:var(--aqmx-line-height-prose);display:grid}.aqmx-callout[data-aqmx-tone=warning]{border-color:var(--aqmx-warning-border);color:var(--aqmx-warning-text);background:var(--aqmx-warning-surface)}.aqmx-callout[data-aqmx-tone=danger]{border-color:var(--aqmx-danger-border);color:var(--aqmx-danger-text);background:var(--aqmx-danger-surface)}.aqmx-callout-title{font-size:var(--aqmx-font-size-title);font-weight:600;line-height:var(--aqmx-line-height-compact);margin:0}.aqmx-callout-body{margin:0}.aqmx-callout-action{min-block-size:var(--aqmx-control);padding-inline:var(--aqmx-space-6);border:var(--aqmx-space-1) solid currentColor;border-radius:var(--aqmx-radius-m);color:inherit;font:inherit;cursor:pointer;background:0 0;justify-self:start;align-items:center;font-weight:600;text-decoration:none;display:inline-flex}.aqmx-progress{z-index:2;block-size:var(--aqmx-space-3);pointer-events:none;position:absolute;inset-block-start:0;inset-inline:0;overflow:hidden}.aqmx-progress:before{content:"";background:var(--aqmx-accent);inline-size:38.1966%;animation:aqmx-progress calc(var(--aqmx-duration-4) * 3) var(--aqmx-ease) infinite;position:absolute;inset-block:0}@keyframes aqmx-progress{0%{inset-inline-start:-38.1966%}to{inset-inline-start:100%}}.aqmx-empty{place-content:center;justify-items:center;gap:var(--aqmx-space-6);block-size:100%;padding:var(--aqmx-space-8);color:var(--aqmx-text-muted);text-align:center;line-height:var(--aqmx-line-height-prose);display:grid}.aqmx-empty-title{color:var(--aqmx-text);font-size:var(--aqmx-font-size-title);margin:0;font-weight:600}.aqmx-skeleton{gap:var(--aqmx-space-6);padding:var(--aqmx-space-7);display:grid}.aqmx-skeleton>span{block-size:var(--aqmx-space-6);border-radius:var(--aqmx-radius-s);background:var(--aqmx-surface-sunken);animation:aqmx-pulse calc(var(--aqmx-duration-4) * 4) ease-in-out infinite;display:block}.aqmx-skeleton>span:nth-child(3n+1){inline-size:61.8034%}.aqmx-skeleton>span:nth-child(3n+2){inline-size:100%}.aqmx-skeleton>span:nth-child(3n){inline-size:85.4102%}@keyframes aqmx-pulse{50%{opacity:.618}}.aqmx-sr-only{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;block-size:1px;inline-size:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.aqmx-toast{z-index:60;max-inline-size:min(34rem, calc(100% - var(--aqmx-space-8)));padding:var(--aqmx-space-5) var(--aqmx-space-6);border:var(--aqmx-space-1) solid var(--aqmx-warning-border);border-radius:var(--aqmx-radius-m);color:var(--aqmx-warning-text);background:var(--aqmx-warning-surface);box-shadow:var(--aqmx-shadow);position:absolute;inset-block-start:var(--aqmx-space-6);inset-inline-end:var(--aqmx-space-7)}@media (prefers-reduced-motion:reduce){.aqmx-progress:before,.aqmx-skeleton>span{animation:none}}@media (forced-colors:active){.aqmx-icon-button[aria-pressed=true],.aqmx-icon-button[aria-expanded=true]{forced-color-adjust:none;color:highlighttext;background:highlight}.aqmx-icon-button[aria-disabled=true]{color:graytext}.aqmx-root :focus-visible{outline-color:highlight}.aqmx-progress:before{forced-color-adjust:none;background:highlight}.aqmx-skeleton>span{forced-color-adjust:none;background:graytext}}.aqmx-explorer{gap:var(--aqmx-space-5);block-size:100%;min-block-size:0;min-inline-size:0;padding:var(--aqmx-space-4);color:var(--aqmx-text);background:var(--aqmx-surface);flex-direction:column;display:flex;position:relative;overflow-y:auto}.aqmx-explorer-header{align-items:center;gap:var(--aqmx-space-4);min-block-size:var(--aqmx-control);flex-wrap:wrap;flex:none;display:flex}.aqmx-explorer-source{min-inline-size:min(100%,9rem);block-size:var(--aqmx-control);padding-inline:var(--aqmx-space-5);border:var(--aqmx-space-1) solid var(--aqmx-border);border-radius:var(--aqmx-radius-m);color:var(--aqmx-text);background:var(--aqmx-surface-raised);font:inherit;text-overflow:ellipsis;cursor:pointer;flex:9rem}.aqmx-explorer-actions{justify-content:flex-end;align-items:center;gap:var(--aqmx-space-2);flex-wrap:wrap;flex:0 auto;margin-inline-start:auto;display:flex}.aqmx-explorer-body{gap:var(--aqmx-space-5);flex-direction:column;flex:1 1 0;min-block-size:0;min-inline-size:0;display:flex;position:relative}.aqmx-labelled-button{gap:var(--aqmx-space-2);inline-size:auto;padding-inline:var(--aqmx-space-3);white-space:nowrap;display:inline-flex}.aqmx-explorer-refresh-failure,.aqmx-explorer-expand-all-stop{padding:var(--aqmx-space-4) var(--aqmx-space-5);border:var(--aqmx-space-1) solid;border-radius:var(--aqmx-radius-m);line-height:var(--aqmx-line-height-compact);overflow-wrap:anywhere;flex:none;margin:0}.aqmx-explorer-refresh-failure{border-color:var(--aqmx-danger-border);color:var(--aqmx-danger-text);background:var(--aqmx-danger-surface)}.aqmx-explorer-expand-all-stop{border-color:var(--aqmx-info-border);color:var(--aqmx-info-text);background:var(--aqmx-info-surface)}.aqmx-explorer-actions>.aqmx-labelled-button[data-aqmx-busy=true]>svg{animation:aqmx-explorer-spin calc(var(--aqmx-duration-4) * 3) linear infinite}@keyframes aqmx-explorer-spin{to{rotate:1turn}}.aqmx-explorer-actions>.aqmx-icon-button[aria-busy=true]{cursor:progress}.aqmx-explorer-menu{z-index:50;gap:var(--aqmx-space-1);max-block-size:100%;inline-size:min(21rem,100%);padding:var(--aqmx-space-3);border:var(--aqmx-space-1) solid var(--aqmx-border);border-radius:var(--aqmx-radius-m);background:var(--aqmx-surface-raised);box-shadow:var(--aqmx-shadow);display:grid;position:absolute;inset-block-start:0;inset-inline-end:0;overflow-y:auto}.aqmx-explorer-menu-item{align-items:center;gap:var(--aqmx-space-5);inline-size:100%;min-block-size:var(--aqmx-control);padding-inline:var(--aqmx-space-5);border-radius:var(--aqmx-radius-s);color:var(--aqmx-text);font:inherit;text-align:start;cursor:pointer;transition:background-color var(--aqmx-duration-1) var(--aqmx-ease);background:0 0;border:0;display:flex}.aqmx-explorer-menu-item:hover,.aqmx-explorer-menu-item:focus-visible{background:var(--aqmx-surface-sunken)}.aqmx-explorer-menu-icon{inline-size:var(--aqmx-icon);block-size:var(--aqmx-icon);color:var(--aqmx-text-muted);flex:none}.aqmx-explorer-menu-name{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-inline-size:0;overflow:hidden}.aqmx-explorer-menu-value{color:var(--aqmx-accent);font-variant-numeric:tabular-nums;flex:none;font-weight:600}.aqmx-explorer-notices{gap:var(--aqmx-space-5);flex:none;display:grid}.aqmx-explorer-tree-area{min-block-size:calc(var(--aqmx-control) * 3);flex-direction:column;flex:1 1 0;min-inline-size:0;display:flex;position:relative}.aqmx-explorer-tree{flex:1 1 0;min-block-size:0;position:relative}.aqmx-explorer-tree-view{block-size:100%}.aqmx-explorer-tree-progress{z-index:1;align-items:center;gap:var(--aqmx-space-2) var(--aqmx-space-4);max-inline-size:calc(100% - var(--aqmx-space-5) * 3);padding:var(--aqmx-space-3) var(--aqmx-space-5);border:var(--aqmx-space-1) solid var(--aqmx-border);border-radius:var(--aqmx-radius-m);color:var(--aqmx-text-muted);background:var(--aqmx-surface-raised);box-shadow:var(--aqmx-shadow);line-height:var(--aqmx-line-height-compact);overflow-wrap:anywhere;pointer-events:none;animation:aqmx-appear var(--aqmx-duration-2) ease-out var(--aqmx-delay-progress) both;flex-wrap:wrap;margin:0;display:flex;position:absolute;inset-block-end:var(--aqmx-space-9);inset-inline-start:var(--aqmx-space-5)}.aqmx-explorer-tree-progress-icon{inline-size:var(--aqmx-icon);block-size:var(--aqmx-icon);animation:aqmx-explorer-spin calc(var(--aqmx-duration-4) * 3) linear infinite;flex:none}@keyframes aqmx-appear{0%{opacity:0}}.aqmx-explorer-tree-probe{inline-size:0;block-size:var(--aqmx-control);visibility:hidden;pointer-events:none;color:var(--aqmx-tree-line);forced-color-adjust:none;position:absolute;inset-block-start:0;inset-inline-start:0;overflow:hidden}.aqmx-explorer-loading{gap:var(--aqmx-space-5);padding:var(--aqmx-space-6) var(--aqmx-space-5);color:var(--aqmx-text-muted);display:grid}.aqmx-explorer-loading-text{margin:0}.aqmx-explorer-loading>.aqmx-skeleton{padding:0}.aqmx-explorer-tree-message{padding:var(--aqmx-space-5);overflow-y:auto}.aqmx-explorer-empty-icon{inline-size:var(--aqmx-space-8);block-size:var(--aqmx-space-8);color:var(--aqmx-border-strong)}.aqmx-explorer-empty-body{margin:0}.aqmx-explorer-tree [role=treeitem]{border-radius:var(--aqmx-radius-s);color:var(--aqmx-text);transition:background-color var(--aqmx-duration-1) var(--aqmx-ease)}.aqmx-explorer-tree [role=treeitem]:hover{color:var(--aqmx-text);background:var(--aqmx-tree-row-tint)}.aqmx-explorer-tree [role=treeitem][data-entry-item-selected=true]{color:var(--aqmx-text);background:var(--aqmx-tree-row-tint);box-shadow:inset var(--aqmx-space-2) 0 0 currentColor}.aqmx-explorer-tree [role=treeitem][data-entry-selected=true],.aqmx-explorer-tree [role=treeitem][data-aqmx-open=true]{color:var(--aqmx-accent);background:var(--aqmx-tree-row-accent-tint)}.aqmx-explorer-tree [role=treeitem]:focus-visible{outline-offset:calc(-1 * var(--aqmx-focus-ring-width))}.aqmx-explorer-tree [role=treeitem][data-entry-item-selected=true]:focus-visible{box-shadow:inset calc(var(--aqmx-space-2) + var(--aqmx-focus-ring-width)) 0 0 currentColor}.aqmx-explorer-tree [role=treeitem][data-aqmx-open=true]{font-weight:600}.aqmx-explorer-tree [role=treeitem][data-aqmx-children=loading]{animation:aqmx-pulse calc(var(--aqmx-duration-4) * 4) ease-in-out infinite}.aqmx-explorer-tree [role=treeitem][data-aqmx-loop]{color:var(--aqmx-text-muted);font-style:italic}.aqmx-explorer-folder-failed{--aqmx-folder-name-floor:3em;align-items:center;min-inline-size:0;max-inline-size:100%;display:flex}.aqmx-explorer-folder-name{flex:10000 1 var(--aqmx-folder-name-floor);text-overflow:ellipsis;white-space:nowrap;min-inline-size:0;max-inline-size:max-content;overflow:hidden}.aqmx-explorer-folder-error{min-inline-size:0;color:var(--aqmx-danger-text);text-indent:var(--aqmx-space-4);text-overflow:ellipsis;white-space:nowrap;flex:1 1 0;overflow:hidden}.aqmx-explorer-folder-retry{flex:1000000 0 calc(var(--aqmx-icon) + (var(--aqmx-space-3) + var(--aqmx-space-1)) * 2);min-inline-size:calc(var(--aqmx-icon) + (var(--aqmx-space-3) + var(--aqmx-space-1)) * 2);max-inline-size:max-content;min-block-size:var(--aqmx-control);padding:0 var(--aqmx-space-3);border:var(--aqmx-space-1) solid var(--aqmx-danger-border);border-radius:var(--aqmx-radius-s);color:var(--aqmx-danger-text);background:var(--aqmx-danger-surface);font:inherit;line-height:var(--aqmx-line-height-compact);cursor:pointer;justify-content:center;align-items:center;margin-inline-start:var(--aqmx-space-4);display:inline-flex;overflow:hidden}.aqmx-explorer-folder-retry:focus-visible{outline-offset:calc(-1 * (var(--aqmx-focus-ring-width) + var(--aqmx-space-1)))}.aqmx-explorer-folder-retry-icon{inline-size:var(--aqmx-icon);block-size:var(--aqmx-icon);flex:none}.aqmx-explorer-folder-retry-text{min-inline-size:0;text-indent:var(--aqmx-space-3);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.aqmx-explorer-tray{gap:var(--aqmx-space-4);min-block-size:calc(var(--aqmx-control) + (var(--aqmx-space-5) + var(--aqmx-space-1)) * 2);padding:var(--aqmx-space-5);border:var(--aqmx-space-1) solid var(--aqmx-border);border-radius:var(--aqmx-radius-m);background:var(--aqmx-surface-raised);box-shadow:var(--aqmx-shadow);flex-direction:column;flex:0 auto;display:flex;overflow-y:auto}.aqmx-explorer-tray-header{justify-content:space-between;align-items:center;gap:var(--aqmx-space-5);flex:none;display:flex}.aqmx-explorer-tray-title{margin:0;font-weight:600}.aqmx-explorer-tray-button{min-block-size:var(--aqmx-control);padding-inline:var(--aqmx-space-5);border-radius:var(--aqmx-radius-m);color:var(--aqmx-accent);font:inherit;cursor:pointer;background:0 0;border:0;flex:none;font-weight:600}.aqmx-explorer-tray-button:hover{background:var(--aqmx-surface-sunken)}.aqmx-explorer-tray-list{gap:var(--aqmx-space-1);min-block-size:var(--aqmx-control);max-block-size:calc(var(--aqmx-control) * 3);flex:0 auto;margin:0;padding:0;list-style:none;display:grid;overflow-y:auto}.aqmx-explorer-tray-item{align-items:center;gap:var(--aqmx-space-4);display:flex}.aqmx-explorer-tray-name{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-inline-size:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden}.aqmx-explorer-tray-open{min-block-size:var(--aqmx-control);padding-inline:var(--aqmx-space-6);border-radius:var(--aqmx-radius-m);color:var(--aqmx-surface);background:var(--aqmx-accent);font:inherit;cursor:pointer;border:0;flex:none;font-weight:600}@media (forced-colors:active){.aqmx-explorer-tree [role=treeitem][data-entry-item-selected=true],.aqmx-explorer-tree [role=treeitem][data-entry-selected=true],.aqmx-explorer-tree [role=treeitem][data-aqmx-open=true],.aqmx-explorer-menu-item[aria-checked=true]{forced-color-adjust:none;color:highlighttext;background:highlight}.aqmx-explorer-menu-item[aria-checked=true] *,.aqmx-explorer-tree [role=treeitem][data-entry-item-selected=true] *,.aqmx-explorer-tree [role=treeitem][data-entry-selected=true] *,.aqmx-explorer-tree [role=treeitem][data-aqmx-open=true] *{color:inherit!important}.aqmx-explorer-tree [role=treeitem][data-entry-item-selected=true]:focus-visible,.aqmx-explorer-tree [role=treeitem][data-entry-selected=true]:focus-visible,.aqmx-explorer-tree [role=treeitem][data-aqmx-open=true]:focus-visible{outline-color:highlighttext}.aqmx-explorer-tray-button,.aqmx-explorer-tray-open,.aqmx-explorer-tray-remove{border:var(--aqmx-space-1) solid ButtonText}}@media (prefers-reduced-motion:reduce){.aqmx-explorer-actions>.aqmx-labelled-button[data-aqmx-busy=true]>svg,.aqmx-explorer-tree-progress-icon,.aqmx-explorer-tree [role=treeitem][data-aqmx-children=loading]{animation:none}.aqmx-explorer-tree-view{transition-duration:0s!important}}.aqmx-document{contain:layout paint;--aqmd-toolbar-inset-block-start:calc(var(--aqmx-focus-ring-width) + var(--aqmx-focus-ring-offset));block-size:100%;min-block-size:0;inline-size:100%;min-inline-size:0;color:var(--aqmx-text);background:var(--aqmx-surface);flex-direction:column;display:flex;position:relative;overflow:hidden}.aqmx-document [data-theme]{background-color:#0000!important}.aqmx-document .markdown-body :is(sup,sub) a{outline-offset:var(--aqmx-focus-ring-offset)!important}.aqmx-document *{scroll-margin:calc(var(--aqmx-focus-ring-width) + var(--aqmx-focus-ring-offset))}.aqmx-document[aria-busy=true]>:not(.aqmx-progress){opacity:.618;transition:opacity var(--aqmx-duration-3) var(--aqmx-ease)}.aqmx-document-viewer{flex:1 1 0;min-block-size:0;min-inline-size:0}.aqmx-document-notice{margin:var(--aqmx-space-6) var(--aqmx-space-7) 0;overflow-wrap:anywhere;flex:none}.aqmx-document-failure{min-block-size:0;padding:var(--aqmx-space-7);flex:1 1 0;overflow:auto}.aqmx-document-file{flex-direction:column;flex:1 1 0;min-block-size:0;min-inline-size:0;display:flex}.aqmx-document-toolbar{align-items:center;gap:var(--aqmx-space-3);padding:var(--aqmx-space-3) var(--aqmx-space-6);border-block-end:var(--aqmx-space-1) solid var(--aqmx-border);background:var(--aqmx-surface-raised);flex-wrap:wrap;flex:none;display:flex}.aqmx-document-file-icon{color:var(--aqmx-text-muted);flex:none;display:inline-flex}.aqmx-document-file-icon>svg{inline-size:var(--aqmx-icon);block-size:var(--aqmx-icon)}.aqmx-document-title{text-overflow:ellipsis;white-space:nowrap;flex:1 1 0;min-inline-size:0;font-weight:600;overflow:hidden}.aqmx-document-level{min-inline-size:var(--aqmx-space-9);color:var(--aqmx-text-muted);font-variant-numeric:tabular-nums;text-align:center;flex:none}.aqmx-document-canvas{min-block-size:0;padding:var(--aqmx-space-7);background-color:var(--aqmx-surface-sunken);background-image:radial-gradient(var(--aqmx-border) var(--aqmx-space-1), transparent var(--aqmx-space-1));background-size:var(--aqmx-space-6) var(--aqmx-space-6);flex:1 1 0;grid-template:minmax(0,1fr)/minmax(0,1fr);display:grid;overflow:auto}.aqmx-document-image{margin:auto;display:block}.aqmx-document-image[data-aqmx-image-mode=fit]{object-fit:contain;max-block-size:100%;max-inline-size:100%}.aqmx-document-image[data-aqmx-image-mode=level]{max-block-size:none;max-inline-size:none}.aqmx-document-stage{min-block-size:0;padding:var(--aqmx-space-7);background:var(--aqmx-surface-sunken);flex:1 1 0;grid-template:minmax(0,1fr)/minmax(0,1fr);place-items:center;display:grid;overflow:auto}.aqmx-document-file[data-aqmx-media-type=pdf]>.aqmx-document-stage{place-items:stretch stretch;padding:0}.aqmx-document-video{border-radius:var(--aqmx-radius-l);background:var(--aqmx-surface-raised);max-block-size:100%;max-inline-size:100%;box-shadow:var(--aqmx-shadow)}.aqmx-document-audio{inline-size:min(100%, var(--aqmx-column-measure))}.aqmx-document-frame{background:var(--aqmx-surface);border:0;block-size:100%;inline-size:100%}.aqmx-document-inert-link{color:var(--aqmx-text-muted);cursor:not-allowed;text-decoration-style:dotted}.aqmx-document[data-aqmx-document-flow=content]{contain:layout;block-size:auto;overflow:visible}.aqmx-document[data-aqmx-document-flow=content]>*,.aqmx-document[data-aqmx-document-flow=content] .aqmx-document-file>*{flex:none}.aqmx-document[data-aqmx-document-flow=content] .aqmx-document-frame{aspect-ratio:1/1.41421;block-size:auto}.aqmx-multi-stage{--aqmx-multi-panel-body-floor:calc(3rem * var(--aqmx-line-height-prose));--aqdd-stage-padding-block:var(--aqmx-space-7);--aqdd-stage-padding-inline:var(--aqmx-space-6);--aqdd-column-gap:var(--aqmx-space-6);--aqdd-panel-surface:var(--aqmx-surface);--aqdd-panel-border:var(--aqmx-border);--aqdd-panel-title-color:var(--aqmx-text);--aqdd-header-button-color:var(--aqmx-text-muted);--aqdd-header-button-hover-color:var(--aqmx-text);--aqdd-header-button-hover-surface:var(--aqmx-surface-sunken);--aqdd-custom-badge-color:var(--aqmx-warning-text);--aqdd-custom-badge-surface:var(--aqmx-warning-surface);--aqdd-restore-button-color:var(--aqmx-surface);--aqdd-restore-button-surface:var(--aqmx-accent);--aqdd-restore-button-hover-surface:var(--aqmx-info-text);--aqdd-drop-placeholder-color:var(--aqmx-accent);--aqdd-drop-placeholder-surface:var(--aqmx-surface);--aqdd-drop-placeholder-target-surface:var(--aqmx-accent-surface);background:var(--aqmx-surface-sunken);block-size:100%;min-block-size:0;min-inline-size:0;color:var(--aqmx-text);flex-direction:column;display:flex;position:relative}.aqmx-multi-stage:focus-visible{outline:var(--aqmx-focus-ring-width) solid var(--aqmx-focus);outline-offset:calc(-1 * var(--aqmx-focus-ring-width))}.aqmx-multi-probe{inline-size:var(--aqmx-column-measure);visibility:hidden;pointer-events:none;block-size:0;font-size:1rem;position:absolute;inset-block-start:0;inset-inline-start:0;overflow:hidden}.aqmx-multi-layout{flex:auto;min-block-size:0}.aqmx-multi-skeleton{grid-template-columns:repeat(auto-fit, minmax(min(100%, var(--aqmx-column-measure)), 1fr));align-content:start;gap:var(--aqmx-space-7);padding:var(--aqmx-space-7);display:grid;overflow:hidden}.aqmx-multi-skeleton-column{border:var(--aqmx-space-1) solid var(--aqmx-border);border-radius:var(--aqmx-radius-l);background:var(--aqmx-surface)}.aqmx-multi-column{min-inline-size:0}.aqmx-multi-panel{outline:var(--aqmx-space-1) solid var(--aqmx-border-strong);outline-offset:0;color:var(--aqmx-text)}.aqmx-multi-panel[data-maximized]{outline-color:var(--aqmx-focus)}.aqmx-multi-panel-frame{content-visibility:auto;contain-intrinsic-block-size:auto var(--aqmx-space-9);min-block-size:100%}.aqmx-multi-panel-frame:focus-visible{outline:var(--aqmx-focus-ring-width) solid var(--aqmx-focus);outline-offset:calc(-1 * var(--aqmx-focus-ring-width))}.aqmx-multi-panel-frame[data-aqmx-maximized=false]{-webkit-user-select:none;user-select:none}.aqmx-multi-panel-frame .aqmx-document-toolbar>.aqmx-icon-button{flex-shrink:1;min-inline-size:0}.aqmx-multi-toast{animation:aqmx-multi-toast-in var(--aqmx-duration-3) var(--aqmx-ease)}.aqmx-multi-toast:popover-open{max-inline-size:min(34rem, calc(var(--aqmx-multi-toast-room) - var(--aqmx-space-8)));margin:0;position:fixed;inset:auto;inset-block-start:calc(var(--aqmx-multi-toast-block-start) + var(--aqmx-space-6));inset-inline-end:calc(var(--aqmx-multi-toast-inline-end) + var(--aqmx-space-7));overflow:visible}@keyframes aqmx-multi-toast-in{0%{opacity:0;translate:0 calc(var(--aqmx-space-5) * -1)}}@media (prefers-reduced-motion:reduce){.aqmx-multi-toast{animation:none}}@media (forced-colors:active){.aqmx-multi-stage:focus-visible,.aqmx-multi-stage :focus-visible,.aqmx-multi-panel[data-maximized]{outline-color:highlight}}.aqmx-split-frame{--aqmx-sheet-target:2.75rem;flex-direction:column;flex:auto;min-block-size:0;min-inline-size:0;display:flex;position:relative}.aqmx-rem-probe{visibility:hidden;pointer-events:none;block-size:0;inline-size:1rem;position:absolute;inset-block-start:0;inset-inline-start:0}.aqmx-split{flex:auto;min-block-size:0}.aqmx-split-explorer,.aqmx-split-content{background:var(--aqmx-surface);min-block-size:0}.aqmx-split-explorer{background:var(--aqmx-surface-raised)}.aqmx-split-handle{background:var(--aqmx-surface-raised);transition:background-color var(--aqmx-duration-2) var(--aqmx-ease)}.aqmx-split-handle:hover,.aqmx-split-handle:focus-visible{background:var(--aqmx-surface-sunken)}.aqmx-sheet-bar{padding:var(--aqmx-space-3) var(--aqmx-space-5);border-block-end:var(--aqmx-space-1) solid var(--aqmx-border);background:var(--aqmx-surface-raised);flex:none;align-items:center;display:flex}.aqmx-sheet-toggle,.aqmx-sheet-close{justify-content:center;align-items:center;gap:var(--aqmx-space-5);min-inline-size:var(--aqmx-sheet-target);min-block-size:var(--aqmx-sheet-target);padding:0 var(--aqmx-space-6);border:var(--aqmx-space-1) solid var(--aqmx-border);border-radius:var(--aqmx-radius-m);color:var(--aqmx-text);background:var(--aqmx-surface);font:inherit;cursor:pointer;transition:background-color var(--aqmx-duration-2) var(--aqmx-ease);display:inline-flex}.aqmx-sheet-close{z-index:1;color:var(--aqmx-text-muted);background:var(--aqmx-surface-raised);border-color:#0000;padding:0;position:relative}.aqmx-sheet-toggle:hover,.aqmx-sheet-close:hover{background:var(--aqmx-surface-sunken)}.aqmx-sheet-toggle>svg,.aqmx-sheet-close>svg{inline-size:var(--aqmx-icon);block-size:var(--aqmx-icon);flex:none}.aqmx-sheet{inline-size:min(calc(100vw - var(--aqmx-space-9)), 21rem);border:0;border-inline-end:var(--aqmx-space-1) solid var(--aqmx-border);block-size:100dvh;max-block-size:none;max-inline-size:none;color:var(--aqmx-text);background:var(--aqmx-surface-raised);box-shadow:var(--aqmx-shadow);margin:0;padding:0;position:fixed;inset-block:0;inset-inline:0 auto}.aqmx-sheet::backdrop{background:#0f172a61}.aqmx-sheet-panel{flex-direction:column;block-size:100%;display:flex}.aqmx-sheet-header{z-index:1;justify-content:flex-end;align-items:center;gap:var(--aqmx-space-3);padding:var(--aqmx-space-3);border-block-end:var(--aqmx-space-1) solid var(--aqmx-border);flex:none;display:flex;position:relative}.aqmx-sheet-notice-slot>.aqmx-toast{--aqmx-sheet-notice-band:calc(var(--aqmx-sheet-target) + var(--aqmx-space-3) * 2);--aqmx-sheet-notice-line:calc(var(--aqmx-line-height-compact) * 1em);--aqmx-sheet-notice-inset:calc((var(--aqmx-sheet-notice-band) - var(--aqmx-sheet-notice-line)) / 2);z-index:0;max-inline-size:none;padding-block:var(--aqmx-sheet-notice-inset) 0;padding-inline:var(--aqmx-space-6) var(--aqmx-space-3);border:0;border-block-end:var(--aqmx-space-1) solid var(--aqmx-warning-border);line-height:var(--aqmx-line-height-compact);overflow-wrap:anywhere;box-shadow:none;border-radius:0;position:absolute;inset-block:0 auto;inset-inline:0}.aqmx-sheet-notice-slot>.aqmx-toast:before{content:"";float:inline-end;inline-size:calc(var(--aqmx-sheet-target) + var(--aqmx-space-3));block-size:calc(var(--aqmx-sheet-notice-band) - var(--aqmx-sheet-notice-inset) - var(--aqmx-space-1))}.aqmx-sheet-notice-slot>.aqmx-toast:after{content:"";block-size:calc(var(--aqmx-sheet-notice-inset) - var(--aqmx-space-1));display:block}.aqmx-sheet-body{isolation:isolate;flex-direction:column;flex:auto;min-block-size:0;display:flex;position:relative;overflow:hidden}.aqmx-single{flex-direction:column;flex:auto;min-block-size:0;display:flex;position:relative}.aqmx-single-toolbar{padding:var(--aqmx-space-2) var(--aqmx-space-3);flex:none;justify-content:flex-end;align-items:center;display:flex}.aqmx-single>.aqmx-document{flex:1 1 0;block-size:auto;min-block-size:0}.aqmx-single-refresh-failure{margin:0 var(--aqmx-space-3) var(--aqmx-space-2);padding:var(--aqmx-space-4) var(--aqmx-space-5);border:var(--aqmx-space-1) solid var(--aqmx-danger-border);border-radius:var(--aqmx-radius-m);color:var(--aqmx-danger-text);background:var(--aqmx-danger-surface);line-height:var(--aqmx-line-height-compact);overflow-wrap:anywhere;flex:none}.aqmx-single-toolbar>.aqmx-icon-button[data-aqmx-busy=true]>svg{animation:aqmx-explorer-spin calc(var(--aqmx-duration-4) * 3) linear infinite}.aqmx-single-toolbar>.aqmx-icon-button[aria-busy=true]{cursor:progress}.aqmx-split-fallback,.aqmx-stage-fallback{background:var(--aqmx-surface-sunken);flex:auto;min-block-size:0}@media (forced-colors:active){.aqmx-sheet-toggle[aria-expanded=true]{forced-color-adjust:none;color:highlighttext;background:highlight}.aqmx-sheet-toggle:focus-visible,.aqmx-sheet-close:focus-visible{outline-color:highlight}.aqmx-split-handle:focus-visible{outline:var(--aqmx-focus-ring-width) solid Highlight!important;outline-offset:calc(-1 * var(--aqmx-focus-ring-width))!important}}@media (prefers-reduced-motion:reduce){.aqmx-split-handle,.aqmx-split-handle *{transition-duration:0s!important}.aqmx-single-toolbar>.aqmx-icon-button[data-aqmx-busy=true]>svg{animation:none}}}@layer utilities;
|
|
3
|
+
@layer theme,base;@layer components{.aqmx-root{--aqmx-space-1:.0625rem;--aqmx-space-2:.125rem;--aqmx-space-3:.1875rem;--aqmx-space-4:.3125rem;--aqmx-space-5:.5rem;--aqmx-space-6:.8125rem;--aqmx-space-7:1.3125rem;--aqmx-space-8:2.125rem;--aqmx-space-9:3.4375rem;--aqmx-control:2.125rem;--aqmx-focus-ring-width:var(--aqmx-space-2);--aqmx-focus-ring-offset:var(--aqmx-space-2);--aqmx-icon:1rem;--aqmx-radius-s:.3125rem;--aqmx-radius-m:.5rem;--aqmx-radius-l:.8125rem;--aqmx-duration-1:89ms;--aqmx-duration-2:.144s;--aqmx-duration-3:.233s;--aqmx-duration-4:.377s;--aqmx-delay-progress:.233s;--aqmx-ease:cubic-bezier(.2, 0, 0, 1);--aqmx-font-size-body:.8125rem;--aqmx-font-size-title:1rem;--aqmx-font-size-display:1.3125rem;--aqmx-line-height-compact:1.382;--aqmx-line-height-prose:1.618;--aqmx-column-measure:40ch;--aqmx-surface:#fff;--aqmx-surface-raised:#f8fafc;--aqmx-surface-sunken:#f1f5f9;--aqmx-border:#cbd5e1;--aqmx-border-strong:#94a3b8;--aqmx-text:#0f172a;--aqmx-text-muted:#475569;--aqmx-accent:#1d4ed8;--aqmx-accent-surface:#dbeafe;--aqmx-tree-line:#64748b;--aqmx-tree-row-tint:#0f172a0f;--aqmx-tree-row-accent-tint:#2563eb29;--aqmx-focus:#2563eb;--aqmx-info-surface:#eff6ff;--aqmx-info-border:#93c5fd;--aqmx-info-text:#1e3a8a;--aqmx-warning-surface:#fffbeb;--aqmx-warning-border:#fcd34d;--aqmx-warning-text:#78350f;--aqmx-danger-surface:#fef2f2;--aqmx-danger-border:#fca5a5;--aqmx-danger-text:#7f1d1d;--aqmx-shadow:0 .0625rem .1875rem #0f172a21, 0 .3125rem .8125rem #0f172a14}.aqmx-root[data-aqmx-theme=dark]{--aqmx-surface:#0f172a;--aqmx-surface-raised:#1e293b;--aqmx-surface-sunken:#020617;--aqmx-border:#334155;--aqmx-border-strong:#64748b;--aqmx-text:#e2e8f0;--aqmx-text-muted:#94a3b8;--aqmx-accent:#93c5fd;--aqmx-accent-surface:#1e3a8a;--aqmx-tree-line:#94a3b8;--aqmx-tree-row-tint:#e2e8f00f;--aqmx-tree-row-accent-tint:#2563eb70;--aqmx-focus:#60a5fa;--aqmx-info-surface:#172554;--aqmx-info-border:#1d4ed8;--aqmx-info-text:#bfdbfe;--aqmx-warning-surface:#422006;--aqmx-warning-border:#a16207;--aqmx-warning-text:#fde68a;--aqmx-danger-surface:#450a0a;--aqmx-danger-border:#b91c1c;--aqmx-danger-text:#fecaca;--aqmx-shadow:0 .0625rem .1875rem #0000008c, 0 .3125rem .8125rem #00000057}@media (pointer:coarse){.aqmx-root{--aqmx-control:2.75rem}}@media (forced-colors:active){.aqmx-root.aqmx-root[data-aqmx-theme]{--aqmx-tree-line:CanvasText}}@media (prefers-reduced-motion:reduce){.aqmx-root{--aqmx-duration-1:0s;--aqmx-duration-2:0s;--aqmx-duration-3:0s;--aqmx-duration-4:0s}}.aqmx-root{block-size:100%;min-block-size:0;inline-size:100%;min-inline-size:0;color:var(--aqmx-text);background:var(--aqmx-surface);font-size:var(--aqmx-font-size-body);line-height:var(--aqmx-line-height-compact);flex-direction:column;display:flex;position:relative}.aqmx-root *,.aqmx-root :before,.aqmx-root :after{box-sizing:border-box}.aqmx-icon-button{inline-size:var(--aqmx-control);block-size:var(--aqmx-control);border-radius:var(--aqmx-radius-m);color:var(--aqmx-text-muted);cursor:pointer;transition:background-color var(--aqmx-duration-2) var(--aqmx-ease), color var(--aqmx-duration-2) var(--aqmx-ease);background:0 0;border:0;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.aqmx-icon-button:hover{color:var(--aqmx-text);background:var(--aqmx-surface-sunken)}.aqmx-icon-button[aria-pressed=true],.aqmx-icon-button[aria-expanded=true]{color:var(--aqmx-accent);background:var(--aqmx-accent-surface)}.aqmx-icon-button:disabled{opacity:.382;cursor:not-allowed}.aqmx-icon-button[aria-disabled=true]{cursor:not-allowed}.aqmx-icon-button[aria-disabled=true]:hover{color:var(--aqmx-text-muted);background:0 0}.aqmx-icon-button>svg{inline-size:var(--aqmx-icon);block-size:var(--aqmx-icon)}.aqmx-icon-button[aria-disabled=true]>svg{opacity:.382}.aqmx-root :focus-visible{outline:var(--aqmx-focus-ring-width) solid var(--aqmx-focus);outline-offset:var(--aqmx-focus-ring-offset)}.aqmx-callout{gap:var(--aqmx-space-5);padding:var(--aqmx-space-6);border:var(--aqmx-space-1) solid var(--aqmx-info-border);border-radius:var(--aqmx-radius-l);color:var(--aqmx-info-text);background:var(--aqmx-info-surface);line-height:var(--aqmx-line-height-prose);display:grid}.aqmx-callout[data-aqmx-tone=warning]{border-color:var(--aqmx-warning-border);color:var(--aqmx-warning-text);background:var(--aqmx-warning-surface)}.aqmx-callout[data-aqmx-tone=danger]{border-color:var(--aqmx-danger-border);color:var(--aqmx-danger-text);background:var(--aqmx-danger-surface)}.aqmx-callout-title{font-size:var(--aqmx-font-size-title);font-weight:600;line-height:var(--aqmx-line-height-compact);margin:0}.aqmx-callout-body{margin:0}.aqmx-callout-action{min-block-size:var(--aqmx-control);padding-inline:var(--aqmx-space-6);border:var(--aqmx-space-1) solid currentColor;border-radius:var(--aqmx-radius-m);color:inherit;font:inherit;cursor:pointer;background:0 0;justify-self:start;align-items:center;font-weight:600;text-decoration:none;display:inline-flex}.aqmx-progress{z-index:2;block-size:var(--aqmx-space-3);pointer-events:none;position:absolute;inset-block-start:0;inset-inline:0;overflow:hidden}.aqmx-progress:before{content:"";background:var(--aqmx-accent);inline-size:38.1966%;animation:aqmx-progress calc(var(--aqmx-duration-4) * 3) var(--aqmx-ease) infinite;position:absolute;inset-block:0}@keyframes aqmx-progress{0%{inset-inline-start:-38.1966%}to{inset-inline-start:100%}}.aqmx-empty{place-content:center;justify-items:center;gap:var(--aqmx-space-6);block-size:100%;padding:var(--aqmx-space-8);color:var(--aqmx-text-muted);text-align:center;line-height:var(--aqmx-line-height-prose);display:grid}.aqmx-empty-title{color:var(--aqmx-text);font-size:var(--aqmx-font-size-title);margin:0;font-weight:600}.aqmx-skeleton{gap:var(--aqmx-space-6);padding:var(--aqmx-space-7);display:grid}.aqmx-skeleton>span{block-size:var(--aqmx-space-6);border-radius:var(--aqmx-radius-s);background:var(--aqmx-surface-sunken);animation:aqmx-pulse calc(var(--aqmx-duration-4) * 4) ease-in-out infinite;display:block}.aqmx-skeleton>span:nth-child(3n+1){inline-size:61.8034%}.aqmx-skeleton>span:nth-child(3n+2){inline-size:100%}.aqmx-skeleton>span:nth-child(3n){inline-size:85.4102%}@keyframes aqmx-pulse{50%{opacity:.618}}.aqmx-sr-only{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;block-size:1px;inline-size:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.aqmx-toast{z-index:60;max-inline-size:min(34rem, calc(100% - var(--aqmx-space-8)));padding:var(--aqmx-space-5) var(--aqmx-space-6);border:var(--aqmx-space-1) solid var(--aqmx-warning-border);border-radius:var(--aqmx-radius-m);color:var(--aqmx-warning-text);background:var(--aqmx-warning-surface);box-shadow:var(--aqmx-shadow);position:absolute;inset-block-start:var(--aqmx-space-6);inset-inline-end:var(--aqmx-space-7)}@media (prefers-reduced-motion:reduce){.aqmx-progress:before,.aqmx-skeleton>span{animation:none}}@media (forced-colors:active){.aqmx-icon-button[aria-pressed=true],.aqmx-icon-button[aria-expanded=true]{forced-color-adjust:none;color:highlighttext;background:highlight}.aqmx-icon-button[aria-disabled=true]{color:graytext}.aqmx-root :focus-visible{outline-color:highlight}.aqmx-progress:before{forced-color-adjust:none;background:highlight}.aqmx-skeleton>span{forced-color-adjust:none;background:graytext}}.aqmx-explorer{gap:var(--aqmx-space-5);block-size:100%;min-block-size:0;min-inline-size:0;padding:var(--aqmx-space-4);color:var(--aqmx-text);background:var(--aqmx-surface);flex-direction:column;display:flex;position:relative;overflow-y:auto}.aqmx-explorer-header{align-items:center;gap:var(--aqmx-space-4);min-block-size:var(--aqmx-control);flex-wrap:wrap;flex:none;display:flex}.aqmx-explorer-source{min-inline-size:min(100%,9rem);block-size:var(--aqmx-control);padding-inline:var(--aqmx-space-5);border:var(--aqmx-space-1) solid var(--aqmx-border);border-radius:var(--aqmx-radius-m);color:var(--aqmx-text);background:var(--aqmx-surface-raised);font:inherit;text-overflow:ellipsis;cursor:pointer;flex:9rem}.aqmx-explorer-actions{justify-content:flex-end;align-items:center;gap:var(--aqmx-space-2);flex-wrap:wrap;flex:0 auto;min-inline-size:0;margin-inline-start:auto;display:flex}.aqmx-explorer-body{gap:var(--aqmx-space-5);flex-direction:column;flex:1 1 0;min-block-size:0;min-inline-size:0;display:flex;position:relative}.aqmx-labelled-button{gap:var(--aqmx-space-2);min-inline-size:var(--aqmx-control);inline-size:auto;padding-inline:var(--aqmx-space-3);white-space:nowrap;flex:0 auto;display:inline-flex}.aqmx-labelled-button>svg{flex:none}.aqmx-labelled-button>span{text-overflow:ellipsis;min-inline-size:0;overflow:hidden}.aqmx-explorer-refresh-failure,.aqmx-explorer-expand-all-stop{padding:var(--aqmx-space-4) var(--aqmx-space-5);border:var(--aqmx-space-1) solid;border-radius:var(--aqmx-radius-m);line-height:var(--aqmx-line-height-compact);overflow-wrap:anywhere;flex:none;margin:0}.aqmx-explorer-refresh-failure{border-color:var(--aqmx-danger-border);color:var(--aqmx-danger-text);background:var(--aqmx-danger-surface)}.aqmx-explorer-expand-all-stop{border-color:var(--aqmx-info-border);color:var(--aqmx-info-text);background:var(--aqmx-info-surface)}.aqmx-explorer-actions>.aqmx-labelled-button[data-aqmx-busy=true]>svg{animation:aqmx-explorer-spin calc(var(--aqmx-duration-4) * 3) linear infinite}@keyframes aqmx-explorer-spin{to{rotate:1turn}}.aqmx-explorer-actions>.aqmx-icon-button[aria-busy=true]{cursor:progress}.aqmx-explorer-menu{z-index:50;gap:var(--aqmx-space-1);max-block-size:100%;inline-size:min(21rem,100%);padding:var(--aqmx-space-3);border:var(--aqmx-space-1) solid var(--aqmx-border);border-radius:var(--aqmx-radius-m);background:var(--aqmx-surface-raised);box-shadow:var(--aqmx-shadow);display:grid;position:absolute;inset-block-start:0;inset-inline-end:0;overflow-y:auto}.aqmx-explorer-menu-item{align-items:center;gap:var(--aqmx-space-5);inline-size:100%;min-block-size:var(--aqmx-control);padding-inline:var(--aqmx-space-5);border-radius:var(--aqmx-radius-s);color:var(--aqmx-text);font:inherit;text-align:start;cursor:pointer;transition:background-color var(--aqmx-duration-1) var(--aqmx-ease);background:0 0;border:0;display:flex}.aqmx-explorer-menu-item:hover,.aqmx-explorer-menu-item:focus-visible{background:var(--aqmx-surface-sunken)}.aqmx-explorer-menu-icon{inline-size:var(--aqmx-icon);block-size:var(--aqmx-icon);color:var(--aqmx-text-muted);flex:none}.aqmx-explorer-menu-name{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-inline-size:0;overflow:hidden}.aqmx-explorer-menu-value{color:var(--aqmx-accent);font-variant-numeric:tabular-nums;flex:none;font-weight:600}.aqmx-explorer-notices{gap:var(--aqmx-space-5);flex:none;display:grid}.aqmx-explorer-tree-area{min-block-size:calc(var(--aqmx-control) * 3);flex-direction:column;flex:1 1 0;min-inline-size:0;display:flex;position:relative}.aqmx-explorer-tree{flex:1 1 0;min-block-size:0;position:relative}.aqmx-explorer-tree-view{block-size:100%}.aqmx-explorer-tree-progress{z-index:1;align-items:center;gap:var(--aqmx-space-2) var(--aqmx-space-4);max-inline-size:calc(100% - var(--aqmx-space-5) * 3);padding:var(--aqmx-space-3) var(--aqmx-space-5);border:var(--aqmx-space-1) solid var(--aqmx-border);border-radius:var(--aqmx-radius-m);color:var(--aqmx-text-muted);background:var(--aqmx-surface-raised);box-shadow:var(--aqmx-shadow);line-height:var(--aqmx-line-height-compact);overflow-wrap:anywhere;pointer-events:none;animation:aqmx-appear var(--aqmx-duration-2) ease-out var(--aqmx-delay-progress) both;flex-wrap:wrap;margin:0;display:flex;position:absolute;inset-block-end:var(--aqmx-space-9);inset-inline-start:var(--aqmx-space-5)}.aqmx-explorer-tree-progress-icon{inline-size:var(--aqmx-icon);block-size:var(--aqmx-icon);animation:aqmx-explorer-spin calc(var(--aqmx-duration-4) * 3) linear infinite;flex:none}@keyframes aqmx-appear{0%{opacity:0}}.aqmx-explorer-tree-probe{inline-size:0;block-size:var(--aqmx-control);visibility:hidden;pointer-events:none;color:var(--aqmx-tree-line);forced-color-adjust:none;position:absolute;inset-block-start:0;inset-inline-start:0;overflow:hidden}.aqmx-explorer-loading{gap:var(--aqmx-space-5);padding:var(--aqmx-space-6) var(--aqmx-space-5);color:var(--aqmx-text-muted);display:grid}.aqmx-explorer-loading-text{margin:0}.aqmx-explorer-loading>.aqmx-skeleton{padding:0}.aqmx-explorer-tree-message{padding:var(--aqmx-space-5);overflow-y:auto}.aqmx-explorer-empty-icon{inline-size:var(--aqmx-space-8);block-size:var(--aqmx-space-8);color:var(--aqmx-border-strong)}.aqmx-explorer-empty-body{margin:0}.aqmx-explorer-tree [role=treeitem]{border-radius:var(--aqmx-radius-s);color:var(--aqmx-text);transition:background-color var(--aqmx-duration-1) var(--aqmx-ease)}.aqmx-explorer-tree [role=treeitem]:hover{color:var(--aqmx-text);background:var(--aqmx-tree-row-tint)}.aqmx-explorer-tree [role=treeitem][data-entry-item-selected=true]{color:var(--aqmx-text);background:var(--aqmx-tree-row-tint);box-shadow:inset var(--aqmx-space-2) 0 0 currentColor}.aqmx-explorer-tree [role=treeitem][data-entry-selected=true],.aqmx-explorer-tree [role=treeitem][data-aqmx-open=true]{color:var(--aqmx-accent);background:var(--aqmx-tree-row-accent-tint)}.aqmx-explorer-tree [role=treeitem]:focus-visible{outline-offset:calc(-1 * var(--aqmx-focus-ring-width))}.aqmx-explorer-tree [role=treeitem][data-entry-item-selected=true]:focus-visible{box-shadow:inset calc(var(--aqmx-space-2) + var(--aqmx-focus-ring-width)) 0 0 currentColor}.aqmx-explorer-tree [role=treeitem][data-aqmx-open=true]{font-weight:600}.aqmx-explorer-tree [role=treeitem][data-aqmx-children=loading]{animation:aqmx-pulse calc(var(--aqmx-duration-4) * 4) ease-in-out infinite}.aqmx-explorer-tree [role=treeitem][data-aqmx-loop]{color:var(--aqmx-text-muted);font-style:italic}.aqmx-explorer-folder-failed{--aqmx-folder-name-floor:3em;align-items:center;min-inline-size:0;max-inline-size:100%;display:flex}.aqmx-explorer-folder-name{flex:10000 1 var(--aqmx-folder-name-floor);text-overflow:ellipsis;white-space:nowrap;min-inline-size:0;max-inline-size:max-content;overflow:hidden}.aqmx-explorer-folder-error{min-inline-size:0;color:var(--aqmx-danger-text);text-indent:var(--aqmx-space-4);text-overflow:ellipsis;white-space:nowrap;flex:1 1 0;overflow:hidden}.aqmx-explorer-folder-retry{flex:1000000 0 calc(var(--aqmx-icon) + (var(--aqmx-space-3) + var(--aqmx-space-1)) * 2);min-inline-size:calc(var(--aqmx-icon) + (var(--aqmx-space-3) + var(--aqmx-space-1)) * 2);max-inline-size:max-content;min-block-size:var(--aqmx-control);padding:0 var(--aqmx-space-3);border:var(--aqmx-space-1) solid var(--aqmx-danger-border);border-radius:var(--aqmx-radius-s);color:var(--aqmx-danger-text);background:var(--aqmx-danger-surface);font:inherit;line-height:var(--aqmx-line-height-compact);cursor:pointer;justify-content:center;align-items:center;margin-inline-start:var(--aqmx-space-4);display:inline-flex;overflow:hidden}.aqmx-explorer-folder-retry:focus-visible{outline-offset:calc(-1 * (var(--aqmx-focus-ring-width) + var(--aqmx-space-1)))}.aqmx-explorer-folder-retry-icon{inline-size:var(--aqmx-icon);block-size:var(--aqmx-icon);flex:none}.aqmx-explorer-folder-retry-text{min-inline-size:0;text-indent:var(--aqmx-space-3);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.aqmx-explorer-tray{gap:var(--aqmx-space-4);min-block-size:calc(var(--aqmx-control) + (var(--aqmx-space-5) + var(--aqmx-space-1)) * 2);padding:var(--aqmx-space-5);border:var(--aqmx-space-1) solid var(--aqmx-border);border-radius:var(--aqmx-radius-m);background:var(--aqmx-surface-raised);box-shadow:var(--aqmx-shadow);flex-direction:column;flex:0 auto;display:flex;overflow-y:auto}.aqmx-explorer-tray-header{justify-content:space-between;align-items:center;gap:var(--aqmx-space-5);flex:none;display:flex}.aqmx-explorer-tray-title{margin:0;font-weight:600}.aqmx-explorer-tray-button{min-block-size:var(--aqmx-control);padding-inline:var(--aqmx-space-5);border-radius:var(--aqmx-radius-m);color:var(--aqmx-accent);font:inherit;cursor:pointer;background:0 0;border:0;flex:none;font-weight:600}.aqmx-explorer-tray-button:hover{background:var(--aqmx-surface-sunken)}.aqmx-explorer-tray-list{gap:var(--aqmx-space-1);min-block-size:var(--aqmx-control);max-block-size:calc(var(--aqmx-control) * 3);flex:0 auto;margin:0;padding:0;list-style:none;display:grid;overflow-y:auto}.aqmx-explorer-tray-item{align-items:center;gap:var(--aqmx-space-4);display:flex}.aqmx-explorer-tray-name{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-inline-size:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden}.aqmx-explorer-tray-open{min-block-size:var(--aqmx-control);padding-inline:var(--aqmx-space-6);border-radius:var(--aqmx-radius-m);color:var(--aqmx-surface);background:var(--aqmx-accent);font:inherit;cursor:pointer;border:0;flex:none;font-weight:600}@media (forced-colors:active){.aqmx-explorer-tree [role=treeitem][data-entry-item-selected=true],.aqmx-explorer-tree [role=treeitem][data-entry-selected=true],.aqmx-explorer-tree [role=treeitem][data-aqmx-open=true],.aqmx-explorer-menu-item[aria-checked=true]{forced-color-adjust:none;color:highlighttext;background:highlight}.aqmx-explorer-menu-item[aria-checked=true] *,.aqmx-explorer-tree [role=treeitem][data-entry-item-selected=true] *,.aqmx-explorer-tree [role=treeitem][data-entry-selected=true] *,.aqmx-explorer-tree [role=treeitem][data-aqmx-open=true] *{color:inherit!important}.aqmx-explorer-tree [role=treeitem][data-entry-item-selected=true]:focus-visible,.aqmx-explorer-tree [role=treeitem][data-entry-selected=true]:focus-visible,.aqmx-explorer-tree [role=treeitem][data-aqmx-open=true]:focus-visible{outline-color:highlighttext}.aqmx-explorer-tray-button,.aqmx-explorer-tray-open,.aqmx-explorer-tray-remove{border:var(--aqmx-space-1) solid ButtonText}}@media (prefers-reduced-motion:reduce){.aqmx-explorer-actions>.aqmx-labelled-button[data-aqmx-busy=true]>svg,.aqmx-explorer-tree-progress-icon,.aqmx-explorer-tree [role=treeitem][data-aqmx-children=loading]{animation:none}.aqmx-explorer-tree-view{transition-duration:0s!important}}.aqmx-document{contain:layout paint;--aqmd-toolbar-inset-block-start:calc(var(--aqmx-focus-ring-width) + var(--aqmx-focus-ring-offset));block-size:100%;min-block-size:0;inline-size:100%;min-inline-size:0;color:var(--aqmx-text);background:var(--aqmx-surface);flex-direction:column;display:flex;position:relative;overflow:hidden}.aqmx-document [data-theme]{background-color:#0000!important}.aqmx-document .markdown-body :is(sup,sub) a{outline-offset:var(--aqmx-focus-ring-offset)!important}.aqmx-document *{scroll-margin:calc(var(--aqmx-focus-ring-width) + var(--aqmx-focus-ring-offset))}.aqmx-document[aria-busy=true]>:not(.aqmx-progress){opacity:.618;transition:opacity var(--aqmx-duration-3) var(--aqmx-ease)}.aqmx-document-viewer{flex:1 1 0;min-block-size:0;min-inline-size:0}.aqmx-document-notice{margin:var(--aqmx-space-6) var(--aqmx-space-7) 0;overflow-wrap:anywhere;flex:none}.aqmx-document-failure{min-block-size:0;padding:var(--aqmx-space-7);flex:1 1 0;overflow:auto}.aqmx-document-file{flex-direction:column;flex:1 1 0;min-block-size:0;min-inline-size:0;display:flex}.aqmx-document-toolbar{align-items:center;gap:var(--aqmx-space-3);padding:var(--aqmx-space-3) var(--aqmx-space-6);border-block-end:var(--aqmx-space-1) solid var(--aqmx-border);background:var(--aqmx-surface-raised);flex-wrap:wrap;flex:none;display:flex}.aqmx-document-file-icon{color:var(--aqmx-text-muted);flex:none;display:inline-flex}.aqmx-document-file-icon>svg{inline-size:var(--aqmx-icon);block-size:var(--aqmx-icon)}.aqmx-document-title{text-overflow:ellipsis;white-space:nowrap;flex:1 1 0;min-inline-size:0;font-weight:600;overflow:hidden}.aqmx-document-level{min-inline-size:var(--aqmx-space-9);color:var(--aqmx-text-muted);font-variant-numeric:tabular-nums;text-align:center;flex:none}.aqmx-document-canvas{min-block-size:0;padding:var(--aqmx-space-7);background-color:var(--aqmx-surface-sunken);background-image:radial-gradient(var(--aqmx-border) var(--aqmx-space-1), transparent var(--aqmx-space-1));background-size:var(--aqmx-space-6) var(--aqmx-space-6);flex:1 1 0;grid-template:minmax(0,1fr)/minmax(0,1fr);display:grid;overflow:auto}.aqmx-document-image{margin:auto;display:block}.aqmx-document-image[data-aqmx-image-mode=fit]{object-fit:contain;max-block-size:100%;max-inline-size:100%}.aqmx-document-image[data-aqmx-image-mode=level]{max-block-size:none;max-inline-size:none}.aqmx-document-stage{min-block-size:0;padding:var(--aqmx-space-7);background:var(--aqmx-surface-sunken);flex:1 1 0;grid-template:minmax(0,1fr)/minmax(0,1fr);place-items:center;display:grid;overflow:auto}.aqmx-document-file[data-aqmx-media-type=pdf]>.aqmx-document-stage{place-items:stretch stretch;padding:0}.aqmx-document-video{border-radius:var(--aqmx-radius-l);background:var(--aqmx-surface-raised);max-block-size:100%;max-inline-size:100%;box-shadow:var(--aqmx-shadow)}.aqmx-document-audio{inline-size:min(100%, var(--aqmx-column-measure))}.aqmx-document-frame{background:var(--aqmx-surface);border:0;block-size:100%;inline-size:100%}.aqmx-document-inert-link{color:var(--aqmx-text-muted);cursor:not-allowed;text-decoration-style:dotted}.aqmx-document[data-aqmx-document-flow=content]{contain:layout;block-size:auto;overflow:visible}.aqmx-document[data-aqmx-document-flow=content]>*,.aqmx-document[data-aqmx-document-flow=content] .aqmx-document-file>*{flex:none}.aqmx-document[data-aqmx-document-flow=content] .aqmx-document-frame{aspect-ratio:1/1.41421;block-size:auto}.aqmx-document-refresh-bar{flex-direction:column;flex:none;display:flex}.aqmx-document-refresh-row{padding:calc(var(--aqmx-focus-ring-width) + var(--aqmx-focus-ring-offset));justify-content:flex-end;align-items:center;display:flex}.aqmx-document-refresh-failure{margin:0 var(--aqmx-space-3) var(--aqmx-space-2);padding:var(--aqmx-space-4) var(--aqmx-space-5);border:var(--aqmx-space-1) solid var(--aqmx-danger-border);border-radius:var(--aqmx-radius-m);color:var(--aqmx-danger-text);background:var(--aqmx-danger-surface);line-height:var(--aqmx-line-height-compact);overflow-wrap:anywhere}.aqmx-document-refresh-row>.aqmx-icon-button[data-aqmx-busy=true]>svg{animation:aqmx-explorer-spin calc(var(--aqmx-duration-4) * 3) linear infinite}.aqmx-document-refresh-row>.aqmx-icon-button[aria-busy=true]{cursor:progress}@media (prefers-reduced-motion:reduce){.aqmx-document-refresh-row>.aqmx-icon-button[data-aqmx-busy=true]>svg{animation:none}}.aqmx-multi-stage{--aqmx-multi-panel-body-floor:calc(3rem * var(--aqmx-line-height-prose));--aqdd-stage-padding-block:var(--aqmx-space-7);--aqdd-stage-padding-inline:var(--aqmx-space-6);--aqdd-column-gap:var(--aqmx-space-6);--aqdd-panel-surface:var(--aqmx-surface);--aqdd-panel-border:var(--aqmx-border);--aqdd-panel-title-color:var(--aqmx-text);--aqdd-header-button-color:var(--aqmx-text-muted);--aqdd-header-button-hover-color:var(--aqmx-text);--aqdd-header-button-hover-surface:var(--aqmx-surface-sunken);--aqdd-custom-badge-color:var(--aqmx-warning-text);--aqdd-custom-badge-surface:var(--aqmx-warning-surface);--aqdd-restore-button-color:var(--aqmx-surface);--aqdd-restore-button-surface:var(--aqmx-accent);--aqdd-restore-button-hover-surface:var(--aqmx-info-text);--aqdd-drop-placeholder-color:var(--aqmx-accent);--aqdd-drop-placeholder-surface:var(--aqmx-surface);--aqdd-drop-placeholder-target-surface:var(--aqmx-accent-surface);background:var(--aqmx-surface-sunken);block-size:100%;min-block-size:0;min-inline-size:0;color:var(--aqmx-text);flex-direction:column;display:flex;position:relative}.aqmx-multi-stage:focus-visible{outline:var(--aqmx-focus-ring-width) solid var(--aqmx-focus);outline-offset:calc(-1 * var(--aqmx-focus-ring-width))}.aqmx-multi-probe{inline-size:var(--aqmx-column-measure);visibility:hidden;pointer-events:none;block-size:0;font-size:1rem;position:absolute;inset-block-start:0;inset-inline-start:0;overflow:hidden}.aqmx-multi-layout{flex:auto;min-block-size:0}.aqmx-multi-skeleton{grid-template-columns:repeat(auto-fit, minmax(min(100%, var(--aqmx-column-measure)), 1fr));align-content:start;gap:var(--aqmx-space-7);padding:var(--aqmx-space-7);display:grid;overflow:hidden}.aqmx-multi-skeleton-column{border:var(--aqmx-space-1) solid var(--aqmx-border);border-radius:var(--aqmx-radius-l);background:var(--aqmx-surface)}.aqmx-multi-column{min-inline-size:0}.aqmx-multi-panel{outline:var(--aqmx-space-1) solid var(--aqmx-border-strong);outline-offset:0;color:var(--aqmx-text)}.aqmx-multi-panel[data-maximized]{outline-color:var(--aqmx-focus)}.aqmx-multi-panel-frame{content-visibility:auto;contain-intrinsic-block-size:auto var(--aqmx-space-9);min-block-size:100%}.aqmx-multi-panel-frame:focus-visible{outline:var(--aqmx-focus-ring-width) solid var(--aqmx-focus);outline-offset:calc(-1 * var(--aqmx-focus-ring-width))}.aqmx-multi-panel-frame[data-aqmx-maximized=false]{-webkit-user-select:none;user-select:none}.aqmx-multi-panel-frame>.aqmx-document-refresh-bar{z-index:1;background:var(--aqmx-surface);position:sticky;inset-block-start:0}.aqmx-multi-panel-frame{--aqmx-multi-panel-pinned-block-size:0px}.aqmx-multi-panel-frame .aqmx-document *{scroll-margin-block-start:calc(var(--aqmx-multi-panel-pinned-block-size) + var(--aqmx-focus-ring-width) + var(--aqmx-focus-ring-offset))}.aqmx-multi-panel-frame .aqmx-document-toolbar>.aqmx-icon-button{flex-shrink:1;min-inline-size:0}.aqmx-multi-toast{animation:aqmx-multi-toast-in var(--aqmx-duration-3) var(--aqmx-ease)}.aqmx-multi-toast:popover-open{max-inline-size:min(34rem, calc(var(--aqmx-multi-toast-room) - var(--aqmx-space-8)));margin:0;position:fixed;inset:auto;inset-block-start:calc(var(--aqmx-multi-toast-block-start) + var(--aqmx-space-6));inset-inline-end:calc(var(--aqmx-multi-toast-inline-end) + var(--aqmx-space-7));overflow:visible}@keyframes aqmx-multi-toast-in{0%{opacity:0;translate:0 calc(var(--aqmx-space-5) * -1)}}@media (prefers-reduced-motion:reduce){.aqmx-multi-toast{animation:none}}@media (forced-colors:active){.aqmx-multi-stage:focus-visible,.aqmx-multi-stage :focus-visible,.aqmx-multi-panel[data-maximized]{outline-color:highlight}}.aqmx-split-frame{--aqmx-sheet-target:2.75rem;flex-direction:column;flex:auto;min-block-size:0;min-inline-size:0;display:flex;position:relative}.aqmx-rem-probe{visibility:hidden;pointer-events:none;block-size:0;inline-size:1rem;position:absolute;inset-block-start:0;inset-inline-start:0}.aqmx-split{flex:auto;min-block-size:0}.aqmx-split-explorer,.aqmx-split-content{background:var(--aqmx-surface);min-block-size:0}.aqmx-split-explorer{background:var(--aqmx-surface-raised)}.aqmx-split-handle{background:var(--aqmx-surface-raised);transition:background-color var(--aqmx-duration-2) var(--aqmx-ease)}.aqmx-split-handle:hover,.aqmx-split-handle:focus-visible{background:var(--aqmx-surface-sunken)}.aqmx-sheet-bar{padding:var(--aqmx-space-3) var(--aqmx-space-5);border-block-end:var(--aqmx-space-1) solid var(--aqmx-border);background:var(--aqmx-surface-raised);flex:none;align-items:center;display:flex}.aqmx-sheet-toggle,.aqmx-sheet-close{justify-content:center;align-items:center;gap:var(--aqmx-space-5);min-inline-size:var(--aqmx-sheet-target);min-block-size:var(--aqmx-sheet-target);padding:0 var(--aqmx-space-6);border:var(--aqmx-space-1) solid var(--aqmx-border);border-radius:var(--aqmx-radius-m);color:var(--aqmx-text);background:var(--aqmx-surface);font:inherit;cursor:pointer;transition:background-color var(--aqmx-duration-2) var(--aqmx-ease);display:inline-flex}.aqmx-sheet-close{z-index:1;color:var(--aqmx-text-muted);background:var(--aqmx-surface-raised);border-color:#0000;padding:0;position:relative}.aqmx-sheet-toggle:hover,.aqmx-sheet-close:hover{background:var(--aqmx-surface-sunken)}.aqmx-sheet-toggle>svg,.aqmx-sheet-close>svg{inline-size:var(--aqmx-icon);block-size:var(--aqmx-icon);flex:none}.aqmx-sheet{inline-size:min(calc(100vw - var(--aqmx-space-9)), 21rem);border:0;border-inline-end:var(--aqmx-space-1) solid var(--aqmx-border);block-size:100dvh;max-block-size:none;max-inline-size:none;color:var(--aqmx-text);background:var(--aqmx-surface-raised);box-shadow:var(--aqmx-shadow);margin:0;padding:0;position:fixed;inset-block:0;inset-inline:0 auto}.aqmx-sheet::backdrop{background:#0f172a61}.aqmx-sheet-panel{flex-direction:column;block-size:100%;display:flex}.aqmx-sheet-header{z-index:1;justify-content:flex-end;align-items:center;gap:var(--aqmx-space-3);padding:var(--aqmx-space-3);border-block-end:var(--aqmx-space-1) solid var(--aqmx-border);flex:none;display:flex;position:relative}.aqmx-sheet-notice-slot>.aqmx-toast{--aqmx-sheet-notice-band:calc(var(--aqmx-sheet-target) + var(--aqmx-space-3) * 2);--aqmx-sheet-notice-line:calc(var(--aqmx-line-height-compact) * 1em);--aqmx-sheet-notice-inset:calc((var(--aqmx-sheet-notice-band) - var(--aqmx-sheet-notice-line)) / 2);z-index:0;max-inline-size:none;padding-block:var(--aqmx-sheet-notice-inset) 0;padding-inline:var(--aqmx-space-6) var(--aqmx-space-3);border:0;border-block-end:var(--aqmx-space-1) solid var(--aqmx-warning-border);line-height:var(--aqmx-line-height-compact);overflow-wrap:anywhere;box-shadow:none;border-radius:0;position:absolute;inset-block:0 auto;inset-inline:0}.aqmx-sheet-notice-slot>.aqmx-toast:before{content:"";float:inline-end;inline-size:calc(var(--aqmx-sheet-target) + var(--aqmx-space-3));block-size:calc(var(--aqmx-sheet-notice-band) - var(--aqmx-sheet-notice-inset) - var(--aqmx-space-1))}.aqmx-sheet-notice-slot>.aqmx-toast:after{content:"";block-size:calc(var(--aqmx-sheet-notice-inset) - var(--aqmx-space-1));display:block}.aqmx-sheet-body{isolation:isolate;flex-direction:column;flex:auto;min-block-size:0;display:flex;position:relative;overflow:hidden}.aqmx-document-column{flex-direction:column;min-block-size:0;display:flex}.aqmx-document-column>.aqmx-document{flex:1 1 0;block-size:auto;min-block-size:0}.aqmx-single{flex:auto;position:relative}.aqmx-tree-document{block-size:100%;inline-size:100%}.aqmx-split-fallback,.aqmx-stage-fallback{background:var(--aqmx-surface-sunken);flex:auto;min-block-size:0}@media (forced-colors:active){.aqmx-sheet-toggle[aria-expanded=true]{forced-color-adjust:none;color:highlighttext;background:highlight}.aqmx-sheet-toggle:focus-visible,.aqmx-sheet-close:focus-visible{outline-color:highlight}.aqmx-split-handle:focus-visible{outline:var(--aqmx-focus-ring-width) solid Highlight!important;outline-offset:calc(-1 * var(--aqmx-focus-ring-width))!important}}@media (prefers-reduced-motion:reduce){.aqmx-split-handle,.aqmx-split-handle *{transition-duration:0s!important}}}@layer utilities;
|
|
@@ -18,9 +18,9 @@ Tests and host styling may rely on the `data-testid` values and `data-aqmx-*` at
|
|
|
18
18
|
| `data-testid="markdown-explorer-sheet-announcer"` | The sheet's polite live region (`role="status"`), inside the dialog | Holds the announcements while the sheet is open (the modal dialog hides the root's region), with the same text as the root's region would; empty while the sheet is closed and when it opens |
|
|
19
19
|
| `data-testid="markdown-explorer-split-loading"` | Placeholder while the tree view's code loads | |
|
|
20
20
|
| `data-testid="markdown-explorer-single"` | Single view frame | |
|
|
21
|
-
| `data-testid="markdown-explorer-
|
|
22
|
-
| `data-testid="markdown-explorer-
|
|
23
|
-
| `data-testid="markdown-explorer-
|
|
21
|
+
| `data-testid="markdown-explorer-document-refresh-bar"` | The document refresh bar | In the tree and single views, above the document region (the button at its inline end; absent while the tree view shows no document); in the multi view, the first row of each panel's body, inside the panel's scroll area and pinned (sticky) to its top while the document scrolls under it. Holds the button row and, below it, the failure notice |
|
|
22
|
+
| `data-testid="markdown-explorer-document-refresh"` | The Refresh document button, in the bar | Reads that document alone again past the host's caches. The page views name it by its visible label (`refreshDocumentLabel`; tooltip `refreshDocument`); a panel's is icon-only, with `aria-label` and `title` set to `refreshDocumentNamed` (the panel title). Carries `aria-busy="true"` and `data-aqmx-busy="true"` while it runs, and `aria-disabled="true"` then and while the document loads (never `disabled`) |
|
|
23
|
+
| `data-testid="markdown-explorer-document-refresh-failure"` | Notice of a document refresh that failed, inside the bar below its button row | No live role (the announcer speaks the same text); carries `data-aqmx-failure` (the reason). Shown while the copy of the document it kept stays on screen: a newer answer that replaces or confirms that copy, a move to another document (it does not come back on return) and that control's next refresh take it down |
|
|
24
24
|
| `data-aqmx-key-repeat="allow"` | A control inside the explorer, or an element around controls | Opts the control (or every control inside the element) in to the key repeat of a held Enter, which the explorer otherwise ignores: a press of Enter that began on the control keeps activating it once per repeat while the focus stays there, and a press that began elsewhere and handed the focus to it does not (see [Held keys](layout.md#held-keys)); the explorer sets it on Zoom in and Zoom out |
|
|
25
25
|
|
|
26
26
|
## Explorer
|
|
@@ -29,7 +29,7 @@ Tests and host styling may rely on the `data-testid` values and `data-aqmx-*` at
|
|
|
29
29
|
| --- | --- | --- |
|
|
30
30
|
| `data-testid="markdown-explorer-explorer"` | The explorer `nav` | Carries `data-aqmx-mode` (`tree` / `multi`) |
|
|
31
31
|
| `data-testid="markdown-explorer-source-select"` | Source switcher | Shown with more than one source |
|
|
32
|
-
| `data-testid="markdown-explorer-refresh"` | The
|
|
32
|
+
| `data-testid="markdown-explorer-refresh"` | The explorer's Refresh tree button | Named by its visible label (`refreshTreeLabel`; tooltip `refreshTree`). Carries `aria-disabled="true"`, `aria-busy="true"` and `data-aqmx-busy="true"` while the tree reloads (never `disabled`) |
|
|
33
33
|
| `data-testid="markdown-explorer-refresh-failure"` | Alert of a tree reload that failed | `role="alert"`, first in the explorer's body; carries `data-aqmx-failure` (the reason). The old tree stays below it; shown until the next refresh starts, and only while its source is the pane's |
|
|
34
34
|
| `data-testid="markdown-explorer-expand-all"` / `-collapse-all` | Tree controls | `aria-disabled="true"` while unavailable (never `disabled`). Expand all also carries `aria-busy="true"`, `data-aqmx-busy="true"` and `aria-disabled="true"` while a press runs (it ignores presses meanwhile); Collapse all ends a running press |
|
|
35
35
|
| `data-testid="markdown-explorer-tree-settings"` | Tree settings button | |
|
package/docs/behavior/layout.md
CHANGED
|
@@ -78,6 +78,9 @@ Documents in the multi-panel view sit in `multiView.columns` logical columns (4
|
|
|
78
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
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
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.
|
|
81
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.
|
|
82
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.
|
|
83
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.
|
|
@@ -93,7 +96,9 @@ Documents in the multi-panel view sit in `multiView.columns` logical columns (4
|
|
|
93
96
|
## The document region
|
|
94
97
|
|
|
95
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.
|
|
96
|
-
- A
|
|
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.
|
|
97
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.
|
|
98
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.
|
|
99
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.
|
|
@@ -104,16 +109,17 @@ Documents in the multi-panel view sit in `multiView.columns` logical columns (4
|
|
|
104
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.
|
|
105
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.
|
|
106
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.
|
|
107
|
-
- 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); an element
|
|
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.
|
|
108
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).
|
|
109
|
-
In a page view such a link (`#top` or `#`,
|
|
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.
|
|
110
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.
|
|
111
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.
|
|
112
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;
|
|
113
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.
|
|
114
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.
|
|
115
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.
|
|
116
|
-
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.
|
|
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.
|
|
117
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,
|
|
118
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.
|
|
119
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.
|
|
@@ -140,14 +146,18 @@ The tree and multi-panel views are separate parts of the page, so the control a
|
|
|
140
146
|
|
|
141
147
|
## Busy and unavailable controls
|
|
142
148
|
|
|
143
|
-
A header button that is unavailable never takes the `disabled` attribute, which would drop the focus to the page body: Refresh 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).
|
|
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).
|
|
144
150
|
|
|
145
|
-
Refresh carries its label as visible text next to its icon (`
|
|
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`).
|
|
146
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`).
|
|
147
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).
|
|
148
154
|
|
|
149
|
-
|
|
150
|
-
|
|
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.
|
|
151
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.
|
|
152
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.
|
|
153
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.
|
|
@@ -165,6 +175,7 @@ Each reader has an announcer of their own: when another reader signs in, the reb
|
|
|
165
175
|
|
|
166
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.
|
|
167
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.
|
|
168
179
|
|
|
169
180
|
## Held keys
|
|
170
181
|
|
|
@@ -182,7 +193,8 @@ Space needs no such rule on buttons, which act when the key is released, and a t
|
|
|
182
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.
|
|
183
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)).
|
|
184
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.
|
|
185
|
-
- 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.
|
|
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.
|
|
186
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.
|
|
187
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 φ).
|
|
188
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.
|
|
@@ -8,13 +8,14 @@ This page is part of the host-facing contract of `@aiquants/markdown-explorer`;
|
|
|
8
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
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
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`).
|
|
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
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
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
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
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
16
|
- `treeRegion` names the tree itself, apart from `explorerRegion`, which names the explorer's landmark (and the sheet on narrow frames).
|
|
17
|
-
- `
|
|
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).
|
|
18
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.
|
|
19
20
|
|
|
20
21
|
## Overrides
|
|
@@ -32,7 +33,7 @@ const HANDBOOK_LABELS = {
|
|
|
32
33
|
|
|
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;
|
|
34
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:
|
|
35
|
-
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)`, `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.
|
|
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.
|
|
36
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.
|
|
37
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.
|
|
38
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.
|
|
@@ -35,12 +35,15 @@ Markdown HTML returned by a source is sanitized on the server with DOMPurify, in
|
|
|
35
35
|
- removed elements: `script`, `style`, `noscript`, `template`, `form`, `button`, `textarea`, `select`, `option`, `object`, `embed`, `base`, `meta`, `link`, `frame`, `frameset`; `input` survives only as a disabled checkbox (task lists);
|
|
36
36
|
- removed attributes: every event handler (`on*`), `srcdoc`, `formaction`; `style` keeps only `text-align`;
|
|
37
37
|
- URLs keep only the `http:`, `https:`, `mailto:` and `tel:` schemes and relative references, and `data:` URLs are removed from every element;
|
|
38
|
-
- `iframe` survives only with an `https:` source whose host is listed exactly in `config.iframeHosts` (a longer host such as `www.youtube.com.example.net`, or a protocol-relative source, is removed);
|
|
39
|
-
- `allow`, `allowfullscreen`, `frameborder` and `referrerpolicy` survive only on iframes: `referrerpolicy` only as `no-referrer`, `origin`, `same-origin`, `strict-origin` or `strict-origin-when-cross-origin
|
|
38
|
+
- `iframe` survives only with an `https:` source without a user name, a password or an explicit port whose host is listed exactly in `config.iframeHosts` (a longer host such as `www.youtube.com.example.net`, or a protocol-relative source, is removed), judged on the `src` trimmed as DOMPurify trims it; this gate is `allowedFrameUrlOf` of `@aiquants/markdown/embeds`, the same function the viewer applies, and the viewer also never renders an iframe of the page's own origin;
|
|
39
|
+
- `allow`, `allowfullscreen`, `frameborder` and `referrerpolicy` survive only on iframes: `referrerpolicy` only as `no-referrer`, `origin`, `same-origin`, `strict-origin` or `strict-origin-when-cross-origin` (`EMBED_FRAME_REFERRER_POLICIES`), and `allow` reduced to `autoplay`, `clipboard-write`, `encrypted-media`, `fullscreen`, `picture-in-picture` and `web-share`, each without its allowlist (`reduceFrameFeatures`);
|
|
40
40
|
- `target` survives only on `a` and `area`, and a link with any `target` other than `_self` gets `rel="noopener noreferrer"`;
|
|
41
41
|
- `id` attributes that would clobber `document` or form properties (`cookie`, `forms`, `location`, …) are removed — except on headings, which the table of contents needs and which cannot clobber `document`;
|
|
42
42
|
- the markup converters write for GitHub alerts and Zenn's message and details containers survives as written, through DOMPurify's own allow-lists: `div`, `p`, `aside`, `span`, `details` and `summary` with their `class` (and `aria-hidden` on a message's symbol), and an alert's inline SVG icon, `svg` with its `class`, `viewBox`, `version`, `width`, `height` and `aria-hidden` and `path` with its `d` (the README's Documents gives the markup, and its Load the styles the `@aiquants/markdown` stylesheets that draw it). Inside it, as anywhere else, event handlers, `script`, `foreignObject`, `use` and `animate` are removed and URLs follow the rules above;
|
|
43
|
-
- `data-*` attributes survive only for the Markdown renderer's contract — `data-link-kind`, `data-doc-path`, `data-doc-exists`, `data-footnote-ref` and `data-footnote-backref` on links, `data-asset-src` and `data-asset-exists` on images, `data-footnotes` on sections — so the renderer's plugin
|
|
43
|
+
- `data-*` attributes survive only for the Markdown renderer's contract — `data-link-kind`, `data-doc-path`, `data-doc-exists`, `data-footnote-ref` and `data-footnote-backref` on links, `data-asset-src` and `data-asset-exists` on images, `data-footnotes` on sections, and the embed placeholder below — so the renderer's iframe plugin trigger (`data-plugin-type`) and the `<div data-plugin-type>` placeholders of `@aiquants/markdown` 5 never reach the browser; SVG and MathML pass through DOMPurify's own allow-lists;
|
|
44
|
+
- the embed placeholder, `data-embed-url` on a `<p>` (the converter's mark of a paragraph whose only content is one absolute `http(s)` link without credentials, holding the link's `href` byte for byte), survives only where `placeholderLinkOf` of `@aiquants/markdown/embeds` accepts it, whatever the provider.
|
|
45
|
+
DOMPurify visits a parent before its children, so the sanitizer records the paragraph's attribute as parsed and, once DOMPurify is done with the attributes of a link directly inside that paragraph, restores it only when the paragraph holds exactly that one link (besides comments, white space, U+00A0 and U+3000) and the parsed attribute value equals the link's `href` as DOMPurify trimmed it.
|
|
46
|
+
A padded attribute value, another `href`, an `href` DOMPurify removed (`javascript:`, `data:`), a relative or credential-bearing URL, a second link, text, any other element (even one after the link that DOMPurify then removes) or the attribute on another element leaves the paragraph and its link without it; a duplicated attribute is read as its first occurrence, as the HTML parser keeps it. A forged placeholder can therefore at most re-create what the converter writes for the bare link, and the viewer validates it again before it embeds or cards anything.
|
|
44
47
|
Of a link the renderer hands to the explorer's link component (every link with an `href` that is not external), the anchor the explorer renders carries the link's `title`, `lang`, `dir`, `role`, `aria-*`, `data-footnote-*` and an `id` in the `user-content-` namespace, which the renderer passes on, beside the explorer's own `data-link-kind`, `data-doc-path` and `data-doc-exists`; the renderer passes no event handler, `style`, `href`-like or other attribute (see the README's Documents).
|
|
45
48
|
|
|
46
49
|
### The viewer's allow list
|
|
@@ -49,6 +52,7 @@ The document viewer (`@aiquants/markdown`) applies its own allow list to the san
|
|
|
49
52
|
|
|
50
53
|
- `class` keeps only the viewer's own tokens — the converter's markup for GitHub alerts and their Octicon titles, Zenn's messages and details, footnotes (`footnotes`, `footnote-ref`, `footnote-backref`) and formulas (`math`, `inline`, `display`) — and a code block's `language-*`; every other token is dropped, so a document cannot restyle or position an element with the explorer's or the host's classes. A task list's `contains-task-list` and `task-list-item` do not survive either, so task-list items keep their list markers;
|
|
51
54
|
- `id` survives only on headings, on a converter's footnotes (`fnref:N`, or `fnrefK:N` for a later reference to the same note, on a reference's `sup`; `fn:N` on a note's `li`) and in the `user-content-` namespace (remark-gfm's footnote ids); every other element, a link included, loses its id, so a document cannot take a name the page or a script looks up;
|
|
55
|
+
an anchor a document writes in raw HTML is therefore `<a name="…">`, which a fragment reaches by its name (`<a name="faq"></a>` by `#faq`), or an id in the `user-content-` namespace (`<a id="user-content-faq"></a>` by `#user-content-faq`). DOMPurify, before the viewer, drops an `id` or `name` whose value is a property of the document or of a form (`title`, `images`, `cookie`, …), except a heading's id, which the sanitizer keeps out of that check;
|
|
52
56
|
- the author's `target` and `rel` never survive (the viewer opens an external link in a new tab itself, with `rel="noreferrer"`), and neither do `srcset`, `tabindex` or `name` outside links.
|
|
53
57
|
|
|
54
58
|
The sanitizer itself keeps `class` and `id` as DOMPurify does and leaves their reduction to the viewer. The viewer's lists are the single definition of the converter's class tokens and fragment targets, and `@aiquants/markdown` exports neither the lists nor the functions that apply them, so the sanitizer could adopt the rule only as a copy: a second definition that drifts from the converter and the viewer, keeping or dropping a token the other does not.
|
|
@@ -76,7 +80,7 @@ In a tree or children listing, an entry with an empty name, or with a path the b
|
|
|
76
80
|
|
|
77
81
|
## Responses
|
|
78
82
|
|
|
79
|
-
- Every JSON data response carries `Content-Type: application/json; charset=utf-8`, `Cache-Control: private, no-store`, `X-Content-Type-Options: nosniff`, `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; sandbox` and `Cross-Origin-Resource-Policy: same-origin`, and has a JSON body.
|
|
83
|
+
- Every JSON data response of the explorer's own carries `Content-Type: application/json; charset=utf-8`, `Cache-Control: private, no-store`, `X-Content-Type-Options: nosniff`, `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; sandbox` and `Cross-Origin-Resource-Policy: same-origin`, and has a JSON body.
|
|
80
84
|
- The page loader's responses carry `Cache-Control: private, no-store` through the exported `headers`, to both the HTML document and the single-fetch data.
|
|
81
85
|
- The data route answers a React Router single-fetch request (`<dataPath>/<operation>.data`: its own URL path ends in `.data`, which React Router strips from the route's `url` argument and `params` but not from `request.url`) with a `not-found` failure (404) before authenticating or calling a source. React Router would run the route for it, read the whole response into memory and keep none of the headers above but `Set-Cookie`.
|
|
82
86
|
- Status codes: 200 for a tree, children, document or refresh answer (a refresh answers the reloaded tree, as a tree request does, failures included), 400 for a missing or malformed parameter, 401 when the reader is signed out, 403 when the reader may not use the explorer (or a refresh, or a document request marked as one's, lacks its proof), 404 for an unknown operation or source and for a children request to a source without `children()`, 405 (with `Allow`) for a wrong method.
|
|
@@ -95,6 +99,20 @@ In a tree or children listing, an entry with an empty name, or with a path the b
|
|
|
95
99
|
A body with a `Content-Length` carries exactly that many bytes: the bytes past it are dropped and the host's stream is cancelled, and a stream that ends short fails the response, so a file that changes while it is served never gets a body that disagrees with its length and ETag.
|
|
96
100
|
- Responses never contain tokens, principals, server paths or raw error messages. A failure carries only its reason and an optional host-defined code (`[a-z0-9-]{1,64}`); everything else goes to the server's `logger`. `new ExplorerError` throws a `RangeError` for a malformed code, and a failure document returned with one is logged and sent as `failed`.
|
|
97
101
|
|
|
102
|
+
- The `link-card` operation passes the answers of `@aiquants/markdown`'s link-card handler through unchanged (see [Link cards](#link-cards)): a card is `200` JSON with `Cache-Control: private, max-age=1800`, a link without a card `204 No Content` with `X-Link-Card-Skip`, and the handler's own errors JSON with `no-store`; each carries `nosniff` and never `Set-Cookie`. Only the explorer's own refusals in front of it (`not-found`, 405, a refused authentication) are the explorer's failure JSON above.
|
|
103
|
+
|
|
104
|
+
## Link cards
|
|
105
|
+
|
|
106
|
+
- **One route, off by default.** Cards exist only with `embeds: { linkCards: true }`; the endpoint is the data route's `link-card` operation (`<dataPath>/link-card?url=<href>`, `GET` and `HEAD`), which `defineExplorerConfig` writes into `config.embeds.linkCardEndpoint`, and no other route serves cards. With link cards off, `link-card` answers exactly as an unknown operation does (`not-found`, whatever the method). The `.data` refusal comes first, then the operation, then the method (405 with `Allow: GET, HEAD`), all before authenticating.
|
|
107
|
+
- **Read-only authentication.** The operation calls `authenticate(request, { readOnly: true })`; every other request passes `{ readOnly: false }`. In read-only mode the host only reads the session: it never refreshes a token, writes no cookie and returns no `headers`. Cards ask in the background and can still be in flight while the reader signs out, and a refreshed session cookie arriving after the sign-out deleted it would sign the reader back in.
|
|
108
|
+
- **Fail closed.** A read-only authentication that carries any header (whatever its status; an empty `headers` holds none) is a broken contract: none of its headers is forwarded, the failure is logged once through `logger` with the operation `authenticate`, and the request is answered `failed` (500, `private, no-store`, no `Set-Cookie`).
|
|
109
|
+
A malformed authentication, or an `authenticate` that throws, is answered the same way; an `ExplorerError` answers its reason without a log, as on every operation, and an aborted request is not logged.
|
|
110
|
+
Nothing logged quotes a header value: `headers` that `Headers` cannot read are refused with a fixed message (on every request, the page loader included), and a `Response` that `authenticate` throws — React Router's `throw redirect(...)`, whose headers carry cookies — is logged by its status alone. `signed-out` and `forbidden` both mean no card: the handler answers its 401 with `no-store` before fetching anything.
|
|
111
|
+
- **SSRF guards live in `@aiquants/markdown`.** The explorer creates `createLinkCardHandler` once, with `authorize` as its only option, and adds no option of its own: internal addresses never become cards in any environment.
|
|
112
|
+
The handler refuses loopback, private, link-local, CGNAT and every other non-global IPv4 or IPv6 address in every form the URL parser normalizes, internal names (`localhost`, single-label names, `.local`, `.internal`, `.home.arpa`), names that resolve to such addresses (checked again at connect time against DNS rebinding) and every redirect hop to them, and it bounds time, redirects, headers and body.
|
|
113
|
+
Its answer for such a link — `204 No Content` with `X-Link-Card-Skip: private-network` — is passed through unchanged, and `@aiquants/markdown`'s link card shows the link as a compact card and writes one `console.info` line; the explorer logs nothing of its own for cards.
|
|
114
|
+
- **Same origin.** The browser asks the page's own origin with its credentials and `redirect: "error"`, so no `connect-src` beyond `'self'` is needed and no cross-origin page can read an answer (the data route grants no CORS).
|
|
115
|
+
|
|
98
116
|
## Caches and identity
|
|
99
117
|
|
|
100
118
|
- The server's tree cache is keyed by the reader's scope (derived from the principal's stable key) and the source. `cacheScope: "shared"` lets readers share one entry when the tree does not depend on who reads it; `createFileSystemSource` uses `"principal"` unless told otherwise, which a source wrapped to filter its tree per reader must keep. Entries expire after `treeCache.ttlMs`, and the cache holds at most `treeCache.maxEntries` trees and `treeCache.maxBytes` of their JSON.
|
|
@@ -107,7 +125,7 @@ In a tree or children listing, an entry with an empty name, or with a path the b
|
|
|
107
125
|
|
|
108
126
|
## What the host must do
|
|
109
127
|
|
|
110
|
-
- Authenticate in `authenticate` and keep `principalKey` stable for one reader.
|
|
128
|
+
- Authenticate in `authenticate` and keep `principalKey` stable for one reader. Honour its `mode`: with `{ readOnly: true }` (the `link-card` operation) only read the session — never refresh a token, write no cookie and return no `headers`; the explorer refuses an authentication that does.
|
|
111
129
|
- Set `scopeSecret` from configuration, kept out of the source code and the browser: at least 32 characters, the same on every server instance.
|
|
112
130
|
- Keep the package external in your server bundle, so the sanitizer's worker file stays next to the server entry.
|
|
113
131
|
- Implement the ports: `document` and `asset` (and `children` for lazy folders) read what the explorer admitted, with the `readPath` it hands them, and report the same `version` for the same file.
|
|
@@ -47,6 +47,8 @@ Every URL resolves either to a location or to a redirect to its canonical form.
|
|
|
47
47
|
An acceptable document path is non-empty, at most 4096 UTF-16 code units long, well-formed (no lone surrogate, which no URL can carry), and contains neither a control character (U+0000–U+001F, U+007F) nor a backslash. Whether the path exists, and whether it is denied, is decided by the server for the document request; the URL itself stays as written.
|
|
48
48
|
|
|
49
49
|
The page loader answers a non-canonical URL with a redirect, and the browser applies the same canonicalization with a replacing navigation when a URL is reached without the loader (a navigation that does not revalidate). A redirect result carries the location its URL names, and the browser draws that location while the replacing navigation catches up, so the host's frame stays mounted.
|
|
50
|
+
A redirect keeps the URL's fragment on a page load and on the explorer's own replacing navigation: the browser carries the fragment across the loader's HTTP redirect of a document request (whose `Location` has none), and the replacing navigation appends the current one to the canonical URL, so loading `/docs#%E6%A6%82%E8%A6%81` ends on the tree view's URL with `#%E6%A6%82%E8%A6%81`.
|
|
51
|
+
A client-side navigation from another route to a non-canonical URL loses its fragment: the loader runs and redirects, and React Router builds the redirected location from the redirect's `Location` alone. To keep a fragment, link to the canonical URL (`explorerHref`) with the fragment appended.
|
|
50
52
|
|
|
51
53
|
## Document links
|
|
52
54
|
|
|
@@ -54,10 +56,12 @@ A Markdown link that should open another document inside the explorer is resolve
|
|
|
54
56
|
|
|
55
57
|
- `data-link-kind="doc"` and `data-doc-path`: the target's document path exactly as the explorer addresses documents — with the source's `pathPrefix`, resolved against the linking document's folder (`./` and `../` applied), percent-decoded once, without query or fragment, and an acceptable document path.
|
|
56
58
|
- The fragment of the `href` (`#section`) is kept: the explorer appends it to the link it writes. A link to a section of the document being shown is, in a page view, a router navigation like any other document link (one history entry, the hash in the URL), after which the pane scrolls to the section; in a multi-view panel a plain click only scrolls the panel and the URL keeps no hash.
|
|
59
|
+
The section is the element the fragment indicates inside the shown document, found as HTML's "select the indicated part" finds it: the fragment as written, then the fragment percent-decoded once as the URL standard decodes it (a malformed escape stays as written and a malformed UTF-8 sequence becomes U+FFFD, so a hand-typed fragment never stops the jump), each looked up first as an element's `id` and then as the `name` of an `a` element, the first match in tree order winning.
|
|
60
|
+
A heading's id (with `@aiquants/markdown` 6 or later, GitHub's automatic id such as `#概要` or `#api-の使い方`, or one written as `{#id}`), an `<a name="…">` anchor and an id in the `user-content-` namespace are the targets a document can name (see [Security](security.md#the-viewers-allow-list)).
|
|
57
61
|
Either way the section reaches the start of the pane's own scroller while every scroller around it moves only as far as needed, and the focus moves to it when it was in the document (see [Layout](layout.md#the-document-region)); a host's React Router `<ScrollRestoration>` may scroll a page view's section into view itself first (see the README's hosting notes). The rest of the `href` is not used for document links.
|
|
58
62
|
- `data-doc-exists="false"` marks a target the parser knows is missing; the link still opens it, and the document shows its `not-found` failure.
|
|
59
63
|
- A link that resolves outside every source (above the prefix, for example) keeps `data-link-kind="doc"` without `data-doc-path`; the explorer renders it inert, titled by `inertLink`, as it does a link whose `data-doc-path` is not an acceptable document path.
|
|
60
|
-
- Fragment-only links (`#section`) are ordinary links: leave them unmarked. In a page view a fragment-only link is the browser's own fragment navigation; in a multi-view panel its `href` is the tree view of the panel's document with the fragment (what a new tab or a copied address opens), and a plain click scrolls the panel instead. A fragment that
|
|
64
|
+
- Fragment-only links (`#section`) are ordinary links: leave them unmarked. In a page view a fragment-only link is the browser's own fragment navigation; in a multi-view panel its `href` is the tree view of the panel's document with the fragment (what a new tab or a copied address opens), and a plain click scrolls the panel instead. They reach the same targets, found the same way. A fragment that indicates no element of the document but HTML's top of the document (`#top` in any ASCII case, or the empty `#`) takes the pane back to its start (see [Layout](layout.md#the-document-region)).
|
|
61
65
|
- Links to other sites need no mark: the viewer renders a link without `data-link-kind` itself, in a new tab with `rel="noreferrer"` and never through the explorer's link component, when its `href` names a scheme in any case (`https:`, `HTTPS:`, `mailto:`) or another host (`//host/…`); `data-link-kind="external"` marks one explicitly. The sanitizer removes an `href` whose scheme it does not allow (`javascript:`, for example).
|
|
62
66
|
- A link keeps the attributes a converter gives it for assistive technology and footnotes: `title`, `lang`, `dir`, `role`, `aria-*`, `data-footnote-*` and an `id` in the `user-content-` namespace reach the anchor the explorer renders (the viewer hands them to the explorer's link component, and the sanitizer keeps `data-footnote-ref` and `data-footnote-backref` among the `data-*` attributes); the viewer drops any other `id` of a link.
|
|
63
67
|
A footnote reference's `id` may sit on its link (remark-gfm, `user-content-fnref-…`) or on the `sup` around it (goldmark, `fnref:N`, and `fnrefK:N` for a later reference to the same note), and a note's on its `li` (`user-content-fn-…` or `fn:N`): the viewer keeps exactly these, so each is the target of its link's `href`, and the reference keeps its `aria-describedby` and the back-reference its `aria-label`. An inert link's `title` is the explorer's reason instead of the document's.
|
|
@@ -96,7 +100,9 @@ The browser accepts the `url` of an image, media, text or file document only whe
|
|
|
96
100
|
|
|
97
101
|
## Table of contents
|
|
98
102
|
|
|
99
|
-
Each entry of the document viewer's table of contents links to its heading's own address: the shown document's address as the explorer writes it, with the router's basename, followed by `#` and the heading's id percent-encoded (`encodeURIComponent`).
|
|
103
|
+
Each entry of the document viewer's table of contents links to its heading's own address: the shown document's address as the explorer writes it, with the router's basename, followed by `#` and the heading's id percent-encoded (`encodeURIComponent`).
|
|
104
|
+
The heading `## 概要`, whose GitHub id is `概要`, links to `…#%E6%A6%82%E8%A6%81`, which the browser's address bar shows as `#概要`.
|
|
105
|
+
In the tree and single views that address is the page's URL (the same view, with its tree source and display state); in a multi-view panel it is the tree view of the panel's document, with the URL's tree source, as for the panel's fragment-only links. The entry of a heading without an id is text and links nowhere.
|
|
100
106
|
|
|
101
107
|
- A plain click on an entry, or Enter on it, makes the explorer's own jump to the heading (see [History](#history) and [Layout](layout.md#the-document-region)).
|
|
102
108
|
- A modified or middle click, a new tab or a copied address opens the entry's href, so it reaches the heading in a new page; a panel's entry opens the tree view and never duplicates the multi view.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiquants/markdown-explorer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Markdown document explorer for React Router 8 (framework mode): a source tree with a single-document view and a multi-panel reading view, storage-agnostic server ports, golden-ratio layout and an en / ja UI.",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css"
|
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@aiquants/directory-tree": "^4.1.0",
|
|
62
62
|
"@aiquants/drag-drop-panels": "^0.9.1",
|
|
63
|
-
"@aiquants/markdown": "^
|
|
63
|
+
"@aiquants/markdown": "^6.1.0",
|
|
64
64
|
"@aiquants/resize-panels": "^2.1.0",
|
|
65
|
-
"@aiquants/virtualscroll": "^3.
|
|
65
|
+
"@aiquants/virtualscroll": "^3.14.0",
|
|
66
66
|
"react": "^19.2.7",
|
|
67
67
|
"react-dom": "^19.2.7",
|
|
68
68
|
"react-router": "^8.0.0"
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@aiquants/directory-tree": "4.1.0",
|
|
72
72
|
"@aiquants/drag-drop-panels": "0.9.1",
|
|
73
|
-
"@aiquants/markdown": "
|
|
73
|
+
"@aiquants/markdown": "6.1.0",
|
|
74
74
|
"@aiquants/resize-panels": "2.1.0",
|
|
75
|
-
"@aiquants/virtualscroll": "3.
|
|
75
|
+
"@aiquants/virtualscroll": "3.14.0",
|
|
76
76
|
"@heroicons/react": "^2.2.0",
|
|
77
77
|
"@playwright/test": "^1.59.1",
|
|
78
78
|
"@tailwindcss/cli": "^4.1.18",
|