@fasl-work/caos-app-shell 0.3.0 → 0.5.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/README.md +14 -3
- package/dist/index.d.ts +31 -1
- package/dist/index.js +51 -2
- package/package.json +11 -8
- package/styles.css +65 -4
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ fix lands in one place.
|
|
|
13
13
|
```bash
|
|
14
14
|
npm i @fasl-work/caos-app-shell
|
|
15
15
|
# peer deps (the app provides them):
|
|
16
|
-
npm i react react-dom react-router-dom lucide-react katex zustand
|
|
16
|
+
npm i react react-dom react-router-dom lucide-react katex zustand
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## Use
|
|
@@ -60,9 +60,20 @@ const config: ShellConfig = {
|
|
|
60
60
|
|
|
61
61
|
## Exports
|
|
62
62
|
|
|
63
|
-
`AppShell`, `ThemeToggle`, `LanguageToggle`, `useThemeStore`, `applyTheme`, `readTheme`, `useLangStore`,
|
|
63
|
+
`AppShell`, `WorkbenchShell`, `ThemeToggle`, `LanguageToggle`, `useThemeStore`, `applyTheme`, `readTheme`, `useLangStore`,
|
|
64
64
|
`useShellLang`, `usePausedViz`, `createVizLoop`, `CaseSelector` (+ `caseModel` helpers), `Tabs`, `SubTabs`,
|
|
65
65
|
`Callout`, `Equation`, `InlineMath`, `Figure`, `CitationsProvider`, `Cite`, `Refs`, `ReferenceList`, plus
|
|
66
|
-
the `@fasl-work/caos-app-shell/styles.css` design system.
|
|
66
|
+
the `@fasl-work/caos-app-shell/styles.css` design system.
|
|
67
|
+
|
|
68
|
+
### Authenticated workbenches
|
|
69
|
+
|
|
70
|
+
`WorkbenchShell` is the shared frame for products whose authenticated UI needs a desktop sidebar and
|
|
71
|
+
mobile bottom navigation. It renders typed routes and the main landmark while the product supplies its
|
|
72
|
+
own stateful controls through `brand`, `sidebarFooter`, `headerLead`, `headerActions`, and `overlays`.
|
|
73
|
+
Authentication, account policy, preference persistence, chat, and modal content remain product-owned.
|
|
74
|
+
The component uses the stable core `NavLink` contract and supports `react-router` 6, 7, and 8.
|
|
75
|
+
Browser applications on 6/7 install the matching `react-router-dom`, which supplies the core peer;
|
|
76
|
+
Router 8 applications provide `react-router` directly. Do not mix different core and DOM majors in
|
|
77
|
+
one application.
|
|
67
78
|
|
|
68
79
|
MIT · part of the Faena mining-analytics hub.
|
package/dist/index.d.ts
CHANGED
|
@@ -68,6 +68,36 @@ declare function AppShell({ config, children }: {
|
|
|
68
68
|
children: ReactNode;
|
|
69
69
|
}): react.JSX.Element;
|
|
70
70
|
|
|
71
|
+
/** One authenticated workbench destination rendered by the shared responsive navigation frame. */
|
|
72
|
+
interface WorkbenchRoute {
|
|
73
|
+
path: string;
|
|
74
|
+
label: ReactNode;
|
|
75
|
+
icon?: ReactNode;
|
|
76
|
+
end?: boolean;
|
|
77
|
+
ariaLabel?: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Slots owned by a product around the shared workbench layout.
|
|
81
|
+
*
|
|
82
|
+
* The shell deliberately does not own authentication, preferences, account state, chat, or modal
|
|
83
|
+
* content. Products supply those controls as slots while this package owns the frame, navigation
|
|
84
|
+
* semantics, responsive layout classes, and main-content landmark.
|
|
85
|
+
*/
|
|
86
|
+
interface WorkbenchShellProps {
|
|
87
|
+
brand: ReactNode;
|
|
88
|
+
routes: WorkbenchRoute[];
|
|
89
|
+
navigationLabel: string;
|
|
90
|
+
children: ReactNode;
|
|
91
|
+
sidebarFooter?: ReactNode;
|
|
92
|
+
headerLead?: ReactNode;
|
|
93
|
+
headerActions?: ReactNode;
|
|
94
|
+
overlays?: ReactNode;
|
|
95
|
+
mainId?: string;
|
|
96
|
+
className?: string;
|
|
97
|
+
}
|
|
98
|
+
/** Shared CAOS frame for authenticated consoles with desktop sidebar and mobile bottom navigation. */
|
|
99
|
+
declare function WorkbenchShell({ brand, routes, navigationLabel, children, sidebarFooter, headerLead, headerActions, overlays, mainId, className, }: WorkbenchShellProps): react.JSX.Element;
|
|
100
|
+
|
|
71
101
|
/** Light/dark icon toggle. Shows the icon of the theme you would switch TO. */
|
|
72
102
|
declare function ThemeToggle(): react.JSX.Element;
|
|
73
103
|
|
|
@@ -316,4 +346,4 @@ declare function ReferenceList({ ids, heading }: {
|
|
|
316
346
|
heading?: string;
|
|
317
347
|
}): react.JSX.Element;
|
|
318
348
|
|
|
319
|
-
export { AppShell, type ArchTab, type ArchitectureConfig, ArchitectureModal, Callout, type CaseDef, type CaseGroup, type CaseKind, CaseSelector, type CaseSelectorProps, type CaseSelectorText, type Citation, CitationsProvider, Cite, Equation, Figure, InlineMath, type Lang, LanguageToggle, type PausedVizController, ReferenceList, Refs, type ShellConfig, type ShellRoute, type SubTabDef, SubTabs, type TabDef, Tabs, type Theme, ThemeToggle, type UsePausedVizOptions, type VizFrame, type VizLoop, type VizLoopDeps, type VizLoopOptions, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, groupByCategory, readCaseParam, readTheme, sourcesPresent, useLangStore, usePausedViz, useShellLang, useThemeStore, withCaseParam };
|
|
349
|
+
export { AppShell, type ArchTab, type ArchitectureConfig, ArchitectureModal, Callout, type CaseDef, type CaseGroup, type CaseKind, CaseSelector, type CaseSelectorProps, type CaseSelectorText, type Citation, CitationsProvider, Cite, Equation, Figure, InlineMath, type Lang, LanguageToggle, type PausedVizController, ReferenceList, Refs, type ShellConfig, type ShellRoute, type SubTabDef, SubTabs, type TabDef, Tabs, type Theme, ThemeToggle, type UsePausedVizOptions, type VizFrame, type VizLoop, type VizLoopDeps, type VizLoopOptions, type WorkbenchRoute, WorkbenchShell, type WorkbenchShellProps, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, groupByCategory, readCaseParam, readTheme, sourcesPresent, useLangStore, usePausedViz, useShellLang, useThemeStore, withCaseParam };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useState, useRef, useEffect, useCallback, useId, useContext } from 'react';
|
|
2
|
-
import { NavLink } from 'react-router
|
|
2
|
+
import { NavLink } from 'react-router';
|
|
3
3
|
import { Sun, Moon, Languages, Boxes, Github, Globe, Briefcase, Info } from 'lucide-react';
|
|
4
4
|
import { create } from 'zustand';
|
|
5
5
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
@@ -308,6 +308,55 @@ function AppShell({ config, children }) {
|
|
|
308
308
|
] }) }) })
|
|
309
309
|
] });
|
|
310
310
|
}
|
|
311
|
+
function WorkbenchShell({
|
|
312
|
+
brand,
|
|
313
|
+
routes,
|
|
314
|
+
navigationLabel,
|
|
315
|
+
children,
|
|
316
|
+
sidebarFooter,
|
|
317
|
+
headerLead,
|
|
318
|
+
headerActions,
|
|
319
|
+
overlays,
|
|
320
|
+
mainId = "main-content",
|
|
321
|
+
className
|
|
322
|
+
}) {
|
|
323
|
+
const rootClassName = ["caos-workbench-shell", "app-shell", className].filter(Boolean).join(" ");
|
|
324
|
+
return /* @__PURE__ */ jsxs("div", { className: rootClassName, children: [
|
|
325
|
+
/* @__PURE__ */ jsxs(
|
|
326
|
+
"aside",
|
|
327
|
+
{
|
|
328
|
+
className: "caos-workbench-sidebar sidebar",
|
|
329
|
+
style: { "--caos-workbench-route-count": routes.length },
|
|
330
|
+
children: [
|
|
331
|
+
brand,
|
|
332
|
+
/* @__PURE__ */ jsx("nav", { "aria-label": navigationLabel, children: routes.map((route) => /* @__PURE__ */ jsxs(
|
|
333
|
+
NavLink,
|
|
334
|
+
{
|
|
335
|
+
to: route.path,
|
|
336
|
+
end: route.end ?? route.path === "/",
|
|
337
|
+
"aria-label": route.ariaLabel,
|
|
338
|
+
className: ({ isActive }) => isActive ? "active" : void 0,
|
|
339
|
+
children: [
|
|
340
|
+
route.icon,
|
|
341
|
+
/* @__PURE__ */ jsx("span", { children: route.label })
|
|
342
|
+
]
|
|
343
|
+
},
|
|
344
|
+
route.path
|
|
345
|
+
)) }),
|
|
346
|
+
sidebarFooter
|
|
347
|
+
]
|
|
348
|
+
}
|
|
349
|
+
),
|
|
350
|
+
/* @__PURE__ */ jsxs("div", { className: "caos-workbench-main shell-main", children: [
|
|
351
|
+
/* @__PURE__ */ jsxs("header", { className: "caos-workbench-header topbar", children: [
|
|
352
|
+
headerLead,
|
|
353
|
+
headerActions
|
|
354
|
+
] }),
|
|
355
|
+
/* @__PURE__ */ jsx("main", { id: mainId, className: "caos-workbench-content content", children })
|
|
356
|
+
] }),
|
|
357
|
+
overlays
|
|
358
|
+
] });
|
|
359
|
+
}
|
|
311
360
|
|
|
312
361
|
// src/lib/vizLoop.ts
|
|
313
362
|
function createVizLoop(frame, options, deps) {
|
|
@@ -788,4 +837,4 @@ function ReferenceList({ ids, heading }) {
|
|
|
788
837
|
] });
|
|
789
838
|
}
|
|
790
839
|
|
|
791
|
-
export { AppShell, ArchitectureModal, Callout, CaseSelector, CitationsProvider, Cite, Equation, Figure, InlineMath, LanguageToggle, ReferenceList, Refs, SubTabs, Tabs, ThemeToggle, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, groupByCategory, readCaseParam, readTheme, sourcesPresent, useLangStore, usePausedViz, useShellLang, useThemeStore, withCaseParam };
|
|
840
|
+
export { AppShell, ArchitectureModal, Callout, CaseSelector, CitationsProvider, Cite, Equation, Figure, InlineMath, LanguageToggle, ReferenceList, Refs, SubTabs, Tabs, ThemeToggle, WorkbenchShell, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, groupByCategory, readCaseParam, readTheme, sourcesPresent, useLangStore, usePausedViz, useShellLang, useThemeStore, withCaseParam };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fasl-work/caos-app-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "tsup",
|
|
31
|
-
"test": "node --import tsx --test test/vizLoop.test.ts test/caseModel.test.ts",
|
|
31
|
+
"test": "node --import tsx --test test/vizLoop.test.ts test/caseModel.test.ts test/workbenchShell.test.tsx",
|
|
32
32
|
"prepublishOnly": "npm run build"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"lucide-react": ">=0.400.0",
|
|
37
37
|
"react": "^18 || ^19",
|
|
38
38
|
"react-dom": "^18 || ^19",
|
|
39
|
-
"react-router
|
|
39
|
+
"react-router": "^6 || ^7 || ^8",
|
|
40
40
|
"zustand": "^4 || ^5"
|
|
41
41
|
},
|
|
42
|
-
"devDependencies": {
|
|
42
|
+
"devDependencies": {
|
|
43
43
|
"@types/katex": "^0.16.7",
|
|
44
44
|
"@types/react": "^19.0.0",
|
|
45
45
|
"@types/react-dom": "^19.0.0",
|
|
@@ -47,10 +47,13 @@
|
|
|
47
47
|
"lucide-react": "^0.469.0",
|
|
48
48
|
"react": "^19.0.0",
|
|
49
49
|
"react-dom": "^19.0.0",
|
|
50
|
-
"react-router
|
|
50
|
+
"react-router": "^8.3.0",
|
|
51
51
|
"tsup": "^8.3.5",
|
|
52
52
|
"tsx": "^4.23.0",
|
|
53
53
|
"typescript": "^5.7.3",
|
|
54
|
-
"zustand": "^5.0.2"
|
|
55
|
-
}
|
|
56
|
-
|
|
54
|
+
"zustand": "^5.0.2"
|
|
55
|
+
},
|
|
56
|
+
"overrides": {
|
|
57
|
+
"esbuild": "0.28.1"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/styles.css
CHANGED
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
--color-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 6px 16px rgba(0, 0, 0, 0.45);
|
|
26
26
|
--font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
27
27
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
28
|
-
--maxw: 1200px;
|
|
28
|
+
--maxw: 1200px; /* reading width, for PROSE pages */
|
|
29
|
+
--maxw-wide: 100%; /* instrument width, for workbench pages */
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
[data-theme="light"] {
|
|
@@ -69,6 +70,10 @@ code, kbd { font-family: var(--font-mono); }
|
|
|
69
70
|
.container { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: 1.5rem; }
|
|
70
71
|
.page { flex: 1 1 auto; width: 100%; }
|
|
71
72
|
.page-body { width: 100%; max-width: var(--maxw); margin-inline: auto; padding: 2.25rem 1.5rem 3rem; }
|
|
73
|
+
/* A WORKBENCH is not prose. Capping an instrument at a 1200px reading measure wastes every pixel beyond
|
|
74
|
+
it, which is why the App surfaces across the line render their visualization as a thumbnail on a wide
|
|
75
|
+
display. Prose pages keep the reading width; the App route opts into the full viewport. */
|
|
76
|
+
.page-body.wide { max-width: var(--maxw-wide); padding-inline: 1.25rem; }
|
|
72
77
|
.muted { color: var(--color-fg-subtle); }
|
|
73
78
|
.faint { color: var(--color-fg-faint); }
|
|
74
79
|
.mono { font-family: var(--font-mono); font-size: 0.9em; }
|
|
@@ -155,7 +160,18 @@ code, kbd { font-family: var(--font-mono); }
|
|
|
155
160
|
.callout-body p:last-child { margin-bottom: 0; }
|
|
156
161
|
|
|
157
162
|
/* ── Tabs ── */
|
|
158
|
-
.tablist { display: flex; gap: 0.35rem; padding-bottom: 0.6rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--color-border); flex-wrap:
|
|
163
|
+
.tablist { display: flex; gap: 0.35rem; padding-bottom: 0.6rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--color-border); flex-wrap: nowrap; }
|
|
164
|
+
/* SINGLE-LINE TAB BAR.
|
|
165
|
+
`flex-wrap: wrap` lets a 14-to-18 tab bar spill onto a second and third row, and every extra row is
|
|
166
|
+
vertical space taken from the instrument below it, permanently, on every render. A tab bar is
|
|
167
|
+
navigation chrome: it gets ONE row and scrolls horizontally when it does not fit.
|
|
168
|
+
Scrollbar is hidden but the row stays scrollable by wheel, trackpad, drag and keyboard. */
|
|
169
|
+
.tablist { flex-wrap: nowrap !important; overflow-x: auto; overflow-y: hidden;
|
|
170
|
+
scrollbar-width: none; -ms-overflow-style: none; padding-bottom: 0.4rem; margin-bottom: 0.9rem;
|
|
171
|
+
scroll-behavior: smooth; }
|
|
172
|
+
.tablist::-webkit-scrollbar { display: none; }
|
|
173
|
+
.tablist .tab, .tablist button { flex: 0 0 auto; white-space: nowrap; }
|
|
174
|
+
|
|
159
175
|
.tab { padding: 0.5rem 1rem; border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--color-fg-subtle); font-size: 0.92rem; font-weight: 550; transition: color 0.15s, background-color 0.15s, border-color 0.15s; }
|
|
160
176
|
.tab:hover { color: var(--color-fg); background: var(--color-surface-2); }
|
|
161
177
|
.tab.active { color: var(--color-accent); background: var(--color-accent-soft); border-color: color-mix(in oklab, var(--color-accent) 35%, transparent); font-weight: 650; }
|
|
@@ -238,5 +254,50 @@ code, kbd { font-family: var(--font-mono); }
|
|
|
238
254
|
.cs-kind { font-size: 0.62rem; font-weight: 700; line-height: 1; padding: 0.12rem 0.28rem; border-radius: 4px; border: 1px solid var(--color-border); color: var(--color-fg-faint); background: var(--color-surface-2); align-self: center; }
|
|
239
255
|
.cs-kind-real { color: var(--color-accent-2); border-color: color-mix(in oklab, var(--color-accent-2) 40%, transparent); }
|
|
240
256
|
.cs-kind-uploaded { color: var(--color-warn); border-color: color-mix(in oklab, var(--color-warn) 40%, transparent); }
|
|
241
|
-
.cs-modified { display: flex; align-items: center; gap: 0.5rem; }
|
|
242
|
-
.cs-reset { border: none; background: transparent; color: var(--color-accent); font-size: 0.8rem; font-weight: 600; padding: 0.1rem 0.2rem; text-decoration: underline; text-underline-offset: 2px; }
|
|
257
|
+
.cs-modified { display: flex; align-items: center; gap: 0.5rem; }
|
|
258
|
+
.cs-reset { border: none; background: transparent; color: var(--color-accent); font-size: 0.8rem; font-weight: 600; padding: 0.1rem 0.2rem; text-decoration: underline; text-underline-offset: 2px; }
|
|
259
|
+
|
|
260
|
+
/* Authenticated workbench frame. Product styles may refine tokens and controls, but the package owns
|
|
261
|
+
the responsive navigation/layout contract and keeps route/content landmarks consistent. */
|
|
262
|
+
.caos-workbench-shell { min-height: 100vh; display: grid; grid-template-columns: 238px minmax(0, 1fr); }
|
|
263
|
+
.caos-workbench-sidebar { position: fixed; inset: 0 auto 0 0; z-index: 20; width: 238px; display: flex; flex-direction: column; padding: 1.25rem 0.875rem; border-right: 1px solid var(--color-border); background: var(--color-surface); }
|
|
264
|
+
.caos-workbench-sidebar nav { display: grid; gap: 0.25rem; }
|
|
265
|
+
.caos-workbench-sidebar nav a { display: flex; align-items: center; gap: 0.75rem; padding: 0.625rem 0.7rem; border: 1px solid transparent; border-radius: 0.7rem; color: var(--color-fg-subtle); font-size: 0.9rem; font-weight: 650; }
|
|
266
|
+
.caos-workbench-sidebar nav a:hover { color: var(--color-fg); background: var(--color-surface-2); text-decoration: none; }
|
|
267
|
+
.caos-workbench-sidebar nav a.active { color: var(--color-accent); border-color: color-mix(in oklab, var(--color-accent) 24%, transparent); background: var(--color-accent-soft); }
|
|
268
|
+
.caos-workbench-main { grid-column: 2; min-width: 0; }
|
|
269
|
+
.caos-workbench-header { position: sticky; top: 0; z-index: 15; min-height: 64px; display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0 2rem; border-bottom: 1px solid var(--color-border); background: color-mix(in oklab, var(--color-bg) 88%, transparent); backdrop-filter: blur(12px); }
|
|
270
|
+
.caos-workbench-content { width: min(1540px, 100%); margin: 0 auto; padding: 2.25rem 2rem 4rem; }
|
|
271
|
+
|
|
272
|
+
@media (max-width: 760px) {
|
|
273
|
+
.caos-workbench-shell { display: block; padding-bottom: 66px; }
|
|
274
|
+
.caos-workbench-sidebar { inset: auto 0 0; width: auto; height: 62px; display: block; border: 0; border-top: 1px solid var(--color-border); padding: 5px 8px; }
|
|
275
|
+
.caos-workbench-sidebar > :not(nav) { display: none; }
|
|
276
|
+
.caos-workbench-sidebar nav { grid-template-columns: repeat(var(--caos-workbench-route-count, 5), minmax(0, 1fr)); }
|
|
277
|
+
.caos-workbench-sidebar nav a { display: grid; justify-items: center; gap: 2px; overflow: hidden; padding: 5px; font-size: 0.7rem; text-overflow: ellipsis; white-space: nowrap; }
|
|
278
|
+
.caos-workbench-main { min-width: 0; }
|
|
279
|
+
.caos-workbench-header { min-height: 58px; padding: 0 1rem; }
|
|
280
|
+
.caos-workbench-content { padding: 1.5rem 1rem 2.5rem; }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* CONTAINED LAYOUT. A `nowrap` tab bar sizes its container to its intrinsic width unless every
|
|
284
|
+
ancestor is allowed to shrink: a flex/grid item defaults to `min-width: auto`. Without this, making
|
|
285
|
+
the tab bar a single scrollable row pushes the whole page wider than the viewport and the user has to
|
|
286
|
+
drag sideways to reach the right edge. Measured on ChargeCascade: scrollWidth 1817 on a 1600 viewport. */
|
|
287
|
+
html, body { overflow-x: hidden; }
|
|
288
|
+
.page-body { max-width: 100%; }
|
|
289
|
+
.tabs, .tabpanel { min-width: 0; max-width: 100%; }
|
|
290
|
+
.tablist { min-width: 0; max-width: 100%; width: 100%; }
|
|
291
|
+
|
|
292
|
+
/* NO MAGIC CHROME NUMBER. An app surface must not compute its height from a hardcoded guess at the
|
|
293
|
+
header and footer: the guess drifts the moment either changes, leaving either a dead gap above the
|
|
294
|
+
footer or a few pixels of scroll. Measured on ChargeCascade: guess 150px, real 175px (header 57 +
|
|
295
|
+
footer 70 + 48px body padding), so 25px of scroll and a 48px gap. Apps that fill the viewport add
|
|
296
|
+
`.app-shell.fixed` and let flex distribute the height. */
|
|
297
|
+
.app-shell.fixed { height: 100dvh; min-height: 100dvh; overflow: hidden; }
|
|
298
|
+
.app-shell.fixed > .page { flex: 1 1 auto; min-height: 0; display: flex; }
|
|
299
|
+
.app-shell.fixed .page-body.wide { flex: 1 1 auto; min-height: 0; padding-top: 1rem; padding-bottom: 1rem; }
|
|
300
|
+
|
|
301
|
+
/* A viewport-filling app must not carry the prose footer margin: it becomes dead space between the
|
|
302
|
+
content and the chrome. Measured on ChargeCascade as exactly 48px (3rem). */
|
|
303
|
+
.app-shell.fixed .site-footer { margin-top: 0; }
|