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.
- package/AGENTS.md +21 -28
- package/AUTHORING.md +68 -229
- package/CONTRIBUTING.md +10 -23
- package/README.md +96 -87
- package/SETUP.md +50 -114
- package/docs/development.md +43 -248
- package/docs/workflow.md +6 -108
- package/html/README.md +140 -0
- package/html/browser.ts +103 -0
- package/html/document.ts +38 -0
- package/html/floor.ts +34 -0
- package/html/index.html +5 -0
- package/html/inspect.ts +313 -0
- package/html/preview.css +62 -0
- package/html/preview.tsx +463 -0
- package/html/review-hints.ts +63 -0
- package/html/server.ts +99 -0
- package/html/source.ts +104 -0
- package/html/starter.ts +6 -0
- package/html/theme-authoring.md +176 -0
- package/html/theme.md +61 -0
- package/index.html +9 -9
- package/package.json +21 -35
- package/plugin.json +1 -1
- package/public/og.png +0 -0
- package/runtime/browser-B-29TH1a.mjs +595 -0
- package/runtime/floor-Cmk7G3pU.mjs +41 -0
- package/runtime/konpeki.mjs +90 -1410
- package/runtime/server-BAqC_5WD.mjs +2 -0
- package/runtime/server-DfRpfcY9.mjs +205 -0
- package/runtime/source-B_Ui9tMp.mjs +141 -0
- package/runtime/source-CZQq9GUO.mjs +2 -0
- package/skills/konpeki/SKILL.md +88 -172
- package/skills/konpeki/assets/blank.html +17 -0
- package/skills/konpeki/floor.md +76 -0
- package/skills/konpeki/references/cover.md +27 -0
- package/skills/konpeki/references/long-document.md +40 -0
- package/skills/konpeki/references/one-pager.md +27 -0
- package/skills/konpeki/references/patterns.md +118 -0
- package/skills/konpeki/references/resume.md +28 -0
- package/skills/konpeki/references/slides.md +28 -0
- package/skills/konpeki/scripts/ensure-runtime.mjs +10 -31
- package/skills/konpeki/scripts/prepare-document.mjs +25 -14
- package/src/components/PageBoard.tsx +156 -0
- package/src/lib/alignment.ts +21 -0
- package/src/lib/page-board.ts +25 -0
- package/src/lib/review-position.ts +19 -0
- package/src/styles/base.css +4 -6
- package/src/styles/feedback.css +97 -61
- package/src/styles/shell.css +95 -323
- package/theme-base.css +90 -0
- package/theme.css +56 -0
- package/vite.config.ts +2 -5
- package/composition/README.md +0 -156
- package/composition/compile.ts +0 -227
- package/composition/document.ts +0 -600
- package/composition/schema.json +0 -3001
- package/composition/schema.ts +0 -437
- package/composition/theme-tokens.ts +0 -16
- package/composition/types.ts +0 -269
- package/composition/validate.ts +0 -226
- package/composition/vector.ts +0 -143
- package/composition/visualizations.ts +0 -319
- package/design/README.md +0 -17
- package/design/palettes/README.md +0 -14
- package/design/palettes/base.ts +0 -14
- package/design/palettes/candidates.ts +0 -19
- package/design/palettes/index.ts +0 -78
- package/design/review/color-theme.md +0 -44
- package/design/review/layout.md +0 -16
- package/design/review/text.md +0 -18
- package/design/review/typography.md +0 -15
- package/design/review/visuals.md +0 -31
- package/design/semantic-patterns.md +0 -43
- package/design/themes/README.md +0 -40
- package/design/themes/index.ts +0 -24
- package/design/visual-languages/technical-product.md +0 -17
- package/design/visual-review.md +0 -88
- package/lib/assets.d.ts +0 -8
- package/lib/charts.ts +0 -18
- package/lib/contrast.ts +0 -16
- package/lib/layouts.ts +0 -50
- package/lib/slide.tsx +0 -42
- package/lib/taste.ts +0 -17
- package/lib/text.tsx +0 -89
- package/lib/typeface.ts +0 -44
- package/scripts/migrate-react-page.ts +0 -120
- package/skills/konpeki/assets/blank.json +0 -23
- package/slides/README.md +0 -153
- package/slides/architecture/PROMPT.md +0 -31
- package/slides/architecture/index.tsx +0 -102
- package/slides/article-brief/PROMPT.md +0 -35
- package/slides/article-brief/index.tsx +0 -71
- package/slides/bar-chart/PROMPT.md +0 -39
- package/slides/bar-chart/index.tsx +0 -97
- package/slides/comparison/PROMPT.md +0 -29
- package/slides/comparison/index.tsx +0 -95
- package/slides/decision-memo/PROMPT.md +0 -34
- package/slides/decision-memo/index.tsx +0 -85
- package/slides/delivery-plan/PROMPT.md +0 -45
- package/slides/delivery-plan/index.tsx +0 -105
- package/slides/experiment/PROMPT.md +0 -44
- package/slides/experiment/index.tsx +0 -127
- package/slides/incident-workflow/PROMPT.md +0 -57
- package/slides/incident-workflow/index.tsx +0 -78
- package/slides/introducing-konpeki/PROMPT.md +0 -40
- package/slides/introducing-konpeki/README.md +0 -76
- package/slides/introducing-konpeki/SOURCE.md +0 -26
- package/slides/introducing-konpeki/author.ts +0 -165
- package/slides/introducing-konpeki/composition.json +0 -3270
- package/slides/line-chart/PROMPT.md +0 -40
- package/slides/line-chart/index.tsx +0 -72
- package/slides/migration/PROMPT.md +0 -38
- package/slides/migration/index.tsx +0 -89
- package/slides/og-images/PROMPT.md +0 -21
- package/slides/og-images/index.tsx +0 -76
- package/slides/product-introduction/PROMPT.md +0 -24
- package/slides/product-introduction/index.tsx +0 -105
- package/slides/research-brief/PROMPT.md +0 -40
- package/slides/research-brief/index.tsx +0 -104
- package/slides/results-explanation/PROMPT.md +0 -32
- package/slides/results-explanation/index.tsx +0 -96
- package/slides/retrospective/PROMPT.md +0 -43
- package/slides/retrospective/index.tsx +0 -105
- package/slides/sankey/PROMPT.md +0 -11
- package/slides/sankey/index.tsx +0 -93
- package/slides/teaching/PROMPT.md +0 -45
- package/slides/teaching/index.tsx +0 -124
- package/slides/vertical-bar-charts/PROMPT.md +0 -13
- package/slides/vertical-bar-charts/index.tsx +0 -97
- package/src/app/App.tsx +0 -820
- package/src/components/BuildOrb.tsx +0 -40
- package/src/components/Canvas.tsx +0 -1185
- package/src/components/DiagramTypeIcon.tsx +0 -78
- package/src/components/InspectorPanel.tsx +0 -773
- package/src/components/LeftPanel.tsx +0 -120
- package/src/components/PageSizePicker.tsx +0 -30
- package/src/components/Presentation.tsx +0 -105
- package/src/components/RevisionNotes.tsx +0 -56
- package/src/components/RightPanel.tsx +0 -201
- package/src/components/VectorOverflowWarning.tsx +0 -46
- package/src/components/WorkspaceChrome.tsx +0 -288
- package/src/components/ui.tsx +0 -53
- package/src/lib/examples/react-page-migration.json +0 -1295
- package/src/lib/examples.ts +0 -42
- package/src/lib/export-png.ts +0 -104
- package/src/lib/file-session.ts +0 -87
- package/src/lib/history.ts +0 -53
- package/src/lib/model.ts +0 -188
- package/src/lib/page-size.ts +0 -24
- package/src/lib/presentation.ts +0 -17
- package/src/lib/review.ts +0 -26
- package/src/lib/storage.ts +0 -71
- package/src/lib/theme.ts +0 -25
- package/src/lib/use-file-session.ts +0 -227
- package/src/main.tsx +0 -29
- package/src/styles/canvas.css +0 -299
- package/src/styles/chrome.css +0 -384
- package/src/styles/component-previews.css +0 -386
- package/src/styles/left-panel.css +0 -166
- package/src/styles/presentation.css +0 -72
- package/src/styles/right-panel.css +0 -1215
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
-
import type {
|
|
3
|
-
CompositionSlide,
|
|
4
|
-
ThemeMode,
|
|
5
|
-
} from "../../composition/types.ts";
|
|
6
|
-
import type { Draft } from "../lib/model.ts";
|
|
7
|
-
|
|
8
|
-
export type LeftPanelView = "pages" | "notes";
|
|
9
|
-
|
|
10
|
-
function ThumbnailCanvas({
|
|
11
|
-
slide,
|
|
12
|
-
themeMode,
|
|
13
|
-
pageIndex,
|
|
14
|
-
pageTotal,
|
|
15
|
-
}: {
|
|
16
|
-
slide: CompositionSlide;
|
|
17
|
-
themeMode: ThemeMode;
|
|
18
|
-
pageIndex: number;
|
|
19
|
-
pageTotal: number;
|
|
20
|
-
}) {
|
|
21
|
-
return (
|
|
22
|
-
<span className={`slide-thumbnail-canvas ${themeMode}`} style={{ aspectRatio: slide.canvas.width / slide.canvas.height }}>
|
|
23
|
-
{slide.components.map((component) => {
|
|
24
|
-
const rect = component.preferredRect;
|
|
25
|
-
const style = {
|
|
26
|
-
left: `${rect.x * 100 / slide.canvas.width}%`,
|
|
27
|
-
top: `${rect.y * 100 / slide.canvas.height}%`,
|
|
28
|
-
width: `${rect.width * 100 / slide.canvas.width}%`,
|
|
29
|
-
height: `${rect.height * 100 / slide.canvas.height}%`,
|
|
30
|
-
} as CSSProperties;
|
|
31
|
-
return (
|
|
32
|
-
<span
|
|
33
|
-
key={component.id}
|
|
34
|
-
className={`thumbnail-component kind-${component.kind} ${component.kind === "text-block" ? `role-${component.appearance.role}` : ""}`}
|
|
35
|
-
style={style}
|
|
36
|
-
/>
|
|
37
|
-
);
|
|
38
|
-
})}
|
|
39
|
-
{slide.pageNumber?.style !== "none" && (
|
|
40
|
-
<span className="thumbnail-page-number">
|
|
41
|
-
{String(pageIndex).padStart(2, "0")}
|
|
42
|
-
{slide.pageNumber?.style === "01/02"
|
|
43
|
-
? `/${String(pageTotal).padStart(2, "0")}`
|
|
44
|
-
: ""}
|
|
45
|
-
</span>
|
|
46
|
-
)}
|
|
47
|
-
</span>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function LeftPanel({
|
|
52
|
-
draft,
|
|
53
|
-
activeSlideId,
|
|
54
|
-
collapsed,
|
|
55
|
-
view,
|
|
56
|
-
onView,
|
|
57
|
-
revisionNotes,
|
|
58
|
-
onSelectSlide,
|
|
59
|
-
onAddSlide,
|
|
60
|
-
onRemoveSlide,
|
|
61
|
-
}: {
|
|
62
|
-
draft: Draft;
|
|
63
|
-
activeSlideId: string;
|
|
64
|
-
collapsed: boolean;
|
|
65
|
-
view: LeftPanelView;
|
|
66
|
-
onView: (view: LeftPanelView) => void;
|
|
67
|
-
revisionNotes?: ReactNode;
|
|
68
|
-
onSelectSlide: (id: string) => void;
|
|
69
|
-
onAddSlide: () => void;
|
|
70
|
-
onRemoveSlide: (id: string) => void;
|
|
71
|
-
}) {
|
|
72
|
-
return (
|
|
73
|
-
<aside className={`left-panel${collapsed ? " collapsed" : ""}`} aria-label="Workspace panel" inert={collapsed}>
|
|
74
|
-
<div className="panel-toolbar">
|
|
75
|
-
{revisionNotes ? <div className="panel-tabs" aria-label="Left panel view">
|
|
76
|
-
{(["pages", "notes"] as const).map(tab => <button key={tab} type="button"
|
|
77
|
-
aria-pressed={view === tab} className={view === tab ? "active" : ""}
|
|
78
|
-
onClick={() => onView(tab)}>{tab === "pages" ? "Pages" : "Notes"}</button>)}
|
|
79
|
-
</div> : <span className="panel-heading">Pages</span>}
|
|
80
|
-
</div>
|
|
81
|
-
<div className="slide-list" hidden={!!revisionNotes && view !== "pages"}>
|
|
82
|
-
{draft.slides.map((item, index) => (
|
|
83
|
-
<div key={item.id} className="slide-thumbnail-item">
|
|
84
|
-
<button
|
|
85
|
-
type="button"
|
|
86
|
-
className={`slide-thumbnail ${item.id === activeSlideId ? "selected" : ""}`}
|
|
87
|
-
aria-current={item.id === activeSlideId ? "page" : undefined}
|
|
88
|
-
onClick={() => onSelectSlide(item.id)}
|
|
89
|
-
>
|
|
90
|
-
<ThumbnailCanvas
|
|
91
|
-
slide={item}
|
|
92
|
-
themeMode={draft.theme?.mode ?? "paper"}
|
|
93
|
-
pageIndex={index + 1}
|
|
94
|
-
pageTotal={draft.slides.length}
|
|
95
|
-
/>
|
|
96
|
-
<span>{item.name}</span>
|
|
97
|
-
</button>
|
|
98
|
-
<button
|
|
99
|
-
type="button"
|
|
100
|
-
className="remove-slide"
|
|
101
|
-
aria-label={`Remove ${item.name}`}
|
|
102
|
-
onClick={() => onRemoveSlide(item.id)}
|
|
103
|
-
>
|
|
104
|
-
<span className="panel-close-icon" aria-hidden="true" />
|
|
105
|
-
</button>
|
|
106
|
-
</div>
|
|
107
|
-
))}
|
|
108
|
-
<button
|
|
109
|
-
type="button"
|
|
110
|
-
className="add-slide"
|
|
111
|
-
aria-label="Add page"
|
|
112
|
-
onClick={onAddSlide}
|
|
113
|
-
>
|
|
114
|
-
<span aria-hidden="true">+</span>
|
|
115
|
-
</button>
|
|
116
|
-
</div>
|
|
117
|
-
{revisionNotes && <div className="left-notes" hidden={view !== "notes"}>{revisionNotes}</div>}
|
|
118
|
-
</aside>
|
|
119
|
-
);
|
|
120
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { CompositionSlide } from "../../composition/types.ts";
|
|
2
|
-
import { pagePresets, pageSizeIssue, resizePage } from "../lib/page-size.ts";
|
|
3
|
-
|
|
4
|
-
export function PageSizePicker({ slide, onChange }: { slide: CompositionSlide; onChange: (slide: CompositionSlide) => void }) {
|
|
5
|
-
const preset = pagePresets.find((item) => item.width === slide.canvas.width && item.height === slide.canvas.height);
|
|
6
|
-
function apply(w: number, h: number, destination: CompositionSlide["intendedViewingSize"] = "custom") {
|
|
7
|
-
const size = { width: w, height: h };
|
|
8
|
-
if (!pageSizeIssue(slide, size)) {
|
|
9
|
-
onChange(resizePage(slide, size, destination));
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
return <fieldset>
|
|
13
|
-
<legend>Size</legend>
|
|
14
|
-
<label className="field">
|
|
15
|
-
<span className="sr-only">Size</span>
|
|
16
|
-
<select name="page-format" value={preset?.name ?? "Imported"} onChange={(event) => {
|
|
17
|
-
const next = pagePresets.find((item) => item.name === event.target.value);
|
|
18
|
-
if (next) apply(next.width, next.height, next.destination);
|
|
19
|
-
}}>
|
|
20
|
-
{!preset && <option value="Imported" disabled>{slide.canvas.width} × {slide.canvas.height} · Imported size</option>}
|
|
21
|
-
{pagePresets.map((item) => {
|
|
22
|
-
const unavailable = Boolean(pageSizeIssue(slide, item));
|
|
23
|
-
return <option key={item.name} value={item.name} disabled={unavailable}>
|
|
24
|
-
{item.name} · {item.width} × {item.height}{unavailable ? " · unavailable" : ""}
|
|
25
|
-
</option>;
|
|
26
|
-
})}
|
|
27
|
-
</select>
|
|
28
|
-
</label>
|
|
29
|
-
</fieldset>;
|
|
30
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import type { Draft } from "../lib/model.ts";
|
|
3
|
-
import { presentationAction } from "../lib/presentation.ts";
|
|
4
|
-
import { Canvas } from "./Canvas.tsx";
|
|
5
|
-
|
|
6
|
-
const noop = () => {};
|
|
7
|
-
|
|
8
|
-
export function Presentation({
|
|
9
|
-
draft,
|
|
10
|
-
initialSlideId,
|
|
11
|
-
onExit,
|
|
12
|
-
}: {
|
|
13
|
-
draft: Draft;
|
|
14
|
-
initialSlideId: string;
|
|
15
|
-
onExit: () => void;
|
|
16
|
-
}) {
|
|
17
|
-
const [index, setIndex] = useState(() =>
|
|
18
|
-
Math.max(0, draft.slides.findIndex((slide) => slide.id === initialSlideId)),
|
|
19
|
-
);
|
|
20
|
-
const overlay = useRef<HTMLDivElement>(null);
|
|
21
|
-
const slide = draft.slides[index];
|
|
22
|
-
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
overlay.current?.focus();
|
|
25
|
-
}, []);
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
function keydown(event: KeyboardEvent) {
|
|
29
|
-
const interactiveTarget =
|
|
30
|
-
event.target instanceof Element &&
|
|
31
|
-
Boolean(event.target.closest("button,a,input,textarea,select,[contenteditable=true]"));
|
|
32
|
-
const action = presentationAction(
|
|
33
|
-
event.key,
|
|
34
|
-
index,
|
|
35
|
-
draft.slides.length,
|
|
36
|
-
interactiveTarget,
|
|
37
|
-
);
|
|
38
|
-
if (action === undefined) return;
|
|
39
|
-
event.preventDefault();
|
|
40
|
-
if (action === "exit") onExit();
|
|
41
|
-
else setIndex(action);
|
|
42
|
-
}
|
|
43
|
-
window.addEventListener("keydown", keydown);
|
|
44
|
-
return () => window.removeEventListener("keydown", keydown);
|
|
45
|
-
}, [draft.slides.length, index, onExit]);
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<div
|
|
49
|
-
ref={overlay}
|
|
50
|
-
className="presentation"
|
|
51
|
-
role="dialog"
|
|
52
|
-
aria-modal="true"
|
|
53
|
-
aria-label="Presentation mode"
|
|
54
|
-
tabIndex={-1}
|
|
55
|
-
>
|
|
56
|
-
<Canvas
|
|
57
|
-
key={slide.id}
|
|
58
|
-
mode="present"
|
|
59
|
-
draft={draft}
|
|
60
|
-
activeSlideId={slide.id}
|
|
61
|
-
onSelect={noop}
|
|
62
|
-
onSlideName={noop}
|
|
63
|
-
onComponent={noop}
|
|
64
|
-
onEditEnd={noop}
|
|
65
|
-
onAdd={noop}
|
|
66
|
-
onNotice={noop}
|
|
67
|
-
/>
|
|
68
|
-
<nav className="presentation-controls" aria-label="Presentation controls">
|
|
69
|
-
<button
|
|
70
|
-
type="button"
|
|
71
|
-
disabled={index === 0}
|
|
72
|
-
aria-label="Previous page"
|
|
73
|
-
onClick={() => setIndex((value) => Math.max(0, value - 1))}
|
|
74
|
-
>
|
|
75
|
-
←
|
|
76
|
-
</button>
|
|
77
|
-
<span aria-live="polite">
|
|
78
|
-
{index + 1} / {draft.slides.length}
|
|
79
|
-
</span>
|
|
80
|
-
<button
|
|
81
|
-
type="button"
|
|
82
|
-
disabled={index === draft.slides.length - 1}
|
|
83
|
-
aria-label="Next page"
|
|
84
|
-
onClick={() =>
|
|
85
|
-
setIndex((value) => Math.min(draft.slides.length - 1, value + 1))
|
|
86
|
-
}
|
|
87
|
-
>
|
|
88
|
-
→
|
|
89
|
-
</button>
|
|
90
|
-
<button
|
|
91
|
-
type="button"
|
|
92
|
-
onClick={() => {
|
|
93
|
-
if (document.fullscreenElement) void document.exitFullscreen();
|
|
94
|
-
else void overlay.current?.requestFullscreen();
|
|
95
|
-
}}
|
|
96
|
-
>
|
|
97
|
-
Full screen
|
|
98
|
-
</button>
|
|
99
|
-
<button type="button" onClick={onExit}>
|
|
100
|
-
Exit
|
|
101
|
-
</button>
|
|
102
|
-
</nav>
|
|
103
|
-
</div>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import type { CompositionDocument } from "../../composition/types.ts";
|
|
3
|
-
import { componentInstanceLabel } from "../lib/model.ts";
|
|
4
|
-
import { activeRequest, type ReviewState, type ReviewTarget } from "../lib/review.ts";
|
|
5
|
-
|
|
6
|
-
export function RevisionNotes({ document, target, review, disabled, onAdd, onRemove, onSelect }: {
|
|
7
|
-
document: CompositionDocument;
|
|
8
|
-
target: ReviewTarget;
|
|
9
|
-
review: ReviewState;
|
|
10
|
-
disabled: boolean;
|
|
11
|
-
onAdd: (target: ReviewTarget, text: string) => Promise<void>;
|
|
12
|
-
onRemove: (id: string) => Promise<void>;
|
|
13
|
-
onSelect: (target: ReviewTarget) => void;
|
|
14
|
-
}) {
|
|
15
|
-
const [drafts, setDrafts] = useState<Record<string, string>>({});
|
|
16
|
-
const [saving, setSaving] = useState(false);
|
|
17
|
-
const [error, setError] = useState("");
|
|
18
|
-
const key = JSON.stringify(target);
|
|
19
|
-
const text = drafts[key] ?? "";
|
|
20
|
-
function label(value: ReviewTarget) {
|
|
21
|
-
const slide = document.slides.find(s => s.id === value.slideId);
|
|
22
|
-
if (!slide) return "Deleted page";
|
|
23
|
-
const component = slide.components.find(c => c.id === value.componentId);
|
|
24
|
-
if (value.componentId && !component) return `${slide.name} · Deleted component`;
|
|
25
|
-
const name = component ? componentInstanceLabel(slide.components, component.id) : "Page";
|
|
26
|
-
const element = value.elementId && component?.customVisual?.format === "vector"
|
|
27
|
-
? component.customVisual.elements.find(e => e.id === value.elementId) : undefined;
|
|
28
|
-
return `${slide.name} · ${name}${value.elementId ? ` · ${element ? value.elementId : "Deleted element"}` : ""}`;
|
|
29
|
-
}
|
|
30
|
-
async function run(action: () => Promise<void>) {
|
|
31
|
-
setSaving(true); setError("");
|
|
32
|
-
try { await action(); }
|
|
33
|
-
catch (error) { setError(error instanceof Error ? error.message : "Could not save revision note"); }
|
|
34
|
-
finally { setSaving(false); }
|
|
35
|
-
}
|
|
36
|
-
const pending = review.notes.filter(n => !n.resolved);
|
|
37
|
-
return <section className="revision-notes" aria-label="Revision notes">
|
|
38
|
-
<form onSubmit={event => { event.preventDefault(); void run(async () => {
|
|
39
|
-
await onAdd(target, text);
|
|
40
|
-
setDrafts(current => ({ ...current, [key]: "" }));
|
|
41
|
-
}); }}>
|
|
42
|
-
<label htmlFor="revision-note">Request a change</label>
|
|
43
|
-
<p className="revision-note-scope">{label(target)}</p>
|
|
44
|
-
<textarea id="revision-note" value={text} maxLength={4000} rows={3} placeholder="Describe what to change…" disabled={disabled || saving}
|
|
45
|
-
onChange={event => setDrafts(current => ({ ...current, [key]: event.target.value }))} />
|
|
46
|
-
<div className="revision-note-submit"><button type="submit" disabled={disabled || saving || !text.trim()}>{saving ? "Saving…" : "Add note"}</button></div>
|
|
47
|
-
</form>
|
|
48
|
-
{error && <p role="alert" className="revision-note-error">{error}</p>}
|
|
49
|
-
<p className="revision-note-count">{pending.length} pending</p>
|
|
50
|
-
{pending.length > 0 && <ol className="revision-note-list">{pending.map((note, i) => <li key={note.id} id={`note-${note.id}`}>
|
|
51
|
-
<button type="button" className="revision-note-target" onClick={() => onSelect(note)}><span>{i + 1}</span>{label(note)}</button>
|
|
52
|
-
<p>{note.text}</p>
|
|
53
|
-
<button type="button" className="revision-note-remove" disabled={disabled || saving || activeRequest(review.request)} aria-label={`Remove note ${i + 1}`} onClick={() => { void run(() => onRemove(note.id)); }}>Remove</button>
|
|
54
|
-
</li>)}</ol>}
|
|
55
|
-
</section>;
|
|
56
|
-
}
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import type {
|
|
3
|
-
CompositionComponent,
|
|
4
|
-
CompositionSlide,
|
|
5
|
-
} from "../../composition/types.ts";
|
|
6
|
-
import { componentInstanceLabel, type Draft } from "../lib/model.ts";
|
|
7
|
-
import { InspectorPanel } from "./InspectorPanel.tsx";
|
|
8
|
-
|
|
9
|
-
export type RightPanelView = "settings" | "layers";
|
|
10
|
-
|
|
11
|
-
function LayersPanel({
|
|
12
|
-
slide,
|
|
13
|
-
selectedComponentId,
|
|
14
|
-
onSelectComponent,
|
|
15
|
-
onReorderPaintOrder,
|
|
16
|
-
}: {
|
|
17
|
-
slide: CompositionSlide;
|
|
18
|
-
selectedComponentId?: string;
|
|
19
|
-
onSelectComponent: (id: string) => void;
|
|
20
|
-
onReorderPaintOrder: (ids: string[]) => void;
|
|
21
|
-
}) {
|
|
22
|
-
const [dragging, setDragging] = useState<string>();
|
|
23
|
-
const layers = [...slide.paintOrder].reverse();
|
|
24
|
-
function moveLayer(id: string, toIndex: number) {
|
|
25
|
-
const next = layers.filter((item) => item !== id);
|
|
26
|
-
next.splice(Math.max(0, Math.min(toIndex, next.length)), 0, id);
|
|
27
|
-
onReorderPaintOrder(next.reverse());
|
|
28
|
-
}
|
|
29
|
-
return (
|
|
30
|
-
<section className="layers-panel" aria-labelledby="layers-heading">
|
|
31
|
-
<div className="layers-heading-row">
|
|
32
|
-
<h2 id="layers-heading">Components & layers</h2>
|
|
33
|
-
<span>Front to back</span>
|
|
34
|
-
</div>
|
|
35
|
-
{layers.length ? (
|
|
36
|
-
<div className="layer-list">
|
|
37
|
-
{layers.map((id, index) => {
|
|
38
|
-
const component = slide.components.find((item) => item.id === id)!;
|
|
39
|
-
const groups = slide.groups
|
|
40
|
-
.filter((group) => group.childIds.includes(id))
|
|
41
|
-
.map((group) => group.label?.trim() || group.id);
|
|
42
|
-
const label = componentInstanceLabel(slide.components, id);
|
|
43
|
-
return (
|
|
44
|
-
<div
|
|
45
|
-
key={id}
|
|
46
|
-
data-layer={id}
|
|
47
|
-
className={`layer-row ${selectedComponentId === id ? "selected" : ""} ${dragging === id ? "dragging" : ""}`}
|
|
48
|
-
draggable
|
|
49
|
-
onDragStart={(event) => {
|
|
50
|
-
setDragging(id);
|
|
51
|
-
event.dataTransfer.effectAllowed = "move";
|
|
52
|
-
event.dataTransfer.setData("text/plain", id);
|
|
53
|
-
}}
|
|
54
|
-
onDragEnd={() => setDragging(undefined)}
|
|
55
|
-
onDragOver={(event) => {
|
|
56
|
-
event.preventDefault();
|
|
57
|
-
event.dataTransfer.dropEffect = "move";
|
|
58
|
-
}}
|
|
59
|
-
onDrop={(event) => {
|
|
60
|
-
event.preventDefault();
|
|
61
|
-
const source = dragging || event.dataTransfer.getData("text/plain");
|
|
62
|
-
if (source && source !== id) moveLayer(source, index);
|
|
63
|
-
setDragging(undefined);
|
|
64
|
-
}}
|
|
65
|
-
>
|
|
66
|
-
<span className="layer-drag" aria-hidden="true">⠿</span>
|
|
67
|
-
<button
|
|
68
|
-
type="button"
|
|
69
|
-
className="layer-select"
|
|
70
|
-
onClick={() => onSelectComponent(id)}
|
|
71
|
-
>
|
|
72
|
-
<strong>{label}</strong>
|
|
73
|
-
<span>{groups.length ? groups.join(", ") : "Page root"}</span>
|
|
74
|
-
</button>
|
|
75
|
-
<span className="layer-actions">
|
|
76
|
-
<button
|
|
77
|
-
type="button"
|
|
78
|
-
aria-label={`Move ${label} forward`}
|
|
79
|
-
disabled={index === 0}
|
|
80
|
-
onClick={() => moveLayer(id, index - 1)}
|
|
81
|
-
>↑</button>
|
|
82
|
-
<button
|
|
83
|
-
type="button"
|
|
84
|
-
aria-label={`Move ${label} backward`}
|
|
85
|
-
disabled={index === layers.length - 1}
|
|
86
|
-
onClick={() => moveLayer(id, index + 1)}
|
|
87
|
-
>↓</button>
|
|
88
|
-
</span>
|
|
89
|
-
</div>
|
|
90
|
-
);
|
|
91
|
-
})}
|
|
92
|
-
</div>
|
|
93
|
-
) : (
|
|
94
|
-
<p className="layers-empty">No components yet.</p>
|
|
95
|
-
)}
|
|
96
|
-
</section>
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function RightPanel({
|
|
101
|
-
view,
|
|
102
|
-
collapsed,
|
|
103
|
-
component,
|
|
104
|
-
componentLabel,
|
|
105
|
-
selectedComponentId,
|
|
106
|
-
selectedVectorElementId,
|
|
107
|
-
vectorEditRequest,
|
|
108
|
-
slide,
|
|
109
|
-
draft,
|
|
110
|
-
onView,
|
|
111
|
-
onCollapsedChange,
|
|
112
|
-
onSelectSlide,
|
|
113
|
-
onSelectComponent,
|
|
114
|
-
onSelectVectorElement,
|
|
115
|
-
onReorderPaintOrder,
|
|
116
|
-
onComponent,
|
|
117
|
-
onSlide,
|
|
118
|
-
onDraft,
|
|
119
|
-
onEditEnd,
|
|
120
|
-
}: {
|
|
121
|
-
view: RightPanelView;
|
|
122
|
-
collapsed: boolean;
|
|
123
|
-
component?: CompositionComponent;
|
|
124
|
-
componentLabel?: string;
|
|
125
|
-
selectedComponentId?: string;
|
|
126
|
-
selectedVectorElementId?: string;
|
|
127
|
-
vectorEditRequest?: number;
|
|
128
|
-
slide: CompositionSlide;
|
|
129
|
-
draft: Draft;
|
|
130
|
-
onView: (view: RightPanelView) => void;
|
|
131
|
-
onCollapsedChange: (collapsed: boolean) => void;
|
|
132
|
-
onSelectSlide: () => void;
|
|
133
|
-
onSelectComponent: (id: string) => void;
|
|
134
|
-
onSelectVectorElement: (id?: string) => void;
|
|
135
|
-
onReorderPaintOrder: (ids: string[]) => void;
|
|
136
|
-
onComponent: (component: CompositionComponent, mergeKey?: string) => void;
|
|
137
|
-
onSlide: (slide: CompositionSlide, mergeKey?: string) => void;
|
|
138
|
-
onDraft: (patch: Partial<Draft>, mergeKey?: string) => void;
|
|
139
|
-
onEditEnd: () => void;
|
|
140
|
-
}) {
|
|
141
|
-
return (
|
|
142
|
-
<aside className={`right-panel${collapsed ? " collapsed" : ""}`} aria-label="Inspector panel">
|
|
143
|
-
<div className="panel-toolbar">
|
|
144
|
-
<div className="panel-tabs" aria-label="Right panel view" inert={collapsed}>
|
|
145
|
-
{(["settings", "layers"] as const).map((tab) => (
|
|
146
|
-
<button
|
|
147
|
-
key={tab}
|
|
148
|
-
type="button"
|
|
149
|
-
aria-pressed={view === tab}
|
|
150
|
-
className={view === tab ? "active" : ""}
|
|
151
|
-
onClick={() => onView(tab)}
|
|
152
|
-
>
|
|
153
|
-
{tab === "settings" ? "Settings" : "Layers"}
|
|
154
|
-
</button>
|
|
155
|
-
))}
|
|
156
|
-
</div>
|
|
157
|
-
<button
|
|
158
|
-
type="button"
|
|
159
|
-
className="panel-toggle"
|
|
160
|
-
aria-label={collapsed ? "Expand right panel" : "Collapse right panel"}
|
|
161
|
-
aria-expanded={!collapsed}
|
|
162
|
-
onClick={() => onCollapsedChange(!collapsed)}
|
|
163
|
-
>
|
|
164
|
-
<svg
|
|
165
|
-
className="panel-collapse-icon"
|
|
166
|
-
viewBox="0 0 24 24"
|
|
167
|
-
aria-hidden="true"
|
|
168
|
-
>
|
|
169
|
-
<rect x="3" y="4" width="18" height="16" rx="2" />
|
|
170
|
-
<path d={collapsed ? "M15 4v16M11 9l-3 3 3 3" : "M15 4v16M8 9l3 3-3 3"} />
|
|
171
|
-
</svg>
|
|
172
|
-
</button>
|
|
173
|
-
</div>
|
|
174
|
-
<div className="right-panel-body" inert={collapsed}>
|
|
175
|
-
{view === "settings" ? (
|
|
176
|
-
<InspectorPanel
|
|
177
|
-
component={component}
|
|
178
|
-
componentLabel={componentLabel}
|
|
179
|
-
slide={slide}
|
|
180
|
-
draft={draft}
|
|
181
|
-
selectedVectorElementId={selectedVectorElementId}
|
|
182
|
-
vectorEditRequest={vectorEditRequest}
|
|
183
|
-
onSelectSlide={onSelectSlide}
|
|
184
|
-
onSelectVectorElement={onSelectVectorElement}
|
|
185
|
-
onComponent={onComponent}
|
|
186
|
-
onSlide={onSlide}
|
|
187
|
-
onDraft={onDraft}
|
|
188
|
-
onEditEnd={onEditEnd}
|
|
189
|
-
/>
|
|
190
|
-
) : (
|
|
191
|
-
<LayersPanel
|
|
192
|
-
slide={slide}
|
|
193
|
-
selectedComponentId={selectedComponentId}
|
|
194
|
-
onSelectComponent={onSelectComponent}
|
|
195
|
-
onReorderPaintOrder={onReorderPaintOrder}
|
|
196
|
-
/>
|
|
197
|
-
)}
|
|
198
|
-
</div>
|
|
199
|
-
</aside>
|
|
200
|
-
);
|
|
201
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
import type { CompositionSlide, CompositionDocument } from "../../composition/types.ts";
|
|
3
|
-
|
|
4
|
-
/** Measure rendered text, including nested SVG transforms, after fonts settle. */
|
|
5
|
-
export function VectorOverflowWarning({ slide, theme }: {
|
|
6
|
-
slide: CompositionSlide;
|
|
7
|
-
theme: CompositionDocument["theme"];
|
|
8
|
-
}) {
|
|
9
|
-
const [count, setCount] = useState(0);
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
let cancelled = false;
|
|
12
|
-
let frame = 0;
|
|
13
|
-
function measure() {
|
|
14
|
-
frame = requestAnimationFrame(() => {
|
|
15
|
-
if (cancelled) return;
|
|
16
|
-
let overflow = 0;
|
|
17
|
-
document.querySelectorAll<HTMLElement>(".stage .text-block-content").forEach((text) => {
|
|
18
|
-
const surface = text.parentElement;
|
|
19
|
-
if (surface && (text.scrollHeight > surface.clientHeight + 1 || text.scrollWidth > surface.clientWidth + 1)) overflow++;
|
|
20
|
-
});
|
|
21
|
-
document.querySelectorAll<SVGSVGElement>(".stage .custom-vector-art").forEach((svg) => {
|
|
22
|
-
const root = svg.getScreenCTM();
|
|
23
|
-
if (!root) return;
|
|
24
|
-
const view = svg.viewBox.baseVal;
|
|
25
|
-
svg.querySelectorAll<SVGTextElement>("text").forEach((text) => {
|
|
26
|
-
const matrix = text.getScreenCTM();
|
|
27
|
-
if (!matrix) return;
|
|
28
|
-
const box = text.getBBox();
|
|
29
|
-
const transform = root.inverse().multiply(matrix);
|
|
30
|
-
const corners = [[box.x, box.y], [box.x + box.width, box.y],
|
|
31
|
-
[box.x, box.y + box.height], [box.x + box.width, box.y + box.height]];
|
|
32
|
-
if (corners.some(([x, y]) => {
|
|
33
|
-
const p = new DOMPoint(x, y).matrixTransform(transform);
|
|
34
|
-
return p.x < view.x - 1 || p.y < view.y - 1 || p.x > view.x + view.width + 1 || p.y > view.y + view.height + 1;
|
|
35
|
-
})) overflow++;
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
setCount(overflow);
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
void document.fonts.ready.then(measure);
|
|
42
|
-
document.fonts.addEventListener("loadingdone", measure);
|
|
43
|
-
return () => { cancelled = true; cancelAnimationFrame(frame); document.fonts.removeEventListener("loadingdone", measure); };
|
|
44
|
-
}, [slide, theme]);
|
|
45
|
-
return count ? <p role="status" className="field-error">{count} text {count === 1 ? "element exceeds" : "elements exceed"} its component viewport on this slide. Revise the text or geometry; nothing was resized.</p> : null;
|
|
46
|
-
}
|