@deskwork/studio 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pages/dashboard/affordances.d.ts +19 -24
- package/dist/pages/dashboard/affordances.d.ts.map +1 -1
- package/dist/pages/dashboard/affordances.js +37 -36
- package/dist/pages/dashboard/affordances.js.map +1 -1
- package/dist/pages/dashboard/affordances.ts +41 -40
- package/dist/pages/dashboard/header.d.ts +9 -3
- package/dist/pages/dashboard/header.d.ts.map +1 -1
- package/dist/pages/dashboard/header.js +9 -29
- package/dist/pages/dashboard/header.js.map +1 -1
- package/dist/pages/dashboard/header.ts +9 -31
- package/dist/pages/dashboard/section.d.ts +42 -15
- package/dist/pages/dashboard/section.d.ts.map +1 -1
- package/dist/pages/dashboard/section.js +118 -69
- package/dist/pages/dashboard/section.js.map +1 -1
- package/dist/pages/dashboard/section.ts +120 -74
- package/dist/pages/dashboard.d.ts +23 -17
- package/dist/pages/dashboard.d.ts.map +1 -1
- package/dist/pages/dashboard.js +105 -25
- package/dist/pages/dashboard.js.map +1 -1
- package/dist/pages/dashboard.ts +107 -26
- package/dist/pages/help.js +1 -1
- package/dist/pages/help.ts +1 -1
- package/dist/pages/index.d.ts +8 -6
- package/dist/pages/index.d.ts.map +1 -1
- package/dist/pages/index.js +16 -14
- package/dist/pages/index.js.map +1 -1
- package/dist/pages/index.ts +16 -14
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +11 -0
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Single-stage section renderer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* with a section heading (stage name + entry count) and either
|
|
6
|
-
* of rows or an empty-state placeholder.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Each of the eight stage sections (plus the Distribution placeholder)
|
|
5
|
+
* renders with a section heading (stage name + entry count) and either
|
|
6
|
+
* a list of rows or an empty-state placeholder. Each row carries the
|
|
7
|
+
* entry's slug, title, updated-at timestamp, and stage-gated verb
|
|
8
|
+
* buttons. Per DESKWORK-STATE-MACHINE.md Commandment III, rows do NOT
|
|
9
|
+
* surface iteration counts or reviewState — those were retired in
|
|
10
|
+
* v0.19 along with the legacy reviewState concept.
|
|
11
|
+
*
|
|
12
|
+
* On mobile, each section is fronted by a collapsible tile (see
|
|
13
|
+
* `renderStageTile`); on desktop the tiles are display:none and the
|
|
14
|
+
* `<h2 class="er-section-head">` heading carries the stage name.
|
|
10
15
|
*/
|
|
11
16
|
import { html, unsafe } from "../html.js";
|
|
12
|
-
import {
|
|
17
|
+
import { renderRowActions } from "./affordances.js";
|
|
13
18
|
const STAGE_ORNAMENTS = {
|
|
14
19
|
Ideas: '◇',
|
|
15
20
|
Planned: '§',
|
|
@@ -34,13 +39,16 @@ const STAGE_EMPTY_MESSAGES = {
|
|
|
34
39
|
* Render one entry as a single dashboard row. Carries inline:
|
|
35
40
|
* - slug (linked to the review surface)
|
|
36
41
|
* - title
|
|
37
|
-
* - iteration count for the entry's currentStage
|
|
38
|
-
* - reviewState badge (or an em-dash placeholder)
|
|
39
42
|
* - updatedAt timestamp
|
|
40
43
|
* - per-stage action buttons
|
|
44
|
+
*
|
|
45
|
+
* Per DESKWORK-STATE-MACHINE.md (v5): revisions (the iteration counter)
|
|
46
|
+
* are bookkeeping and do NOT surface in routine UI. The previous
|
|
47
|
+
* "iteration: N" inline display was a violation — operators see
|
|
48
|
+
* revisions only via the View History surface and revert flows.
|
|
49
|
+
* reviewState badges are likewise retired (Commandment III).
|
|
41
50
|
*/
|
|
42
51
|
export function renderRow(entry, index, defaultSite) {
|
|
43
|
-
const iteration = iterationForCurrentStage(entry);
|
|
44
52
|
const reviewLink = `/dev/editorial-review/entry/${entry.uuid}`;
|
|
45
53
|
const search = [entry.slug, entry.title, entry.keywords.join(' ')].join(' ').toLowerCase();
|
|
46
54
|
// Hierarchical entries (slugs containing `/`) get a visual indent
|
|
@@ -58,93 +66,134 @@ export function renderRow(entry, index, defaultSite) {
|
|
|
58
66
|
<span class="er-row-slug"><a href="${reviewLink}"
|
|
59
67
|
title="open the review surface">${entry.slug}</a></span>
|
|
60
68
|
<span class="er-calendar-title">${entry.title}</span>
|
|
61
|
-
<span class="er-calendar-meta er-calendar-meta-iteration"
|
|
62
|
-
data-iteration="${iteration}">iteration: ${iteration}</span>
|
|
63
69
|
<time class="er-calendar-meta er-calendar-meta-updated" data-format="date"
|
|
64
70
|
datetime="${entry.updatedAt}" title="${entry.updatedAt}">${formatDate(entry.updatedAt)}</time>
|
|
65
71
|
</div>
|
|
66
|
-
|
|
72
|
+
<span class="er-calendar-status" aria-hidden="true"></span>
|
|
67
73
|
${renderRowActions(entry, defaultSite)}
|
|
68
74
|
</div>
|
|
69
75
|
</div>`);
|
|
70
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Render the stage tile (mobile-only collapsible head). Hidden on desktop
|
|
79
|
+
* via dashboard-mobile.css; the existing `<h2 class="er-section-head">`
|
|
80
|
+
* carries the head on desktop and is hidden at <=600px so the tile takes
|
|
81
|
+
* over.
|
|
82
|
+
*
|
|
83
|
+
* Empty stages render the same tile shape but with `is-empty` styling and
|
|
84
|
+
* `disabled` so taps are no-ops (operator can still SEE the empty stage
|
|
85
|
+
* in the pipeline shape — they just can't drill in to nothing).
|
|
86
|
+
*
|
|
87
|
+
* Review-state sub-counts (e.g. "5 · 3 in review") were removed in v0.19
|
|
88
|
+
* per operator: review state isn't user-facing data and is slated for
|
|
89
|
+
* backend removal; the tile shows total entry count only.
|
|
90
|
+
*/
|
|
91
|
+
function renderStageTile(stage, count) {
|
|
92
|
+
const isEmpty = count === 0;
|
|
93
|
+
const classes = isEmpty ? 'er-stage-tile is-empty' : 'er-stage-tile';
|
|
94
|
+
const disabledAttr = isEmpty ? ' disabled' : '';
|
|
95
|
+
return unsafe(html `
|
|
96
|
+
<button class="${classes}" type="button"
|
|
97
|
+
data-stage-tile="${stage}"
|
|
98
|
+
aria-expanded="false"
|
|
99
|
+
aria-controls="stage-${stage.toLowerCase()}"${unsafe(disabledAttr)}>
|
|
100
|
+
<span class="er-stage-tile-glyph" aria-hidden="true">${STAGE_ORNAMENTS[stage]}</span>
|
|
101
|
+
<span class="er-stage-tile-name">${stage}</span>
|
|
102
|
+
<span class="er-stage-tile-count"><span class="num">${count}</span></span>
|
|
103
|
+
<span class="er-stage-tile-chev" aria-hidden="true">›</span>
|
|
104
|
+
</button>`);
|
|
105
|
+
}
|
|
71
106
|
/**
|
|
72
107
|
* Render one full stage section: heading + ornaments + count + rows.
|
|
73
108
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
109
|
+
* The output is wrapped in a `.er-stage-block` div that pairs a mobile-
|
|
110
|
+
* only stage tile (the collapsible head) with the existing section. On
|
|
111
|
+
* desktop, the tile is `display: none` and the section's `<h2>` head
|
|
112
|
+
* carries the heading as before. On mobile, the section's head is hidden
|
|
113
|
+
* and the tile is shown; tapping the tile toggles a `data-collapsed`
|
|
114
|
+
* attribute on the section that hides/shows its rows. Single-expand
|
|
115
|
+
* (tapping one tile collapses the others) is handled by
|
|
116
|
+
* `dashboard/stage-tiles.ts`.
|
|
117
|
+
*
|
|
118
|
+
* Empty stages still render their tile (so the pipeline shape is visible
|
|
119
|
+
* at-rest on phone) but the empty section body itself is hidden on mobile.
|
|
120
|
+
*
|
|
121
|
+
* Empty stages on desktop render compact (just the heading, no placeholder
|
|
122
|
+
* body) — keeps the operator's sense of pipeline shape without padding
|
|
123
|
+
* the dashboard with multi-line empty placeholders for low-volume
|
|
77
124
|
* calendars (#112). The hover title still surfaces the stage's
|
|
78
125
|
* "what to run next" hint when the operator points at the heading.
|
|
79
126
|
*/
|
|
80
127
|
export function renderStageSection(stage, entries, defaultSite) {
|
|
128
|
+
const tile = renderStageTile(stage, entries.length);
|
|
81
129
|
if (entries.length === 0) {
|
|
82
130
|
return unsafe(html `
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
131
|
+
<div class="er-stage-block" data-stage-block="${stage}">
|
|
132
|
+
${tile}
|
|
133
|
+
<section class="er-section er-section--empty"
|
|
134
|
+
id="stage-${stage.toLowerCase()}" data-stage-section="${stage}"
|
|
135
|
+
data-empty-stage="${stage}">
|
|
136
|
+
<h2 class="er-section-head er-section-head--empty"
|
|
137
|
+
title="${STAGE_EMPTY_MESSAGES[stage]}">
|
|
138
|
+
<span>${stage}</span>
|
|
139
|
+
<span class="ornament">${STAGE_ORNAMENTS[stage]}</span>
|
|
140
|
+
<span class="count">№ 00</span>
|
|
141
|
+
</h2>
|
|
142
|
+
</section>
|
|
143
|
+
</div>`);
|
|
93
144
|
}
|
|
94
145
|
const body = unsafe(entries.map((e, i) => renderRow(e, i, defaultSite).__raw).join(''));
|
|
95
146
|
return unsafe(html `
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
147
|
+
<div class="er-stage-block" data-stage-block="${stage}">
|
|
148
|
+
${tile}
|
|
149
|
+
<section class="er-section" id="stage-${stage.toLowerCase()}" data-stage-section="${stage}">
|
|
150
|
+
<h2 class="er-section-head">
|
|
151
|
+
<span>${stage}</span>
|
|
152
|
+
<span class="ornament">${STAGE_ORNAMENTS[stage]}</span>
|
|
153
|
+
<span class="count">№ ${entries.length}</span>
|
|
154
|
+
</h2>
|
|
155
|
+
${body}
|
|
156
|
+
</section>
|
|
157
|
+
</div>`);
|
|
104
158
|
}
|
|
105
159
|
/**
|
|
106
|
-
* Render the reserved Distribution placeholder.
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
160
|
+
* Render the reserved Distribution placeholder. Distribution isn't a
|
|
161
|
+
* pipeline stage in the formal sense (no entries flow through it; it
|
|
162
|
+
* lives under its own model when shortform cross-posts arrive), but on
|
|
163
|
+
* the mobile dashboard it renders as a stage tile alongside the rest
|
|
164
|
+
* so the operator's pipeline-shape scan stays uniform — see operator
|
|
165
|
+
* feedback on 2026-05-09. The tile is `is-empty` + `disabled` until
|
|
166
|
+
* DistributionRecords land in the data layer.
|
|
167
|
+
*
|
|
168
|
+
* On desktop, the existing section + heading + placeholder text render
|
|
169
|
+
* as before; the tile is `display: none` per dashboard-mobile.css.
|
|
110
170
|
*/
|
|
111
171
|
export function renderDistributionPlaceholder() {
|
|
112
172
|
return unsafe(html `
|
|
113
|
-
<
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
173
|
+
<div class="er-stage-block" data-stage-block="Distribution">
|
|
174
|
+
<button class="er-stage-tile is-empty" type="button"
|
|
175
|
+
data-stage-tile="Distribution"
|
|
176
|
+
aria-expanded="false"
|
|
177
|
+
aria-controls="stage-distribution" disabled>
|
|
178
|
+
<span class="er-stage-tile-glyph" aria-hidden="true">⌘</span>
|
|
179
|
+
<span class="er-stage-tile-name">Distribution</span>
|
|
180
|
+
<span class="er-stage-tile-count"><span class="num">0</span></span>
|
|
181
|
+
<span class="er-stage-tile-chev" aria-hidden="true">›</span>
|
|
182
|
+
</button>
|
|
183
|
+
<section class="er-section" id="stage-distribution" data-stage-section="Distribution">
|
|
184
|
+
<h2 class="er-section-head">
|
|
185
|
+
<span>Distribution</span>
|
|
186
|
+
<span class="ornament">⌘</span>
|
|
187
|
+
</h2>
|
|
188
|
+
<div class="er-empty" style="padding: 1rem 0.25rem; font-size: 0.95rem;">
|
|
189
|
+
Reserved for shortform DistributionRecords — separate model.
|
|
190
|
+
</div>
|
|
191
|
+
</section>
|
|
192
|
+
</div>`);
|
|
122
193
|
}
|
|
123
194
|
function formatDate(iso) {
|
|
124
195
|
// Trim to YYYY-MM-DD for compact display. Full timestamp is on the
|
|
125
196
|
// <span title="...">.
|
|
126
197
|
return iso.slice(0, 10);
|
|
127
198
|
}
|
|
128
|
-
/**
|
|
129
|
-
* Render the status column for one row.
|
|
130
|
-
*
|
|
131
|
-
* #243 — when the entry has no reviewState, render an EMPTY span (no
|
|
132
|
-
* `<a>`, no em-dash placeholder, no stamp box). Operators were reading
|
|
133
|
-
* the previous box-with-em-dash as a button-like action; the slug-link
|
|
134
|
-
* in the row body already routes to the review surface, so the badge
|
|
135
|
-
* slot only carries information when there's a real reviewState.
|
|
136
|
-
*
|
|
137
|
-
* Grid alignment is preserved because CSS Grid auto-sizes columns from
|
|
138
|
-
* the widest cell — empty cells leave their slot blank without breaking
|
|
139
|
-
* sibling alignment.
|
|
140
|
-
*/
|
|
141
|
-
function renderStatusCell(entry, reviewLink) {
|
|
142
|
-
if (entry.reviewState === undefined) {
|
|
143
|
-
return unsafe('<span class="er-calendar-status" aria-hidden="true"></span>');
|
|
144
|
-
}
|
|
145
|
-
return unsafe(html `
|
|
146
|
-
<span class="er-calendar-status"><a href="${reviewLink}"
|
|
147
|
-
title="open the review surface for ${entry.slug}"
|
|
148
|
-
class="er-stamp-link">${renderReviewStateBadge(entry.reviewState)}</a></span>`);
|
|
149
|
-
}
|
|
150
199
|
//# sourceMappingURL=section.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"section.js","sourceRoot":"","sources":["../../../src/pages/dashboard/section.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"section.js","sourceRoot":"","sources":["../../../src/pages/dashboard/section.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Single-stage section renderer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* with a section heading (stage name + entry count) and either
|
|
6
|
-
* of rows or an empty-state placeholder.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Each of the eight stage sections (plus the Distribution placeholder)
|
|
5
|
+
* renders with a section heading (stage name + entry count) and either
|
|
6
|
+
* a list of rows or an empty-state placeholder. Each row carries the
|
|
7
|
+
* entry's slug, title, updated-at timestamp, and stage-gated verb
|
|
8
|
+
* buttons. Per DESKWORK-STATE-MACHINE.md Commandment III, rows do NOT
|
|
9
|
+
* surface iteration counts or reviewState — those were retired in
|
|
10
|
+
* v0.19 along with the legacy reviewState concept.
|
|
11
|
+
*
|
|
12
|
+
* On mobile, each section is fronted by a collapsible tile (see
|
|
13
|
+
* `renderStageTile`); on desktop the tiles are display:none and the
|
|
14
|
+
* `<h2 class="er-section-head">` heading carries the stage name.
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
17
|
import { html, unsafe, type RawHtml } from '../html.ts';
|
|
13
18
|
import type { Entry, Stage } from '@deskwork/core/schema/entry';
|
|
14
|
-
import {
|
|
15
|
-
iterationForCurrentStage,
|
|
16
|
-
renderReviewStateBadge,
|
|
17
|
-
renderRowActions,
|
|
18
|
-
} from './affordances.ts';
|
|
19
|
+
import { renderRowActions } from './affordances.ts';
|
|
19
20
|
|
|
20
21
|
const STAGE_ORNAMENTS: Record<Stage, string> = {
|
|
21
22
|
Ideas: '◇',
|
|
@@ -43,13 +44,16 @@ const STAGE_EMPTY_MESSAGES: Record<Stage, string> = {
|
|
|
43
44
|
* Render one entry as a single dashboard row. Carries inline:
|
|
44
45
|
* - slug (linked to the review surface)
|
|
45
46
|
* - title
|
|
46
|
-
* - iteration count for the entry's currentStage
|
|
47
|
-
* - reviewState badge (or an em-dash placeholder)
|
|
48
47
|
* - updatedAt timestamp
|
|
49
48
|
* - per-stage action buttons
|
|
49
|
+
*
|
|
50
|
+
* Per DESKWORK-STATE-MACHINE.md (v5): revisions (the iteration counter)
|
|
51
|
+
* are bookkeeping and do NOT surface in routine UI. The previous
|
|
52
|
+
* "iteration: N" inline display was a violation — operators see
|
|
53
|
+
* revisions only via the View History surface and revert flows.
|
|
54
|
+
* reviewState badges are likewise retired (Commandment III).
|
|
50
55
|
*/
|
|
51
56
|
export function renderRow(entry: Entry, index: number, defaultSite: string): RawHtml {
|
|
52
|
-
const iteration = iterationForCurrentStage(entry);
|
|
53
57
|
const reviewLink = `/dev/editorial-review/entry/${entry.uuid}`;
|
|
54
58
|
const search = [entry.slug, entry.title, entry.keywords.join(' ')].join(' ').toLowerCase();
|
|
55
59
|
// Hierarchical entries (slugs containing `/`) get a visual indent
|
|
@@ -69,23 +73,63 @@ export function renderRow(entry: Entry, index: number, defaultSite: string): Raw
|
|
|
69
73
|
<span class="er-row-slug"><a href="${reviewLink}"
|
|
70
74
|
title="open the review surface">${entry.slug}</a></span>
|
|
71
75
|
<span class="er-calendar-title">${entry.title}</span>
|
|
72
|
-
<span class="er-calendar-meta er-calendar-meta-iteration"
|
|
73
|
-
data-iteration="${iteration}">iteration: ${iteration}</span>
|
|
74
76
|
<time class="er-calendar-meta er-calendar-meta-updated" data-format="date"
|
|
75
77
|
datetime="${entry.updatedAt}" title="${entry.updatedAt}">${formatDate(entry.updatedAt)}</time>
|
|
76
78
|
</div>
|
|
77
|
-
|
|
79
|
+
<span class="er-calendar-status" aria-hidden="true"></span>
|
|
78
80
|
${renderRowActions(entry, defaultSite)}
|
|
79
81
|
</div>
|
|
80
82
|
</div>`);
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Render the stage tile (mobile-only collapsible head). Hidden on desktop
|
|
87
|
+
* via dashboard-mobile.css; the existing `<h2 class="er-section-head">`
|
|
88
|
+
* carries the head on desktop and is hidden at <=600px so the tile takes
|
|
89
|
+
* over.
|
|
90
|
+
*
|
|
91
|
+
* Empty stages render the same tile shape but with `is-empty` styling and
|
|
92
|
+
* `disabled` so taps are no-ops (operator can still SEE the empty stage
|
|
93
|
+
* in the pipeline shape — they just can't drill in to nothing).
|
|
94
|
+
*
|
|
95
|
+
* Review-state sub-counts (e.g. "5 · 3 in review") were removed in v0.19
|
|
96
|
+
* per operator: review state isn't user-facing data and is slated for
|
|
97
|
+
* backend removal; the tile shows total entry count only.
|
|
98
|
+
*/
|
|
99
|
+
function renderStageTile(stage: Stage, count: number): RawHtml {
|
|
100
|
+
const isEmpty = count === 0;
|
|
101
|
+
const classes = isEmpty ? 'er-stage-tile is-empty' : 'er-stage-tile';
|
|
102
|
+
const disabledAttr = isEmpty ? ' disabled' : '';
|
|
103
|
+
return unsafe(html`
|
|
104
|
+
<button class="${classes}" type="button"
|
|
105
|
+
data-stage-tile="${stage}"
|
|
106
|
+
aria-expanded="false"
|
|
107
|
+
aria-controls="stage-${stage.toLowerCase()}"${unsafe(disabledAttr)}>
|
|
108
|
+
<span class="er-stage-tile-glyph" aria-hidden="true">${STAGE_ORNAMENTS[stage]}</span>
|
|
109
|
+
<span class="er-stage-tile-name">${stage}</span>
|
|
110
|
+
<span class="er-stage-tile-count"><span class="num">${count}</span></span>
|
|
111
|
+
<span class="er-stage-tile-chev" aria-hidden="true">›</span>
|
|
112
|
+
</button>`);
|
|
113
|
+
}
|
|
114
|
+
|
|
83
115
|
/**
|
|
84
116
|
* Render one full stage section: heading + ornaments + count + rows.
|
|
85
117
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
118
|
+
* The output is wrapped in a `.er-stage-block` div that pairs a mobile-
|
|
119
|
+
* only stage tile (the collapsible head) with the existing section. On
|
|
120
|
+
* desktop, the tile is `display: none` and the section's `<h2>` head
|
|
121
|
+
* carries the heading as before. On mobile, the section's head is hidden
|
|
122
|
+
* and the tile is shown; tapping the tile toggles a `data-collapsed`
|
|
123
|
+
* attribute on the section that hides/shows its rows. Single-expand
|
|
124
|
+
* (tapping one tile collapses the others) is handled by
|
|
125
|
+
* `dashboard/stage-tiles.ts`.
|
|
126
|
+
*
|
|
127
|
+
* Empty stages still render their tile (so the pipeline shape is visible
|
|
128
|
+
* at-rest on phone) but the empty section body itself is hidden on mobile.
|
|
129
|
+
*
|
|
130
|
+
* Empty stages on desktop render compact (just the heading, no placeholder
|
|
131
|
+
* body) — keeps the operator's sense of pipeline shape without padding
|
|
132
|
+
* the dashboard with multi-line empty placeholders for low-volume
|
|
89
133
|
* calendars (#112). The hover title still surfaces the stage's
|
|
90
134
|
* "what to run next" hint when the operator points at the heading.
|
|
91
135
|
*/
|
|
@@ -94,50 +138,75 @@ export function renderStageSection(
|
|
|
94
138
|
entries: readonly Entry[],
|
|
95
139
|
defaultSite: string,
|
|
96
140
|
): RawHtml {
|
|
141
|
+
const tile = renderStageTile(stage, entries.length);
|
|
142
|
+
|
|
97
143
|
if (entries.length === 0) {
|
|
98
144
|
return unsafe(html`
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
145
|
+
<div class="er-stage-block" data-stage-block="${stage}">
|
|
146
|
+
${tile}
|
|
147
|
+
<section class="er-section er-section--empty"
|
|
148
|
+
id="stage-${stage.toLowerCase()}" data-stage-section="${stage}"
|
|
149
|
+
data-empty-stage="${stage}">
|
|
150
|
+
<h2 class="er-section-head er-section-head--empty"
|
|
151
|
+
title="${STAGE_EMPTY_MESSAGES[stage]}">
|
|
152
|
+
<span>${stage}</span>
|
|
153
|
+
<span class="ornament">${STAGE_ORNAMENTS[stage]}</span>
|
|
154
|
+
<span class="count">№ 00</span>
|
|
155
|
+
</h2>
|
|
156
|
+
</section>
|
|
157
|
+
</div>`);
|
|
109
158
|
}
|
|
110
159
|
|
|
111
160
|
const body = unsafe(entries.map((e, i) => renderRow(e, i, defaultSite).__raw).join(''));
|
|
112
161
|
|
|
113
162
|
return unsafe(html`
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
163
|
+
<div class="er-stage-block" data-stage-block="${stage}">
|
|
164
|
+
${tile}
|
|
165
|
+
<section class="er-section" id="stage-${stage.toLowerCase()}" data-stage-section="${stage}">
|
|
166
|
+
<h2 class="er-section-head">
|
|
167
|
+
<span>${stage}</span>
|
|
168
|
+
<span class="ornament">${STAGE_ORNAMENTS[stage]}</span>
|
|
169
|
+
<span class="count">№ ${entries.length}</span>
|
|
170
|
+
</h2>
|
|
171
|
+
${body}
|
|
172
|
+
</section>
|
|
173
|
+
</div>`);
|
|
122
174
|
}
|
|
123
175
|
|
|
124
176
|
/**
|
|
125
|
-
* Render the reserved Distribution placeholder.
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
177
|
+
* Render the reserved Distribution placeholder. Distribution isn't a
|
|
178
|
+
* pipeline stage in the formal sense (no entries flow through it; it
|
|
179
|
+
* lives under its own model when shortform cross-posts arrive), but on
|
|
180
|
+
* the mobile dashboard it renders as a stage tile alongside the rest
|
|
181
|
+
* so the operator's pipeline-shape scan stays uniform — see operator
|
|
182
|
+
* feedback on 2026-05-09. The tile is `is-empty` + `disabled` until
|
|
183
|
+
* DistributionRecords land in the data layer.
|
|
184
|
+
*
|
|
185
|
+
* On desktop, the existing section + heading + placeholder text render
|
|
186
|
+
* as before; the tile is `display: none` per dashboard-mobile.css.
|
|
129
187
|
*/
|
|
130
188
|
export function renderDistributionPlaceholder(): RawHtml {
|
|
131
189
|
return unsafe(html`
|
|
132
|
-
<
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
190
|
+
<div class="er-stage-block" data-stage-block="Distribution">
|
|
191
|
+
<button class="er-stage-tile is-empty" type="button"
|
|
192
|
+
data-stage-tile="Distribution"
|
|
193
|
+
aria-expanded="false"
|
|
194
|
+
aria-controls="stage-distribution" disabled>
|
|
195
|
+
<span class="er-stage-tile-glyph" aria-hidden="true">⌘</span>
|
|
196
|
+
<span class="er-stage-tile-name">Distribution</span>
|
|
197
|
+
<span class="er-stage-tile-count"><span class="num">0</span></span>
|
|
198
|
+
<span class="er-stage-tile-chev" aria-hidden="true">›</span>
|
|
199
|
+
</button>
|
|
200
|
+
<section class="er-section" id="stage-distribution" data-stage-section="Distribution">
|
|
201
|
+
<h2 class="er-section-head">
|
|
202
|
+
<span>Distribution</span>
|
|
203
|
+
<span class="ornament">⌘</span>
|
|
204
|
+
</h2>
|
|
205
|
+
<div class="er-empty" style="padding: 1rem 0.25rem; font-size: 0.95rem;">
|
|
206
|
+
Reserved for shortform DistributionRecords — separate model.
|
|
207
|
+
</div>
|
|
208
|
+
</section>
|
|
209
|
+
</div>`);
|
|
141
210
|
}
|
|
142
211
|
|
|
143
212
|
function formatDate(iso: string): string {
|
|
@@ -145,26 +214,3 @@ function formatDate(iso: string): string {
|
|
|
145
214
|
// <span title="...">.
|
|
146
215
|
return iso.slice(0, 10);
|
|
147
216
|
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Render the status column for one row.
|
|
151
|
-
*
|
|
152
|
-
* #243 — when the entry has no reviewState, render an EMPTY span (no
|
|
153
|
-
* `<a>`, no em-dash placeholder, no stamp box). Operators were reading
|
|
154
|
-
* the previous box-with-em-dash as a button-like action; the slug-link
|
|
155
|
-
* in the row body already routes to the review surface, so the badge
|
|
156
|
-
* slot only carries information when there's a real reviewState.
|
|
157
|
-
*
|
|
158
|
-
* Grid alignment is preserved because CSS Grid auto-sizes columns from
|
|
159
|
-
* the widest cell — empty cells leave their slot blank without breaking
|
|
160
|
-
* sibling alignment.
|
|
161
|
-
*/
|
|
162
|
-
function renderStatusCell(entry: Entry, reviewLink: string): RawHtml {
|
|
163
|
-
if (entry.reviewState === undefined) {
|
|
164
|
-
return unsafe('<span class="er-calendar-status" aria-hidden="true"></span>');
|
|
165
|
-
}
|
|
166
|
-
return unsafe(html`
|
|
167
|
-
<span class="er-calendar-status"><a href="${reviewLink}"
|
|
168
|
-
title="open the review surface for ${entry.slug}"
|
|
169
|
-
class="er-stamp-link">${renderReviewStateBadge(entry.reviewState)}</a></span>`);
|
|
170
|
-
}
|
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Studio dashboard page — `/dev/editorial-studio`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* The dashboard renders eight stage sections — Ideas → Planned →
|
|
5
|
+
* Outlining → Drafting → Final → Published, plus Blocked and
|
|
6
|
+
* Cancelled — backed by sidecar reads under
|
|
7
|
+
* `<projectRoot>/.deskwork/entries/*.json`, with a Distribution
|
|
8
|
+
* placeholder pinned at the end. Each row carries the entry's slug,
|
|
9
|
+
* title, updated-at timestamp, and stage-gated verb buttons that
|
|
10
|
+
* clipboard-copy `/deskwork:<verb> <slug>` (THESIS Consequence 2 —
|
|
11
|
+
* the studio routes commands; skills do the work). On phone (≤600px)
|
|
12
|
+
* each stage section is collapsed by default and fronted by a tile
|
|
13
|
+
* (see Compact-1 in DESIGN-STANDARDS.md); on desktop everything is
|
|
14
|
+
* expanded with the existing `<h2 class="er-section-head">` heading
|
|
15
|
+
* carrying the stage name.
|
|
11
16
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
17
|
+
* Per DESKWORK-STATE-MACHINE.md (Commandment III), reviewState is
|
|
18
|
+
* RETIRED. Rows do NOT carry per-stage iteration counts or
|
|
19
|
+
* reviewState badges; that legacy "at-a-glance" surfacing was
|
|
20
|
+
* removed in v0.19.
|
|
15
21
|
*
|
|
16
22
|
* The renderer's data flow:
|
|
17
23
|
* 1. loadDashboardData reads every sidecar and groups by stage.
|
|
18
|
-
* 2. Each
|
|
19
|
-
* 3. The Distribution placeholder
|
|
24
|
+
* 2. Each stage renders via `renderStageSection`.
|
|
25
|
+
* 3. The Distribution placeholder renders below the stage sections.
|
|
26
|
+
* 4. The mobile-only Compose chrome (FAB + slide-up sheet) renders
|
|
27
|
+
* at the page tail; CSS hides it on desktop.
|
|
20
28
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* the new dashboard does not currently consume it (sidecars are the
|
|
25
|
-
* data source, not the on-disk content tree).
|
|
29
|
+
* `getIndex` is preserved for signature compatibility with the
|
|
30
|
+
* override resolver in server.ts; the dashboard does not consume it
|
|
31
|
+
* (sidecars are the data source, not the on-disk content tree).
|
|
26
32
|
*/
|
|
27
33
|
import type { StudioContext } from '../routes/api.ts';
|
|
28
34
|
import type { ContentIndex } from '@deskwork/core/content-index';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/pages/dashboard.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/pages/dashboard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAUtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEjE;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,YAAY,CAAC;AAElE;;;GAGG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,aAAa,EAClB,QAAQ,CAAC,EAAE,oBAAoB,GAC9B,OAAO,CAAC,MAAM,CAAC,CA0CjB"}
|