@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,10 @@
1
+ import { createContext, useContext } from "react";
2
+ const ApprovalActionsContext = createContext(null);
3
+ export const ApprovalActionsProvider = ApprovalActionsContext.Provider;
4
+ /** 承認画面の操作を頼む口(承認画面の中でだけ使える) */
5
+ export const useApprovalActions = () => {
6
+ const actions = useContext(ApprovalActionsContext);
7
+ if (!actions)
8
+ throw new Error("承認画面の中で使ってください");
9
+ return actions;
10
+ };
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from "react";
2
+ import type { FileRef } from "./_parts/fileRef.js";
3
+ export type ViewBridge = {
4
+ fileUrl: (file: FileRef) => Promise<string>;
5
+ uploadFile: (file: File) => Promise<FileRef>;
6
+ copyText: (text: string) => Promise<void>;
7
+ };
8
+ export declare const ViewBridgeProvider: ({ bridge, children, }: {
9
+ bridge: ViewBridge;
10
+ children: ReactNode;
11
+ }) => import("react").JSX.Element;
12
+ export declare const useViewBridge: () => ViewBridge;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ const ViewBridgeContext = createContext(null);
4
+ export const ViewBridgeProvider = ({ bridge, children, }) => (_jsx(ViewBridgeContext.Provider, { value: bridge, children: children }));
5
+ export const useViewBridge = () => {
6
+ const bridge = useContext(ViewBridgeContext);
7
+ if (!bridge)
8
+ throw new Error("ViewBridgeProvider の中で使ってください");
9
+ return bridge;
10
+ };
@@ -0,0 +1,2 @@
1
+ import type { CSSProperties } from "react";
2
+ export declare const documentCardStyle: (narrow: boolean) => CSSProperties;
@@ -0,0 +1,16 @@
1
+ // 承認画面の「書類」カードの枠(標準ビュー・回答レビュー・お問い合わせ回答で同じ形)。
2
+ // 狭い幅(useNarrow)では左右の余白を詰めて本文の幅を確保する
3
+ export const documentCardStyle = (narrow) => ({
4
+ maxWidth: "720px",
5
+ margin: "0 auto",
6
+ display: "flex",
7
+ flexDirection: "column",
8
+ gap: "var(--space-4)",
9
+ background: "var(--surface)",
10
+ border: "var(--border-w) solid var(--border)",
11
+ borderRadius: "var(--radius-m)",
12
+ boxShadow: "var(--shadow)",
13
+ padding: narrow
14
+ ? "var(--space-5) var(--space-4) var(--space-6)"
15
+ : "var(--space-6) var(--space-10) var(--space-8)",
16
+ });
@@ -0,0 +1,5 @@
1
+ import type { FileRef } from "./fileRef.js";
2
+ export declare const FilePreview: ({ file, maxHeight, }: {
3
+ file: FileRef;
4
+ maxHeight?: string;
5
+ }) => import("react").JSX.Element;
@@ -0,0 +1,101 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { useViewBridge } from "../_bridge.js";
4
+ // file 参照の閲覧表示:private バケットなので描画のたびに閲覧用署名 URL を(ホストに頼んで)取り、
5
+ // 画像(GIF 含む)と PDF はそのままインラインで見せる。それ以外は名前チップのリンクにする。
6
+ // NodeIoCards の IoFileItem と同じ経路だが、こちらは「見て判断する」ための大きさで出す。
7
+ // PDF は請求書・契約書のように「現物を見て承認する」書類で必要になる(承認画面の中で
8
+ // 完結させたいので、別タブへ飛ばさない)
9
+ const chipStyle = {
10
+ display: "inline-flex",
11
+ alignItems: "center",
12
+ gap: "4px",
13
+ maxWidth: "100%",
14
+ border: "var(--border-w) solid var(--border)",
15
+ borderRadius: "var(--radius-s)",
16
+ background: "var(--surface-2)",
17
+ padding: "2px 8px",
18
+ fontSize: "var(--text-xs)",
19
+ color: "var(--ink)",
20
+ textDecoration: "none",
21
+ };
22
+ // mime_type は上流が入れてくれないことがあるので拡張子でも判定する
23
+ const isImageFile = (file) => file.mime_type?.startsWith("image/") ??
24
+ /\.(gif|png|jpe?g|webp|avif|svg)$/i.test(file.name);
25
+ const isPdfFile = (file) => file.mime_type === "application/pdf" || /\.pdf$/i.test(file.name);
26
+ export const FilePreview = ({ file, maxHeight = "420px", }) => {
27
+ const bridge = useViewBridge();
28
+ const [url, setUrl] = useState(null);
29
+ // 署名 URL を取れなかった(ファイルが消えた・パスが違う)ことを「読み込み中」と
30
+ // 区別する:失敗を黙って読み込み中のまま固めると、原因が画面から分からない
31
+ const [failed, setFailed] = useState(false);
32
+ useEffect(() => {
33
+ let alive = true;
34
+ setUrl(null);
35
+ setFailed(false);
36
+ bridge
37
+ .fileUrl(file)
38
+ .then((signed) => {
39
+ if (alive)
40
+ setUrl(signed);
41
+ })
42
+ .catch(() => {
43
+ if (alive)
44
+ setFailed(true);
45
+ });
46
+ return () => {
47
+ alive = false;
48
+ };
49
+ }, [bridge, file]);
50
+ const caption = (_jsx("span", { style: { fontSize: "var(--text-xs)", color: "var(--muted)" }, children: file.name }));
51
+ // PDF はブラウザ内蔵ビューアに任せる(依存を増やさない)。maxHeight は画像では
52
+ // 上限だが、PDF では枠の高さそのものになる(内側でスクロールさせるため)
53
+ if (isPdfFile(file)) {
54
+ return (_jsxs("figure", { style: { margin: 0, display: "grid", gap: "var(--space-1)" }, children: [url ? (_jsx("iframe", { src: url, title: file.name, style: {
55
+ display: "block",
56
+ width: "100%",
57
+ height: maxHeight,
58
+ background: "var(--surface-2)",
59
+ borderRadius: "var(--radius-s)",
60
+ border: "var(--border-w) solid var(--border)",
61
+ } })) : (_jsx("div", { style: {
62
+ display: "grid",
63
+ placeItems: "center",
64
+ height: maxHeight,
65
+ background: "var(--surface-2)",
66
+ borderRadius: "var(--radius-s)",
67
+ border: "var(--border-w) dashed var(--border)",
68
+ fontSize: "var(--text-xs)",
69
+ color: "var(--muted)",
70
+ textAlign: "center",
71
+ padding: "0 var(--space-4)",
72
+ }, children: failed ? "ファイルを読み込めませんでした" : "読み込み中…" })), _jsxs("figcaption", { style: {
73
+ display: "flex",
74
+ justifyContent: "space-between",
75
+ gap: "var(--space-2)",
76
+ }, children: [caption, url && (_jsx("a", { href: url, target: "_blank", rel: "noreferrer", style: { fontSize: "var(--text-xs)", color: "var(--muted)" }, children: "\u5225\u30BF\u30D6\u3067\u958B\u304F" }))] })] }));
77
+ }
78
+ if (!isImageFile(file)) {
79
+ return url ? (_jsxs("a", { href: url, target: "_blank", rel: "noreferrer", style: chipStyle, children: [_jsx("span", { "aria-hidden": "true", children: "\uD83D\uDCCE" }), file.name] })) : (_jsxs("span", { style: chipStyle, children: [_jsx("span", { "aria-hidden": "true", children: "\uD83D\uDCCE" }), file.name] }));
80
+ }
81
+ return (_jsxs("figure", { style: { margin: 0, display: "grid", gap: "var(--space-1)" }, children: [url ? (_jsx("a", { href: url, target: "_blank", rel: "noreferrer", children: _jsx("img", { src: url, alt: file.name, style: {
82
+ display: "block",
83
+ width: "100%",
84
+ maxHeight,
85
+ objectFit: "contain",
86
+ background: "var(--surface-2)",
87
+ borderRadius: "var(--radius-s)",
88
+ border: "var(--border-w) solid var(--border)",
89
+ } }) })) : (_jsx("div", { style: {
90
+ display: "grid",
91
+ placeItems: "center",
92
+ height: "160px",
93
+ background: "var(--surface-2)",
94
+ borderRadius: "var(--radius-s)",
95
+ border: "var(--border-w) dashed var(--border)",
96
+ fontSize: "var(--text-xs)",
97
+ color: "var(--muted)",
98
+ textAlign: "center",
99
+ padding: "0 var(--space-4)",
100
+ }, children: failed ? "画像を読み込めませんでした" : "読み込み中…" })), _jsx("figcaption", { children: caption })] }));
101
+ };
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from "react";
2
+ /** 文字列全体が1つの URL か */
3
+ export declare const isUrl: (value: string) => boolean;
4
+ export declare const linkify: (text: string, prefix?: string) => ReactNode[];
5
+ export declare const Linkify: ({ text }: {
6
+ text: string;
7
+ }) => import("react").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // テキスト中の素の URL(http/https)をクリックできるリンクにする。
3
+ // 値の表示(I/O・タイムライン・承認ビュー等)はどこでもこれを通す。
4
+ // 引用符・括弧・全角の区切りは URL に含めず、末尾の句読点も外す
5
+ const urlPattern = /https?:\/\/[^\s"'<>()[\]{}()「」『』【】、。]+/g;
6
+ const trailingPunct = /[.,;:!?]+$/;
7
+ /** 文字列全体が1つの URL か */
8
+ export const isUrl = (value) => /^https?:\/\/\S+$/.test(value.trim());
9
+ export const linkify = (text, prefix = "") => {
10
+ const nodes = [];
11
+ let last = 0;
12
+ for (const match of text.matchAll(urlPattern)) {
13
+ const start = match.index ?? 0;
14
+ const url = match[0].replace(trailingPunct, "");
15
+ if (url.length <= "https://".length)
16
+ continue;
17
+ if (start > last)
18
+ nodes.push(text.slice(last, start));
19
+ nodes.push(_jsx("a", { href: url, target: "_blank", rel: "noreferrer",
20
+ // 行クリックで選択・開閉する親の中でも、リンクはリンクとして開く
21
+ onClick: (e) => e.stopPropagation(), style: { overflowWrap: "anywhere" }, children: url }, `${prefix}${start}`));
22
+ last = start + url.length;
23
+ }
24
+ if (last < text.length)
25
+ nodes.push(text.slice(last));
26
+ return nodes;
27
+ };
28
+ export const Linkify = ({ text }) => _jsx(_Fragment, { children: linkify(text) });
@@ -0,0 +1,44 @@
1
+ /** 長文(=Markdown として描く)か。入力欄の昇格判定と共通の基準 */
2
+ export declare const isLongText: (value: string) => boolean;
3
+ type ListItem = {
4
+ text: string;
5
+ nested: boolean;
6
+ };
7
+ export type TableAlign = "left" | "center" | "right" | null;
8
+ export type Block = {
9
+ kind: "heading";
10
+ level: number;
11
+ text: string;
12
+ } | {
13
+ kind: "list";
14
+ ordered: boolean;
15
+ items: ListItem[];
16
+ } | {
17
+ kind: "quote";
18
+ lines: string[];
19
+ } | {
20
+ kind: "code";
21
+ lines: string[];
22
+ } | {
23
+ kind: "rule";
24
+ } | {
25
+ kind: "table";
26
+ align: TableAlign[];
27
+ header: string[];
28
+ rows: string[][];
29
+ } | {
30
+ kind: "para";
31
+ lines: string[];
32
+ };
33
+ /** 行を読んでブロックに割る(描画から切り離してテストできるよう公開している) */
34
+ export declare const parseBlocks: (source: string) => Block[];
35
+ export declare const Markdown: ({ text }: {
36
+ text: string;
37
+ }) => import("react").JSX.Element;
38
+ /**
39
+ * 内容の高さまで伸びる textarea(上限を超えたらその中でスクロール)。
40
+ * 返り値は ref にそのまま渡す。「編集」を押して後から現れる欄でも、
41
+ * 付いた瞬間に測るので初回から正しい高さで出る
42
+ */
43
+ export declare const useAutoGrow: (value: string) => ((el: HTMLTextAreaElement | null) => void);
44
+ export {};
@@ -0,0 +1,313 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useLayoutEffect, useRef, } from "react";
3
+ import { linkify } from "./Linkify.js";
4
+ // AI の下書き・調査レポートは Markdown で届く。生ソースのまま読ませると読み物として
5
+ // 読めないので整形して出す。frontend は依存最小主義なのでレンダラは自前で持ち、
6
+ // 実際に出る記法だけを見る:見出し / 箇条書き・番号付き / 引用 / コードブロック /
7
+ // 水平線 / 表 / 段落と、インラインの強調・コード・リンク(素の URL を含む)。
8
+ // 対応しない記法は素のテキストとしてそのまま出す=壊さない
9
+ /** 長文(=Markdown として描く)か。入力欄の昇格判定と共通の基準 */
10
+ export const isLongText = (value) => value.includes("\n") || value.length > 80;
11
+ const bodyText = {
12
+ fontSize: "var(--text-base)",
13
+ lineHeight: 1.9,
14
+ margin: 0,
15
+ overflowWrap: "anywhere",
16
+ };
17
+ const codeChip = {
18
+ fontFamily: "var(--font-mono)",
19
+ fontSize: "0.9em",
20
+ background: "var(--surface-2)",
21
+ border: "var(--border-w) solid var(--border)",
22
+ borderRadius: "var(--radius-s)",
23
+ padding: "1px 5px",
24
+ };
25
+ // インライン:**太字** / `コード` / [リンク](url)。分割は捕捉グループ付きなので
26
+ // 区切り自身も配列に残る(split の仕様)
27
+ const inlineToken = /(`[^`\n]+`|\*\*[^*\n]+\*\*|\[[^\]\n]+\]\([^)\s]+\))/g;
28
+ const renderInline = (text, prefix = "") => text.split(inlineToken).map((token, i) => {
29
+ if (!token)
30
+ return null;
31
+ const key = `${prefix}${i}`;
32
+ if (token.startsWith("`") && token.endsWith("`") && token.length > 2) {
33
+ return (_jsx("code", { style: codeChip, children: token.slice(1, -1) }, key));
34
+ }
35
+ if (token.startsWith("**") && token.endsWith("**") && token.length > 4) {
36
+ return (_jsx("strong", { style: {
37
+ fontWeight: "var(--weight-bold)",
38
+ }, children: token.slice(2, -2) }, key));
39
+ }
40
+ const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(token);
41
+ if (link) {
42
+ return (_jsx("a", { href: link[2], target: "_blank", rel: "noreferrer", children: link[1] }, key));
43
+ }
44
+ // 素の URL もリンクにする
45
+ return _jsx("span", { children: linkify(token, `${key}-`) }, key);
46
+ });
47
+ // 段落内の改行は改行として残す(日本語の下書きは1行1文で書かれることが多い)
48
+ const renderLines = (lines) => lines.flatMap((line, i) => i === 0
49
+ ? renderInline(line, "0-")
50
+ : [
51
+ _jsx("br", {}, `br${i}`),
52
+ ...renderInline(line, `${i}-`),
53
+ ]);
54
+ const headingOf = /^(#{1,6})\s+(.*)$/;
55
+ const bulletOf = /^(\s*)[-*+]\s+(.*)$/;
56
+ const orderedOf = /^(\s*)\d+[.)]\s+(.*)$/;
57
+ const quoteOf = /^>\s?(.*)$/;
58
+ const ruleOf = /^(-{3,}|\*{3,}|_{3,})\s*$/;
59
+ // 表(GFM):`| a | b |` の行の直後に区切り行(`|---|:-:|--:|`)があれば表。
60
+ // 外側の | は無くてもよい。セルの分割は `\|` でエスケープした | を除く
61
+ const splitCells = (line) => {
62
+ let body = line.trim();
63
+ if (body.startsWith("|"))
64
+ body = body.slice(1);
65
+ if (body.endsWith("|") && !body.endsWith("\\|"))
66
+ body = body.slice(0, -1);
67
+ return body
68
+ .split(/(?<!\\)\|/)
69
+ .map((cell) => cell.trim().replace(/\\\|/g, "|"));
70
+ };
71
+ const delimiterCellOf = /^:?-+:?$/;
72
+ const alignOf = (cell) => cell.startsWith(":") && cell.endsWith(":")
73
+ ? "center"
74
+ : cell.endsWith(":")
75
+ ? "right"
76
+ : cell.startsWith(":")
77
+ ? "left"
78
+ : null;
79
+ const isTableStart = (lines, i) => {
80
+ const head = lines[i] ?? "";
81
+ const delimiter = lines[i + 1] ?? "";
82
+ if (!head.includes("|") || !delimiter.includes("-"))
83
+ return false;
84
+ const cells = splitCells(delimiter);
85
+ return (cells.length > 0 &&
86
+ cells.every((cell) => delimiterCellOf.test(cell)) &&
87
+ cells.length === splitCells(head).length);
88
+ };
89
+ // 表の本体行は空行か別ブロックの始まりまで(| を含まない行も1セルの行として受ける)
90
+ const isBlockStart = (line) => headingOf.test(line) ||
91
+ bulletOf.test(line) ||
92
+ orderedOf.test(line) ||
93
+ quoteOf.test(line) ||
94
+ ruleOf.test(line.trim()) ||
95
+ line.trimStart().startsWith("```");
96
+ /** 行を読んでブロックに割る(描画から切り離してテストできるよう公開している) */
97
+ export const parseBlocks = (source) => {
98
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
99
+ const blocks = [];
100
+ let i = 0;
101
+ while (i < lines.length) {
102
+ const line = lines[i] ?? "";
103
+ if (line.trim() === "") {
104
+ i += 1;
105
+ continue;
106
+ }
107
+ // コードブロック(閉じないまま終わっても、そこまでをコードとして出す)
108
+ if (line.trimStart().startsWith("```")) {
109
+ const body = [];
110
+ i += 1;
111
+ while (i < lines.length &&
112
+ !(lines[i] ?? "").trimStart().startsWith("```")) {
113
+ body.push(lines[i] ?? "");
114
+ i += 1;
115
+ }
116
+ // 閉じの ``` を飛ばす(閉じないまま終端に着いたときは何も飛ばさない)
117
+ i += 1;
118
+ blocks.push({ kind: "code", lines: body });
119
+ continue;
120
+ }
121
+ if (isTableStart(lines, i)) {
122
+ const header = splitCells(line);
123
+ const align = splitCells(lines[i + 1] ?? "").map(alignOf);
124
+ const rows = [];
125
+ i += 2;
126
+ while (i < lines.length) {
127
+ const current = lines[i] ?? "";
128
+ if (current.trim() === "" || isBlockStart(current))
129
+ break;
130
+ const cells = splitCells(current);
131
+ // 列数は見出しに揃える(足りなければ空セル、余れば捨てる)
132
+ rows.push(header.map((_, col) => cells[col] ?? ""));
133
+ i += 1;
134
+ }
135
+ blocks.push({ kind: "table", align, header, rows });
136
+ continue;
137
+ }
138
+ if (ruleOf.test(line.trim())) {
139
+ blocks.push({ kind: "rule" });
140
+ i += 1;
141
+ continue;
142
+ }
143
+ const heading = headingOf.exec(line);
144
+ if (heading) {
145
+ blocks.push({
146
+ kind: "heading",
147
+ level: (heading[1] ?? "#").length,
148
+ text: heading[2] ?? "",
149
+ });
150
+ i += 1;
151
+ continue;
152
+ }
153
+ const quote = quoteOf.exec(line);
154
+ if (quote) {
155
+ const body = [];
156
+ while (i < lines.length) {
157
+ const next = quoteOf.exec(lines[i] ?? "");
158
+ if (!next)
159
+ break;
160
+ body.push(next[1] ?? "");
161
+ i += 1;
162
+ }
163
+ blocks.push({ kind: "quote", lines: body });
164
+ continue;
165
+ }
166
+ const bullet = bulletOf.exec(line);
167
+ const ordered = orderedOf.exec(line);
168
+ if (bullet || ordered) {
169
+ const isOrdered = ordered !== null && bullet === null;
170
+ const items = [];
171
+ while (i < lines.length) {
172
+ const current = lines[i] ?? "";
173
+ const next = isOrdered
174
+ ? orderedOf.exec(current)
175
+ : bulletOf.exec(current);
176
+ if (!next)
177
+ break;
178
+ items.push({
179
+ text: next[2] ?? "",
180
+ // ネストは1段だけ表現する(2段以上は同じ深さに畳む)
181
+ nested: (next[1] ?? "").length >= 2,
182
+ });
183
+ i += 1;
184
+ }
185
+ blocks.push({ kind: "list", ordered: isOrdered, items });
186
+ continue;
187
+ }
188
+ // 段落:空行 or 他のブロックの始まりまで
189
+ const body = [];
190
+ while (i < lines.length) {
191
+ const current = lines[i] ?? "";
192
+ if (current.trim() === "" ||
193
+ isBlockStart(current) ||
194
+ isTableStart(lines, i)) {
195
+ break;
196
+ }
197
+ body.push(current);
198
+ i += 1;
199
+ }
200
+ blocks.push({ kind: "para", lines: body });
201
+ }
202
+ return blocks;
203
+ };
204
+ const headingStyle = (level) => ({
205
+ margin: 0,
206
+ fontSize: level <= 1
207
+ ? "var(--text-lg)"
208
+ : level === 2
209
+ ? "var(--text-md)"
210
+ : "var(--text-base)",
211
+ fontWeight: "var(--weight-bold)",
212
+ lineHeight: "var(--leading-tight)",
213
+ ...(level <= 2
214
+ ? {
215
+ borderBottom: "var(--border-w) solid var(--border)",
216
+ paddingBottom: "var(--space-2)",
217
+ }
218
+ : { color: "var(--ink)" }),
219
+ });
220
+ const tableCell = {
221
+ padding: "var(--space-1) var(--space-3)",
222
+ borderBottom: "var(--border-w) solid var(--border)",
223
+ verticalAlign: "top",
224
+ overflowWrap: "anywhere",
225
+ };
226
+ const Blocks = ({ blocks }) => (_jsx(_Fragment, { children: blocks.map((block, i) => {
227
+ // ブロックは位置で安定(テキスト全体が置き換わる表示専用リスト)
228
+ const key = `${block.kind}${i}`;
229
+ switch (block.kind) {
230
+ case "heading": {
231
+ // 見出しレベルは Markdown の意味をそのまま持たせる(h1..h6)
232
+ const Tag = `h${Math.min(6, block.level)}`;
233
+ return (_jsx(Tag, { style: headingStyle(block.level), children: renderInline(block.text) }, key));
234
+ }
235
+ case "list": {
236
+ const Tag = block.ordered ? "ol" : "ul";
237
+ return (_jsx(Tag, { style: {
238
+ ...bodyText,
239
+ paddingLeft: "var(--space-5)",
240
+ display: "grid",
241
+ gap: "var(--space-1)",
242
+ }, children: block.items.map((item, j) => (_jsx("li", { style: item.nested ? { marginLeft: "var(--space-5)" } : {}, children: renderInline(item.text) }, j))) }, key));
243
+ }
244
+ case "quote":
245
+ return (_jsx("blockquote", { style: {
246
+ ...bodyText,
247
+ margin: 0,
248
+ paddingLeft: "var(--space-4)",
249
+ borderLeft: "var(--border-w-strong) solid var(--border-strong)",
250
+ color: "var(--muted)",
251
+ }, children: renderLines(block.lines) }, key));
252
+ case "code":
253
+ return (_jsx("pre", { style: {
254
+ margin: 0,
255
+ padding: "var(--space-3) var(--space-4)",
256
+ background: "var(--surface-2)",
257
+ border: "var(--border-w) solid var(--border)",
258
+ borderRadius: "var(--radius-s)",
259
+ fontSize: "var(--text-sm)",
260
+ lineHeight: 1.7,
261
+ overflowX: "auto",
262
+ }, children: block.lines.join("\n") }, key));
263
+ case "rule":
264
+ return (_jsx("hr", { style: {
265
+ margin: 0,
266
+ border: "none",
267
+ borderTop: "var(--border-w) solid var(--border)",
268
+ } }, key));
269
+ case "table":
270
+ return (_jsx("div", { style: { overflowX: "auto" }, children: _jsxs("table", { style: {
271
+ borderCollapse: "collapse",
272
+ fontSize: "var(--text-sm)",
273
+ lineHeight: 1.6,
274
+ minWidth: "100%",
275
+ }, children: [_jsx("thead", { children: _jsx("tr", { children: block.header.map((cell, col) => (_jsx("th", { style: {
276
+ ...tableCell,
277
+ textAlign: block.align[col] ?? "left",
278
+ fontWeight: "var(--weight-bold)",
279
+ color: "var(--muted)",
280
+ background: "var(--surface-2)",
281
+ whiteSpace: "nowrap",
282
+ }, children: renderInline(cell) }, col))) }) }), _jsx("tbody", { children: block.rows.map((row, r) => (_jsx("tr", { children: row.map((cell, col) => (_jsx("td", { style: {
283
+ ...tableCell,
284
+ textAlign: block.align[col] ?? "left",
285
+ }, children: renderInline(cell) }, col))) }, r))) })] }) }, key));
286
+ default:
287
+ return (_jsx("p", { style: bodyText, children: renderLines(block.lines) }, key));
288
+ }
289
+ }) }));
290
+ export const Markdown = ({ text }) => (_jsx("div", { style: { display: "grid", gap: "var(--space-4)" }, children: _jsx(Blocks, { blocks: parseBlocks(text) }) }));
291
+ // 一度縮めてから測らないと、削ったときに高さが戻らない
292
+ const fitToContent = (el) => {
293
+ if (!el)
294
+ return;
295
+ el.style.height = "auto";
296
+ el.style.height = `${el.scrollHeight}px`;
297
+ };
298
+ /**
299
+ * 内容の高さまで伸びる textarea(上限を超えたらその中でスクロール)。
300
+ * 返り値は ref にそのまま渡す。「編集」を押して後から現れる欄でも、
301
+ * 付いた瞬間に測るので初回から正しい高さで出る
302
+ */
303
+ export const useAutoGrow = (value) => {
304
+ const ref = useRef(null);
305
+ // biome-ignore lint/correctness/useExhaustiveDependencies: value は測り直しの引き金(本文では読まない)
306
+ useLayoutEffect(() => {
307
+ fitToContent(ref.current);
308
+ }, [value]);
309
+ return useCallback((el) => {
310
+ ref.current = el;
311
+ fitToContent(el);
312
+ }, []);
313
+ };
@@ -0,0 +1,19 @@
1
+ export type JsonSchema = {
2
+ type?: string;
3
+ title?: string;
4
+ description?: string;
5
+ properties?: Record<string, JsonSchema>;
6
+ required?: string[];
7
+ enum?: unknown[];
8
+ items?: JsonSchema;
9
+ "x-mawaru-type"?: string;
10
+ };
11
+ /** フォーム描画できる schema(properties を持つ object)なら型を付けて返す */
12
+ export declare const asFormSchema: (schema: unknown) => JsonSchema | null;
13
+ export declare const SchemaForm: ({ schema, value, readOnly, onChange, }: {
14
+ schema: JsonSchema;
15
+ value: Record<string, unknown>;
16
+ /** 値を直せない局面(担当外・全員承認・対応済み)では「編集」を出さない */
17
+ readOnly?: boolean;
18
+ onChange: (v: Record<string, unknown>) => void;
19
+ }) => import("react").JSX.Element;