konpeki 0.3.0 → 0.4.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.
Files changed (162) hide show
  1. package/AGENTS.md +21 -28
  2. package/AUTHORING.md +68 -229
  3. package/CONTRIBUTING.md +10 -23
  4. package/README.md +96 -87
  5. package/SETUP.md +50 -114
  6. package/docs/development.md +43 -248
  7. package/docs/workflow.md +6 -108
  8. package/html/README.md +140 -0
  9. package/html/browser.ts +103 -0
  10. package/html/document.ts +38 -0
  11. package/html/floor.ts +34 -0
  12. package/html/index.html +5 -0
  13. package/html/inspect.ts +313 -0
  14. package/html/preview.css +62 -0
  15. package/html/preview.tsx +463 -0
  16. package/html/review-hints.ts +63 -0
  17. package/html/server.ts +99 -0
  18. package/html/source.ts +104 -0
  19. package/html/starter.ts +6 -0
  20. package/html/theme-authoring.md +176 -0
  21. package/html/theme.md +61 -0
  22. package/index.html +9 -9
  23. package/package.json +21 -35
  24. package/plugin.json +1 -1
  25. package/public/og.png +0 -0
  26. package/runtime/browser-B-29TH1a.mjs +595 -0
  27. package/runtime/floor-Cmk7G3pU.mjs +41 -0
  28. package/runtime/konpeki.mjs +90 -1410
  29. package/runtime/server-BAqC_5WD.mjs +2 -0
  30. package/runtime/server-DfRpfcY9.mjs +205 -0
  31. package/runtime/source-B_Ui9tMp.mjs +141 -0
  32. package/runtime/source-CZQq9GUO.mjs +2 -0
  33. package/skills/konpeki/SKILL.md +88 -172
  34. package/skills/konpeki/assets/blank.html +17 -0
  35. package/skills/konpeki/floor.md +76 -0
  36. package/skills/konpeki/references/cover.md +27 -0
  37. package/skills/konpeki/references/long-document.md +40 -0
  38. package/skills/konpeki/references/one-pager.md +27 -0
  39. package/skills/konpeki/references/patterns.md +118 -0
  40. package/skills/konpeki/references/resume.md +28 -0
  41. package/skills/konpeki/references/slides.md +28 -0
  42. package/skills/konpeki/scripts/ensure-runtime.mjs +10 -31
  43. package/skills/konpeki/scripts/prepare-document.mjs +25 -14
  44. package/src/components/PageBoard.tsx +156 -0
  45. package/src/lib/alignment.ts +21 -0
  46. package/src/lib/page-board.ts +25 -0
  47. package/src/lib/review-position.ts +19 -0
  48. package/src/styles/base.css +4 -6
  49. package/src/styles/feedback.css +97 -61
  50. package/src/styles/shell.css +95 -323
  51. package/theme-base.css +90 -0
  52. package/theme.css +56 -0
  53. package/vite.config.ts +2 -5
  54. package/composition/README.md +0 -156
  55. package/composition/compile.ts +0 -227
  56. package/composition/document.ts +0 -600
  57. package/composition/schema.json +0 -3001
  58. package/composition/schema.ts +0 -437
  59. package/composition/theme-tokens.ts +0 -16
  60. package/composition/types.ts +0 -269
  61. package/composition/validate.ts +0 -226
  62. package/composition/vector.ts +0 -143
  63. package/composition/visualizations.ts +0 -319
  64. package/design/README.md +0 -17
  65. package/design/palettes/README.md +0 -14
  66. package/design/palettes/base.ts +0 -14
  67. package/design/palettes/candidates.ts +0 -19
  68. package/design/palettes/index.ts +0 -78
  69. package/design/review/color-theme.md +0 -44
  70. package/design/review/layout.md +0 -16
  71. package/design/review/text.md +0 -18
  72. package/design/review/typography.md +0 -15
  73. package/design/review/visuals.md +0 -31
  74. package/design/semantic-patterns.md +0 -43
  75. package/design/themes/README.md +0 -40
  76. package/design/themes/index.ts +0 -24
  77. package/design/visual-languages/technical-product.md +0 -17
  78. package/design/visual-review.md +0 -88
  79. package/lib/assets.d.ts +0 -8
  80. package/lib/charts.ts +0 -18
  81. package/lib/contrast.ts +0 -16
  82. package/lib/layouts.ts +0 -50
  83. package/lib/slide.tsx +0 -42
  84. package/lib/taste.ts +0 -17
  85. package/lib/text.tsx +0 -89
  86. package/lib/typeface.ts +0 -44
  87. package/scripts/migrate-react-page.ts +0 -120
  88. package/skills/konpeki/assets/blank.json +0 -23
  89. package/slides/README.md +0 -153
  90. package/slides/architecture/PROMPT.md +0 -31
  91. package/slides/architecture/index.tsx +0 -102
  92. package/slides/article-brief/PROMPT.md +0 -35
  93. package/slides/article-brief/index.tsx +0 -71
  94. package/slides/bar-chart/PROMPT.md +0 -39
  95. package/slides/bar-chart/index.tsx +0 -97
  96. package/slides/comparison/PROMPT.md +0 -29
  97. package/slides/comparison/index.tsx +0 -95
  98. package/slides/decision-memo/PROMPT.md +0 -34
  99. package/slides/decision-memo/index.tsx +0 -85
  100. package/slides/delivery-plan/PROMPT.md +0 -45
  101. package/slides/delivery-plan/index.tsx +0 -105
  102. package/slides/experiment/PROMPT.md +0 -44
  103. package/slides/experiment/index.tsx +0 -127
  104. package/slides/incident-workflow/PROMPT.md +0 -57
  105. package/slides/incident-workflow/index.tsx +0 -78
  106. package/slides/introducing-konpeki/PROMPT.md +0 -40
  107. package/slides/introducing-konpeki/README.md +0 -76
  108. package/slides/introducing-konpeki/SOURCE.md +0 -26
  109. package/slides/introducing-konpeki/author.ts +0 -165
  110. package/slides/introducing-konpeki/composition.json +0 -3270
  111. package/slides/line-chart/PROMPT.md +0 -40
  112. package/slides/line-chart/index.tsx +0 -72
  113. package/slides/migration/PROMPT.md +0 -38
  114. package/slides/migration/index.tsx +0 -89
  115. package/slides/og-images/PROMPT.md +0 -21
  116. package/slides/og-images/index.tsx +0 -76
  117. package/slides/product-introduction/PROMPT.md +0 -24
  118. package/slides/product-introduction/index.tsx +0 -105
  119. package/slides/research-brief/PROMPT.md +0 -40
  120. package/slides/research-brief/index.tsx +0 -104
  121. package/slides/results-explanation/PROMPT.md +0 -32
  122. package/slides/results-explanation/index.tsx +0 -96
  123. package/slides/retrospective/PROMPT.md +0 -43
  124. package/slides/retrospective/index.tsx +0 -105
  125. package/slides/sankey/PROMPT.md +0 -11
  126. package/slides/sankey/index.tsx +0 -93
  127. package/slides/teaching/PROMPT.md +0 -45
  128. package/slides/teaching/index.tsx +0 -124
  129. package/slides/vertical-bar-charts/PROMPT.md +0 -13
  130. package/slides/vertical-bar-charts/index.tsx +0 -97
  131. package/src/app/App.tsx +0 -820
  132. package/src/components/BuildOrb.tsx +0 -40
  133. package/src/components/Canvas.tsx +0 -1185
  134. package/src/components/DiagramTypeIcon.tsx +0 -78
  135. package/src/components/InspectorPanel.tsx +0 -773
  136. package/src/components/LeftPanel.tsx +0 -120
  137. package/src/components/PageSizePicker.tsx +0 -30
  138. package/src/components/Presentation.tsx +0 -105
  139. package/src/components/RevisionNotes.tsx +0 -56
  140. package/src/components/RightPanel.tsx +0 -201
  141. package/src/components/VectorOverflowWarning.tsx +0 -46
  142. package/src/components/WorkspaceChrome.tsx +0 -288
  143. package/src/components/ui.tsx +0 -53
  144. package/src/lib/examples/react-page-migration.json +0 -1295
  145. package/src/lib/examples.ts +0 -42
  146. package/src/lib/export-png.ts +0 -104
  147. package/src/lib/file-session.ts +0 -87
  148. package/src/lib/history.ts +0 -53
  149. package/src/lib/model.ts +0 -188
  150. package/src/lib/page-size.ts +0 -24
  151. package/src/lib/presentation.ts +0 -17
  152. package/src/lib/review.ts +0 -26
  153. package/src/lib/storage.ts +0 -71
  154. package/src/lib/theme.ts +0 -25
  155. package/src/lib/use-file-session.ts +0 -227
  156. package/src/main.tsx +0 -29
  157. package/src/styles/canvas.css +0 -299
  158. package/src/styles/chrome.css +0 -384
  159. package/src/styles/component-previews.css +0 -386
  160. package/src/styles/left-panel.css +0 -166
  161. package/src/styles/presentation.css +0 -72
  162. package/src/styles/right-panel.css +0 -1215
