@fasl-work/caos-app-shell 0.14.1 → 0.14.3
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/dist/index.d.ts +10 -1
- package/dist/index.js +62 -3
- package/gate/inpage.mjs +24 -0
- package/gate/run.mjs +4 -1
- package/package.json +1 -1
- package/styles.css +10 -4
package/dist/index.d.ts
CHANGED
|
@@ -69,7 +69,16 @@ interface FocusShellProps {
|
|
|
69
69
|
*/
|
|
70
70
|
landmark?: boolean;
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Full-viewport, scenario-scoped analysis frame. The product supplies only its instrument and controls.
|
|
74
|
+
*
|
|
75
|
+
* The frame measures its overlay chrome and sets three lengths on `.caos-focus-stage` (custom properties, so the instrument
|
|
76
|
+
* inherits them): `--caos-focus-clear-top` (the stage's top edge to the lowest chrome box in its upper half, plus 8px),
|
|
77
|
+
* `--caos-focus-clear-bottom` (likewise from the bottom edge, 0 or unset when nothing sits in the lower half) and
|
|
78
|
+
* `--caos-focus-label-bottom` (the label's bottom edge from the stage's top). A product that renders its own instrument keeps
|
|
79
|
+
* its controls clear of the chrome with `padding-top: var(--caos-focus-clear-top, 4.6rem)`. Before the first measurement and in
|
|
80
|
+
* a non-browser render the properties are unset and the CSS fallbacks apply.
|
|
81
|
+
*/
|
|
73
82
|
declare function FocusShell({ stage, rail, title, description, hud, onExit, exitLabel, stageLabel, landmark }: FocusShellProps): react.JSX.Element;
|
|
74
83
|
|
|
75
84
|
/** Light/dark icon toggle. Shows the icon of the theme you would switch TO. */
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { useThemeStore, useShellLang, useLangStore, SHELL_TOKENS, pick, useStage
|
|
|
2
2
|
export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, formatNumber, formatTicks, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens } from './chunk-YECZXGVG.js';
|
|
3
3
|
import { APP_ROUTE, LANDING_ROUTE } from './chunk-33WTUQ7Q.js';
|
|
4
4
|
export { APP_ROUTE, LANDING_ROUTE, LANG_STORAGE_KEY, STANDARD_ROUTES, THEME_BOOT_SCRIPT, THEME_STORAGE_KEY } from './chunk-33WTUQ7Q.js';
|
|
5
|
-
import { createContext, Component, useState, useRef, useId, useEffect, useCallback, useContext, useMemo,
|
|
5
|
+
import { createContext, Component, useState, useRef, useId, useEffect, useLayoutEffect, useCallback, useContext, useMemo, Children } from 'react';
|
|
6
6
|
import { useInRouterContext, NavLink, useLocation, Navigate, Link } from 'react-router';
|
|
7
7
|
import { Sun, Moon, Languages, Boxes, CodeXml, Globe, Briefcase, Info, ChevronLeft, ChevronRight, Maximize2, RotateCw } from 'lucide-react';
|
|
8
8
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
@@ -483,10 +483,65 @@ function WorkbenchShell({
|
|
|
483
483
|
overlays
|
|
484
484
|
] });
|
|
485
485
|
}
|
|
486
|
+
var GAP = 8;
|
|
487
|
+
function measureChrome(stage) {
|
|
488
|
+
const box = stage.getBoundingClientRect();
|
|
489
|
+
if (box.height <= 0 || box.width <= 0) return;
|
|
490
|
+
const label = stage.querySelector(":scope > .caos-focus-label");
|
|
491
|
+
const chrome2 = [
|
|
492
|
+
label,
|
|
493
|
+
...Array.from(stage.querySelectorAll(":scope > .caos-focus-hud > div")),
|
|
494
|
+
stage.querySelector(":scope > .caos-focus-actions")
|
|
495
|
+
].filter((e) => e !== null);
|
|
496
|
+
const write = (name, px) => {
|
|
497
|
+
const next = px === null ? "" : `${px}px`;
|
|
498
|
+
if (stage.style.getPropertyValue(name) === next) return false;
|
|
499
|
+
if (px === null) stage.style.removeProperty(name);
|
|
500
|
+
else stage.style.setProperty(name, next);
|
|
501
|
+
return true;
|
|
502
|
+
};
|
|
503
|
+
const labelBottom = label ? Math.ceil(label.getBoundingClientRect().bottom - box.top) : null;
|
|
504
|
+
const moved = write("--caos-focus-label-bottom", labelBottom !== null && labelBottom > 0 ? labelBottom : null);
|
|
505
|
+
if (moved) {
|
|
506
|
+
measureChrome(stage);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const mid = box.height / 2;
|
|
510
|
+
let top = 0;
|
|
511
|
+
let bottom = 0;
|
|
512
|
+
for (const el of chrome2) {
|
|
513
|
+
const r = el.getBoundingClientRect();
|
|
514
|
+
if (r.width <= 0 || r.height <= 0) continue;
|
|
515
|
+
if (r.top - box.top < mid) top = Math.max(top, r.bottom - box.top + GAP);
|
|
516
|
+
else bottom = Math.max(bottom, box.bottom - r.top + GAP);
|
|
517
|
+
}
|
|
518
|
+
write("--caos-focus-clear-top", top > 0 ? Math.ceil(top) : null);
|
|
519
|
+
write("--caos-focus-clear-bottom", bottom > 0 ? Math.ceil(bottom) : null);
|
|
520
|
+
}
|
|
486
521
|
function FocusShell({ stage, rail, title, description, hud, onExit, exitLabel, stageLabel, landmark = true }) {
|
|
487
522
|
const Root = landmark ? "main" : "div";
|
|
523
|
+
const stageRef = useRef(null);
|
|
524
|
+
useLayoutEffect(() => {
|
|
525
|
+
const el = stageRef.current;
|
|
526
|
+
if (el) measureChrome(el);
|
|
527
|
+
});
|
|
528
|
+
useLayoutEffect(() => {
|
|
529
|
+
const el = stageRef.current;
|
|
530
|
+
if (!el) return void 0;
|
|
531
|
+
const run = () => measureChrome(el);
|
|
532
|
+
window.addEventListener("resize", run);
|
|
533
|
+
let ro;
|
|
534
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
535
|
+
ro = new ResizeObserver(run);
|
|
536
|
+
for (const c of Array.from(el.querySelectorAll(":scope > .caos-focus-label, :scope > .caos-focus-hud, :scope > .caos-focus-actions"))) ro.observe(c);
|
|
537
|
+
}
|
|
538
|
+
return () => {
|
|
539
|
+
window.removeEventListener("resize", run);
|
|
540
|
+
ro?.disconnect();
|
|
541
|
+
};
|
|
542
|
+
}, []);
|
|
488
543
|
return /* @__PURE__ */ jsxs(Root, { className: "caos-focus-shell", "data-focus-shell": "", children: [
|
|
489
|
-
/* @__PURE__ */ jsxs("section", { className: "caos-focus-stage", "aria-label": stageLabel, children: [
|
|
544
|
+
/* @__PURE__ */ jsxs("section", { ref: stageRef, className: "caos-focus-stage", "aria-label": stageLabel, children: [
|
|
490
545
|
/* @__PURE__ */ jsx("div", { className: "caos-focus-instrument", children: stage }),
|
|
491
546
|
/* @__PURE__ */ jsxs("div", { className: "caos-focus-label", children: [
|
|
492
547
|
/* @__PURE__ */ jsx("strong", { children: title }),
|
|
@@ -3355,6 +3410,10 @@ function CaseWorkbench(props) {
|
|
|
3355
3410
|
cases && /* @__PURE__ */ jsx(CaseSelector, { lang, ...cases, accessory: besideSelect ? enter : cases.accessory }),
|
|
3356
3411
|
variants && /* @__PURE__ */ jsx(VariantBar, { ...variants })
|
|
3357
3412
|
] }) : null;
|
|
3413
|
+
const focusPickers = cases || variants ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3414
|
+
cases && /* @__PURE__ */ jsx(CaseSelector, { lang, ...cases, accessory: cases.accessory }),
|
|
3415
|
+
variants && /* @__PURE__ */ jsx(VariantBar, { ...variants })
|
|
3416
|
+
] }) : null;
|
|
3358
3417
|
const railHead = pickers || enter ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3359
3418
|
pickers,
|
|
3360
3419
|
besideSelect ? null : enter
|
|
@@ -3382,7 +3441,7 @@ function CaseWorkbench(props) {
|
|
|
3382
3441
|
stageLabel: FOCUS_TEXT.stage[lang],
|
|
3383
3442
|
landmark: false,
|
|
3384
3443
|
stage: /* @__PURE__ */ jsx("div", { className: "caos-cw caos-cw-focus", ...instrumentData, "data-focus-group": open?.id, "data-instrument": "", children: /* @__PURE__ */ jsx("div", { className: "tabs", children: open && /* @__PURE__ */ jsx("div", { className: "tabpanel", role: "tabpanel", tabIndex: 0, "aria-label": open.label, "data-panel": open.id, children: /* @__PURE__ */ jsx(PanelBoundary, { panel: open.id, children: /* @__PURE__ */ jsx(DocRefsScope, { children: open.content }) }) }) }) }),
|
|
3385
|
-
rail: /* @__PURE__ */ jsx("div", { className: "caos-focus-rail-body", children: /* @__PURE__ */ jsx(RailBody, { rail: rail ?? null, railHead:
|
|
3444
|
+
rail: /* @__PURE__ */ jsx("div", { className: "caos-focus-rail-body", children: /* @__PURE__ */ jsx(RailBody, { rail: rail ?? null, railHead: focusPickers, railLabel }) })
|
|
3386
3445
|
}
|
|
3387
3446
|
) }) });
|
|
3388
3447
|
}
|
package/gate/inpage.mjs
CHANGED
|
@@ -1036,8 +1036,32 @@ export function installLib() {
|
|
|
1036
1036
|
const panel = stage ? stage.querySelector('[role="tabpanel"]') : null;
|
|
1037
1037
|
const st = document.querySelector('.caos-focus-stage');
|
|
1038
1038
|
const box = st ? st.getBoundingClientRect() : null;
|
|
1039
|
+
// a stage control the Focus chrome (the label, the HUD tiles, the actions) sits on: more than 1px in both directions
|
|
1040
|
+
const chrome = st
|
|
1041
|
+
? [
|
|
1042
|
+
...[...st.querySelectorAll(':scope > .caos-focus-label')].map((e) => [e, 'the stage label']),
|
|
1043
|
+
...[...st.querySelectorAll(':scope > .caos-focus-hud > div')].map((e) => [e, 'the HUD']),
|
|
1044
|
+
...[...st.querySelectorAll(':scope > .caos-focus-actions')].map((e) => [e, 'the Focus actions']),
|
|
1045
|
+
].filter(([e]) => visible(e))
|
|
1046
|
+
: [];
|
|
1047
|
+
const covered = [];
|
|
1048
|
+
const controlSel = '[role="tab"], button, a[href], select, input, textarea, summary, [role="button"], [role="radio"], [role="checkbox"], [role="switch"], [role="slider"]';
|
|
1049
|
+
for (const c of document.querySelectorAll('.caos-focus-instrument')) {
|
|
1050
|
+
for (const el of c.querySelectorAll(controlSel)) {
|
|
1051
|
+
if (covered.length >= 4) break;
|
|
1052
|
+
if (!visible(el)) continue;
|
|
1053
|
+
const r = el.getBoundingClientRect();
|
|
1054
|
+
const hit = chrome.find(([e]) => {
|
|
1055
|
+
const b = e.getBoundingClientRect();
|
|
1056
|
+
return r.left < b.right - 1 && r.right > b.left + 1 && r.top < b.bottom - 1 && r.bottom > b.top + 1;
|
|
1057
|
+
});
|
|
1058
|
+
if (hit) covered.push({ control: (el.getAttribute('aria-label') || el.textContent || el.tagName).trim().replace(/\s+/g, ' ').slice(0, 40), chrome: hit[1] });
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1039
1061
|
return {
|
|
1040
1062
|
focus: Boolean(shell),
|
|
1063
|
+
covered,
|
|
1064
|
+
enterInside: [...document.querySelectorAll('[data-focus-shell] [data-focus-enter]')].filter(visible).length,
|
|
1041
1065
|
app: Boolean(document.querySelector('[data-workbench]')),
|
|
1042
1066
|
caseId: stage ? stage.getAttribute('data-case') : null,
|
|
1043
1067
|
group: shell ? (stage ? stage.getAttribute('data-focus-group') : null) : panel ? panel.getAttribute('data-panel') : null,
|
package/gate/run.mjs
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
// Space, its hidden face out of the accessibility tree; a carousel reaches every card, never scrolls sideways
|
|
28
28
|
// G17 animated drawings an animated SVG has its reduced-motion block, plays for 10 s at most and is still under reduced motion
|
|
29
29
|
// G18 Focus mode the App's Focus control opens the case's open group in the full viewport and returns to the same case and
|
|
30
|
-
// group (by the exit control and by Escape); no page scroll, the rail without scroll, one tab row
|
|
30
|
+
// group (by the exit control and by Escape); no page scroll, the rail without scroll, one tab row; no stage
|
|
31
|
+
// control under the Focus chrome (label, HUD, actions); no Focus control inside the Focus frame
|
|
31
32
|
// plus the ADR-0071 and ADR-0017 measures (viewport, one-row tabs, rail without scroll, centred prose, captions,
|
|
32
33
|
// references).
|
|
33
34
|
|
|
@@ -889,6 +890,8 @@ export async function runGate(o) {
|
|
|
889
890
|
if (f.tabRows.some((r) => r > 1)) fail('G18', at, `${label}: a tab row wraps onto ${Math.max(...f.tabRows)} rows`);
|
|
890
891
|
if (big && f.share < opt.instrumentMin) fail('G18', at, `${label}: the stage covers ${(100 * f.share).toFixed(1)}% of the viewport (floor ${(100 * opt.instrumentMin).toFixed(0)}%)`);
|
|
891
892
|
if (f.exitInside === false) fail('G18', at, `${label}: the exit control is outside the viewport`);
|
|
893
|
+
for (const c of f.covered) fail('G18', at, `${label}: ${c.chrome} covers the control "${c.control}" (the stage must clear the Focus chrome)`);
|
|
894
|
+
if (f.enterInside > 0) fail('G18', at, `${label}: the Focus control shows inside Focus mode (it opens the mode the reader is already in)`);
|
|
892
895
|
for (const m of await page.evaluate(() => window.__caosGate.drawingText())) fail('G10', at, m);
|
|
893
896
|
if (s.contrast) for (const m of await page.evaluate(() => window.__caosGate.contrast())) fail('G13', at, m);
|
|
894
897
|
if (index === 0) await saveShot(page, `focus_${slug(s.route)}_${s.w}x${s.h}_${s.theme}_${s.lang}.png`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fasl-work/caos-app-shell",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.3",
|
|
4
4
|
"description": "Shared web-app shell, workbench primitives, content primitives, design system and measured gate for the CAOS / Faena public apps (ADR-0016, ADR-0017, ADR-0071).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Felipe Santibáñez-Leal",
|
package/styles.css
CHANGED
|
@@ -672,7 +672,7 @@ svg.lucide { flex: none; }
|
|
|
672
672
|
.caos-focus-label { position: absolute; top: 14px; left: 14px; max-width: min(46ch,calc(100% - 230px)); display: grid; gap: 4px; padding: 10px 12px; pointer-events: none; }
|
|
673
673
|
.caos-focus-label strong { font-size: 0.96rem; line-height: 1.2; }
|
|
674
674
|
.caos-focus-label span { font-size: 0.78rem; line-height: 1.4; color: var(--color-fg-subtle); }
|
|
675
|
-
.caos-focus-hud { position: absolute; left: 14px; top:
|
|
675
|
+
.caos-focus-hud { position: absolute; left: 14px; top: calc(var(--caos-focus-label-bottom, 96px) + 8px); display: grid; gap: 6px; pointer-events: none; }
|
|
676
676
|
.caos-focus-hud > div { display: grid; gap: 1px; padding: 7px 10px; min-width: 120px; }
|
|
677
677
|
.caos-focus-hud strong { font: 700 1rem var(--font-mono,monospace); }
|
|
678
678
|
.caos-focus-hud span { color: var(--color-fg-subtle); font-size: 0.61rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
@@ -754,6 +754,8 @@ svg.lucide { flex: none; }
|
|
|
754
754
|
.caos-exec-badge { align-self: flex-start; padding: 0.1rem 0.5rem; font-size: var(--text-xs); border: 1px solid var(--color-border); border-radius: 999px; color: var(--color-fg-subtle); }
|
|
755
755
|
.caos-exec-cause { min-width: 0; overflow-wrap: anywhere; }
|
|
756
756
|
.caos-exec-cause-head { display: flex; justify-content: space-between; gap: 1rem; }
|
|
757
|
+
/* the cause's share stays whole: the row lets its label wrap anywhere, and the squeezed value broke into "3", "5", "%" at 390px */
|
|
758
|
+
.caos-exec-cause-head > .caos-exec-value { flex: none; white-space: nowrap; overflow-wrap: normal; }
|
|
757
759
|
.caos-exec-bar { height: 0.45rem; margin-top: 0.3rem; background: var(--color-surface-2); border-radius: 999px; overflow: hidden; }
|
|
758
760
|
.caos-exec-bar > span { display: block; height: 100%; background: var(--color-accent); }
|
|
759
761
|
.caos-exec-cdef { font-size: var(--text-xs); line-height: 1.4; color: var(--color-fg-faint); }
|
|
@@ -823,11 +825,15 @@ svg.lucide { flex: none; }
|
|
|
823
825
|
|
|
824
826
|
/* == Focus mode for the App (shell 0.14.0, ADR-0070) ================================================================== */
|
|
825
827
|
|
|
826
|
-
/* The stage is the open group; the label overlay (heading and line) sits above it, so the group starts below the label.
|
|
827
|
-
|
|
828
|
+
/* The stage is the open group; the label overlay (heading and line) sits above it, so the group starts below the label.
|
|
829
|
+
FocusShell measures the overlay chrome and sets, on .caos-focus-stage, three lengths the instrument inherits (shell 0.14.3):
|
|
830
|
+
--caos-focus-clear-top (stage top to the lowest chrome box in the upper half, plus 8px), --caos-focus-clear-bottom (stage
|
|
831
|
+
bottom to the highest chrome box in the lower half, plus 8px; unset when none) and --caos-focus-label-bottom (the label's
|
|
832
|
+
bottom edge). The fallbacks below apply before the first measurement and in a non-browser render. */
|
|
833
|
+
.caos-focus-instrument > .caos-cw-focus { position: absolute; inset: 0; padding: var(--caos-focus-clear-top, 4.6rem) 0.9rem max(0.9rem, var(--caos-focus-clear-bottom, 0px)); }
|
|
828
834
|
.caos-focus-instrument > .caos-cw-focus > .tabs { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
|
|
829
835
|
.caos-focus-rail-body { display: flex; flex-direction: column; gap: var(--space-3); padding: 0.85rem; min-width: 0; }
|
|
830
836
|
.caos-focus-enter { align-self: flex-start; }
|
|
831
837
|
@media (max-width: 900px) {
|
|
832
|
-
.caos-focus-instrument > .caos-cw-focus { padding: 6.6rem 0.5rem 0.5rem; }
|
|
838
|
+
.caos-focus-instrument > .caos-cw-focus { padding: var(--caos-focus-clear-top, 6.6rem) 0.5rem max(0.5rem, var(--caos-focus-clear-bottom, 0px)); }
|
|
833
839
|
}
|