explico 0.3.0 → 0.4.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +72 -1
  2. package/README.md +21 -2
  3. package/dist/compiler/astro.d.ts +30 -0
  4. package/dist/compiler/astro.js +79 -0
  5. package/dist/compiler/cli.js +5 -9
  6. package/dist/compiler/course-config.d.ts +29 -4
  7. package/dist/compiler/course-config.js +12 -0
  8. package/dist/compiler/lesson-derivation.d.ts +1 -1
  9. package/dist/compiler/lesson-derivation.js +2 -2
  10. package/dist/compiler/manifest.d.ts +25 -4
  11. package/dist/compiler/manifest.js +59 -27
  12. package/dist/reference/equations-validate.d.ts +8 -3
  13. package/dist/reference/equations-validate.js +1 -1
  14. package/dist/reference/equations.d.mts +58 -22
  15. package/dist/reference/equations.mjs +75 -26
  16. package/dist/reference/keyed-float-validate.d.ts +9 -3
  17. package/dist/reference/keyed-float-validate.js +1 -1
  18. package/dist/reference/keyed-jsx-floats.d.mts +18 -10
  19. package/dist/reference/keyed-jsx-floats.mjs +42 -18
  20. package/dist/reference/remark-notation.d.mts +54 -5
  21. package/dist/reference/remark-notation.mjs +161 -17
  22. package/docs/authoring/README.md +4 -0
  23. package/docs/authoring/migrating.md +62 -0
  24. package/docs/authoring/notation.md +8 -7
  25. package/docs/authoring/structure.md +139 -0
  26. package/package.json +1 -1
  27. package/src/compiler/astro.ts +127 -0
  28. package/src/compiler/cli.ts +5 -9
  29. package/src/compiler/course-config.ts +39 -3
  30. package/src/compiler/lesson-derivation.ts +2 -2
  31. package/src/compiler/manifest.ts +108 -36
  32. package/src/components/notation/NotationLayer.astro +11 -0
  33. package/src/components/starlight/LayoutSidebar.astro +62 -1
  34. package/src/components/starlight/LessonFooter.astro +11 -0
  35. package/src/components/table/Table.astro +3 -2
  36. package/src/reference/equations-validate.ts +6 -4
  37. package/src/reference/equations.mjs +75 -26
  38. package/src/reference/keyed-float-validate.ts +7 -4
  39. package/src/reference/keyed-jsx-floats.mjs +42 -18
  40. package/src/reference/remark-notation.mjs +161 -17
  41. package/src/styles/global.css +129 -0
@@ -22,7 +22,11 @@
22
22
  * `equations.test.ts` holds equations to.
23
23
  */
24
24
 
25
- import { stripFencedCode, stripJsxComponents } from './equations.mjs';
25
+ import {
26
+ resolveNumbering,
27
+ stripFencedCode,
28
+ stripJsxComponents,
29
+ } from './equations.mjs';
26
30
 
27
31
  /** Same shape as `EQ_KEY_PATTERN` — the notation-key grammar without dots. */
28
32
  export const KEYED_FLOAT_KEY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -33,15 +37,22 @@ function floatNumber(section, index) {
33
37
  }
34
38
 
35
39
  /**
36
- * The visible number for one float. With `chapter` (the lesson's course
37
- * ordinal) the scheme is `‹chapter›.‹indexInLesson›` and `##` headings are
38
- * ignored; without it, the legacy `##`-section scheme. Mirrors
39
- * `displayEquationNumber` in `equations.mjs`.
40
+ * The visible number for one float. With a `prefix` (the part's ordinal) the
41
+ * scheme is `‹part›.‹offset + indexInLesson›` and `##` headings are ignored;
42
+ * without it, the legacy `##`-section scheme. Mirrors `displayEquationNumber`
43
+ * in `equations.mjs`. Each kind of float counts on its own — a `<Table>` never
44
+ * advances the `<Figure>` sequence — so `offset` is per kind too.
40
45
  */
