@deskwork/studio 0.18.0 → 0.19.0

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.
@@ -1,15 +1,20 @@
1
1
  /**
2
2
  * Single-stage section renderer.
3
3
  *
4
- * Pipeline-redesign Task 34. Each of the eight stage sections renders
5
- * with a section heading (stage name + entry count) and either a list
6
- * of rows or an empty-state placeholder. Per-row HTML carries the
7
- * sidecar-derived state inline (iteration count + reviewState badge)
8
- * so an operator can see at a glance where each entry sits without
9
- * opening it.
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 { iterationForCurrentStage, renderReviewStateBadge, renderRowActions, } from "./affordances.js";
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
- ${renderStatusCell(entry, reviewLink)}
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
- * Empty stages render compact (just the heading, no placeholder body)
75
- * keeps the operator's sense of pipeline shape without padding the
76
- * dashboard with multi-line empty placeholders for low-volume
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
- <section class="er-section er-section--empty"
84
- id="stage-${stage.toLowerCase()}" data-stage-section="${stage}"
85
- data-empty-stage="${stage}">
86
- <h2 class="er-section-head er-section-head--empty"
87
- title="${STAGE_EMPTY_MESSAGES[stage]}">
88
- <span>${stage}</span>
89
- <span class="ornament">${STAGE_ORNAMENTS[stage]}</span>
90
- <span class="count">№ 00</span>
91
- </h2>
92
- </section>`);
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
- <section class="er-section" id="stage-${stage.toLowerCase()}" data-stage-section="${stage}">
97
- <h2 class="er-section-head">
98
- <span>${stage}</span>
99
- <span class="ornament">${STAGE_ORNAMENTS[stage]}</span>
100
- <span class="count">№ ${entries.length}</span>
101
- </h2>
102
- ${body}
103
- </section>`);
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. Stays a separate
107
- * sibling of the stage sections distribution records (shortform
108
- * cross-posts) are tracked under their own model and the dashboard
109
- * surfaces only a placeholder here until that integration lands.
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
- <section class="er-section" id="stage-distribution" data-stage-section="Distribution">
114
- <h2 class="er-section-head">
115
- <span>Distribution</span>
116
- <span class="ornament">⌘</span>
117
- </h2>
118
- <div class="er-empty" style="padding: 1rem 0.25rem; font-size: 0.95rem;">
119
- Reserved for shortform DistributionRecords — separate model.
120
- </div>
121
- </section>`);
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":"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"}
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
- * Pipeline-redesign Task 34. Each of the eight stage sections renders
5
- * with a section heading (stage name + entry count) and either a list
6
- * of rows or an empty-state placeholder. Per-row HTML carries the
7
- * sidecar-derived state inline (iteration count + reviewState badge)
8
- * so an operator can see at a glance where each entry sits without
9
- * opening it.
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
- ${renderStatusCell(entry, reviewLink)}
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
- * Empty stages render compact (just the heading, no placeholder body)
87
- * keeps the operator's sense of pipeline shape without padding the
88
- * dashboard with multi-line empty placeholders for low-volume
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
- <section class="er-section er-section--empty"
100
- id="stage-${stage.toLowerCase()}" data-stage-section="${stage}"
101
- data-empty-stage="${stage}">
102
- <h2 class="er-section-head er-section-head--empty"
103
- title="${STAGE_EMPTY_MESSAGES[stage]}">
104
- <span>${stage}</span>
105
- <span class="ornament">${STAGE_ORNAMENTS[stage]}</span>
106
- <span class="count">№ 00</span>
107
- </h2>
108
- </section>`);
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
- <section class="er-section" id="stage-${stage.toLowerCase()}" data-stage-section="${stage}">
115
- <h2 class="er-section-head">
116
- <span>${stage}</span>
117
- <span class="ornament">${STAGE_ORNAMENTS[stage]}</span>
118
- <span class="count">№ ${entries.length}</span>
119
- </h2>
120
- ${body}
121
- </section>`);
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. Stays a separate
126
- * sibling of the stage sections distribution records (shortform
127
- * cross-posts) are tracked under their own model and the dashboard
128
- * surfaces only a placeholder here until that integration lands.
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
- <section class="er-section" id="stage-distribution" data-stage-section="Distribution">
133
- <h2 class="er-section-head">
134
- <span>Distribution</span>
135
- <span class="ornament">⌘</span>
136
- </h2>
137
- <div class="er-empty" style="padding: 1rem 0.25rem; font-size: 0.95rem;">
138
- Reserved for shortform DistributionRecords — separate model.
139
- </div>
140
- </section>`);
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
- * Pipeline-redesign Task 34. The dashboard renders eight stage
5
- * sections — Ideas PlannedOutliningDrafting Final
6
- * Published, plus Blocked and Cancelled — backed by sidecar reads
7
- * under `<projectRoot>/.deskwork/entries/*.json`. Each row carries
8
- * the entry's iteration count for its current stage and a
9
- * reviewState badge so an operator can see at a glance where each
10
- * entry sits without opening it.
4
+ * The dashboard renders eight stage sections — Ideas → Planned →
5
+ * OutliningDraftingFinalPublished, 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
- * Replaces the legacy calendar.md + workflow store rendering. The
13
- * scaffold (folio, masthead, filter strip, layout) is preserved so
14
- * existing CSS keeps working.
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 of the eight stages renders via `renderStageSection`.
19
- * 3. The Distribution placeholder pins beneath the stage sections.
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
- * The legacy export `renderDashboard` stays server.ts wires it as
22
- * the page handler. The `getIndex` parameter is preserved for
23
- * signature compatibility with the override resolver in server.ts;
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;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAWtD,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,CAwCjB"}
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"}