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