nimiq-branding-cli 1.1.1 → 1.2.1

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 (76) hide show
  1. package/.audit/report.json +71 -0
  2. package/.audit/report.md +33 -0
  3. package/AUDIT.md +60 -0
  4. package/LINT.md +158 -0
  5. package/README.md +47 -4
  6. package/align/canonical.json +44 -0
  7. package/audit/learnings.json +51 -0
  8. package/bin/nq.js +88 -6
  9. package/hooks/ci-additions.md +53 -0
  10. package/hooks/pre-commit +10 -0
  11. package/hooks/session-start.sh +3 -0
  12. package/hooks/stack-align.yml +75 -0
  13. package/package.json +4 -4
  14. package/registry/SKILL-BLOCK.md +20 -0
  15. package/registry/components/account-header/meta.json +11 -1
  16. package/registry/components/account-list/meta.json +9 -1
  17. package/registry/components/account-ring/meta.json +9 -1
  18. package/registry/components/address-display/html/address-display.css +17 -13
  19. package/registry/components/address-display/html/demo.html +8 -10
  20. package/registry/components/address-display/meta.json +37 -7
  21. package/registry/components/address-display/vue/AddressDisplay.vue +11 -9
  22. package/registry/components/address-input/meta.json +53 -10
  23. package/registry/components/amount/meta.json +60 -11
  24. package/registry/components/amount-input/meta.json +55 -10
  25. package/registry/components/amount-with-fee/meta.json +58 -11
  26. package/registry/components/app-showcase-card/meta.json +17 -4
  27. package/registry/components/backup-banner/meta.json +29 -5
  28. package/registry/components/balance-distribution/meta.json +25 -5
  29. package/registry/components/buttons/meta.json +38 -7
  30. package/registry/components/card/meta.json +20 -4
  31. package/registry/components/close-button/meta.json +19 -4
  32. package/registry/components/consensus-icon/meta.json +26 -5
  33. package/registry/components/copyable/meta.json +25 -5
  34. package/registry/components/fiat-amount/meta.json +49 -9
  35. package/registry/components/flag-hex/html/demo.html +55 -0
  36. package/registry/components/flag-hex/html/flag-hex.css +21 -0
  37. package/registry/components/flag-hex/html/flag-hex.html +38 -0
  38. package/registry/components/flag-hex/meta.json +43 -0
  39. package/registry/components/flag-hex/vue/FlagHex.vue +98 -0
  40. package/registry/components/hero-section/meta.json +9 -4
  41. package/registry/components/honeycomb-band/meta.json +47 -9
  42. package/registry/components/identicon/meta.json +8 -1
  43. package/registry/components/label-input/meta.json +49 -9
  44. package/registry/components/loading-spinner/meta.json +19 -4
  45. package/registry/components/page-body/meta.json +23 -5
  46. package/registry/components/page-footer/meta.json +22 -5
  47. package/registry/components/page-header/meta.json +43 -8
  48. package/registry/components/payment-info-line/meta.json +10 -1
  49. package/registry/components/price-chart/meta.json +67 -11
  50. package/registry/components/qr-code/meta.json +22 -5
  51. package/registry/components/search-bar/meta.json +25 -5
  52. package/registry/components/select-bar/meta.json +37 -7
  53. package/registry/components/slider-toggle/meta.json +45 -8
  54. package/registry/components/small-page/meta.json +19 -4
  55. package/registry/components/status-alert/meta.json +37 -7
  56. package/registry/components/status-screen/meta.json +70 -13
  57. package/registry/components/swap-balance-bar/meta.json +8 -1
  58. package/registry/components/timer/meta.json +54 -10
  59. package/registry/components/toast-notification/meta.json +49 -9
  60. package/registry/components/tooltip/meta.json +69 -13
  61. package/registry/components/transaction-list/meta.json +9 -1
  62. package/registry/index.json +10 -0
  63. package/schemas/nimiq-stack.v1.json +103 -0
  64. package/scripts/_browser.mjs +27 -0
  65. package/scripts/align.mjs +435 -0
  66. package/scripts/apply-safe-bump.mjs +47 -0
  67. package/scripts/audit.mjs +247 -0
  68. package/scripts/bootstrap-provenance.mjs +126 -0
  69. package/scripts/hooks.mjs +152 -0
  70. package/scripts/lint.mjs +333 -0
  71. package/scripts/scaffold.mjs +391 -0
  72. package/scripts/sync-skill.mjs +84 -0
  73. package/scripts/verify.mjs +2 -2
  74. package/test/align.test.mjs +166 -0
  75. package/test/scaffold.test.mjs +103 -0
  76. package/upstream-pins.json +35 -0
