@fasl-work/caos-app-shell 0.1.1 → 0.1.2

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 CHANGED
@@ -2,6 +2,28 @@ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import * as zustand from 'zustand';
4
4
 
5
+ interface ArchTab {
6
+ id: string;
7
+ /** tab label. */
8
+ en: string;
9
+ es: string;
10
+ /** the explanation body (paragraphs separated by a blank line). */
11
+ body_en: string;
12
+ body_es: string;
13
+ /** an inline '<svg…>…</svg>' string OR a path under the app's public/ (fetched + inlined). */
14
+ svg: string;
15
+ }
16
+ interface ArchitectureConfig {
17
+ /** modal title override (defaults to "Architecture / How it works"). */
18
+ title_en?: string;
19
+ title_es?: string;
20
+ tabs: ArchTab[];
21
+ }
22
+ declare function ArchitectureModal({ config, onClose }: {
23
+ config: ArchitectureConfig;
24
+ onClose: () => void;
25
+ }): react.JSX.Element | null;
26
+
5
27
  interface ShellRoute {
6
28
  path: string;
7
29
  en: string;
@@ -23,6 +45,8 @@ interface ShellConfig {
23
45
  };
24
46
  /** Human version string (X.XX.XXX) shown in the footer. */
25
47
  version: string;
48
+ /** In-app Architecture / "How it works" modal (ADR-0058). When present, an ⓘ button appears in the header. */
49
+ architecture?: ArchitectureConfig;
26
50
  }
27
51
  /** The shared CAOS/Faena app shell: sticky header (brand + nav + icon-links + lang/theme) + footer.
28
52
  * Wrap your <Routes> (or single landing) in it, inside a Router. */
@@ -136,4 +160,4 @@ declare function ReferenceList({ ids, heading }: {
136
160
  heading?: string;
137
161
  }): react.JSX.Element;
138
162
 
139
- export { AppShell, Callout, type Citation, CitationsProvider, Cite, Equation, Figure, InlineMath, type Lang, LanguageToggle, ReferenceList, Refs, type ShellConfig, type ShellRoute, type SubTabDef, SubTabs, type TabDef, Tabs, type Theme, ThemeToggle, applyTheme, readTheme, useLangStore, useShellLang, useThemeStore };
163
+ export { AppShell, type ArchTab, type ArchitectureConfig, ArchitectureModal, Callout, type Citation, CitationsProvider, Cite, Equation, Figure, InlineMath, type Lang, LanguageToggle, ReferenceList, Refs, type ShellConfig, type ShellRoute, type SubTabDef, SubTabs, type TabDef, Tabs, type Theme, ThemeToggle, applyTheme, readTheme, useLangStore, useShellLang, useThemeStore };
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
+ import { createContext, useState, useRef, useEffect, useId, useContext } from 'react';
1
2
  import { NavLink } from 'react-router-dom';
2
- import { Sun, Moon, Languages, Boxes, Github, Globe, Briefcase } from 'lucide-react';
3
+ import { Sun, Moon, Languages, Boxes, Github, Globe, Briefcase, Info } from 'lucide-react';
3
4
  import { create } from 'zustand';
4
5
  import { jsxs, jsx } from 'react/jsx-runtime';
5
- import { createContext, useId, useState, useContext } from 'react';
6
6
  import katex from 'katex';
7
7
 
8
8
  // src/shell/AppShell.tsx
@@ -119,6 +119,131 @@ function LanguageToggle() {
119
119
  /* @__PURE__ */ jsx("span", { className: "lang-code", children: lang })
120
120
  ] });
121
121
  }