41
- function keyedFloatNumber({ chapter, section, indexInSection, indexInLesson }) {
42
- return chapter == null
46
+ function keyedFloatNumber({
47
+ prefix,
48
+ offset = 0,
49
+ section,
50
+ indexInSection,
51
+ indexInLesson,
52
+ }) {
53
+ return prefix == null
43
54
  ? floatNumber(section, indexInSection)
44
- : `${chapter}.${indexInLesson}`;
55
+ : `${prefix}.${offset + indexInLesson}`;
45
56
  }
46
57
 
47
58
  /** The anchor id for a float, e.g. `{prefix:'tab', key:'foo'}` → `tab-foo`. */
@@ -71,13 +82,15 @@ function readIdAttribute(attributesSource) {
71
82
  /**
72
83
  * The raw-text equivalent of `collectKeyedJsxFloats`, mirroring
73
84
  * `scanEquationLabels`: every top-level `<TagName …>` in appearance order.
74
- * With `chapter` (the lesson's course ordinal) the number is `‹chapter›.‹n›`,
75
- * `n` sequential over the whole lesson; without it, `##` headings advance a
76
- * section counter. Other JSX components' bodies are blanked first so a
77
- * `<Table>` nested inside e.g. `<CompactExample>` (and any `##`-like text
78
- * inside an unrelated component) is never counted.
85
+ * With `numbering` (`{ prefix, offset }` — the part's ordinal and the count of
86
+ * this kind of float in earlier lessons of that part) the number is
87
+ * `‹part›.‹offset + n›`, `n` sequential over the whole lesson; without it, `##`
88
+ * headings advance a section counter. Other JSX components' bodies are blanked
89
+ * first so a `<Table>` nested inside e.g. `<CompactExample>` (and any `##`-like
90
+ * text inside an unrelated component) is never counted.
79
91
  */
80
- export function scanKeyedJsxFloats(markdown, { tagName, chapter }) {
92
+ export function scanKeyedJsxFloats(markdown, { tagName, numbering }) {
93
+ const { prefix, offset } = resolveNumbering(numbering);
81
94
  const text = stripJsxComponents(stripFencedCode(markdown), tagName);
82
95
  const tokens = [];
83
96
  const heading = /^[ \t]{0,3}##[ \t]+\S/gm;
@@ -106,7 +119,8 @@ export function scanKeyedJsxFloats(markdown, { tagName, chapter }) {
106
119
  labels.push({
107
120
  key: token.key,
108
121
  number: keyedFloatNumber({
109
- chapter,
122
+ prefix,
123
+ offset,
110
124
  section,
111
125
  indexInSection,
112
126
  indexInLesson,
@@ -118,14 +132,23 @@ export function scanKeyedJsxFloats(markdown, { tagName, chapter }) {
118
132
  return labels;
119
133
  }
120
134
 
135
+ /**
136
+ * How many floats of this kind a lesson body holds — the input to the next
137
+ * lesson's `offset` within the same part.
138
+ */
139
+ export function countKeyedJsxFloats(markdown, { tagName }) {
140
+ return scanKeyedJsxFloats(markdown, { tagName }).length;
141
+ }
142
+
121
143
  /**
122
144
  * The mdast equivalent of `scanKeyedJsxFloats`: walk the tree's *top-level*
123
145
  * children in document order, advancing the section counter on every
124
146
  * depth-2 heading and emitting a record for every `mdxJsxFlowElement` named
125
- * `tagName` — keyed or not, exactly like `collectDisplayEquations`. `chapter`
147
+ * `tagName` — keyed or not, exactly like `collectDisplayEquations`. `numbering`
126
148
  * is threaded the same way as in `scanKeyedJsxFloats`.
127
149
  */
128
- export function collectKeyedJsxFloats(tree, { tagName, chapter }) {
150
+ export function collectKeyedJsxFloats(tree, { tagName, numbering }) {
151
+ const { prefix, offset } = resolveNumbering(numbering);
129
152
  const floats = [];
130
153
  let section = 0;
131
154
  let indexInSection = 0;
@@ -151,7 +174,8 @@ export function collectKeyedJsxFloats(tree, { tagName, chapter }) {
151
174
  floats.push({
152
175
  key: key ?? null,
153
176
  number: keyedFloatNumber({
154
- chapter,
177
+ prefix,
178
+ offset,
155
179
  section,
156
180
  indexInSection,
157
181
  indexInLesson,
@@ -671,26 +671,146 @@ function inspectExplainCalls(value, resolve, file, node, references) {
671
671
  }
672
672
  }
673
673
 
674
+ /**
675
+ * The numbering inputs a lesson page needs, read off one manifest.
676
+ *
677
+ * Every number on a page comes from two places that MUST agree: the page's own
678
+ * counters (`numbering`) and the manifest tables a cross-page `[[slug#eq-key]]`
679
+ * is resolved against (`equations` / `tables` / `figures`). Deriving all four
680
+ * from the same manifest in one step is what stops a course config from
681
+ * supplying one without the other — the omission that produced page-local
682
+ * `(2.2)` and cross-page `(4.5)` for the same equation.
683
+ *
684
+ * `numbering` maps a lesson slug to `{ part, equations, tables, figures }`:
685
+ * the part's ordinal (the number before the dot) and, per kind of float, how
686
+ * many of them the earlier lessons of that part already used.
687
+ */
688
+ export function numberingFromManifest(manifest) {
689
+ if (!manifest || typeof manifest !== 'object') return {};
690
+ return {
691
+ equations: manifest.equations?.numbersBySlug ?? {},
692
+ tables: manifest.tables?.numbersBySlug ?? {},
693
+ figures: manifest.figures?.numbersBySlug ?? {},
694
+ numbering: Object.fromEntries(
695
+ (manifest.lessons ?? []).map((lesson) => [
696
+ lesson.slug,
697
+ {
698
+ part: lesson.part,
699
+ equations: lesson.numbering?.equations ?? 0,
700
+ tables: lesson.numbering?.tables ?? 0,
701
+ figures: lesson.numbering?.figures ?? 0,
702
+ },
703
+ ]),
704
+ ),
705
+ };
706
+ }
707
+
708
+ /**
709
+ * `{ prefix, offset }` for one kind of float on one page, from whichever
710
+ * numbering option the caller supplied. `numbering` is the current form;
711
+ * `chapters` is the superseded per-lesson form (prefix = the lesson's ordinal,
712
+ * no offset) and is still read so an unmigrated config keeps working.
713
+ */
714
+ function pageNumbering(options, slug, kind) {
715
+ if (!slug) return undefined;
716
+ const placement = options.numbering?.[slug];
717
+ if (placement?.part) {
718
+ return { prefix: placement.part, offset: placement[kind] ?? 0 };
719
+ }
720
+ const chapter = options.chapters?.[slug];
721
+ return chapter ? { prefix: chapter, offset: 0 } : undefined;
722
+ }
723
+
724
+ /**
725
+ * Fail when the number this page just computed for a keyed float disagrees
726
+ * with the number the manifest publishes for the same key (WERN — a reference
727
+ * that shows a different number than its target). The two are produced by the
728
+ * same algorithm from the same content, so any disagreement means the render
729
+ * path and the manifest were fed different inputs; in practice, an
730
+ * `astro.config.mjs` that passes the manifest number tables but not
731
+ * `numbering`, which silently drops the page back to the legacy `##`-section
732
+ * scheme.
733
+ */
734
+ function assertManifestNumberParity({
735
+ file,
736
+ slug,
737
+ pageLabels,
738
+ crossPage,
739
+ placement,
740
+ noun,
741
+ syntax,
742
+ }) {
743
+ // No entry for this slug at all means the caller supplied no manifest table
744
+ // (hand-built test trees, non-lesson pages) — nothing to cross-check.
745
+ const published = crossPage?.[slug];
746
+ if (!published) return;
747
+
748
+ for (const [key, number] of pageLabels) {
749
+ const expected = published[key];
750
+ if (expected === undefined) {
751
+ fail(
752
+ file,
753
+ `Stale Explico manifest for ${slug}: this page numbers ${noun} ` +
754
+ `${syntax(key)} as (${number}), but the manifest lists no such ` +
755
+ `${noun} for the lesson. Recompile the manifest before rendering.`,
756
+ );
757
+ }
758
+ if (expected !== number) {
759
+ fail(
760
+ file,
761
+ `Inconsistent ${noun} numbering for ${syntax(key)} in ${slug}: this ` +
762
+ `page renders it as (${number}) while a cross-page reference to it ` +
763
+ `resolves to (${expected}). ` +
764
+ (placement === undefined
765
+ ? 'The remarkNotation options carry the manifest number tables ' +
766
+ 'but no `numbering` entry for this lesson, so the page fell ' +
767
+ 'back to the legacy `##`-section scheme. Pass `manifest` to ' +
768
+ 'remarkNotation so both sides use the part scheme.'
769
+ : 'The page counters and the manifest disagree; recompile the ' +
770
+ 'manifest from the current content.'),
771
+ );
772
+ }
773
+ }
774
+ }
775
+
674
776
  /**
675
777
  * @param {{
676
778
  * definitions?: Map<string, object> | readonly object[] | Record<string, object> | (() => Map<string, object> | readonly object[] | Record<string, object>),
677
779
  * resolve?: (key: string, context: object) => object | undefined,
678
780
  * unknown?: 'error' | 'warn' | 'ignore',
679
781
  * base?: string,
782
+ * manifest?: object,
680
783
  * equations?: Record<string, Record<string, string>>,
681
784
  * tables?: Record<string, Record<string, string>>,
682
785
  * figures?: Record<string, Record<string, string>>,
786
+ * numbering?: Record<string, { part: number, equations: number, tables: number, figures: number }>,
683
787
  * chapters?: Record<string, number>,
684
788
  * }} [options]
685
789
  *
686
- * `chapters` maps a lesson slug to its 1-based course ordinal. When the
687
- * current page has one, every display equation / `<Table>` / `<Figure>` is
688
- * numbered `‹chapter›.‹n›` (`##` headings ignored); with no entry the legacy
689
- * `##`-section scheme applies. It must agree with `manifest.lessons[].chapter`,
690
- * which is what feeds `equations`/`tables`/`figures` above.
790
+ * `manifest` is the preferred wiring: pass the compiled manifest and the four
791
+ * numbering inputs below are read off it together, so they cannot drift apart.
792
+ * They stay available individually for hand-built trees, and an explicitly
793
+ * supplied one still wins over the manifest.
794
+ *
795
+ * `numbering` maps a lesson slug to its **part** (the number before the dot)
796
+ * and, per kind of float, how many the earlier lessons of that part already
797
+ * used. Every display equation / `<Table>` / `<Figure>` on the page is then
798
+ * numbered `‹part›.‹offset + n›`, with `##` and `###` headings ignored; with no
799
+ * entry the legacy `##`-section scheme applies. It must agree with
800
+ * `manifest.lessons[]`, which is what feeds `equations`/`tables`/`figures`
801
+ * above — supplying the tables without `numbering` numbers the same equation
802
+ * twice, and `assertManifestNumberParity` turns that into a build error rather
803
+ * than a page whose references disagree with their targets.
804
+ *
805
+ * `chapters` is the superseded per-lesson form (slug → the lesson's course
806
+ * ordinal, no running offset). Still read when `numbering` has no entry, so a
807
+ * config written against the old option keeps building.
691
808
  */
692
809
  export default function remarkNotation(options = {}) {
693
810
  const unknown = options.unknown ?? 'error';
811
+ // The manifest supplies every numbering input at once; an explicit option
812
+ // still overrides it, so existing per-field wiring keeps working.
813
+ const numbering = { ...numberingFromManifest(options.manifest), ...options };
694
814
 
695
815
  return function transform(tree, file) {
696
816
  const suppliedDefinitions =
@@ -703,10 +823,10 @@ export default function remarkNotation(options = {}) {
703
823
  );
704
824
  const localDefinitions = localDefinitionsFromFile(file);
705
825
  const lessonId = lessonIdFrom(file);
706
- // The lesson's course ordinal (see the `chapters` option). Falsy / missing
707
- // ⇒ the legacy `##`-section numbering, so `0` and `undefined` behave alike.
708
- const chapter =
709
- (lessonId && options.chapters?.[lessonId.replace(/\./g, '/')]) || undefined;
826
+ const lessonSlug = lessonId ? lessonId.replace(/\./g, '/') : undefined;
827
+ // `{ prefix, offset }` per kind of float — see the `numbering` option.
828
+ // Missing ⇒ the legacy `##`-section numbering.
829
+ const equationNumbering = pageNumbering(numbering, lessonSlug, 'equations');
710
830
  const registryFile = isNotationRegistryFile(file);
711
831
  const mathScope = pageGlyphScope(
712
832
  localDefinitions,
@@ -718,8 +838,9 @@ export default function remarkNotation(options = {}) {
718
838
 
719
839
  // Equation identity (D7): assign every `\label{eq:…}` its number before any
720
840
  // math node is rewritten, so a `[[eq-key]]` earlier in the page still
721
- // resolves. `chapter` picks the `‹chapter›.‹n›` vs `##`-section scheme.
722
- const equationLabels = collectEquationLabels(tree, chapter);
841
+ // resolves. `equationNumbering` picks the `‹part›.‹n›` vs `##`-section
842
+ // scheme.
843
+ const equationLabels = collectEquationLabels(tree, equationNumbering);
723
844
  const pageLabels = new Map();
724
845
  for (const label of equationLabels) {
725
846
  if (pageLabels.has(label.key)) {
@@ -734,14 +855,25 @@ export default function remarkNotation(options = {}) {
734
855
  const eqCtx = {
735
856
  base: options.base ?? '',
736
857
  pageLabels,
737
- crossPage: options.equations ?? {},
858
+ crossPage: numbering.equations ?? {},
738
859
  };
860
+ // WERN guard: what this page prints next to the equation must be what a
861
+ // reference from another page prints for it.
862
+ assertManifestNumberParity({
863
+ file,
864
+ slug: lessonSlug,
865
+ pageLabels,
866
+ crossPage: eqCtx.crossPage,
867
+ placement: equationNumbering,
868
+ noun: 'equation',
869
+ syntax: (key) => `eq:${key}`,
870
+ });
739
871
 
740
872
  // Number and anchor every display equation on a lesson page, keyed or
741
873
  // not (F3). Non-lesson pages (notation registry, stray docs) keep the
742
874
  // keyed-only behaviour so their body math is left untouched.
743
875
  const displayEquations = lessonId
744
- ? collectDisplayEquations(tree, chapter)
876
+ ? collectDisplayEquations(tree, equationNumbering)
745
877
  : [];
746
878
  const equationByNode = new Map(
747
879
  displayEquations.map((equation) => [equation.node, equation]),
@@ -750,9 +882,10 @@ export default function remarkNotation(options = {}) {
750
882
  // Keyed floats (F-tables-figures): every `<Table>`/`<Figure>` on a lesson
751
883
  // page is numbered and anchored, keyed or not — same rule as equations,
752
884
  // via the shared engine in `keyed-jsx-floats.mjs`.
753
- const makeFloatCtx = (tagName, crossPage) => {
885
+ const makeFloatCtx = (tagName, crossPage, kind) => {
886
+ const floatNumbering = pageNumbering(numbering, lessonSlug, kind);
754
887
  const floats = lessonId
755
- ? collectKeyedJsxFloats(tree, { tagName, chapter })
888
+ ? collectKeyedJsxFloats(tree, { tagName, numbering: floatNumbering })
756
889
  : [];
757
890
  const pageFloatLabels = new Map();
758
891
  for (const float of floats) {
@@ -766,6 +899,15 @@ export default function remarkNotation(options = {}) {
766
899
  }
767
900
  pageFloatLabels.set(float.key, float.number);
768
901
  }
902
+ assertManifestNumberParity({
903
+ file,
904
+ slug: lessonSlug,
905
+ pageLabels: pageFloatLabels,
906
+ crossPage: crossPage ?? {},
907
+ placement: floatNumbering,
908
+ noun: tagName,
909
+ syntax: (key) => `<${tagName} id="${key}">`,
910
+ });
769
911
  return {
770
912
  ctx: {
771
913
  base: options.base ?? '',
@@ -777,11 +919,13 @@ export default function remarkNotation(options = {}) {
777
919
  };
778
920
  const { ctx: tableCtx, byNode: tableByNode } = makeFloatCtx(
779
921
  'Table',
780
- options.tables,
922
+ numbering.tables,
923
+ 'tables',
781
924
  );
782
925
  const { ctx: figureCtx, byNode: figureByNode } = makeFloatCtx(
783
926
  'Figure',
784
- options.figures,
927
+ numbering.figures,
928
+ 'figures',
785
929
  );
786
930
 
787
931
  const resolve = (key, kind, node) => {
@@ -448,6 +448,135 @@
448
448
  min-height: 100%;
449
449
  }
450
450
 
451
+ /* A compact chapter remains useful as a row of lesson icons. The
452
+ link text doubles as a pointer/keyboard tooltip; opening the chapter via
453
+ its summary restores Starlight's normal, fully labelled list. */
454
+ .layout-compact-sidebar
455
+ details[data-compact]
456
+ > ul:not(.top-level) {
457
+ display: flex;
458
+ flex-wrap: wrap;
459
+ gap: 0.35rem;
460
+ margin: 0.2rem 0 0.35rem;
461
+ padding-inline-start: 0.5rem;
462
+ }
463
+
464
+ .layout-compact-sidebar
465
+ details[data-compact]
466
+ > ul:not(.top-level)
467
+ > li {
468
+ position: relative;
469
+ margin-inline-start: 0;
470
+ border-inline-start: 0;
471
+ padding-inline-start: 0;
472
+ }
473
+
474
+ .layout-compact-sidebar
475
+ details[data-compact]
476
+ > ul:not(.top-level)
477
+ > li
478
+ > a {
479
+ display: grid;
480
+ position: relative;
481
+ place-items: center;
482
+ width: 1.75rem;
483
+ height: 1.75rem;
484
+ border: 1px solid var(--sl-color-gray-5);
485
+ border-radius: 0.4rem;
486
+ padding: 0;
487
+ color: var(--sl-color-gray-3);
488
+ background: var(--sl-color-bg-sidebar);
489
+ }
490
+
491
+ .layout-compact-sidebar
492
+ details[data-compact]
493
+ > ul:not(.top-level)
494
+ > li
495
+ > a::before {
496
+ content: '';
497
+ width: 0.65rem;
498
+ height: 0.78rem;
499
+ border: 1.5px solid currentColor;
500
+ border-radius: 0.13rem;
501
+ box-shadow: 0.18rem 0.18rem 0 -0.1rem var(--sl-color-bg-sidebar),
502
+ 0.18rem 0.18rem 0 0 currentColor;
503
+ }
504
+
505
+ .layout-compact-sidebar
506
+ details[data-compact]
507
+ > ul:not(.top-level)
508
+ > li
509
+ > a:hover,
510
+ .layout-compact-sidebar
511
+ details[data-compact]
512
+ > ul:not(.top-level)
513
+ > li
514
+ > a:focus-visible {
515
+ border-color: var(--sl-color-accent);
516
+ color: var(--sl-color-text-accent);
517
+ background: var(--sl-color-accent-low);
518
+ }
519
+
520
+ .layout-compact-sidebar
521
+ details[data-compact]
522
+ > ul:not(.top-level)
523
+ > li
524
+ > a[aria-current='page'] {
525
+ color: var(--sl-color-text-invert);
526
+ background: var(--sl-color-text-accent);
527
+ }
528
+
529
+ .layout-compact-sidebar
530
+ details[data-compact]
531
+ > ul:not(.top-level)
532
+ > li
533
+ > a
534
+ > span {
535
+ position: absolute;
536
+ z-index: 2;
537
+ inset-block-start: 50%;
538
+ inset-inline-start: calc(100% + 0.45rem);
539
+ width: max-content;
540
+ max-width: 12rem;
541
+ border: 1px solid var(--sl-color-gray-5);
542
+ border-radius: 0.35rem;
543
+ padding: 0.3rem 0.5rem;
544
+ color: var(--sl-color-white);
545
+ background: var(--sl-color-bg-nav);
546
+ box-shadow: var(--sl-shadow-md);
547
+ font-size: var(--sl-text-xs);
548
+ font-weight: 500;
549
+ line-height: 1.3;
550
+ opacity: 0;
551
+ pointer-events: none;
552
+ transform: translateY(-50%) translateX(-0.2rem);
553
+ transition:
554
+ opacity 100ms ease,
555
+ transform 100ms ease;
556
+ }
557
+
558
+ .layout-compact-sidebar
559
+ details[data-compact]
560
+ > ul:not(.top-level)
561
+ > li
562
+ > a:is(:hover, :focus-visible)
563
+ > span {
564
+ opacity: 1;
565
+ transform: translateY(-50%) translateX(0);
566
+ }
567
+
568
+ .layout-compact-sidebar details[data-compact] > summary .caret {
569
+ transform: rotateZ(0);
570
+ }
571
+
572
+ [dir='rtl']
573
+ .layout-compact-sidebar
574
+ details[data-compact]
575
+ > summary
576
+ .caret {
577
+ transform: rotateZ(180deg);
578
+ }
579
+
451
580
  .layout-navigation-toggle {
452
581
  position: fixed;
453
582
  z-index: calc(var(--sl-z-index-menu) + 1);