@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 +25 -1
- package/dist/index.js +132 -3
- package/package.json +15 -5
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.
|
|
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": {
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/fsantibanezleal/CAOS_APP_SHELL.git"
|
|
10
|
+
},
|
|
8
11
|
"type": "module",
|
|
9
|
-
"publishConfig": {
|
|
10
|
-
|
|
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": [
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"styles.css"
|
|
28
|
+
],
|
|
19
29
|
"scripts": {
|
|
20
30
|
"build": "tsup",
|
|
21
31
|
"prepublishOnly": "npm run build"
|