@@ -0,0 +1,71 @@
1
+ {
2
+ "generatedAtNote": "timestamp stamped by caller",
3
+ "verdict": "clean",
4
+ "repos": {
5
+ "hub": {
6
+ "branch": "master",
7
+ "pinned": "cf2ea419",
8
+ "live": "cf2ea419",
9
+ "pinnedFull": "cf2ea41941e433fa8e28ff4fe3d60870005c2115",
10
+ "liveFull": "cf2ea41941e433fa8e28ff4fe3d60870005c2115",
11
+ "via": "local",
12
+ "drifted": false
13
+ },
14
+ "wallet": {
15
+ "branch": "master",
16
+ "pinned": "656ed569",
17
+ "live": "656ed569",
18
+ "pinnedFull": "656ed569dac0e20ff61bba95a4fef14fce3541d6",
19
+ "liveFull": "656ed569dac0e20ff61bba95a4fef14fce3541d6",
20
+ "via": "local",
21
+ "drifted": false
22
+ },
23
+ "nimiq-ui": {
24
+ "branch": "main",
25
+ "pinned": "68e96cde",
26
+ "live": "68e96cde",
27
+ "pinnedFull": "68e96cdeb1104bd5cd1556f5cd2619b8509c4529",
28
+ "liveFull": "68e96cdeb1104bd5cd1556f5cd2619b8509c4529",
29
+ "via": "local",
30
+ "drifted": false
31
+ },
32
+ "nimiq-style": {
33
+ "branch": "master",
34
+ "pinned": "21c10a14",
35
+ "live": "21c10a14",
36
+ "pinnedFull": "21c10a14b324a133ddb78f906015765bf6cb13ec",
37
+ "liveFull": "21c10a14b324a133ddb78f906015765bf6cb13ec",
38
+ "via": "local",
39
+ "drifted": false
40
+ },
41
+ "vue-components": {
42
+ "branch": "master",
43
+ "pinned": "3c5b9724",
44
+ "live": "3c5b9724",
45
+ "pinnedFull": "3c5b97247e68a8bdcba5e904ff8ac29482995759",
46
+ "liveFull": "3c5b97247e68a8bdcba5e904ff8ac29482995759",
47
+ "via": "local",
48
+ "drifted": false
49
+ }
50
+ },
51
+ "driftedComponents": [],
52
+ "tokenDrift": [],
53
+ "unknownPaths": [],
54
+ "verify": {
55
+ "ran": false
56
+ },
57
+ "newComponents": [
58
+ "vue-components:src/components/Account.vue",
59
+ "vue-components:src/components/AccountDetails.vue",
60
+ "vue-components:src/components/AccountSelector.vue",
61
+ "vue-components:src/components/BottomOverlay.vue",
62
+ "vue-components:src/components/Carousel.vue",
63
+ "vue-components:src/components/CircleSpinner.vue",
64
+ "vue-components:src/components/CopyableField.vue",
65
+ "vue-components:src/components/LanguageSelector.vue",
66
+ "vue-components:src/components/LongPressButton.vue",
67
+ "vue-components:src/components/QrScanner.vue",
68
+ "vue-components:src/components/Wallet.vue"
69
+ ],
70
+ "summary": "verdict: clean · 0/5 upstream(s) drifted · 0 component(s) upstream-touched · 0 token-drift · 0 unknown path(s) · verify skipped"
71
+ }
@@ -0,0 +1,33 @@
1
+ # nq audit report
2
+
3
+ **verdict: clean · 0/5 upstream(s) drifted · 0 component(s) upstream-touched · 0 token-drift · 0 unknown path(s) · verify skipped**
4
+
5
+ ## Upstream pins vs live
6
+
7
+ | repo | branch | pinned | live | drifted | changed files |
8
+ |---|---|---|---|---|---|
9
+ | hub | master | `cf2ea419` | `cf2ea419` | no | — |
10
+ | wallet | master | `656ed569` | `656ed569` | no | — |
11
+ | nimiq-ui | main | `68e96cde` | `68e96cde` | no | — |
12
+ | nimiq-style | master | `21c10a14` | `21c10a14` | no | — |
13
+ | vue-components | master | `3c5b9724` | `3c5b9724` | no | — |
14
+
15
+ ## New-component radar
16
+
17
+ Upstream components with no registry port yet (candidates — cross-ref FEATURES.md):
18
+
19
+ - `vue-components:src/components/Account.vue`
20
+ - `vue-components:src/components/AccountDetails.vue`
21
+ - `vue-components:src/components/AccountSelector.vue`
22
+ - `vue-components:src/components/BottomOverlay.vue`
23
+ - `vue-components:src/components/Carousel.vue`
24
+ - `vue-components:src/components/CircleSpinner.vue`
25
+ - `vue-components:src/components/CopyableField.vue`
26
+ - `vue-components:src/components/LanguageSelector.vue`
27
+ - `vue-components:src/components/LongPressButton.vue`
28
+ - `vue-components:src/components/QrScanner.vue`
29
+ - `vue-components:src/components/Wallet.vue`
30
+
31
+ ---
32
+
33
+ Verdicts: **clean** (nothing moved) · **safe** (only learnings-"ignore" churn → auto-PR pin bump) · **risky** (component/token/unknown/verify-fail → human review).
package/AUDIT.md ADDED
@@ -0,0 +1,60 @@
1
+ # Staying current with Nimiq — the self-learning branding audit
2
+
3
+ `nq verify` proves our **port matches the vendored upstream truth render**. But that render is a
4
+ frozen snapshot — if Nimiq redesigns a component or changes a token upstream, `verify` stays green
5
+ against a stale reference. `nq audit` closes that gap: it watches the **live** Nimiq source and tells
6
+ you when the real design has moved away from what the registry ships.
7
+
8
+ ## The two axes of branding accuracy
9
+
10
+ | Axis | Question | Tool |
11
+ |---|---|---|
12
+ | Port fidelity | Does our component still render like the upstream truth? | `nq verify` |
13
+ | Upstream currency | Has the live Nimiq source moved away from our pin? | `nq audit` |
14
+
15
+ ## What `nq audit` does
16
+
17
+ 1. **Upstream drift** — compares each `pinned` commit in [`upstream-pins.json`](upstream-pins.json)
18
+ to the live branch tip (`git ls-remote` locally or in CI). For drifted repos it gets the changed
19
+ file list (local clone `git diff`, or the GitHub compare API in CI).
20
+ 2. **Provenance intersection** — every component's `meta.json` has a `source` block listing the exact
21
+ upstream files it was ported from. Changed files are intersected with these → drift is attributed to
22
+ the precise components it touches. **A changed file that any component lists is always escalated to
23
+ `risky` before the learnings rules run**, so broad benign-churn rules can never hide a real change.
24
+ 3. **Token / framework drift** — flags changes to the design-token sources (`nimiq-style`, `@nimiq/css`).
25
+ 4. **Port fidelity** — runs `nq verify all` and folds the result in.
26
+ 5. **New-component radar** — lists upstream `@nimiq/vue-components` with no registry port yet.
27
+
28
+ Output: `.audit/report.json` (machine) + `.audit/report.md` (human).
29
+
30
+ ## Verdicts
31
+
32
+ - **clean** — nothing moved, ports green. No action.
33
+ - **safe** — upstream moved, but every changed file is learnings-classified benign (deps, tests, i18n,
34
+ app logic, flow views), no component/token touched, and verify passed → the weekly workflow **auto-PRs
35
+ a pin bump**. Safe because the gate proved nothing visual changed.
36
+ - **risky** — a component's real source changed (needs hand re-port), a token changed, an **unknown** path
37
+ needs triage, or verify failed → the workflow opens/updates a **rolling GitHub issue** for a human.
38
+
39
+ ## The self-learning loop (`audit/learnings.json`)
40
+
41
+ Each changed path is classified `ignore` / `token` / `branding` by glob rules. Unmatched paths are
42
+ `unknown` and surfaced for triage. **Resolving a triage = appending a rule** (with `seenCount++` and a
43
+ `learnedFrom` note). Next run, that churn auto-classifies — fewer false alarms, faster known fixes. The
44
+ store gets smarter every week. Seed rules came from the 2026-06-16 reconciliation of the hub/wallet
45
+ Bitcoin/Ledger/swap drift, which touched none of our ports.
46
+
47
+ ## Weekly automation
48
+
49
+ [`.github/workflows/audit.yml`](.github/workflows/audit.yml) runs every Monday (and on demand via
50
+ `workflow_dispatch`): re-snaps references for a platform-consistent verify, runs `nq audit`, then
51
+ auto-PRs safe pin bumps or files the rolling issue. The pin record in `upstream-pins.json` is the
52
+ single committed source of truth for "what we are current with".
53
+
54
+ ## Triage workflow (when you get a `risky` issue)
55
+
56
+ 1. Read `.audit/report.md`. For each **upstream-touched component**, diff the upstream file and re-port
57
+ the truth render → `node scripts/snap.mjs <name>` → `node bin/nq.js verify <name>`.
58
+ 2. For each **unknown path**, decide if it's benign or branding-relevant and add a rule to
59
+ `audit/learnings.json` (that's the learning).
60
+ 3. Bump the pin in `upstream-pins.json`, run `nq sync-skill`, commit.
package/LINT.md ADDED
@@ -0,0 +1,158 @@
1
+ # nq lint — brand + breathability enforcement
2
+
3
+ `nq audit` watches the *upstream* Nimiq design for drift. `nq lint` is the other direction:
4
+ it renders **your** page (desktop + a 390px mobile pass) and checks it against the Nimiq rules,
5
+ so the brand discipline that used to live only as prose (the 21 rules + AI-slop blacklist in the
6
+ `nimiq-ui` skill) becomes something a machine can actually *deny*, not just something a human is
7
+ asked to remember.
8
+
9
+ ```bash
10
+ nq lint <file.html | url> # render + check; exit 1 if any ERROR
11
+ nq lint page.html --fix # also auto-fix the safe text violations in place
12
+ nq lint https://site/ --json # machine-readable
13
+ ```
14
+
15
+ It renders with the same Playwright harness as `nq verify` (no new runtime dep). For URLs it
16
+ dismisses a language-picker splash (e.g. nimiq.tech) and scrolls to trigger lazy sections;
17
+ decorative SVG fields (honeycomb / identicon fences) are excluded from every measurement.
18
+
19
+ > **Lint source files, not live URLs, when you can.** A built/static `.html` file renders
20
+ > deterministically. A live SPA adds splash gates, lazy hydration and thousands of decorative
21
+ > nodes — handled, but fragile. URL mode is for spot-checks; the gate should point at source.
22
+
23
+ ---
24
+
25
+ ## Two layers
26
+
27
+ ### ERRORS — off-brand slop (hard-fail, exit 1)
28
+ Unambiguous, deterministic, and verified **not** to fire on nimiq.com itself (see calibration).
29
+ These are the "make us not do AI things" set.
30
+
31
+ | Check | Rule | Auto-fix |
32
+ |---|---|---|
33
+ | em/en dashes in copy | skill rule 18 | `--fix` → comma |
34
+ | periods on display titles / CTAs | skill rule 16 | `--fix` → strip |
35
+ | glassmorphism (translucent surface + backdrop blur) | skill rule 10 | manual |
36
+ | borders on inputs | skill rule 1 | manual (inset box-shadow) |
37
+ | off-palette colors | skill rule 13 | manual |
38
+ | low-contrast blue/navy text on dark | skill rule 20 | manual → `#0CA6FE` / white |
39
+
40
+ **Blue/navy on dark (rule 20)** is the headline a11y+brand check. A blue-family foreground (hue
41
+ 195–245°) sitting on a dark surface (luminance < 0.12) below the AA floor — **4.5:1 normal, 3.0:1
42
+ large** — fails. Raw `#0582CA` on navy = 3.63:1 and `#265DD7` = 2.61:1 both error; the on-dark
43
+ variant `#0CA6FE` (5.68:1) and white pass cleanly. The background is read from the element's *own*
44
+ surface first (so blue text on a light card nested in a dark section is not a false positive), and
45
+ gradient-filled CTAs with white text are never flagged.
46
+
47
+ ### WARNINGS — breathability / density (advisory, never block)
48
+ These **cannot** hard-fail: nimiq.com's own marketing trips several of them (it runs 12 font
49
+ sizes and 31–38% dense sections). They're calibrated to the measured Nimiq envelope and exist
50
+ to flag *"this is getting busy, is it justified?"* — the deterministic half of PRINCIPLES law 4
51
+ ("white space does the structural work").
52
+
53
+ **Breathability / density**
54
+
55
+ | Check | Threshold | Why |
56
+ |---|---|---|
57
+ | body text wider than ~88ch | `> 88ch` | prose measure caps at 78ch; nimiq.com p90 ≈ 84ch |
58
+ | dense sections (text-ink ratio) | `> 18%` of a full-width band | nimiq.com pages run 5–12% page ink |
59
+ | off-scale spacing | values not on the curated scale | warn-and-snap, not fail |
60
+ | type-scale sprawl | `> 12` distinct text sizes | calm app ≈ 4; busy marketing ≈ 12 |
61
+ | colored / long / pill uppercase eyebrow | colored, >24 chars, or a pill | grey section labels are fine (rules 8, 17) |
62
+
63
+ **Depth / motion / form** (from the design recon)
64
+
65
+ | Check | Threshold |
66
+ |---|---|
67
+ | non-pill action buttons | a text button radius below a full pill (and not a nav trigger) |
68
+ | flat-fill colored button | a brand-colored button with no radial gradient |
69
+ | wrong gradient anchor | a linear-gradient, or a radial not at bottom-right / `100% 100%` |
70
+ | non-Nimiq easing | the Material `cubic-bezier(0.4,0,0.2,1)` on a button (use `cubic-bezier(0.25,0,0,1)`) |
71
+ | off-scale border-radius | not on `3 · 4 · 6 · 8 · 10 · 12` or a full pill / 50% |
72
+ | harsh near-black shadow | `rgba(0,0,0, > 0.22)` (Nimiq shadows are soft, low-alpha) |
73
+ | underlined links | a body `<a>` with `text-decoration: underline` (links are bold, no underline) |
74
+ | NIM address not in Fira Mono | a 4-char-grouped address rendered in a proportional font |
75
+ | gold-tinted UI icon | a non-logo icon tinted gold (gold = brand mark only) |
76
+
77
+ **Mobile (a second pass at 390px)**
78
+
79
+ | Check | Threshold |
80
+ |---|---|
81
+ | horizontal overflow | `scrollWidth > viewport` by > 4px |
82
+ | tap targets too small | a button / input / link-button under 30px (inline text links are exempt) |
83
+ | text smaller than 12px | any text element below 12px at mobile width |
84
+
85
+ The curated spacing scale (from `assets/css/modern/spacing.css`, desktop max of each step):
86
+ `8 · 12 · 16 · 24 · 32 · 40 · 48 · 72 · 80 · 96 · 144 · 200`. Sub-8px is treated as optical
87
+ nudging, not layout, and ignored. The radius scale: `3 · 4 · 6 · 8 · 10 · 12` plus full pills
88
+ (500/999/9999) and circles (50%).
89
+
90
+ ---
91
+
92
+ ## Exceptions are part of the spec
93
+
94
+ Calibration against real pages proved that a naive version of each rule flags Nimiq's own
95
+ sites. The carve-outs are load-bearing, not afterthoughts:
96
+
97
+ - **Social brand icons** — Discord/Telegram/X/YouTube/etc. colors are exempt from the palette
98
+ rule (you can't recolor a third-party logo). Reported separately as `exempt social-icon colors`.
99
+ - **Blue-tinted neutrals** — Nimiq grays carry a deliberate violet spin, so "neutral" is tested
100
+ by *saturation relative to lightness*, not a flat channel spread.
101
+ - **Secondary palette** — purple `#5F4B8B`, pink `#FA7268`, light-green `#88B04B`, brown
102
+ `#795548` (+ gradient starts) are brand colors, not violations.
103
+ - **Abbreviations** — `p.a.`, `e.g.`, `U.S.` don't count as title periods (internal-dot guard).
104
+ - **Translucent-only glass** — a near-opaque panel (≥85% bg) with a faint backdrop blur is a
105
+ solid surface, not glassmorphism. Only `10–85%` translucency + backdrop blur is flagged.
106
+ - **Grey section eyebrows** — short grey uppercase labels ("THE APPS", "TRUSTED BY") are the
107
+ brand's own pattern; only *colored*, *>24-char*, or *pill* uppercase is flagged.
108
+ - **Addresses & codes** — `NQ…` addresses and short alphanumeric codes (`L51C`) are uppercase by
109
+ nature and excluded from the eyebrow check.
110
+ - **Own-surface contrast** — blue/navy text is judged against the element's *own* background
111
+ first, so blue text on a light card inside a dark section isn't a false error; gradient-filled
112
+ CTAs (white text) are never flagged.
113
+ - **Inline links & nav triggers** — inline text links are exempt from the mobile tap-target check;
114
+ `nav`/`header` text buttons are exempt from the non-pill check.
115
+
116
+ ---
117
+
118
+ ## Calibration (measured, not invented)
119
+
120
+ Thresholds were set by rendering real Nimiq pages at 1440px and measuring the envelope — the
121
+ same "measure reality" method that proved the no-em-dash rule. Verified outcome:
122
+
123
+ | Page | ERRORS | Note |
124
+ |---|---|---|
125
+ | nimiq.com home (reference) | **0** | the gate must never flag the brand's own site, even with the rule-20 contrast + depth/motion checks |
126
+ | nimiq.com/about | 0 | — |
127
+ | nimiq.tech | real findings | catches genuine em-dashes + a "Phase 2 · preview" colored eyebrow + 3 dense bands |
128
+ | positive control | **2** | a deliberate `#0582CA`/`#265DD7`-on-navy page errors at 3.63/2.61:1; `#0CA6FE` + white pass |
129
+
130
+ Re-run the calibration any time the rules change:
131
+ `node bin/nq.js lint https://www.nimiq.com/` **must stay at 0 errors.** If a new rule flags the
132
+ reference, the rule is wrong — fix the rule, not nimiq.com. (That principle just caught four
133
+ over-strict rules during the 2026-06-20 hardening, including a blue-on-navy false error that the
134
+ own-surface-bg fix resolved.)
135
+
136
+ ---
137
+
138
+ ## Exit codes & `--fix`
139
+
140
+ - Exit `1` if any ERROR; `0` otherwise (warnings never affect exit). Wire the exit code into a
141
+ pre-commit hook or CI step to make it a real gate.
142
+ - `--fix` (local files only) applies the safe **text** transforms — em/en dashes → comma,
143
+ strip trailing title periods — and reports what it changed. It never silently green-washes:
144
+ geometry fixes (measure width, spacing snap, glass → solid) are reported for a human/agent,
145
+ because rewriting layout unsupervised can break the page.
146
+
147
+ ---
148
+
149
+ ## Roadmap
150
+
151
+ - **Render-mapped geometry autofix** — map a flagged rendered element back to its source line so
152
+ `--fix` can also constrain over-wide text and snap off-scale spacing, each behind a
153
+ re-render-and-verify check so a fix that breaks layout is rejected instead of shipped.
154
+ - **Layer 3 — optional aesthetic judgment (BYO-key).** A cheap vision pass for the irreducible
155
+ residue the metrics can't see ("is this busyness *justified*? does it feel Nimiq or generic
156
+ SaaS?"). Off by default; the deterministic layers above stay free, offline, and keyless.
157
+ - **`learnings.json` for lint** — same self-learning store as `nq audit`, so confirmed false
158
+ positives are remembered and suppressed across runs.
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # nimiq-branding-cli
2
2
 
3
3
  Scaffold **pixel-accurate Nimiq-branded UI components** into any project — Vue 3 SFCs or plain
4
- HTML/CSS — from a registry of 39 components where every one is pixel-diffed against the real
5
- Nimiq apps before it ships, plus the team's real asset library (logos, icons, flags, imagery).
4
+ HTML/CSS — from a registry of 40 components (39 pixel-diffed against the real Nimiq apps before
5
+ they ship, plus 1 original brand composition), plus the team's real asset library (logos, icons,
6
+ flags, imagery). A weekly self-learning audit keeps it current with live Nimiq design — see
7
+ [AUDIT.md](AUDIT.md).
6
8
 
