@fasl-work/caos-app-shell 0.6.6 → 0.6.8
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 +71 -13
- package/package.json +3 -1
- package/styles.css +8 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, useState, useRef, useEffect, useCallback,
|
|
1
|
+
import { createContext, useState, useRef, useId, useEffect, useCallback, useContext } from 'react';
|
|
2
2
|
import { useLocation, NavLink } from 'react-router';
|
|
3
3
|
import { Sun, Moon, Languages, Boxes, CodeXml, Globe, Briefcase, Info } from 'lucide-react';
|
|
4
4
|
import { create } from 'zustand';
|
|
@@ -127,19 +127,53 @@ function ArchitectureModal({ config, onClose }) {
|
|
|
127
127
|
const [active, setActive] = useState(tabs[0]?.id ?? "");
|
|
128
128
|
const [svg, setSvg] = useState(null);
|
|
129
129
|
const [err, setErr] = useState(null);
|
|
130
|
+
const [fullSize, setFullSize] = useState(false);
|
|
131
|
+
const [nativeWidth, setNativeWidth] = useState(800);
|
|
132
|
+
const modalRef = useRef(null);
|
|
133
|
+
const diagramRef = useRef(null);
|
|
130
134
|
const closeRef = useRef(null);
|
|
135
|
+
const latestClose = useRef(onClose);
|
|
136
|
+
latestClose.current = onClose;
|
|
137
|
+
const id = useId();
|
|
131
138
|
useEffect(() => {
|
|
139
|
+
const opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
140
|
+
const focusable = () => [...modalRef.current?.querySelectorAll(
|
|
141
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
142
|
+
) ?? []].filter((element) => element.tabIndex >= 0 && element.getClientRects().length > 0);
|
|
132
143
|
const onKey = (e) => {
|
|
133
|
-
if (e.key === "Escape")
|
|
144
|
+
if (e.key === "Escape") {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
e.stopPropagation();
|
|
147
|
+
latestClose.current();
|
|
148
|
+
}
|
|
149
|
+
if (e.key !== "Tab") return;
|
|
150
|
+
const elements = focusable(), first = elements[0], last = elements.at(-1);
|
|
151
|
+
if (!first || !last) {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!modalRef.current?.contains(document.activeElement) || (e.shiftKey ? document.activeElement === first : document.activeElement === last)) {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
(e.shiftKey ? last : first).focus();
|
|
158
|
+
}
|
|
134
159
|
};
|
|
135
|
-
|
|
160
|
+
const onFocus = (e) => {
|
|
161
|
+
if (e.target instanceof Node && !modalRef.current?.contains(e.target)) closeRef.current?.focus();
|
|
162
|
+
};
|
|
163
|
+
window.addEventListener("keydown", onKey, true);
|
|
164
|
+
document.addEventListener("focusin", onFocus);
|
|
136
165
|
closeRef.current?.focus();
|
|
137
|
-
return () =>
|
|
138
|
-
|
|
166
|
+
return () => {
|
|
167
|
+
window.removeEventListener("keydown", onKey, true);
|
|
168
|
+
document.removeEventListener("focusin", onFocus);
|
|
169
|
+
if (opener?.isConnected) opener.focus();
|
|
170
|
+
};
|
|
171
|
+
}, []);
|
|
139
172
|
const tab = tabs.find((t) => t.id === active) ?? tabs[0];
|
|
140
173
|
useEffect(() => {
|
|
141
174
|
if (!tab) return;
|
|
142
175
|
setErr(null);
|
|
176
|
+
setFullSize(false);
|
|
143
177
|
const raw = tab.svg.trim();
|
|
144
178
|
if (raw.startsWith("<svg")) {
|
|
145
179
|
setSvg(raw);
|
|
@@ -166,12 +200,19 @@ function ArchitectureModal({ config, onClose }) {
|
|
|
166
200
|
cancelled = true;
|
|
167
201
|
};
|
|
168
202
|
}, [tab]);
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
const element = diagramRef.current?.querySelector("svg");
|
|
205
|
+
if (!element) return;
|
|
206
|
+
const width = element.viewBox.baseVal.width || element.width.baseVal.value;
|
|
207
|
+
setNativeWidth(Number.isFinite(width) && width > 0 ? width : 800);
|
|
208
|
+
}, [svg]);
|
|
169
209
|
if (!tab) return null;
|
|
170
210
|
const title = (es ? config.title_es : config.title_en) ?? (es ? "Arquitectura / C\xF3mo funciona" : "Architecture / How it works");
|
|
171
211
|
const body = (es ? tab.body_es : tab.body_en).split(/\n\s*\n/);
|
|
172
212
|
return /* @__PURE__ */ jsx(
|
|
173
213
|
"div",
|
|
174
214
|
{
|
|
215
|
+
ref: modalRef,
|
|
175
216
|
role: "dialog",
|
|
176
217
|
"aria-modal": "true",
|
|
177
218
|
"aria-label": title,
|
|
@@ -204,9 +245,20 @@ function ArchitectureModal({ config, onClose }) {
|
|
|
204
245
|
return /* @__PURE__ */ jsx(
|
|
205
246
|
"button",
|
|
206
247
|
{
|
|
248
|
+
id: `${id}-${t.id}`,
|
|
207
249
|
role: "tab",
|
|
208
250
|
"aria-selected": sel,
|
|
251
|
+
"aria-controls": `${id}-panel`,
|
|
252
|
+
tabIndex: sel ? 0 : -1,
|
|
209
253
|
onClick: () => setActive(t.id),
|
|
254
|
+
onKeyDown: (event) => {
|
|
255
|
+
const index = tabs.indexOf(t);
|
|
256
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1 : event.key === "ArrowRight" ? (index + 1) % tabs.length : event.key === "ArrowLeft" ? (index - 1 + tabs.length) % tabs.length : -1;
|
|
257
|
+
if (next < 0) return;
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
setActive(tabs[next].id);
|
|
260
|
+
document.getElementById(`${id}-${tabs[next].id}`)?.focus();
|
|
261
|
+
},
|
|
210
262
|
style: {
|
|
211
263
|
fontSize: 12.5,
|
|
212
264
|
padding: "5px 10px",
|
|
@@ -223,20 +275,25 @@ function ArchitectureModal({ config, onClose }) {
|
|
|
223
275
|
t.id
|
|
224
276
|
);
|
|
225
277
|
}) }),
|
|
226
|
-
/* @__PURE__ */ jsxs("div", { role: "tabpanel", "data-arch-lang": es ? "es" : "en", style: { overflowY: "auto", padding: "14px 16px 18px" }, children: [
|
|
278
|
+
/* @__PURE__ */ jsxs("div", { id: `${id}-panel`, role: "tabpanel", "aria-labelledby": `${id}-${tab.id}`, "data-arch-lang": es ? "es" : "en", style: { overflowY: "auto", minHeight: 0, padding: "14px 16px 18px" }, children: [
|
|
227
279
|
body.map((p, i) => /* @__PURE__ */ jsx("p", { style: { color: "var(--color-fg)", fontSize: 13, lineHeight: 1.65, margin: i === 0 ? "0 0 10px" : "10px 0" }, children: p }, i)),
|
|
228
280
|
err && /* @__PURE__ */ jsxs("div", { style: { color: "var(--color-warn, #f87171)", fontSize: 12 }, children: [
|
|
229
281
|
"SVG: ",
|
|
230
282
|
err
|
|
231
283
|
] }),
|
|
232
284
|
!err && svg === null && /* @__PURE__ */ jsx("div", { style: { color: "var(--color-fg-faint)", fontSize: 12 }, children: "\u2026" }),
|
|
233
|
-
svg !== null && /* @__PURE__ */
|
|
234
|
-
"
|
|
235
|
-
{
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
285
|
+
svg !== null && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
286
|
+
/* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": fullSize, "aria-controls": `${id}-diagram`, onClick: () => setFullSize((value) => !value), style: { margin: "0 0 8px", fontSize: 12, padding: "5px 10px" }, children: fullSize ? es ? "Ajustar diagrama" : "Fit diagram" : es ? "Leer a tama\xF1o completo" : "Read at full size" }),
|
|
287
|
+
/* @__PURE__ */ jsx("div", { id: `${id}-diagram`, role: "region", "aria-label": es ? "Diagrama de arquitectura" : "Architecture diagram", tabIndex: 0, style: { border: "1px solid var(--color-border)", borderRadius: 8, padding: 8, background: "var(--color-bg, var(--color-surface))", overflow: "auto", maxWidth: "100%" }, children: /* @__PURE__ */ jsx(
|
|
288
|
+
"div",
|
|
289
|
+
{
|
|
290
|
+
ref: diagramRef,
|
|
291
|
+
className: "caos-architecture-diagram",
|
|
292
|
+
style: { width: fullSize ? nativeWidth : "100%", maxWidth: fullSize ? "none" : "100%" },
|
|
293
|
+
dangerouslySetInnerHTML: { __html: svg }
|
|
294
|
+
}
|
|
295
|
+
) })
|
|
296
|
+
] })
|
|
240
297
|
] })
|
|
241
298
|
]
|
|
242
299
|
}
|
|
@@ -267,6 +324,7 @@ function AppShell({ config, children }) {
|
|
|
267
324
|
{
|
|
268
325
|
to: r.path,
|
|
269
326
|
end: r.path === "/",
|
|
327
|
+
onFocus: (event) => event.currentTarget.scrollIntoView({ block: "nearest", inline: "nearest" }),
|
|
270
328
|
className: ({ isActive }) => isActive ? "nav-link active" : "nav-link",
|
|
271
329
|
children: lang === "es" ? r.es : r.en
|
|
272
330
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fasl-work/caos-app-shell",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
4
4
|
"description": "Shared web-app shell + content primitives + design system for the CAOS / Faena public apps (ADR-0016).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Felipe Santibáñez-Leal",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "tsup",
|
|
31
31
|
"test": "node --import tsx --test \"test/**/*.test.ts\" \"test/**/*.test.tsx\"",
|
|
32
|
+
"test:browser": "node --import tsx --test \"e2e/**/*.test.ts\"",
|
|
32
33
|
"prepublishOnly": "npm run build"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
"zustand": "^4 || ^5"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
44
|
+
"@playwright/test": "1.63.0",
|
|
43
45
|
"@types/katex": "^0.16.7",
|
|
44
46
|
"@types/react": "^19.0.0",
|
|
45
47
|
"@types/react-dom": "^19.0.0",
|
package/styles.css
CHANGED
|
@@ -110,7 +110,13 @@ code, kbd { font-family: var(--font-mono); }
|
|
|
110
110
|
/* ── Misc ── */
|
|
111
111
|
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
|
112
112
|
:where(a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])):focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; border-radius: 4px; }
|
|
113
|
-
@media (max-width: 760px) {
|
|
113
|
+
@media (max-width: 760px) {
|
|
114
|
+
.header-inner { height: auto; min-height: 56px; flex-wrap: wrap; gap: 0 0.5rem; padding: 4px 1rem; }
|
|
115
|
+
.header-actions { gap: 0; }
|
|
116
|
+
.header-actions .icon-btn { padding: 0.35rem; }
|
|
117
|
+
.main-nav { order: 3; flex: 0 0 100%; min-width: 0; overflow-x: auto; white-space: nowrap; padding: 3px 0; scrollbar-width: thin; }
|
|
118
|
+
.main-nav .nav-link { flex: none; padding: 0.3rem 0.6rem; font-size: 0.8rem; }
|
|
119
|
+
}
|
|
114
120
|
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
115
121
|
|
|
116
122
|
/* ── Prose / content pages ── (fill the page-body width with grids/figures/sub-tab rails, not a narrow column) */
|
|
@@ -327,6 +333,7 @@ html, body { overflow-x: hidden; }
|
|
|
327
333
|
[data-arch-lang] .l-es { display: none; }
|
|
328
334
|
[data-arch-lang="es"] .l-en { display: none; }
|
|
329
335
|
[data-arch-lang="es"] .l-es { display: inline; }
|
|
336
|
+
.caos-architecture-diagram > svg { display: block; width: 100%; height: auto; max-width: none; }
|
|
330
337
|
|
|
331
338
|
/* The compact one-of-N case picker (ADR-0071 rule 7). Ten cases in four categories
|
|
332
339
|
cost a 308px chip block, 21% of a 2560x1440 viewport, and one row as a dropdown. */
|