@mawaru/sdk 0.38.0 → 0.40.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 (91) hide show
  1. package/_schemas/ai-manifest.test.ts +8 -0
  2. package/_schemas/ai-manifest.ts +2 -0
  3. package/_schemas/graph.test.ts +51 -9
  4. package/_schemas/graph.ts +23 -8
  5. package/_schemas/human-manifest.test.ts +129 -0
  6. package/_schemas/human-manifest.ts +158 -0
  7. package/_schemas/node.ts +4 -0
  8. package/_schemas/port-spec.test.ts +164 -40
  9. package/_schemas/port-spec.ts +77 -26
  10. package/dist/_cli/login.js +1 -15
  11. package/dist/_cli/openBrowser.d.ts +1 -0
  12. package/dist/_cli/openBrowser.js +15 -0
  13. package/dist/_cli/previewApi.d.ts +1 -1
  14. package/dist/_cli/previewCommand.d.ts +8 -0
  15. package/dist/_cli/previewCommand.js +95 -37
  16. package/dist/_cli/previewFiles.d.ts +2 -1
  17. package/dist/_cli/previewFiles.js +42 -19
  18. package/dist/_cli/previewSlack.d.ts +1 -1
  19. package/dist/_cli/previewSlack.js +1 -1
  20. package/dist/_schemas/ai-manifest.d.ts +3 -0
  21. package/dist/_schemas/ai-manifest.js +2 -0
  22. package/dist/_schemas/graph.d.ts +23 -3
  23. package/dist/_schemas/graph.js +20 -8
  24. package/dist/_schemas/human-manifest.d.ts +27 -0
  25. package/dist/_schemas/human-manifest.js +121 -0
  26. package/dist/_schemas/node.d.ts +1 -0
  27. package/dist/_schemas/node.js +4 -0
  28. package/dist/_schemas/port-spec.d.ts +8 -9
  29. package/dist/_schemas/port-spec.js +64 -9
  30. package/dist/cli.js +3 -2
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +1 -0
  33. package/dist/init.js +22 -3
  34. package/dist/typegen.d.ts +3 -1
  35. package/dist/typegen.js +96 -14
  36. package/dist/validate.js +55 -0
  37. package/dist/views/_approval/ApprovalBar.d.ts +20 -0
  38. package/dist/views/_approval/ApprovalBar.js +74 -0
  39. package/dist/views/_approvalActions.d.ts +18 -0
  40. package/dist/views/_approvalActions.js +10 -0
  41. package/dist/views/_bridge.d.ts +12 -0
  42. package/dist/views/_bridge.js +10 -0
  43. package/dist/views/_parts/DocumentCard.d.ts +2 -0
  44. package/dist/views/_parts/DocumentCard.js +16 -0
  45. package/dist/views/_parts/FilePreview.d.ts +5 -0
  46. package/dist/views/_parts/FilePreview.js +101 -0
  47. package/dist/views/_parts/Linkify.d.ts +7 -0
  48. package/dist/views/_parts/Linkify.js +28 -0
  49. package/dist/views/_parts/Markdown.d.ts +44 -0
  50. package/dist/views/_parts/Markdown.js +313 -0
  51. package/dist/views/_parts/SchemaForm.d.ts +19 -0
  52. package/dist/views/_parts/SchemaForm.js +310 -0
  53. package/dist/views/_parts/fileRef.d.ts +7 -0
  54. package/dist/views/_parts/fileRef.js +11 -0
  55. package/dist/views/_parts/useNarrow.d.ts +5 -0
  56. package/dist/views/_parts/useNarrow.js +23 -0
  57. package/dist/views/host.d.ts +18 -0
  58. package/dist/views/host.js +20 -0
  59. package/dist/views/human.d.ts +73 -0
  60. package/dist/views/human.js +6 -0
  61. package/dist/views/index.d.ts +8 -0
  62. package/dist/views/index.js +12 -0
  63. package/dist/views/protocol.d.ts +34 -0
  64. package/dist/views/protocol.js +5 -0
  65. package/dist/views/runtime.d.ts +3 -0
  66. package/dist/views/runtime.js +86 -0
  67. package/dist/{slack-view.d.ts → views/slack.d.ts} +1 -1
  68. package/docs/development.md +45 -5
  69. package/index.ts +1 -0
  70. package/package.json +36 -4
  71. package/skills/create-loop/SKILL.md +6 -3
  72. package/views/_approval/ApprovalBar.tsx +239 -0
  73. package/views/_approvalActions.tsx +31 -0
  74. package/views/_bridge.tsx +36 -0
  75. package/views/_parts/DocumentCard.ts +18 -0
  76. package/views/_parts/FilePreview.tsx +180 -0
  77. package/views/_parts/Linkify.test.ts +40 -0
  78. package/views/_parts/Linkify.tsx +40 -0
  79. package/views/_parts/Markdown.test.ts +131 -0
  80. package/views/_parts/Markdown.tsx +472 -0
  81. package/views/_parts/SchemaForm.tsx +742 -0
  82. package/views/_parts/fileRef.ts +21 -0
  83. package/views/_parts/useNarrow.ts +24 -0
  84. package/views/host.ts +23 -0
  85. package/views/human.ts +91 -0
  86. package/views/index.ts +16 -0
  87. package/views/protocol.ts +40 -0
  88. package/views/runtime.tsx +132 -0
  89. package/views/slack.test.ts +40 -0
  90. package/{slack-view.ts → views/slack.ts} +3 -3
  91. /package/dist/{slack-view.js → views/slack.js} +0 -0
