feat(magazines): render magazine labels as Magpul dot-matrix glyphs - #90
Conversation
The range picker renders two month grids and react-day-picker draws outside-days, so a day near a month boundary carries the same accessible name in both. A page-wide lookup was a strict-mode violation on exactly those dates, which made the spec fail depending on when the suite ran. Scope the day button to the grid for its own month. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Requirements from ce-brainstorm (three review rounds), enriched to implementation-ready. Resolves the how for GitHub issue #20. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
One glyph per line, five 3-wide rows of painted/unpainted marks, parsed once at module load. Malformed rows throw rather than yielding a partial font, so a bad transcription fails in CI instead of rendering a wrong pattern someone paints onto a floorplate. The fixture ships with zero glyph rows until Magpul's diagram is transcribed. An empty table is a valid state, and it is what keeps the feature dark until the data lands. Covers R1, R2 (U1). Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Free-text brandModel normalizes to one dense uppercase token, then matches an ordered built-in list by required-substring containment. Entries are keyed on the distinctive family marker alone, never a brand token: MAGPUL does not contain the substring PMAG, so requiring it would reject the shorthand 'Magpul GL9' and silently drop a 2-cell magazine to the 4-cell fallback. Covers R3, R4, R5 (U2). Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
One pure function returning a hidden | matrix | unrepresentable union, so the presentation layer branches once instead of threading nullable fields. The glyph table is a parameter, which is what makes every rule testable today against a synthetic font while the shipped one is empty. Font coverage is checked against the whole stored label before any truncation, so a label carrying an unsupported character is unrepresentable even when its trailing digits would have fit. Covers R6-R10 (U3). Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
One circle per dot, role=img with a single accessible name, so the ~60 dots need no per-dot aria-hidden. Painted and unpainted dots differ in radius as well as fill: they measure only 2.67:1 against each other in the dark theme, so radius is what actually carries R15's 'visually distinct' clause and survives a future token retune. --dot-painted/--dot-unpainted alias --foreground/--muted-foreground. --border reads as the repo's 'faint but present' token but fails WCAG 1.4.11 against --card in both themes (1.26:1 dark, 1.40:1 light); the new contrast test asserts the 3:1 floor so a retune cannot silently break it -- Tailwind v4 no-ops an unknown utility without erroring. Geometry is fixed at 16px pitch / 10px painted / 6px unpainted, giving 216x74px at 4 cells, inside the card's ~248px content width at 320px. Covers R11, R12, R13, R15 (U5). Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
The detail page passed the viewer's magpulMode; R6 keys on the magazine owner's. getMagazine now resolves the owner's flag and returns it, with the query strictly after the authorization gate so it cannot become an oracle for a magazine the actor cannot see. A missing owner row throws rather than defaulting to false, mirroring the write path. The prop is renamed magpulMode -> ownerMagpulMode so the change of meaning is visible at every call site rather than silently repointed. That prop also feeds the edit form's label mask, but editing is owner-only, so viewer and owner are already the same account whenever the form renders -- this removes a latent divergence, it does not fix a live defect. The detail view now places the matrix below the field list. Splicing it inside the <dl> would break the definition-list content model and shift the last-row border, changing the view even with Magpul mode off. Covers R6, R4, R9, R10, R14 (U4, U6). Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
The glyph table ships empty, so the matrix is suppressed and the spec asserts that through the real render path: the label renders as text and no img-role graphic appears. Overflow is measured via scrollWidth at 320px rather than asserted as 'no layout break', which passes trivially on an empty page. Both tests delete their magazine through the UI before finishing. The personas are shared with magpul-mode and theme specs, and alphabetical file ordering runs this spec first, so leaving rows behind would break magpul-mode's cold-start assertion on every full-suite run. Assertions on a rendered matrix land with the glyph transcription; no test is skipped or fixme'd to hold their place. Covers R6, R13, R14 (U7). Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
The owner-magpulMode query was written out verbatim three times in this file. Extract resolveOwnerMagpulMode and call it from createMagazine, updateMagazine, and getMagazine. In getMagazine it now runs concurrently with attachCompatibility -- both are independent once the authorization and existence gates have passed, and both still run strictly after them. Pull the console-error tracker and the overflow measurement into e2e fixtures; theme.spec.ts adopts the tracker so each has two real consumers. responsive-overflow.spec.ts keeps its inline check, which also probes maxScrollX and would not be a drop-in. The contrast test now strips comments and matches braces by depth rather than taking the first closing brace, so a nested at-rule or a comment mentioning a theme selector cannot silently slice the wrong CSS. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Matching is substring containment over free-text brandModel, so the bare '762X51' entry matched any magazine whose model string carried that caliber -- 'PTR-91 7.62X51' resolved as a *confirmed* 4-cell PMAG floorplate, with no unrecognized-model caveat, for a magazine that has no dot matrix at all. That is the failure the module warns about: a wrong count on a matched entry is worse than a miss, because only the miss carries the caveat. Drop the entry; such magazines now fall back to 4 cells and say so. Tokens must be Magpul model designations, never a caliber, which is brand-agnostic by definition. The SVG was also sized from the floorplate's capacity while drawing only as many cell groups as the label has characters, so any label shorter than the floorplate painted dots on the left and left dead space on the right. Size from what is drawn; the accessible name still names capacity. The e2e cleanup now runs in a finally block. The personas are shared and the specs run serially in alphabetical order, so a failed assertion left a magazine behind and broke magpul-mode's cold-start step with an error pointing at the wrong spec. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughChangesMagazine detail pages now use the magazine owner’s Magpul mode. The PR adds dot-matrix resolution, accessible SVG rendering, theme tokens, empty-glyph handling, fallback messages, and Playwright coverage. The glyph table remains intentionally empty, so no matrix image renders until glyph data is added. ChangesMagazine dot-matrix label
Sequence Diagram(s)sequenceDiagram
participant MagazineDetailPage
participant getMagazine
participant MagazineDetailView
participant DotMatrixLabel
participant resolveDotMatrix
MagazineDetailPage->>getMagazine: request magazine and ownerMagpulMode
getMagazine->>MagazineDetailPage: return magazine, permission, ownerMagpulMode
MagazineDetailPage->>MagazineDetailView: pass owner mode and label data
MagazineDetailView->>DotMatrixLabel: render label
DotMatrixLabel->>resolveDotMatrix: resolve label and capacity
resolveDotMatrix->>DotMatrixLabel: return hidden, matrix, or unrepresentable result
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
✨ Finishing Touches✨ Simplify code
Warning Review ran into problems🔥 ProblemsThese MCP integrations need to be re-authenticated in the Integrations settings: Notion Comment |
There was a problem hiding this comment.
Pull request overview
Adds the full “Magpul dot-matrix label” resolution + rendering pipeline for magazine detail pages, while intentionally shipping dark (empty glyph table suppresses rendering) until the authoritative glyph data is transcribed. It also corrects the detail read path to use the magazine owner’s magpulMode and includes test/CI guards (domain-unit coverage, contrast checks, and E2E coverage), plus an unrelated E2E strict-mode fix for the date-range picker.
Changes:
- Introduces pure domain modules for glyph parsing, floorplate cell-count resolution, and label→matrix resolution (discriminated union), with comprehensive unit tests.
- Updates
getMagazineto returnownerMagpulModeand wires the detail page/component to use owner-scoped behavior. - Adds dot-matrix UI component + CSS tokens + contrast guard tests; adds ships-dark E2E coverage and refactors shared E2E helpers.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/domain/magazines/service.ts | Adds owner-scoped magpulMode resolver and returns ownerMagpulMode from getMagazine. |
| src/domain/magazines/glyphs.ts | Implements glyph-table parsing and exports MAGPUL_GLYPHS parsed at module load. |
| src/domain/magazines/floorplate.ts | Adds brandModel→cell-count resolver with an explicit model token list and fallback semantics. |
| src/domain/magazines/dot-matrix.ts | Adds pure resolver returning `hidden |
| src/domain/magazines/tests/service.test.ts | Adds integration coverage for owner-vs-viewer magpulMode behavior in getMagazine. |
| src/domain/magazines/tests/glyphs.test.ts | Adds parser tests for glyph fixture format and shipped “ships dark” assertions. |
| src/domain/magazines/tests/floorplate.test.ts | Adds unit tests for model normalization and cell-count resolution, including collision guards. |
| src/domain/magazines/tests/dot-matrix.test.ts | Adds unit tests for acceptance examples and boundary cases using synthetic glyph tables. |
| src/domain/magazines/tests/dot-matrix-contrast.test.ts | Adds CSS-token parsing + contrast assertions for dot tokens in both themes. |
| src/data/raw.ts | Embeds MAGPUL_GLYPHS_RAW (generated reference data module). |
| src/data/magpul-glyphs.txt | Adds the (currently empty) checked-in glyph fixture source file with format documentation. |
| app/globals.css | Adds dot token aliases and bridges them into Tailwind color variables. |
| app/(app)/magazines/dot-matrix-label.tsx | Adds client component rendering SVG dot matrix + caveats/messages. |
| app/(app)/magazines/magazine-detail-view.tsx | Wires the dot-matrix component into the magazine detail view and renames prop to ownerMagpulMode. |
| app/(app)/magazines/[id]/page.tsx | Passes ownerMagpulMode from getMagazine into the detail view. |
| e2e/magazine-dot-matrix.spec.ts | Adds ships-dark E2E coverage for dot-matrix wiring, overflow, and console errors. |
| e2e/theme.spec.ts | Refactors console error tracking to shared helper. |
| e2e/magazine-inventory-filter.spec.ts | Fixes date picker strict-mode flake by scoping day selection to the correct month grid. |
| e2e/fixtures/console-errors.ts | Adds shared console/pageerror tracking helper. |
| e2e/fixtures/overflow.ts | Adds shared overflow assertion helper. |
| docs/plans/2026-08-02-001-feat-magazine-dot-matrix-label-plan.md | Adds the implementation plan/spec for this feature and its requirements/units. |
| docs/residual-review-findings/20-render-magazine-label-as-a-magpul-paint-pen-dot-matrix-in-the-detail-view.md | Records review findings intentionally not applied in this PR. |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
app/(app)/magazines/dot-matrix-label.tsx (1)
74-156: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftAdd component-level tests for
DotMatrixLabel.This component has no direct test coverage in the reviewed files: SVG sizing math, the
aria-labeltext, and the two distinctCalloutfallback branches (unrepresentable,!cellCountVerified) are all untested outside the domain resolver. The end-to-end test can only observe whether the label renders, not whether the accessible name, geometry, or fallback copy are correct.Add unit or component tests that assert: the
aria-labeltext for a representable label, the SVGwidth/viewBoxfor a knowncells.length, and the correctCallouttext for both fallback branches.As per coding guidelines,
**/*.test.{ts,tsx,js,jsx}requires that "new behavior needs coverage." Based on learnings from the external tools context, the PR's own residual findings note that "SVG markup, accessibility copy, overflow messaging, and fallback branches have no component-level tests."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/`(app)/magazines/dot-matrix-label.tsx around lines 74 - 156, Add component-level tests for DotMatrixLabel covering a representable label’s aria-label, SVG width and viewBox derived from the known cells.length, the unrepresentable branch’s Callout message, and the !cellCountVerified branch’s caveat text. Use stable fixture inputs and mock or reuse the resolver data as needed so each assertion verifies the component output rather than only resolver behavior.Sources: Coding guidelines, MCP tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/plans/2026-08-02-001-feat-magazine-dot-matrix-label-plan.md`:
- Around line 390-394: Update the DotMatrixResult rendering requirement to
specify that the SVG viewBox dimensions are derived from the rendered cells
array length, not the floorplate’s full cell count. Keep the sizing behavior
that prevents unused cells from appearing for short labels, and revise only the
affected wording in the component-rendering step.
In `@src/data/raw.ts`:
- Around line 119-143: Unify the glyph fixture so MAGPUL_GLYPHS_RAW and
src/data/magpul-glyphs.txt cannot diverge: choose one canonical source and make
the production export and tests consume it consistently. Update the
MAGPUL_GLYPHS loading path and the existing .txt parsing test accordingly, then
add a parity test that detects any mismatch between the source and
exported/loaded glyph data.
In `@src/domain/magazines/dot-matrix.ts`:
- Around line 22-39: Extend the DotMatrixResult unrepresentable variant with a
reason distinguishing unsupported characters from capacity overflow, and update
resolveDotMatrix to return the appropriate reason in both failure paths. Update
dot-matrix-label.tsx’s buildDoesNotFitMessage presentation logic to render an
accurate message for each reason while preserving the existing capacity message
for doesNotFit.
In `@src/domain/magazines/glyphs.ts`:
- Around line 83-98: Validate each glyph key in the parser before duplicate and
row processing, using the shared Magpul label character-set symbol rather than
only checking character length. Reject unsupported keys such as "." or lowercase
characters with the parser’s malformed-input error path, and add tests covering
unsupported glyph keys while preserving valid glyph parsing.
---
Nitpick comments:
In `@app/`(app)/magazines/dot-matrix-label.tsx:
- Around line 74-156: Add component-level tests for DotMatrixLabel covering a
representable label’s aria-label, SVG width and viewBox derived from the known
cells.length, the unrepresentable branch’s Callout message, and the
!cellCountVerified branch’s caveat text. Use stable fixture inputs and mock or
reuse the resolver data as needed so each assertion verifies the component
output rather than only resolver behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: f0760a98-369f-481d-97ae-bf23ccb43a1e
📒 Files selected for processing (22)
app/(app)/magazines/[id]/page.tsxapp/(app)/magazines/dot-matrix-label.tsxapp/(app)/magazines/magazine-detail-view.tsxapp/globals.cssdocs/plans/2026-08-02-001-feat-magazine-dot-matrix-label-plan.mddocs/residual-review-findings/20-render-magazine-label-as-a-magpul-paint-pen-dot-matrix-in-the-detail-view.mde2e/fixtures/console-errors.tse2e/fixtures/overflow.tse2e/magazine-dot-matrix.spec.tse2e/magazine-inventory-filter.spec.tse2e/theme.spec.tssrc/data/magpul-glyphs.txtsrc/data/raw.tssrc/domain/magazines/__tests__/dot-matrix-contrast.test.tssrc/domain/magazines/__tests__/dot-matrix.test.tssrc/domain/magazines/__tests__/floorplate.test.tssrc/domain/magazines/__tests__/glyphs.test.tssrc/domain/magazines/__tests__/service.test.tssrc/domain/magazines/dot-matrix.tssrc/domain/magazines/floorplate.tssrc/domain/magazines/glyphs.tssrc/domain/magazines/service.ts
Review caught that the 'shipped fixture parses' test read MAGPUL_GLYPHS_RAW, not the .txt it named -- so editing the file and forgetting to regenerate raw.ts would pass CI while production kept the stale font. Transcribing that file is the follow-up this whole feature waits on, which makes silent drift the one failure it cannot afford. The test now reads the file from disk and asserts it matches the embedded copy. src/data/calibers.txt and manufacturers.txt have the same untested drift; left alone as out of scope. Also from review: the contrast test's hex extraction matched 3-8 digits while the parser only handles 6, so an unsupported token would have thrown opaquely instead of naming itself; the e2e console tracker attached after the add-magazine flow had already run, so it could not have seen errors from it; and the plan still documented the caliber-only token entry the code now bans, which would have walked the next reader back into the cross-brand false match. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/domain/magazines/__tests__/dot-matrix-contrast.test.ts`:
- Around line 77-78: Update extractHexToken so its regular expression requires a
non-hex boundary after the six-digit value, preventing an 8-digit color such as
`#11223344` from matching `#112233`. Add a regression test in the existing
dot-matrix contrast test suite confirming 8-digit hex values are rejected.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: d906e5fc-361d-4b4a-9c6c-73733ceac956
📒 Files selected for processing (4)
docs/plans/2026-08-02-001-feat-magazine-dot-matrix-label-plan.mde2e/magazine-dot-matrix.spec.tssrc/domain/magazines/__tests__/dot-matrix-contrast.test.tssrc/domain/magazines/__tests__/glyphs.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- src/domain/magazines/tests/glyphs.test.ts
- e2e/magazine-dot-matrix.spec.ts
- docs/plans/2026-08-02-001-feat-magazine-dot-matrix-label-plan.md
The parser only checked that a glyph key was one character, so a transcription slip could introduce a '.' or a lowercase glyph. R9 treats a label carrying a character absent from the font as unrepresentable, so a stray accepted glyph would make a grandfathered nonconforming label render instead -- defeating R9 on exactly the labels it protects. Keys now validate against MAGPUL_LABEL_ALLOWED_RE rather than a second copy of the character set. The hyphen stays valid, with a test saying so. Also corrects U5 in the plan, which still described the SVG as sized from the floorplate's capacity after the code moved to sizing from the cells actually drawn. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Tightening the extraction to {6} in the previous commit left a subtler
hole: the pattern matched the leading six digits of an #RRGGBBAA value,
so the contrast helper would validate a colour the stylesheet never
declares and still pass. Added a hex-digit boundary plus regression
tests for the 8-digit and 3-digit cases.
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Read off Magpul's published dot-matrix PDF by rendering it at 300 DPI, recovering the 30x20 dot lattice, and sampling every position -- then verified against the sheet by the owner. All 36 glyphs, no eyeballing. Two things the sheet settled. Its digit row runs 1-9 then 0, so reading it left-to-right as 0-9 would have shifted every digit by one. And it carries no hyphen and no punctuation at all, which R1 assumed it did: issue #21 still permits '-' in a label, so a stored label containing one is now unrepresentable under R9. Left as an open product question rather than decided here. R2's 3x5 cell is confirmed rather than assumed, so the geometry and row format stand as built. The feature is no longer dark. The unit test pinning an empty table and the e2e asserting the matrix is absent both encoded the pre-font world; they now assert AE1, AE2/AE8 and AE6 against a real render. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
…ph font (#92) Closes the two open items left after #90 — and supersedes #91, which carried the same findings doc on a branch that went stale when #90 squash-merged. ## The verification gap (the higher-risk half) `glyphs.test.ts` spot-checked the exact dot pattern for `0` and `1`. The other **34 glyphs were checked for presence, not shape** — a row shift or a bad PDF crop in any letter passed every test in the repo. Nothing anywhere looked at rendered SVG dot positions either; AE1's e2e read the accessible name (`U S 0 4`), not the pattern. - All 36 glyphs are now asserted against `EXPECTED_GLYPHS`, recorded in **Magpul's own sheet ordering** (digits `1`-`9` then `0`) rather than the fixture's, so a bad regeneration can't pass by matching the artifact it was derived from. A companion test asserts the expected table still covers all 36, so deleting a row can't quietly shrink the guard. - Confirmed to bite: corrupting a single dot in glyph `G` turns two tests red. - **`resolveDotMatrix` had never run against the shipped font.** Every resolver test used a synthetic fixture that carries a hyphen Magpul never drew. A second describe block re-runs the acceptance examples against the real `MAGPUL_GLYPHS` — which immediately showed AE4 (`AR-X`) taking a *different rule* under each table. - AE1's e2e now reads `r` off all 60 circles and compares the drawn pattern to a string restated from the sheet. That's the only assertion spanning **fixture file → parser → resolver → geometry**. - R4/R9 copy was exported and rendered but read by nothing. Now asserted at the builder level (`dot-matrix-messages.test.ts`) and as rendered text in the e2e. ## The hyphen question — resolved by dropping The floorplate has no hyphen cell, so a hyphen can never be painted. But #21 permits one in a stored label, so `A-1` was ordinary user input that rendered nothing and reported *"This label does not fit this magazine's floorplate"* — which is false; it never reached the length check. **R8 already drops characters**: `US04` on a 2-cell GL9 paints `04` and discards `US`. Refusing to paint `A-1` over one hyphen while happily painting `04` for `US04` was the inconsistent position. So an unpaintable character is now dropped and the rest is drawn, with a caveat naming what was left out. The collision this admits (`A-1` and `A1` paint alike) already existed under R8, where `US04` and `XY04` both paint `04`. **Rejected:** narrowing #21's allowed character set, which would invalidate stored labels and need a migration. `unrepresentable` still gains a `reason` discriminant — it stays reachable for a label with *nothing* paintable in it (`--`), and that case now gets its own message instead of the false "does not fit". That also closes the parked CodeRabbit finding from #90. ## Test plan - [x] `just ci-check` green — **45 e2e passed** (up from 42) - [x] Glyph guard proven to fail on a one-dot corruption, then reverted - [x] New e2e: hyphen drop + caveat, R4 unrecognized-model caveat wording, R9 does-not-fit wording - [x] Plan amended: R1, R2, R9, new R9a, AE4/AE5/AE5b, KTD8, Verification Contract - [x] Stale "ships dark" comments corrected in four source files ## Still open (recorded in `docs/residual-review-findings/`) - **Per-model cell counts** — `MODEL_CELL_COUNTS` carries only the GL9 and LR/SR seeds; everything else renders under R4's unverified 4-cell fallback. Now the largest unverified area in the feature. - `calibers.txt` / `manufacturers.txt` have the same `.txt`↔`raw.ts` drift the glyph fixture had before #90. --------- Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Renders a magazine's
labelin the detail view as Magpul PMAG Gen M3 paint-pen dot-matrix glyphs, so an owner can copy the pattern onto a floorplate instead of translating characters out of a PDF by hand. Closes #20.This ships dark — read this first
src/data/magpul-glyphs.txtdeliberately contains zero glyph rows. Magpul's diagram has not been transcribed yet, and an empty glyph table suppresses the matrix entirely, so merging this changes nothing an owner sees. That is the point: shipping a guessed dot pattern would tell someone to put permanent paint on hardware in the wrong places.Turning the feature on is a data-only follow-up — transcribe the fixture, extend the model list — provided the diagram confirms a 3-column cell. Issue #20 describes "approximately 4 columns"; if that turns out right, the row format, the
GlyphCelltype, and the geometry all need rework. Confirm the column count before transcribing the whole font.How it works
Seven units, each its own commit:
glyphs.ts) — one glyph per line, five 3-wide rows of#/., parsed once at module load. Malformed rows throw rather than yielding a partial font, so a bad transcription fails CI instead of rendering a wrong pattern.floorplate.ts) — free-textbrandModelnormalizes to one dense uppercase token, matched against a built-in list by substring containment. Unrecognized models fall back to 4 cells and say so.dot-matrix.ts) — one pure function returninghidden | matrix | unrepresentable. The glyph table is a parameter, which is what makes every rule testable today against a synthetic font while the shipped one is empty.service.ts) — the detail page was passing the viewer's Magpul mode; the requirement keys on the magazine owner's. The new lookup runs strictly after the authorization gate so it cannot become an oracle for a magazine you cannot see.dot-matrix-label.tsx) — inline SVG, one<circle>per dot,role="img"with a single accessible name.Product decisions came from a brainstorm that went through three multi-persona review rounds; the plan (
docs/plans/2026-08-02-001-feat-magazine-dot-matrix-label-plan.md) is the source of truth for all fifteen requirements. Two decisions in it are marked user-directed: a floorplate's cell count means one face, and the model list is built in, not owner-editable.Two bugs review caught
762X51caliber token matched any magazine whose model string carried that caliber —PTR-91 7.62X51resolved as a confirmed 4-cell PMAG floorplate with no caveat, for a magazine with no dot matrix at all. Worse than a miss, since only a miss carries the warning. Caliber tokens are now banned from the list.Also fixed a pre-existing e2e flake: the date-range picker renders two month grids with outside-days, so a page-wide day lookup was a strict-mode violation on dates near a month boundary — it failed depending on when the suite ran.
Accessibility
Painted and unpainted dots are distinguished by radius as well as colour. They measure only 2.67:1 against each other in the dark theme, so a colour threshold could not carry WCAG 1.4.11's "visually distinct" clause. A test asserts both dot tokens clear 3:1 against
--cardin both themes and fails if either is declared in only one — Tailwind v4 silently no-ops an unknown utility, so nothing else would catch it.Test plan
just ci-checkgreen (lockfile, biome, format, typecheck, pre-commit, 833 unit/integration, 41 e2e)dot-matrix.test.tsfixme'd testsFindings that were reviewed but not applied are recorded in
docs/residual-review-findings/.