@fasl-work/caos-app-shell 0.15.0 → 0.17.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/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, useLayoutEffect, useCallback, useContext, useMemo, Children } from 'react';
5
+ import { createContext, Component, useState, useRef, useId, useEffect, useLayoutEffect, useCallback, useContext, useMemo, Children, isValidElement } 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';
@@ -540,7 +540,7 @@ function FocusShell({ stage, rail, title, description, hud, onExit, exitLabel, s
540
540
  ro?.disconnect();
541
541
  };
542
542
  }, []);
543
- return /* @__PURE__ */ jsxs(Root, { className: "caos-focus-shell", "data-focus-shell": "", children: [
543
+ return /* @__PURE__ */ jsxs(Root, { className: rail === null || rail === void 0 ? "caos-focus-shell caos-focus-norail" : "caos-focus-shell", "data-focus-shell": "", children: [
544
544
  /* @__PURE__ */ jsxs("section", { ref: stageRef, className: "caos-focus-stage", "aria-label": stageLabel, children: [
545
545
  /* @__PURE__ */ jsx("div", { className: "caos-focus-instrument", children: stage }),
546
546
  /* @__PURE__ */ jsxs("div", { className: "caos-focus-label", children: [
@@ -557,7 +557,7 @@ function FocusShell({ stage, rail, title, description, hud, onExit, exitLabel, s
557
557
  /* @__PURE__ */ jsx("button", { type: "button", "data-focus-exit": "", onClick: onExit, children: exitLabel })
558
558
  ] })
559
559
  ] }),
560
- /* @__PURE__ */ jsx("aside", { className: "caos-focus-rail", "data-rail": "", children: rail })
560
+ rail !== null && rail !== void 0 && /* @__PURE__ */ jsx("aside", { className: "caos-focus-rail", "data-rail": "", children: rail })
561
561
  ] });
562
562
  }
563
563
 
@@ -795,17 +795,64 @@ function CaseSelector(props) {
795
795
  const visible = casesInSource(cases, source);
796
796
  const groups = groupByCategory(visible);
797
797
  const modified = cases.find((c) => c.id === modifiedFromId) ?? null;
798
+ const wrapRef = useRef(null);
799
+ const [minHeight, setMinHeight] = useState(null);
800
+ const nameKey = visible.map((c) => c.name).join("");
801
+ useLayoutEffect(() => {
802
+ const wrap = wrapRef.current;
803
+ const display = wrap?.querySelector(".cs-select-display");
804
+ if (!wrap || !display) return void 0;
805
+ const names = nameKey ? nameKey.split("") : [];
806
+ const measure = () => {
807
+ const st = getComputedStyle(display);
808
+ const room = display.clientWidth - parseFloat(st.paddingLeft) - parseFloat(st.paddingRight);
809
+ if (!(room > 0)) return;
810
+ const copy = document.createElement("span");
811
+ copy.className = "cs-select-text";
812
+ copy.setAttribute("aria-hidden", "true");
813
+ copy.setAttribute("style", `position:absolute;visibility:hidden;left:0;top:0;width:${room}px;display:block;overflow:visible;max-height:none;-webkit-line-clamp:unset;line-clamp:unset`);
814
+ display.append(copy);
815
+ let tallest = 0;
816
+ for (const n of names) {
817
+ copy.textContent = n;
818
+ tallest = Math.max(tallest, copy.getBoundingClientRect().height);
819
+ }
820
+ const line = parseFloat(getComputedStyle(copy).lineHeight) || parseFloat(st.fontSize) * 1.25;
821
+ copy.remove();
822
+ const frame = parseFloat(st.paddingTop) + parseFloat(st.paddingBottom) + parseFloat(st.borderTopWidth) + parseFloat(st.borderBottomWidth);
823
+ const next = tallest > line * 1.5 ? Math.ceil((tallest + frame) * 100) / 100 : null;
824
+ setMinHeight((prev) => prev === next ? prev : next);
825
+ };
826
+ measure();
827
+ let width = wrap.getBoundingClientRect().width;
828
+ const ro = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
829
+ const w = wrap.getBoundingClientRect().width;
830
+ if (w !== width) {
831
+ width = w;
832
+ measure();
833
+ }
834
+ });
835
+ ro?.observe(wrap);
836
+ void document.fonts?.ready.then(measure);
837
+ return () => ro?.disconnect();
838
+ }, [layout, nameKey]);
839
+ const options = groups.flatMap((g) => g.cases);
840
+ const selectedName = (options.find((c) => c.id === selectedId) ?? options.find((c) => !c.disabled))?.name ?? "";
798
841
  const select = /* @__PURE__ */ jsx(
799
842
  "select",
800
843
  {
801
844
  "data-control": "case",
802
- className: "select cs-select",
845
+ className: "select cs-select cs-select-native",
803
846
  "aria-label": ariaLabel ?? "Case selector",
804
847
  value: selectedId,
805
848
  onChange: (e) => onSelect(e.target.value),
806
849
  children: groups.map((g) => /* @__PURE__ */ jsx("optgroup", { label: g.category || " ", children: g.cases.map((c) => /* @__PURE__ */ jsx("option", { value: c.id, disabled: c.disabled, title: caseTooltip(c) || void 0, children: c.name }, c.id)) }, g.category || "_"))
807
850
  }
808
851
  );
852
+ const selectBox = /* @__PURE__ */ jsxs("div", { className: "cs-select-wrap", ref: wrapRef, children: [
853
+ /* @__PURE__ */ jsx("div", { className: "cs-select-display", style: minHeight === null ? void 0 : { minHeight }, "aria-hidden": "true", children: /* @__PURE__ */ jsx("span", { className: "cs-select-text", children: selectedName }) }),
854
+ select
855
+ ] });
809
856
  return /* @__PURE__ */ jsxs("div", { className: `cs${className ? ` ${className}` : ""}`, role: "group", "aria-label": ariaLabel ?? "Case selector", children: [
810
857
  source && lanes.length > 1 && /* @__PURE__ */ jsxs("div", { className: "cs-source", children: [
811
858
  /* @__PURE__ */ jsx("span", { className: "cs-source-label", children: t2.source }),
@@ -824,9 +871,9 @@ function CaseSelector(props) {
824
871
  ] }),
825
872
  source && source !== "synthetic" && /* @__PURE__ */ jsx("p", { className: "cs-locked banner", children: lockedNote ?? t2.lockedNote }),
826
873
  layout === "select" && (props.accessory ? /* @__PURE__ */ jsxs("div", { className: "cs-select-row", children: [
827
- select,
874
+ selectBox,
828
875
  props.accessory
829
- ] }) : select),
876
+ ] }) : selectBox),
830
877
  layout === "chips" && groups.map((g) => /* @__PURE__ */ jsxs("div", { className: "cs-group", role: "group", "aria-label": g.category || void 0, "data-control": "case", children: [
831
878
  g.category && /* @__PURE__ */ jsx("span", { className: "cs-group-label", children: g.category }),
832
879
  /* @__PURE__ */ jsx("div", { className: "cs-chips", children: g.cases.map((c) => {
@@ -867,7 +914,8 @@ function caseKindOf(c) {
867
914
  return c.kind ?? "synthetic";
868
915
  }
869
916
  var ViewMemoryContext = createContext(null);
870
- var memoryKey = (kind, ariaLabel, ids) => `${kind}|${ariaLabel ?? ""}|${ids.join(",")}`;
917
+ var PanelPathContext = createContext("");
918
+ var memoryKey = (kind, path, ids) => `${kind}|${path}|${ids.join(",")}`;
871
919
  var CitationsContext = createContext({ list: [], byId: {} });
872
920
  function CitationsProvider({ items, children }) {
873
921
  const byId = {};
@@ -964,9 +1012,9 @@ function DocReferences({ ids }) {
964
1012
  const c = byId[id];
965
1013
  const h = href(c);
966
1014
  return /* @__PURE__ */ jsxs("li", { children: [
967
- /* @__PURE__ */ jsx("span", { children: c.citation }),
1015
+ /* @__PURE__ */ jsx("span", { translate: "no", children: c.citation }),
968
1016
  " ",
969
- h ? /* @__PURE__ */ jsx("a", { href: h, target: "_blank", rel: "noreferrer noopener", className: "faint", children: c.doi ? `doi:${c.doi}` : "link" }) : null
1017
+ h ? /* @__PURE__ */ jsx("a", { href: h, target: "_blank", rel: "noreferrer noopener", className: "faint", children: c.doi ? /* @__PURE__ */ jsx("span", { translate: "no", children: `doi:${c.doi}` }) : "link" }) : null
970
1018
  ] }, id);
971
1019
  }) })
972
1020
  ] });
@@ -990,19 +1038,20 @@ function ReferenceList({ ids, heading }) {
990
1038
  /* @__PURE__ */ jsx("ol", { className: "reference-list", children: items.map((c) => {
991
1039
  const h = href(c);
992
1040
  return /* @__PURE__ */ jsxs("li", { children: [
993
- /* @__PURE__ */ jsx("span", { children: c.citation }),
1041
+ /* @__PURE__ */ jsx("span", { translate: "no", children: c.citation }),
994
1042
  " ",
995
- h ? /* @__PURE__ */ jsx("a", { href: h, target: "_blank", rel: "noreferrer noopener", className: "faint", children: c.doi ? `doi:${c.doi}` : "link" }) : null
1043
+ h ? /* @__PURE__ */ jsx("a", { href: h, target: "_blank", rel: "noreferrer noopener", className: "faint", children: c.doi ? /* @__PURE__ */ jsx("span", { translate: "no", children: `doi:${c.doi}` }) : "link" }) : null
996
1044
  ] }, c.id);
997
1045
  }) })
998
1046
  ] });
999
1047
  }
1000
1048
  var MAX_PEER_TABS = 6;
1001
- function Tabs({ tabs, initial, value, onChange, ariaLabel }) {
1049
+ function Tabs({ tabs, initial, value, onChange, ariaLabel, trailing }) {
1002
1050
  const baseId = useId();
1003
1051
  const first = tabs[0]?.id ?? "";
1004
1052
  const memory = useContext(ViewMemoryContext);
1005
- const mkey = memoryKey("tabs", ariaLabel, tabs.map((t2) => t2.id));
1053
+ const path = useContext(PanelPathContext);
1054
+ const mkey = memoryKey("tabs", path, tabs.map((t2) => t2.id));
1006
1055
  const [own, setOwn] = useState(() => {
1007
1056
  const kept = memory?.get(mkey);
1008
1057
  return kept && tabs.some((t2) => t2.id === kept) ? kept : initial ?? first;
@@ -1033,7 +1082,7 @@ function Tabs({ tabs, initial, value, onChange, ariaLabel }) {
1033
1082
  }
1034
1083
  }
1035
1084
  const current = tabs.find((t2) => t2.id === active) ?? tabs[0];
1036
- return /* @__PURE__ */ jsxs("div", { className: "tabs", children: [
1085
+ return /* @__PURE__ */ jsxs("div", { className: trailing === void 0 ? "tabs" : "tabs tabs-has-slot", children: [
1037
1086
  /* @__PURE__ */ jsx("div", { className: "tablist", role: "tablist", "aria-label": ariaLabel, ref: rowRef, children: tabs.map((tab, idx) => {
1038
1087
  const selected = tab.id === current?.id;
1039
1088
  return /* @__PURE__ */ jsx(
@@ -1054,6 +1103,7 @@ function Tabs({ tabs, initial, value, onChange, ariaLabel }) {
1054
1103
  tab.id
1055
1104
  );
1056
1105
  }) }),
1106
+ trailing !== void 0 && /* @__PURE__ */ jsx("div", { className: "tabs-slot", "data-tabs-slot": "", children: trailing }),
1057
1107
  current && /* @__PURE__ */ jsx(
1058
1108
  "div",
1059
1109
  {
@@ -1063,7 +1113,7 @@ function Tabs({ tabs, initial, value, onChange, ariaLabel }) {
1063
1113
  tabIndex: 0,
1064
1114
  className: "tabpanel",
1065
1115
  "data-panel": current.id,
1066
- children: /* @__PURE__ */ jsx(PanelBoundary, { panel: current.id, children: /* @__PURE__ */ jsx(DocRefsScope, { children: current.content }) })
1116
+ children: /* @__PURE__ */ jsx(PanelPathContext.Provider, { value: `${path}/${current.id}`, children: /* @__PURE__ */ jsx(PanelBoundary, { panel: current.id, children: /* @__PURE__ */ jsx(DocRefsScope, { children: current.content }) }) })
1067
1117
  },
1068
1118
  current.id
1069
1119
  )
@@ -1073,7 +1123,8 @@ function SubTabs({ tabs, initial, value, onChange, ariaLabel, orientation = "hor
1073
1123
  const baseId = useId();
1074
1124
  const first = tabs[0]?.id ?? "";
1075
1125
  const memory = useContext(ViewMemoryContext);
1076
- const mkey = memoryKey("subtabs", ariaLabel, tabs.map((t2) => t2.id));
1126
+ const path = useContext(PanelPathContext);
1127
+ const mkey = memoryKey("subtabs", path, tabs.map((t2) => t2.id));
1077
1128
  const [own, setOwn] = useState(() => {
1078
1129
  const kept = memory?.get(mkey);
1079
1130
  return kept && tabs.some((t2) => t2.id === kept) ? kept : initial ?? first;
@@ -1147,7 +1198,7 @@ function SubTabs({ tabs, initial, value, onChange, ariaLabel, orientation = "hor
1147
1198
  tabIndex: 0,
1148
1199
  className: "subtabpanel",
1149
1200
  "data-panel": current.id,
1150
- children: /* @__PURE__ */ jsx(PanelBoundary, { panel: current.id, children: /* @__PURE__ */ jsx(DocRefsScope, { children: current.content }) })
1201
+ children: /* @__PURE__ */ jsx(PanelPathContext.Provider, { value: `${path}/${current.id}`, children: /* @__PURE__ */ jsx(PanelBoundary, { panel: current.id, children: /* @__PURE__ */ jsx(DocRefsScope, { children: current.content }) }) })
1151
1202
  },
1152
1203
  current.id
1153
1204
  ) })
@@ -2920,20 +2971,155 @@ function useUrlView(ids, param, value, onChange) {
2920
2971
  function viewParamOf(option) {
2921
2972
  return option === false ? null : typeof option === "string" ? option : "view";
2922
2973
  }
2923
- function SurfacePage({ title, lede, actions, tabs, value, onChange, deepLinkView = true, ariaLabel, instrument, children }) {
2974
+
2975
+ // src/workbench/focus.ts
2976
+ var FOCUS_TEXT = {
2977
+ enter: { en: "Focus", es: "Enfocar" },
2978
+ enterLabel: { en: "Open this case in focus mode", es: "Abrir este caso en modo enfoque" },
2979
+ enterViewLabel: { en: "Open this view in focus mode", es: "Abrir esta vista en modo enfoque" },
2980
+ exit: { en: "Back to the App", es: "Volver a la App" },
2981
+ stage: { en: "Focus stage", es: "Escenario de enfoque" }
2982
+ };
2983
+ var FOCUS_PARAM = "focus";
2984
+
2985
+ // src/shell/useFocusMode.ts
2986
+ function useFocusMode(cfg, options = {}) {
2987
+ const enabled = options.enabled ?? true;
2988
+ const param = cfg?.param ?? FOCUS_PARAM;
2989
+ const [on, setOn] = useState(
2990
+ () => cfg !== null && typeof window !== "undefined" && readCaseParam(window.location.search, param) === "1" && (options.initialAllowed?.() ?? true)
2991
+ );
2992
+ const enterRef = useRef(null);
2993
+ const memory = useRef(/* @__PURE__ */ new Map());
2994
+ const turned = useRef(null);
2995
+ const onChangeRef = useRef(cfg?.onChange);
2996
+ onChangeRef.current = cfg?.onChange;
2997
+ const setFocus = useCallback(
2998
+ (next) => {
2999
+ turned.current = next ? "in" : "out";
3000
+ setOn(next);
3001
+ if (next) replaceQueryParam(param, "1");
3002
+ else removeQueryParam(param);
3003
+ onChangeRef.current?.(next);
3004
+ },
3005
+ [param]
3006
+ );
3007
+ const show = cfg !== null && on && enabled;
3008
+ useEffect(() => {
3009
+ if (cfg === null || enabled) return;
3010
+ if (on) setOn(false);
3011
+ if (typeof window !== "undefined" && readCaseParam(window.location.search, param) === "1") removeQueryParam(param);
3012
+ }, [cfg === null, enabled, on, param]);
3013
+ useEffect(() => {
3014
+ if (!show) return void 0;
3015
+ const onKey = (e) => {
3016
+ if (e.key !== "Escape" || e.defaultPrevented) return;
3017
+ e.preventDefault();
3018
+ setFocus(false);
3019
+ };
3020
+ document.addEventListener("keydown", onKey);
3021
+ return () => document.removeEventListener("keydown", onKey);
3022
+ }, [show, setFocus]);
3023
+ useEffect(() => {
3024
+ if (!show) return void 0;
3025
+ const frame = document.querySelector("[data-focus-shell]");
3026
+ if (!frame) return void 0;
3027
+ const made = [];
3028
+ for (let node = frame; node && node !== document.body; node = node.parentElement) {
3029
+ for (const sib of Array.from(node.parentElement?.children ?? [])) {
3030
+ if (sib !== node && !sib.hasAttribute("inert") && sib.tagName !== "SCRIPT" && sib.tagName !== "STYLE") {
3031
+ sib.setAttribute("inert", "");
3032
+ made.push(sib);
3033
+ }
3034
+ }
3035
+ }
3036
+ return () => {
3037
+ for (const el of made) el.removeAttribute("inert");
3038
+ };
3039
+ }, [show]);
3040
+ useEffect(() => {
3041
+ if (turned.current === "in" && show) document.querySelector("[data-focus-exit]")?.focus();
3042
+ else if (turned.current === "out" && !show) enterRef.current?.focus();
3043
+ turned.current = null;
3044
+ }, [show]);
3045
+ return { show, setFocus, enterRef, memory: memory.current };
3046
+ }
3047
+ function textOf(node) {
3048
+ if (typeof node === "string" || typeof node === "number") return String(node);
3049
+ if (Array.isArray(node)) return node.map(textOf).join("");
3050
+ if (isValidElement(node)) return textOf(node.props.children);
3051
+ return "";
3052
+ }
3053
+ var viewFocus = (tab) => !tab || !tab.focus ? null : tab.focus === true ? {} : tab.focus;
3054
+ function SurfacePage({ title, lede, actions, tabs, value, onChange, deepLinkView = true, ariaLabel, instrument, focus, children }) {
2924
3055
  const lang = useShellLang();
2925
- const [open, select] = useUrlView((tabs ?? []).map((t2) => t2.id), tabs ? viewParamOf(deepLinkView) : null, value, onChange);
2926
- const head = title !== void 0 || lede !== void 0 || actions !== void 0;
2927
- return /* @__PURE__ */ jsxs("div", { className: "page-body wide caos-surface", "data-surface": "", children: [
3056
+ const placeAbove = useContext(PanelPathContext);
3057
+ const ids = (tabs ?? []).map((t2) => t2.id);
3058
+ const viewParam = tabs ? viewParamOf(deepLinkView) : null;
3059
+ const [open, select] = useUrlView(ids, viewParam, value, onChange);
3060
+ const focusCfg = focus === true ? {} : focus || null;
3061
+ const openTab = tabs?.find((t2) => t2.id === open) ?? tabs?.[0];
3062
+ const openFocus = focusCfg ? viewFocus(openTab) : null;
3063
+ const mode = useFocusMode(focusCfg, {
3064
+ enabled: openFocus !== null,
3065
+ // a link opens Focus only on a view with a stage: the view it names (the first one when it names none), never an unknown one
3066
+ initialAllowed: () => {
3067
+ const named = value ?? (viewParam && typeof window !== "undefined" ? readCaseParam(window.location.search, viewParam) : null);
3068
+ if (named !== null && !ids.includes(named)) return false;
3069
+ return viewFocus(tabs?.find((t2) => t2.id === (named ?? ids[0]))) !== null;
3070
+ }
3071
+ });
3072
+ const reserve = focusCfg !== null && Boolean(tabs?.some((t2) => viewFocus(t2) !== null));
3073
+ const idle = openFocus === null;
3074
+ const enter = reserve && /* @__PURE__ */ jsxs(
3075
+ "button",
3076
+ {
3077
+ ref: mode.enterRef,
3078
+ type: "button",
3079
+ className: "btn caos-focus-enter",
3080
+ "data-focus-enter": "",
3081
+ "aria-label": FOCUS_TEXT.enterViewLabel[lang],
3082
+ title: FOCUS_TEXT.enterViewLabel[lang],
3083
+ "aria-hidden": idle ? true : void 0,
3084
+ tabIndex: idle ? -1 : void 0,
3085
+ style: idle ? { visibility: "hidden" } : void 0,
3086
+ onClick: () => mode.setFocus(true),
3087
+ children: [
3088
+ /* @__PURE__ */ jsx(Maximize2, { size: 14, "aria-hidden": "true" }),
3089
+ /* @__PURE__ */ jsx("span", { children: FOCUS_TEXT.enter[lang] })
3090
+ ]
3091
+ }
3092
+ );
3093
+ if (mode.show && openTab && openFocus) {
3094
+ const label = textOf(openTab.label);
3095
+ const surfaceTitle = title !== void 0 ? pick(title, lang) : "";
3096
+ return /* @__PURE__ */ jsx(ViewMemoryContext.Provider, { value: mode.memory, children: /* @__PURE__ */ jsx(
3097
+ FocusShell,
3098
+ {
3099
+ title: openFocus.title !== void 0 ? pick(openFocus.title, lang) : label,
3100
+ description: openFocus.description !== void 0 ? pick(openFocus.description, lang) : surfaceTitle,
3101
+ hud: (openFocus.hud ?? []).map((h) => ({ label: pick(h.label, lang), value: h.value })),
3102
+ onExit: () => mode.setFocus(false),
3103
+ exitLabel: FOCUS_TEXT.exit[lang],
3104
+ stageLabel: FOCUS_TEXT.stage[lang],
3105
+ landmark: false,
3106
+ stage: /* @__PURE__ */ jsx("div", { className: "caos-surface-focus", "data-surface-focus": "", "data-view-param": viewParam ?? "", "data-focus-param": focusCfg?.param ?? FOCUS_PARAM, "data-focus-view": openTab.id, "data-focus-group": openTab.id, "data-instrument": "", children: /* @__PURE__ */ jsx("div", { className: "tabs", children: /* @__PURE__ */ jsx("div", { className: "tabpanel", role: "tabpanel", tabIndex: 0, "aria-label": label || void 0, "data-panel": openTab.id, children: /* @__PURE__ */ jsx(PanelPathContext.Provider, { value: `${placeAbove}/${openTab.id}`, children: /* @__PURE__ */ jsx(PanelBoundary, { panel: openTab.id, children: /* @__PURE__ */ jsx(DocRefsScope, { children: openTab.content }) }) }) }) }) }),
3107
+ rail: openFocus.rail !== void 0 && openFocus.rail !== null ? /* @__PURE__ */ jsx("div", { className: "caos-focus-rail-body", children: openFocus.rail }) : null
3108
+ }
3109
+ ) });
3110
+ }
3111
+ const head = title !== void 0 || lede !== void 0 || actions !== void 0 && actions !== null;
3112
+ const page = /* @__PURE__ */ jsxs("div", { className: "page-body wide caos-surface", "data-surface": "", "data-view-param": reserve ? viewParam ?? "" : void 0, children: [
2928
3113
  head && /* @__PURE__ */ jsxs("div", { className: "caos-surface-head", children: [
2929
3114
  /* @__PURE__ */ jsxs("div", { className: "caos-surface-heading", children: [
2930
3115
  title !== void 0 && /* @__PURE__ */ jsx("h1", { className: "caos-surface-title", children: pick(title, lang) }),
2931
3116
  lede !== void 0 && /* @__PURE__ */ jsx("p", { className: "caos-surface-lede", children: lede })
2932
3117
  ] }),
2933
- actions !== void 0 && /* @__PURE__ */ jsx("div", { className: "caos-surface-actions", children: actions })
3118
+ actions !== void 0 && actions !== null && /* @__PURE__ */ jsx("div", { className: "caos-surface-actions", children: actions })
2934
3119
  ] }),
2935
- /* @__PURE__ */ jsx("div", { className: "caos-surface-body", "data-instrument": instrument ? "" : void 0, children: tabs ? /* @__PURE__ */ jsx(Tabs, { tabs, value: open, onChange: select, ariaLabel: pick(ariaLabel ?? title ?? { en: "Views", es: "Vistas" }, lang) }) : children })
3120
+ /* @__PURE__ */ jsx("div", { className: "caos-surface-body", "data-instrument": instrument ? "" : void 0, children: tabs ? /* @__PURE__ */ jsx(Tabs, { tabs, value: open, onChange: select, ariaLabel: pick(ariaLabel ?? title ?? { en: "Views", es: "Vistas" }, lang), trailing: enter || void 0 }) : children })
2936
3121
  ] });
3122
+ return focusCfg ? /* @__PURE__ */ jsx(ViewMemoryContext.Provider, { value: mode.memory, children: page }) : page;
2937
3123
  }
2938
3124
 
2939
3125
  // src/lib/breakpoints.ts
@@ -3298,15 +3484,6 @@ function VariantBar({ variants, activeId, onSelect, title, lane }) {
3298
3484
  active?.note && /* @__PURE__ */ jsx("p", { className: "caos-variant-note", children: pick(active.note, lang) })
3299
3485
  ] });
3300
3486
  }
3301
-
3302
- // src/workbench/focus.ts
3303
- var FOCUS_TEXT = {
3304
- enter: { en: "Focus", es: "Enfocar" },
3305
- enterLabel: { en: "Open this case in focus mode", es: "Abrir este caso en modo enfoque" },
3306
- exit: { en: "Back to the App", es: "Volver a la App" },
3307
- stage: { en: "Focus stage", es: "Escenario de enfoque" }
3308
- };
3309
- var FOCUS_PARAM = "focus";
3310
3487
  var MAX_WORKBENCH_GROUPS = MAX_PEER_TABS;
3311
3488
  function panel(id, lane, provenance, content) {
3312
3489
  return /* @__PURE__ */ jsx("div", { className: "caos-cw-panel", "data-group": id, "data-lane": lane, "data-provenance": provenance, children: content });
@@ -3315,56 +3492,8 @@ function CaseWorkbench(props) {
3315
3492
  const { caseId, cases, source, controls, replayOnly, variants, rail, railLabel, groups, compare, context: context2, group, onGroupChange, deepLinkView = true, ariaLabel, focus } = props;
3316
3493
  const lang = useShellLang();
3317
3494
  const focusCfg = focus === true ? {} : focus || null;
3318
- const focusParam = focusCfg?.param ?? FOCUS_PARAM;
3319
- const [focusOn, setFocusOn] = useState(() => focusCfg !== null && typeof window !== "undefined" && readCaseParam(window.location.search, focusParam) === "1");
3320
- const enterRef = useRef(null);
3321
- const memory = useRef(/* @__PURE__ */ new Map());
3322
- const turned = useRef(null);
3323
- const onChangeRef = useRef(focusCfg?.onChange);
3324
- onChangeRef.current = focusCfg?.onChange;
3325
- const setFocus = useCallback(
3326
- (on) => {
3327
- turned.current = on ? "in" : "out";
3328
- setFocusOn(on);
3329
- if (on) replaceQueryParam(focusParam, "1");
3330
- else removeQueryParam(focusParam);
3331
- onChangeRef.current?.(on);
3332
- },
3333
- [focusParam]
3334
- );
3335
- const showFocus = focusCfg !== null && focusOn;
3336
- useEffect(() => {
3337
- if (!showFocus) return void 0;
3338
- const onKey = (e) => {
3339
- if (e.key !== "Escape" || e.defaultPrevented) return;
3340
- e.preventDefault();
3341
- setFocus(false);
3342
- };
3343
- document.addEventListener("keydown", onKey);
3344
- return () => document.removeEventListener("keydown", onKey);
3345
- }, [showFocus, setFocus]);
3346
- useEffect(() => {
3347
- if (!showFocus) return void 0;
3348
- const frame = document.querySelector("[data-focus-shell]");
3349
- if (!frame) return void 0;
3350
- const made = [];
3351
- for (let node = frame; node && node !== document.body; node = node.parentElement) {
3352
- for (const sib of Array.from(node.parentElement?.children ?? [])) {
3353
- if (sib !== node && !sib.hasAttribute("inert") && sib.tagName !== "SCRIPT" && sib.tagName !== "STYLE") {
3354
- sib.setAttribute("inert", "");
3355
- made.push(sib);
3356
- }
3357
- }
3358
- }
3359
- return () => {
3360
- for (const el of made) el.removeAttribute("inert");
3361
- };
3362
- }, [showFocus]);
3363
- useEffect(() => {
3364
- if (turned.current === "in" && showFocus) document.querySelector("[data-focus-exit]")?.focus();
3365
- else if (turned.current === "out" && !showFocus) enterRef.current?.focus();
3366
- turned.current = null;
3367
- }, [showFocus]);
3495
+ const { show: showFocus, setFocus, enterRef, memory } = useFocusMode(focusCfg);
3496
+ const placeAbove = useContext(PanelPathContext);
3368
3497
  const variantId = variants?.activeId;
3369
3498
  const stateKey = useMemo(
3370
3499
  () => makeStateKey({ caseId, variantId: variantId ?? null, source: source ?? null, controls: controls ?? {} }),
@@ -3405,19 +3534,12 @@ function CaseWorkbench(props) {
3405
3534
  ]
3406
3535
  }
3407
3536
  );
3408
- const besideSelect = Boolean(cases && (cases.layout ?? "chips") === "select");
3409
3537
  const pickers = cases || variants ? /* @__PURE__ */ jsxs(Fragment, { children: [
3410
- cases && /* @__PURE__ */ jsx(CaseSelector, { lang, ...cases, accessory: besideSelect ? enter : cases.accessory }),
3538
+ cases && /* @__PURE__ */ jsx(CaseSelector, { lang, ...cases }),
3411
3539
  variants && /* @__PURE__ */ jsx(VariantBar, { ...variants })
3412
3540
  ] }) : 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;
3417
- const railHead = pickers || enter ? /* @__PURE__ */ jsxs(Fragment, { children: [
3418
- pickers,
3419
- besideSelect ? null : enter
3420
- ] }) : null;
3541
+ const railHead = pickers;
3542
+ const focusPickers = pickers;
3421
3543
  const instrumentData = {
3422
3544
  "data-case-workbench": "",
3423
3545
  "data-group-count": tabs.length,
@@ -3430,7 +3552,7 @@ function CaseWorkbench(props) {
3430
3552
  if (showFocus && focusCfg) {
3431
3553
  const open = tabs.find((t2) => t2.id === openGroup) ?? tabs[0];
3432
3554
  const caseName = cases?.cases.find((c) => c.id === caseId)?.name ?? caseId;
3433
- return /* @__PURE__ */ jsx(ViewMemoryContext.Provider, { value: memory.current, children: /* @__PURE__ */ jsx(WorkbenchStateContext.Provider, { value: { stateKey, caseId, variantId, source }, children: /* @__PURE__ */ jsx(
3555
+ return /* @__PURE__ */ jsx(ViewMemoryContext.Provider, { value: memory, children: /* @__PURE__ */ jsx(WorkbenchStateContext.Provider, { value: { stateKey, caseId, variantId, source }, children: /* @__PURE__ */ jsx(
3434
3556
  FocusShell,
3435
3557
  {
3436
3558
  title: focusCfg.title !== void 0 ? pick(focusCfg.title, lang) : caseName,
@@ -3440,18 +3562,19 @@ function CaseWorkbench(props) {
3440
3562
  exitLabel: FOCUS_TEXT.exit[lang],
3441
3563
  stageLabel: FOCUS_TEXT.stage[lang],
3442
3564
  landmark: false,
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 }) }) }) }) }),
3565
+ stage: /* @__PURE__ */ jsx("div", { className: "caos-cw caos-cw-focus", ...instrumentData, "data-focus-group": open?.id, "data-focus-param": focusCfg.param ?? FOCUS_PARAM, "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(PanelPathContext.Provider, { value: `${placeAbove}/${open.id}`, children: /* @__PURE__ */ jsx(PanelBoundary, { panel: open.id, children: /* @__PURE__ */ jsx(DocRefsScope, { children: open.content }) }) }) }) }) }),
3444
3566
  rail: /* @__PURE__ */ jsx("div", { className: "caos-focus-rail-body", children: /* @__PURE__ */ jsx(RailBody, { rail: rail ?? null, railHead: focusPickers, railLabel }) })
3445
3567
  }
3446
3568
  ) }) });
3447
3569
  }
3448
- return /* @__PURE__ */ jsx(ViewMemoryContext.Provider, { value: memory.current, children: /* @__PURE__ */ jsx(WorkbenchStateContext.Provider, { value: { stateKey, caseId, variantId, source }, children: /* @__PURE__ */ jsx(WorkbenchLayout, { rail: rail ?? null, railHead, railLabel, children: /* @__PURE__ */ jsx("div", { className: "caos-cw", ...instrumentData, children: /* @__PURE__ */ jsx(
3570
+ return /* @__PURE__ */ jsx(ViewMemoryContext.Provider, { value: memory, children: /* @__PURE__ */ jsx(WorkbenchStateContext.Provider, { value: { stateKey, caseId, variantId, source }, children: /* @__PURE__ */ jsx(WorkbenchLayout, { rail: rail ?? null, railHead, railLabel, children: /* @__PURE__ */ jsx("div", { className: "caos-cw", ...instrumentData, children: /* @__PURE__ */ jsx(
3449
3571
  Tabs,
3450
3572
  {
3451
3573
  tabs,
3452
3574
  value: openGroup,
3453
3575
  onChange: selectGroup,
3454
- ariaLabel: pick(ariaLabel ?? { en: "Case views", es: "Vistas del caso" }, lang)
3576
+ ariaLabel: pick(ariaLabel ?? { en: "Case views", es: "Vistas del caso" }, lang),
3577
+ trailing: enter || void 0
3455
3578
  }
3456
3579
  ) }) }) }) });
3457
3580
  }
@@ -3655,4 +3778,4 @@ function ViewsRow({ children, shares }) {
3655
3778
  }) });
3656
3779
  }
3657
3780
 
3658
- export { AppShell, ArchitectureModal, BREAKPOINTS, BarChart, Callout, CardDeck, CaseSelector, CaseWorkbench, ChipGroup, CitationsProvider, Cite, DECK_TEXT, DOC_AREAS, DocPage, DocReferences, DocRefsScope, DocSection, EXECUTIVE_SCHEMA_VERSION, Equation, ExecutiveView, FLIP_LIMITS, FLIP_TEXT, FOCUS_PARAM, FOCUS_TEXT, FOUR_CS, Figure, FlipCard, FocusShell, Gauge, Glossary, InlineMath, Knob, Landing, LaneBadge, LanguageToggle, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, PlotCard, Question, Readout, ReferenceList, Refs, Stat, SubTabs, SurfacePage, TabGroups, Tabs, ThemeToggle, VariantBar, Verdict, ViewsRow, WorkbenchLayout, WorkbenchShell, WorkbenchStateContext, caseKind, caseTooltip, casesInSource, createVizLoop, deckPosition, deckWindow, firstAppearance, fitLabel, fontFamily, groupByCategory, isStale, makeStateKey, niceStep, niceTicks, perPageFor, readCaseParam, removeQueryParam, replaceQueryParam, sourcesPresent, textWidth, usePausedViz, useUrlView, useWorkbenchState, validateArchitectureConfig, validateExecutive, viewParamOf, widestLabel, withCaseParam };
3781
+ export { AppShell, ArchitectureModal, BREAKPOINTS, BarChart, Callout, CardDeck, CaseSelector, CaseWorkbench, ChipGroup, CitationsProvider, Cite, DECK_TEXT, DOC_AREAS, DocPage, DocReferences, DocRefsScope, DocSection, EXECUTIVE_SCHEMA_VERSION, Equation, ExecutiveView, FLIP_LIMITS, FLIP_TEXT, FOCUS_PARAM, FOCUS_TEXT, FOUR_CS, Figure, FlipCard, FocusShell, Gauge, Glossary, InlineMath, Knob, Landing, LaneBadge, LanguageToggle, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, PlotCard, Question, Readout, ReferenceList, Refs, Stat, SubTabs, SurfacePage, TabGroups, Tabs, ThemeToggle, VariantBar, Verdict, ViewsRow, WorkbenchLayout, WorkbenchShell, WorkbenchStateContext, caseKind, caseTooltip, casesInSource, createVizLoop, deckPosition, deckWindow, firstAppearance, fitLabel, fontFamily, groupByCategory, isStale, makeStateKey, niceStep, niceTicks, perPageFor, readCaseParam, removeQueryParam, replaceQueryParam, sourcesPresent, textWidth, useFocusMode, usePausedViz, useUrlView, useWorkbenchState, validateArchitectureConfig, validateExecutive, viewParamOf, widestLabel, withCaseParam };
package/gate/inpage.mjs CHANGED
@@ -181,6 +181,67 @@ export function installLib() {
181
181
  };
182
182
  };
183
183
 
184
+ /**
185
+ * Selects whose selected option does not fit the room it leaves, or a case select that shows another name than the one it holds (BL-074, shell
186
+ * 0.17.0; the reviews of 0.17.0). What is measured is what the reader sees:
187
+ * - a select with the shell's display (`.cs-select-wrap`, the case select since the trial of 0.17.0) shows the name whole at any length in the
188
+ * display's real `.cs-select-text` element: the name is cut when that element overflows its box (`scrollHeight` or `scrollWidth` past its
189
+ * `clientHeight` or `clientWidth` by more than 1px, e.g. a product stylesheet that clamps it), measured on the element itself, never on a probe
190
+ * copy, and when the display's own box is smaller than its content (a product stylesheet that fixes its height); the display is stale when the name it shows, whitespace normalized, differs from the text of the option the select holds;
191
+ * - any other select shows its text on one line: the room is its width less its own CSS padding and less the native arrow, which is
192
+ * measured, not assumed (a probe select of the same class holding one character, sized to its content; 0 with `appearance: none`).
193
+ * Plain text is measured with the select's own font on one canvas. Runs in the App and inside Focus (its rail holds a case select too).
194
+ */
195
+ const selectCuts = () => {
196
+ const cuts = [];
197
+ const mctx = document.createElement('canvas').getContext('2d');
198
+ const norm = (t) => (t ?? '').trim().replace(/\s+/g, ' ');
199
+ for (const sel of document.querySelectorAll('select')) {
200
+ if (!mctx || !visible(sel)) continue;
201
+ const ss = getComputedStyle(sel);
202
+ const held = norm(sel.options[sel.selectedIndex]?.textContent);
203
+ const display = sel.closest('.cs-select-wrap')?.querySelector('.cs-select-display');
204
+ if (display) {
205
+ const t = display.querySelector('.cs-select-text') ?? display;
206
+ const shown = norm(t.textContent);
207
+ if (shown !== held) {
208
+ cuts.push(`${describe(sel)} shows "${shown}" while the select holds "${held}" (a stale display)`);
209
+ continue;
210
+ }
211
+ const overV = t.scrollHeight > t.clientHeight + 1;
212
+ const overH = t.scrollWidth > t.clientWidth + 1;
213
+ if (overV || overH) {
214
+ const line = Number.parseFloat(getComputedStyle(t).lineHeight) || 1;
215
+ cuts.push(`${describe(sel)} cuts the case name "${shown}" (the name needs ${Math.round(t.scrollHeight / line)} lines, ${Math.round(t.scrollHeight)}px high and ${Math.round(t.scrollWidth)}px wide, and its box holds ${Math.round(t.clientHeight)}px by ${Math.round(t.clientWidth)}px; held option "${held}")`);
216
+ } else if (display.scrollHeight > display.clientHeight + 1 || display.scrollWidth > display.clientWidth + 1) {
217
+ // the text fits its own box but the display's box is smaller (a product stylesheet that fixes the display's height)
218
+ cuts.push(`${describe(sel)} cuts the case name "${shown}" in its display (the name needs ${Math.round(display.scrollHeight)}px by ${Math.round(display.scrollWidth)}px, the display holds ${Math.round(display.clientHeight)}px by ${Math.round(display.clientWidth)}px; held option "${held}")`);
219
+ }
220
+ continue;
221
+ }
222
+ const text = held;
223
+ if (!text) continue;
224
+ mctx.font = `${ss.fontStyle} ${ss.fontWeight} ${ss.fontSize} ${ss.fontFamily}`;
225
+ const pad = Number.parseFloat(ss.paddingLeft) + Number.parseFloat(ss.paddingRight);
226
+ let arrow = 0;
227
+ const host = sel.parentElement;
228
+ if (host) {
229
+ const probe = sel.cloneNode(false);
230
+ for (const a of [...probe.attributes]) if (a.name !== 'class') probe.removeAttribute(a.name);
231
+ probe.append(new Option('x'));
232
+ probe.setAttribute('style', 'position:absolute;visibility:hidden;width:auto;min-width:0;max-width:none;flex:none;left:0;top:0');
233
+ host.append(probe);
234
+ const pv = getComputedStyle(probe);
235
+ arrow = Math.max(0, probe.clientWidth - Number.parseFloat(pv.paddingLeft) - Number.parseFloat(pv.paddingRight) - mctx.measureText('x').width);
236
+ probe.remove();
237
+ }
238
+ const need = mctx.measureText(text).width;
239
+ const has = sel.clientWidth - pad - arrow;
240
+ if (need > has + 1) cuts.push(`${describe(sel)} cuts "${text}" (${Math.round(need)}px of text in ${Math.round(has)}px of room, the ${Math.round(arrow)}px arrow taken out)`);
241
+ }
242
+ return cuts;
243
+ };
244
+
184
245
  /** The static layout of the current view (G5, G6 inputs, ADR-0071 and ADR-0017 measures). */
185
246
  G.facts = () => {
186
247
  const vw = window.innerWidth;
@@ -272,6 +333,8 @@ export function installLib() {
272
333
  if (!named) truncated.push(describe(el));
273
334
  }
274
335
 
336
+ const cutSelects = selectCuts();
337
+
275
338
  // Containers that cut their content: overflow hidden or clip with more content than box (S1, G5).
276
339
  const clipped = [];
277
340
  const containers = new Set([de, body, document.getElementById('root'), ...document.querySelectorAll('main, .page, .page-body, [data-instrument], [data-rail], .tabpanel, .caos-cw, [data-plot]')]);
@@ -415,6 +478,8 @@ export function installLib() {
415
478
  rail,
416
479
  truncated: truncated.slice(0, 5),
417
480
  truncatedCount: truncated.length,
481
+ cutSelects: cutSelects.slice(0, 5),
482
+ cutSelectCount: cutSelects.length,
418
483
  clipped,
419
484
  instrument,
420
485
  surfaces,
@@ -1029,7 +1094,9 @@ export function installLib() {
1029
1094
  /** The App or Focus mode as the reader meets it: which one, the case and the group, the URL, the page and the rail. */
1030
1095
  G.focusFacts = () => {
1031
1096
  const shell = document.querySelector('[data-focus-shell]');
1032
- const stage = document.querySelector('[data-case-workbench]');
1097
+ // the instrument of the App: a case workbench, or (a surface App, 0.16.0) the surface or its Focus frame
1098
+ // by explicit order, never the first match in document order: a workbench nested in a surface view is read as the workbench
1099
+ const stage = document.querySelector('[data-case-workbench]') ?? document.querySelector('[data-surface-focus]') ?? document.querySelector('[data-surface]');
1033
1100
  const rail = document.querySelector('.caos-focus-rail');
1034
1101
  const lists = [...document.querySelectorAll('[data-focus-shell] [role="tablist"]')].filter(visible);
1035
1102
  const rows = lists.map((t) => new Set([...t.querySelectorAll('[role="tab"]')].map((b) => Math.round(b.getBoundingClientRect().top))).size);
@@ -1061,12 +1128,19 @@ export function installLib() {
1061
1128
  return {
1062
1129
  focus: Boolean(shell),
1063
1130
  covered,
1131
+ cutSelects: (() => { const c = selectCuts(); return { count: c.length, list: c.slice(0, 5) }; })(),
1064
1132
  enterInside: [...document.querySelectorAll('[data-focus-shell] [data-focus-enter]')].filter(visible).length,
1065
- app: Boolean(document.querySelector('[data-workbench]')),
1133
+ app: Boolean(document.querySelector('[data-workbench], [data-surface]')),
1066
1134
  caseId: stage ? stage.getAttribute('data-case') : null,
1067
1135
  group: shell ? (stage ? stage.getAttribute('data-focus-group') : null) : panel ? panel.getAttribute('data-panel') : null,
1068
1136
  search: location.search,
1069
- subview: (() => { const t = document.querySelector('[data-case-workbench] .subtab.active'); return t ? t.getAttribute('data-tab') : null; })(),
1137
+ // the query parameter that holds Focus (`focus`, or `param` of the product's focus configuration), written on the Focus frame
1138
+ focusParam: (() => { const e = document.querySelector('[data-focus-param]'); return e ? e.getAttribute('data-focus-param') : null; })(),
1139
+ // a head with no title, lede or actions (an empty head row) on a surface
1140
+ emptyHead: (() => { const h = document.querySelector('[data-surface] > .caos-surface-head'); return h && !h.querySelector('.caos-surface-title, .caos-surface-lede, .caos-surface-actions') ? Math.round(h.getBoundingClientRect().height) : null; })(),
1141
+ // the query parameter the surface holds its open view in (`deepLinkView`): `view`, or the product's own name
1142
+ viewParam: (() => { const e = document.querySelector('[data-surface-focus]') ?? document.querySelector('[data-surface]'); return e && e.hasAttribute('data-view-param') ? e.getAttribute('data-view-param') : null; })(),
1143
+ subview: (() => { const t = document.querySelector('[data-case-workbench] .subtab.active, [data-surface] .subtab.active, [data-surface-focus] .subtab.active'); return t ? t.getAttribute('data-tab') : null; })(),
1070
1144
  nestedMain: document.querySelectorAll('main').length > 1 || Boolean(document.querySelector('main main')),
1071
1145
  vw: window.innerWidth,
1072
1146
  vh: window.innerHeight,