7
9
  > Unofficial community project — see [NOTICE.md](NOTICE.md). All visuals are the Nimiq team's
8
10
  > real shipped files or faithful ports of their open-source components, never hand-drawn
@@ -29,18 +31,52 @@ ln -s "$PWD/bin/nq.js" ~/.local/bin/nq # or: npm link
29
31
  ## Use
30
32
 
31
33
  ```
32
- nq list # browse the 39-component registry
34
+ nq list # browse the 40-component registry
33
35
  nq init --style modern # drop Nimiq design tokens into your project
34
36
  nq add amount-input # copy a component (+ deps + CSS + real assets) into src/components
35
37
  nq add account-header --html # plain HTML/CSS variant instead of Vue
36
38
  nq assets search wallet # search 182 vendored files + 323 nimiq-icons + 422 hexagon flags
37
39
  nq assets add icon:logos-nimiq-horizontal flag:cr-hexagon world-map
38
40
  nq verify all # (repo dev) re-run pixel verification against references
41
+ nq audit # (repo dev) check the LIVE Nimiq upstreams for branding drift
42
+ nq sync-skill # (repo dev) regenerate the nimiq-ui skill block from index.json
39
43
  ```
40
44
 
41
45
  Open `showcase.html` for the full component gallery and `supporting-elements.html` for the