122
+ var cache = {};
123
+ function ArchitectureModal({ config, onClose }) {
124
+ const lang = useShellLang();
125
+ const es = lang === "es";
126
+ const tabs = config.tabs;
127
+ const [active, setActive] = useState(tabs[0]?.id ?? "");
128
+ const [svg, setSvg] = useState(null);
129
+ const [err, setErr] = useState(null);
130
+ const closeRef = useRef(null);
131
+ useEffect(() => {
132
+ const onKey = (e) => {
133
+ if (e.key === "Escape") onClose();
134
+ };
135
+ window.addEventListener("keydown", onKey);
136
+ closeRef.current?.focus();
137
+ return () => window.removeEventListener("keydown", onKey);
138
+ }, [onClose]);
139
+ const tab = tabs.find((t) => t.id === active) ?? tabs[0];
140
+ useEffect(() => {
141
+ if (!tab) return;
142
+ setErr(null);
143
+ const raw = tab.svg.trim();
144
+ if (raw.startsWith("<svg")) {
145
+ setSvg(raw);
146
+ return;
147
+ }
148
+ if (cache[raw]) {
149
+ setSvg(cache[raw]);
150
+ return;
151
+ }
152
+ setSvg(null);
153
+ let cancelled = false;
154
+ const baseUrl = import.meta.env?.BASE_URL || "/";
155
+ const url = baseUrl + raw.replace(/^\//, "");
156
+ fetch(url).then(async (r) => {
157
+ if (!r.ok) throw new Error(`${r.status}`);
158
+ return r.text();
159
+ }).then((text) => {
160
+ cache[raw] = text;
161
+ if (!cancelled) setSvg(text);
162
+ }).catch((e) => {
163
+ if (!cancelled) setErr(String(e?.message ?? e));
164
+ });
165
+ return () => {
166
+ cancelled = true;
167
+ };
168
+ }, [tab]);
169
+ if (!tab) return null;
170
+ const title = (es ? config.title_es : config.title_en) ?? (es ? "Arquitectura / C\xF3mo funciona" : "Architecture / How it works");
171
+ const body = (es ? tab.body_es : tab.body_en).split(/\n\s*\n/);
172
+ return /* @__PURE__ */ jsx(
173
+ "div",
174
+ {
175
+ role: "dialog",
176
+ "aria-modal": "true",
177
+ "aria-label": title,
178
+ onClick: onClose,
179
+ style: { position: "fixed", inset: 0, background: "rgba(0,0,0,0.6)", zIndex: 1e3, display: "flex", alignItems: "center", justifyContent: "center", padding: 24 },
180
+ children: /* @__PURE__ */ jsxs(
181
+ "div",
182
+ {
183
+ onClick: (e) => e.stopPropagation(),
184
+ style: { background: "var(--color-surface)", color: "var(--color-fg)", border: "1px solid var(--color-border)", borderRadius: 10, width: "100%", maxWidth: 1e3, maxHeight: "92vh", display: "flex", flexDirection: "column", overflow: "hidden" },
185
+ children: [
186
+ /* @__PURE__ */ jsxs("header", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", padding: "10px 14px", background: "var(--color-surface-2, var(--color-surface))", borderBottom: "1px solid var(--color-border)" }, children: [
187
+ /* @__PURE__ */ jsx("strong", { style: { fontSize: 14 }, children: title }),
188
+ /* @__PURE__ */ jsxs(
189
+ "button",
190
+ {
191
+ ref: closeRef,
192
+ onClick: onClose,
193
+ "aria-label": es ? "cerrar" : "close",
194
+ style: { fontSize: 12, padding: "3px 10px", borderRadius: 6, border: "1px solid var(--color-border)", background: "transparent", color: "var(--color-fg-subtle)", cursor: "pointer" },
195
+ children: [
196
+ es ? "cerrar" : "close",
197
+ " \u2715"
198
+ ]
199
+ }
200
+ )
201
+ ] }),
202
+ /* @__PURE__ */ jsx("div", { role: "tablist", "aria-label": title, style: { display: "flex", flexWrap: "wrap", gap: 4, padding: "8px 14px 0", borderBottom: "1px solid var(--color-border)" }, children: tabs.map((t) => {
203
+ const sel = t.id === active;
204
+ return /* @__PURE__ */ jsx(
205
+ "button",
206
+ {
207
+ role: "tab",
208
+ "aria-selected": sel,
209
+ onClick: () => setActive(t.id),
210
+ style: {
211
+ fontSize: 12.5,
212
+ padding: "5px 10px",
213
+ borderRadius: "6px 6px 0 0",
214
+ border: "1px solid var(--color-border)",
215
+ borderBottom: "none",
216
+ cursor: "pointer",
217
+ background: sel ? "var(--color-accent)" : "transparent",
218
+ color: sel ? "var(--color-accent-fg, #fff)" : "var(--color-fg-subtle)",
219
+ fontWeight: sel ? 600 : 400
220
+ },
221
+ children: es ? t.es : t.en
222
+ },
223
+ t.id
224
+ );
225
+ }) }),
226
+ /* @__PURE__ */ jsxs("div", { role: "tabpanel", style: { overflowY: "auto", padding: "14px 16px 18px" }, children: [
227
+ 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
+ err && /* @__PURE__ */ jsxs("div", { style: { color: "var(--color-warn, #f87171)", fontSize: 12 }, children: [
229
+ "SVG: ",
230
+ err
231
+ ] }),
232
+ !err && svg === null && /* @__PURE__ */ jsx("div", { style: { color: "var(--color-fg-faint)", fontSize: 12 }, children: "\u2026" }),
233
+ svg !== null && /* @__PURE__ */ jsx(
234
+ "div",
235
+ {
236
+ style: { border: "1px solid var(--color-border)", borderRadius: 8, padding: 8, background: "var(--color-bg, var(--color-surface))", overflowX: "auto" },
237
+ dangerouslySetInnerHTML: { __html: svg }
238
+ }
239
+ )
240
+ ] })
241
+ ]
242
+ }
243
+ )
244
+ }
245
+ );
246
+ }
122
247
  var PERSONAL = "https://fsantibanezleal.github.io";
