@bongos/core 1.24.46 → 1.24.48

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.
@@ -60,6 +60,9 @@
60
60
  shadows, motion, stacking — every surface loads. Imported FIRST so this
61
61
  sheet's own rules follow it; the hall's scale names below are aliases of it. */
62
62
  @import url("/modules/ui-design/public/tokens.css");
63
+ /* The hall's one button and one field (task 1005084): .btn and its modifiers,
64
+ the text field / select / textarea. Straight after the scales it spends. */
65
+ @import url("controls.css");
63
66
  @import url("page-notes.css");
64
67
  @import url("craft-review.css");
65
68
 
@@ -430,20 +433,7 @@ code, kbd, pre {
430
433
  the composition around it. */
431
434
  .num, .tabular { font-variant-numeric: tabular-nums; }
432
435
 
433
- select, input[type="text"], input[type="search"], textarea {
434
- font-family: var(--font-body);
435
- font-size: 14px;
436
- color: var(--ink);
437
- background: none;
438
- border: 1px solid var(--rule);
439
- border-radius: var(--radius-pill);
440
- min-height: 30px;
441
- padding: 5px 12px;
442
- }
443
- select:hover, input[type="text"]:hover, input[type="search"]:hover { border-color: var(--ink-faint); }
444
- textarea { border-radius: var(--radius-sm); }
445
- select:disabled { opacity: 0.55; cursor: not-allowed; }
446
- select option { background: var(--bg-card); color: var(--ink); }
436
+ /* The field (text input, select, textarea) is controls.css's (task 1005084). */
447
437
 
448
438
  button { font-family: var(--font-body); }
449
439
 
@@ -1422,8 +1412,7 @@ html.setup-gate-open { overflow: hidden; }
1422
1412
  }
1423
1413
  .setup-gate code { font-family: var(--font-mono); font-size: 14px; padding: 1px 6px; border-radius: 6px; background: var(--glass); }
1424
1414
  .setup-gate__crafts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
1425
- .setup-gate__craft,
1426
- .setup-gate__save {
1415
+ .setup-gate__craft {
1427
1416
  min-height: 36px;
1428
1417
  padding: 0 16px;
1429
1418
  border-radius: var(--radius-pill);
@@ -1435,8 +1424,8 @@ html.setup-gate-open { overflow: hidden; }
1435
1424
  cursor: pointer;
1436
1425
  }
1437
1426
  .setup-gate__craft[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-tint); }
1438
- .setup-gate__save { margin-top: 12px; background: var(--accent); border-color: var(--accent); color: var(--accent-on); }
1439
- .setup-gate__save:disabled { opacity: .5; cursor: default; }
1427
+ /* The save is the shared primary door (controls.css, task 1005084). */
1428
+ .setup-gate__save { margin-top: 12px; }
1440
1429
  .setup-gate__err { margin: 8px 0 0; font-size: 14px; color: var(--danger); }
1441
1430
  .setup-gate__foot { margin: 24px 0 0; font-size: 14px; }
1442
1431
  .setup-gate__foot a { color: var(--ink-soft); }
@@ -2634,23 +2623,16 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
2634
2623
  wherever there is room and drops it where there is not. */
2635
2624
  min-width: min(150px, 100%);
2636
2625
  max-width: 100%;
2637
- min-height: 32px;
2638
- font-family: var(--font-body);
2639
- padding: 4px 8px;
2640
- font-size: 13.5px;
2641
2626
  text-transform: none;
2642
2627
  letter-spacing: normal;
2643
2628
  font-weight: 400;
2644
2629
  }
2645
- /* task 1829: the Work Board free-text search box, sized to match the filter
2646
- selects it sits alongside. */
2630
+ /* task 1829: the Work Board free-text search box. Both it and the selects are
2631
+ the shared field (controls.css, task 1005084); the filter only sets widths
2632
+ and undoes the label voice they would inherit. */
2647
2633
  .filter input[type="search"] {
2648
2634
  min-width: min(200px, 100%);
2649
2635
  max-width: 100%;
2650
- min-height: 32px;
2651
- font-family: var(--font-body);
2652
- padding: 4px 8px;
2653
- font-size: 13.5px;
2654
2636
  text-transform: none;
2655
2637
  letter-spacing: normal;
2656
2638
  font-weight: 400;
@@ -2673,94 +2655,8 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
2673
2655
  }
