@bongos/core 1.19.534 → 1.19.536
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.
- package/.bongos-core.json +74 -39
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +11 -0
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/api/openapi.json +186 -3
- package/docs/api-reference.md +10 -2
- package/docs/copy-inventory.md +64 -43
- package/docs/copy-registry.json +293 -62
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +8 -0
- package/modules/government/catalog.js +1 -0
- package/modules/government/migrations/government_010_project_settings_manage.sql +58 -0
- package/modules/hall-ui/public/hall-kit.js +93 -7
- package/modules/hall-ui/public/project-settings.css +90 -0
- package/modules/hall-ui/public/project-settings.html +68 -0
- package/modules/hall-ui/public/project-settings.js +249 -0
- package/modules/hall-ui/public/roadmap-timeline.js +105 -0
- package/modules/hall-ui/public/roadmap.html +7 -1
- package/modules/hall-ui/public/roadmap.js +16 -0
- package/modules/hall-ui/public/shell.js +3 -0
- package/modules/hall-ui/public/style.css +4 -2
- package/modules/hall-ui/public/watch.html +4 -0
- package/modules/hall-ui/public/watch.js +116 -0
- package/modules/lifecycle/goal-category.js +82 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/routes/goals.js +16 -0
- package/modules/lifecycle/routes/project-settings.js +263 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/hall-preview/refresh-fixtures.js +6 -0
- package/src/bongos/serve-internal.js +12 -1
- package/src/module-api.js +13 -1
- package/tests/hall_kit_charts.mjs +173 -0
- package/tests/hall_nav.mjs +3 -1
- package/tests/hall_oversight_world.mjs +159 -0
- package/tests/hall_page_gate_map.mjs +4 -0
- package/tests/hall_reading_world.mjs +153 -0
- package/tests/project_settings_page.mjs +337 -0
|
@@ -1517,5 +1517,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1517
1517
|
joinabilityMode did at 1.19.374.
|
|
1518
1518
|
1.19.534 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1519
1519
|
landed since 1.19.533 with no explicit bump. run 33885355024. (task 1002620)
|
|
1520
|
+
1.19.535 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1521
|
+
landed since 1.19.534 with no explicit bump. run 33886686350. (task 1002620)
|
|
1522
|
+
1.19.536 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1523
|
+
landed since 1.19.535 with no explicit bump. run 33888048777. (task 1002620)
|
|
1520
1524
|
---------------------------------------------------------------------------
|
|
1521
1525
|
```
|
|
@@ -633,6 +633,12 @@ function createClient(opts = {}) {
|
|
|
633
633
|
// GET /profiles/{handle} — rank: public — GET /profiles/:handle
|
|
634
634
|
getProfilesHandle: (args) => request("GET", "/profiles/{handle}", { hasBody: false }, args),
|
|
635
635
|
},
|
|
636
|
+
"projectSettings": {
|
|
637
|
+
// GET /project-settings — rank: metic+archon — GET /project-settings
|
|
638
|
+
getProjectSettings: (args) => request("GET", "/project-settings", { hasBody: false }, args),
|
|
639
|
+
// PATCH /project-settings/rot-days — rank: metic+archon — PATCH /project-settings/rot-days
|
|
640
|
+
patchProjectSettingsRotDays: (args) => request("PATCH", "/project-settings/rot-days", { hasBody: true }, args),
|
|
641
|
+
},
|
|
636
642
|
"projects": {
|
|
637
643
|
// GET /projects — rank: metic+archon — GET /projects
|
|
638
644
|
getProjects: (args) => request("GET", "/projects", { hasBody: false }, args),
|
|
@@ -894,6 +900,8 @@ function createClient(opts = {}) {
|
|
|
894
900
|
getWorkCategories: (args) => request("GET", "/work-categories", { hasBody: false }, args),
|
|
895
901
|
// POST /work-categories — rank: metic+archon — POST /work-categories
|
|
896
902
|
postWorkCategories: (args) => request("POST", "/work-categories", { hasBody: true }, args),
|
|
903
|
+
// PATCH /work-categories/{id} — rank: metic+archon — PATCH /work-categories/:id
|
|
904
|
+
patchWorkCategoriesId: (args) => request("PATCH", "/work-categories/{id}", { hasBody: true }, args),
|
|
897
905
|
},
|
|
898
906
|
};
|
|
899
907
|
}
|
|
@@ -200,6 +200,7 @@ const PERMISSIONS = [
|
|
|
200
200
|
// personally opted in (task 1003177, ADR 0187 §5), so this atom cannot put
|
|
201
201
|
// anyone in a room with a stranger on its own.
|
|
202
202
|
{ key: 'mingle.manage', system: false, floor: 'metic', guards: 'PUT /mingle/project (the project half of mingle consent), POST /mingle/round (run a pairing round) — task 1003177, goal 1000075' },
|
|
203
|
+
{ key: 'project.settings.manage', system: false, floor: 'metic', guards: "PATCH /project-settings/* — the project's own live knobs (the rot timer today) and the work-category set's shape (PATCH /work-categories/:id). Metic, matching goal.create's reasoning that extending the project's vocabulary is a planning act rather than an administrative one — task 1003280, goal 1000072" },
|
|
203
204
|
{ key: 'project.curate', system: false, floor: 'metic', guards: 'platform-identity: GET/POST /projects, /projects/invite, PATCH /projects/:id/featured, GET /scouting (ADR 0157, was archon)' },
|
|
204
205
|
{ key: 'override_request.decide', system: false, floor: 'metic', guards: 'GET /override-requests, POST .../:id/decide (ADR 0157, was archon)' },
|
|
205
206
|
{ key: 'claim.release.any', system: false, floor: 'metic', scope: 'any', resource: 'claim', guards: 'POST /claims/:id/release-on-behalf (ADR 0157, was archon)' },
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
-- government_010_project_settings_manage.sql — grant the new `project.settings.manage`
|
|
2
|
+
-- atom to the Metic and Archon rank-roles (task 1003280, goal 1000072, criterion
|
|
3
|
+
-- legibility-project-settings).
|
|
4
|
+
--
|
|
5
|
+
-- The project-settings page carries the five fundamentals of a project, and TWO of
|
|
6
|
+
-- them are live knobs a person may turn: the rot timer (project_settings, migration
|
|
7
|
+
-- core_231) and the shape of the work-category set (work_categories, core_226). Both
|
|
8
|
+
-- writes need a gate, and BV1.R105 says a modules/*/routes gate is a permission atom
|
|
9
|
+
-- and never a rank — tests/government_require_permission.mjs fails the build on a new
|
|
10
|
+
-- `requireRank`.
|
|
11
|
+
--
|
|
12
|
+
-- FLOOR `metic`, by the `goal.create` analogue rather than archon. `goal.create`
|
|
13
|
+
-- already gates POST /work-categories on the reasoning that "extending the project's
|
|
14
|
+
-- vocabulary is a planning act", and this atom governs the same vocabulary's SHAPE
|
|
15
|
+
-- (label, order, whether it is still offered) plus one reporting timer. What makes
|
|
16
|
+
-- that safe is the ADR 0192 wall the category set already stands behind: a category
|
|
17
|
+
-- is ADVISORY — no gate, route guard, rank check, feed filter or dependency rule may
|
|
18
|
+
-- read it (scripts/gds/category-advisory-guard.js fails the build if that changes) —
|
|
19
|
+
-- so this atom cannot alter who may do what. The rot timer is the reporting half of
|
|
20
|
+
-- ADR 0226's asymmetry: it only ever ASKS a human, and the timer that ENFORCES
|
|
21
|
+
-- (src/stale-timer.js, which auto-releases claims) is deliberately NOT reachable
|
|
22
|
+
-- from here.
|
|
23
|
+
--
|
|
24
|
+
-- A BRAND-NEW key: no rank holds it yet, so BOTH rungs the floor implies are granted
|
|
25
|
+
-- here. RANK_SEED is a cumulative superset — floor `metic` means {metic, archon} —
|
|
26
|
+
-- and the drift guard (tests/government_seed.mjs) compares the UNION of the run-once
|
|
27
|
+
-- seed plus every later grant migration against that floor-derived seed, per rank.
|
|
28
|
+
-- Minting the catalog row WITHOUT this migration is exactly what that guard caught
|
|
29
|
+
-- on this task, as it did on task 1003177.
|
|
30
|
+
--
|
|
31
|
+
-- COPIES government_009_mingle_manage.sql, NOT a `governance_*` file. Those predate
|
|
32
|
+
-- government_001_rename_from_governance, which renamed `governance_role_permissions`
|
|
33
|
+
-- → `government_rank_permissions` and `role_key` → `rank_key` with no compatibility
|
|
34
|
+
-- shim, so copying one writes to a table that no longer exists and errors at apply
|
|
35
|
+
-- time on any fresh install.
|
|
36
|
+
--
|
|
37
|
+
-- Additive only: one new permission key granted to two ranks; nothing is revoked.
|
|
38
|
+
-- Idempotent (ON CONFLICT DO NOTHING); forward-safe (INSERT only).
|
|
39
|
+
--
|
|
40
|
+
-- One block PER RANK, in the government_004/009 shape: the drift guard scans for
|
|
41
|
+
-- `SELECT '<rank>', unnest(ARRAY[…])` literally, so a combined
|
|
42
|
+
-- `unnest(ARRAY['metic','archon']), 'key'` form parses as zero grants and reds it.
|
|
43
|
+
|
|
44
|
+
BEGIN;
|
|
45
|
+
|
|
46
|
+
INSERT INTO government_rank_permissions (rank_key, permission_key)
|
|
47
|
+
SELECT 'metic', unnest(ARRAY[
|
|
48
|
+
'project.settings.manage'
|
|
49
|
+
])
|
|
50
|
+
ON CONFLICT (rank_key, permission_key) DO NOTHING;
|
|
51
|
+
|
|
52
|
+
INSERT INTO government_rank_permissions (rank_key, permission_key)
|
|
53
|
+
SELECT 'archon', unnest(ARRAY[
|
|
54
|
+
'project.settings.manage'
|
|
55
|
+
])
|
|
56
|
+
ON CONFLICT (rank_key, permission_key) DO NOTHING;
|
|
57
|
+
|
|
58
|
+
COMMIT;
|
|
@@ -737,13 +737,25 @@
|
|
|
737
737
|
+ '</svg>';
|
|
738
738
|
}
|
|
739
739
|
|
|
740
|
-
// Horizontal labelled bars. rows: [{ label, value, valueLabel }].
|
|
740
|
+
// Horizontal labelled bars. rows: [{ label, value, start, valueLabel }].
|
|
741
741
|
// opts: { max, tone: 'accent'|'neutral', labelWidth, valueWidth, barHeight,
|
|
742
742
|
// pill, ariaLabel, list }
|
|
743
743
|
//
|
|
744
744
|
// `max` is the caller's when several series must share one scale; otherwise
|
|
745
745
|
// the largest value here. It floors at 1 so an all-zero set draws empty
|
|
746
746
|
// tracks instead of dividing by zero.
|
|
747
|
+
//
|
|
748
|
+
// A row's optional `start` OFFSETS its bar instead of beginning it at the
|
|
749
|
+
// track's left edge, which turns the same primitive into a SPAN series — a
|
|
750
|
+
// timeline. It arrived with the roadmap's versions (task 1003447): each one
|
|
751
|
+
// holds a `started_at` and a `shipped_at`, and the series those two dates
|
|
752
|
+
// support is "when did each version run", every bar on one shared axis. That
|
|
753
|
+
// is a bar with a left offset and nothing else, so it is an OPTION here
|
|
754
|
+
// rather than a second class family — the same seam every other difference
|
|
755
|
+
// between call sites rides. `start` is in the SAME units as `value` and
|
|
756
|
+
// measured from the axis origin, so the kit still never learns what a date
|
|
757
|
+
// is: a caller passes `max` as the axis span and `start` as the offset into
|
|
758
|
+
// it, exactly as it already passes x0/x1/yMax to the plot.
|
|
747
759
|
function barSeriesHtml(rows, opts = {}) {
|
|
748
760
|
const list = (rows || []).filter((r) => r && r.label !== undefined && r.label !== null);
|
|
749
761
|
if (!list.length) return '';
|
|
@@ -775,14 +787,34 @@
|
|
|
775
787
|
+ (opts.ariaLabel ? ` aria-label="${esc(opts.ariaLabel)}"` : '')
|
|
776
788
|
+ '>'
|
|
777
789
|
+ list.map((r, i) => {
|
|
778
|
-
|
|
790
|
+
// A POSITIVE value always draws at least a sliver. Rounding alone sends
|
|
791
|
+
// anything under half a percent of the max to `width:0%`, which reads as
|
|
792
|
+
// "none" — the one thing it is not. Zero stays zero: an empty track is
|
|
793
|
+
// the honest picture of nothing, and the column series makes the
|
|
794
|
+
// opposite choice for the opposite reason (a zero bucket there is a week
|
|
795
|
+
// that happened). Load-bearing for a span: a version that started and
|
|
796
|
+
// shipped inside one day is a real span half a pixel wide.
|
|
797
|
+
const raw = Math.round((vals[i] / max) * 100);
|
|
798
|
+
const w = vals[i] > 0 ? Math.max(1, raw) : raw;
|
|
799
|
+
// The offset takes the SAME floor, and for the same reason: a span that
|
|
800
|
+
// begins just after the axis origin but rounds to 0% draws flush against
|
|
801
|
+
// the track edge, which is the picture of "started first" — and on a
|
|
802
|
+
// timeline that is a claim about ordering, not a rounding artefact.
|
|
803
|
+
const start = Number(r.start);
|
|
804
|
+
const rawOff = Number.isFinite(start) && start > 0 ? Math.round((start / max) * 100) : 0;
|
|
805
|
+
const off = Number.isFinite(start) && start > 0 ? Math.max(1, rawOff) : 0;
|
|
806
|
+
// Offset by MARGIN, not `left`: the fill is an ordinary block inside the
|
|
807
|
+
// track, so absolute positioning would need a positioned ancestor the
|
|
808
|
+
// track does not declare. Omitted entirely at zero, so every existing
|
|
809
|
+
// caller's markup is byte-identical to what it was.
|
|
810
|
+
const geom = off > 0 ? `margin-left:${off}%;width:${w}%` : `width:${w}%`;
|
|
779
811
|
const val = r.valueLabel === undefined || r.valueLabel === null
|
|
780
812
|
? String(vals[i])
|
|
781
813
|
: String(r.valueLabel);
|
|
782
814
|
return `<div class="kit-bars__row"${rowRole}>`
|
|
783
815
|
+ `<span class="kit-bars__label">${esc(r.label)}</span>`
|
|
784
816
|
+ `<span class="kit-bars__track" aria-hidden="true">`
|
|
785
|
-
+ `<span class="kit-bars__fill" style="
|
|
817
|
+
+ `<span class="kit-bars__fill" style="${geom}"></span></span>`
|
|
786
818
|
+ `<span class="kit-bars__val">${esc(val)}</span>`
|
|
787
819
|
+ '</div>';
|
|
788
820
|
}).join('')
|
|
@@ -818,10 +850,60 @@
|
|
|
818
850
|
return { width, height, pad, base: height - pad.b };
|
|
819
851
|
}
|
|
820
852
|
|
|
853
|
+
// The VERTICAL window a series should be drawn in — the companion to
|
|
854
|
+
// timeStepGeometry's yMin/yMax, and geometry rather than date semantics, which
|
|
855
|
+
// is why it lives here rather than at the call site (task 1003447).
|
|
856
|
+
//
|
|
857
|
+
// `zeroBased` (the default) is right whenever zero is a state the series can
|
|
858
|
+
// actually reach: a count of dead exports sitting at zero for a week is the
|
|
859
|
+
// reading, and lifting the floor off it would invent a change. Pass false for
|
|
860
|
+
// a LEVEL — repo-health's total_loc — where zero is unreachable and a zero
|
|
861
|
+
// floor squashes a month of real movement into a flat band across the top.
|
|
862
|
+
// A fitted floor magnifies every wiggle, so a caller that passes false owes
|
|
863
|
+
// the reader the range in words somewhere on the page.
|
|
864
|
+
//
|
|
865
|
+
// `air` is the fraction of the range left as breathing room (12% by default)
|
|
866
|
+
// so the line neither rides the top edge nor welds itself to the axis. A FLAT
|
|
867
|
+
// series has no range to take a fraction of, so the air falls back to the
|
|
868
|
+
// value's own magnitude — and, for a series flat at zero, to 1, which leaves
|
|
869
|
+
// it drawn honestly along the axis.
|
|
870
|
+
//
|
|
871
|
+
// A MISSING entry is dropped, not read as zero: a null from a snapshot that
|
|
872
|
+
// predates a column would otherwise drag `lo` to zero and draw a cliff nothing
|
|
873
|
+
// recorded. Note this is the one place in the kit that must NOT go through
|
|
874
|
+
// `chartNums` — that maps a null to 0 on purpose, because a gap-filled DAILY
|
|
875
|
+
// feed really does mean "nothing happened that day". Here a null means "not
|
|
876
|
+
// measured", and the two are opposite readings of the same byte. `Number(null)`
|
|
877
|
+
// and `Number('')` are both a finite 0, so the emptiness has to be tested for
|
|
878
|
+
// before the coercion, never after it.
|
|
879
|
+
function plotWindow(values, opts = {}) {
|
|
880
|
+
const nums = (values || [])
|
|
881
|
+
.filter((v) => v !== null && v !== undefined && v !== '')
|
|
882
|
+
.map(Number)
|
|
883
|
+
.filter((n) => Number.isFinite(n));
|
|
884
|
+
if (!nums.length) return { lo: 0, hi: 0, yMin: 0, yMax: 1, n: 0 };
|
|
885
|
+
const lo = Math.min(...nums);
|
|
886
|
+
const hi = Math.max(...nums);
|
|
887
|
+
const frac = Number.isFinite(Number(opts.air)) ? Number(opts.air) : 0.12;
|
|
888
|
+
const air = ((hi - lo) || Math.abs(hi) || 1) * frac;
|
|
889
|
+
const zeroBased = opts.zeroBased !== false;
|
|
890
|
+
return { lo, hi, yMin: zeroBased ? 0 : lo - air, yMax: hi + air, n: nums.length };
|
|
891
|
+
}
|
|
892
|
+
|
|
821
893
|
// A STEP line over a time axis: the value holds until the next recorded
|
|
822
894
|
// point, then rises — which is what a cumulative "how much by now" series
|
|
823
895
|
// means, and why this is not a straight polyline between samples.
|
|
824
896
|
// points: [{ x, value }] in data space, sorted. Returns { d, area, xs }.
|
|
897
|
+
//
|
|
898
|
+
// `yMin` (default 0) is the value the AXIS sits at, and it exists because the
|
|
899
|
+
// second kind of series this plot draws is not cumulative. A cumulative line
|
|
900
|
+
// starts at nothing and every reading is "how much by now", so zero is the
|
|
901
|
+
// only honest floor. A LEVEL — repo-health's total_loc, task 1003447 — is a
|
|
902
|
+
// large number that moves a little: on a zero floor its whole month of change
|
|
903
|
+
// is a flat band across the top, and the chart says nothing the headline
|
|
904
|
+
// figure did not. A caller that fits the floor to the data MUST say so on the
|
|
905
|
+
// page (watch.js puts the range in the figure's foot), because a non-zero
|
|
906
|
+
// baseline silently multiplies how big every wiggle looks.
|
|
825
907
|
function timeStepGeometry(points, opts = {}) {
|
|
826
908
|
const { width, height, pad, base } = plotBox(opts);
|
|
827
909
|
const list = points || [];
|
|
@@ -829,11 +911,14 @@
|
|
|
829
911
|
const x0 = Number(opts.x0 ?? list[0].x);
|
|
830
912
|
const x1 = Number(opts.x1 ?? list[list.length - 1].x);
|
|
831
913
|
const last = list[list.length - 1];
|
|
832
|
-
// A span of 0 (one point, no window) would divide by zero;
|
|
914
|
+
// A span of 0 (one point, no window) would divide by zero; a flat series
|
|
915
|
+
// whose yMin and yMax are equal likewise.
|
|
833
916
|
const spanX = (x1 - x0) || 1;
|
|
834
|
-
const
|
|
917
|
+
const yMin = Number.isFinite(Number(opts.yMin)) ? Number(opts.yMin) : 0;
|
|
918
|
+
const yMax = Number(opts.yMax ?? last.value);
|
|
919
|
+
const spanY = ((Number.isFinite(yMax) ? yMax : 0) - yMin) || 1;
|
|
835
920
|
const px = (t) => pad.l + ((t - x0) / spanX) * (width - pad.l - pad.r);
|
|
836
|
-
const py = (v) => height - pad.b - (v /
|
|
921
|
+
const py = (v) => height - pad.b - ((v - yMin) / spanY) * (height - pad.t - pad.b);
|
|
837
922
|
let d = `M${px(list[0].x).toFixed(1)},${py(list[0].value).toFixed(1)}`;
|
|
838
923
|
list.forEach((p, i) => {
|
|
839
924
|
if (i === 0) return;
|
|
@@ -961,7 +1046,8 @@
|
|
|
961
1046
|
plotSvgHtml, plotValueLabelHtml, timeStepSeriesHtml, timeStepGeometry, columnSeriesHtml,
|
|
962
1047
|
// plotBox is exported so a caller mapping pointer x back into viewBox
|
|
963
1048
|
// space reads the plot's width instead of re-declaring the number.
|
|
964
|
-
|
|
1049
|
+
// plotWindow fits the vertical axis to a level series (task 1003447).
|
|
1050
|
+
plotBox, plotWindow,
|
|
965
1051
|
// states
|
|
966
1052
|
emptyStateHtml, skeletonRows, collapsibleCard,
|
|
967
1053
|
// ship-time task visual (task 1003109)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/* project-settings.css — the five-fundamentals page (task 1003280, goal 1000072).
|
|
2
|
+
The ROW is oversight.css's .ov-row and the panel is style.css's .scroll; what
|
|
3
|
+
lives here is only what those two do not already say: the state pill that marks a
|
|
4
|
+
row unwritable, the small control cluster a knob row carries, and the provenance
|
|
5
|
+
foot that names the file a value resolves from. */
|
|
6
|
+
|
|
7
|
+
/* The provenance line. Every row has one — it is the point of the page — so it is
|
|
8
|
+
quiet by default and never competes with the value above it. */
|
|
9
|
+
.ps-prov {
|
|
10
|
+
margin: 6px 0 0;
|
|
11
|
+
font-size: 12px;
|
|
12
|
+
line-height: 1.5;
|
|
13
|
+
color: var(--ink-soft);
|
|
14
|
+
}
|
|
15
|
+
.ps-prov code {
|
|
16
|
+
font-size: 11px;
|
|
17
|
+
overflow-wrap: anywhere; /* a table[key] path must wrap on a phone, never widen the page */
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* The state pill: what kind of value this is. A reader must be able to tell "you
|
|
21
|
+
cannot type here" from "this is empty" at a glance, which is why absent and
|
|
22
|
+
not-yet-real do not share a colour with live. */
|
|
23
|
+
.ps-pill {
|
|
24
|
+
display: inline-block;
|
|
25
|
+
padding: 1px 8px;
|
|
26
|
+
border-radius: 999px;
|
|
27
|
+
font-size: 11px;
|
|
28
|
+
font-weight: 600;
|
|
29
|
+
letter-spacing: 0.01em;
|
|
30
|
+
border: 1px solid currentColor;
|
|
31
|
+
vertical-align: middle;
|
|
32
|
+
}
|
|
33
|
+
.ps-pill--live { color: var(--ok); }
|
|
34
|
+
.ps-pill--readonly { color: var(--ink-soft); }
|
|
35
|
+
.ps-pill--absent { color: var(--warn); }
|
|
36
|
+
|
|
37
|
+
/* A knob's controls. Wraps rather than widening: the page never scrolls sideways
|
|
38
|
+
(hall-ui/CLAUDE.md), and this cluster is the only thing on the page wide enough
|
|
39
|
+
to threaten that at 390px. */
|
|
40
|
+
.ps-controls {
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-wrap: wrap;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: 8px;
|
|
45
|
+
margin-top: 10px;
|
|
46
|
+
}
|
|
47
|
+
.ps-controls input[type="number"] { width: 7em; }
|
|
48
|
+
.ps-controls input[type="text"] { min-width: 0; flex: 1 1 12em; }
|
|
49
|
+
|
|
50
|
+
/* The category list inside its row. A plain list, not a nested ledger: it is edited
|
|
51
|
+
in place and a table would put a horizontal scrollport inside a card that already
|
|
52
|
+
is one. */
|
|
53
|
+
.ps-cats { margin: 10px 0 0; padding: 0; list-style: none; }
|
|
54
|
+
.ps-cat {
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-wrap: wrap;
|
|
57
|
+
align-items: center;
|
|
58
|
+
gap: 8px;
|
|
59
|
+
padding: 8px 0;
|
|
60
|
+
border-top: var(--line) solid var(--rule-soft);
|
|
61
|
+
}
|
|
62
|
+
.ps-cat--off { opacity: 0.6; }
|
|
63
|
+
.ps-cat__slug {
|
|
64
|
+
font-size: 11px;
|
|
65
|
+
color: var(--ink-soft);
|
|
66
|
+
flex: 0 0 auto;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* The invalid-value note: a configured value the resolver could not use. It renders
|
|
70
|
+
on the screen of the person who can fix it rather than 500-ing the page — the
|
|
71
|
+
reporting half of ADR 0226's asymmetric fail posture. */
|
|
72
|
+
.ps-invalid {
|
|
73
|
+
margin: 8px 0 0;
|
|
74
|
+
padding: 8px 10px;
|
|
75
|
+
border-radius: 6px;
|
|
76
|
+
font-size: 12px;
|
|
77
|
+
color: var(--warn);
|
|
78
|
+
border: 1px solid currentColor;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* The sibling note on the stale-timer row — the OTHER timer, the one that enforces.
|
|
82
|
+
Indented and quiet: it is context that stops a misreading, not a second setting. */
|
|
83
|
+
.ps-sibling {
|
|
84
|
+
margin: 10px 0 0;
|
|
85
|
+
padding-left: 10px;
|
|
86
|
+
border-left: var(--line) solid var(--rule);
|
|
87
|
+
font-size: 12px;
|
|
88
|
+
color: var(--ink-soft);
|
|
89
|
+
}
|
|
90
|
+
.ps-sibling strong { color: inherit; font-weight: 600; }
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Project settings — {{worldName}}</title>
|
|
7
|
+
<meta name="description" content="The fundamentals of this project in one place — the stale timer, the category set, how credits are calculated, whether equity is real, and what share of upmarket revenue is paid on credits.">
|
|
8
|
+
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
+
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
12
|
+
<link rel="stylesheet" href="/builders/oversight.css">
|
|
13
|
+
<link rel="stylesheet" href="/builders/project-settings.css">
|
|
14
|
+
</head>
|
|
15
|
+
<body data-page="project-settings" data-page-title="Project settings">
|
|
16
|
+
|
|
17
|
+
<div class="app">
|
|
18
|
+
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
19
|
+
<div class="app-scrim" id="app-scrim" hidden></div>
|
|
20
|
+
<div class="app-body">
|
|
21
|
+
<header class="app-topbar" id="app-topbar"></header>
|
|
22
|
+
<div class="app-content">
|
|
23
|
+
<!-- The LABEL voice (the page-head--board idiom, task 1003307): on a working
|
|
24
|
+
surface the page's own name is the least interesting thing on it. -->
|
|
25
|
+
<div class="page-head page-head--board">
|
|
26
|
+
<h1 class="page-head__h">Project settings</h1>
|
|
27
|
+
<p class="page-head__sub" id="ps-sub">Loading…</p>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<main id="hall-main">
|
|
31
|
+
<!-- The sealed state is a panel with a heading, not a bare line on the ground
|
|
32
|
+
(task 1003469). The page gate is the real wall; this is what a reader who
|
|
33
|
+
followed a link sees instead of an empty page. -->
|
|
34
|
+
<section class="scroll" id="ps-sealed" aria-labelledby="ps-sealed-h" hidden>
|
|
35
|
+
<h2 class="scroll__h" id="ps-sealed-h">Restricted</h2>
|
|
36
|
+
<p class="scroll__lede" id="ps-sealed-body">This page opens at a higher rank.</p>
|
|
37
|
+
</section>
|
|
38
|
+
|
|
39
|
+
<section class="scroll" id="ps-body" aria-labelledby="ps-body-h" hidden>
|
|
40
|
+
<h2 class="scroll__h" id="ps-body-h">The fundamentals</h2>
|
|
41
|
+
<p class="scroll__lede">
|
|
42
|
+
Five things every project settles. Two of them are knobs you can turn here;
|
|
43
|
+
the other three are decided in committed configuration, and each says which
|
|
44
|
+
file decides it.
|
|
45
|
+
</p>
|
|
46
|
+
<!-- One mount, five rows, written by project-settings.js. The rows are NOT
|
|
47
|
+
authored in this file: each carries a value plus the provenance the
|
|
48
|
+
server resolved it through, and markup that could drift from that is the
|
|
49
|
+
thing this page exists to end. -->
|
|
50
|
+
<div id="ps-rows"><span class="kit-skel"></span></div>
|
|
51
|
+
</section>
|
|
52
|
+
</main>
|
|
53
|
+
|
|
54
|
+
<footer class="app-foot" id="app-foot"></footer>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
60
|
+
|
|
61
|
+
<script src="/builders/shell.js?v=2026-07-02-shell1"></script>
|
|
62
|
+
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
63
|
+
<script src="/builders/palette.js" defer></script>
|
|
64
|
+
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
65
|
+
<script src="/builders/hall-kit.js"></script>
|
|
66
|
+
<script src="/builders/project-settings.js?v=2026-09-04-ps1"></script>
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|