42
46
  wallet + marketing element demos.
43
47
 
48
+ ## Fleet stack alignment (`nq align` / `nq new-app` / `nq hooks`)
49
+
50
+ Branding accuracy (`nq audit`/`nq verify`) keeps the UI matching Nimiq's design. `nq align`
51
+ keeps a whole **app** on the canonical Nimiq fleet stack — same verdict vocabulary
52
+ (`clean` / `safe-drift` / `risky-fail`).
53
+
54
+ ```
55
+ nq new-app my-app # scaffold a CANONICAL app: Bun+Hono+bun:sqlite+vanilla PWA+
56
+ # @nimiq/style + inline rpc-block-scan settlement + Fly deploy
57
+ # kit + ci.yml + a stamped nimiq-stack.json + /health. Aligns clean.
58
+ nq new-app readonly --no-chain # informational app (chainApp:false → skip settlement/styling parity)
59
+ nq new-app pay --settlement rpc --deploy fly
60
+ # (nq new <name> still scaffolds a UI registry component, unchanged)
61
+
62
+ nq align # grade the app in cwd against the canonical fleet baseline
63
+ nq align --all ~/Projects # grade every app dir under a folder
64
+ nq align --fix # safe autofixes only (write/repair nimiq-stack.json)
65
+ nq align --fail-on=settlement,styling # nonzero exit for the pre-commit / CI gate
66
+
67
+ nq hooks install # git pre-commit gate + SessionStart banner + weekly GH Action
68
+ ```
69
+
70
+ **The load-bearing axis is SETTLEMENT.** The `@nimiq/core` light client never reaches
71
+ consensus on our hosts, so any `@nimiq/core/web` import or `Client.create(` /
72
+ `waitForConsensusEstablished(` in `src/` is a **HARD FAIL**. Chain reads must use the
73
+ rpc-block-scan path (the `nimiq-settlement` package). `@nimiq/core` is offline-crypto-only.
74
+
75
+ Each app declares a root `nimiq-stack.json` (schema: `schemas/nimiq-stack.v1.json`); the
76
+ canonical fleet baseline `nq align` grades against lives in `align/canonical.json`. Apps
77
+ that are intentionally off-stack (e.g. nimiq.tech, nimiq-ads, gateflo) set `"exempt": true`
78
+ and are reported but never failed.
79
+
44
80
  ## How pixel accuracy is enforced