123
248
  var PORTFOLIO = "https://fasl-work.com";
124
249
  function AppShell({ config, children }) {
@@ -127,6 +252,8 @@ function AppShell({ config, children }) {
127
252
  const routes = config.routes ?? [];
128
253
  const personal = config.links.personal ?? PERSONAL;
129
254
  const portfolio = config.links.portfolio ?? PORTFOLIO;
255
+ const [archOpen, setArchOpen] = useState(false);
256
+ const archLabel = lang === "es" ? "Arquitectura / C\xF3mo funciona" : "Architecture / How it works";
130
257
  return /* @__PURE__ */ jsxs("div", { className: "app-shell", children: [
131
258
  /* @__PURE__ */ jsx("header", { className: "site-header", children: /* @__PURE__ */ jsxs("div", { className: "header-inner", children: [
132
259
  /* @__PURE__ */ jsxs(NavLink, { to: "/", className: "brand", "aria-label": config.product.name, children: [
@@ -147,11 +274,13 @@ function AppShell({ config, children }) {
147
274
  /* @__PURE__ */ jsx("a", { className: "icon-btn", href: config.links.github, target: "_blank", rel: "noreferrer noopener", "aria-label": c.github, title: c.github, children: /* @__PURE__ */ jsx(Github, { size: 18, "aria-hidden": "true" }) }),
148
275
  /* @__PURE__ */ jsx("a", { className: "icon-btn", href: personal, target: "_blank", rel: "noreferrer noopener", "aria-label": c.personal, title: c.personal, children: /* @__PURE__ */ jsx(Globe, { size: 18, "aria-hidden": "true" }) }),
149
276
  /* @__PURE__ */ jsx("a", { className: "icon-btn", href: portfolio, target: "_blank", rel: "noreferrer noopener", "aria-label": c.portfolio, title: c.portfolio, children: /* @__PURE__ */ jsx(Briefcase, { size: 18, "aria-hidden": "true" }) }),
277
+ config.architecture && /* @__PURE__ */ jsx("button", { className: "icon-btn", type: "button", onClick: () => setArchOpen(true), "aria-label": archLabel, title: archLabel, children: /* @__PURE__ */ jsx(Info, { size: 18, "aria-hidden": "true" }) }),
150
278
  /* @__PURE__ */ jsx("span", { className: "header-sep", "aria-hidden": "true" }),
151
279
  /* @__PURE__ */ jsx(LanguageToggle, {}),
152
280
  /* @__PURE__ */ jsx(ThemeToggle, {})
153
281
  ] })
154
282
  ] }) }),
283
+ config.architecture && archOpen && /* @__PURE__ */ jsx(ArchitectureModal, { config: config.architecture, onClose: () => setArchOpen(false) }),
155
284
  /* @__PURE__ */ jsx("main", { className: "page", children }),
156
285
  /* @__PURE__ */ jsx("footer", { className: "site-footer", children: /* @__PURE__ */ jsxs("div", { className: "footer-inner", children: [
157
286
  /* @__PURE__ */ jsxs("div", { className: "footer-meta", children: [
@@ -368,4 +497,4 @@ function ReferenceList({ ids, heading }) {
368
497
  ] });
369
498
  }
370
499
 
371
- export { AppShell, Callout, CitationsProvider, Cite, Equation, Figure, InlineMath, LanguageToggle, ReferenceList, Refs, SubTabs, Tabs, ThemeToggle, applyTheme, readTheme, useLangStore, useShellLang, useThemeStore };
500
+ export { AppShell, ArchitectureModal, Callout, CitationsProvider, Cite, Equation, Figure, InlineMath, LanguageToggle, ReferenceList, Refs, SubTabs, Tabs, ThemeToggle, applyTheme, readTheme, useLangStore, useShellLang, useThemeStore };
package/package.json CHANGED
@@ -1,13 +1,20 @@
1
1
  {
2
2
  "name": "@fasl-work/caos-app-shell",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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",
7
- "repository": { "type": "git", "url": "git+https://github.com/fsantibanezleal/CAOS_APP_SHELL.git" },
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/fsantibanezleal/CAOS_APP_SHELL.git"
10
+ },
8
11
  "type": "module",
9
- "publishConfig": { "access": "public" },
10
- "sideEffects": ["*.css"],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "sideEffects": [
16
+ "*.css"
17
+ ],
11
18
  "exports": {
12
19
  ".": {
13
20
  "types": "./dist/index.d.ts",
@@ -15,7 +22,10 @@
15
22
  },
16
23
  "./styles.css": "./styles.css"
17
24
  },
18
- "files": ["dist", "styles.css"],
25
+ "files": [
26
+ "dist",
27
+ "styles.css"
28
+ ],
19
29
  "scripts": {
20
30
  "build": "tsup",
21
31
  "prepublishOnly": "npm run build"