@bongos/core 1.24.29 → 1.24.31
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 +198 -108
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +12 -0
- package/clients/bongos-client/index.cjs +12 -0
- package/clients/bongos-client/index.d.ts +12 -0
- package/clients/bongos-client/index.mjs +12 -0
- package/docs/api/openapi.json +174 -1
- package/docs/api-reference.md +12 -2
- package/docs/copy-inventory.md +226 -224
- package/docs/copy-registry.json +583 -549
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +18 -37
- package/docs/page-readings.json +306 -361
- package/modules/copy-desk/module.json +1 -1
- package/modules/copy-desk/routes/copy-desk-studio.js +103 -0
- package/modules/copy-desk/tests/copy_no_cms.mjs +2 -2
- package/modules/government/db.js +29 -0
- package/modules/government/docket.js +61 -4
- package/modules/government/routes/government.js +2 -0
- package/modules/hall-ui/public/ai-studio.html +1 -1
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/builders.js +3 -79
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/craft-mode.js +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/hall-render.js +68 -167
- package/modules/hall-ui/public/hall-widgets.js +4 -45
- package/modules/hall-ui/public/home-city-draw.js +583 -0
- package/modules/hall-ui/public/home-city.css +102 -0
- package/modules/hall-ui/public/home-city.js +339 -0
- package/modules/hall-ui/public/home-gallery.css +129 -0
- package/modules/hall-ui/public/home-gallery.js +349 -0
- package/modules/hall-ui/public/home-studio.css +72 -0
- package/modules/hall-ui/public/home-studio.js +203 -0
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +9 -7
- package/modules/hall-ui/public/index.states.json +73 -8
- package/modules/hall-ui/public/market-watch-item.html +1 -1
- package/modules/hall-ui/public/market-watch-rules.html +1 -1
- package/modules/hall-ui/public/market-watch-settings.js +6 -1
- package/modules/hall-ui/public/market-watch.html +141 -54
- package/modules/hall-ui/public/market-watch.js +54 -0
- package/modules/hall-ui/public/market-watch.states.json +2 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +2 -3
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +9 -1
- package/modules/hall-ui/public/style.css +74 -27
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/home-city.md +189 -0
- package/modules/hall-ui/records/loader.md +1 -1
- package/modules/hall-ui/records/market-watch-settings.md +16 -3
- package/modules/hall-ui/records/market-watch.md +3 -1
- package/modules/lifecycle/home-city.js +388 -0
- package/modules/lifecycle/home-gallery.js +320 -0
- package/modules/lifecycle/migrations/lifecycle_020_work_appreciations.sql +36 -0
- package/modules/lifecycle/module.json +2 -0
- package/modules/lifecycle/routes/home-city.js +29 -0
- package/modules/lifecycle/routes/home-gallery.js +37 -0
- package/modules/market-watch/routes/settings.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +38 -0
- package/scripts/bongos/leak-scan-allowlist.js +8 -0
- package/scripts/hall-preview/README.md +1 -0
- package/scripts/hall-preview/server.js +10 -0
- package/src/bongos/serve-internal.js +17 -3
- package/src/module-api.js +1 -1
- package/tests/claim_from_task_and_home.mjs +8 -43
- package/tests/hall_board_world.mjs +10 -12
- package/tests/hall_builders_page_pass.mjs +18 -27
- package/tests/hall_home_ships_bounded.mjs +4 -3
- package/tests/hall_loading.mjs +1 -1
- package/tests/hall_nav.mjs +11 -4
- package/tests/hall_page_gate_map.mjs +7 -4
- package/tests/home_bounded_reads.mjs +16 -31
- package/tests/home_city.mjs +580 -0
- package/tests/home_city_widget.mjs +260 -0
- package/tests/home_gallery.mjs +255 -0
- package/tests/home_gallery_widget.mjs +220 -0
- package/tests/home_studio.mjs +169 -0
- package/tests/home_studio_widget.mjs +157 -0
- package/tests/leaderboard_roles.mjs +3 -2
- package/tests/market_watch_pages.mjs +162 -0
- package/tests/nav_permission_atoms.mjs +2 -2
- package/modules/hall-ui/public/market-watch-settings.html +0 -115
- package/modules/hall-ui/public/market-watch-settings.states.json +0 -14
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/* modules/hall-ui/public/home-city.css — Home's project city (task 1005054; v16, task 1005058).
|
|
2
|
+
The scene card, the ladder of builders beside it and the time slider under it
|
|
3
|
+
(home-city.js; record modules/hall-ui/records/home-city.md). Every colour is a
|
|
4
|
+
hall token or a mix of one; the tower faces are drawing data in
|
|
5
|
+
home-city-draw.js. One sky for both the city and the ladder, so the two read
|
|
6
|
+
as one scene. No :root here; motion is CSS and the hall's reduced-motion rule (style.css, `prefers-reduced-motion: reduce`)
|
|
7
|
+
stops it. */
|
|
8
|
+
|
|
9
|
+
.home-city { padding: 0; overflow: hidden; background: linear-gradient(var(--bg-card), var(--bg)); }
|
|
10
|
+
.hc-stage { display: flex; align-items: center; gap: 4px; }
|
|
11
|
+
.hc-svg { display: block; flex: 1; min-width: 0; height: auto; }
|
|
12
|
+
.hc-svg .hc-wait__n { font: 700 11px var(--font-display); fill: var(--accent-on); }
|
|
13
|
+
.hc-svg .hc-av__i { font: 600 13px var(--font-display); }
|
|
14
|
+
.hc-svg [data-tip] { cursor: default; }
|
|
15
|
+
.hc-svg a[href] { cursor: pointer; }
|
|
16
|
+
.hc-svg a:focus { outline: none; }
|
|
17
|
+
.hc-svg a:focus-visible .hc-face { filter: brightness(1.35); }
|
|
18
|
+
.hc-svg .hc-tower:hover .hc-face { filter: brightness(1.3); }
|
|
19
|
+
/* the core and the museum stay quiet on the builder's Home so the new buildings lead;
|
|
20
|
+
each comes back to full colour under the pointer or the keyboard */
|
|
21
|
+
.hc-svg .hc-quiet { opacity: .3; filter: saturate(0) brightness(.85); transition: opacity .3s, filter .3s; }
|
|
22
|
+
.hc-svg .hc-quiet:hover, .hc-svg .hc-quiet:focus-visible { opacity: .95; filter: none; }
|
|
23
|
+
.hc-svg .hc-core:hover .hc-face, .hc-svg .hc-museum:hover .hc-face { filter: brightness(1.15); }
|
|
24
|
+
.hc-svg .hc-sky:hover circle, .hc-svg .hc-sky:focus-visible circle { opacity: 1; }
|
|
25
|
+
|
|
26
|
+
/* the ideator's sky (task 1005059): night falls over the same city, seen from the street.
|
|
27
|
+
The sky is the page's own ground tinted toward the sea, deeper at the horizon; the
|
|
28
|
+
skyline's outlines are hidden until one of the three groups is pointed at or focused,
|
|
29
|
+
then the whole group lights at once. */
|
|
30
|
+
.home-city.hc-is-sky { background: linear-gradient(color-mix(in srgb, var(--bg) 86%, var(--sea)), color-mix(in srgb, var(--bg-card) 86%, var(--accent))); }
|
|
31
|
+
html[data-theme="dark"] .home-city.hc-is-sky { background: linear-gradient(color-mix(in srgb, var(--bg) 92%, var(--sea)), var(--bg) 40%, color-mix(in srgb, var(--bg) 58%, var(--sea))); }
|
|
32
|
+
@media (prefers-color-scheme: dark) {
|
|
33
|
+
html:not([data-theme]) .home-city.hc-is-sky { background: linear-gradient(color-mix(in srgb, var(--bg) 92%, var(--sea)), var(--bg) 40%, color-mix(in srgb, var(--bg) 58%, var(--sea))); }
|
|
34
|
+
}
|
|
35
|
+
.hc-is-sky .hc-time { background: color-mix(in srgb, var(--bg-card) 70%, transparent); }
|
|
36
|
+
.hc-svg .hc-sky__label { font: 500 11px var(--font-display); letter-spacing: .06em; }
|
|
37
|
+
.hc-svg .hc-ol { stroke-opacity: 0; transition: stroke-opacity .25s; }
|
|
38
|
+
.hc-svg:has(.hc-hit[data-group="area"]:hover, .hc-hit[data-group="area"]:focus-visible) .hc-ol-area,
|
|
39
|
+
.hc-svg:has(.hc-hit[data-group="civic"]:hover, .hc-hit[data-group="civic"]:focus-visible) .hc-ol-civic,
|
|
40
|
+
.hc-svg:has(.hc-hit[data-group="museum"]:hover, .hc-hit[data-group="museum"]:focus-visible) .hc-ol-museum { stroke-opacity: .9; }
|
|
41
|
+
|
|
42
|
+
/* the ladder: a small face and one thin line split by craft */
|
|
43
|
+
.hc-ladder { position: relative; flex: none; width: 230px; margin: 24px 28px 24px 0; }
|
|
44
|
+
.hc-rung { position: absolute; left: 0; right: 0; height: 34px; display: grid; grid-template-columns: 28px minmax(0, 1fr); align-items: center; gap: 12px; transition: top .55s cubic-bezier(.2, .7, .2, 1); }
|
|
45
|
+
.hc-av { position: relative; width: 28px; height: 28px; border-radius: 50%; background: var(--rule-soft); display: grid; place-items: center; overflow: hidden; font: 600 11px var(--font-display); color: var(--ink-soft); }
|
|
46
|
+
.hc-av img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
|
47
|
+
.hc-av--agent { background: color-mix(in srgb, var(--sea) 26%, var(--bg-card)); }
|
|
48
|
+
.hc-av svg { width: 15px; height: 15px; }
|
|
49
|
+
.hc-rung--me .hc-av { color: var(--ink); box-shadow: 0 0 0 1.5px var(--ink-soft); }
|
|
50
|
+
.hc-line { display: flex; gap: 3px; align-items: flex-end; height: 22px; transition: width .55s cubic-bezier(.2, .7, .2, 1); }
|
|
51
|
+
.hc-seg { display: flex; flex-direction: column; justify-content: flex-end; gap: 3px; min-width: 26px; color: var(--ink-faint); }
|
|
52
|
+
.hc-seg b { display: flex; align-items: center; gap: 3px; font: 600 10.5px/1 var(--font-display); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
53
|
+
.hc-seg .hc-ico { width: 11px; height: 11px; flex: none; opacity: .8; }
|
|
54
|
+
.hc-seg i { display: block; height: 3px; border-radius: 2px; background: var(--rule); }
|
|
55
|
+
.hc-rung--me .hc-seg { color: var(--ink-soft); }
|
|
56
|
+
.hc-rung--me .hc-seg i { background: var(--ink-soft); }
|
|
57
|
+
.hc-seg:hover { color: var(--ink); }
|
|
58
|
+
.hc-seg:hover i { background: var(--ink); }
|
|
59
|
+
|
|
60
|
+
/* the time slider: live at its right end, a quiet player beside it */
|
|
61
|
+
.hc-time { display: flex; align-items: center; gap: 12px; padding: 18px 24px 16px; background: var(--bg-card); }
|
|
62
|
+
.hc-player { display: flex; gap: 2px; flex: none; }
|
|
63
|
+
.hc-p { width: 30px; height: 30px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--ink-faint); cursor: pointer; display: grid; place-items: center; }
|
|
64
|
+
.hc-p svg { width: 13px; height: 13px; }
|
|
65
|
+
.hc-p:hover { color: var(--ink); background: var(--rule-soft); }
|
|
66
|
+
.hc-p[aria-pressed="true"] { color: var(--ink); }
|
|
67
|
+
.hc-p:disabled { opacity: .3; cursor: default; background: transparent; }
|
|
68
|
+
.hc-p-play .hc-play__off, .hc-p-play.is-playing .hc-play__on { display: none; }
|
|
69
|
+
.hc-p-play.is-playing .hc-play__off { display: block; }
|
|
70
|
+
.hc-p:focus-visible, .hc-slider:focus-visible, .hc-live:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
71
|
+
.hc-track { position: relative; flex: 1; min-width: 0; }
|
|
72
|
+
.hc-when { position: absolute; top: -18px; transform: translateX(-50%); font: 600 11px/1 var(--font-display); padding: 3px 8px; border-radius: var(--radius-pill); background: var(--ink); color: var(--bg-card); opacity: 0; transition: opacity .25s; white-space: nowrap; pointer-events: none; }
|
|
73
|
+
.hc-when.is-on { opacity: 1; }
|
|
74
|
+
.hc-live { display: inline-flex; align-items: center; gap: 6px; min-height: 24px; padding: 0; border: 0; background: transparent; cursor: pointer; font: 600 11px var(--font-display); color: var(--ink-soft); }
|
|
75
|
+
.hc-live i { width: 7px; height: 7px; border-radius: 50%; background: var(--ink-faint); }
|
|
76
|
+
.hc-live.is-live { color: var(--ink); }
|
|
77
|
+
.hc-live.is-live i { background: var(--ok); animation: hc-live 2.4s ease-out infinite; }
|
|
78
|
+
.hc-slider { width: 100%; margin: 0; appearance: none; -webkit-appearance: none; background: transparent; height: 24px; cursor: pointer; }
|
|
79
|
+
.hc-slider::-webkit-slider-runnable-track { height: 4px; border-radius: 2px; background: var(--rule-soft); }
|
|
80
|
+
.hc-slider::-moz-range-track { height: 4px; border-radius: 2px; background: var(--rule-soft); }
|
|
81
|
+
.hc-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; border-radius: 50%; background: var(--ink); margin-top: -8px; border: 3px solid var(--bg-card); }
|
|
82
|
+
.hc-slider::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--ink); border: 3px solid var(--bg-card); }
|
|
83
|
+
.hc-ticks { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: var(--ink-faint); margin-top: 2px; }
|
|
84
|
+
|
|
85
|
+
.hc-tip { position: fixed; pointer-events: none; z-index: var(--z-modal); background: var(--ink); color: var(--bg-card); font-size: 12px; line-height: 1.35; padding: 8px 11px; border-radius: 9px; max-width: 260px; opacity: 0; transform: translate(-50%, calc(-100% - 12px)); }
|
|
86
|
+
.hc-tip.is-on { opacity: 1; }
|
|
87
|
+
.hc-tip b { display: block; font-size: 13px; }
|
|
88
|
+
|
|
89
|
+
/* live, and nothing more: a crane's slab climbs, this week's ideas glow (20s+) */
|
|
90
|
+
@keyframes hc-lift { 0%, 12% { transform: translateY(0); } 55%, 70% { transform: translateY(-24px); } 100% { transform: translateY(0); } }
|
|
91
|
+
@keyframes hc-glow { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
|
|
92
|
+
@keyframes hc-live { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ok) 55%, transparent); } 100% { box-shadow: 0 0 0 8px transparent; } }
|
|
93
|
+
.hc-slab { animation: hc-lift 20s ease-in-out infinite; }
|
|
94
|
+
.hc-orb, .hc-twinkle { animation: hc-glow 24s ease-in-out infinite; }
|
|
95
|
+
|
|
96
|
+
/* a phone: the ladder runs full width under the city */
|
|
97
|
+
@media (max-width: 700px) {
|
|
98
|
+
.hc-stage { flex-direction: column; align-items: stretch; }
|
|
99
|
+
.hc-svg { width: 100%; }
|
|
100
|
+
.hc-ladder { width: auto; margin: 12px 18px 4px; }
|
|
101
|
+
.hc-time { padding: 6px 16px 16px; }
|
|
102
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// modules/hall-ui/public/home-city.js — Home's project city (task 1005054).
|
|
2
|
+
//
|
|
3
|
+
// The page half of the owner's Home redesign (docs/design/mocks/hall/home/v14.html,
|
|
4
|
+
// record modules/hall-ui/records/home-city.md): the city scene, the ladder of
|
|
5
|
+
// builders beside it, and the time slider under it. It asks the server for ONE
|
|
6
|
+
// bounded weekly summary (GET /home/city) and never a feed it would slice — the
|
|
7
|
+
// rule tests/home_bounded_reads.mjs holds Home to. The drawing is
|
|
8
|
+
// home-city-draw.js (pure); this file owns the read, the DOM, the slider and hover.
|
|
9
|
+
//
|
|
10
|
+
// v16 (task 1005058): the city is the one city of every craft (home-city-draw.js), so
|
|
11
|
+
// this file also hands the drawing the Governor City (window.GovCity, city-draw.js)
|
|
12
|
+
// and turns its district doors into craft switches. The time slider is LIVE at its
|
|
13
|
+
// right end; moving it, or the quiet player under it (rewind, play, fast forward),
|
|
14
|
+
// glides the city through every week in between — the owner asked for a rewind, not
|
|
15
|
+
// a jump. That glide is the one motion here driven from script (a requestAnimationFrame
|
|
16
|
+
// loop that redraws the city at a fractional week); under prefers-reduced-motion it
|
|
17
|
+
// does not run at all and every control jumps straight to where it was going. The
|
|
18
|
+
// ladder's rows and a crane's slab still move in CSS, which the hall's rule stops.
|
|
19
|
+
//
|
|
20
|
+
// The ideator's Home (task 1005059): while the craft is Ideating (OTBCraftMode, its
|
|
21
|
+
// otb:craft event) the same widget draws the sky over the city's skyline
|
|
22
|
+
// (home-city-draw.js skySvg) from the same read, and the ladder ranks the week's
|
|
23
|
+
// ideators — ideas filed, votes cast, ideas that became work. Every other craft keeps
|
|
24
|
+
// the builder's city exactly as it was.
|
|
25
|
+
(function () {
|
|
26
|
+
'use strict';
|
|
27
|
+
if (!window.OTB || typeof window.OTB.contributeHallWidget !== 'function') return;
|
|
28
|
+
const Draw = window.OTBHomeCityDraw;
|
|
29
|
+
if (!Draw) return;
|
|
30
|
+
const API = '/api/bongos';
|
|
31
|
+
const WEEKS = 10;
|
|
32
|
+
const RUNG = 40;
|
|
33
|
+
// the player's speeds, in weeks per frame at 60 fps: rewind and fast forward cross the
|
|
34
|
+
// whole slider in about five and four seconds; play is about a week every 1.6 s
|
|
35
|
+
const SPEED = { rw: -1 / 34, play: 1 / 96, ff: 1 / 26 };
|
|
36
|
+
const STILL = () => !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
|
37
|
+
const CRAFTS = [
|
|
38
|
+
['art', 'artistry', 'Art', 'pages designed'],
|
|
39
|
+
['build', 'building', 'Build', 'shipped'],
|
|
40
|
+
['idea', 'ideating', 'Ideas', 'ideas'],
|
|
41
|
+
['gov', 'governing', 'Govern', 'votes and decisions'],
|
|
42
|
+
];
|
|
43
|
+
// the ideator's line: a lightbulb, a star, and an arrow into the work
|
|
44
|
+
const IDEATOR = [
|
|
45
|
+
['idea', 'ideating', 'Ideas', 'ideas filed'],
|
|
46
|
+
['vote', '#vote', 'Votes', 'votes cast'],
|
|
47
|
+
['work', '#work', 'Became work', 'ideas became work'],
|
|
48
|
+
];
|
|
49
|
+
const GLYPH = {
|
|
50
|
+
'#vote': '<path d="M8 2.2l1.7 3.6 3.9.5-2.9 2.7.8 3.9L8 11l-3.5 1.9.8-3.9L2.4 6.3l3.9-.5z"/>',
|
|
51
|
+
'#work': '<path d="M2.5 2.5l7 7M9.5 9.5l-1 4 5-5-4 1z"/>',
|
|
52
|
+
};
|
|
53
|
+
const SKY_LABEL = 'The project\'s ideas as a night sky over the city\'s real skyline, with this week\'s ideators ranked beside it';
|
|
54
|
+
const CITY_LABEL = 'The project as a city: one tower per working area, as tall as what has shipped there, with the builders of the week beside it';
|
|
55
|
+
|
|
56
|
+
let host = null, model = null, viewerId = null, t = 0, target = 0, raf = null, run = null, lastWeek = -1, shown = false, gallery = null;
|
|
57
|
+
const rungs = new Map();
|
|
58
|
+
|
|
59
|
+
function mode() {
|
|
60
|
+
const t = document.documentElement.dataset.theme;
|
|
61
|
+
if (t === 'light' || t === 'dark') return t;
|
|
62
|
+
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function atLive(v) { return !model || v >= model.W - 1 - 1e-3; }
|
|
66
|
+
function weekName(i) {
|
|
67
|
+
if (!model || i === model.W - 1) return 'this week';
|
|
68
|
+
const w = model.weeks[i];
|
|
69
|
+
const d = w && w.start ? new Date(w.start + 'T00:00:00Z') : null;
|
|
70
|
+
return d && !isNaN(d) ? 'week of ' + d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' }) : 'that week';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Ideating draws the sky; every other craft (or none) the builder's city.
|
|
74
|
+
function craft() {
|
|
75
|
+
const CM = window.OTBCraftMode;
|
|
76
|
+
const m = CM && typeof CM.current === 'function' ? CM.current() : document.documentElement.dataset.craft;
|
|
77
|
+
return m === 'ideating' ? 'ideating' : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function craftIcon(key) {
|
|
81
|
+
if (GLYPH[key]) return '<svg class="hc-ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + GLYPH[key] + '</svg>';
|
|
82
|
+
const CM = window.OTBCraftMode;
|
|
83
|
+
return CM && typeof CM.iconHtml === 'function' ? CM.iconHtml(key, 'hc-ico') : '';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---- the scene: the drawing's shapes built as SVG nodes (city.js's split),
|
|
87
|
+
// so every name and title lands as an attribute value or text, never markup ----
|
|
88
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
89
|
+
function build(n) {
|
|
90
|
+
const e = document.createElementNS(NS, n.tag);
|
|
91
|
+
Object.keys(n.attrs).forEach((k) => e.setAttribute(k, String(n.attrs[k])));
|
|
92
|
+
if (n.text != null) e.textContent = n.text;
|
|
93
|
+
n.kids.forEach((c) => e.appendChild(build(c)));
|
|
94
|
+
return e;
|
|
95
|
+
}
|
|
96
|
+
function drawScene() {
|
|
97
|
+
const svg = host.querySelector('.hc-svg');
|
|
98
|
+
const sky = craft() === 'ideating';
|
|
99
|
+
const draw = sky ? Draw.skySvg : Draw.citySvg;
|
|
100
|
+
const out = draw(model, atLive(t) ? model.W - 1 : t, { mode: mode(), gov: window.GovCity || null });
|
|
101
|
+
host.classList.toggle('hc-is-sky', sky);
|
|
102
|
+
svg.setAttribute('aria-label', sky ? SKY_LABEL : CITY_LABEL);
|
|
103
|
+
host.querySelector('#home-city-h').textContent = sky ? 'The project\'s ideas this week' : 'The project this week';
|
|
104
|
+
svg.setAttribute('viewBox', out.viewBox);
|
|
105
|
+
svg.replaceChildren(...out.nodes.map(build));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---- the ladder: a small face and one line split by craft, ranked ----
|
|
109
|
+
function rungFor(row) {
|
|
110
|
+
let r = rungs.get(row.id);
|
|
111
|
+
if (r) return r;
|
|
112
|
+
r = document.createElement('div');
|
|
113
|
+
r.className = 'hc-rung';
|
|
114
|
+
const av = document.createElement('span');
|
|
115
|
+
av.className = 'hc-av' + (row.agent ? ' hc-av--agent' : '');
|
|
116
|
+
if (row.agent) av.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="8" width="14" height="11" rx="3" fill="none" stroke="currentColor" stroke-width="1.6"/><circle cx="9.5" cy="13.5" r="1.5" fill="currentColor"/><circle cx="14.5" cy="13.5" r="1.5" fill="currentColor"/><path d="M12 8V5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
|
|
117
|
+
else {
|
|
118
|
+
av.textContent = Draw.initial(row.name);
|
|
119
|
+
if (row.avatar && /^https:\/\//.test(row.avatar)) {
|
|
120
|
+
const img = document.createElement('img');
|
|
121
|
+
img.src = row.avatar; img.alt = ''; img.loading = 'lazy';
|
|
122
|
+
img.addEventListener('error', () => img.remove());
|
|
123
|
+
av.appendChild(img);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const line = document.createElement('span');
|
|
127
|
+
line.className = 'hc-line';
|
|
128
|
+
r.append(av, line);
|
|
129
|
+
host.querySelector('.hc-ladder').appendChild(r);
|
|
130
|
+
rungs.set(row.id, r);
|
|
131
|
+
return r;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function drawLadder() {
|
|
135
|
+
const week = Math.floor(t + 1e-6);
|
|
136
|
+
const who = craft();
|
|
137
|
+
const rows = Draw.ladderFor(model, week, viewerId, who);
|
|
138
|
+
const kinds = who === 'ideating' ? IDEATOR : CRAFTS;
|
|
139
|
+
const ladder = host.querySelector('.hc-ladder');
|
|
140
|
+
ladder.setAttribute('aria-label', who === 'ideating' ? 'This week\'s ideators, ranked by what they did' : 'This week\'s builders, ranked by what they did');
|
|
141
|
+
ladder.style.height = (Math.max(1, rows.length) * RUNG) + 'px';
|
|
142
|
+
const top = Math.max(1, ...rows.map((r) => r.total));
|
|
143
|
+
const room = Math.max(60, ladder.clientWidth - 40);
|
|
144
|
+
const shown = new Set();
|
|
145
|
+
rows.forEach((row, n) => {
|
|
146
|
+
const r = rungFor(row);
|
|
147
|
+
shown.add(row.id);
|
|
148
|
+
r.hidden = false;
|
|
149
|
+
r.classList.toggle('hc-rung--me', row.me);
|
|
150
|
+
r.style.top = (n * RUNG) + 'px';
|
|
151
|
+
r.dataset.tip = row.name + (row.me ? ' (you)' : '') + ' · ' + row.total;
|
|
152
|
+
r.dataset.tipBody = weekName(week);
|
|
153
|
+
const line = r.querySelector('.hc-line');
|
|
154
|
+
line.textContent = '';
|
|
155
|
+
const crafts = kinds.filter((c) => row.v[c[0]] > 0);
|
|
156
|
+
line.style.width = Math.max(crafts.length * 29, room * row.total / top).toFixed(1) + 'px';
|
|
157
|
+
crafts.forEach((c) => {
|
|
158
|
+
const seg = document.createElement('span');
|
|
159
|
+
seg.className = 'hc-seg';
|
|
160
|
+
seg.style.flex = row.v[c[0]] + ' 1 0';
|
|
161
|
+
seg.dataset.tip = row.name + ' · ' + c[2];
|
|
162
|
+
seg.dataset.tipBody = row.v[c[0]] + ' ' + c[3] + ' ' + weekName(week);
|
|
163
|
+
const lab = document.createElement('b');
|
|
164
|
+
lab.innerHTML = craftIcon(c[1]);
|
|
165
|
+
lab.appendChild(document.createTextNode(String(row.v[c[0]])));
|
|
166
|
+
seg.append(lab, document.createElement('i'));
|
|
167
|
+
line.appendChild(seg);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
rungs.forEach((r, id) => { if (!shown.has(id)) r.hidden = true; });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---- time: live at the right end; the city glides to wherever the slider is sent ----
|
|
174
|
+
function when(v) {
|
|
175
|
+
if (atLive(v)) return 'Live';
|
|
176
|
+
const w0 = model.weeks[0] && model.weeks[0].start ? Date.parse(model.weeks[0].start + 'T00:00:00Z') : NaN;
|
|
177
|
+
return isNaN(w0) ? weekName(Math.floor(v)) : new Date(w0 + v * 7 * 864e5).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' });
|
|
178
|
+
}
|
|
179
|
+
function setT(v) {
|
|
180
|
+
t = Math.max(0, Math.min(model.W - 1, v));
|
|
181
|
+
const live = atLive(t);
|
|
182
|
+
const s = host.querySelector('.hc-slider');
|
|
183
|
+
s.value = String(t);
|
|
184
|
+
s.setAttribute('aria-valuetext', live ? 'Live' : when(t));
|
|
185
|
+
const pill = host.querySelector('.hc-when');
|
|
186
|
+
pill.textContent = when(t);
|
|
187
|
+
pill.style.left = (t / Math.max(1, model.W - 1) * 100) + '%';
|
|
188
|
+
pill.classList.toggle('is-on', !live);
|
|
189
|
+
const l = host.querySelector('.hc-live');
|
|
190
|
+
l.classList.toggle('is-live', live);
|
|
191
|
+
l.querySelector('span').textContent = live ? 'Live' : 'Back to live';
|
|
192
|
+
drawScene();
|
|
193
|
+
const w = Math.floor(t + 1e-6);
|
|
194
|
+
if (w !== lastWeek) { lastWeek = w; drawLadder(); }
|
|
195
|
+
buttons();
|
|
196
|
+
}
|
|
197
|
+
function buttons() {
|
|
198
|
+
const b = (k) => host.querySelector(`.hc-p-${k}`);
|
|
199
|
+
b('play').classList.toggle('is-playing', run === 'play');
|
|
200
|
+
b('play').setAttribute('aria-label', run === 'play' ? 'Pause' : atLive(t) ? 'Play from the start' : 'Play');
|
|
201
|
+
['rw', 'play', 'ff'].forEach((k) => b(k).setAttribute('aria-pressed', String(run === k)));
|
|
202
|
+
b('rw').disabled = t <= 1e-3 && run !== 'rw';
|
|
203
|
+
b('ff').disabled = atLive(t) && run !== 'ff';
|
|
204
|
+
}
|
|
205
|
+
function halt() { if (raf) cancelAnimationFrame(raf); raf = null; run = null; target = t; if (host) buttons(); }
|
|
206
|
+
function tick() {
|
|
207
|
+
raf = requestAnimationFrame(() => {
|
|
208
|
+
let v;
|
|
209
|
+
if (run) { v = t + SPEED[run]; if (v <= 0 || v >= model.W - 1) { v = Math.max(0, Math.min(model.W - 1, v)); run = null; } target = v; }
|
|
210
|
+
else { v = t + (target - t) * 0.16; if (Math.abs(target - v) < 0.004) v = target; }
|
|
211
|
+
setT(v);
|
|
212
|
+
if (run || v !== target) tick(); else { raf = null; buttons(); }
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function glideTo(v) {
|
|
216
|
+
run = null; target = Math.max(0, Math.min(model.W - 1, v));
|
|
217
|
+
if (STILL()) { const v = target; halt(); setT(v); return; }
|
|
218
|
+
if (!raf) tick();
|
|
219
|
+
}
|
|
220
|
+
function press(k) {
|
|
221
|
+
if (run === k) { halt(); return; }
|
|
222
|
+
if (k === 'play' && atLive(t)) setT(0);
|
|
223
|
+
if (STILL()) { halt(); setT(k === 'rw' ? 0 : model.W - 1); return; }
|
|
224
|
+
run = k; buttons();
|
|
225
|
+
if (!raf) tick();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---- the doors: the museum and the stars switch the craft (the craft's own Home); the
|
|
229
|
+
// core is a plain link to the street and a building a plain link to its goal ----
|
|
230
|
+
function wireDoors() {
|
|
231
|
+
host.addEventListener('click', (e) => {
|
|
232
|
+
const d = e.target.closest('[data-craft-door]');
|
|
233
|
+
if (!d || !host.contains(d)) return;
|
|
234
|
+
const CM = window.OTBCraftMode;
|
|
235
|
+
if (!CM || typeof CM.choose !== 'function') return;
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
CM.choose(d.getAttribute('data-craft-door'));
|
|
238
|
+
window.scrollTo(0, 0);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---- one tooltip for the whole widget; labels are ledger data, so text only ----
|
|
243
|
+
function wireTip() {
|
|
244
|
+
let tip = document.getElementById('hc-tip');
|
|
245
|
+
if (!tip) { tip = document.createElement('div'); tip.id = 'hc-tip'; tip.className = 'hc-tip'; tip.setAttribute('role', 'tooltip'); document.body.appendChild(tip); }
|
|
246
|
+
host.addEventListener('pointerover', (e) => {
|
|
247
|
+
const n = e.target.closest('[data-tip]');
|
|
248
|
+
if (!n || !host.contains(n)) return;
|
|
249
|
+
const r = n.getBoundingClientRect();
|
|
250
|
+
tip.textContent = '';
|
|
251
|
+
const b = document.createElement('b'); b.textContent = n.dataset.tip; tip.appendChild(b);
|
|
252
|
+
if (n.dataset.tipBody) tip.appendChild(document.createTextNode(n.dataset.tipBody));
|
|
253
|
+
tip.style.left = (r.left + r.width / 2) + 'px';
|
|
254
|
+
tip.style.top = r.top + 'px';
|
|
255
|
+
tip.classList.add('is-on');
|
|
256
|
+
});
|
|
257
|
+
host.addEventListener('pointerout', (e) => { if (e.target.closest('[data-tip]')) tip.classList.remove('is-on'); });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function ticks() {
|
|
261
|
+
const tk = host.querySelector('.hc-ticks');
|
|
262
|
+
const first = model.weeks[0] && model.weeks[0].start ? new Date(model.weeks[0].start + 'T00:00:00Z') : null;
|
|
263
|
+
const mid = model.weeks[Math.floor(model.W / 2)] && model.weeks[Math.floor(model.W / 2)].start ? new Date(model.weeks[Math.floor(model.W / 2)].start + 'T00:00:00Z') : null;
|
|
264
|
+
const mon = (d) => (d && !isNaN(d) ? d.toLocaleDateString('en-GB', { month: 'short', timeZone: 'UTC' }) : '');
|
|
265
|
+
const live = host.querySelector('.hc-live');
|
|
266
|
+
tk.textContent = '';
|
|
267
|
+
[mon(first), mon(mid)].forEach((s) => { const sp = document.createElement('span'); sp.textContent = s; tk.appendChild(sp); });
|
|
268
|
+
tk.appendChild(live);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function load() {
|
|
272
|
+
const api = window.BongosClient && window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
273
|
+
if (!api) return;
|
|
274
|
+
let res;
|
|
275
|
+
try { res = await api.request('GET', `${API}/home/city?weeks=${WEEKS}`); } catch (_) { return; }
|
|
276
|
+
if (!res || !res.ok || !res.data) return; // signed out, gated or failed: Home keeps its other cards, never an empty plate
|
|
277
|
+
model = Draw.cityModel(res.data);
|
|
278
|
+
viewerId = res.data.viewer && res.data.viewer.id != null ? String(res.data.viewer.id) : null;
|
|
279
|
+
if (!model.W || !model.areas.length) return;
|
|
280
|
+
const s = host.querySelector('.hc-slider');
|
|
281
|
+
s.max = String(model.W - 1);
|
|
282
|
+
ticks();
|
|
283
|
+
shown = true;
|
|
284
|
+
craftHome();
|
|
285
|
+
target = model.W - 1;
|
|
286
|
+
setT(model.W - 1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---- the artist's Home (task 1005060): in Artistry the same section is the gallery,
|
|
290
|
+
// home-gallery.js, the bench in the museum the city's art district opens onto ----
|
|
291
|
+
function craftHome() {
|
|
292
|
+
const art = !!gallery && document.documentElement.dataset.craft === 'artistry';
|
|
293
|
+
host.classList.toggle('hc--gallery', art);
|
|
294
|
+
if (gallery) gallery.show(art);
|
|
295
|
+
host.hidden = !(art || shown);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function mount(hostEl) {
|
|
299
|
+
host = hostEl;
|
|
300
|
+
host.innerHTML =
|
|
301
|
+
'<h2 id="home-city-h" class="sr-only">The project this week</h2>' +
|
|
302
|
+
'<div class="hc-stage">' +
|
|
303
|
+
'<svg class="hc-svg" role="img"></svg>' +
|
|
304
|
+
'<div class="hc-ladder" aria-label="This week\'s builders, ranked by what they did"></div>' +
|
|
305
|
+
'</div>' +
|
|
306
|
+
'<div class="hc-time">' +
|
|
307
|
+
'<div class="hc-player" role="group" aria-label="Time">' +
|
|
308
|
+
'<button type="button" class="hc-p hc-p-rw" aria-label="Rewind"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 3.5v9L2.5 8zM14 3.5v9L8.5 8z" fill="currentColor"/></svg></button>' +
|
|
309
|
+
'<button type="button" class="hc-p hc-p-play" aria-label="Play">' +
|
|
310
|
+
'<svg class="hc-play__on" viewBox="0 0 16 16" aria-hidden="true"><path d="M4.5 3v10l8-5z" fill="currentColor"/></svg>' +
|
|
311
|
+
'<svg class="hc-play__off" viewBox="0 0 16 16" aria-hidden="true"><path d="M4.5 3h2.4v10H4.5zM9.1 3h2.4v10H9.1z" fill="currentColor"/></svg>' +
|
|
312
|
+
'</button>' +
|
|
313
|
+
'<button type="button" class="hc-p hc-p-ff" aria-label="Fast forward"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2 3.5v9L7.5 8zM8 3.5v9L13.5 8z" fill="currentColor"/></svg></button>' +
|
|
314
|
+
'</div>' +
|
|
315
|
+
'<div class="hc-track"><div class="hc-when" aria-hidden="true"></div>' +
|
|
316
|
+
'<input type="range" class="hc-slider" min="0" max="0" step="any" value="0" aria-label="Time, from the start of the version to live">' +
|
|
317
|
+
'<div class="hc-ticks"></div>' +
|
|
318
|
+
'<button type="button" class="hc-live is-live"><i aria-hidden="true"></i><span>Live</span></button>' +
|
|
319
|
+
'</div>' +
|
|
320
|
+
'</div>';
|
|
321
|
+
host.querySelector('.hc-slider').addEventListener('input', (e) => { if (model) glideTo(Number(e.target.value)); });
|
|
322
|
+
['rw', 'play', 'ff'].forEach((k) => host.querySelector(`.hc-p-${k}`).addEventListener('click', () => { if (model) press(k); }));
|
|
323
|
+
host.querySelector('.hc-live').addEventListener('click', () => { if (model) glideTo(model.W - 1); });
|
|
324
|
+
wireTip();
|
|
325
|
+
wireDoors();
|
|
326
|
+
if (window.OTBHomeGallery) gallery = window.OTBHomeGallery.attach(host);
|
|
327
|
+
document.addEventListener('otb:craft', craftHome);
|
|
328
|
+
craftHome();
|
|
329
|
+
new MutationObserver(() => { if (model) drawScene(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
330
|
+
document.addEventListener('otb:craft', () => { if (model) { drawScene(); drawLadder(); } });
|
|
331
|
+
window.addEventListener('resize', () => { if (model) drawLadder(); });
|
|
332
|
+
load();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
window.OTB.contributeHallWidget({
|
|
336
|
+
id: 'home-city', label: 'The project', order: 15, core: true, mount,
|
|
337
|
+
view: 'home', zone: 'home-stats', sectionClass: 'scroll home-city', labelledby: 'home-city-h', hiddenByDefault: true,
|
|
338
|
+
});
|
|
339
|
+
})();
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* modules/hall-ui/public/home-gallery.css — the artist's Home: a bench in the museum
|
|
2
|
+
(task 1005060; mock docs/design/mocks/hall/home/v16-artistry.html, record
|
|
3
|
+
modules/hall-ui/records/home-city.md § "The gallery"). A quiet wall, a floor and a
|
|
4
|
+
bench barely there, and the art the largest thing in the room in a thin frame.
|
|
5
|
+
Every colour is a hall token or a mix of one; the only literal is black with alpha,
|
|
6
|
+
as a shadow. The tokens carry both modes, so no rule here is written per mode.
|
|
7
|
+
No :root; motion is CSS and the hall's reduced-motion rule stops it. */
|
|
8
|
+
|
|
9
|
+
/* home-city.js shows the gallery in Artistry and the city otherwise */
|
|
10
|
+
.home-city.hc--gallery > :not(.hg) { display: none; }
|
|
11
|
+
.home-city:not(.hc--gallery) > .hg { display: none; }
|
|
12
|
+
|
|
13
|
+
.hg-room {
|
|
14
|
+
--hg-wall: color-mix(in srgb, var(--bg-card) 94%, var(--accent));
|
|
15
|
+
--hg-wall-low: color-mix(in srgb, var(--bg) 90%, var(--accent-deep));
|
|
16
|
+
--hg-floor: color-mix(in srgb, var(--bg-deep) 86%, var(--accent-deep));
|
|
17
|
+
--hg-spot: color-mix(in srgb, var(--accent) 14%, transparent);
|
|
18
|
+
--hg-frame: rgba(0, 0, 0, .82);
|
|
19
|
+
--hg-shadow: rgba(0, 0, 0, .45);
|
|
20
|
+
position: relative; height: 760px; overflow: hidden; user-select: none; touch-action: pan-y; outline: none;
|
|
21
|
+
color: var(--ink);
|
|
22
|
+
background: linear-gradient(var(--hg-wall), var(--hg-wall-low) 88%, transparent 88%), var(--hg-floor);
|
|
23
|
+
}
|
|
24
|
+
.hg-room:focus-visible { box-shadow: inset 0 0 0 2px var(--focus, var(--accent)); }
|
|
25
|
+
.hg-room::before { content: ""; position: absolute; inset: 0 0 12% 0; background: radial-gradient(ellipse 40% 64% at 46% 44%, var(--hg-spot), transparent 70%); pointer-events: none; }
|
|
26
|
+
/* the skirting board and the floorboards */
|
|
27
|
+
.hg-room::after { content: ""; position: absolute; left: 0; right: 0; top: 88%; bottom: 0; border-top: 2px solid color-mix(in srgb, var(--hg-wall-low) 82%, var(--ink)); background: repeating-linear-gradient(90deg, transparent 0 160px, color-mix(in srgb, var(--ink) 5%, transparent) 160px 161px); pointer-events: none; }
|
|
28
|
+
.hg-bench { position: absolute; left: 50%; bottom: -30px; width: 46%; height: 44px; transform: translateX(-50%); border-radius: 14px 14px 0 0; background: linear-gradient(color-mix(in srgb, var(--ink) 70%, var(--accent-deep)), var(--ink)); opacity: .22; filter: blur(1.2px); z-index: 3; pointer-events: none; }
|
|
29
|
+
|
|
30
|
+
/* the wall title, in vinyl lettering */
|
|
31
|
+
.hg-vinyl { position: absolute; left: 44px; top: 34px; z-index: 4; }
|
|
32
|
+
.hg-h { margin: 0; font: 300 30px/1.2 var(--font-display); letter-spacing: .01em; color: var(--ink); }
|
|
33
|
+
.hg-sub { margin: 6px 0 0; font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-soft); }
|
|
34
|
+
.hg-sort { display: flex; gap: 18px; margin-top: 16px; }
|
|
35
|
+
.hg-sort button, .hg-way { all: unset; cursor: pointer; box-sizing: border-box; min-height: 24px; min-width: 24px; display: inline-flex; align-items: center; color: var(--ink-soft); }
|
|
36
|
+
.hg-sort button { font-size: 12.5px; letter-spacing: .04em; border-bottom: 1px solid transparent; }
|
|
37
|
+
.hg-sort button[aria-pressed="true"] { color: var(--ink); border-color: var(--ink); }
|
|
38
|
+
.hg-ways { display: flex; flex-wrap: wrap; gap: 4px 22px; margin-top: 22px; }
|
|
39
|
+
.hg-way { font-size: 13px; letter-spacing: .01em; text-decoration: none; gap: 6px; }
|
|
40
|
+
.hg-way:hover, .hg-way[aria-pressed="true"] { color: var(--ink); }
|
|
41
|
+
.hg-sort button:focus-visible, .hg-way:focus-visible { outline: 2px solid var(--focus, var(--accent)); outline-offset: 3px; }
|
|
42
|
+
|
|
43
|
+
/* the wall, one work at a time along it */
|
|
44
|
+
.hg-wall { position: absolute; inset: 0 0 12% 0; overflow: hidden; }
|
|
45
|
+
.hg-track { position: absolute; inset: 0; display: flex; transition: transform .7s cubic-bezier(.22, .7, .2, 1); }
|
|
46
|
+
.hg-track.is-drag { transition: none; }
|
|
47
|
+
.hg-hang { flex: 0 0 100%; position: relative; }
|
|
48
|
+
.hg-work { position: absolute; left: 49%; top: 188px; transform: translateX(-50%); width: min(700px, 56%); transition: opacity .3s; }
|
|
49
|
+
.hg-canvas { position: relative; aspect-ratio: 16 / 10; overflow: hidden; background: var(--bg-deep); border: 3px solid var(--hg-frame); box-shadow: 0 18px 34px -20px var(--hg-shadow); }
|
|
50
|
+
.hg-canvas img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: top center; pointer-events: none; -webkit-user-drag: none; }
|
|
51
|
+
|
|
52
|
+
/* the wall label: small, at the work's foot to its right; click to read it closer */
|
|
53
|
+
.hg-label { position: absolute; left: calc(100% + 28px); bottom: 0; width: 158px; font-size: 10px; line-height: 1.4; color: var(--ink); }
|
|
54
|
+
.hg-text { display: block; cursor: zoom-in; transform-origin: 100% 100%; transition: transform .35s cubic-bezier(.2, .7, .2, 1); border-radius: 4px; outline-offset: 6px; }
|
|
55
|
+
.hg-text:focus-visible { outline: 2px solid var(--focus, var(--accent)); }
|
|
56
|
+
.hg-who { display: block; font-weight: 700; font-size: 10.5px; }
|
|
57
|
+
.hg-bio { display: block; margin-bottom: 6px; color: var(--ink-soft); }
|
|
58
|
+
.hg-title { display: block; }
|
|
59
|
+
.hg-medium, .hg-place { display: block; color: var(--ink-soft); }
|
|
60
|
+
.hg-words, .hg-credit { display: none; margin: 8px 0 0; }
|
|
61
|
+
.hg-credit { color: var(--ink-soft); }
|
|
62
|
+
.hg-hang.is-reading .hg-text { transform: scale(1.85); cursor: zoom-out; }
|
|
63
|
+
.hg-hang.is-reading .hg-words, .hg-hang.is-reading .hg-credit { display: block; }
|
|
64
|
+
.hg-hang.is-reading .hg-work > .hg-canvas { opacity: .4; }
|
|
65
|
+
/* the signature, in the artist's hand, on the wall below the label; the heart never changes size */
|
|
66
|
+
.hg-sigline { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
|
|
67
|
+
.hg-sig { font: 500 17px/1 "Caveat", "Segoe Script", "Bradley Hand", "Snell Roundhand", cursive; color: var(--ink); opacity: .82; transform: rotate(-2deg); white-space: nowrap; }
|
|
68
|
+
.hg-heart { all: unset; cursor: pointer; flex: none; width: 28px; height: 28px; display: grid; place-items: center; border-radius: 50%; color: var(--ink-soft); }
|
|
69
|
+
.hg-heart svg { width: 15px; height: 15px; }
|
|
70
|
+
.hg-heart:hover { color: var(--ink); }
|
|
71
|
+
.hg-heart[aria-pressed="true"] { color: var(--accent-deep); }
|
|
72
|
+
.hg-heart[aria-pressed="true"] path { fill: currentColor; }
|
|
73
|
+
.hg-heart:focus-visible { outline: 2px solid var(--focus, var(--accent)); }
|
|
74
|
+
|
|
75
|
+
/* moving along the wall */
|
|
76
|
+
.hg-edge { all: unset; position: absolute; top: 0; bottom: 12%; width: 9%; min-width: 44px; z-index: 4; cursor: pointer; display: grid; place-items: center; color: var(--ink-soft); opacity: .55; transition: opacity .2s; }
|
|
77
|
+
.hg-edge:hover, .hg-edge:focus-visible { opacity: 1; }
|
|
78
|
+
.hg-edge:focus-visible svg { outline: 2px solid var(--focus, var(--accent)); outline-offset: 6px; border-radius: 50%; }
|
|
79
|
+
.hg-edge svg { width: 28px; height: 28px; fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }
|
|
80
|
+
.hg-prev { left: 0; } .hg-next { right: 0; }
|
|
81
|
+
.hg-edge[disabled] { opacity: 0; pointer-events: none; }
|
|
82
|
+
.hg-count { position: absolute; left: 49%; top: calc(88% - 30px); transform: translateX(-50%); z-index: 4; font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-soft); }
|
|
83
|
+
.hg-empty { position: absolute; left: 50%; top: 46%; transform: translate(-50%, -50%); width: min(440px, 80%); margin: 0; text-align: center; font-size: 14px; color: var(--ink-soft); }
|
|
84
|
+
.hg-room.is-empty .hg-edge, .hg-room.is-empty .hg-fsbtn { display: none; }
|
|
85
|
+
|
|
86
|
+
/* full screen: only the art, its caption, the signature and the heart */
|
|
87
|
+
.hg-fsbtn { all: unset; position: absolute; right: 16px; top: 16px; z-index: 5; width: 32px; height: 32px; display: grid; place-items: center; border-radius: 50%; color: var(--ink-soft); cursor: pointer; opacity: .6; }
|
|
88
|
+
.hg-fsbtn:hover, .hg-fsbtn:focus-visible { opacity: 1; }
|
|
89
|
+
.hg-fsbtn:focus-visible { outline: 2px solid var(--focus, var(--accent)); }
|
|
90
|
+
.hg-fsbtn svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
|
|
91
|
+
.hg-cap { display: none; position: absolute; left: 50%; bottom: 22px; transform: translateX(-50%); font-size: 12px; color: var(--ink-soft); white-space: nowrap; max-width: 60vw; overflow: hidden; text-overflow: ellipsis; }
|
|
92
|
+
.hg-room:fullscreen, .hg-room.is-fs { --hg-spot: transparent; background: var(--hg-wall-low); }
|
|
93
|
+
.hg-room.is-fs { position: fixed; inset: 0; height: auto; z-index: 1000; }
|
|
94
|
+
.hg-room:fullscreen::after, .hg-room.is-fs::after, .hg-room:fullscreen .hg-bench, .hg-room.is-fs .hg-bench,
|
|
95
|
+
.hg-room:fullscreen .hg-vinyl, .hg-room.is-fs .hg-vinyl, .hg-room:fullscreen .hg-count, .hg-room.is-fs .hg-count { display: none; }
|
|
96
|
+
.hg-room:fullscreen .hg-wall, .hg-room.is-fs .hg-wall, .hg-room:fullscreen .hg-edge, .hg-room.is-fs .hg-edge { bottom: 0; }
|
|
97
|
+
.hg-room:fullscreen .hg-work, .hg-room.is-fs .hg-work { left: 50%; top: 50%; transform: translate(-50%, calc(-50% - 22px)); width: min(86vw, calc((100vh - 130px) * 1.6)); }
|
|
98
|
+
.hg-room:fullscreen .hg-canvas, .hg-room.is-fs .hg-canvas { border: 0; box-shadow: none; opacity: 1; }
|
|
99
|
+
.hg-room:fullscreen .hg-label, .hg-room.is-fs .hg-label { position: absolute; left: auto; right: 0; top: calc(100% + 10px); bottom: auto; width: auto; margin: 0; }
|
|
100
|
+
.hg-room:fullscreen .hg-text, .hg-room.is-fs .hg-text { display: none; }
|
|
101
|
+
.hg-room:fullscreen .hg-cap, .hg-room.is-fs .hg-cap { display: block; }
|
|
102
|
+
|
|
103
|
+
/* the full gallery: every work on the wall at once, hung salon-style */
|
|
104
|
+
.hg-salon { position: absolute; left: 44px; right: 44px; top: 210px; bottom: calc(12% + 20px); overflow: auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 34px 30px; align-content: start; padding: 4px; z-index: 4; }
|
|
105
|
+
.hg-salon[hidden] { display: none; }
|
|
106
|
+
.hg-room.is-all .hg-wall, .hg-room.is-all .hg-edge, .hg-room.is-all .hg-count, .hg-room.is-all .hg-fsbtn { display: none; }
|
|
107
|
+
.hg-piece { all: unset; cursor: pointer; display: block; width: 100%; }
|
|
108
|
+
.hg-pc { position: relative; display: block; aspect-ratio: 16 / 10; overflow: hidden; background: var(--bg-deep); border: 2px solid var(--hg-frame); box-shadow: 0 12px 22px -16px var(--hg-shadow); transition: transform .25s; }
|
|
109
|
+
.hg-pc img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: top center; }
|
|
110
|
+
.hg-piece:hover .hg-pc, .hg-piece:focus-visible .hg-pc { transform: translateY(-3px); }
|
|
111
|
+
.hg-piece:focus-visible { outline: 2px solid var(--focus, var(--accent)); outline-offset: 6px; }
|
|
112
|
+
.hg-pt { display: block; margin-top: 9px; font-size: 10.5px; color: var(--ink); }
|
|
113
|
+
.hg-pl { display: flex; align-items: center; gap: 5px; margin-top: 2px; font-size: 10px; color: var(--ink-soft); }
|
|
114
|
+
.hg-pl svg { width: 11px; height: 11px; }
|
|
115
|
+
|
|
116
|
+
@media (max-width: 860px) {
|
|
117
|
+
.hg-room { height: 660px; }
|
|
118
|
+
.hg-vinyl { left: 20px; right: 20px; top: 20px; }
|
|
119
|
+
.hg-h { font-size: 24px; }
|
|
120
|
+
.hg-work { top: 196px; left: 50%; width: 86%; }
|
|
121
|
+
.hg-label { position: static; width: auto; margin-top: 14px; }
|
|
122
|
+
/* a phone has no room to scale the label: read closer, it opens over the dimmed picture */
|
|
123
|
+
.hg-hang.is-reading .hg-text { transform: none; }
|
|
124
|
+
.hg-hang.is-reading .hg-label { position: absolute; left: 0; right: 0; top: 0; margin: 0; font-size: 12px; }
|
|
125
|
+
.hg-hang.is-reading .hg-who { font-size: 12.5px; }
|
|
126
|
+
.hg-hang.is-reading .hg-work > .hg-canvas { opacity: 0; }
|
|
127
|
+
.hg-count { left: 50%; }
|
|
128
|
+
.hg-salon { left: 20px; right: 20px; top: 196px; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 22px 16px; }
|
|
129
|
+
}
|