45
81
 
46
82
  Every registry component carries:
@@ -65,7 +101,10 @@ disabled) and diffs it against `reference.png` with pixelmatch. Components whose
65
101
  | `onmax/nimiq-ui` | modern `nimiq-css` (oklch tokens, auto dark mode) |
66
102
  | `references/screenshots/` | captured reference screenshots of live Nimiq properties |
67
103
 
68
- Upstream clones live in `upstream/` (gitignored — re-clone with `git clone --depth 1`).
104
+ Upstream clones live in `upstream/` (gitignored — re-clone with `git clone --depth 1`). The exact
105
+ commits the registry was verified against are recorded in [`upstream-pins.json`](upstream-pins.json) —
106
+ the committed source of truth for "what we are current with". `nq audit` watches the live tips against
107
+ these pins; see [AUDIT.md](AUDIT.md).
69
108
 
70
109
  ## Repo layout
71
110
 
@@ -79,5 +118,9 @@ assets/
79
118
  css/legacy/ vendored @nimiq/style (nq-* classes)
80
119
  tokens.md design-token quick reference
81
120
  scripts/verify.mjs pixel-diff harness (playwright + pixelmatch)
121
+ scripts/audit.mjs live-upstream branding-drift engine (nq audit)
122
+ scripts/sync-skill.mjs regenerates the nimiq-ui skill block from index.json
123
+ upstream-pins.json the upstream commits the registry is verified against
124
+ audit/learnings.json self-learning store: which upstream churn is benign vs branding
82
125
  references/screenshots side-by-side reference captures of live Nimiq UIs