@@ -0,0 +1,310 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState, } from "react";
3
+ import { useViewBridge } from "../_bridge.js";
4
+ import { isFileRef } from "./fileRef.js";
5
+ import { isLongText, Markdown, useAutoGrow } from "./Markdown.js";
6
+ const isFileSchema = (schema) => schema?.["x-mawaru-type"] === "file";
7
+ /** フォーム描画できる schema(properties を持つ object)なら型を付けて返す */
8
+ export const asFormSchema = (schema) => {
9
+ if (schema == null || typeof schema !== "object")
10
+ return null;
11
+ const s = schema;
12
+ if (s.type !== "object")
13
+ return null;
14
+ if (!s.properties || Object.keys(s.properties).length === 0)
15
+ return null;
16
+ return s;
17
+ };
18
+ const nameStyle = {
19
+ fontFamily: "var(--font-mono)",
20
+ fontSize: "var(--text-sm)",
21
+ fontWeight: "var(--weight-medium)",
22
+ color: "var(--ink)",
23
+ whiteSpace: "nowrap",
24
+ };
25
+ // 見出し行の右に置く小さな操作(「編集」「表示に戻す」)
26
+ const headActionStyle = {
27
+ minHeight: "22px",
28
+ padding: "0 var(--space-2)",
29
+ fontSize: "var(--text-xs)",
30
+ fontWeight: "var(--weight-normal)",
31
+ flex: "none",
32
+ };
33
+ const FieldHead = ({ name, schema, required, marker, action, }) => (_jsxs("span", { style: {
34
+ display: "flex",
35
+ // boolean ではチェックボックスと横並びの flex アイテムになる。残り幅に広げて説明を出す
36
+ flex: 1,
37
+ minWidth: 0,
38
+ alignItems: "baseline",
39
+ gap: "var(--space-2)",
40
+ fontSize: "var(--text-xs)",
41
+ color: "var(--muted)",
42
+ }, children: [marker, _jsx("span", { style: nameStyle, children: name }), required && (_jsx("span", { style: { color: "var(--accent)", flex: "none" }, children: "*" })), _jsx("span", { style: {
43
+ flex: 1,
44
+ width: 0,
45
+ overflow: "hidden",
46
+ textOverflow: "ellipsis",
47
+ whiteSpace: "nowrap",
48
+ }, children: schema.description }), action] }));
49
+ // array・未知の型のフォールバック:フィールド単位の JSON 入力。
50
+ // 入力途中の不正 JSON は text に留め、valid になった時点で親へ反映する。
51
+ const JsonField = ({ value, onChange, }) => {
52
+ const [text, setText] = useState(() => JSON.stringify(value ?? null, null, 2));
53
+ const [invalid, setInvalid] = useState(false);
54
+ const ref = useAutoGrow(text);
55
+ return (_jsxs("div", { style: { display: "grid", gap: "2px" }, children: [_jsx("textarea", { ref: ref, rows: 3, value: text, onChange: (e) => {
56
+ setText(e.target.value);
57
+ try {
58
+ onChange(JSON.parse(e.target.value));
59
+ setInvalid(false);
60
+ }
61
+ catch {
62
+ setInvalid(true);
63
+ }
64
+ }, style: {
65
+ fontFamily: "var(--font-mono)",
66
+ fontSize: "var(--text-xs)",
67
+ lineHeight: 1.6,
68
+ maxHeight: "60vh",
69
+ resize: "vertical",
70
+ } }), invalid && (_jsx("p", { role: "alert", style: { fontSize: "var(--text-xs)" }, children: "JSON \u3068\u3057\u3066\u4E0D\u6B63\u3067\u3059" }))] }));
71
+ };
72
+ // ---- file 型(x-mawaru-type: "file")の添付フィールド ----
73
+ // 選択と同時に backend(POST /tenants/:id/files)へアップロードし、フォーム値には
74
+ // path 参照(fileRef)を入れる。実体は private バケットに置かれ、実行時に署名 URL が発行される
75
+ const fileRefOf = (value) => isFileRef(value) ? value : null;
76
+ const formatSize = (size) => {
77
+ if (size == null)
78
+ return null;
79
+ if (size >= 1024 * 1024)
80
+ return `${(size / 1024 / 1024).toFixed(1)}MB`;
81
+ return `${Math.max(1, Math.round(size / 1024))}KB`;
82
+ };
83
+ const useFileUpload = () => {
84
+ const bridge = useViewBridge();
85
+ const [uploading, setUploading] = useState(false);
86
+ const [error, setError] = useState(null);
87
+ const upload = async (list) => {
88
+ setUploading(true);
89
+ setError(null);
90
+ try {
91
+ const refs = [];
92
+ for (const file of Array.from(list)) {
93
+ refs.push(await bridge.uploadFile(file));
94
+ }
95
+ return refs;
96
+ }
97
+ catch (err) {
98
+ setError(err instanceof Error ? err.message : String(err));
99
+ return [];
100
+ }
101
+ finally {
102
+ setUploading(false);
103
+ }
104
+ };
105
+ return { upload, uploading, error };
106
+ };
107
+ const FileChip = ({ file, onRemove, }) => (_jsxs("span", { style: {
108
+ display: "inline-flex",
109
+ alignItems: "center",
110
+ gap: "var(--space-1)",
111
+ maxWidth: "100%",
112
+ border: "var(--border-w) solid var(--border)",
113
+ borderRadius: "var(--radius-s)",
114
+ background: "var(--surface-2)",
115
+ padding: "2px var(--space-2)",
116
+ fontSize: "var(--text-xs)",
117
+ }, children: [_jsx("span", { "aria-hidden": "true", children: "\uD83D\uDCCE" }), _jsx("span", { style: {
118
+ minWidth: 0,
119
+ overflow: "hidden",
120
+ textOverflow: "ellipsis",
121
+ whiteSpace: "nowrap",
122
+ }, children: file.name }), formatSize(file.size) && (_jsx("span", { style: { color: "var(--muted)", flex: "none" }, children: formatSize(file.size) })), _jsx("button", { type: "button", className: "ghost", onClick: onRemove, "aria-label": `${file.name} を外す`, style: {
123
+ minHeight: "18px",
124
+ padding: "0 4px",
125
+ fontSize: "var(--text-xs)",
126
+ }, children: "\u2715" })] }));
127
+ // 破線のドロップゾーン兼選択ボタン。クリックで隠し input[type=file] を開き、
128
+ // ドラッグ&ドロップも受け付ける(アップロード中は無効化)
129
+ const FilePicker = ({ label, multiple, uploading, onPick, }) => {
130
+ const inputRef = useRef(null);
131
+ // dragenter/leave は子要素をまたぐたびに発火するため深さを数えて判定する
132
+ const depthRef = useRef(0);
133
+ const [dragOver, setDragOver] = useState(false);
134
+ const hasFiles = (e) => Array.from(e.dataTransfer.types).includes("Files");
135
+ return (_jsxs(_Fragment, { children: [_jsx("input", { ref: inputRef, type: "file", multiple: multiple, style: { display: "none" }, onChange: (e) => {
136
+ if (e.target.files && e.target.files.length > 0) {
137
+ onPick(e.target.files);
138
+ }
139
+ e.target.value = "";
140
+ } }), _jsxs("button", { type: "button", onClick: () => inputRef.current?.click(), disabled: uploading, onDragEnter: (e) => {
141
+ if (uploading || !hasFiles(e))
142
+ return;
143
+ e.preventDefault();
144
+ depthRef.current += 1;
145
+ setDragOver(true);
146
+ }, onDragOver: (e) => {
147
+ if (uploading || !hasFiles(e))
148
+ return;
149
+ e.preventDefault();
150
+ }, onDragLeave: () => {
151
+ if (depthRef.current > 0)
152
+ depthRef.current -= 1;
153
+ if (depthRef.current === 0)
154
+ setDragOver(false);
155
+ }, onDrop: (e) => {
156
+ e.preventDefault();
157
+ depthRef.current = 0;
158
+ setDragOver(false);
159
+ if (uploading)
160
+ return;
161
+ if (e.dataTransfer.files.length > 0)
162
+ onPick(e.dataTransfer.files);
163
+ }, style: {
164
+ display: "flex",
165
+ alignItems: "center",
166
+ justifyContent: "center",
167
+ gap: "var(--space-1)",
168
+ width: "100%",
169
+ minHeight: "40px",
170
+ padding: "var(--space-2)",
171
+ border: `var(--border-w) dashed ${dragOver ? "var(--accent)" : "var(--border)"}`,
172
+ borderRadius: "var(--radius-s)",
173
+ background: dragOver ? "var(--surface-2)" : "transparent",
174
+ color: dragOver ? "var(--accent)" : "var(--muted)",
175
+ fontSize: "var(--text-xs)",
176
+ }, children: [_jsx("span", { "aria-hidden": "true", children: "\uD83D\uDCCE" }), uploading ? "アップロード中…" : `${label}、またはドラッグ&ドロップ`] })] }));
177
+ };
178
+ // file 単体:未選択(null)⇄ 選択済みチップ
179
+ const FileField = ({ value, onChange, }) => {
180
+ const { upload, uploading, error } = useFileUpload();
181
+ const file = fileRefOf(value);
182
+ const pick = async (list) => {
183
+ const refs = await upload(list);
184
+ if (refs[0])
185
+ onChange(refs[0]);
186
+ };
187
+ return (_jsxs("div", { style: { display: "grid", gap: "var(--space-1)" }, children: [file ? (_jsx(FileChip, { file: file, onRemove: () => onChange(null) })) : (_jsx(FilePicker, { label: "\u30D5\u30A1\u30A4\u30EB\u3092\u9078\u629E", uploading: uploading, onPick: pick })), error && (_jsx("p", { role: "alert", style: { margin: 0, fontSize: "var(--text-xs)" }, children: error }))] }));
188
+ };
189
+ // file 配列:チップのリスト+追加(複数選択可)
190
+ const FileArrayField = ({ value, onChange, }) => {
191
+ const { upload, uploading, error } = useFileUpload();
192
+ const files = Array.isArray(value)
193
+ ? value.map(fileRefOf).filter((f) => f !== null)
194
+ : [];
195
+ const pick = async (list) => {
196
+ const refs = await upload(list);
197
+ if (refs.length > 0)
198
+ onChange([...files, ...refs]);
199
+ };
200
+ return (_jsxs("div", { style: { display: "grid", gap: "var(--space-1)" }, children: [files.map((file, i) => (_jsx(FileChip, { file: file, onRemove: () => onChange(files.filter((_, j) => j !== i)) }, file.path))), _jsx(FilePicker, { label: "\u30D5\u30A1\u30A4\u30EB\u3092\u8FFD\u52A0", multiple: true, uploading: uploading, onPick: pick }), error && (_jsx("p", { role: "alert", style: { margin: 0, fontSize: "var(--text-xs)" }, children: error }))] }));
201
+ };
202
+ // 長文の raw 編集欄:内容の高さまで伸び、上限(60vh)を超えたらその中でスクロールする
203
+ const LongTextArea = ({ value, onChange, autoFocus, }) => {
204
+ const ref = useAutoGrow(value);
205
+ return (_jsx("textarea", { ref: ref,
206
+ // biome-ignore lint/a11y/noAutofocus: 「編集」を押して現れる欄なので即入力できる方が自然
207
+ autoFocus: autoFocus, rows: 3, value: value, onChange: (e) => onChange(e.target.value), style: {
208
+ fontSize: "var(--text-base)",
209
+ lineHeight: 1.9,
210
+ maxHeight: "60vh",
211
+ resize: "vertical",
212
+ } }));
213
+ };
214
+ // string フィールド:長文・改行入りの値は textarea に昇格する(AI の下書き等)。
215
+ // 入力中に input ⇄ textarea が flip してフォーカスが飛ばないよう、昇格したら戻さない
216
+ const StringInput = ({ value, onChange, }) => {
217
+ const long = isLongText(value);
218
+ const [multiline, setMultiline] = useState(long);
219
+ useEffect(() => {
220
+ if (long)
221
+ setMultiline(true);
222
+ }, [long]);
223
+ return multiline ? (_jsx(LongTextArea, { value: value, onChange: onChange })) : (_jsx("input", { type: "text", value: value, onChange: (e) => onChange(e.target.value) }));
224
+ };
225
+ // 値の入った長文 string=読み物。既定は Markdown の整形表示で、「編集」を押した時だけ
226
+ // raw に入れ替わる。長文が複数あるときの2つ目以降は畳んだ状態で始まる(見出しだけ出す)
227
+ const LongTextField = ({ name, schema, required, value, editable, defaultCollapsed, onChange, }) => {
228
+ // 開閉は <details>:読み取り専用の局面で書類全体を <fieldset disabled> で包んでも
229
+ // (フォーム部品ではないので)畳んだ本文を開いて読める
230
+ const [open, setOpen] = useState(!defaultCollapsed);
231
+ const [editing, setEditing] = useState(false);
232
+ const lines = value.split("\n").length;
233
+ return (_jsxs("details", { open: open, onToggle: (e) => setOpen(e.currentTarget.open), children: [_jsx("summary", { style: { cursor: "pointer", listStyle: "none" }, children: _jsx(FieldHead, { name: name, schema: schema, required: required, marker: _jsx("span", { "aria-hidden": "true", style: { flex: "none" }, children: open ? "▾" : "▸" }), action: open ? (editable ? (_jsx("button", { type: "button", className: "ghost",
234
+ // summary の中なので、押しても開閉させない
235
+ onClick: (e) => {
236
+ e.preventDefault();
237
+ setEditing((current) => !current);
238
+ }, style: headActionStyle, children: editing ? "表示に戻す" : "編集" })) : null) : (_jsxs("span", { style: {
239
+ flex: "none",
240
+ fontVariantNumeric: "var(--numeric)",
241
+ }, children: [lines, "\u884C"] })) }) }), _jsx("div", { style: {
242
+ display: "grid",
243
+ marginTop: "var(--space-3)",
244
+ borderTop: "var(--border-w) solid var(--border)",
245
+ paddingTop: "var(--space-3)",
246
+ }, children: editing ? (_jsx(LongTextArea, { value: value, onChange: onChange, autoFocus: true })) : (_jsx(Markdown, { text: value })) })] }));
247
+ };
248
+ const Field = ({ name, schema, required, value, readOnly, asDocument, defaultCollapsed, onChange, }) => {
249
+ const head = _jsx(FieldHead, { name: name, schema: schema, required: required });
250
+ // file 型は enum / type 判定より先に見る(実体は object / array なので)
251
+ if (isFileSchema(schema)) {
252
+ return (_jsxs("div", { style: { display: "grid", gap: "var(--space-1)" }, children: [head, _jsx(FileField, { value: value, onChange: onChange })] }));
253
+ }
254
+ if (schema.type === "array" && isFileSchema(schema.items)) {
255
+ return (_jsxs("div", { style: { display: "grid", gap: "var(--space-1)" }, children: [head, _jsx(FileArrayField, { value: value, onChange: onChange })] }));
256
+ }
257
+ const options = schema.enum;
258
+ if (options && options.length > 0) {
259
+ const idx = options.findIndex((opt) => JSON.stringify(opt) === JSON.stringify(value));
260
+ return (_jsxs("label", { children: [head, _jsxs("select", { value: String(idx), onChange: (e) => onChange(options[Number(e.target.value)]), children: [idx < 0 && (_jsx("option", { value: "-1", disabled: true, children: "\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044" })), options.map((opt, i) => (_jsx("option", { value: String(i), children: typeof opt === "string" ? opt : JSON.stringify(opt) }, JSON.stringify(opt))))] })] }));
261
+ }
262
+ switch (schema.type) {
263
+ case "string": {
264
+ const text = typeof value === "string" ? value : "";
265
+ if (asDocument) {
266
+ return (_jsx(LongTextField, { name: name, schema: schema, required: required, value: text, editable: !readOnly, defaultCollapsed: defaultCollapsed, onChange: onChange }));
267
+ }
268
+ return (_jsxs("div", { style: { display: "grid", gap: "var(--space-1)" }, children: [head, _jsx(StringInput, { value: text, onChange: onChange })] }));
269
+ }
270
+ case "number":
271
+ case "integer":
272
+ return (_jsxs("label", { children: [head, _jsx("input", { type: "number", step: schema.type === "integer" ? 1 : "any", value: typeof value === "number" ? value : "", onChange: (e) => {
273
+ const raw = e.target.value;
274
+ onChange(raw === "" ? null : Number(raw));
275
+ } })] }));
276
+ case "boolean":
277
+ return (_jsxs("label", { style: {
278
+ display: "flex",
279
+ alignItems: "center",
280
+ gap: "var(--space-2)",
281
+ }, children: [_jsx("input", { type: "checkbox", checked: Boolean(value), onChange: (e) => onChange(e.target.checked), style: { minHeight: 0, width: "14px", height: "14px" } }), head] }));
282
+ case "object": {
283
+ if (!schema.properties || Object.keys(schema.properties).length === 0) {
284
+ break;
285
+ }
286
+ const obj = (value != null && typeof value === "object" ? value : {});
287
+ return (_jsxs("div", { style: { display: "grid", gap: "var(--space-2)" }, children: [head, _jsx("div", { style: {
288
+ borderLeft: "var(--border-w) solid var(--border)",
289
+ paddingLeft: "var(--space-3)",
290
+ }, children: _jsx(SchemaForm, { schema: schema, value: obj, readOnly: readOnly, onChange: onChange }) })] }));
291
+ }
292
+ }
293
+ return (_jsxs("div", { style: { display: "grid", gap: "var(--space-1)" }, children: [head, _jsx(JsonField, { value: value, onChange: onChange })] }));
294
+ };
295
+ export const SchemaForm = ({ schema, value, readOnly = false, onChange, }) => {
296
+ const fields = Object.entries(schema.properties ?? {});
297
+ // 中身の入った長文=読み物(Markdown 表示)。開いた時点の値で決め、以後は変えない:
298
+ // 入力中に 80 文字を超えた瞬間に入力欄が表示に化けてフォーカスが飛ぶのを避ける
299
+ // (空欄から書き始める手動実行の入力フォームは従来どおり入力欄のまま)
300
+ const [documentKeys] = useState(() => fields
301
+ .filter(([key]) => {
302
+ const field = value[key];
303
+ return typeof field === "string" && isLongText(field);
304
+ })
305
+ .map(([key]) => key));
306
+ return (_jsx("div", { style: { display: "grid", gap: "var(--space-5)" }, children: fields.map(([key, child]) => (_jsx(Field, { name: key, schema: child, required: schema.required?.includes(key) ?? false, value: value[key], readOnly: readOnly, asDocument: documentKeys.includes(key),
307
+ // 読み物が複数あるとき、先頭(=主役の成果物)だけ開いて以降は畳む。
308
+ // key 名には依存せず、スキーマの並び順だけで決める
309
+ defaultCollapsed: key !== documentKeys[0], onChange: (v) => onChange({ ...value, [key]: v }) }, key))) }));
310
+ };
@@ -0,0 +1,7 @@
1
+ export type FileRef = {
2
+ path: string;
3
+ name: string;
4
+ mime_type?: string;
5
+ size?: number;
6
+ };
7
+ export declare const isFileRef: (value: unknown) => value is FileRef;
@@ -0,0 +1,11 @@
1
+ export const isFileRef = (value) => {
2
+ if (typeof value !== "object" || value === null)
3
+ return false;
4
+ const v = value;
5
+ return (typeof v.path === "string" &&
6
+ v.path !== "" &&
7
+ typeof v.name === "string" &&
8
+ v.name !== "" &&
9
+ (v.mime_type === undefined || typeof v.mime_type === "string") &&
10
+ (v.size === undefined || typeof v.size === "number"));
11
+ };
@@ -0,0 +1,5 @@
1
+ export declare const NARROW_WIDTH = 560;
2
+ export declare const useNarrow: <T extends HTMLElement>() => {
3
+ ref: import("react").RefObject<T | null>;
4
+ narrow: boolean;
5
+ };
@@ -0,0 +1,23 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ // 承認ビューの幅対応。判定は**ビューポートではなくコンテナの幅**で行う。
3
+ //
4
+ // 開発プレビュー(/dev/views/approval)は幅の確認を「狭いコンテナ」で再現するので、
5
+ // media query で判定すると実機とプレビューで挙動がズレる(プレビューでは常に
6
+ // デスクトップ扱いになる)。ResizeObserver でコンテナ自身を測れば両方で同じになる。
7
+ export const NARROW_WIDTH = 560;
8
+ export const useNarrow = () => {
9
+ const ref = useRef(null);
10
+ const [narrow, setNarrow] = useState(false);
11
+ useEffect(() => {
12
+ const element = ref.current;
13
+ if (!element)
14
+ return;
15
+ const observer = new ResizeObserver(([entry]) => {
16
+ if (entry)
17
+ setNarrow(entry.contentRect.width < NARROW_WIDTH);
18
+ });
19
+ observer.observe(element);
20
+ return () => observer.disconnect();
21
+ }, []);
22
+ return { ref, narrow };
23
+ };
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ export { ApprovalActionsProvider } from "./_approvalActions.js";
3
+ export { ViewBridgeProvider } from "./_bridge.js";
4
+ export * from "./protocol.js";
5
+ export declare const viewToHostSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
6
+ tag: z.ZodLiteral<"mawaru:view">;
7
+ type: z.ZodLiteral<"ready">;
8
+ }, z.core.$strip>, z.ZodObject<{
9
+ tag: z.ZodLiteral<"mawaru:view">;
10
+ type: z.ZodLiteral<"error">;
11
+ message: z.ZodString;
12
+ }, z.core.$strip>, z.ZodObject<{
13
+ tag: z.ZodLiteral<"mawaru:view">;
14
+ type: z.ZodLiteral<"request">;
15
+ id: z.ZodNumber;
16
+ method: z.ZodString;
17
+ args: z.ZodUnknown;
18
+ }, z.core.$strip>], "type">;
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ import { VIEW_MESSAGE_TAG } from "./protocol.js";
3
+ export { ApprovalActionsProvider } from "./_approvalActions.js";
4
+ // ホスト(mawaru の画面)だけが使うもの(公開 API ではない):sandbox iframe からのメッセージを確かめる形
5
+ // (protocol.ts の型と同じ。iframe の中は信用できないコードなので、形を確かめてから使う)と、
6
+ // ビューに頼む口を差す提供部品
7
+ export { ViewBridgeProvider } from "./_bridge.js";
8
+ export * from "./protocol.js";
9
+ const tag = z.literal(VIEW_MESSAGE_TAG);
10
+ export const viewToHostSchema = z.discriminatedUnion("type", [
11
+ z.object({ tag, type: z.literal("ready") }),
12
+ z.object({ tag, type: z.literal("error"), message: z.string() }),
13
+ z.object({
14
+ tag,
15
+ type: z.literal("request"),
16
+ id: z.number().int(),
17
+ method: z.string(),
18
+ args: z.unknown(),
19
+ }),
20
+ ]);
@@ -0,0 +1,73 @@
1
+ import type { ReactNode } from "react";
2
+ import type { JsonSchema } from "../_schemas/node.js";
3
+ /** 承認画面の局面:承認待ち/承認済み/差し戻し後(AI 再実行待ち)/失敗/キャンセル */
4
+ export type ApprovalPhase = "waiting" | "approved" | "rejected" | "failed" | "cancelled";
5
+ /**
6
+ * 承認バーの材料。部品の `ApprovalBar` はこれをそのまま使う。承認バーを自分で書くときに読む。
7
+ */
8
+ export type ApprovalDecisionContext = {
9
+ /** 見ている人が今決定できる(担当で承認待ち)。全員の承認が要るノードでも true(値を直せるかは `editable`) */
10
+ canDecide: boolean;
11
+ /** 承認したあとに流れる先のノード名(つながっていなければ null) */
12
+ approveTargetName: string | null;
13
+ /** 差し戻し先のノード名(無ければ差し戻せない) */
14
+ rejectTargetName: string | null;
15
+ /** 承認・差し戻し以外の出口。`needsInput` は、今の値の型が出口と合わず、値を入れるダイアログが要るもの */
16
+ exits: {
17
+ key: string;
18
+ name: string;
19
+ needsInput: boolean;
20
+ }[];
21
+ /** 担当でない人が開いたときの、担当の名前(担当なら null) */
22
+ otherAssignees: string[] | null;
23
+ /** 全員の承認が要るときの進み具合(`approvedByMe` は自分がもう承認したか)。要らなければ null */
24
+ progress: {
25
+ done: number;
26
+ total: number;
27
+ approvedByMe: boolean;
28
+ } | null;
29
+ /** 失敗した step の理由(失敗していなければ null) */
30
+ failedReason: string | null;
31
+ };
32
+ /** mawaru から承認ビューへ渡す材料 */
33
+ export type ApprovalViewContext<TIn = unknown> = {
34
+ /** 届いた値(step の input そのもの)。ビューで直した値はビューの state に持つ */
35
+ output: TIn;
36
+ /** `output` の型= human の in ポートの型(ビューの宣言が写っている)。`SchemaForm` に渡すとフォームになる */
37
+ outputSchema: JsonSchema | null;
38
+ /** ループ変数(この step が始まった時点の写し)。予約キー `input` が run の起動入力 */
39
+ refs: unknown;
40
+ /** この human ノードの名前 */
41
+ nodeName: string;
42
+ /** 局面 */
43
+ phase: ApprovalPhase;
44
+ /** 値を直してよい(承認待ちで、見ている人が担当。全員の承認が要るノードでは false) */
45
+ editable: boolean;
46
+ /** 承認バーの材料 */
47
+ decision: ApprovalDecisionContext;
48
+ };
49
+ /**
50
+ * 承認ビュー:ctx を受けて承認画面(上の帯より下の全部)を描く React コンポーネント。
51
+ * 承認バーは `<ApprovalBar ctx={ctx} />` を置く(置かなければ承認バーは出ない)。
52
+ */
53
+ export type ApprovalView<TIn = unknown> = {
54
+ bivariance(props: {
55
+ ctx: ApprovalViewContext<TIn>;
56
+ }): ReactNode;
57
+ }["bivariance"];
58
+ /** 承認ビューを定義する。型を付けるためだけの関数(値はそのまま返す) */
59
+ export declare const defineApprovalView: <TIn = unknown>(view: ApprovalView<TIn>) => ApprovalView<TIn>;
60
+ /** 手元プレビュー(npx mawaru preview human/<dir>)のケース。nodes/human/<dir>/cases.ts の export default */
61
+ export type ApprovalViewCase = {
62
+ name: string;
63
+ /** `ctx.output`(= step の input) */
64
+ input: unknown;
65
+ refs?: unknown;
66
+ /** 省略時は name */
67
+ nodeName?: string;
68
+ /** 省略時は承認待ち */
69
+ phase?: ApprovalPhase;
70
+ };
71
+ export { ApprovalBar } from "./_approval/ApprovalBar.js";
72
+ export { type ApprovalActions, useApprovalActions } from "./_approvalActions.js";
73
+ export * from "./index.js";
@@ -0,0 +1,6 @@
1
+ /** 承認ビューを定義する。型を付けるためだけの関数(値はそのまま返す) */
2
+ export const defineApprovalView = (view) => view;
3
+ export { ApprovalBar } from "./_approval/ApprovalBar.js";
4
+ export { useApprovalActions } from "./_approvalActions.js";
5
+ // 部品キット(土台)もここから import できる
6
+ export * from "./index.js";
@@ -0,0 +1,8 @@
1
+ export { useViewBridge, type ViewBridge } from "./_bridge.js";
2
+ export { documentCardStyle } from "./_parts/DocumentCard.js";
3
+ export { FilePreview } from "./_parts/FilePreview.js";
4
+ export { type FileRef, isFileRef } from "./_parts/fileRef.js";
5
+ export { isUrl, Linkify, linkify } from "./_parts/Linkify.js";
6
+ export { isLongText, Markdown, useAutoGrow } from "./_parts/Markdown.js";
7
+ export { asFormSchema, type JsonSchema as FormSchema, SchemaForm, } from "./_parts/SchemaForm.js";
8
+ export { NARROW_WIDTH, useNarrow } from "./_parts/useNarrow.js";
@@ -0,0 +1,12 @@
1
+ // ビューの部品キット(`@mawaru/sdk/views`)。種類(承認・…)に依らない土台:部品・ホストに頼む口。
2
+ // mawaru が顧客 repo のビューをまとめるとき、mawaru の今の版のこのキットを同梱する。
3
+ // **公開 API**:部品の props とホストに頼む口は後方互換でしか変えない
4
+ // (docs/tasks/wip/承認ビューの顧客repo配置.md の「互換の管理」)
5
+ export { useViewBridge } from "./_bridge.js";
6
+ export { documentCardStyle } from "./_parts/DocumentCard.js";
7
+ export { FilePreview } from "./_parts/FilePreview.js";
8
+ export { isFileRef } from "./_parts/fileRef.js";
9
+ export { isUrl, Linkify, linkify } from "./_parts/Linkify.js";
10
+ export { isLongText, Markdown, useAutoGrow } from "./_parts/Markdown.js";
11
+ export { asFormSchema, SchemaForm, } from "./_parts/SchemaForm.js";
12
+ export { NARROW_WIDTH, useNarrow } from "./_parts/useNarrow.js";
@@ -0,0 +1,34 @@
1
+ export declare const VIEW_MESSAGE_TAG = "mawaru:view";
2
+ export type ThemeMode = "light" | "dark";
3
+ export type ViewToHost = {
4
+ tag: typeof VIEW_MESSAGE_TAG;
5
+ type: "ready";
6
+ } | {
7
+ tag: typeof VIEW_MESSAGE_TAG;
8
+ type: "error";
9
+ message: string;
10
+ } | {
11
+ tag: typeof VIEW_MESSAGE_TAG;
12
+ type: "request";
13
+ id: number;
14
+ method: string;
15
+ args: unknown;
16
+ };
17
+ export type HostToView = {
18
+ tag: typeof VIEW_MESSAGE_TAG;
19
+ type: "ctx";
20
+ ctx: unknown;
21
+ mode: ThemeMode;
22
+ } | {
23
+ tag: typeof VIEW_MESSAGE_TAG;
24
+ type: "response";
25
+ id: number;
26
+ ok: true;
27
+ value: unknown;
28
+ } | {
29
+ tag: typeof VIEW_MESSAGE_TAG;
30
+ type: "response";
31
+ id: number;
32
+ ok: false;
33
+ error: string;
34
+ };
@@ -0,0 +1,5 @@
1
+ // sandbox iframe の中の実行部分(runtime.tsx)とホスト(mawaru の画面)とのメッセージの形。
2
+ // **公開 API ではない**:iframe の中の実行部分とホストは同じデプロイの mawaru のコード(まとめるときに
3
+ // その時点の版を同梱する)なので、いつ変えてもよい(docs/tasks/wip/承認ビューの顧客repo配置.md の「互換の管理」)。
4
+ // iframe に入る JS を小さく保つため、ここには型と定数だけを置く(ホストの検査は host.ts)
5
+ export const VIEW_MESSAGE_TAG = "mawaru:view";
@@ -0,0 +1,3 @@
1
+ import type { ApprovalView } from "./human.js";
2
+ export declare const requestHost: <T>(method: string, args: unknown) => Promise<T>;
3
+ export declare const runApprovalView: (View: ApprovalView) => void;
@@ -0,0 +1,86 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Component } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { ApprovalActionsProvider, } from "./_approvalActions.js";
5
+ import { ViewBridgeProvider } from "./_bridge.js";
6
+ import { VIEW_MESSAGE_TAG, } from "./protocol.js";
7
+ const post = (message) =>
8
+ // 受け手は親(ホスト)の window だけ。不透明オリジンの iframe からは親のオリジンを指定できない
9
+ window.parent.postMessage({ tag: VIEW_MESSAGE_TAG, ...message }, "*");
10
+ const messageOf = (err) => err instanceof Error ? err.message : String(err);
11
+ let nextRequestId = 1;
12
+ const pending = new Map();
13
+ const ctxListeners = new Set();
14
+ const onHostMessage = (event) => {
15
+ if (event.source !== window.parent)
16
+ return;
17
+ const message = event.data;
18
+ if (message?.tag !== VIEW_MESSAGE_TAG)
19
+ return;
20
+ if (message.type === "response") {
21
+ const waiting = pending.get(message.id);
22
+ if (!waiting)
23
+ return;
24
+ pending.delete(message.id);
25
+ if (message.ok)
26
+ waiting.resolve(message.value);
27
+ else
28
+ waiting.reject(new Error(message.error));
29
+ return;
30
+ }
31
+ if (message.type === "ctx") {
32
+ document.documentElement.dataset.mode = message.mode;
33
+ for (const listener of ctxListeners)
34
+ listener(message.ctx, message.mode);
35
+ }
36
+ };
37
+ // ホストに頼む(返事が来るまで待つ)
38
+ export const requestHost = (method, args) => new Promise((resolve, reject) => {
39
+ const id = nextRequestId++;
40
+ pending.set(id, {
41
+ resolve: resolve,
42
+ reject,
43
+ });
44
+ post({ type: "request", id, method, args });
45
+ });
46
+ const bridge = {
47
+ fileUrl: (file) => requestHost("fileUrl", { file }),
48
+ uploadFile: (file) => requestHost("uploadFile", { file }),
49
+ copyText: (text) => requestHost("copyText", { text }),
50
+ };
51
+ // 描画の例外をホストへ知らせる(ホストは標準ビューに差し替える)
52
+ class ReportErrors extends Component {
53
+ state = { failed: false };
54
+ static getDerivedStateFromError() {
55
+ return { failed: true };
56
+ }
57
+ componentDidCatch(error) {
58
+ post({ type: "error", message: messageOf(error) });
59
+ }
60
+ render() {
61
+ return this.state.failed ? null : this.props.children;
62
+ }
63
+ }
64
+ // iframe の中の共通の準備:ホストからのメッセージ・例外の報告。
65
+ // 最初の ctx が届いたら render を呼ぶ(以降の ctx も同じ render に渡す)
66
+ const startView = (render) => {
67
+ window.addEventListener("message", onHostMessage);
68
+ window.addEventListener("error", (event) => post({ type: "error", message: event.message }));
69
+ window.addEventListener("unhandledrejection", (event) => post({ type: "error", message: messageOf(event.reason) }));
70
+ const container = document.getElementById("root");
71
+ if (!container)
72
+ throw new Error("#root がありません");
73
+ const root = createRoot(container);
74
+ ctxListeners.add((ctx) => root.render(_jsx(ViewBridgeProvider, { bridge: bridge, children: _jsx(ReportErrors, { children: render(ctx) }) })));
75
+ post({ type: "ready" });
76
+ };
77
+ // ---- 承認ビュー ----
78
+ const actions = {
79
+ decide: (port, value) => requestHost("decide", { port, value }),
80
+ openRejectDialog: () => requestHost("openRejectDialog", {}),
81
+ openExitDialog: (key, value) => requestHost("openExitDialog", { key, value }),
82
+ close: () => requestHost("close", {}),
83
+ };
84
+ // ビューはコンポーネント1つ。状態(直した値・選択など)はビューが useState で持ち、ctx が変わっても
85
+ // (局面・担当の更新)同じコンポーネントとして描き直すので保たれる
86
+ export const runApprovalView = (View) => startView((ctx) => (_jsx(ApprovalActionsProvider, { value: actions, children: _jsx(View, { ctx: ctx }) })));
@@ -1,4 +1,4 @@
1
- import type { JsonSchema, NodeKind } from "./_schemas/node.js";
1
+ import type { JsonSchema, NodeKind } from "../_schemas/node.js";
2
2
  export type SlackBlocks = unknown[];
3
3
  export type FileRef = {
4
4
  path: string;