@bongos/core 1.19.544 → 1.19.546
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 +87 -42
- package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +115 -0
- package/docs/copy-inventory.md +42 -18
- package/docs/copy-registry.json +271 -25
- package/docs/module-api-changelog.md +4 -0
- package/docs/research/task-network-map/real-a-every-task.png +0 -0
- package/docs/research/task-network-map/real-b-dependency-forest.png +0 -0
- package/docs/research/task-network-map/real-c-goals.png +0 -0
- package/docs/research/task-network-map/real-d-joined-by-goal.png +0 -0
- package/modules/hall-ui/public/hall-kit.js +76 -41
- package/modules/hall-ui/public/profile-charts.js +8 -2
- package/modules/hall-ui/public/shell.js +30 -1
- package/modules/hall-ui/public/studio.css +268 -0
- package/modules/hall-ui/public/studio.html +74 -0
- package/modules/hall-ui/public/studio.js +213 -0
- package/modules/hall-ui/public/studio.states.json +54 -0
- package/modules/hall-ui/public/style.css +9 -0
- package/modules/ui-design/skills/brandkit/SKILL.md +5 -1
- package/modules/ui-design/skills/design-taste-frontend/SKILL.md +5 -1
- package/modules/ui-design/skills/design-taste-frontend-v1/SKILL.md +5 -1
- package/modules/ui-design/skills/gpt-taste/SKILL.md +5 -1
- package/modules/ui-design/skills/high-end-visual-design/SKILL.md +5 -1
- package/modules/ui-design/skills/image-to-code/SKILL.md +5 -1
- package/modules/ui-design/skills/imagegen-frontend-mobile/SKILL.md +5 -1
- package/modules/ui-design/skills/imagegen-frontend-web/SKILL.md +5 -1
- package/modules/ui-design/skills/impeccable/SKILL.md +5 -1
- package/modules/ui-design/skills/industrial-brutalist-ui/SKILL.md +5 -1
- package/modules/ui-design/skills/minimalist-ui/SKILL.md +5 -1
- package/modules/ui-design/skills/redesign-existing-projects/SKILL.md +5 -1
- package/modules/ui-design/skills/stitch-design-taste/SKILL.md +5 -1
- package/modules/ui-design/skills/style/SKILL.md +5 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +10 -3
- package/scripts/gds/skill-lint.js +43 -9
- package/scripts/gds/spike-task-map.js +178 -9
- package/src/bongos/serve-internal.js +1 -1
- package/src/module-api.js +1 -1
- package/tests/hall_kit_charts.mjs +59 -0
- package/tests/hall_nav.mjs +6 -1
- package/tests/hall_page_gate_map.mjs +5 -0
- package/tests/hall_studio_world.mjs +143 -0
- package/tests/skill_lint.mjs +51 -2
- package/tests/spike_task_map.mjs +0 -0
- package/tests/ui_design_skills.mjs +12 -2
|
@@ -1033,69 +1033,100 @@
|
|
|
1033
1033
|
+ '</div>';
|
|
1034
1034
|
}
|
|
1035
1035
|
|
|
1036
|
-
//
|
|
1037
|
-
// one cell per calendar day, BANDED by count rather than scaled, its reading a
|
|
1038
|
-
// per-cell title (plus an aria summary on the whole grid), not a crosshair. The
|
|
1039
|
-
// hall's THIRD chart family (task 1003572, criterion legibility-series-are-drawn):
|
|
1040
|
-
// OTBKit's other two shapes are a horizontal bar series and a plot on a time axis;
|
|
1041
|
-
// a 2-D year grid is neither, so it earns its own primitive rather than a hand-roll
|
|
1042
|
-
// at the page (modules/hall-ui/CLAUDE.md; tests/hall_audit.mjs section 6b).
|
|
1036
|
+
// The calendar's date arithmetic, shared by the window and the grid.
|
|
1043
1037
|
//
|
|
1044
|
-
// THE
|
|
1045
|
-
//
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1038
|
+
// THE TIMEZONE TRAP: local_date is already the builder's own day, so the grid is built
|
|
1039
|
+
// by ORDINAL arithmetic on the 'YYYY-MM-DD' parts (Date.UTC on the parsed integers,
|
|
1040
|
+
// floored to an epoch day) — never `new Date('2026-09-03')`, which is UTC-midnight and
|
|
1041
|
+
// would shift a cell a day west of Greenwich. Same epoch-day floor cumulateByDay uses,
|
|
1042
|
+
// so no cell moves relative to the other charts. calDow is Sunday-indexed: epoch day 0
|
|
1043
|
+
// (1970-01-01) is a Thursday, hence the +4.
|
|
1044
|
+
const calYmdToOrd = (ymd) => {
|
|
1045
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd);
|
|
1046
|
+
if (!m) return null;
|
|
1047
|
+
return Math.floor(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) / 86400000);
|
|
1048
|
+
};
|
|
1049
|
+
const calOrdToYmd = (ord) => {
|
|
1050
|
+
const dt = new Date(ord * 86400000);
|
|
1051
|
+
return `${dt.getUTCFullYear()}-${String(dt.getUTCMonth() + 1).padStart(2, '0')}-${String(dt.getUTCDate()).padStart(2, '0')}`;
|
|
1052
|
+
};
|
|
1053
|
+
const calDow = (o) => (((o + 4) % 7) + 7) % 7;
|
|
1054
|
+
|
|
1055
|
+
// calendarWindow(days, opts) — the DRAWN window and what falls inside it, resolved
|
|
1056
|
+
// once so the grid and whatever caption sits beside it can never disagree (task
|
|
1057
|
+
// 1003583). Two bugs came from computing this twice: the aria summary tallied every
|
|
1058
|
+
// row handed in rather than the cells drawn, and profile-charts.js counted
|
|
1059
|
+
// `days.filter(d => d.count > 0)` over the whole bounded array — so "N days shipped
|
|
1060
|
+
// this year" could name a day sitting outside the grid's first partial week.
|
|
1054
1061
|
//
|
|
1055
|
-
//
|
|
1056
|
-
//
|
|
1057
|
-
|
|
1058
|
-
// is UTC-midnight and would shift a cell a day west of Greenwich. Same epoch-day
|
|
1059
|
-
// floor cumulateByDay uses, so no cell moves relative to the other charts.
|
|
1060
|
-
function calendarGridHtml(days, opts = {}) {
|
|
1062
|
+
// Returns null when there is nothing real to draw, so callers keep their own empty
|
|
1063
|
+
// state rather than rendering an empty grid.
|
|
1064
|
+
function calendarWindow(days, opts = {}) {
|
|
1061
1065
|
const list = (days || []).filter((d) => d && typeof d.local_date === 'string');
|
|
1062
|
-
const ymdToOrd = (ymd) => {
|
|
1063
|
-
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd);
|
|
1064
|
-
if (!m) return null;
|
|
1065
|
-
return Math.floor(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) / 86400000);
|
|
1066
|
-
};
|
|
1067
|
-
const ordToYmd = (ord) => {
|
|
1068
|
-
const dt = new Date(ord * 86400000);
|
|
1069
|
-
return `${dt.getUTCFullYear()}-${String(dt.getUTCMonth() + 1).padStart(2, '0')}-${String(dt.getUTCDate()).padStart(2, '0')}`;
|
|
1070
|
-
};
|
|
1071
1066
|
const counts = new Map();
|
|
1072
1067
|
let maxOrd = null;
|
|
1073
|
-
let activeDays = 0;
|
|
1074
|
-
let totalShips = 0;
|
|
1075
1068
|
for (const d of list) {
|
|
1076
|
-
const o =
|
|
1069
|
+
const o = calYmdToOrd(d.local_date);
|
|
1077
1070
|
if (o == null) continue;
|
|
1078
1071
|
const c = Math.max(0, Math.round(Number(d.count) || 0));
|
|
1079
1072
|
if (c <= 0) continue;
|
|
1080
1073
|
counts.set(o, (counts.get(o) || 0) + c);
|
|
1081
1074
|
if (maxOrd == null || o > maxOrd) maxOrd = o;
|
|
1082
1075
|
}
|
|
1083
|
-
if (maxOrd == null) return
|
|
1084
|
-
for (const [, c] of counts) { activeDays += 1; totalShips += c; }
|
|
1076
|
+
if (maxOrd == null) return null;
|
|
1085
1077
|
|
|
1086
1078
|
// The grid ENDS at `today` (the builder's, when given) so a quiet recent week still
|
|
1087
1079
|
// reads as time that passed, not a truncated chart; falls back to the last ship day.
|
|
1088
|
-
const todayOrd = (opts.today &&
|
|
1080
|
+
const todayOrd = (opts.today && calYmdToOrd(opts.today)) || maxOrd;
|
|
1089
1081
|
const endOrd = Math.max(todayOrd, maxOrd);
|
|
1090
1082
|
const weeks = Math.max(1, Math.min(53, Math.round(Number(opts.weeks)) || 53));
|
|
1091
1083
|
|
|
1092
1084
|
// Columns are weeks, rows Sun..Sat. Epoch day 0 (1970-01-01) is a Thursday, so the
|
|
1093
1085
|
// Sunday-indexed weekday is (ord + 4) mod 7. The last column is the week holding
|
|
1094
1086
|
// endOrd; walk back `weeks` Sundays to the first cell.
|
|
1095
|
-
const
|
|
1096
|
-
const endSun = endOrd - dow(endOrd);
|
|
1087
|
+
const endSun = endOrd - calDow(endOrd);
|
|
1097
1088
|
const startSun = endSun - (weeks - 1) * 7;
|
|
1098
1089
|
|
|
1090
|
+
// Tallied over the DRAWN range only — [startSun, endOrd] — not over everything
|
|
1091
|
+
// handed in. A day the caller supplied that falls before the first column is real
|
|
1092
|
+
// history, but it is not on this picture, so it is not in this picture's summary.
|
|
1093
|
+
let activeDays = 0;
|
|
1094
|
+
let totalShips = 0;
|
|
1095
|
+
for (const [o, c] of counts) {
|
|
1096
|
+
if (o < startSun || o > endOrd) continue;
|
|
1097
|
+
activeDays += 1;
|
|
1098
|
+
totalShips += c;
|
|
1099
|
+
}
|
|
1100
|
+
return { counts, startSun, endOrd, weeks, activeDays, totalShips };
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// calendarGridHtml(days, opts) — a year of ship-days as a contribution calendar:
|
|
1104
|
+
// one cell per calendar day, BANDED by count rather than scaled, its reading a
|
|
1105
|
+
// per-cell title (plus an aria summary on the whole grid), not a crosshair. The
|
|
1106
|
+
// hall's THIRD chart family (task 1003572, criterion legibility-series-are-drawn):
|
|
1107
|
+
// OTBKit's other two shapes are a horizontal bar series and a plot on a time axis;
|
|
1108
|
+
// a 2-D year grid is neither, so it earns its own primitive rather than a hand-roll
|
|
1109
|
+
// at the page (modules/hall-ui/CLAUDE.md; tests/hall_audit.mjs section 6b).
|
|
1110
|
+
//
|
|
1111
|
+
// THE READING, decided and written down (the task asked which): a NON-picture reader
|
|
1112
|
+
// gets the exact figure two ways — each cell carries a `title` ("2026-09-03 · 3
|
|
1113
|
+
// ships" / "… · no ships"), and the grid carries an aria-label summarising the
|
|
1114
|
+
// window (active days + total ships). No table twin: unlike a line, a calendar's
|
|
1115
|
+
// value IS its position, so a per-cell title reads the same fact the picture shows,
|
|
1116
|
+
// and a 366-row table beneath would be longer than the thing it explains.
|
|
1117
|
+
//
|
|
1118
|
+
// `days`: [{ local_date:'YYYY-MM-DD', count }] — SPARSE (only days that had ships),
|
|
1119
|
+
// already resolved in the BUILDER's timezone server-side (GET /me). Empty → '' so
|
|
1120
|
+
// the caller renders its own empty state, never an empty grid.
|
|
1121
|
+
//
|
|
1122
|
+
// The window it draws, and the tallies its summary quotes, come from calendarWindow —
|
|
1123
|
+
// see the timezone trap documented there.
|
|
1124
|
+
function calendarGridHtml(days, opts = {}) {
|
|
1125
|
+
const win = calendarWindow(days, opts);
|
|
1126
|
+
if (!win) return ''; // nothing real to draw → caller's empty state
|
|
1127
|
+
const { counts, startSun, endOrd, weeks, activeDays, totalShips } = win;
|
|
1128
|
+
const ordToYmd = calOrdToYmd;
|
|
1129
|
+
|
|
1099
1130
|
// level 0 = empty; 1..5 map to the anti-gaming daily cap (streak.js caps a day at
|
|
1100
1131
|
// 5), so the darkest band means "hit the day's max", not "over some threshold".
|
|
1101
1132
|
const level = (c) => (c <= 0 ? 0 : Math.min(5, c));
|
|
@@ -1112,8 +1143,11 @@
|
|
|
1112
1143
|
}
|
|
1113
1144
|
}
|
|
1114
1145
|
|
|
1146
|
+
// The window is DESCRIBED, not assumed: a caller passing weeks:6 used to be handed
|
|
1147
|
+
// an aria label saying "over the last year" (task 1003583).
|
|
1148
|
+
const span = weeks === 53 ? 'the last year' : `the last ${weeks} week${weeks === 1 ? '' : 's'}`;
|
|
1115
1149
|
const summary = opts.ariaLabel
|
|
1116
|
-
|| `Ships per day over
|
|
1150
|
+
|| `Ships per day over ${span}: ${totalShips} ship${totalShips === 1 ? '' : 's'} across ${activeDays} day${activeDays === 1 ? '' : 's'}.`;
|
|
1117
1151
|
// The legend is aria-hidden: it decorates the bands, it carries no fact the cell
|
|
1118
1152
|
// titles and the summary do not already state in words.
|
|
1119
1153
|
const legend = '<div class="kit-cal__key" aria-hidden="true"><span class="kit-cal__key-label">less</span>'
|
|
@@ -1141,6 +1175,7 @@
|
|
|
1141
1175
|
sparklineHtml, barSeriesHtml, chartCardHtml, linePoints, cumulate, cumulateByDay,
|
|
1142
1176
|
// the year-grid contribution calendar (task 1003572) — the hall's third chart family
|
|
1143
1177
|
calendarGridHtml,
|
|
1178
|
+
calendarWindow,
|
|
1144
1179
|
// plotted charts: the axis frame + the two series shapes that ride it
|
|
1145
1180
|
plotSvgHtml, plotValueLabelHtml, timeStepSeriesHtml, timeStepGeometry, columnSeriesHtml,
|
|
1146
1181
|
// plotBox is exported so a caller mapping pointer x back into viewBox
|
|
@@ -267,9 +267,15 @@
|
|
|
267
267
|
// module doc's honest-exit rule; DONE WHEN #4).
|
|
268
268
|
function calendarChart({ id, streak }) {
|
|
269
269
|
const days = (streak && Array.isArray(streak.ship_days)) ? streak.ship_days : [];
|
|
270
|
-
const
|
|
270
|
+
const opts = { today: streak && streak.today };
|
|
271
|
+
const grid = window.OTBKit.calendarGridHtml(days, opts);
|
|
271
272
|
if (!grid) return null;
|
|
272
|
-
|
|
273
|
+
// The caption counts what the GRID draws, not what the server sent (task 1003583).
|
|
274
|
+
// `days` is bounded to 371 days server-side while the grid spans 53 weeks back to a
|
|
275
|
+
// Sunday, so filtering `days` here could name a day the picture does not contain —
|
|
276
|
+
// and the number sits directly under that picture. One window, read twice.
|
|
277
|
+
const win = window.OTBKit.calendarWindow(days, opts);
|
|
278
|
+
const active = win ? win.activeDays : 0;
|
|
273
279
|
const cur = Number(streak && streak.current) || 0;
|
|
274
280
|
const lng = Number(streak && streak.longest) || 0;
|
|
275
281
|
// The scalars in words, in the .pchart__read slot the other figures use for their
|
|
@@ -94,6 +94,10 @@
|
|
|
94
94
|
leaderboard: '<path d="M3.5 15.5v-6M9 15.5V3.5m5.5 12v-9"/>',
|
|
95
95
|
people: '<circle cx="6.5" cy="6.5" r="2.5"/><path d="M2.5 15c.5-2.4 2-3.7 4-3.7s3.5 1.3 4 3.7"/><circle cx="12.5" cy="7" r="2"/><path d="M12 11.5c1.8.1 3 1.3 3.5 3.3"/>',
|
|
96
96
|
docs: '<path d="M4 3.5h7l3 3v10H4v-13Z"/><path d="M11 3.5v3h3"/>',
|
|
97
|
+
// task 1003117 (R06 of goal 1000074): the Studio (artist mode). A sprout —
|
|
98
|
+
// the zen-garden reading of "a place an artist wants to be", so it does not
|
|
99
|
+
// reuse a glyph that would read as one more docs/goals page.
|
|
100
|
+
studio: '<path d="M9 15.5V8.5"/><path d="M9 8.5C9 5.5 6.5 3.5 3.5 3.5c0 3 2.5 5 5.5 5Z"/><path d="M9 10C9 7.5 11.2 5.5 14 5.5c0 2.7-2.2 4.5-5 4.5Z"/>',
|
|
97
101
|
settings: '<circle cx="9" cy="9" r="2.5"/><path d="M9 2.5v2m0 9v2M2.5 9h2m9 0h2M4.4 4.4l1.4 1.4m6.4 6.4 1.4 1.4m0-9.2-1.4 1.4M5.8 12.2l-1.4 1.4"/>',
|
|
98
102
|
admin: '<path d="M9 2.5 15 5v4c0 3.5-2.4 6-6 7-3.6-1-6-3.5-6-7V5l6-2.5Z"/>',
|
|
99
103
|
signout: '<path d="M7 3.5H4a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h3M12 12l3-3-3-3M15 9H7"/>',
|
|
@@ -153,6 +157,16 @@
|
|
|
153
157
|
// everyone else who has a thought, which is most of the value.
|
|
154
158
|
{ id: 'ideas', label: 'Ideas', icon: 'goals', href: '/ideas', gate: 'signed' },
|
|
155
159
|
{ id: 'copy-desk', label: 'Copy desk', icon: 'docs', href: '/copy-desk', gate: 'signed', module: 'copy-desk' },
|
|
160
|
+
// task 1003117 (R06 of goal 1000074): the Studio — artist mode, "a place an
|
|
161
|
+
// artist wants to be, not a form". Beside the copy desk (the artist's other
|
|
162
|
+
// surface), for the same reason both sit here and not in Docs (a place you
|
|
163
|
+
// ACT, not reference) or You (its subject is the product, not a person).
|
|
164
|
+
// gate 'signed' and NOT craft-gated: an artist is a craft, not a rank, so
|
|
165
|
+
// hiding the artist's own room behind the artist craft would hide it from
|
|
166
|
+
// the artist — the exact mistake copy-desk's comment above names. NO module
|
|
167
|
+
// gate: it degrades to reviews alone on an instance without copy-desk,
|
|
168
|
+
// rather than vanishing.
|
|
169
|
+
{ id: 'studio', label: 'Studio', icon: 'studio', href: '/studio', gate: 'signed' },
|
|
156
170
|
] },
|
|
157
171
|
// Your own numbers. task 2174 folded Achievements into Standing and this task
|
|
158
172
|
// retires its nav entry; VIEW_ALIASES still routes #achievements here, so old
|
|
@@ -549,6 +563,21 @@
|
|
|
549
563
|
else delete root.dataset.social;
|
|
550
564
|
}
|
|
551
565
|
|
|
566
|
+
// task 1003117 (R06 of goal 1000074): EMPHASISE the Studio nav entry for a
|
|
567
|
+
// builder whose craft includes artist — the "emphasized entry for artist-craft
|
|
568
|
+
// builders" half of artist mode. Cosmetic only (the page is open to every rank,
|
|
569
|
+
// and the server enforces nothing on craft), and craft is not a rank, so it
|
|
570
|
+
// reads preferred_disciplines off /me rather than the access ladder. A warm
|
|
571
|
+
// entry is the world's own language: the accent marks the live thing, and for
|
|
572
|
+
// an artist that is their room.
|
|
573
|
+
function applyCraftEmphasis(builder) {
|
|
574
|
+
var el = document.getElementById('nav-studio');
|
|
575
|
+
if (!el) return;
|
|
576
|
+
var crafts = (builder && builder.preferred_disciplines) || [];
|
|
577
|
+
var isArtist = Array.isArray(crafts) && crafts.indexOf('artist') !== -1;
|
|
578
|
+
el.classList.toggle('nav-item--craft', isArtist);
|
|
579
|
+
}
|
|
580
|
+
|
|
552
581
|
// applyAccess({ authed, rank, graduated, showWork, social }) — settle nav
|
|
553
582
|
// visibility and the social mode.
|
|
554
583
|
// Called by builders.js on the index (it owns /me there) and by the shell's own
|
|
@@ -560,7 +589,7 @@
|
|
|
560
589
|
if ('graduated' in state) access.graduated = !!state.graduated;
|
|
561
590
|
if ('showWork' in state) access.showWork = !!state.showWork;
|
|
562
591
|
if ('social' in state) applySocial(state.social);
|
|
563
|
-
if (state.builder) renderUser(state.builder);
|
|
592
|
+
if (state.builder) { renderUser(state.builder); applyCraftEmphasis(state.builder); }
|
|
564
593
|
}
|
|
565
594
|
document.querySelectorAll('#app-sidebar [data-gate]').forEach(function (el) {
|
|
566
595
|
var ok = gateAllows(el.dataset.gate);
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/* modules/hall-ui/public/studio.css — the Studio (artist mode), /builders/studio.
|
|
2
|
+
R06 of goal 1000074 (the artist's loop), task 1003117.
|
|
3
|
+
|
|
4
|
+
The criterion (artist-mode): "a place an artist wants to be, not a form. A
|
|
5
|
+
focus environment on the Cloud Bongos screen — a lofi-girl-style zen garden of
|
|
6
|
+
focus and creativity — rather than another table of rows." So this sheet is
|
|
7
|
+
deliberately NOT the oversight row archetype the copy desk rides. It is a calm
|
|
8
|
+
room: one thing to tend at a time, generous air, tonal separation, and one
|
|
9
|
+
slow ambient presence — the warm "lamp" that gives a lofi room its light.
|
|
10
|
+
|
|
11
|
+
THE ONE WARM THING RULE, kept (DESIGN.md): the salmon accent lights only what
|
|
12
|
+
is pressable (the primary action) or LIVE. The ambient lamp is the live thing
|
|
13
|
+
— a slow-breathing warm presence, the room's own light — and nothing else here
|
|
14
|
+
is warm: the eyebrows, the step numbers and the stones are ink, never accent.
|
|
15
|
+
|
|
16
|
+
Tokens only — no colour literals (tests/hall_audit.mjs §9 holds every hall
|
|
17
|
+
sheet to that), so every colour is a var() or a color-mix() of one, and both
|
|
18
|
+
modes come for free from style.css's tier. The mode marker html[data-artist]
|
|
19
|
+
is set on this page's <html> (the social-mode pattern, shell.js applySocial):
|
|
20
|
+
entering the studio IS entering the mode, so the calm reads off the attribute. */
|
|
21
|
+
|
|
22
|
+
/* The room. Generous vertical air so the one focus sits in space, not in a list.
|
|
23
|
+
position:relative anchors the ambient layer below it. */
|
|
24
|
+
.studio {
|
|
25
|
+
position: relative;
|
|
26
|
+
padding: clamp(28px, 6vh, 76px) 0 64px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* The ambient presence — the lamp. A single soft warm disc, low and slow, the
|
|
30
|
+
only atmosphere the room draws (the hall's --plate ground is a deferred seam,
|
|
31
|
+
owner's call, so nothing is painted into it here). overflow:hidden clips the
|
|
32
|
+
disc to the room so it can never widen the document (the no-sideways rule). */
|
|
33
|
+
.studio-atmos {
|
|
34
|
+
position: absolute;
|
|
35
|
+
inset: 0;
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
pointer-events: none;
|
|
38
|
+
z-index: 0;
|
|
39
|
+
}
|
|
40
|
+
.studio-atmos::before {
|
|
41
|
+
content: "";
|
|
42
|
+
position: absolute;
|
|
43
|
+
top: -24%;
|
|
44
|
+
right: -10%;
|
|
45
|
+
width: min(680px, 82vw);
|
|
46
|
+
aspect-ratio: 1;
|
|
47
|
+
border-radius: 50%;
|
|
48
|
+
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
|
49
|
+
animation: studio-lamp 38s ease-in-out infinite;
|
|
50
|
+
}
|
|
51
|
+
/* A second, cooler and fainter presence low-left, so the room has depth rather
|
|
52
|
+
than one lamp in a corner. --glass is the world's tonal wash token (translucent
|
|
53
|
+
ink on paper in light, translucent white on black in dark) — the sanctioned way
|
|
54
|
+
to lay a neutral wash that reads right in both modes, never a raw --ink mix into
|
|
55
|
+
transparent (which inverts; tests/hall_audit.mjs bans it). */
|
|
56
|
+
.studio-atmos::after {
|
|
57
|
+
content: "";
|
|
58
|
+
position: absolute;
|
|
59
|
+
bottom: -30%;
|
|
60
|
+
left: -14%;
|
|
61
|
+
width: min(560px, 72vw);
|
|
62
|
+
aspect-ratio: 1;
|
|
63
|
+
border-radius: 50%;
|
|
64
|
+
background: var(--glass);
|
|
65
|
+
animation: studio-lamp 52s ease-in-out infinite reverse;
|
|
66
|
+
}
|
|
67
|
+
/* Long-period, transform/opacity only, and dead under the shell's global
|
|
68
|
+
reduced-motion kill switch (style.css) — the DESIGN.md rule for ambient loops. */
|
|
69
|
+
@keyframes studio-lamp {
|
|
70
|
+
0%, 100% { transform: scale(1); opacity: .85; }
|
|
71
|
+
50% { transform: scale(1.08); opacity: 1; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Everything the reader acts on sits above the lamp. */
|
|
75
|
+
.studio-head,
|
|
76
|
+
.studio-focus,
|
|
77
|
+
.studio-rest { position: relative; z-index: 1; }
|
|
78
|
+
|
|
79
|
+
/* The head: a mono eyebrow naming the mode, then the greeting as the display
|
|
80
|
+
voice (Manrope light, tight), then the live count as a quiet sub-line. */
|
|
81
|
+
.studio-head { margin-bottom: clamp(22px, 4vh, 40px); max-width: var(--measure); }
|
|
82
|
+
.studio-eyebrow {
|
|
83
|
+
margin: 0 0 10px;
|
|
84
|
+
font-family: var(--font-mono);
|
|
85
|
+
font-size: 11px;
|
|
86
|
+
letter-spacing: .14em;
|
|
87
|
+
text-transform: uppercase;
|
|
88
|
+
color: var(--ink-faint);
|
|
89
|
+
}
|
|
90
|
+
.studio-greet {
|
|
91
|
+
margin: 0 0 8px;
|
|
92
|
+
font-weight: 300;
|
|
93
|
+
font-size: clamp(28px, 4.4vw, 46px);
|
|
94
|
+
line-height: 1.04;
|
|
95
|
+
letter-spacing: -.028em;
|
|
96
|
+
color: var(--ink);
|
|
97
|
+
}
|
|
98
|
+
.studio-sub {
|
|
99
|
+
margin: 0;
|
|
100
|
+
font-size: 15.5px;
|
|
101
|
+
line-height: 1.5;
|
|
102
|
+
color: var(--ink-soft);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* The focus — the one thing to tend now. Tone separates it from the room (the
|
|
106
|
+
v3 hall rule: a --bg-card panel at the full radius, no drawn edge), and it is
|
|
107
|
+
held to a reading width so it stays a card and not a banner. */
|
|
108
|
+
.studio-focus {
|
|
109
|
+
max-width: 720px;
|
|
110
|
+
background: var(--bg-card);
|
|
111
|
+
border-radius: var(--radius);
|
|
112
|
+
padding: clamp(22px, 3.2vw, 34px);
|
|
113
|
+
}
|
|
114
|
+
.studio-card__eyebrow {
|
|
115
|
+
margin: 0 0 14px;
|
|
116
|
+
font-family: var(--font-mono);
|
|
117
|
+
font-size: 11px;
|
|
118
|
+
letter-spacing: .12em;
|
|
119
|
+
text-transform: uppercase;
|
|
120
|
+
color: var(--ink-faint);
|
|
121
|
+
}
|
|
122
|
+
.studio-card__title {
|
|
123
|
+
margin: 0 0 14px;
|
|
124
|
+
font-weight: 300;
|
|
125
|
+
font-size: clamp(20px, 2.6vw, 26px);
|
|
126
|
+
line-height: 1.2;
|
|
127
|
+
letter-spacing: -.02em;
|
|
128
|
+
color: var(--ink);
|
|
129
|
+
}
|
|
130
|
+
/* The product's own words, shown as evidence for a flag — mono, wrapping
|
|
131
|
+
preserved (leading/trailing space is often exactly what is wrong), in a soft
|
|
132
|
+
tonal well so the quote reads as quotation, not as this room's own voice. */
|
|
133
|
+
.studio-card__quote {
|
|
134
|
+
display: block;
|
|
135
|
+
margin: 0 0 16px;
|
|
136
|
+
padding: 12px 14px;
|
|
137
|
+
border-radius: var(--radius-sm);
|
|
138
|
+
background: var(--glass);
|
|
139
|
+
font-family: var(--font-mono);
|
|
140
|
+
font-size: 13.5px;
|
|
141
|
+
line-height: 1.5;
|
|
142
|
+
color: var(--ink);
|
|
143
|
+
white-space: pre-wrap;
|
|
144
|
+
overflow-wrap: anywhere;
|
|
145
|
+
}
|
|
146
|
+
.studio-card__context {
|
|
147
|
+
margin: 0 0 18px;
|
|
148
|
+
font-size: 14.5px;
|
|
149
|
+
line-height: 1.55;
|
|
150
|
+
color: var(--ink-soft);
|
|
151
|
+
max-width: var(--measure);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* The two steps of a review (R04): coach the grader, then remake or tweak. Not
|
|
155
|
+
a checklist widget — a gentle reminder of the shape of the work, so the room
|
|
156
|
+
describes the craft without becoming the form the criterion warns against. */
|
|
157
|
+
.studio-steps {
|
|
158
|
+
list-style: none;
|
|
159
|
+
margin: 0 0 20px;
|
|
160
|
+
padding: 0;
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-direction: column;
|
|
163
|
+
gap: 12px;
|
|
164
|
+
}
|
|
165
|
+
.studio-step {
|
|
166
|
+
display: flex;
|
|
167
|
+
gap: 12px;
|
|
168
|
+
align-items: baseline;
|
|
169
|
+
font-size: 14.5px;
|
|
170
|
+
line-height: 1.5;
|
|
171
|
+
color: var(--ink);
|
|
172
|
+
}
|
|
173
|
+
.studio-step__n {
|
|
174
|
+
flex: none;
|
|
175
|
+
min-width: 16px;
|
|
176
|
+
font-family: var(--font-mono);
|
|
177
|
+
font-size: 12px;
|
|
178
|
+
color: var(--ink-faint);
|
|
179
|
+
}
|
|
180
|
+
.studio-step small {
|
|
181
|
+
display: block;
|
|
182
|
+
margin-top: 2px;
|
|
183
|
+
font-size: 13px;
|
|
184
|
+
color: var(--ink-faint);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* The one place the accent is spent on a control. Ghost is the calm pass —
|
|
188
|
+
"let it be for now" advances to the next piece without changing anything. */
|
|
189
|
+
.studio-card__actions {
|
|
190
|
+
display: flex;
|
|
191
|
+
flex-wrap: wrap;
|
|
192
|
+
gap: 10px;
|
|
193
|
+
align-items: center;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* What else waits — a gentle line and a row of stones, one per remaining piece
|
|
197
|
+
(the zen garden), never a table. Non-interactive on purpose: the room asks the
|
|
198
|
+
reader to tend one thing, not to scan a queue. */
|
|
199
|
+
.studio-rest {
|
|
200
|
+
margin-top: clamp(20px, 3.5vh, 34px);
|
|
201
|
+
max-width: 720px;
|
|
202
|
+
display: flex;
|
|
203
|
+
flex-wrap: wrap;
|
|
204
|
+
align-items: center;
|
|
205
|
+
gap: 14px;
|
|
206
|
+
}
|
|
207
|
+
.studio-rest__line {
|
|
208
|
+
margin: 0;
|
|
209
|
+
font-size: 13.5px;
|
|
210
|
+
color: var(--ink-faint);
|
|
211
|
+
}
|
|
212
|
+
.studio-stones {
|
|
213
|
+
display: flex;
|
|
214
|
+
flex-wrap: wrap;
|
|
215
|
+
gap: 9px;
|
|
216
|
+
align-items: center;
|
|
217
|
+
}
|
|
218
|
+
.studio-stone {
|
|
219
|
+
width: 11px;
|
|
220
|
+
height: 11px;
|
|
221
|
+
border-radius: 50%;
|
|
222
|
+
background: var(--ink-faint);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* Where the full queues live — the calm room is the front door; the tables it
|
|
226
|
+
replaces stay one link away for when the reader wants the whole list. */
|
|
227
|
+
.studio-links {
|
|
228
|
+
position: relative;
|
|
229
|
+
z-index: 1;
|
|
230
|
+
margin-top: clamp(26px, 4vh, 44px);
|
|
231
|
+
padding-top: 18px;
|
|
232
|
+
border-top: 1px solid var(--rule-soft);
|
|
233
|
+
display: flex;
|
|
234
|
+
flex-wrap: wrap;
|
|
235
|
+
gap: 8px 22px;
|
|
236
|
+
max-width: 720px;
|
|
237
|
+
font-size: 13.5px;
|
|
238
|
+
color: var(--ink-faint);
|
|
239
|
+
}
|
|
240
|
+
.studio-links a { color: var(--accent-ink); }
|
|
241
|
+
|
|
242
|
+
/* The empty room — a good sign, said plainly, not a dead panel. */
|
|
243
|
+
.studio-empty {
|
|
244
|
+
max-width: 720px;
|
|
245
|
+
background: var(--bg-card);
|
|
246
|
+
border-radius: var(--radius);
|
|
247
|
+
padding: clamp(24px, 3.4vw, 36px);
|
|
248
|
+
}
|
|
249
|
+
.studio-empty h2 {
|
|
250
|
+
margin: 0 0 8px;
|
|
251
|
+
font-weight: 300;
|
|
252
|
+
font-size: clamp(19px, 2.4vw, 24px);
|
|
253
|
+
letter-spacing: -.02em;
|
|
254
|
+
color: var(--ink);
|
|
255
|
+
}
|
|
256
|
+
.studio-empty p {
|
|
257
|
+
margin: 0;
|
|
258
|
+
font-size: 14.5px;
|
|
259
|
+
line-height: 1.55;
|
|
260
|
+
color: var(--ink-soft);
|
|
261
|
+
max-width: var(--measure);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Phone: the room keeps its air but the head and cards take the full width. */
|
|
265
|
+
@media (max-width: 640px) {
|
|
266
|
+
.studio { padding-top: 20px; }
|
|
267
|
+
.studio-card__actions { gap: 8px; }
|
|
268
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!-- data-artist="on" is the studio's MODE marker (shell.js applySocial's pattern,
|
|
3
|
+
set here statically so entering the room IS entering the mode, with no paint
|
|
4
|
+
flash). It is the surface half of goal 1000074's argument: the artist's craft
|
|
5
|
+
gets its own place, not one more table on the engineer's screen. -->
|
|
6
|
+
<html lang="en" data-artist="on">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="utf-8">
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
10
|
+
<title>Studio — {{worldName}}</title>
|
|
11
|
+
<meta name="description" content="Artist mode — a calm place to tend the words and art the project shows people, one piece at a time: the review that shipped, and the copy someone flagged.">
|
|
12
|
+
<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>">
|
|
13
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
14
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
15
|
+
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
16
|
+
<link rel="stylesheet" href="/builders/studio.css">
|
|
17
|
+
</head>
|
|
18
|
+
<body data-page="studio" data-page-title="Studio">
|
|
19
|
+
|
|
20
|
+
<div class="app">
|
|
21
|
+
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
22
|
+
<div class="app-scrim" id="app-scrim" hidden></div>
|
|
23
|
+
<div class="app-body">
|
|
24
|
+
<header class="app-topbar" id="app-topbar"></header>
|
|
25
|
+
<div class="app-content">
|
|
26
|
+
|
|
27
|
+
<!-- The studio is a room, not a board, so it carries its OWN head voice
|
|
28
|
+
(a greeting in the display face) rather than the quiet .page-head--board
|
|
29
|
+
a working table uses. The static h1 is the no-JS / signed-out fallback;
|
|
30
|
+
studio.js rewrites it to "Good evening, <name>" once /me answers, and
|
|
31
|
+
fills the sub-line with what waits. It is the page's one <h1>. -->
|
|
32
|
+
<main id="hall-main" class="studio">
|
|
33
|
+
<div class="studio-atmos" aria-hidden="true"></div>
|
|
34
|
+
|
|
35
|
+
<header class="studio-head">
|
|
36
|
+
<p class="studio-eyebrow">artist mode</p>
|
|
37
|
+
<h1 class="studio-greet" id="studio-greet">The studio</h1>
|
|
38
|
+
<p class="studio-sub" id="studio-sub">A quiet place to tend the words and art the project shows people.</p>
|
|
39
|
+
</header>
|
|
40
|
+
|
|
41
|
+
<!-- The one thing to tend now. Rendered by studio.js; the markup here is
|
|
42
|
+
the loading state (and the no-JS fallback), never an empty panel. -->
|
|
43
|
+
<section class="studio-focus" id="studio-focus" aria-live="polite">
|
|
44
|
+
<div class="studio-empty">
|
|
45
|
+
<h2>Opening the studio…</h2>
|
|
46
|
+
<p>Gathering the reviews and flagged copy that wait for you.</p>
|
|
47
|
+
</div>
|
|
48
|
+
</section>
|
|
49
|
+
|
|
50
|
+
<!-- What else waits — the garden's remaining stones. Hidden until there
|
|
51
|
+
is more than the one in focus. -->
|
|
52
|
+
<div class="studio-rest" id="studio-rest" hidden></div>
|
|
53
|
+
|
|
54
|
+
<!-- The full tables the room replaces stay one link away, for when the
|
|
55
|
+
reader wants the whole list rather than one piece at a time. -->
|
|
56
|
+
<nav class="studio-links" aria-label="The full queues">
|
|
57
|
+
<a href="/builders/copy-desk">The copy desk — every flagged string</a>
|
|
58
|
+
<a href="/builders/work">The task board — all work</a>
|
|
59
|
+
</nav>
|
|
60
|
+
</main>
|
|
61
|
+
|
|
62
|
+
<footer class="app-foot" id="app-foot"></footer>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
68
|
+
|
|
69
|
+
<script src="/builders/shell.js?v=2026-07-02-shell1"></script>
|
|
70
|
+
<script src="/builders/palette.js" defer></script>
|
|
71
|
+
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
72
|
+
<script src="/builders/studio.js?v=2026-09-04-studio1"></script>
|
|
73
|
+
</body>
|
|
74
|
+
</html>
|