83
126
  ```
@@ -0,0 +1,44 @@
1
+ {
2
+ "description": "The CANONICAL Nimiq fleet baseline that `nq align` grades every app's nimiq-stack.json against. Derived from the proven shipping stack (SnapPOS RPC-settlement, nimiq-split reference manifest, TipJar/Beelink deploy kit). This file is the single committed source of truth for 'what a canonical chain app looks like'. Drift off these values is graded with the same vocabulary as nq audit: clean (matches), safe-drift (different but defensible — warn), risky-fail (load-bearing axis violated — fail).",
3
+ "canonicalVersion": "0.1.0",
4
+ "schemaVersion": 1,
5
+ "stack": {
6
+ "framework": "vanilla-pwa",
7
+ "server": "hono",
8
+ "runtime": "bun",
9
+ "build": "none",
10
+ "packageManager": "bun"
11
+ },
12
+ "styling": {
13
+ "source": "nimiq-ui"
14
+ },
15
+ "settlement": {
16
+ "pattern": "rpc-block-scan",
17
+ "lib": "nimiq-settlement",
18
+ "coreRole": "offline-crypto-only",
19
+ "acceptedPatterns": ["rpc-block-scan", "mock", "noop"],
20
+ "acceptedLibs": ["nimiq-settlement", "inline"],
21
+ "forbiddenPatterns": ["light-client"]
22
+ },
23
+ "deploy": {
24
+ "target": "fly",
25
+ "region": "ord",
26
+ "storage": "fly-volume",
27
+ "edge": {
28
+ "provider": "cloudflare",
29
+ "proxied": true
30
+ }
31
+ },
32
+ "config": {
33
+ "tsconfig": "local-strict",
34
+ "lint": "none",
35
+ "fileSizeGuard": 800,
36
+ "ci": true
37
+ },
38
+ "settlementPackage": "nimiq-settlement",
39
+ "lightClient": {
40
+ "forbiddenImports": ["@nimiq/core/web"],
41
+ "forbiddenCalls": ["Client.create(", "waitForConsensusEstablished("],
42
+ "reason": "The @nimiq/core light client never reaches consensus on our hosts (WASM addEventListener bug, hangs in Bun/Node through 2.7.0). Chain reads MUST use the rpc-block-scan path via the nimiq-settlement package; @nimiq/core is offline-crypto-only (key/tx signing)."
43
+ }
44
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "description": "Self-learning store for nq audit. Each rule classifies a changed upstream path (\"<repo>:<path>\", glob: ** = any incl. /, * = any excl. /) as `ignore` (benign churn — never affects our visual ports), `token` (design-framework/token change — review nq init + tokens), or `branding` (visual source change). SAFETY: a changed file that a component lists in meta.json `source.files` is ALWAYS escalated to risky BEFORE these rules run, so broad `ignore` rules cannot mask a real component change. Unmatched paths are `unknown` → surfaced for triage; resolving one means appending a rule here with seenCount++ (that is the learning).",
3
+ "rules": [
4
+ { "pattern": "*:**/*.spec.ts", "verdict": "ignore", "reason": "unit test", "seenCount": 0 },
5
+ { "pattern": "*:**/*.test.ts", "verdict": "ignore", "reason": "unit test", "seenCount": 0 },
6
+ { "pattern": "*:**/test/**", "verdict": "ignore", "reason": "test dir", "seenCount": 0 },
7
+ { "pattern": "*:**/tests/**", "verdict": "ignore", "reason": "test dir", "seenCount": 0 },
8
+ { "pattern": "*:**/__tests__/**", "verdict": "ignore", "reason": "test dir", "seenCount": 0 },
9
+ { "pattern": "*:**/__mocks__/**", "verdict": "ignore", "reason": "test mocks", "seenCount": 0 },
10
+ { "pattern": "*:**/*.stories.*", "verdict": "ignore", "reason": "storybook story", "seenCount": 0 },
11
+ { "pattern": "*:package.json", "verdict": "ignore", "reason": "dependency manifest", "seenCount": 0 },
12
+ { "pattern": "*:package-lock.json", "verdict": "ignore", "reason": "lockfile", "seenCount": 0 },
13
+ { "pattern": "*:yarn.lock", "verdict": "ignore", "reason": "lockfile", "seenCount": 0 },
14
+ { "pattern": "*:pnpm-lock.yaml", "verdict": "ignore", "reason": "lockfile", "seenCount": 0 },
15
+ { "pattern": "*:.github/**", "verdict": "ignore", "reason": "CI config", "seenCount": 0 },
16
+ { "pattern": "*:.husky/**", "verdict": "ignore", "reason": "git hooks", "seenCount": 0 },
17
+ { "pattern": "*:.vscode/**", "verdict": "ignore", "reason": "editor config", "seenCount": 0 },
18
+ { "pattern": "*:**/*.config.*", "verdict": "ignore", "reason": "build/tool config", "seenCount": 0 },
19
+ { "pattern": "*:tsconfig*", "verdict": "ignore", "reason": "ts config", "seenCount": 0 },
20
+ { "pattern": "*:README*", "verdict": "ignore", "reason": "docs", "seenCount": 0 },
21
+ { "pattern": "*:CHANGELOG*", "verdict": "ignore", "reason": "docs", "seenCount": 0 },
22
+ { "pattern": "*:**/*.md", "verdict": "ignore", "reason": "docs", "seenCount": 0 },
23
+ { "pattern": "*:**/i18n/**", "verdict": "ignore", "reason": "translations", "seenCount": 0 },
24
+ { "pattern": "*:**/*.po", "verdict": "ignore", "reason": "translations", "seenCount": 0 },
25
+ { "pattern": "*:src/i18n/**", "verdict": "ignore", "reason": "translations", "seenCount": 0 },
26
+ { "pattern": "*:src/lang/**", "verdict": "ignore", "reason": "translations", "seenCount": 0 },
27
+ { "pattern": "*:**/*Sentry*", "verdict": "ignore", "reason": "telemetry, not branding", "seenCount": 0 },
28
+ { "pattern": "*:**/*sentry*", "verdict": "ignore", "reason": "telemetry, not branding", "seenCount": 0 },
29
+ { "pattern": "*:src/stores/**", "verdict": "ignore", "reason": "app state — visual ports live in the .vue/.css files in provenance", "seenCount": 0 },
30
+ { "pattern": "*:src/lib/**", "verdict": "ignore", "reason": "app logic — not visual markup/style", "seenCount": 0 },
31
+ { "pattern": "*:src/electron/**", "verdict": "ignore", "reason": "desktop shell", "seenCount": 0 },
32
+ { "pattern": "*:src/composables/**", "verdict": "ignore", "reason": "app logic", "seenCount": 0 },
33
+
34
+ { "pattern": "*:src/views/**", "verdict": "ignore", "reason": "app-flow pages (compose shared components) — we port shared components from src/components, not full views", "seenCount": 1, "learnedAt": "2026-06-16", "learnedFrom": "hub drift 9e9b653→cf2ea419 (Bitcoin/Ledger swap success views)" },
35
+ { "pattern": "*:src/config/**", "verdict": "ignore", "reason": "network/app config", "seenCount": 1, "learnedAt": "2026-06-16" },
36
+ { "pattern": "*:src/*.ts", "verdict": "ignore", "reason": "top-level entrypoint/logic (main/export/iframe/cashlink), not visual markup/style", "seenCount": 1, "learnedAt": "2026-06-16" },
37
+ { "pattern": "*:types/**", "verdict": "ignore", "reason": "type declarations", "seenCount": 1, "learnedAt": "2026-06-16" },
38
+ { "pattern": "*:patches/**", "verdict": "ignore", "reason": "dependency patches", "seenCount": 1, "learnedAt": "2026-06-16" },
39
+ { "pattern": "*:public/**", "verdict": "ignore", "reason": "static/build assets (e.g. bundled bitcoin libs)", "seenCount": 1, "learnedAt": "2026-06-16" },
40
+ { "pattern": "*:demos/**", "verdict": "ignore", "reason": "demo pages", "seenCount": 1, "learnedAt": "2026-06-16" },
41
+ { "pattern": "hub:client/**", "verdict": "ignore", "reason": "hub client subpackage build", "seenCount": 1, "learnedAt": "2026-06-16" },
42
+ { "pattern": "*:bitcoinjs-parts.js", "verdict": "ignore", "reason": "vendored BTC lib build artifact", "seenCount": 1, "learnedAt": "2026-06-16" },
43
+
44
+ { "pattern": "nimiq-style:src/**", "verdict": "token", "reason": "design framework source — colors/typography/buttons/layout; review nq tokens + nq init", "seenCount": 0 },
45
+ { "pattern": "nimiq-style:nimiq-style.min.css", "verdict": "token", "reason": "compiled framework css that nq init/add ship", "seenCount": 0 },
46
+ { "pattern": "nimiq-ui:packages/nimiq-css/**", "verdict": "token", "reason": "modern @nimiq/css token source", "seenCount": 0 },
47
+ { "pattern": "*:**/themes.scss", "verdict": "token", "reason": "theme variables feeding component ports", "seenCount": 0 },
48
+ { "pattern": "*:**/variables.scss", "verdict": "token", "reason": "scss variables feeding component ports", "seenCount": 0 },
49
+ { "pattern": "*:src/scss/**", "verdict": "token", "reason": "shared scss (themes/variables/mixins) feeding component ports", "seenCount": 0 }
50
+ ]
51
+ }
package/bin/nq.js CHANGED
@@ -25,9 +25,31 @@ Usage:
25
25
  nq assets add <name...> Copy official asset(s) into ./nimiq/assets/
26
26
  (icon:<name> extracts from nimiq-icons, flag:<cc> from nimiq-flags)
27
27
  nq principles Print the Nimiq design principles — the soul of this tool
28
- nq new <name> Scaffold a new registry component with the principles
29
- checklist + verification contract embedded
28
+ nq new <name> Scaffold a new REGISTRY component (principles checklist +
29
+ verification contract embedded)
30
+ nq new-app <name> Scaffold a CANONICAL Nimiq fleet app (Bun+Hono+bun:sqlite+
31
+ vanilla PWA + @nimiq/style + nimiq-settlement + Fly kit +
32
+ a stamped nimiq-stack.json + /health). Starts clean on align.
33
+ --no-chain chainApp:false (skip settlement + styling parity)
34
+ --settlement mock|rpc|noop settlement client (default mock)
35
+ --deploy fly|none deploy kit (default fly)
36
+ nq align [path] Grade an app's stack vs the canonical fleet baseline.
37
+ --all <dir> Grade every app dir under <dir>
38
+ --fix Safe autofixes only (write/repair nimiq-stack.json)
39
+ --fail-on settlement,styling Nonzero exit if a listed axis is risky-fail (the gate)
40
+ --quiet One-line drift banner (SessionStart) --json machine output
41
+ SETTLEMENT is load-bearing: any @nimiq/core/web import or
42
+ Client.create( / waitForConsensusEstablished( in src HARD FAILS.
43
+ nq hooks install [repo] Install the drift hooks: git pre-commit (align --fail-on),
44
+ SessionStart banner, weekly GH Action (--write drops the workflow)
30
45
  nq verify <component|all> Render the html variant and diff against the reference PNG
46
+ nq lint <file.html|url> Render a page and enforce the brand rules + breathability.
47
+ --fix Auto-fix the safe text violations in a local file (dashes, title periods)
48
+ --json Machine-readable output
49
+ ERRORS (off-brand slop) fail; WARNINGS (density) advise. See LINT.md
50
+ nq audit [--skip-verify] Check the LIVE Nimiq upstreams for branding drift vs our
51
+ pinned registry; attribute drift to components; write a report
52
+ nq sync-skill Regenerate the nimiq-ui skill's registry block from index.json
31
53
  nq help This message
32
54
 
33
55
  All visual assets are the team's real shipped files (nimiq.com, wallet, hub,
@@ -52,6 +74,19 @@ function parseFlags(args) {
52
74
  if (a === '--vue' || a === '--html') flags.variant = a.slice(2);
53
75
  else if (a === '--out') flags.out = args[++i];
54
76
  else if (a === '--style') flags.style = args[++i];
77
+ else if (a === '--fix') flags.fix = true;
78
+ else if (a === '--json') flags.json = true;
79
+ else if (a === '--quiet') flags.quiet = true;
80
+ else if (a === '--all') flags.all = (args[i + 1] && !args[i + 1].startsWith('--')) ? args[++i] : true;
81
+ else if (a === '--no-chain') flags.noChain = true;
82
+ else if (a.startsWith('--settlement=')) flags.settlement = a.slice('--settlement='.length);
83
+ else if (a === '--settlement') flags.settlement = args[++i];
84
+ else if (a.startsWith('--deploy=')) flags.deploy = a.slice('--deploy='.length);
85
+ else if (a === '--deploy') flags.deploy = args[++i];
86
+ else if (a.startsWith('--fail-on=')) flags.failOn = a.slice('--fail-on='.length);
87
+ else if (a === '--fail-on') flags.failOn = args[++i];
88
+ else if (a === '--check') flags.check = true;
89
+ else if (a === '--write') flags.write = true;
55
90
  else rest.push(a);
56
91
  }
57
92
  return { flags, rest };
@@ -259,10 +294,10 @@ const CHECKLIST = [
259
294
  'reproducible: plain HTML/CSS or standard Vue, passes nq verify',
260
295
  ];
261
296
 
262
- async function cmdNew(name, flags) {
263
- if (!name || !/^[a-z][a-z0-9-]*$/.test(name)) throw new Error('nq new <kebab-case-name>');
297
+ async function cmdNewComponent(name, flags) {
298
+ if (!name || !/^[a-z][a-z0-9-]*$/.test(name)) throw new Error('nq new-component <kebab-case-name>');
264
299
  if (ROOT.includes('node_modules') || ROOT.includes('_npx')) {
265
- throw new Error('nq new scaffolds a component INTO the registry repo — clone it first:\n git clone https://github.com/Andjroo111/nimiq-branding-cli\nthen run nq new from that checkout.');
300
+ throw new Error('nq new-component scaffolds a component INTO the registry repo — clone it first:\n git clone https://github.com/Andjroo111/nimiq-branding-cli\nthen run nq new-component from that checkout.');
266
301
  }
267
302
  const dir = join(ROOT, 'registry', 'components', name);
268
303
  if (existsSync(dir)) throw new Error(`component "${name}" already exists`);
@@ -327,6 +362,37 @@ async function cmdNew(name, flags) {
327
362
  Read the soul of the tool first: nq principles`);