2674
2656
  .pager__range { font-family: var(--font-mono); font-size: 12px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
2675
2657
 
2676
- /* Pill or nothing (DESIGN.md, Shapes). A ghost button is a hairline pill; the
2677
- one filled shape on a page is the accent door, and only where the action is
2678
- the live thing. 28px of box plus the row’s own air clears the 24px floor. */
2679
- .btn-ghost {
2680
- display: inline-flex;
2681
- align-items: center;
2682
- justify-content: center;
2683
- gap: 6px;
2684
- min-height: 30px;
2685
- min-width: 56px;
2686
- padding: 0 14px;
2687
- font-size: 13px;
2688
- text-transform: lowercase;
2689
- color: var(--ink-soft);
2690
- background: none;
2691
- border: 1px solid var(--rule);
2692
- border-radius: var(--radius-pill);
2693
- cursor: pointer;
2694
- }
2695
- .btn-ghost:hover:not(:disabled) { border-color: var(--ink-faint); color: var(--ink); text-decoration: none; }
2696
- .btn-ghost:disabled { color: var(--ink-faint); border-color: var(--rule-soft); cursor: default; }
2697
-
2698
- /* The accent door: the page’s one primary action. --accent-on is the dark ink
2699
- that goes on the salmon; white on it fails AA and is banned. */
2700
- .btn-accent {
2701
- display: inline-flex;
2702
- align-items: center;
2703
- justify-content: center;
2704
- gap: 6px;
2705
- min-height: 34px;
2706
- min-width: 74px;
2707
- padding: 0 16px;
2708
- font-size: 13.5px;
2709
- font-weight: 500;
2710
- text-transform: lowercase;
2711
- color: var(--accent-on);
2712
- background: var(--accent);
2713
- border: 0;
2714
- border-radius: var(--radius-pill);
2715
- cursor: pointer;
2716
- }
2717
- .btn-accent:hover:not(:disabled) { background: var(--accent-lift); color: var(--accent-on); text-decoration: none; }
2718
- .btn-accent:disabled { background: var(--glass); color: var(--ink-faint); cursor: default; }
2719
-
2720
- /* `.btn` / `.btn--ghost` — the SECOND button vocabulary in the hall, used by
2721
- ideas, idea, copy-desk, my-projects and collab. Task 1003305 found it had no
2722
- rule anywhere in the surface: every one of those buttons was rendering as a
2723
- browser default control, on a page whose every other shape is a pill. They
2724
- are not renamed to .btn-accent/.btn-ghost because the class is spelled into
2725
- markup and into tests/idea_spark_hall.mjs; instead the two vocabularies are
2726
- made to say the same thing. `.btn` is the accent door and `.btn--ghost` its
2727
- quiet sibling, which is exactly how the calling pages already use the pair
2728
- (submit vs. discard, mark vs. withdraw). --accent-on is the dark ink for the
2729
- salmon; white on it fails AA and DESIGN.md bans it. */
2730
- .btn {
2731
- display: inline-flex;
2732
- align-items: center;
2733
- justify-content: center;
2734
- gap: 6px;
2735
- min-height: 34px;
2736
- min-width: 74px;
2737
- padding: 0 16px;
2738
- font-family: var(--font-body);
2739
- font-size: 13.5px;
2740
- font-weight: 500;
2741
- text-transform: lowercase;
2742
- color: var(--accent-on);
2743
- background: var(--accent);
2744
- /* transparent rather than absent: collab.css's `.btn--quiet` sets only a
2745
- border-COLOUR, and a modifier cannot paint an edge that was never reserved. */
2746
- border: var(--line) solid transparent;
2747
- border-radius: var(--radius-pill);
2748
- cursor: pointer;
2749
- }
2750
- .btn:hover:not(:disabled) { background: var(--accent-lift); color: var(--accent-on); text-decoration: none; }
2751
- .btn:disabled { background: var(--glass); color: var(--ink-faint); cursor: default; }
2752
- .btn--ghost {
2753
- min-height: 30px;
2754
- min-width: 56px;
2755
- padding: 0 14px;
2756
- font-size: 13px;
2757
- font-weight: 400;
2758
- color: var(--ink-soft);
2759
- background: none;
2760
- border-color: var(--rule);
2761
- }
2762
- .btn--ghost:hover:not(:disabled) { background: none; border-color: var(--ink-faint); color: var(--ink); }
2763
- .btn--ghost:disabled { background: none; color: var(--ink-faint); border-color: var(--rule-soft); }
2658
+ /* The buttons — .btn and its modifiers, and the older names pages still write
2659
+ (.btn-accent, .btn-ghost, .btn--ghost …) — are controls.css's (task 1005084). */
2764
2660
 
2765
2661
  /* ---------- profile / standing ---------- */
2766
2662
 
@@ -3040,11 +2936,8 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
3040
2936
  gap keeps the label and the select together when the lede wraps. */
3041
2937
  .curve__filter { display: inline-flex; align-items: center; gap: 6px; margin-left: 10px; }
3042
2938
  .curve__filter-label { color: var(--ink-soft); }
3043
- .curve__filter-select {
3044
- font: inherit; color: inherit; background: var(--surface-2);
3045
- border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
3046
- padding: 2px 6px; max-width: 16ch;
3047
- }
2939
+ /* the shared field (controls.css); only its width is the filter's */
2940
+ .curve__filter-select { max-width: 16ch; }
3048
2941
 
3049
2942
  .curve__headline {
3050
2943
  display: grid;
@@ -3297,22 +3190,6 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
3297
3190
  }
3298
3191
  .setup__divider::before, .setup__divider::after { content: ""; flex: 1; border-top: 1px solid var(--rule-soft); }
3299
3192
 
3300
- .setup__btn {
3301
- display: inline-flex;
3302
- align-items: center;
3303
- gap: 8px;
3304
- height: 34px;
3305
- padding: 0 16px;
3306
- border-radius: var(--radius-sm);
3307
- border: 1px solid var(--accent);
3308
- background: var(--accent);
3309
- color: #fff;
3310
- font-size: 13.5px;
3311
- font-weight: 600;
3312
- cursor: pointer;
3313
- }
3314
- .setup__btn:hover:not(:disabled) { background: var(--accent-deep); border-color: var(--accent-deep); color: #fff; text-decoration: none; }
3315
- .setup__btn:disabled, .setup__btn--open[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }
3316
3193
  .setup__getcode { margin: 10px 0; }
3317
3194
  /* A <summary> is a control, so it owes the 24px floor like any other; it
3318
3195
  measured 20px tall (task 1003304). The height is bought with padding rather
@@ -3387,31 +3264,9 @@ html[data-door="out"] #hall-main { width: 100%; }
3387
3264
  line-height: 18px;
3388
3265
  }
3389
3266
  .landing__cta { margin: 14px 0; }
3390
- .landing__btn {
3391
- display: inline-flex;
3392
- align-items: center;
3393
- justify-content: center;
3394
- gap: 10px;
3395
- min-height: 46px;
3396
- padding: 0 22px;
3397
- border-radius: var(--radius-pill);
3398
- border: 0;
3399
- background: var(--accent);
3400
- color: var(--accent-on);
3401
- font-size: 14px;
3402
- font-weight: 500;
3403
- text-transform: lowercase;
3404
- cursor: pointer;
3405
- }
3406
- .landing__btn:hover { background: var(--accent-lift); text-decoration: none; color: var(--accent-on); }
3267
+ /* .landing__btn is the shared primary door, --quiet its ghost (controls.css). */
3407
3268
  .landing__btn--block { display: flex; width: 100%; }
3408
3269
  .landing__gh { flex-shrink: 0; }
3409
- .landing__btn--quiet {
3410
- background: transparent;
3411
- color: var(--ink);
3412
- box-shadow: inset 0 0 0 1px var(--border-strong);
3413
- }
3414
- .landing__btn--quiet:hover { background: var(--surface-2); color: var(--ink); }
3415
3270
  .landing__note { font-size: 12.5px; color: var(--ink-faint); }
3416
3271
  .landing__error { color: var(--danger); }
3417
3272
  .landing__error-h { color: var(--danger); margin: 0 0 4px; font-size: 14px; }
@@ -3642,29 +3497,8 @@ html[data-door="out"] #hall-main { width: 100%; }
3642
3497
  the touches chip and the whole-card selection target (promoted from work.css,
3643
3498
  task 1003659, so their cascade position is the same on every page). */
3644
3499
 
3645
- /* The page's one primary action, and the only accent FILL on the board.
3646
- --accent-on is the dark ink that goes on the salmon: white on it fails AA and
3647
- DESIGN.md bans it outright, which is what the old `color: #fff` was. */
3648
- .task__claim {
3649
- display: inline-flex;
3650
- align-items: center;
3651
- justify-content: center;
3652
- min-height: 34px;
3653
- min-width: 74px;
3654
- padding: 0 16px;
3655
- background: var(--accent);
3656
- border: 0;
3657
- color: var(--accent-on);
3658
- border-radius: var(--radius-pill);
3659
- font-size: 13.5px;
3660
- font-weight: 500;
3661
- text-transform: lowercase;
3662
- cursor: pointer;
3663
- white-space: nowrap;
3664
- transition: background 150ms, opacity 150ms;
3665
- }
3666
- .task__claim:hover:not(:disabled) { background: var(--accent-lift); color: var(--accent-on); }
3667
- .task__claim:disabled { background: var(--glass); color: var(--ink-faint); cursor: default; }
3500
+ /* The page's one primary action, and the only accent FILL on the board: the
3501
+ shared primary door at the row size (controls.css, task 1005084). */
3668
3502
 
3669
3503
  /* Files a claimable task is responsible for — a compact count chip in the meta
3670
3504
  row; the full path list rides the tooltip (task 2170 density pass). The mono
@@ -3778,27 +3612,13 @@ html[data-door="out"] #hall-main { width: 100%; }
3778
3612
  color: var(--ink-faint);
3779
3613
  }
3780
3614
 
3781
- .wip__release {
3782
- display: inline-flex;
3783
- align-items: center;
3784
- justify-content: center;
3785
- min-height: 30px;
3786
- padding: 0 14px;
3787
- background: none;
3788
- border: var(--line) solid var(--rule);
3789
- color: var(--ink-soft);
3790
- border-radius: var(--radius-pill);
3791
- font-size: 13px;
3792
- text-transform: lowercase;
3793
- cursor: pointer;
3794
- transition: background 150ms, color 150ms, border-color 150ms;
3795
- }
3615
+ /* .wip__release is the shared ghost at the row size (controls.css); a release
3616
+ is the undo of a claim, so its hover warns. */
3796
3617
  .wip__release:hover:not(:disabled) {
3797
3618
  background: var(--danger-weak);
3798
3619
  color: var(--danger);
3799
3620
  border-color: color-mix(in srgb, var(--danger) 40%, transparent);
3800
3621
  }
3801
- .wip__release:disabled { opacity: 0.55; cursor: default; }
3802
3622
 
3803
3623
  .wip__select {
3804
3624
  appearance: none;
@@ -4064,38 +3884,8 @@ html[data-door="out"] #hall-main { width: 100%; }
4064
3884
  .td-dep:hover { text-decoration: underline; }
4065
3885
  .td-dep-status { color: var(--ink-faint); font-size: 12px; margin-left: 6px; }
4066
3886
  .td-actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 18px 0 0; }
4067
- /* Pill or nothing (DESIGN.md, Shapes): the ghost pill, with --btn--primary the
4068
- one accent door. It carried a literal `#fff` on the salmon until task 1003305
4069
- — the exact pairing DESIGN.md bans, because it fails AA. */
4070
- .td-btn {
4071
- display: inline-flex;
4072
- align-items: center;
4073
- justify-content: center;
4074
- font: inherit;
4075
- font-size: 13px;
4076
- min-height: 30px;
4077
- min-width: 56px;
4078
- padding: 0 14px;
4079
- border: var(--line) solid var(--rule);
4080
- border-radius: var(--radius-pill);
4081
- background: none;
4082
- color: var(--ink-soft);
4083
- text-transform: lowercase;
4084
- cursor: pointer;
4085
- }
4086
- .td-btn:hover:not(:disabled) { border-color: var(--ink-faint); color: var(--ink); text-decoration: none; }
4087
- .td-btn--primary {
4088
- min-height: 34px;
4089
- min-width: 74px;
4090
- padding: 0 16px;
4091
- font-weight: 500;
4092
- background: var(--accent);
4093
- color: var(--accent-on);
4094
- border-color: var(--accent);
4095
- }
4096
- .td-btn--primary:hover:not(:disabled) { background: var(--accent-lift); border-color: var(--accent-lift); color: var(--accent-on); }
4097
- .td-btn:disabled { color: var(--ink-faint); border-color: var(--rule-soft); cursor: default; }
4098
- .td-btn--primary:disabled { background: var(--glass); }
3887
+ /* .td-btn is the shared ghost at the row size and .td-btn--primary the accent
3888
+ door (controls.css, task 1005084). */
4099
3889
  .td-action-note { flex-basis: 100%; font-size: 12.5px; color: var(--ink-soft); margin: 4px 0 0; }
4100
3890
 
4101
3891
  /* Idea triage inside the record overlay (task 1002447). A two-column grid so
@@ -4103,17 +3893,9 @@ html[data-door="out"] #hall-main { width: 100%; }
4103
3893
  .td-form { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 10px; margin: 14px 0 0; }
4104
3894
  .td-field { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
4105
3895
  .td-field--wide { grid-column: 1 / -1; }
4106
- /* The base `select, input` rule already carries the world's field; this only
4107
- re-states the sizing the two-column grid needs. */
4108
- .td-field input, .td-field select {
4109
- font-size: 13px;
4110
- min-width: 0;
4111
- max-width: 100%;
4112
- }
4113
- /* The 24px floor (task 1004301 found it failing): at 13px the base field's padding left a
4114
- number input 21px tall beside a select that clears it — the task page's "Wait for task"
4115
- box. Sized to sit level with its select. */
4116
- .td-field input { min-height: 26px; box-sizing: border-box; }
3896
+ /* The field is the shared one (controls.css); the two-column grid only needs
3897
+ it to shrink with its column. */
3898
+ .td-field input, .td-field select { min-width: 0; max-width: 100%; }
4117
3899
  .td-form .td-actions, .td-form-hint, .td-form .td-action-note { grid-column: 1 / -1; }
4118
3900
  .td-form .td-actions { margin: 4px 0 0; }
4119
3901
  .td-form-hint { font-size: 12.5px; color: var(--ink-soft); margin: 0; }
@@ -4316,7 +4098,6 @@ html[data-door="out"] #hall-main { width: 100%; }
4316
4098
  .filter-views__tag { font-size: 11px; color: var(--ink-faint); margin-left: 6px; }
4317
4099
  .filter-views__empty, .filter-views__note { font-size: 12.5px; color: var(--ink-soft); margin: 0 0 10px; }
4318
4100
  .filter-views__note:empty { display: none; }
4319
- .filter-views__row .btn-ghost { min-height: 24px; padding: 0 8px; font-size: 12px; }
4320
4101
  .filter-views__save { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; }
4321
4102
  .filter-views__save .filter { flex: 1 1 180px; }
4322
4103
  /* On a phone the button can sit anywhere in a wrapped row, so the menu spans the
@@ -4378,9 +4159,6 @@ html[data-door="out"] #hall-main { width: 100%; }
4378
4159
  than edit the markup, the name joins the lede it meant. */
4379
4160
  .scroll__note { margin: 0 0 14px; font-size: 13.5px; color: var(--ink-soft); max-width: var(--measure); }
4380
4161
 
4381
- /* collab.js:73-74 — the settle buttons asked for a small variant of .btn that
4382
- does not exist, so "Someone helped" rendered full-size on every row. */
4383
- .btn--sm { min-height: 26px; padding: 0 11px; font-size: 12.5px; }
4384
4162
 
4385
4163
  /* The notice line: roster.js and blockers.js notice() set body.className='note'
4386
4164
  for an empty / error message, and neither page's sheet defines it. */
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // modules/ui-design/scripts/gen-design-tokens.js — build config/design-tokens.neutral.json
4
+ // from the two files that already own its values (task 1005082, goal 1000138).
5
+ //
6
+ // WHY. The design-tool token file (read by the Claude Design and Figma adapters)
7
+ // was hand-written, and its spacing (4/8/16/24/40/64) and motion (200/250ms) had
8
+ // drifted from what every page actually uses (the 6/10/14/22/34/52 rhythm and the
9
+ // 150/200ms durations in the shared token sheet). A designer starting from it got
10
+ // a spacing scale no page has. Now nothing in that file is typed by hand:
11
+ //
12
+ // colours + font stacks + font links ← config/branding.neutral.json theme.ui
13
+ // spacing, corners, motion ← modules/ui-design/public/tokens.css (--ds-*)
14
+ //
15
+ // The file keeps its existing key names (config/design-tokens.schema.json closes
16
+ // them, and an instance's config/design-tokens.json overrides by those names); each
17
+ // key is mapped to the --ds-* step it means, below.
18
+ //
19
+ // node modules/ui-design/scripts/gen-design-tokens.js write the file
20
+ // node modules/ui-design/scripts/gen-design-tokens.js --check exit 1 if it is stale
21
+ //
22
+ // Guarded by tests/design_tokens_generated.mjs.
23
+
24
+ const fs = require('node:fs');
25
+ const path = require('node:path');
26
+
27
+ const ROOT = path.resolve(__dirname, '..', '..', '..');
28
+ const SHEET = path.join(ROOT, 'modules', 'ui-design', 'public', 'tokens.css');
29
+ const PACK = path.join(ROOT, 'config', 'branding.neutral.json');
30
+ const OUT = path.join(ROOT, 'config', 'design-tokens.neutral.json');
31
+
32
+ // The schema's key → the --ds-* custom property it is generated from.
33
+ const MAP = Object.freeze({
34
+ scale: { xs: '--ds-space-hair', sm: '--ds-space-xs', md: '--ds-space-sm', lg: '--ds-space-md', xl: '--ds-space-lg', '2xl': '--ds-space-xl' },
35
+ radius: { sm: '--ds-radius-tile', md: '--ds-radius-panel', lg: '--ds-radius-panel', pill: '--ds-radius-pill' },
36
+ motion: { fast: '--ds-dur-fast', normal: '--ds-dur-base', slow: '--ds-dur-arrive', easing: '--ds-ease' },
37
+ });
38
+
39
+ // The pack carries no mono stack (it is not one of the fifteen), so the design
40
+ // side states the label/mono face DESIGN.md names (typography.label).
41
+ const MONO = '"JetBrains Mono", ui-monospace, "Cascadia Mono", Consolas, monospace';
42
+
43
+ const NOTE = 'GENERATED by node modules/ui-design/scripts/gen-design-tokens.js; do not edit by hand. '
44
+ + 'Colours, font stacks and font links are config/branding.neutral.json theme.ui; scale, radius and motion are '
45
+ + 'the --ds-* steps of modules/ui-design/public/tokens.css (the mapping is in the generator). To override, copy '
46
+ + 'values into config/design-tokens.json (the instance pack). See DESIGN.md "The hall" and '
47
+ + 'modules/ui-design/docs/design-contract.md.';
48
+
49
+ // The first :root { … } block of the sheet: the light/base values. The dark and
50
+ // reduced-motion blocks restate a few of them and are deliberately not read.
51
+ function readSheetVars(css) {
52
+ const noComments = css.replace(/\/\*[\s\S]*?\*\//g, '');
53
+ const m = noComments.match(/:root\s*\{([^}]*)\}/);
54
+ if (!m) throw new Error('tokens.css: no :root block');
55
+ const vars = {};
56
+ for (const decl of m[1].split(';')) {
57
+ const d = decl.match(/^\s*(--[a-z0-9-]+)\s*:\s*([\s\S]+?)\s*$/i);
58
+ if (d) vars[d[1]] = d[2].replace(/\s+/g, ' ');
59
+ }
60
+ return vars;
61
+ }
62
+
63
+ function build({ sheet = fs.readFileSync(SHEET, 'utf8'), pack = JSON.parse(fs.readFileSync(PACK, 'utf8')) } = {}) {
64
+ const vars = readSheetVars(sheet);
65
+ const ui = (pack.theme && pack.theme.ui) || {};
66
+ const pick = (group) => {
67
+ const out = {};
68
+ for (const [key, prop] of Object.entries(MAP[group])) {
69
+ if (!(prop in vars)) throw new Error(`tokens.css: ${prop} (for ${group}.${key}) is not declared`);
70
+ out[key] = vars[prop];
71
+ }
72
+ return out;
73
+ };
74
+ const fonts = { display: ui.fonts.display, body: ui.fonts.body, mono: MONO };
75
+ if (Array.isArray(ui.fonts.links)) fonts.links = ui.fonts.links.slice();
76
+ const tokens = {
77
+ _note: NOTE,
78
+ colors: Object.assign({}, ui.colors),
79
+ fonts,
80
+ scale: pick('scale'),
81
+ radius: pick('radius'),
82
+ motion: pick('motion'),
83
+ };
84
+ return JSON.stringify(tokens, null, 2) + '\n';
85
+ }
86
+
87
+ module.exports = { build, readSheetVars, MAP, OUT };
88
+
89
+ if (require.main === module) {
90
+ const next = build();
91
+ const current = fs.existsSync(OUT) ? fs.readFileSync(OUT, 'utf8') : '';
92
+ if (process.argv.includes('--check')) {
93
+ if (current === next) { process.stdout.write('design-tokens.neutral.json: current\n'); process.exitCode = 0; }
94
+ else {
95
+ process.stderr.write('design-tokens.neutral.json is STALE: run node modules/ui-design/scripts/gen-design-tokens.js\n');
96
+ process.exitCode = 1;
97
+ }
98
+ } else {
99
+ if (current !== next) fs.writeFileSync(OUT, next);
100
+ process.stdout.write((current === next ? 'design-tokens.neutral.json: unchanged' : 'design-tokens.neutral.json: written') + '\n');
101
+ }
102
+ }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.46",
3
+ "version": "1.24.48",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.46",
9
+ "version": "1.24.48",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.46",
3
+ "version": "1.24.48",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -11247,5 +11247,17 @@
11247
11247
  "id": "1005080",
11248
11248
  "text": "The screenshot checker builders use before shipping a design change shows the real hall pages again, instead of a pop-up covering every page. Two small accessibility fixes came with it: a few tiny links are now big enough to t"
11249
11249
  }
11250
+ ],
11251
+ "1.24.47": [
11252
+ {
11253
+ "id": "1005082",
11254
+ "text": "The design rulebook now describes the builders' hall as it really is, and spells out the name: 'cloud bongos' in the logo, 'Cloud Bongos' in sentences. The design-tool token file is now built automatically from the real styles"
11255
+ }
11256
+ ],
11257
+ "1.24.48": [
11258
+ {
11259
+ "id": "1005084",
11260
+ "text": "The builders' hall now has one button and one text box style everywhere instead of eight button sizes and five box styles; main buttons are the full 46px pill with dark text on the orange, and the setup screen's save button is"
11261
+ }
11250
11262
  ]
