explico 0.11.1 → 0.13.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/CHANGELOG.md +1292 -1
- package/README.md +91 -13
- package/dist/compiler/astro.d.ts +11 -4
- package/dist/compiler/astro.js +48 -15
- package/dist/compiler/cli.d.ts +27 -12
- package/dist/compiler/cli.js +96 -25
- package/dist/compiler/collections.d.ts +33 -2
- package/dist/compiler/collections.js +91 -19
- package/dist/compiler/content-schema.d.ts +31 -0
- package/dist/compiler/content-schema.js +82 -0
- package/dist/compiler/content-schemas.d.ts +422 -0
- package/dist/compiler/content-schemas.js +229 -0
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +30 -2
- package/dist/compiler/exports/combined-html.d.ts +97 -0
- package/dist/compiler/exports/combined-html.js +492 -0
- package/dist/compiler/exports/download-index.d.ts +9 -0
- package/dist/compiler/exports/download-index.js +33 -0
- package/dist/compiler/exports/export-chapters.d.ts +98 -0
- package/dist/compiler/exports/export-chapters.js +164 -0
- package/dist/compiler/exports/export-pages.d.ts +48 -0
- package/dist/compiler/exports/export-pages.js +22 -0
- package/dist/compiler/exports/html-tree.d.ts +22 -0
- package/dist/compiler/exports/html-tree.js +47 -0
- package/dist/compiler/exports/latex/capture.d.ts +106 -0
- package/dist/compiler/exports/latex/capture.js +435 -0
- package/dist/compiler/exports/latex/compile.d.ts +73 -0
- package/dist/compiler/exports/latex/compile.js +115 -0
- package/dist/compiler/exports/latex/document.d.ts +102 -0
- package/dist/compiler/exports/latex/document.js +349 -0
- package/dist/compiler/exports/latex/explico.cls +415 -0
- package/dist/compiler/exports/latex/numbering.d.ts +18 -0
- package/dist/compiler/exports/latex/numbering.js +57 -0
- package/dist/compiler/exports/latex/serialize.d.ts +112 -0
- package/dist/compiler/exports/latex/serialize.js +606 -0
- package/dist/compiler/exports/latex-export.d.ts +44 -0
- package/dist/compiler/exports/latex-export.js +169 -0
- package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
- package/dist/compiler/exports/markdown/serialize.js +527 -0
- package/dist/compiler/exports/markdown-export.d.ts +39 -0
- package/dist/compiler/exports/markdown-export.js +240 -0
- package/dist/compiler/exports/standalone-html.d.ts +15 -5
- package/dist/compiler/exports/standalone-html.js +23 -51
- package/dist/compiler/exports/static-tree.d.ts +253 -0
- package/dist/compiler/exports/static-tree.js +625 -0
- package/dist/compiler/exports/zip.d.ts +16 -0
- package/dist/compiler/exports/zip.js +72 -0
- package/dist/compiler/lesson-derivation.d.ts +8 -2
- package/dist/compiler/lesson-derivation.js +6 -7
- package/dist/compiler/manifest.d.ts +115 -8
- package/dist/compiler/manifest.js +221 -39
- package/dist/compiler/render-diagnostics.d.ts +65 -0
- package/dist/compiler/render-diagnostics.js +56 -0
- package/dist/compiler/resolution-report.d.ts +3 -1
- package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
- package/dist/curriculum/validation.d.ts +9 -3
- package/dist/curriculum/validation.js +35 -17
- package/dist/reference/base-abbreviations.d.mts +2 -2
- package/dist/reference/base-abbreviations.mjs +2 -2
- package/dist/reference/base-operators.d.mts +8 -2
- package/dist/reference/base-operators.mjs +8 -10
- package/dist/reference/base-roman-labels.d.mts +4 -3
- package/dist/reference/base-roman-labels.mjs +4 -3
- package/dist/reference/citation-format.d.mts +35 -0
- package/dist/reference/citation-format.mjs +74 -0
- package/dist/reference/consistency.d.ts +5 -9
- package/dist/reference/consistency.js +34 -38
- package/dist/reference/curriculum-alignment.js +3 -24
- package/dist/reference/derived-entries.d.ts +45 -0
- package/dist/reference/derived-entries.js +50 -0
- package/dist/reference/derived-notation.d.mts +125 -0
- package/dist/reference/derived-notation.mjs +121 -0
- package/dist/reference/diagram-fit.d.ts +45 -0
- package/dist/reference/diagram-fit.js +54 -0
- package/dist/reference/diagram-layout.d.ts +59 -10
- package/dist/reference/diagram-layout.js +85 -24
- package/dist/reference/diagrams-validate.d.ts +1 -1
- package/dist/reference/equations-validate.d.ts +1 -1
- package/dist/reference/equations-validate.js +26 -1
- package/dist/reference/equations.d.mts +27 -0
- package/dist/reference/equations.mjs +120 -22
- package/dist/reference/examples-validate.d.ts +1 -1
- package/dist/reference/figure-fit.d.ts +70 -0
- package/dist/reference/figure-fit.js +60 -0
- package/dist/reference/figure-pictures.d.ts +27 -0
- package/dist/reference/figure-pictures.js +49 -0
- package/dist/reference/figure-symbols.d.ts +32 -0
- package/dist/reference/figure-symbols.js +55 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-abbreviations.js +1 -1
- package/dist/reference/gate-course-rules.d.ts +4 -3
- package/dist/reference/gate-course-rules.js +9 -4
- package/dist/reference/gate-heading-length.d.ts +2 -0
- package/dist/reference/gate-heading-length.js +7 -5
- package/dist/reference/gate-heading-structure.js +26 -0
- package/dist/reference/gate-indices.d.ts +35 -0
- package/dist/reference/gate-indices.js +299 -0
- package/dist/reference/gate-math.d.ts +32 -2
- package/dist/reference/gate-math.js +68 -12
- package/dist/reference/gate-mdx-expression.d.ts +17 -0
- package/dist/reference/gate-mdx-expression.js +29 -0
- package/dist/reference/gate-non-chapter-pages.d.ts +39 -0
- package/dist/reference/gate-non-chapter-pages.js +156 -0
- package/dist/reference/gate-part-pages.d.ts +4 -2
- package/dist/reference/gate-part-pages.js +2 -1
- package/dist/reference/gate-prop-text.d.ts +28 -0
- package/dist/reference/gate-prop-text.js +106 -0
- package/dist/reference/gate-prose-notation.d.ts +3 -1
- package/dist/reference/gate-prose-notation.js +4 -2
- package/dist/reference/gate-retired-keys.d.ts +35 -0
- package/dist/reference/gate-retired-keys.js +58 -0
- package/dist/reference/gloss.d.ts +2 -0
- package/dist/reference/katex-measure.d.ts +17 -0
- package/dist/reference/katex-measure.js +223 -0
- package/dist/reference/keyed-float-validate.js +12 -0
- package/dist/reference/keyed-jsx-floats.d.mts +2 -2
- package/dist/reference/keyed-jsx-floats.mjs +11 -14
- package/dist/reference/lab-katex-validate.d.ts +43 -0
- package/dist/reference/lab-katex-validate.js +96 -0
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-scope.js +13 -2
- package/dist/reference/lint-ignore.js +1 -1
- package/dist/reference/math-glyphs.d.mts +94 -2
- package/dist/reference/math-glyphs.mjs +851 -46
- package/dist/reference/mixed-content.d.ts +8 -3
- package/dist/reference/mixed-content.js +4 -2
- package/dist/reference/page-notation.d.ts +39 -7
- package/dist/reference/page-notation.js +62 -10
- package/dist/reference/raw-floats.mjs +37 -13
- package/dist/reference/references.d.ts +11 -1
- package/dist/reference/references.js +6 -0
- package/dist/reference/registry.js +115 -11
- package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
- package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
- package/dist/reference/rehype-heading-anchors.mjs +11 -1
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/remark-notation.mjs +275 -23
- package/dist/reference/render-lab-math.d.ts +13 -2
- package/dist/reference/render-lab-math.js +18 -6
- package/dist/reference/sections-validate.d.ts +28 -3
- package/dist/reference/sections-validate.js +31 -4
- package/dist/reference/sections.d.mts +24 -6
- package/dist/reference/sections.mjs +92 -17
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/tables-validate.js +24 -1
- package/dist/reference/text-measure.d.ts +10 -0
- package/dist/reference/text-measure.js +41 -0
- package/dist/reference/text-widths.d.ts +10 -0
- package/dist/reference/text-widths.js +1238 -0
- package/dist/reference/types.d.ts +64 -8
- package/dist/rules/catalog.js +223 -32
- package/dist/rules/in-effect.d.ts +1 -1
- package/dist/rules/rules.schema.json +100 -27
- package/dist/rules/settings.d.ts +1 -1
- package/dist/rules/settings.js +7 -0
- package/dist/session/PreferenceStore.d.ts +18 -1
- package/dist/session/PreferenceStore.js +29 -1
- package/docs/authoring/README.md +33 -21
- package/docs/authoring/agents.md +38 -7
- package/docs/authoring/components.md +115 -10
- package/docs/authoring/migrating.md +878 -0
- package/docs/authoring/notation.md +285 -30
- package/docs/authoring/retired.md +1 -0
- package/docs/authoring/structure-rules.md +84 -45
- package/docs/authoring/structure.md +67 -2
- package/docs/authoring/visualizations.md +167 -25
- package/package.json +12 -7
- package/src/compiler/astro.ts +72 -21
- package/src/compiler/cli.ts +143 -35
- package/src/compiler/collections.ts +158 -27
- package/src/compiler/content-schema.ts +108 -0
- package/src/compiler/content-schemas.ts +391 -0
- package/src/compiler/course-config.ts +86 -15
- package/src/compiler/exports/combined-html.ts +669 -0
- package/src/compiler/exports/download-index.ts +36 -0
- package/src/compiler/exports/export-chapters.ts +233 -0
- package/src/compiler/exports/export-pages.ts +44 -0
- package/src/compiler/exports/html-tree.ts +62 -0
- package/src/compiler/exports/latex/capture.ts +651 -0
- package/src/compiler/exports/latex/compile.ts +168 -0
- package/src/compiler/exports/latex/document.ts +479 -0
- package/src/compiler/exports/latex/explico.cls +415 -0
- package/src/compiler/exports/latex/numbering.ts +74 -0
- package/src/compiler/exports/latex/serialize.ts +787 -0
- package/src/compiler/exports/latex/texlive-packages.txt +76 -0
- package/src/compiler/exports/latex-export.ts +255 -0
- package/src/compiler/exports/markdown/serialize.ts +632 -0
- package/src/compiler/exports/markdown-export.ts +329 -0
- package/src/compiler/exports/standalone-html.ts +39 -70
- package/src/compiler/exports/static-tree.ts +944 -0
- package/src/compiler/exports/zip.ts +88 -0
- package/src/compiler/lesson-derivation.ts +13 -8
- package/src/compiler/manifest.ts +387 -40
- package/src/compiler/render-diagnostics.ts +111 -0
- package/src/components/CourseContents.astro +6 -0
- package/src/components/HoverPanel.astro +204 -6
- package/src/components/PartOverview.astro +18 -9
- package/src/components/abbreviation/AbbreviationGlossary.astro +6 -2
- package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
- package/src/components/assessments/AssessmentRunner.tsx +7 -1
- package/src/components/assessments/AssessmentSet.astro +14 -1
- package/src/components/block-heading.ts +38 -0
- package/src/components/card-arrow.ts +18 -4
- package/src/components/card-carry.ts +406 -0
- package/src/components/card-drag.ts +1 -30
- package/src/components/card-fetch.ts +87 -0
- package/src/components/card-manager.ts +1222 -38
- package/src/components/card-memory.ts +5 -1
- package/src/components/card-placement.ts +211 -13
- package/src/components/card-side.ts +281 -31
- package/src/components/citation/CitationLayer.astro +45 -40
- package/src/components/diagram/Diagram.astro +123 -44
- package/src/components/disclosure/print-as-shown.ts +42 -6
- package/src/components/figure/Figure.astro +17 -1
- package/src/components/figure/FloatCaption.astro +4 -17
- package/src/components/figure/figure-state.ts +26 -17
- package/src/components/float-toolbar.ts +274 -0
- package/src/components/memo/Memo.astro +21 -10
- package/src/components/memo/ShortNote.astro +4 -0
- package/src/components/notation/NotationGlossary.astro +22 -17
- package/src/components/notation/NotationLayer.astro +300 -79
- package/src/components/notation/NotationMath.astro +8 -1
- package/src/components/notation/render-notation-math.ts +51 -5
- package/src/components/readable-text.ts +25 -0
- package/src/components/reference/FloatRefLayer.astro +162 -89
- package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
- package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
- package/src/components/starlight/LayoutPageSidebar.astro +17 -3
- package/src/components/starlight/LayoutSocialIcons.astro +79 -29
- package/src/components/starlight/LessonExportActions.astro +384 -310
- package/src/components/starlight/LessonFooter.astro +66 -8
- package/src/components/starlight/PrintController.astro +28 -0
- package/src/components/starlight/TextSizeControl.astro +137 -0
- package/src/components/starlight/print-controller.ts +380 -0
- package/src/components/starlight/section-summary.ts +13 -5
- package/src/components/table/Table.astro +14 -22
- package/src/components/table/table-truncation.ts +15 -0
- package/src/content-config.ts +20 -312
- package/src/curriculum/validation.ts +72 -24
- package/src/reference/base-abbreviations.mjs +2 -2
- package/src/reference/base-operators.mjs +8 -10
- package/src/reference/base-roman-labels.mjs +4 -3
- package/src/reference/citation-format.mjs +74 -0
- package/src/reference/consistency.ts +47 -61
- package/src/reference/curriculum-alignment.ts +4 -41
- package/src/reference/derived-entries.ts +68 -0
- package/src/reference/derived-notation.mjs +121 -0
- package/src/reference/diagram-fit.ts +104 -0
- package/src/reference/diagram-layout.ts +182 -51
- package/src/reference/diagrams-validate.ts +2 -1
- package/src/reference/equations-validate.ts +41 -2
- package/src/reference/equations.mjs +120 -22
- package/src/reference/examples-validate.ts +1 -0
- package/src/reference/figure-fit.ts +139 -0
- package/src/reference/figure-pictures.ts +96 -0
- package/src/reference/figure-symbols.ts +117 -0
- package/src/reference/figures-validate.ts +2 -1
- package/src/reference/gate-abbreviations.ts +1 -1
- package/src/reference/gate-course-rules.ts +22 -7
- package/src/reference/gate-heading-length.ts +25 -7
- package/src/reference/gate-heading-structure.ts +40 -1
- package/src/reference/gate-indices.ts +470 -0
- package/src/reference/gate-math.ts +102 -13
- package/src/reference/gate-mdx-expression.ts +45 -0
- package/src/reference/gate-non-chapter-pages.ts +241 -0
- package/src/reference/gate-part-pages.ts +1 -1
- package/src/reference/gate-prop-text.ts +145 -0
- package/src/reference/gate-prose-notation.ts +19 -5
- package/src/reference/gate-retired-keys.ts +101 -0
- package/src/reference/gloss.ts +2 -0
- package/src/reference/katex-measure.ts +384 -0
- package/src/reference/keyed-float-validate.ts +22 -0
- package/src/reference/keyed-jsx-floats.mjs +11 -14
- package/src/reference/lab-katex-validate.ts +156 -0
- package/src/reference/lab-math-scope.ts +15 -1
- package/src/reference/lint-ignore.ts +4 -1
- package/src/reference/math-glyphs.mjs +851 -46
- package/src/reference/mixed-content.ts +11 -2
- package/src/reference/page-notation.ts +114 -14
- package/src/reference/raw-floats.mjs +37 -13
- package/src/reference/references.ts +14 -0
- package/src/reference/registry.ts +170 -9
- package/src/reference/rehype-fail-katex-errors.mjs +19 -0
- package/src/reference/rehype-heading-anchors.mjs +11 -1
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/remark-notation.mjs +275 -23
- package/src/reference/render-lab-math.ts +40 -5
- package/src/reference/sections-validate.ts +67 -3
- package/src/reference/sections.mjs +92 -17
- package/src/reference/tables-validate.ts +40 -2
- package/src/reference/text-measure.ts +63 -0
- package/src/reference/text-widths.ts +133 -0
- package/src/reference/types.ts +75 -8
- package/src/rules/catalog.ts +241 -31
- package/src/rules/settings.ts +10 -0
- package/src/session/PreferenceStore.ts +53 -0
- package/src/styles/global.css +322 -83
|
@@ -39,6 +39,8 @@ export interface RememberedCard {
|
|
|
39
39
|
readonly moved: MovedSpot | null;
|
|
40
40
|
/** S2, D3: a docked card's top against its trigger's line. */
|
|
41
41
|
readonly sideDy: number | null;
|
|
42
|
+
/** S11: a card's own width, from its edge; absent or `null` takes the column's. */
|
|
43
|
+
readonly width?: number | null;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export interface RememberedPage {
|
|
@@ -80,7 +82,8 @@ function decodeMoved(value: unknown): MovedSpot | null {
|
|
|
80
82
|
return { x: value.x, y: value.y };
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
/** One remembered card, checked field by field; `null` for anything malformed. */
|
|
86
|
+
export function decodeCard(value: unknown): RememberedCard | null {
|
|
84
87
|
if (!isRecord(value)) return null;
|
|
85
88
|
const { layer, item, occurrence, parentKey, mode } = value;
|
|
86
89
|
if (typeof layer !== 'string' || layer === '' || typeof item !== 'string' || item === '') {
|
|
@@ -101,6 +104,7 @@ function decodeCard(value: unknown): RememberedCard | null {
|
|
|
101
104
|
anchor: decodeAnchor(value.anchor),
|
|
102
105
|
moved: mode === 'moved' ? moved : null,
|
|
103
106
|
sideDy: mode === 'side' && isNumber(value.sideDy) ? value.sideDy : null,
|
|
107
|
+
...(isNumber(value.width) && value.width > 0 ? { width: value.width } : {}),
|
|
104
108
|
};
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
* trigger scrolls (`scrollContainersOf`).
|
|
11
11
|
*
|
|
12
12
|
* P2. A card takes the first free spot in a fixed order, judged against the
|
|
13
|
-
* window below the sticky header
|
|
14
|
-
* card (P6). `placeCard` is that decision as a
|
|
15
|
-
* boxes, so `tests/unit/card-placement.test.ts` runs
|
|
16
|
-
* this module measures the page and applies the
|
|
13
|
+
* window below the sticky header and between the side panels at open time,
|
|
14
|
+
* and against every other open card (P6). `placeCard` is that decision as a
|
|
15
|
+
* pure function over plain boxes, so `tests/unit/card-placement.test.ts` runs
|
|
16
|
+
* it in Node; the rest of this module measures the page and applies the
|
|
17
|
+
* answer.
|
|
17
18
|
*/
|
|
19
|
+
import { sidebarsBetween } from './card-side';
|
|
18
20
|
import type { Box } from './card-travel';
|
|
19
21
|
|
|
20
22
|
export type CardSide = 'below' | 'above';
|
|
@@ -27,7 +29,10 @@ export interface SideSpot {
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface PlacementInput {
|
|
30
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* The part of the window a card must fit: below the sticky header and
|
|
34
|
+
* between the side panels, inset by the window edge.
|
|
35
|
+
*/
|
|
31
36
|
readonly band: Box;
|
|
32
37
|
/** The article column's horizontal extent, or `null` when the page has none. */
|
|
33
38
|
readonly column: { readonly left: number; readonly right: number } | null;
|
|
@@ -233,6 +238,175 @@ export function placeCard(input: PlacementInput): Placement {
|
|
|
233
238
|
return result(cover(first), preferred, 6);
|
|
234
239
|
}
|
|
235
240
|
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// K2–K4: kept cards held at the window's edges while the reader carries them.
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
/** K2: the window edge a carried card is held at. */
|
|
246
|
+
export type HoldEdge = 'top' | 'bottom';
|
|
247
|
+
|
|
248
|
+
export interface HoldCard {
|
|
249
|
+
readonly key: string;
|
|
250
|
+
/**
|
|
251
|
+
* Where the card is with the page (P1), in the viewport, at its full
|
|
252
|
+
* height: next to its trigger, in the side column, or where it was moved.
|
|
253
|
+
*/
|
|
254
|
+
readonly box: Box;
|
|
255
|
+
/** K3: all a collapsed card keeps, its header row. */
|
|
256
|
+
readonly headerHeight: number;
|
|
257
|
+
/** K3: when it opened; the older card sits nearer the edge. */
|
|
258
|
+
readonly order: number;
|
|
259
|
+
/** K3: the lowest collapses first: the card's order, or newer once the reader expanded it. */
|
|
260
|
+
readonly rank: number;
|
|
261
|
+
/** K5: a card from another page has no place on this one; it is held at this edge whatever its box. */
|
|
262
|
+
readonly edge?: HoldEdge | null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface HoldBand {
|
|
266
|
+
/** The window under the header, inset by the window edge. */
|
|
267
|
+
readonly top: number;
|
|
268
|
+
readonly bottom: number;
|
|
269
|
+
/** Things fixed at the top edge, which a held card over them stacks under: the side controls row. */
|
|
270
|
+
readonly blocks?: readonly Box[];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface Held {
|
|
274
|
+
readonly edge: HoldEdge;
|
|
275
|
+
/** The card's top, in the viewport. */
|
|
276
|
+
readonly top: number;
|
|
277
|
+
/** K3: shown as its header row only. */
|
|
278
|
+
readonly collapsed: boolean;
|
|
279
|
+
/** K3: the height a card is cut to when its stack has nothing left to collapse; `null` keeps its own. */
|
|
280
|
+
readonly maxHeight: number | null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Whether two boxes are over each other: they share some width, wherever they are vertically. */
|
|
284
|
+
const overX = (a: Box, b: Box): boolean => a.left < b.right - EPSILON && a.right > b.left + EPSILON;
|
|
285
|
+
|
|
286
|
+
const ascending = (a: number, b: number): number => (a < b ? -1 : a > b ? 1 : 0);
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* K2–K4: which kept cards are held at an edge of the window, and where.
|
|
290
|
+
*
|
|
291
|
+
* K2. A card scrolls with the page until its top passes the top edge (under
|
|
292
|
+
* the header, or under a block fixed there, `band.blocks`), or its bottom
|
|
293
|
+
* the bottom edge; then it stays at that edge, until its place with the page
|
|
294
|
+
* comes back inside the window and it follows the page again. A card from
|
|
295
|
+
* another page (`edge`) is always held.
|
|
296
|
+
*
|
|
297
|
+
* K3. Cards at one edge stack outward from it in the order they were opened,
|
|
298
|
+
* only cards that are over each other: a docked card and one in the article
|
|
299
|
+
* each keep their own column. A card reaches the edge once its place passes
|
|
300
|
+
* the stack already there, with every card in it at its full height, so
|
|
301
|
+
* collapsing never changes which cards are held. When the cards held over
|
|
302
|
+
* each other at both edges are taller than the window, the lowest-ranked
|
|
303
|
+
* collapse to their header row, one by one, until they fit or one is left
|
|
304
|
+
* open; that one is cut to the room left.
|
|
305
|
+
*
|
|
306
|
+
* Returns the held cards only; a card not in the map stays with the page.
|
|
307
|
+
*/
|
|
308
|
+
export function heldAt(
|
|
309
|
+
cards: readonly HoldCard[],
|
|
310
|
+
band: HoldBand,
|
|
311
|
+
gap: number,
|
|
312
|
+
): Map<string, Held> {
|
|
313
|
+
const blocks = band.blocks ?? [];
|
|
314
|
+
const ceiling = (card: HoldCard) =>
|
|
315
|
+
blocks
|
|
316
|
+
.filter((block) => overX(block, card.box))
|
|
317
|
+
.reduce((top, block) => Math.max(top, block.bottom + gap), band.top);
|
|
318
|
+
const full = (card: HoldCard) => card.box.bottom - card.box.top;
|
|
319
|
+
/** The room the stack already at an edge takes over `card`, every card at its full height. */
|
|
320
|
+
const taken = (stack: readonly HoldCard[], card: HoldCard) =>
|
|
321
|
+
stack
|
|
322
|
+
.filter((other) => overX(other.box, card.box))
|
|
323
|
+
.reduce((sum, other) => sum + full(other) + gap, 0);
|
|
324
|
+
|
|
325
|
+
// K2: the top edge first, the card nearest it first, so each joins the
|
|
326
|
+
// stack its predecessors made.
|
|
327
|
+
const topKey = (card: HoldCard) =>
|
|
328
|
+
card.edge === 'top' ? Number.NEGATIVE_INFINITY : card.box.top;
|
|
329
|
+
const top: HoldCard[] = [];
|
|
330
|
+
for (const card of cards
|
|
331
|
+
.filter((candidate) => candidate.edge !== 'bottom')
|
|
332
|
+
.sort((a, b) => ascending(topKey(a), topKey(b)) || a.order - b.order)) {
|
|
333
|
+
if (card.edge === 'top' || card.box.top < ceiling(card) + taken(top, card) - EPSILON) {
|
|
334
|
+
top.push(card);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const atTop = new Set(top);
|
|
338
|
+
const bottomKey = (card: HoldCard) =>
|
|
339
|
+
card.edge === 'bottom' ? Number.POSITIVE_INFINITY : card.box.bottom;
|
|
340
|
+
const bottom: HoldCard[] = [];
|
|
341
|
+
for (const card of cards
|
|
342
|
+
.filter((candidate) => !atTop.has(candidate) && candidate.edge !== 'top')
|
|
343
|
+
.sort((a, b) => ascending(bottomKey(b), bottomKey(a)) || a.order - b.order)) {
|
|
344
|
+
if (card.edge === 'bottom' || card.box.bottom > band.bottom - taken(bottom, card) + EPSILON) {
|
|
345
|
+
bottom.push(card);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// K3: collapse the lowest-ranked card over a crowded one, until none is
|
|
350
|
+
// crowded or each crowded stack has one card left open.
|
|
351
|
+
const held = [...top, ...bottom];
|
|
352
|
+
const collapsed = new Set<string>();
|
|
353
|
+
const shown = (card: HoldCard) => (collapsed.has(card.key) ? card.headerHeight : full(card));
|
|
354
|
+
const load = (card: HoldCard) =>
|
|
355
|
+
held
|
|
356
|
+
.filter((other) => overX(other.box, card.box))
|
|
357
|
+
.reduce((sum, other) => sum + shown(other) + gap, -gap);
|
|
358
|
+
const room = (card: HoldCard) => band.bottom - ceiling(card);
|
|
359
|
+
for (let progress = true; progress; ) {
|
|
360
|
+
progress = false;
|
|
361
|
+
for (const card of held) {
|
|
362
|
+
if (load(card) <= room(card) + EPSILON) continue;
|
|
363
|
+
const open = held.filter((other) => overX(other.box, card.box) && !collapsed.has(other.key));
|
|
364
|
+
if (open.length < 2) continue;
|
|
365
|
+
const newest = open.reduce((a, b) => (b.rank > a.rank ? b : a));
|
|
366
|
+
const oldest = open
|
|
367
|
+
.filter((other) => other !== newest)
|
|
368
|
+
.reduce((a, b) => (b.rank < a.rank ? b : a));
|
|
369
|
+
collapsed.add(oldest.key);
|
|
370
|
+
progress = true;
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// The one card left open in a stack that is still too tall is cut to fit.
|
|
375
|
+
const caps = new Map<string, number>();
|
|
376
|
+
for (const card of held) {
|
|
377
|
+
const over = load(card) - room(card);
|
|
378
|
+
if (over <= EPSILON) continue;
|
|
379
|
+
for (const open of held.filter((other) => overX(other.box, card.box) && !collapsed.has(other.key))) {
|
|
380
|
+
const cap = Math.max(open.headerHeight, full(open) - over);
|
|
381
|
+
caps.set(open.key, Math.min(caps.get(open.key) ?? Number.POSITIVE_INFINITY, cap));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const height = (card: HoldCard) => caps.get(card.key) ?? shown(card);
|
|
385
|
+
|
|
386
|
+
const result = new Map<string, Held>();
|
|
387
|
+
const stack = (members: readonly HoldCard[], edge: HoldEdge) => {
|
|
388
|
+
const placed: { readonly box: Box; readonly top: number; readonly bottom: number }[] = [];
|
|
389
|
+
for (const card of [...members].sort((a, b) => a.order - b.order)) {
|
|
390
|
+
const h = height(card);
|
|
391
|
+
const under = placed.filter((other) => overX(other.box, card.box));
|
|
392
|
+
const at =
|
|
393
|
+
edge === 'top'
|
|
394
|
+
? Math.max(ceiling(card), ...under.map((other) => other.bottom + gap))
|
|
395
|
+
: Math.min(band.bottom, ...under.map((other) => other.top - gap)) - h;
|
|
396
|
+
placed.push({ box: card.box, top: at, bottom: at + h });
|
|
397
|
+
result.set(card.key, {
|
|
398
|
+
edge,
|
|
399
|
+
top: at,
|
|
400
|
+
collapsed: collapsed.has(card.key),
|
|
401
|
+
maxHeight: caps.get(card.key) ?? null,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
stack(top, 'top');
|
|
406
|
+
stack(bottom, 'bottom');
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
|
|
236
410
|
// ---------------------------------------------------------------------------
|
|
237
411
|
// The DOM layer.
|
|
238
412
|
// ---------------------------------------------------------------------------
|
|
@@ -268,19 +442,33 @@ export interface PlaceOptions {
|
|
|
268
442
|
readonly sideColumn?: PlacementInput['sideColumn'];
|
|
269
443
|
}
|
|
270
444
|
|
|
271
|
-
/**
|
|
445
|
+
/**
|
|
446
|
+
* The safe region R a card must not cover: the enclosing display equation,
|
|
447
|
+
* the whole float when the card hangs from the float's own number (its
|
|
448
|
+
* toolbar's Pin, `float-toolbar.ts`), else the trigger.
|
|
449
|
+
*/
|
|
272
450
|
export const regionOf = (trigger: HTMLElement): HTMLElement =>
|
|
273
|
-
trigger.closest<HTMLElement>('.katex-display') ??
|
|
451
|
+
trigger.closest<HTMLElement>('.katex-display') ??
|
|
452
|
+
(trigger.matches('a.keyed-equation__number, a.float-caption__number')
|
|
453
|
+
? trigger.closest<HTMLElement>('.keyed-equation, figure')
|
|
454
|
+
: null) ??
|
|
455
|
+
trigger;
|
|
274
456
|
|
|
275
|
-
/**
|
|
457
|
+
/**
|
|
458
|
+
* The window below the sticky header and between the side panels, inset by
|
|
459
|
+
* `edge`, in viewport coordinates. A card wider than the article column
|
|
460
|
+
* centres in this room, not in the whole window: the navigation and the rail
|
|
461
|
+
* paint over the card layer, so a card reaching under one lost its edge.
|
|
462
|
+
*/
|
|
276
463
|
export function cardBand(edge: number): Box {
|
|
277
464
|
const header = document.querySelector<HTMLElement>('.header');
|
|
278
465
|
const headerBottom = header ? header.getBoundingClientRect().bottom : 0;
|
|
279
466
|
const root = document.documentElement;
|
|
467
|
+
const { left, right } = sidebarsBetween(edge);
|
|
280
468
|
return {
|
|
281
|
-
left
|
|
469
|
+
left,
|
|
282
470
|
top: Math.max(edge, headerBottom + edge),
|
|
283
|
-
right:
|
|
471
|
+
right: Math.max(left, right),
|
|
284
472
|
bottom: root.clientHeight - edge,
|
|
285
473
|
};
|
|
286
474
|
}
|
|
@@ -354,11 +542,20 @@ export function placeElement(
|
|
|
354
542
|
): CardAnchor {
|
|
355
543
|
const band = cardBand(options.edge);
|
|
356
544
|
const bandHeight = Math.max(band.bottom - band.top, 0);
|
|
545
|
+
const bandWidth = Math.max(band.right - band.left, 0);
|
|
357
546
|
// Measure with all the room the window offers, then keep whatever part of
|
|
358
|
-
// it the chosen spot allows.
|
|
547
|
+
// it the chosen spot allows. Only a card wider than the room between the
|
|
548
|
+
// side panels is narrowed to it, and measured again as it wraps: an inline
|
|
549
|
+
// cap on every card would replace the stylesheet's own (a reference card's
|
|
550
|
+
// `80ch`) and let a card grow past it.
|
|
359
551
|
el.style.maxHeight = `${bandHeight}px`;
|
|
552
|
+
el.style.maxWidth = '';
|
|
360
553
|
moveTo(el, band.left, band.top);
|
|
361
|
-
|
|
554
|
+
let size = el.getBoundingClientRect();
|
|
555
|
+
if (size.width > bandWidth + EPSILON) {
|
|
556
|
+
el.style.maxWidth = `${bandWidth}px`;
|
|
557
|
+
size = el.getBoundingClientRect();
|
|
558
|
+
}
|
|
362
559
|
const region = regionOf(trigger).getBoundingClientRect();
|
|
363
560
|
const triggerBox = trigger.getBoundingClientRect();
|
|
364
561
|
|
|
@@ -426,7 +623,8 @@ export function anchoredSpot(
|
|
|
426
623
|
/**
|
|
427
624
|
* Put `el` back at its anchor after the layout moved its trigger (P1). A
|
|
428
625
|
* trigger with no box (inside a closed disclosure) leaves the card where it
|
|
429
|
-
* is.
|
|
626
|
+
* is — `card-manager.ts`'s `foldAway` hides a kept one meanwhile. Returns
|
|
627
|
+
* whether the trigger could be measured.
|
|
430
628
|
*/
|
|
431
629
|
export function reanchorElement(
|
|
432
630
|
el: HTMLElement,
|
|
@@ -176,6 +176,53 @@ export function layoutColumn(
|
|
|
176
176
|
return tops;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
/** S11: the reader's width for docked cards, from the width menu. */
|
|
180
|
+
export type SideWidthPreset = 'default' | 'wide' | 'very-wide';
|
|
181
|
+
|
|
182
|
+
export interface SideWidthOption {
|
|
183
|
+
readonly id: SideWidthPreset;
|
|
184
|
+
readonly label: string;
|
|
185
|
+
/** Characters a line of the card's text holds; `null` for the default, the column's own width. */
|
|
186
|
+
readonly characters: number | null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** S11: Default is today's width (S1); the others count characters, so they follow the text size. */
|
|
190
|
+
export const SIDE_WIDTH_OPTIONS: readonly SideWidthOption[] = [
|
|
191
|
+
{ id: 'default', label: 'Default', characters: null },
|
|
192
|
+
{ id: 'wide', label: 'Wide', characters: 65 },
|
|
193
|
+
{ id: 'very-wide', label: 'Very wide', characters: 90 },
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
export interface ClosedRoomInput {
|
|
197
|
+
readonly windowWidth: number;
|
|
198
|
+
/** A collapsed side panel's strip. */
|
|
199
|
+
readonly strip: number;
|
|
200
|
+
/** The article's own start padding, kept when it is slid fully left. */
|
|
201
|
+
readonly contentPad: number;
|
|
202
|
+
readonly articleWidth: number;
|
|
203
|
+
/** S1: from the article to a side card, and from a side card to the rail. */
|
|
204
|
+
readonly gap: number;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** S11: the room beside the article with both side panels collapsed to their strips and the article fully left. */
|
|
208
|
+
export const roomWithPanelsClosed = (input: ClosedRoomInput): number =>
|
|
209
|
+
input.windowWidth - input.strip - input.contentPad - input.articleWidth - 2 * input.gap - input.strip;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* S11: the sizes worth offering: Default always, a wider one only when it
|
|
213
|
+
* fits that room, so a reader who collapses both panels gets the whole
|
|
214
|
+
* card. With only Default left, the menu is not shown at all.
|
|
215
|
+
*/
|
|
216
|
+
export function offeredWidths(
|
|
217
|
+
widths: ReadonlyMap<SideWidthPreset, number>,
|
|
218
|
+
roomClosed: number,
|
|
219
|
+
): SideWidthPreset[] {
|
|
220
|
+
return SIDE_WIDTH_OPTIONS.filter(
|
|
221
|
+
(option) =>
|
|
222
|
+
option.id === 'default' || (widths.get(option.id) ?? Infinity) <= roomClosed + EPSILON,
|
|
223
|
+
).map((option) => option.id);
|
|
224
|
+
}
|
|
225
|
+
|
|
179
226
|
// ---------------------------------------------------------------------------
|
|
180
227
|
// The DOM layer.
|
|
181
228
|
// ---------------------------------------------------------------------------
|
|
@@ -186,6 +233,9 @@ const SLID_ATTRIBUTE = 'data-article-slid';
|
|
|
186
233
|
const START_PROPERTY = '--explico-article-start';
|
|
187
234
|
/** S9, remembered across pages and visits. */
|
|
188
235
|
const KEEP_LEFT_KEY = 'explico:layout:keep-article-left';
|
|
236
|
+
/** S11: the width menu's choice, and whether the reader has resized a card, which offers the menu. */
|
|
237
|
+
const SIDE_WIDTH_KEY = 'explico:layout:side-card-width';
|
|
238
|
+
const SIDE_WIDTH_OFFERED_KEY = 'explico:layout:side-card-width-offered';
|
|
189
239
|
/** S7: below this there is never a side column, the same breakpoint Starlight drops the rail at. */
|
|
190
240
|
const SIDE_QUERY = '(min-width: 72rem)';
|
|
191
241
|
|
|
@@ -241,6 +291,38 @@ export function railLeftEdge(): number {
|
|
|
241
291
|
return rect.width > 0 ? rect.left : windowRight;
|
|
242
292
|
}
|
|
243
293
|
|
|
294
|
+
/**
|
|
295
|
+
* The navigation's right edge: collapsed, its edge strip's, since the panel
|
|
296
|
+
* itself widens over the article while it peeks. `null` without a visible
|
|
297
|
+
* navigation (a phone, a page without one).
|
|
298
|
+
*/
|
|
299
|
+
function navigationRightEdge(): number | null {
|
|
300
|
+
const pane = document.querySelector<HTMLElement>('.sidebar-pane');
|
|
301
|
+
if (!pane || pane.getClientRects().length === 0) return null;
|
|
302
|
+
if (getComputedStyle(pane).visibility === 'hidden') return null;
|
|
303
|
+
const rect = pane.getBoundingClientRect();
|
|
304
|
+
if (rect.width === 0) return null;
|
|
305
|
+
if (root().hasAttribute('data-navigation-hidden')) {
|
|
306
|
+
const strip = getComputedStyle(root()).getPropertyValue('--layout-edge-rail-width').trim();
|
|
307
|
+
const width = strip ? remToPx(strip) : Number.NaN;
|
|
308
|
+
if (Number.isFinite(width) && width > 0) return rect.left + width;
|
|
309
|
+
}
|
|
310
|
+
return rect.right;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* D2, P2: the room between the navigation and the rail, each `edge` px away,
|
|
315
|
+
* in viewport x. A placed card stays in it as a moved one does, so neither
|
|
316
|
+
* side panel paints over part of a card.
|
|
317
|
+
*/
|
|
318
|
+
export function sidebarsBetween(edge: number): { readonly left: number; readonly right: number } {
|
|
319
|
+
const nav = navigationRightEdge();
|
|
320
|
+
return {
|
|
321
|
+
left: (nav ?? 0) + edge,
|
|
322
|
+
right: railLeftEdge() - edge,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
244
326
|
/**
|
|
245
327
|
* S1, S3, S7: the article and the rail, measured with the article at rest.
|
|
246
328
|
* `null` when the layout can never have a side column: below 72rem, a
|
|
@@ -288,49 +370,217 @@ export const readKeepLeft = (): boolean => preferences.get(KEEP_LEFT_KEY) === 't
|
|
|
288
370
|
|
|
289
371
|
export const writeKeepLeft = (on: boolean): void => preferences.set(KEEP_LEFT_KEY, String(on));
|
|
290
372
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
373
|
+
/** S11: the width menu's choice; Default until the reader picks another. */
|
|
374
|
+
export function readSideWidth(): SideWidthPreset {
|
|
375
|
+
const stored = preferences.get(SIDE_WIDTH_KEY);
|
|
376
|
+
return SIDE_WIDTH_OPTIONS.some((option) => option.id === stored)
|
|
377
|
+
? (stored as SideWidthPreset)
|
|
378
|
+
: 'default';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export const writeSideWidth = (preset: SideWidthPreset): void =>
|
|
382
|
+
preferences.set(SIDE_WIDTH_KEY, preset);
|
|
383
|
+
|
|
384
|
+
/** S11: the menu is offered once the reader has resized a card by its edge. */
|
|
385
|
+
export const readSideWidthOffered = (): boolean =>
|
|
386
|
+
preferences.get(SIDE_WIDTH_OFFERED_KEY) === 'true';
|
|
387
|
+
|
|
388
|
+
export const writeSideWidthOffered = (): void => preferences.set(SIDE_WIDTH_OFFERED_KEY, 'true');
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* S11: a size in pixels for each option, measured in the card's own text:
|
|
392
|
+
* Default is `defaultWidth`, the others their characters plus the card's
|
|
393
|
+
* padding and border.
|
|
394
|
+
*/
|
|
395
|
+
export function sideWidthPixels(layer: HTMLElement, defaultWidth: number): Map<SideWidthPreset, number> {
|
|
396
|
+
const probe = document.createElement('span');
|
|
397
|
+
probe.style.cssText =
|
|
398
|
+
'position:absolute;visibility:hidden;width:100ch;font-size:max(12px,var(--sl-text-sm))';
|
|
399
|
+
layer.append(probe);
|
|
400
|
+
const ch = probe.getBoundingClientRect().width / 100;
|
|
401
|
+
probe.remove();
|
|
402
|
+
const chrome = remToPx('2rem') + 2;
|
|
403
|
+
const widths = new Map<SideWidthPreset, number>();
|
|
404
|
+
for (const option of SIDE_WIDTH_OPTIONS) {
|
|
405
|
+
widths.set(
|
|
406
|
+
option.id,
|
|
407
|
+
option.characters === null ? defaultWidth : Math.round(option.characters * ch + chrome),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return widths;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** S11: the room the width menu offers sizes for, with this page's panels and article. */
|
|
414
|
+
export function closedRoom(gap: number): number {
|
|
415
|
+
const article = articleOf();
|
|
416
|
+
if (!article) return 0;
|
|
417
|
+
const style = getComputedStyle(root());
|
|
418
|
+
const strip = remToPx(style.getPropertyValue('--layout-edge-rail-width').trim() || '2.5rem');
|
|
419
|
+
const contentPad = remToPx(style.getPropertyValue('--sl-content-pad-x').trim() || '1.5rem');
|
|
420
|
+
return roomWithPanelsClosed({
|
|
421
|
+
windowWidth: root().clientWidth,
|
|
422
|
+
strip,
|
|
423
|
+
contentPad,
|
|
424
|
+
articleWidth: article.getBoundingClientRect().width,
|
|
425
|
+
gap,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export interface SideControlsState {
|
|
430
|
+
/** Show the row, or hide it. */
|
|
431
|
+
readonly visible: boolean;
|
|
432
|
+
/**
|
|
433
|
+
* Where the row sits, in viewport x: at the side column's left edge while
|
|
434
|
+
* the column is in use, else with its right edge `right` px from the
|
|
435
|
+
* window's, at the article's top right (K1, D7).
|
|
436
|
+
*/
|
|
437
|
+
readonly at: { readonly left: number } | { readonly right: number };
|
|
438
|
+
/** S9: "Article left". */
|
|
439
|
+
readonly keepLeft: { readonly visible: boolean; readonly on: boolean };
|
|
440
|
+
/** K1: "Carry cards", while a card is kept. */
|
|
441
|
+
readonly carry: { readonly visible: boolean; readonly on: boolean };
|
|
442
|
+
/** S11: the width menu, with the sizes it offers; `null` hides it. */
|
|
443
|
+
readonly width: {
|
|
444
|
+
readonly offered: readonly SideWidthPreset[];
|
|
445
|
+
readonly value: SideWidthPreset | 'custom';
|
|
446
|
+
/** Each size's pixels, for its tooltip. */
|
|
447
|
+
readonly characters: ReadonlyMap<SideWidthPreset, number | null>;
|
|
448
|
+
} | null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export interface SideControls {
|
|
452
|
+
update(state: SideControlsState): void;
|
|
453
|
+
/** K2: the row's box while it shows; a card held at the top edge under it stacks below it. */
|
|
454
|
+
box(): DOMRect | null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export interface SideControlsHandlers {
|
|
458
|
+
onKeepLeft(on: boolean): void;
|
|
459
|
+
onCarry(on: boolean): void;
|
|
460
|
+
onWidth(preset: SideWidthPreset): void;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** A switch: a knob and a short label, its full sentence in the tooltip (D6). */
|
|
464
|
+
function createSwitch(label: string, title: string, onToggle: (on: boolean) => void): HTMLButtonElement {
|
|
465
|
+
const button = document.createElement('button');
|
|
466
|
+
button.type = 'button';
|
|
467
|
+
button.className = 'explico-side-switch';
|
|
468
|
+
button.title = title;
|
|
469
|
+
button.setAttribute('aria-pressed', 'false');
|
|
470
|
+
const knob = document.createElement('span');
|
|
471
|
+
knob.className = 'explico-side-switch-knob';
|
|
472
|
+
knob.setAttribute('aria-hidden', 'true');
|
|
473
|
+
const text = document.createElement('span');
|
|
474
|
+
text.textContent = label;
|
|
475
|
+
button.append(knob, text);
|
|
476
|
+
button.addEventListener('click', () => onToggle(button.getAttribute('aria-pressed') !== 'true'));
|
|
477
|
+
return button;
|
|
294
478
|
}
|
|
295
479
|
|
|
296
480
|
/**
|
|
297
|
-
*
|
|
298
|
-
*
|
|
481
|
+
* The row of card controls, fixed just under the site header: at the top of
|
|
482
|
+
* the side column, "Article left" (S9), "Carry cards" (K1) and, once the
|
|
483
|
+
* reader has resized a card, the width menu (S11); with no side column in
|
|
484
|
+
* use, "Carry cards" alone at the article's top right, while a card is kept
|
|
485
|
+
* (D7). Short labels, so the row fits a 1920 px window's column; each
|
|
486
|
+
* control's tooltip says it in full. It mounts right after the card layer,
|
|
299
487
|
* so it sits over the cards scrolling past and under the header.
|
|
300
488
|
*/
|
|
301
|
-
export function
|
|
489
|
+
export function createSideControls(
|
|
302
490
|
after: () => HTMLElement,
|
|
303
|
-
|
|
304
|
-
):
|
|
305
|
-
let
|
|
491
|
+
handlers: SideControlsHandlers,
|
|
492
|
+
): SideControls {
|
|
493
|
+
let row: HTMLElement | null = null;
|
|
494
|
+
let keepLeft: HTMLButtonElement | null = null;
|
|
495
|
+
let carry: HTMLButtonElement | null = null;
|
|
496
|
+
let width: HTMLLabelElement | null = null;
|
|
497
|
+
let select: HTMLSelectElement | null = null;
|
|
306
498
|
|
|
307
499
|
const build = () => {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
500
|
+
row = document.createElement('div');
|
|
501
|
+
row.className = 'explico-side-controls';
|
|
502
|
+
row.hidden = true;
|
|
503
|
+
|
|
504
|
+
keepLeft = createSwitch(
|
|
505
|
+
'Article left',
|
|
506
|
+
'Keep the article on the left after the last side card closes',
|
|
507
|
+
handlers.onKeepLeft,
|
|
508
|
+
);
|
|
509
|
+
keepLeft.dataset.keepArticleLeft = '';
|
|
510
|
+
carry = createSwitch(
|
|
511
|
+
'Carry cards',
|
|
512
|
+
'Keep the cards you pin on screen as you scroll, and bring them along to every page of the course',
|
|
513
|
+
handlers.onCarry,
|
|
514
|
+
);
|
|
515
|
+
carry.dataset.carryCards = '';
|
|
516
|
+
|
|
517
|
+
width = document.createElement('label');
|
|
518
|
+
width.className = 'explico-side-width';
|
|
519
|
+
width.hidden = true;
|
|
520
|
+
const caption = document.createElement('span');
|
|
521
|
+
caption.textContent = 'Width';
|
|
522
|
+
select = document.createElement('select');
|
|
523
|
+
select.dataset.sideCardWidth = '';
|
|
524
|
+
select.setAttribute('aria-label', 'Width of the cards beside the article');
|
|
525
|
+
select.addEventListener('change', () => {
|
|
526
|
+
const value = select?.value;
|
|
527
|
+
if (SIDE_WIDTH_OPTIONS.some((option) => option.id === value)) {
|
|
528
|
+
handlers.onWidth(value as SideWidthPreset);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
width.append(caption, select);
|
|
532
|
+
|
|
533
|
+
row.append(keepLeft, carry, width);
|
|
534
|
+
after().after(row);
|
|
322
535
|
};
|
|
323
536
|
|
|
324
537
|
return {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
538
|
+
box() {
|
|
539
|
+
return row?.isConnected && !row.hidden ? row.getBoundingClientRect() : null;
|
|
540
|
+
},
|
|
541
|
+
update(state) {
|
|
542
|
+
if (!state.visible && !row) return;
|
|
543
|
+
if (!row?.isConnected) build();
|
|
544
|
+
if (!row || !keepLeft || !carry || !width || !select) return;
|
|
545
|
+
row.hidden = !state.visible;
|
|
546
|
+
if (!state.visible) return;
|
|
547
|
+
const { at } = state;
|
|
548
|
+
row.style.left = 'left' in at ? `${Math.round(at.left)}px` : '';
|
|
549
|
+
row.style.right = 'right' in at ? `${Math.round(at.right)}px` : '';
|
|
550
|
+
row.dataset.at = 'left' in at ? 'column' : 'article';
|
|
551
|
+
keepLeft.hidden = !state.keepLeft.visible;
|
|
552
|
+
keepLeft.setAttribute('aria-pressed', String(state.keepLeft.on));
|
|
553
|
+
carry.hidden = !state.carry.visible;
|
|
554
|
+
carry.setAttribute('aria-pressed', String(state.carry.on));
|
|
555
|
+
const menu = state.width;
|
|
556
|
+
width.hidden = !menu;
|
|
557
|
+
if (!menu) return;
|
|
558
|
+
const options = SIDE_WIDTH_OPTIONS.filter((option) => menu.offered.includes(option.id));
|
|
559
|
+
const wanted = [
|
|
560
|
+
...options.map((option) => [option.id, option.label] as const),
|
|
561
|
+
...(menu.value === 'custom' ? [['custom', 'Custom'] as const] : []),
|
|
562
|
+
];
|
|
563
|
+
const current = Array.from(select.options, (option) => `${option.value}=${option.text}`).join('|');
|
|
564
|
+
const next = wanted.map(([value, text]) => `${value}=${text}`).join('|');
|
|
565
|
+
if (current !== next) {
|
|
566
|
+
select.replaceChildren(
|
|
567
|
+
...wanted.map(([value, text]) => {
|
|
568
|
+
const option = document.createElement('option');
|
|
569
|
+
option.value = value;
|
|
570
|
+
option.textContent = text;
|
|
571
|
+
if (value === 'custom') option.disabled = true;
|
|
572
|
+
return option;
|
|
573
|
+
}),
|
|
574
|
+
);
|
|
330
575
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
576
|
+
select.value = menu.value;
|
|
577
|
+
const characters = menu.value === 'custom' ? null : menu.characters.get(menu.value);
|
|
578
|
+
width.title =
|
|
579
|
+
menu.value === 'custom'
|
|
580
|
+
? 'A card here has a width of its own, from its edge; pick a size for every card'
|
|
581
|
+
: characters
|
|
582
|
+
? `Cards beside the article hold ${characters} characters a line`
|
|
583
|
+
: 'Cards beside the article at their usual width';
|
|
334
584
|
},
|
|
335
585
|
};
|
|
336
586
|
}
|