328
363
  }
329
364
 
365
+ async function cmdNew(name, flags) {
366
+ const { scaffoldApp } = await import(join(ROOT, 'scripts', 'scaffold.mjs'));
367
+ const r = await scaffoldApp(name, { noChain: flags.noChain, settlement: flags.settlement, deploy: flags.deploy });
368
+ console.log(`+ scaffolded canonical Nimiq app → ${r.dir}`);
369
+ console.log(` ${r.files.length} files · chainApp=${r.chain}${r.chain ? ` · settlement=${r.settlement}` : ''} · deploy=${r.deploy}`);
370
+ console.log(`\nNext:\n cd ${name}\n bun install\n bun run dev # http://localhost:3000 (try GET /health)\n nq align # should be clean on every axis\n nq hooks install # add the pre-commit settlement/styling gate`);
371
+ }
372
+
373
+ async function cmdHooks(sub, flags) {
374
+ const { installHooks, SESSION_START, WEEKLY_WORKFLOW } = await import(join(ROOT, 'scripts', 'hooks.mjs'));
375
+ if (sub === 'install') {
376
+ const r = await installHooks(rest[1], { write: !!flags.write });
377
+ for (const p of r.wrote) console.log(`+ ${p}`);
378
+ for (const p of r.printed) console.log(` ${p}`);
379
+ console.log(`\nSessionStart advisory — add to .claude/settings.json hooks.SessionStart:\n ${SESSION_START.trim().split('\n').pop()}`);
380
+ console.log(`\nWeekly GH Action: copy hooks/stack-align.yml into .github/workflows/ (or re-run with --write).`);
381
+ return;
382
+ }
383
+ if (sub === 'show' || !sub) {
384
+ console.log('# git pre-commit (.git/hooks/pre-commit):\n');
385
+ const { PRE_COMMIT } = await import(join(ROOT, 'scripts', 'hooks.mjs'));
386
+ console.log(PRE_COMMIT);
387
+ console.log('# SessionStart advisory:\n');
388
+ console.log(SESSION_START);
389
+ console.log('# weekly GH Action (.github/workflows/stack-align.yml):\n');
390
+ console.log(WEEKLY_WORKFLOW);
391
+ return;
392
+ }
393
+ throw new Error(`nq hooks ${sub} — unknown (install | show)`);
394
+ }
395
+
330
396
  async function cmdVerify(target) {
331
397
  const { verify } = await import(join(ROOT, 'scripts', 'verify.mjs'));
332
398
  const names = target === 'all' || !target
@@ -353,9 +419,25 @@ try {
353
419
  case 'init': await cmdInit(flags); break;
354
420
  case 'tokens': await cmdTokens(); break;
355
421
  case 'principles': await cmdPrinciples(); break;
356
- case 'new': await cmdNew(rest[0], flags); break;
422
+ case 'new':
423
+ case 'new-component': await cmdNewComponent(rest[0], flags); break;
424
+ case 'new-app': await cmdNew(rest[0], flags); break;
425
+ case 'align': {
426
+ const { run } = await import(join(ROOT, 'scripts', 'align.mjs'));
427
+ await run(rest, flags);
428
+ break;
429
+ }
430
+ case 'hooks': await cmdHooks(rest[0], flags); break;
357
431
  case 'assets': await cmdAssets(rest[0], rest.slice(1), flags); break;
358
432
  case 'verify': await cmdVerify(rest[0]); break;
433
+ case 'lint': {
434
+ const { lint } = await import(join(ROOT, 'scripts', 'lint.mjs'));
435
+ const r = await lint(rest[0], { fix: flags.fix, json: flags.json });
436
+ if (r.errorCount) process.exitCode = 1;
437
+ break;
438
+ }
439
+ case 'audit': await import(join(ROOT, 'scripts', 'audit.mjs')); break;
440
+ case 'sync-skill': await import(join(ROOT, 'scripts', 'sync-skill.mjs')); break;
359
441
  default: console.log(HELP);
360
442
  }
361
443
  } catch (err) {