11251
11263
  }
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.46'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.48'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -196,7 +196,10 @@ test('goals.html no longer borrows work.css, and work.css no longer holds what i
196
196
  assert.ok(!new RegExp(`^${sel.replace('.', '\\.')}(\\s|:|,|\\{)`, 'm').test(work), `work.css must not restate ${sel}`);
197
197
  }
198
198
  const style = read('style.css');
199
- for (const sel of ['.claim-count-line', '.wip__release', '.wip__select']) {
199
+ for (const sel of ['.claim-count-line', '.wip__select']) {
200
200
  assert.ok(new RegExp(`^${sel.replace('.', '\\.')} \\{`, 'm').test(style), `style.css carries ${sel}`);
201
201
  }
202
+ // task 1005084: .wip__release is the shared ghost (controls.css); style.css keeps its warning hover
203
+ assert.match(read('controls.css'), /\.wip__release/, 'the shared sheet draws .wip__release');
204
+ assert.match(style, /^\.wip__release:hover:not\(:disabled\) \{/m, 'style.css keeps the release\'s warning hover');
202
205
  });
@@ -0,0 +1,56 @@
1
+ // tests/design_tokens_generated.mjs — config/design-tokens.neutral.json is GENERATED
2
+ // from the branding pack and the shared token sheet, and regenerates byte-identical
3
+ // (task 1005082, goal 1000138).
4
+ //
5
+ // WHY. The hand-written file's spacing and motion had drifted from every page's
6
+ // (4/8/16px against the 6/10/14px rhythm). The generator removes the hand; this
7
+ // test is what notices a hand coming back, or a token-sheet edit nobody regenerated.
8
+ //
9
+ // Run: node --test tests/design_tokens_generated.mjs
10
+
11
+ import { strict as assert } from 'node:assert';
12
+ import { test } from 'node:test';
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { createRequire } from 'node:module';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const require = createRequire(import.meta.url);
19
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
20
+ const gen = require(path.join(ROOT, 'modules', 'ui-design', 'scripts', 'gen-design-tokens.js'));
21
+
22
+ test('the committed token file is exactly what the generator writes', () => {
23
+ const committed = fs.readFileSync(path.join(ROOT, 'config', 'design-tokens.neutral.json'), 'utf8');
24
+ assert.equal(committed, gen.build(),
25
+ 'config/design-tokens.neutral.json is stale or hand-edited: run node modules/ui-design/scripts/gen-design-tokens.js');
26
+ });
27
+
28
+ test('every scale, radius and motion value is the shared sheet\'s --ds-* value', () => {
29
+ const vars = gen.readSheetVars(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'public', 'tokens.css'), 'utf8'));
30
+ const tokens = JSON.parse(gen.build());
31
+ for (const group of ['scale', 'radius', 'motion']) {
32
+ for (const [key, prop] of Object.entries(gen.MAP[group])) {
33
+ assert.equal(tokens[group][key], vars[prop], `${group}.${key} must be ${prop}`);
34
+ }
35
+ }
36
+ assert.equal(tokens.scale.xs, '6px', 'the rhythm starts at the 6px hair step');
37
+ });
38
+
39
+ test('a token-sheet change reaches the generated file (the generator reads the sheet, not a copy)', () => {
40
+ const sheet = ':root { --ds-space-hair: 7px; --ds-space-xs: 10px; --ds-space-sm: 14px; --ds-space-md: 22px;'
41
+ + ' --ds-space-lg: 34px; --ds-space-xl: 52px; --ds-radius-tile: 10px; --ds-radius-panel: 16px;'
42
+ + ' --ds-radius-pill: 999px; --ds-dur-fast: 150ms; --ds-dur-base: 200ms; --ds-dur-arrive: 700ms;'
43
+ + ' --ds-ease: cubic-bezier(.2, .7, .3, 1); }';
44
+ const pack = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'branding.neutral.json'), 'utf8'));
45
+ assert.equal(JSON.parse(gen.build({ sheet, pack })).scale.xs, '7px');
46
+ });
47
+
48
+ test('a --ds-* step the mapping needs but the sheet lost is an error, not a silent gap', () => {
49
+ const pack = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'branding.neutral.json'), 'utf8'));
50
+ assert.throws(() => gen.build({ sheet: ':root { --ds-space-hair: 6px; }', pack }), /is not declared/);
51
+ });
52
+
53
+ test('only the base :root block is read: the dark and reduced-motion restatements do not leak in', () => {
54
+ const vars = gen.readSheetVars(':root { --ds-dur-fast: 150ms; } @media (prefers-reduced-motion: reduce) { :root { --ds-dur-fast: 0ms; } }');
55
+ assert.equal(vars['--ds-dur-fast'], '150ms');
56
+ });
@@ -42,6 +42,7 @@ const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
42
42
  const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