@@ -1,773 +0,0 @@
1
- import {
2
- themeIds,
3
- typographyIds,
4
- type CompositionComponent,
5
- type CompositionSlide,
6
- type ChartTemplate,
7
- type DiagramType,
8
- type Rect,
9
- type TextColor,
10
- type ThemeId,
11
- type TypographyId,
12
- type VectorElement,
13
- } from "../../composition/types.ts";
14
- import {
15
- chartDefinitions,
16
- chartTemplates,
17
- diagramCategories,
18
- diagramDefinitions,
19
- } from "../../composition/visualizations.ts";
20
- import { useEffect, useRef, type CSSProperties } from "react";
21
- import {
22
- appearanceOptions,
23
- tableAppearanceForStyle,
24
- tableStyleForAppearance,
25
- tableStyles,
26
- type TableStyle,
27
- } from "../../composition/schema.ts";
28
- import {
29
- clamp,
30
- componentLabels,
31
- defaultAppearance,
32
- type Draft,
33
- } from "../lib/model.ts";
34
- import { composerPalette, themeLabel } from "../lib/theme.ts";
35
- import { getTheme, typographyLabels } from "../../design/themes/index.ts";
36
- import { DiagramTypeIcon } from "./DiagramTypeIcon.tsx";
37
- import { Field, Select, humanize } from "./ui.tsx";
38
- import { moveVectorElement, removeVectorElement } from "../../composition/vector.ts";
39
- import { tokensForAttribute } from "../../composition/theme-tokens.ts";
40
- import { VectorOverflowWarning } from "./VectorOverflowWarning.tsx";
41
- import { PageSizePicker } from "./PageSizePicker.tsx";
42
-
43
- const appearanceLabels: Record<string, string> = {
44
- rule: "Divider",
45
- style: "Number format",
46
- };
47
- const optionLabels: Record<string, string> = {
48
- "01": "Current page — 01",
49
- "01/02": "Current / total — 01/02",
50
- single: "Single",
51
- "two-column": "2 equal columns",
52
- "three-column": "3 equal columns",
53
- "two-plus-two": "2 + 2 grid",
54
- "four-column": "4 columns",
55
- "one-plus-three": "1 + 3 blocks",
56
- "three-plus-one": "3 + 1 blocks",
57
- "header-rule": "Rule under header",
58
- "top-header-bottom": "Top + header + bottom",
59
- "row-rules": "Horizontal rules",
60
- "full-grid": "Full grid",
61
- };
62
-
63
- function VectorActionIcon({ action }: { action: "backward" | "forward" | "delete" }) {
64
- return (
65
- <svg className="vector-action-icon" viewBox="0 0 20 20" aria-hidden="true">
66
- {action === "delete" ? (
67
- <>
68
- <path d="M4 5h12M8 2.75h4M6 5l.65 11h6.7L14 5" />
69
- <path d="M8.25 8v5M11.75 8v5" />
70
- </>
71
- ) : (
72
- <>
73
- <rect x="3" y="7" width="8" height="8" rx="1.5" />
74
- <path d="M7 4h6a2 2 0 0 1 2 2v6" />
75
- {action === "backward" ? (
76
- <path d="m12.5 10 2.5 2.5 2.5-2.5" />
77
- ) : (
78
- <path d="m12.5 6.5 2.5-2.5 2.5 2.5" />
79
- )}
80
- </>
81
- )}
82
- </svg>
83
- );
84
- }
85
-
86
- function previewCount(key: string, value: string) {
87
- if (key === "tableStyle") return 6;
88
- if (key !== "layout") return 4;
89
- if (value === "single") return 1;
90
- if (value === "two-column") return 2;
91
- if (value === "three-column") return 3;
92
- return 4;
93
- }
94
-
95
- function AppearancePicker({
96
- label,
97
- name,
98
- value,
99
- options,
100
- labels,
101
- description,
102
- disabledOptions,
103
- onChange,
104
- }: {
105
- label: string;
106
- name: string;
107
- value: string;
108
- options: readonly string[];
109
- labels?: Partial<Record<string, string>>;
110
- description?: string;
111
- disabledOptions?: ReadonlySet<string>;
112
- onChange: (value: string) => void;
113
- }) {
114
- return (
115
- <div className="field appearance-field">
116
- <span>{label}</span>
117
- <div className="appearance-options" role="group" aria-label={label}>
118
- {options.map((option) => (
119
- <button
120
- key={option}
121
- type="button"
122
- className={value === option ? "selected" : ""}
123
- aria-pressed={value === option}
124
- disabled={disabledOptions?.has(option)}
125
- title={disabledOptions?.has(option) ? "Remove the recorded Sankey topology before changing chart type." : undefined}
126
- onClick={() => onChange(option)}
127
- >
128
- <span
129
- className="appearance-preview"
130
- data-property={name}
131
- data-value={option}
132
- aria-hidden="true"
133
- >
134
- {Array.from({ length: previewCount(name, option) }, (_, index) => (
135
- <i key={index} />
136
- ))}
137
- </span>
138
- <span>{labels?.[option] ?? optionLabels[option] ?? humanize(option)}</span>
139
- </button>
140
- ))}
141
- </div>
142
- {description && <small className="visualization-description">{description}</small>}
143
- </div>
144
- );
145
- }
146
-
147
- function PalettePicker({
148
- value,
149
- mode,
150
- onChange,
151
- }: {
152
- value: ThemeId;
153
- mode: "paper" | "night";
154
- onChange: (value: ThemeId) => void;
155
- }) {
156
- return (
157
- <fieldset className="theme-picker">
158
- <legend>Color palette</legend>
159
- <div className="theme-options">
160
- {themeIds.map((id) => {
161
- const palette = composerPalette(id, mode);
162
- return (
163
- <button
164
- key={id}
165
- type="button"
166
- className={value === id ? "selected" : ""}
167
- data-theme={id}
168
- aria-pressed={value === id}
169
- onClick={() => onChange(id)}
170
- >
171
- <span>{themeLabel(id)}</span>
172
- <span className="theme-swatches" aria-hidden="true">
173
- {[palette.accent, palette.categorical[1], palette.categorical[2]].map(
174
- (color) => (
175
- <i key={color} style={{ background: color }} />
176
- ),
177
- )}
178
- </span>
179
- </button>
180
- );
181
- })}
182
- </div>
183
- </fieldset>
184
- );
185
- }
186
-
187
- function SegmentedControl({ label, value, options, onChange }: {
188
- label: string;
189
- value: string;
190
- options: readonly string[];
191
- onChange: (value: string) => void;
192
- }) {
193
- return (
194
- <fieldset className="choice-picker">
195
- <legend>{label}</legend>
196
- <div className="segmented-options" style={{ gridTemplateColumns: `repeat(${options.length}, 1fr)` }}>
197
- {options.map((option) => (
198
- <button key={option} type="button" aria-pressed={value === option}
199
- className={value === option ? "selected" : ""}
200
- onClick={() => onChange(option)}>
201
- <span>{humanize(option)}</span>
202
- </button>
203
- ))}
204
- </div>
205
- </fieldset>
206
- );
207
- }
208
-
209
- function AutoTemplateButton({ selected, onClick }: { selected: boolean; onClick: () => void }) {
210
- return (
211
- <button type="button" className={selected ? "selected" : ""} aria-pressed={selected} onClick={onClick}>
212
- <svg className="diagram-type-icon" viewBox="0 0 48 32" aria-hidden="true">
213
- <rect x="13" y="5" width="22" height="22" rx="4" />
214
- <circle className="filled" cx="19" cy="11" r="1.5" />
215
- <circle className="filled" cx="29" cy="11" r="1.5" />
216
- <circle className="filled" cx="24" cy="16" r="1.5" />
217
- <circle className="filled" cx="19" cy="21" r="1.5" />
218
- <circle className="filled" cx="29" cy="21" r="1.5" />
219
- </svg>
220
- <span>YOLO</span>
221
- </button>
222
- );
223
- }
224
-
225
- function DiagramTypePicker({
226
- value,
227
- selection,
228
- onChange,
229
- }: {
230
- value: DiagramType;
231
- selection: "auto" | "explicit";
232
- onChange: (value: DiagramType, selection: "auto" | "explicit") => void;
233
- }) {
234
- return (
235
- <details className="field diagram-type-field">
236
- <summary id="diagram-type-label">Template</summary>
237
- <div className="diagram-type-options" aria-labelledby="diagram-type-label">
238
- <div className="diagram-type-category">
239
- <div>
240
- <AutoTemplateButton selected={selection === "auto"} onClick={() => onChange(value, "auto")} />
241
- </div>
242
- </div>
243
- {diagramCategories.map((category) => (
244
- <div className="diagram-type-category" role="group" aria-label={category} key={category}>
245
- <span>{category}</span>
246
- <div>
247
- {Object.entries(diagramDefinitions)
248
- .filter(([, definition]) => definition.category === category)
249
- .map(([type, definition]) => (
250
- <button
251
- type="button"
252
- className={selection === "explicit" && value === type ? "selected" : ""}
253
- aria-pressed={selection === "explicit" && value === type}
254
- key={type}
255
- onClick={() => onChange(type as DiagramType, "explicit")}
256
- >
257
- <DiagramTypeIcon type={type as DiagramType} />
258
- <span>{definition.label}</span>
259
- </button>
260
- ))}
261
- </div>
262
- </div>
263
- ))}
264
- </div>
265
- </details>
266
- );
267
- }
268
-
269
- function ChartTypePicker({ value, selection, hasTopology, onChange }: {
270
- value: ChartTemplate;
271
- selection: "auto" | "explicit";
272
- hasTopology: boolean;
273
- onChange: (value: ChartTemplate, selection: "auto" | "explicit") => void;
274
- }) {
275
- return (
276
- <details className="field chart-type-field diagram-type-field">
277
- <summary id="chart-type-label">Template</summary>
278
- <div className="diagram-type-options chart-type-options" aria-labelledby="chart-type-label">
279
- <div className="diagram-type-category">
280
- <div>
281
- <AutoTemplateButton selected={selection === "auto"} onClick={() => onChange(value, "auto")} />
282
- </div>
283
- </div>
284
- <div className="diagram-type-category" role="group" aria-label="Chart templates">
285
- <div>
286
- {chartTemplates.map((template) => (
287
- <button
288
- key={template}
289
- type="button"
290
- className={selection === "explicit" && value === template ? "selected" : ""}
291
- aria-pressed={selection === "explicit" && value === template}
292
- disabled={hasTopology && template !== "sankey"}
293
- title={hasTopology && template !== "sankey"
294
- ? "Remove the recorded Sankey topology before changing chart type."
295
- : chartDefinitions[template].expression}
296
- onClick={() => onChange(template, "explicit")}
297
- >
298
- <span className="appearance-preview" data-property="template" data-value={template} aria-hidden="true">
299
- {template === "scatter" ? (
300
- <svg viewBox="0 0 36 24">
301
- <path d="M3 2v20h31" fill="none" stroke="var(--preview-muted)" strokeWidth="1.2" />
302
- {[[7, 6], [10, 11], [14, 9], [17, 13], [20, 16], [24, 15], [28, 18], [29, 7], [32, 19]].map(([cx, cy], index) => (
303
- <circle key={index} cx={cx} cy={cy} r="1.35" fill="var(--preview-accent)" />
304
- ))}
305
- </svg>
306
- ) : template === "sankey" ? (
307
- <svg viewBox="0 0 36 24" fill="var(--preview-accent)">
308
- <g fillOpacity="0.3">
309
- <path d="M4 4C10.5 4 10.5 2 17 2V12.5C10.5 12.5 10.5 14.5 4 14.5Z" />
310
- <path d="M4 14.5C10.5 14.5 10.5 17.5 17 17.5V22C10.5 22 10.5 19 4 19Z" />
311
- <path d="M19 8C25.5 8 25.5 10 32 10V14.5C25.5 14.5 25.5 12.5 19 12.5Z" />
312
- <path d="M19 17.5C25.5 17.5 25.5 18 32 18V22.5C25.5 22.5 25.5 22 19 22Z" />
313
- </g>
314
- <path d="M19 2C25.5 2 25.5 1 32 1V7C25.5 7 25.5 8 19 8Z" fillOpacity="0.65" />
315
- <rect x="2" y="4" width="2" height="15" />
316
- <rect x="17" y="2" width="2" height="10.5" />
317
- <rect x="17" y="17.5" width="2" height="4.5" />
318
- <rect x="32" y="1" width="2" height="6" />
319
- <rect x="32" y="10" width="2" height="4.5" />
320
- <rect x="32" y="18" width="2" height="4.5" />
321
- </svg>
322
- ) : Array.from({ length: 4 }, (_, index) => <i key={index} />)}
323
- </span>
324
- <span>{chartDefinitions[template].label}</span>
325
- </button>
326
- ))}
327
- </div>
328
- </div>
329
- </div>
330
- {hasTopology && <small className="visualization-description">Recorded Sankey topology is preserved; remove it before changing chart type, even in YOLO.</small>}
331
- </details>
332
- );
333
- }
334
-
335
- export function InspectorPanel({
336
- component,
337
- componentLabel,
338
- slide,
339
- draft,
340
- selectedVectorElementId,
341
- vectorEditRequest,
342
- onSelectSlide,
343
- onSelectVectorElement,
344
- onComponent,
345
- onSlide,
346
- onDraft,
347
- onEditEnd,
348
- }: {
349
- component?: CompositionComponent;
350
- componentLabel?: string;
351
- slide: CompositionSlide;
352
- draft: Draft;
353
- selectedVectorElementId?: string;
354
- vectorEditRequest?: number;
355
- onSelectSlide: () => void;
356
- onSelectVectorElement: (id?: string) => void;
357
- onComponent: (component: CompositionComponent, mergeKey?: string) => void;
358
- onSlide: (slide: CompositionSlide, mergeKey?: string) => void;
359
- onDraft: (patch: Partial<Draft>, mergeKey?: string) => void;
360
- onEditEnd: () => void;
361
- }) {
362
- const vectorEditor = useRef<HTMLDivElement>(null);
363
- useEffect(() => {
364
- if (!vectorEditRequest) return;
365
- const editor = vectorEditor.current;
366
- const control = editor?.querySelector<HTMLTextAreaElement | HTMLInputElement>("textarea, input");
367
- const target = control ?? editor;
368
- target?.focus();
369
- target?.scrollIntoView({ block: "nearest" });
370
- control?.select();
371
- }, [vectorEditRequest]);
372
- const theme = draft.theme ?? { id: "plex", mode: "paper" };
373
- const typography = getTheme(themeLabel(theme.id), theme.mode, theme.typography);
374
- if (!component)
375
- return (
376
- <section className="inspector-content" aria-labelledby="inspector-heading">
377
- <h2 id="inspector-heading">Page</h2>
378
- <PageSizePicker key={slide.id} slide={slide} onChange={onSlide} />
379
- <h2>Document appearance</h2>
380
- <Select
381
- label="Font"
382
- value={typography.typographyId}
383
- options={typographyIds}
384
- optionLabels={typographyLabels}
385
- onChange={(value) => onDraft({ theme: { ...theme, typography: value as TypographyId } })}
386
- />
387
- <PalettePicker
388
- value={theme.id}
389
- mode={theme.mode}
390
- onChange={(value) =>
391
- onDraft({
392
- theme: {
393
- ...theme,
394
- id: value,
395
- typography: typography.typographyId,
396
- },
397
- })
398
- }
399
- />
400
- <SegmentedControl
401
- label="Background"
402
- value={theme.mode}
403
- options={["paper", "night"]}
404
- onChange={(value) =>
405
- onDraft({
406
- theme: {
407
- ...theme,
408
- mode: value as "paper" | "night",
409
- },
410
- })
411
- }
412
- />
413
- <VectorOverflowWarning slide={slide} theme={draft.theme} />
414
- <h2 className="slide-settings-heading">Page numbering</h2>
415
- <fieldset className="page-number-picker">
416
- <SegmentedControl
417
- label="Format"
418
- value={slide.pageNumber?.style ?? "none"}
419
- options={["none", "01", "01/02"]}
420
- onChange={(value) =>
421
- onSlide({
422
- ...slide,
423
- pageNumber: {
424
- style: value as "none" | "01" | "01/02",
425
- color: slide.pageNumber?.color ?? "muted",
426
- },
427
- })
428
- }
429
- />
430
- {(slide.pageNumber?.style ?? "none") !== "none" && (
431
- <SegmentedControl
432
- label="Color"
433
- value={slide.pageNumber?.color ?? "muted"}
434
- options={["ink", "muted", "accent"]}
435
- onChange={(value) =>
436
- onSlide({
437
- ...slide,
438
- pageNumber: {
439
- style: slide.pageNumber?.style ?? "01",
440
- color: value as TextColor,
441
- },
442
- })
443
- }
444
- />
445
- )}
446
- </fieldset>
447
- </section>
448
- );
449
- const rect = component.preferredRect;
450
- const appearance = component.appearance as Record<string, string> | undefined;
451
- const defaults = defaultAppearance(component.kind);
452
- const previewPalette = composerPalette(draft.theme?.id ?? "plex", draft.theme?.mode ?? "paper");
453
- const vectorVisual = component.customVisual?.format === "vector"
454
- ? component.customVisual
455
- : undefined;
456
- const vectorElement = vectorVisual?.elements.find(
457
- (element) => element.id === selectedVectorElementId,
458
- );
459
- function updateVectorElement(
460
- element: VectorElement,
461
- mergeKey = `vector:${component!.id}:${element.id}`,
462
- ) {
463
- if (!vectorVisual) return;
464
- onComponent({
465
- ...component!,
466
- customVisual: {
467
- ...vectorVisual,
468
- elements: vectorVisual.elements.map((item) =>
469
- item.id === element.id ? element : item,
470
- ),
471
- },
472
- }, mergeKey);
473
- }
474
- function geometry(key: keyof Rect, value: number) {
475
- if (!Number.isFinite(value)) return;
476
- const next = { ...rect };
477
- if (key === "x") next.x = clamp(value, 0, slide.canvas.width - rect.width);
478
- if (key === "y") next.y = clamp(value, 0, slide.canvas.height - rect.height);
479
- if (key === "width")
480
- next.width = clamp(value, Math.min(180, slide.canvas.width - rect.x), slide.canvas.width - rect.x);
481
- if (key === "height")
482
- next.height = clamp(value, Math.min(72, slide.canvas.height - rect.y), slide.canvas.height - rect.y);
483
- onComponent(
484
- { ...component!, preferredRect: next },
485
- `geometry:${component!.id}`,
486
- );
487
- }
488
- return (
489
- <section className={`inspector-content${component.kind === "text-block" && !component.customVisual ? " native-text-inspector" : ""}`} aria-labelledby="inspector-heading"
490
- style={{
491
- "--preview-ink": previewPalette.fg,
492
- "--preview-muted": previewPalette.line,
493
- "--preview-accent": previewPalette.accent,
494
- "--preview-secondary": previewPalette.categorical[1],
495
- "--preview-tertiary": previewPalette.categorical[2],
496
- "--preview-complete": previewPalette.status.complete,
497
- "--preview-attention": previewPalette.status.attention,
498
- "--preview-blocked": previewPalette.status.blocked,
499
- } as CSSProperties}
500
- >
501
- <h2 id="inspector-heading" className="inspector-context">
502
- <button type="button" onClick={onSelectSlide}>Page</button>
503
- <span aria-hidden="true">/</span>
504
- <span>{componentLabel ?? componentLabels[component.kind]}</span>
505
- </h2>
506
- {component.kind === "text-block" && !component.customVisual && <>
507
- <VectorOverflowWarning slide={slide} theme={draft.theme} />
508
- <Field label="Text"><textarea name="text-content" value={component.content ?? ""}
509
- onChange={(event) => onComponent({ ...component, content: event.target.value }, `content:${component.id}`)} onBlur={onEditEnd} /></Field>
510
- <div className="text-typography-fields">
511
- <Field label="Font size"><input type="number" min={8} max={240} value={component.textStyle?.size ?? 36}
512
- onChange={(event) => { const size = event.target.valueAsNumber; if (size >= 8 && size <= 240) onComponent({ ...component, textStyle: { ...component.textStyle, size } }, `font:${component.id}`); }} onBlur={onEditEnd} /></Field>
513
- <Select label="Text color" value={component.textStyle?.color ?? "ink"} options={["ink", "muted", "accent"]}
514
- onChange={(color) => onComponent({ ...component, textStyle: { ...component.textStyle, color: color as "ink" | "muted" | "accent" } })} />
515
- <Select label="Font weight" value={String(component.textStyle?.weight ?? 400)} options={["400", "500", "600"]}
516
- onChange={(weight) => onComponent({ ...component, textStyle: { ...component.textStyle, weight: Number(weight) as 400 | 500 | 600 } })} />
517
- <Field label="Line height"><input type="number" min={1} max={3} step={0.1} value={component.textStyle?.lineHeight ?? 1.4}
518
- onChange={(event) => { const lineHeight = event.target.valueAsNumber; if (lineHeight >= 1 && lineHeight <= 3) onComponent({ ...component, textStyle: { ...component.textStyle, lineHeight } }, `leading:${component.id}`); }} onBlur={onEditEnd} /></Field>
519
- </div>
520
- </>}
521
- <Field label={component.kind === "diagram" || component.kind === "chart" ? "What should this explain?" : "Content intent"}>
522
- <textarea
523
- name="content-intent"
524
- autoComplete="off"
525
- rows={4}
526
- placeholder={component.kind === "diagram" || component.kind === "chart" ? "Describe the relationship or takeaway and supply the facts or data to preserve." : undefined}
527
- value={component.intent ?? ""}
528
- onChange={(e) =>
529
- onComponent(
530
- { ...component, intent: e.target.value },
531
- `intent:${component.id}`,
532
- )
533
- }
534
- onBlur={onEditEnd}
535
- />
536
- </Field>
537
- {component.kind === "diagram" && <DiagramTypePicker
538
- value={component.appearance.type}
539
- selection={component.appearance.selection ?? "explicit"}
540
- onChange={(type, selection) => onComponent({ ...component, appearance: { ...component.appearance, type, selection } })}
541
- />}
542
- {component.kind === "chart" && <ChartTypePicker
543
- value={component.appearance.template}
544
- selection={component.appearance.selection ?? "explicit"}
545
- hasTopology={Boolean(component.topology)}
546
- onChange={(template, selection) => {
547
- if (template === "sankey") {
548
- onComponent({ ...component, appearance: { ...component.appearance, template, selection } });
549
- } else if (!component.topology) {
550
- onComponent({ ...component, topology: undefined, appearance: { ...component.appearance, template, selection } });
551
- }
552
- }}
553
- />}
554
- <fieldset>
555
- <legend>Position & size</legend>
556
- <div className="geometry">
557
- {(["x", "y", "width", "height"] as const).map((key) => (
558
- <Field key={key} label={humanize(key)}>
559
- <span className="parameter-input">
560
- <input
561
- name={key}
562
- autoComplete="off"
563
- type="number"
564
- step="1"
565
- value={Math.round(rect[key])}
566
- onChange={(e) => {
567
- if (e.target.value !== "")
568
- geometry(key, e.target.valueAsNumber);
569
- }}
570
- onBlur={onEditEnd}
571
- />
572
- <span className="parameter-unit" aria-hidden="true">px</span>
573
- </span>
574
- </Field>
575
- ))}
576
- </div>
577
- </fieldset>
578
- {component.customVisual && (
579
- <fieldset className="custom-visual-summary">
580
- <legend>Agent visual</legend>
581
- <dl>
582
- <div>
583
- <dt>Source</dt>
584
- <dd>
585
- {component.customVisual.format === "vector"
586
- ? `${component.customVisual.elements.length} editable vector elements`
587
- : "Legacy self-contained SVG"}
588
- </dd>
589
- </div>
590
- <div>
591
- <dt>Local viewport</dt>
592
- <dd>
593
- {component.customVisual.viewBox.width} × {component.customVisual.viewBox.height}
594
- </dd>
595
- </div>
596
- <div>
597
- <dt>Fit</dt>
598
- <dd>{component.customVisual.fit ?? "contain"}</dd>
599
- </div>
600
- </dl>
601
- {component.customVisual.format !== "vector" && (
602
- <p className="field-help">
603
- This SVG is opaque to the editor. Ask an agent to convert it to editable vector elements.
604
- </p>
605
- )}
606
- {vectorVisual && (
607
- <>
608
- <Field label="Vector element">
609
- <select
610
- value={selectedVectorElementId ?? ""}
611
- onChange={(event) =>
612
- onSelectVectorElement(event.target.value || undefined)
613
- }
614
- >
615
- <option value="">Select an element</option>
616
- {vectorVisual.elements.map((element) => (
617
- <option key={element.id} value={element.id}>
618
- {element.kind} · {element.id}
619
- </option>
620
- ))}
621
- </select>
622
- </Field>
623
- {vectorElement && (
624
- <div className="vector-element-editor" ref={vectorEditor} tabIndex={-1}>
625
- <strong>{humanize(vectorElement.kind)} · {vectorElement.id}</strong>
626
- <div className="vector-actions">
627
- {([-1, 1] as const).map((direction) => (
628
- <button
629
- key={direction}
630
- type="button"
631
- aria-label={direction === -1 ? "Send backward" : "Bring forward"}
632
- title={direction === -1 ? "Send backward" : "Bring forward"}
633
- disabled={moveVectorElement(vectorVisual.elements, vectorElement.id, direction) === vectorVisual.elements}
634
- onClick={() => onComponent({
635
- ...component,
636
- customVisual: {
637
- ...vectorVisual,
638
- elements: moveVectorElement(vectorVisual.elements, vectorElement.id, direction),
639
- },
640
- })}
641
- >
642
- <VectorActionIcon action={direction === -1 ? "backward" : "forward"} />
643
- {direction === -1 ? "Back" : "Forward"}
644
- </button>
645
- ))}
646
- <button
647
- type="button"
648
- className="vector-delete-action"
649
- aria-label="Delete element"
650
- title="Delete element"
651
- onClick={() => {
652
- onComponent({
653
- ...component,
654
- customVisual: {
655
- ...vectorVisual,
656
- elements: removeVectorElement(vectorVisual.elements, vectorElement.id),
657
- },
658
- });
659
- onSelectVectorElement(undefined);
660
- }}
661
- >
662
- <VectorActionIcon action="delete" />
663
- Delete
664
- </button>
665
- </div>
666
- {(vectorElement.kind === "text" || vectorElement.kind === "tspan") && (
667
- <Field label="Text">
668
- <textarea
669
- rows={3}
670
- value={vectorElement.text ?? ""}
671
- onChange={(event) =>
672
- updateVectorElement({
673
- ...vectorElement,
674
- text: event.target.value,
675
- })
676
- }
677
- onBlur={onEditEnd}
678
- />
679
- </Field>
680
- )}
681
- <div className="vector-attributes">
682
- {Object.entries(vectorElement.attributes).map(([name, value]) => (
683
- <Field key={name} label={humanize(name)}>
684
- <input
685
- name={`vector-${name}`}
686
- list={`theme-options-${name}`}
687
- autoComplete="off"
688
- value={value}
689
- onChange={(event) =>
690
- updateVectorElement({
691
- ...vectorElement,
692
- attributes: {
693
- ...vectorElement.attributes,
694
- [name]: event.target.value,
695
- },
696
- })
697
- }
698
- onBlur={onEditEnd}
699
- />
700
- <datalist id={`theme-options-${name}`}>
701
- {tokensForAttribute(name).map((token) => <option key={token} value={`theme:${token}`} />)}
702
- </datalist>
703
- </Field>
704
- ))}
705
- </div>
706
- </div>
707
- )}
708
- </>
709
- )}
710
- </fieldset>
711
- )}
712
- {!component.customVisual && <fieldset
713
- className="appearance-fieldset"
714
- >
715
- <legend>Appearance</legend>
716
- {component.kind === "table" && (
717
- <AppearancePicker
718
- label="Table style"
719
- name="tableStyle"
720
- value={tableStyleForAppearance(component.appearance)}
721
- options={tableStyles}
722
- onChange={(value) =>
723
- onComponent({
724
- ...component,
725
- appearance: {
726
- ...component.appearance,
727
- ...tableAppearanceForStyle(value as TableStyle),
728
- },
729
- })
730
- }
731
- />
732
- )}
733
- {Object.entries(appearanceOptions[component.kind])
734
- .filter(([key]) => !["selection", "type", "template"].includes(key))
735
- .filter(([key]) => component.kind !== "text-block" || ["alignment", "border", "rule"].includes(key))
736
- .filter(([key]) => !["diagram", "chart"].includes(component.kind) || !["density", "emphasis", "colorScheme"].includes(key))
737
- .filter(
738
- ([key]) =>
739
- component.kind !== "table" ||
740
- !["header", "grid", "border", "colorScheme"].includes(key),
741
- )
742
- .filter(([key]) => {
743
- if (key !== "orientation") return true;
744
- if (component.kind === "chart")
745
- return appearance?.template === "grouped-bar";
746
- return true;
747
- })
748
- .map(([key, options]) => {
749
- return (
750
- <AppearancePicker
751
- key={key}
752
- name={key}
753
- label={appearanceLabels[key] ?? humanize(key)}
754
- value={appearance?.[key] ?? defaults[key] ?? options[0]}
755
- options={options}
756
- onChange={(value) => {
757
- const nextAppearance = {
758
- ...component.appearance,
759
- [key]: value,
760
- } as Record<string, string>;
761
- const nextComponent = {
762
- ...component,
763
- appearance: nextAppearance,
764
- } as CompositionComponent;
765
- onComponent(nextComponent);
766
- }}
767
- />
768
- );
769
- })}
770
- </fieldset>}
771
- </section>
772
- );
773
- }