@mawaru/sdk 0.39.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.
- package/_schemas/graph.test.ts +15 -9
- package/_schemas/graph.ts +20 -6
- package/_schemas/human-manifest.test.ts +129 -0
- package/_schemas/human-manifest.ts +158 -0
- package/_schemas/port-spec.test.ts +40 -40
- package/_schemas/port-spec.ts +10 -24
- package/dist/_cli/login.js +1 -15
- package/dist/_cli/openBrowser.d.ts +1 -0
- package/dist/_cli/openBrowser.js +15 -0
- package/dist/_cli/previewApi.d.ts +1 -1
- package/dist/_cli/previewCommand.d.ts +8 -0
- package/dist/_cli/previewCommand.js +95 -37
- package/dist/_cli/previewFiles.d.ts +2 -1
- package/dist/_cli/previewFiles.js +42 -19
- package/dist/_cli/previewSlack.d.ts +1 -1
- package/dist/_cli/previewSlack.js +1 -1
- package/dist/_schemas/graph.d.ts +21 -3
- package/dist/_schemas/graph.js +17 -6
- package/dist/_schemas/human-manifest.d.ts +27 -0
- package/dist/_schemas/human-manifest.js +121 -0
- package/dist/_schemas/port-spec.d.ts +6 -9
- package/dist/_schemas/port-spec.js +7 -7
- package/dist/cli.js +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init.js +22 -3
- package/dist/typegen.d.ts +3 -1
- package/dist/typegen.js +96 -14
- package/dist/validate.js +55 -0
- package/dist/views/_approval/ApprovalBar.d.ts +20 -0
- package/dist/views/_approval/ApprovalBar.js +74 -0
- package/dist/views/_approvalActions.d.ts +18 -0
- package/dist/views/_approvalActions.js +10 -0
- package/dist/views/_bridge.d.ts +12 -0
- package/dist/views/_bridge.js +10 -0
- package/dist/views/_parts/DocumentCard.d.ts +2 -0
- package/dist/views/_parts/DocumentCard.js +16 -0
- package/dist/views/_parts/FilePreview.d.ts +5 -0
- package/dist/views/_parts/FilePreview.js +101 -0
- package/dist/views/_parts/Linkify.d.ts +7 -0
- package/dist/views/_parts/Linkify.js +28 -0
- package/dist/views/_parts/Markdown.d.ts +44 -0
- package/dist/views/_parts/Markdown.js +313 -0
- package/dist/views/_parts/SchemaForm.d.ts +19 -0
- package/dist/views/_parts/SchemaForm.js +310 -0
- package/dist/views/_parts/fileRef.d.ts +7 -0
- package/dist/views/_parts/fileRef.js +11 -0
- package/dist/views/_parts/useNarrow.d.ts +5 -0
- package/dist/views/_parts/useNarrow.js +23 -0
- package/dist/views/host.d.ts +18 -0
- package/dist/views/host.js +20 -0
- package/dist/views/human.d.ts +73 -0
- package/dist/views/human.js +6 -0
- package/dist/views/index.d.ts +8 -0
- package/dist/views/index.js +12 -0
- package/dist/views/protocol.d.ts +34 -0
- package/dist/views/protocol.js +5 -0
- package/dist/views/runtime.d.ts +3 -0
- package/dist/views/runtime.js +86 -0
- package/dist/{slack-view.d.ts → views/slack.d.ts} +1 -1
- package/docs/development.md +40 -2
- package/index.ts +1 -0
- package/package.json +36 -4
- package/skills/create-loop/SKILL.md +6 -3
- package/views/_approval/ApprovalBar.tsx +239 -0
- package/views/_approvalActions.tsx +31 -0
- package/views/_bridge.tsx +36 -0
- package/views/_parts/DocumentCard.ts +18 -0
- package/views/_parts/FilePreview.tsx +180 -0
- package/views/_parts/Linkify.test.ts +40 -0
- package/views/_parts/Linkify.tsx +40 -0
- package/views/_parts/Markdown.test.ts +131 -0
- package/views/_parts/Markdown.tsx +472 -0
- package/views/_parts/SchemaForm.tsx +742 -0
- package/views/_parts/fileRef.ts +21 -0
- package/views/_parts/useNarrow.ts +24 -0
- package/views/host.ts +23 -0
- package/views/human.ts +91 -0
- package/views/index.ts +16 -0
- package/views/protocol.ts +40 -0
- package/views/runtime.tsx +132 -0
- package/views/slack.test.ts +40 -0
- package/{slack-view.ts → views/slack.ts} +3 -3
- /package/dist/{slack-view.js → views/slack.js} +0 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CSSProperties,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useCallback,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
} from "react"
|
|
8
|
+
import { linkify } from "./Linkify.js"
|
|
9
|
+
|
|
10
|
+
// AI の下書き・調査レポートは Markdown で届く。生ソースのまま読ませると読み物として
|
|
11
|
+
// 読めないので整形して出す。frontend は依存最小主義なのでレンダラは自前で持ち、
|
|
12
|
+
// 実際に出る記法だけを見る:見出し / 箇条書き・番号付き / 引用 / コードブロック /
|
|
13
|
+
// 水平線 / 表 / 段落と、インラインの強調・コード・リンク(素の URL を含む)。
|
|
14
|
+
// 対応しない記法は素のテキストとしてそのまま出す=壊さない
|
|
15
|
+
|
|
16
|
+
/** 長文(=Markdown として描く)か。入力欄の昇格判定と共通の基準 */
|
|
17
|
+
export const isLongText = (value: string): boolean =>
|
|
18
|
+
value.includes("\n") || value.length > 80
|
|
19
|
+
|
|
20
|
+
const bodyText: CSSProperties = {
|
|
21
|
+
fontSize: "var(--text-base)",
|
|
22
|
+
lineHeight: 1.9,
|
|
23
|
+
margin: 0,
|
|
24
|
+
overflowWrap: "anywhere",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const codeChip: CSSProperties = {
|
|
28
|
+
fontFamily: "var(--font-mono)",
|
|
29
|
+
fontSize: "0.9em",
|
|
30
|
+
background: "var(--surface-2)",
|
|
31
|
+
border: "var(--border-w) solid var(--border)",
|
|
32
|
+
borderRadius: "var(--radius-s)",
|
|
33
|
+
padding: "1px 5px",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// インライン:**太字** / `コード` / [リンク](url)。分割は捕捉グループ付きなので
|
|
37
|
+
// 区切り自身も配列に残る(split の仕様)
|
|
38
|
+
const inlineToken = /(`[^`\n]+`|\*\*[^*\n]+\*\*|\[[^\]\n]+\]\([^)\s]+\))/g
|
|
39
|
+
|
|
40
|
+
const renderInline = (text: string, prefix = ""): ReactNode[] =>
|
|
41
|
+
text.split(inlineToken).map((token, i) => {
|
|
42
|
+
if (!token) return null
|
|
43
|
+
const key = `${prefix}${i}`
|
|
44
|
+
if (token.startsWith("`") && token.endsWith("`") && token.length > 2) {
|
|
45
|
+
return (
|
|
46
|
+
<code key={key} style={codeChip}>
|
|
47
|
+
{token.slice(1, -1)}
|
|
48
|
+
</code>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
if (token.startsWith("**") && token.endsWith("**") && token.length > 4) {
|
|
52
|
+
return (
|
|
53
|
+
<strong
|
|
54
|
+
key={key}
|
|
55
|
+
style={{
|
|
56
|
+
fontWeight: "var(--weight-bold)" as CSSProperties["fontWeight"],
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
{token.slice(2, -2)}
|
|
60
|
+
</strong>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(token)
|
|
64
|
+
if (link) {
|
|
65
|
+
return (
|
|
66
|
+
<a key={key} href={link[2]} target="_blank" rel="noreferrer">
|
|
67
|
+
{link[1]}
|
|
68
|
+
</a>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
// 素の URL もリンクにする
|
|
72
|
+
return <span key={key}>{linkify(token, `${key}-`)}</span>
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// 段落内の改行は改行として残す(日本語の下書きは1行1文で書かれることが多い)
|
|
76
|
+
const renderLines = (lines: string[]): ReactNode[] =>
|
|
77
|
+
lines.flatMap((line, i) =>
|
|
78
|
+
i === 0
|
|
79
|
+
? renderInline(line, "0-")
|
|
80
|
+
: [
|
|
81
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: 表示専用(テキスト全体が置き換わる)
|
|
82
|
+
<br key={`br${i}`} />,
|
|
83
|
+
...renderInline(line, `${i}-`),
|
|
84
|
+
],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
type ListItem = { text: string; nested: boolean }
|
|
88
|
+
|
|
89
|
+
export type TableAlign = "left" | "center" | "right" | null
|
|
90
|
+
|
|
91
|
+
export type Block =
|
|
92
|
+
| { kind: "heading"; level: number; text: string }
|
|
93
|
+
| { kind: "list"; ordered: boolean; items: ListItem[] }
|
|
94
|
+
| { kind: "quote"; lines: string[] }
|
|
95
|
+
| { kind: "code"; lines: string[] }
|
|
96
|
+
| { kind: "rule" }
|
|
97
|
+
| { kind: "table"; align: TableAlign[]; header: string[]; rows: string[][] }
|
|
98
|
+
| { kind: "para"; lines: string[] }
|
|
99
|
+
|
|
100
|
+
const headingOf = /^(#{1,6})\s+(.*)$/
|
|
101
|
+
const bulletOf = /^(\s*)[-*+]\s+(.*)$/
|
|
102
|
+
const orderedOf = /^(\s*)\d+[.)]\s+(.*)$/
|
|
103
|
+
const quoteOf = /^>\s?(.*)$/
|
|
104
|
+
const ruleOf = /^(-{3,}|\*{3,}|_{3,})\s*$/
|
|
105
|
+
|
|
106
|
+
// 表(GFM):`| a | b |` の行の直後に区切り行(`|---|:-:|--:|`)があれば表。
|
|
107
|
+
// 外側の | は無くてもよい。セルの分割は `\|` でエスケープした | を除く
|
|
108
|
+
const splitCells = (line: string): string[] => {
|
|
109
|
+
let body = line.trim()
|
|
110
|
+
if (body.startsWith("|")) body = body.slice(1)
|
|
111
|
+
if (body.endsWith("|") && !body.endsWith("\\|")) body = body.slice(0, -1)
|
|
112
|
+
return body
|
|
113
|
+
.split(/(?<!\\)\|/)
|
|
114
|
+
.map((cell) => cell.trim().replace(/\\\|/g, "|"))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const delimiterCellOf = /^:?-+:?$/
|
|
118
|
+
|
|
119
|
+
const alignOf = (cell: string): TableAlign =>
|
|
120
|
+
cell.startsWith(":") && cell.endsWith(":")
|
|
121
|
+
? "center"
|
|
122
|
+
: cell.endsWith(":")
|
|
123
|
+
? "right"
|
|
124
|
+
: cell.startsWith(":")
|
|
125
|
+
? "left"
|
|
126
|
+
: null
|
|
127
|
+
|
|
128
|
+
const isTableStart = (lines: string[], i: number): boolean => {
|
|
129
|
+
const head = lines[i] ?? ""
|
|
130
|
+
const delimiter = lines[i + 1] ?? ""
|
|
131
|
+
if (!head.includes("|") || !delimiter.includes("-")) return false
|
|
132
|
+
const cells = splitCells(delimiter)
|
|
133
|
+
return (
|
|
134
|
+
cells.length > 0 &&
|
|
135
|
+
cells.every((cell) => delimiterCellOf.test(cell)) &&
|
|
136
|
+
cells.length === splitCells(head).length
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 表の本体行は空行か別ブロックの始まりまで(| を含まない行も1セルの行として受ける)
|
|
141
|
+
const isBlockStart = (line: string): boolean =>
|
|
142
|
+
headingOf.test(line) ||
|
|
143
|
+
bulletOf.test(line) ||
|
|
144
|
+
orderedOf.test(line) ||
|
|
145
|
+
quoteOf.test(line) ||
|
|
146
|
+
ruleOf.test(line.trim()) ||
|
|
147
|
+
line.trimStart().startsWith("```")
|
|
148
|
+
|
|
149
|
+
/** 行を読んでブロックに割る(描画から切り離してテストできるよう公開している) */
|
|
150
|
+
export const parseBlocks = (source: string): Block[] => {
|
|
151
|
+
const lines = source.replace(/\r\n?/g, "\n").split("\n")
|
|
152
|
+
const blocks: Block[] = []
|
|
153
|
+
let i = 0
|
|
154
|
+
|
|
155
|
+
while (i < lines.length) {
|
|
156
|
+
const line = lines[i] ?? ""
|
|
157
|
+
|
|
158
|
+
if (line.trim() === "") {
|
|
159
|
+
i += 1
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// コードブロック(閉じないまま終わっても、そこまでをコードとして出す)
|
|
164
|
+
if (line.trimStart().startsWith("```")) {
|
|
165
|
+
const body: string[] = []
|
|
166
|
+
i += 1
|
|
167
|
+
while (
|
|
168
|
+
i < lines.length &&
|
|
169
|
+
!(lines[i] ?? "").trimStart().startsWith("```")
|
|
170
|
+
) {
|
|
171
|
+
body.push(lines[i] ?? "")
|
|
172
|
+
i += 1
|
|
173
|
+
}
|
|
174
|
+
// 閉じの ``` を飛ばす(閉じないまま終端に着いたときは何も飛ばさない)
|
|
175
|
+
i += 1
|
|
176
|
+
blocks.push({ kind: "code", lines: body })
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (isTableStart(lines, i)) {
|
|
181
|
+
const header = splitCells(line)
|
|
182
|
+
const align = splitCells(lines[i + 1] ?? "").map(alignOf)
|
|
183
|
+
const rows: string[][] = []
|
|
184
|
+
i += 2
|
|
185
|
+
while (i < lines.length) {
|
|
186
|
+
const current = lines[i] ?? ""
|
|
187
|
+
if (current.trim() === "" || isBlockStart(current)) break
|
|
188
|
+
const cells = splitCells(current)
|
|
189
|
+
// 列数は見出しに揃える(足りなければ空セル、余れば捨てる)
|
|
190
|
+
rows.push(header.map((_, col) => cells[col] ?? ""))
|
|
191
|
+
i += 1
|
|
192
|
+
}
|
|
193
|
+
blocks.push({ kind: "table", align, header, rows })
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (ruleOf.test(line.trim())) {
|
|
198
|
+
blocks.push({ kind: "rule" })
|
|
199
|
+
i += 1
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const heading = headingOf.exec(line)
|
|
204
|
+
if (heading) {
|
|
205
|
+
blocks.push({
|
|
206
|
+
kind: "heading",
|
|
207
|
+
level: (heading[1] ?? "#").length,
|
|
208
|
+
text: heading[2] ?? "",
|
|
209
|
+
})
|
|
210
|
+
i += 1
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const quote = quoteOf.exec(line)
|
|
215
|
+
if (quote) {
|
|
216
|
+
const body: string[] = []
|
|
217
|
+
while (i < lines.length) {
|
|
218
|
+
const next = quoteOf.exec(lines[i] ?? "")
|
|
219
|
+
if (!next) break
|
|
220
|
+
body.push(next[1] ?? "")
|
|
221
|
+
i += 1
|
|
222
|
+
}
|
|
223
|
+
blocks.push({ kind: "quote", lines: body })
|
|
224
|
+
continue
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const bullet = bulletOf.exec(line)
|
|
228
|
+
const ordered = orderedOf.exec(line)
|
|
229
|
+
if (bullet || ordered) {
|
|
230
|
+
const isOrdered = ordered !== null && bullet === null
|
|
231
|
+
const items: ListItem[] = []
|
|
232
|
+
while (i < lines.length) {
|
|
233
|
+
const current = lines[i] ?? ""
|
|
234
|
+
const next = isOrdered
|
|
235
|
+
? orderedOf.exec(current)
|
|
236
|
+
: bulletOf.exec(current)
|
|
237
|
+
if (!next) break
|
|
238
|
+
items.push({
|
|
239
|
+
text: next[2] ?? "",
|
|
240
|
+
// ネストは1段だけ表現する(2段以上は同じ深さに畳む)
|
|
241
|
+
nested: (next[1] ?? "").length >= 2,
|
|
242
|
+
})
|
|
243
|
+
i += 1
|
|
244
|
+
}
|
|
245
|
+
blocks.push({ kind: "list", ordered: isOrdered, items })
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 段落:空行 or 他のブロックの始まりまで
|
|
250
|
+
const body: string[] = []
|
|
251
|
+
while (i < lines.length) {
|
|
252
|
+
const current = lines[i] ?? ""
|
|
253
|
+
if (
|
|
254
|
+
current.trim() === "" ||
|
|
255
|
+
isBlockStart(current) ||
|
|
256
|
+
isTableStart(lines, i)
|
|
257
|
+
) {
|
|
258
|
+
break
|
|
259
|
+
}
|
|
260
|
+
body.push(current)
|
|
261
|
+
i += 1
|
|
262
|
+
}
|
|
263
|
+
blocks.push({ kind: "para", lines: body })
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return blocks
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const headingStyle = (level: number): CSSProperties => ({
|
|
270
|
+
margin: 0,
|
|
271
|
+
fontSize:
|
|
272
|
+
level <= 1
|
|
273
|
+
? "var(--text-lg)"
|
|
274
|
+
: level === 2
|
|
275
|
+
? "var(--text-md)"
|
|
276
|
+
: "var(--text-base)",
|
|
277
|
+
fontWeight: "var(--weight-bold)" as CSSProperties["fontWeight"],
|
|
278
|
+
lineHeight: "var(--leading-tight)",
|
|
279
|
+
...(level <= 2
|
|
280
|
+
? {
|
|
281
|
+
borderBottom: "var(--border-w) solid var(--border)",
|
|
282
|
+
paddingBottom: "var(--space-2)",
|
|
283
|
+
}
|
|
284
|
+
: { color: "var(--ink)" }),
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
const tableCell: CSSProperties = {
|
|
288
|
+
padding: "var(--space-1) var(--space-3)",
|
|
289
|
+
borderBottom: "var(--border-w) solid var(--border)",
|
|
290
|
+
verticalAlign: "top",
|
|
291
|
+
overflowWrap: "anywhere",
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const Blocks = ({ blocks }: { blocks: Block[] }) => (
|
|
295
|
+
<>
|
|
296
|
+
{blocks.map((block, i) => {
|
|
297
|
+
// ブロックは位置で安定(テキスト全体が置き換わる表示専用リスト)
|
|
298
|
+
const key = `${block.kind}${i}`
|
|
299
|
+
switch (block.kind) {
|
|
300
|
+
case "heading": {
|
|
301
|
+
// 見出しレベルは Markdown の意味をそのまま持たせる(h1..h6)
|
|
302
|
+
const Tag = `h${Math.min(6, block.level)}` as "h1"
|
|
303
|
+
return (
|
|
304
|
+
<Tag key={key} style={headingStyle(block.level)}>
|
|
305
|
+
{renderInline(block.text)}
|
|
306
|
+
</Tag>
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
case "list": {
|
|
310
|
+
const Tag = block.ordered ? "ol" : "ul"
|
|
311
|
+
return (
|
|
312
|
+
<Tag
|
|
313
|
+
key={key}
|
|
314
|
+
style={{
|
|
315
|
+
...bodyText,
|
|
316
|
+
paddingLeft: "var(--space-5)",
|
|
317
|
+
display: "grid",
|
|
318
|
+
gap: "var(--space-1)",
|
|
319
|
+
}}
|
|
320
|
+
>
|
|
321
|
+
{block.items.map((item, j) => (
|
|
322
|
+
<li
|
|
323
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: 表示専用
|
|
324
|
+
key={j}
|
|
325
|
+
style={item.nested ? { marginLeft: "var(--space-5)" } : {}}
|
|
326
|
+
>
|
|
327
|
+
{renderInline(item.text)}
|
|
328
|
+
</li>
|
|
329
|
+
))}
|
|
330
|
+
</Tag>
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
case "quote":
|
|
334
|
+
return (
|
|
335
|
+
<blockquote
|
|
336
|
+
key={key}
|
|
337
|
+
style={{
|
|
338
|
+
...bodyText,
|
|
339
|
+
margin: 0,
|
|
340
|
+
paddingLeft: "var(--space-4)",
|
|
341
|
+
borderLeft: "var(--border-w-strong) solid var(--border-strong)",
|
|
342
|
+
color: "var(--muted)",
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
{renderLines(block.lines)}
|
|
346
|
+
</blockquote>
|
|
347
|
+
)
|
|
348
|
+
case "code":
|
|
349
|
+
return (
|
|
350
|
+
<pre
|
|
351
|
+
key={key}
|
|
352
|
+
style={{
|
|
353
|
+
margin: 0,
|
|
354
|
+
padding: "var(--space-3) var(--space-4)",
|
|
355
|
+
background: "var(--surface-2)",
|
|
356
|
+
border: "var(--border-w) solid var(--border)",
|
|
357
|
+
borderRadius: "var(--radius-s)",
|
|
358
|
+
fontSize: "var(--text-sm)",
|
|
359
|
+
lineHeight: 1.7,
|
|
360
|
+
overflowX: "auto",
|
|
361
|
+
}}
|
|
362
|
+
>
|
|
363
|
+
{block.lines.join("\n")}
|
|
364
|
+
</pre>
|
|
365
|
+
)
|
|
366
|
+
case "rule":
|
|
367
|
+
return (
|
|
368
|
+
<hr
|
|
369
|
+
key={key}
|
|
370
|
+
style={{
|
|
371
|
+
margin: 0,
|
|
372
|
+
border: "none",
|
|
373
|
+
borderTop: "var(--border-w) solid var(--border)",
|
|
374
|
+
}}
|
|
375
|
+
/>
|
|
376
|
+
)
|
|
377
|
+
case "table":
|
|
378
|
+
return (
|
|
379
|
+
<div key={key} style={{ overflowX: "auto" }}>
|
|
380
|
+
<table
|
|
381
|
+
style={{
|
|
382
|
+
borderCollapse: "collapse",
|
|
383
|
+
fontSize: "var(--text-sm)",
|
|
384
|
+
lineHeight: 1.6,
|
|
385
|
+
minWidth: "100%",
|
|
386
|
+
}}
|
|
387
|
+
>
|
|
388
|
+
<thead>
|
|
389
|
+
<tr>
|
|
390
|
+
{block.header.map((cell, col) => (
|
|
391
|
+
<th
|
|
392
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: 表示専用
|
|
393
|
+
key={col}
|
|
394
|
+
style={{
|
|
395
|
+
...tableCell,
|
|
396
|
+
textAlign: block.align[col] ?? "left",
|
|
397
|
+
fontWeight:
|
|
398
|
+
"var(--weight-bold)" as CSSProperties["fontWeight"],
|
|
399
|
+
color: "var(--muted)",
|
|
400
|
+
background: "var(--surface-2)",
|
|
401
|
+
whiteSpace: "nowrap",
|
|
402
|
+
}}
|
|
403
|
+
>
|
|
404
|
+
{renderInline(cell)}
|
|
405
|
+
</th>
|
|
406
|
+
))}
|
|
407
|
+
</tr>
|
|
408
|
+
</thead>
|
|
409
|
+
<tbody>
|
|
410
|
+
{block.rows.map((row, r) => (
|
|
411
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: 表示専用
|
|
412
|
+
<tr key={r}>
|
|
413
|
+
{row.map((cell, col) => (
|
|
414
|
+
<td
|
|
415
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: 表示専用
|
|
416
|
+
key={col}
|
|
417
|
+
style={{
|
|
418
|
+
...tableCell,
|
|
419
|
+
textAlign: block.align[col] ?? "left",
|
|
420
|
+
}}
|
|
421
|
+
>
|
|
422
|
+
{renderInline(cell)}
|
|
423
|
+
</td>
|
|
424
|
+
))}
|
|
425
|
+
</tr>
|
|
426
|
+
))}
|
|
427
|
+
</tbody>
|
|
428
|
+
</table>
|
|
429
|
+
</div>
|
|
430
|
+
)
|
|
431
|
+
default:
|
|
432
|
+
return (
|
|
433
|
+
<p key={key} style={bodyText}>
|
|
434
|
+
{renderLines(block.lines)}
|
|
435
|
+
</p>
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
})}
|
|
439
|
+
</>
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
export const Markdown = ({ text }: { text: string }) => (
|
|
443
|
+
<div style={{ display: "grid", gap: "var(--space-4)" }}>
|
|
444
|
+
<Blocks blocks={parseBlocks(text)} />
|
|
445
|
+
</div>
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
// 一度縮めてから測らないと、削ったときに高さが戻らない
|
|
449
|
+
const fitToContent = (el: HTMLTextAreaElement | null) => {
|
|
450
|
+
if (!el) return
|
|
451
|
+
el.style.height = "auto"
|
|
452
|
+
el.style.height = `${el.scrollHeight}px`
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 内容の高さまで伸びる textarea(上限を超えたらその中でスクロール)。
|
|
457
|
+
* 返り値は ref にそのまま渡す。「編集」を押して後から現れる欄でも、
|
|
458
|
+
* 付いた瞬間に測るので初回から正しい高さで出る
|
|
459
|
+
*/
|
|
460
|
+
export const useAutoGrow = (
|
|
461
|
+
value: string,
|
|
462
|
+
): ((el: HTMLTextAreaElement | null) => void) => {
|
|
463
|
+
const ref = useRef<HTMLTextAreaElement | null>(null)
|
|
464
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: value は測り直しの引き金(本文では読まない)
|
|
465
|
+
useLayoutEffect(() => {
|
|
466
|
+
fitToContent(ref.current)
|
|
467
|
+
}, [value])
|
|
468
|
+
return useCallback((el: HTMLTextAreaElement | null) => {
|
|
469
|
+
ref.current = el
|
|
470
|
+
fitToContent(el)
|
|
471
|
+
}, [])
|
|
472
|
+
}
|