Merge dev: family canon, milestone 463 steps 1-6 #206

Merged
bvandeusen merged 10 commits from dev into main 2026-10-06 14:11:29 -04:00
4 changed files with 77 additions and 13 deletions
Showing only changes of commit 1774ee3696 - Show all commits
@@ -414,6 +414,12 @@ watch(() => props.ruleId, load);
</div> </div>
</template> </template>
<!-- `.rule-chip` for the "suggested" marker on a lesson — the chip the rule
panes already use, loaded here because the slide-over can open with no
pane that loads it (a link straight to ?rule=N).
Block 0, as in every pane that loads it: a shared src must sit at
one index everywhere (tests/test_frontend_shared_styles.py). -->
<style src="@/assets/rules-shared.css" />
<style scoped> <style scoped>
.kind { border: 1px solid var(--fs-border-color); border-radius: var(--fs-radius-md); padding: var(--fs-space-3); margin: var(--fs-space-3) 0; } .kind { border: 1px solid var(--fs-border-color); border-radius: var(--fs-radius-md); padding: var(--fs-space-3); margin: var(--fs-space-3) 0; }
.kind legend { font-size: var(--fs-size-tiny); text-transform: uppercase; letter-spacing: var(--fs-tracking-tiny); color: var(--fs-text-tertiary); padding: 0 var(--fs-space-2); } .kind legend { font-size: var(--fs-size-tiny); text-transform: uppercase; letter-spacing: var(--fs-tracking-tiny); color: var(--fs-text-tertiary); padding: 0 var(--fs-space-2); }
@@ -518,8 +524,11 @@ legend { padding: 0 0.35rem; font-size: 0.8rem; color: var(--fs-text-tertiary);
.trash:hover, .close:hover { opacity: 1; } .trash:hover, .close:hover { opacity: 1; }
</style> </style>
<!-- `.rule-chip` for the "suggested" marker on a lesson — the chip the rule <!-- An @import, not a second <style src>: plugin-vue caches ONE descriptor per
panes already use, loaded here because the slide-over can open with no src file, and every other importer of moments-shared.css has it as style
pane that loads it (a link straight to ?rule=N). --> block 1. Here it would be block 2, so whichever SFC registered last decided
<style src="@/assets/rules-shared.css" /> the lookup and the production build failed with "reading 'scoped'" when
<style src="@/assets/moments-shared.css" /> the transform order changed (tests/test_frontend_shared_styles.py). -->
<style>
@import "@/assets/moments-shared.css";
</style>
+5 -4
View File
@@ -313,6 +313,11 @@ onMounted(load);
</div> </div>
</template> </template>
<!-- `.rule-chip` and its preference variant: the marker a rule's kind wears
everywhere else, reused rather than re-spelled.
Block 0, as in every pane that loads it: a shared src must sit at
one index everywhere (tests/test_frontend_shared_styles.py). -->
<style src="@/assets/rules-shared.css" />
<style scoped> <style scoped>
.lesson-detail { .lesson-detail {
max-width: 780px; max-width: 780px;
@@ -459,7 +464,3 @@ onMounted(load);
font-size: 0.88rem; font-size: 0.88rem;
} }
</style> </style>
<!-- `.rule-chip` and its preference variant: the marker a rule's kind wears
everywhere else, reused rather than re-spelled. -->
<style src="@/assets/rules-shared.css" />
+5 -4
View File
@@ -459,6 +459,11 @@ onMounted(() => {
</div> </div>
</template> </template>
<!-- `.rule-chip-preference` marks a preference among the rules offered, as it
does everywhere else a rule's kind is shown.
Block 0, as in every pane that loads it: a shared src must sit at
one index everywhere (tests/test_frontend_shared_styles.py). -->
<style src="@/assets/rules-shared.css" />
<style scoped> <style scoped>
.lesson-editor { .lesson-editor {
max-width: 820px; max-width: 820px;
@@ -606,7 +611,3 @@ onMounted(() => {
.le-small { padding: 0.2rem 0.7rem; font-size: var(--fs-size-label); } .le-small { padding: 0.2rem 0.7rem; font-size: var(--fs-size-label); }
.le-ghost:disabled { opacity: var(--fs-disabled-opacity); cursor: default; } .le-ghost:disabled { opacity: var(--fs-disabled-opacity); cursor: default; }
</style> </style>
<!-- `.rule-chip-preference` marks a preference among the rules offered, as it
does everywhere else a rule's kind is shown. -->
<style src="@/assets/rules-shared.css" />
+53
View File
@@ -0,0 +1,53 @@
"""A stylesheet shared by `<style src>` must sit at the same block index in
every component that loads it that way.
plugin-vue caches one SFC descriptor per `src` file and resolves a request for
`moments-shared.css?vue&type=style&index=N` against whichever component last
registered it. When two components carry the same src at different indexes,
the lookup can land on a component with fewer style blocks, and the production
build dies with "Cannot read properties of undefined (reading 'scoped')". It
depends on transform order, so an unrelated change, such as a new import in a
view, is enough to flip it. vue-tsc cannot see it; only `vite build` can, and
that runs at image-build time, after every test has passed.
"""
from __future__ import annotations
import pathlib
import re
from collections import defaultdict
ROOT = pathlib.Path(__file__).resolve().parents[1] / "frontend" / "src"
STYLE_TAG = re.compile(r"<style\b([^>]*)>")
SRC_ATTR = re.compile(r'\bsrc="([^"]+)"')
def src_indexes(sources: dict[str, str]) -> dict[str, dict[int, list[str]]]:
"""{src: {block index: [component, ...]}} over a set of SFC sources."""
seen: dict[str, dict[int, list[str]]] = defaultdict(lambda: defaultdict(list))
for name, text in sources.items():
for index, attrs in enumerate(STYLE_TAG.findall(text)):
match = SRC_ATTR.search(attrs)
if match:
seen[match.group(1)][index].append(name)
return seen
def disagreements(sources: dict[str, str]) -> dict[str, dict[int, list[str]]]:
return {src: dict(at) for src, at in src_indexes(sources).items() if len(at) > 1}
def test_every_shared_style_src_sits_at_one_block_index():
sources = {str(p.relative_to(ROOT)): p.read_text() for p in ROOT.rglob("*.vue")}
assert sources, f"no .vue files under {ROOT}"
assert disagreements(sources) == {}, (
"a <style src> sheet sits at different block indexes across components — "
"load it with @import in the odd one out"
)
def test_the_guard_can_fail():
sources = {
"A.vue": '<style scoped></style>\n<style src="@/x.css" />',
"B.vue": '<style scoped></style>\n<style src="@/y.css" />\n<style src="@/x.css" />',
}
assert disagreements(sources) == {"@/x.css": {1: ["A.vue"], 2: ["B.vue"]}}