43
43
 
44
44
  const styleCss = read('style.css');
45
+ const controlsCss = read('controls.css');
45
46
  const workCss = read('work.css');
46
47
  const workHtml = read('work.html');
47
48
 
@@ -104,7 +105,8 @@ test('a prose link buys BOTH axes — padding for width, negative margin to give
104
105
  // ---- 3. the world's own bans ------------------------------------------------
105
106
 
106
107
  test('nothing on the board paints text white on the accent', () => {
107
- const claim = rule(styleCss, '.task__claim');
108
+ // task 1005084: the claim is the shared primary door, drawn in controls.css
109
+ const claim = controlsCss.replace(/\/\*[\s\S]*?\*\//g, '').match(/([^{}]*\.task__claim\b[^{}]*)\{([^{}]*background:[^{}]*)\}/)?.[2] ?? null;
108
110
  assert.ok(claim, '.task__claim has a rule');
109
111
  assert.match(claim, /background:\s*var\(--accent\)/, 'the claim button is the page\'s one accent fill');
110
112
  assert.match(claim, /color:\s*var\(--accent-on\)/,
@@ -116,7 +118,9 @@ test('nothing on the board paints text white on the accent', () => {
116
118
  });
117
119
 
118
120
  test('the shared task + bulk-toolbar rules live in style.css (the shared base), not the page-local work.css', () => {
119
- for (const sel of ['.task__age', '.task__age--stale', '.task__tag--newcomer', '.task__tag--mine', '.task__tag--rank', '.task__tag--rank-open', '.task__claim', '.task__touches', '.task', '.bulk-toolbar', '.bulk-toolbar__count', '.bulk-toolbar__actions', '.bulk-toolbar__btn', '.bulk-toolbar__clear']) {
121
+ // .task__claim left for the shared controls.css (task 1005084); work.css still must not restate it
122
+ assert.equal(rule(workCss, '.task__claim'), null, 'work.css must not restate .task__claim');
123
+ for (const sel of ['.task__age', '.task__age--stale', '.task__tag--newcomer', '.task__tag--mine', '.task__tag--rank', '.task__tag--rank-open', '.task__touches', '.task', '.bulk-toolbar', '.bulk-toolbar__count', '.bulk-toolbar__actions', '.bulk-toolbar__btn', '.bulk-toolbar__clear']) {
120
124
  assert.ok(rule(styleCss, sel), `style.css carries ${sel}, beside .task__tag`);
121
125
  assert.equal(rule(workCss, sel), null, `work.css must not restate ${sel}`);
122
126
  }
@@ -147,10 +147,12 @@ test('A: an older branding pack with no joinability falls back to the apply door
147
147
  });
148
148
 
149
149
  test('A: the quiet fill is a defined variant, not an inline colour', () => {
150
- const css = read('style.css');
151
- const i = css.indexOf('\n.landing__btn--quiet {');
152
- assert.ok(i > 0, 'style.css defines .landing__btn--quiet');
153
- const block = css.slice(i, css.indexOf('}', i));
150
+ // task 1005084: .landing__btn--quiet is the shared ghost in controls.css
151
+ const css = read('controls.css').replace(/\/\*[\s\S]*?\*\//g, '');
152
+ const m = css.match(/([^{}]*\.landing__btn--quiet[\s,][^{}]*)\{([^{}]*)\}/);
153
+ assert.ok(m, 'controls.css defines .landing__btn--quiet');
154
+ const block = m[2];
155
+ assert.match(block, /background:\s*transparent/, 'the quiet variant is the ghost');
154
156
  assert.deepEqual(block.match(/#[0-9a-f]{3,8}\b/gi) || [], [], 'the variant carries no literal colour');
155
157
  // Same geometry as the request button means it inherits .landing__btn's
156
158
  // min-height and padding rather than restating them.