@neurocode-ai/session-ui 1.18.8

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 (114) hide show
  1. package/package.json +71 -0
  2. package/src/components/apply-patch-file.test.ts +43 -0
  3. package/src/components/apply-patch-file.ts +78 -0
  4. package/src/components/basic-tool.css +358 -0
  5. package/src/components/basic-tool.stories.tsx +133 -0
  6. package/src/components/basic-tool.tsx +343 -0
  7. package/src/components/dock-prompt.stories.tsx +62 -0
  8. package/src/components/dock-prompt.tsx +23 -0
  9. package/src/components/file-media.tsx +291 -0
  10. package/src/components/file-search.tsx +72 -0
  11. package/src/components/file-ssr.tsx +197 -0
  12. package/src/components/file.css +46 -0
  13. package/src/components/file.tsx +1202 -0
  14. package/src/components/line-comment-annotations.tsx +637 -0
  15. package/src/components/line-comment-styles.ts +292 -0
  16. package/src/components/line-comment.stories.tsx +115 -0
  17. package/src/components/line-comment.tsx +439 -0
  18. package/src/components/markdown-cache.tsx +78 -0
  19. package/src/components/markdown-code-state.test.ts +32 -0
  20. package/src/components/markdown-code-state.ts +22 -0
  21. package/src/components/markdown-inline-code-kind.test.ts +46 -0
  22. package/src/components/markdown-inline-code-kind.ts +1915 -0
  23. package/src/components/markdown-preload.test.ts +18 -0
  24. package/src/components/markdown-shiki.worker.ts +104 -0
  25. package/src/components/markdown-stream.test.ts +194 -0
  26. package/src/components/markdown-stream.ts +110 -0
  27. package/src/components/markdown-worker-protocol.test.ts +81 -0
  28. package/src/components/markdown-worker-protocol.ts +48 -0
  29. package/src/components/markdown-worker-queue.test.ts +49 -0
  30. package/src/components/markdown-worker-queue.ts +64 -0
  31. package/src/components/markdown-worker-transport.test.ts +56 -0
  32. package/src/components/markdown-worker-transport.ts +41 -0
  33. package/src/components/markdown-worker.ts +122 -0
  34. package/src/components/markdown.css +377 -0
  35. package/src/components/markdown.stories.tsx +53 -0
  36. package/src/components/markdown.tsx +678 -0
  37. package/src/components/message-file.test.ts +63 -0
  38. package/src/components/message-file.ts +34 -0
  39. package/src/components/message-nav.css +127 -0
  40. package/src/components/message-nav.stories.tsx +7 -0
  41. package/src/components/message-nav.tsx +102 -0
  42. package/src/components/message-part-text.ts +3 -0
  43. package/src/components/message-part.css +1569 -0
  44. package/src/components/message-part.stories.tsx +7 -0
  45. package/src/components/message-part.test.ts +28 -0
  46. package/src/components/message-part.tsx +2662 -0
  47. package/src/components/session-diff.test.ts +135 -0
  48. package/src/components/session-diff.ts +145 -0
  49. package/src/components/session-retry.tsx +74 -0
  50. package/src/components/session-review.css +247 -0
  51. package/src/components/session-review.stories.tsx +7 -0
  52. package/src/components/session-review.tsx +657 -0
  53. package/src/components/session-turn.css +241 -0
  54. package/src/components/session-turn.stories.tsx +7 -0
  55. package/src/components/session-turn.tsx +543 -0
  56. package/src/components/shell-submessage-motion.stories.tsx +346 -0
  57. package/src/components/shell-submessage.css +23 -0
  58. package/src/components/timeline-playground.stories.tsx +2090 -0
  59. package/src/components/tool-count-label.css +57 -0
  60. package/src/components/tool-count-label.tsx +58 -0
  61. package/src/components/tool-count-summary.css +102 -0
  62. package/src/components/tool-count-summary.stories.tsx +238 -0
  63. package/src/components/tool-count-summary.tsx +52 -0
  64. package/src/components/tool-error-card.css +91 -0
  65. package/src/components/tool-error-card.stories.tsx +92 -0
  66. package/src/components/tool-error-card.tsx +157 -0
  67. package/src/components/tool-status-title.css +89 -0
  68. package/src/components/tool-status-title.tsx +133 -0
  69. package/src/context/data.tsx +64 -0
  70. package/src/context/index.ts +1 -0
  71. package/src/pierre/comment-hover.ts +83 -0
  72. package/src/pierre/commented-lines.ts +91 -0
  73. package/src/pierre/diff-selection.ts +71 -0
  74. package/src/pierre/file-find.ts +485 -0
  75. package/src/pierre/file-runtime.ts +114 -0
  76. package/src/pierre/file-selection.ts +85 -0
  77. package/src/pierre/index.ts +186 -0
  78. package/src/pierre/media.ts +110 -0
  79. package/src/pierre/selection-bridge.ts +132 -0
  80. package/src/pierre/virtualizer.ts +100 -0
  81. package/src/pierre/worker.ts +52 -0
  82. package/src/styles/index.css +14 -0
  83. package/src/v2/components/attachment-card-v2.css +70 -0
  84. package/src/v2/components/attachment-card-v2.tsx +33 -0
  85. package/src/v2/components/basic-tool-v2.css +163 -0
  86. package/src/v2/components/basic-tool-v2.stories.tsx +137 -0
  87. package/src/v2/components/basic-tool-v2.tsx +139 -0
  88. package/src/v2/components/comment-card-v2.tsx +64 -0
  89. package/src/v2/components/line-comment-annotations-v2.tsx +220 -0
  90. package/src/v2/components/prompt-input/attachments.ts +266 -0
  91. package/src/v2/components/prompt-input/index.tsx +706 -0
  92. package/src/v2/components/prompt-input/interaction.ts +482 -0
  93. package/src/v2/components/prompt-input/machine.test.ts +164 -0
  94. package/src/v2/components/prompt-input/machine.ts +261 -0
  95. package/src/v2/components/prompt-input/prompt-input.stories.tsx +221 -0
  96. package/src/v2/components/prompt-input/store.test.ts +116 -0
  97. package/src/v2/components/prompt-input/store.ts +152 -0
  98. package/src/v2/components/prompt-input/types.ts +106 -0
  99. package/src/v2/components/session-file-panel-v2.tsx +43 -0
  100. package/src/v2/components/session-progress-indicator-v2.css +875 -0
  101. package/src/v2/components/session-progress-indicator-v2.stories.tsx +66 -0
  102. package/src/v2/components/session-progress-indicator-v2.tsx +32 -0
  103. package/src/v2/components/session-review-empty-changes-v2.tsx +17 -0
  104. package/src/v2/components/session-review-empty-no-git-v2.tsx +28 -0
  105. package/src/v2/components/session-review-file-preview-v2-virtualize.test.ts +13 -0
  106. package/src/v2/components/session-review-file-preview-v2-virtualize.ts +5 -0
  107. package/src/v2/components/session-review-file-preview-v2.tsx +290 -0
  108. package/src/v2/components/session-review-v2.css +458 -0
  109. package/src/v2/components/session-review-v2.tsx +340 -0
  110. package/src/v2/components/tool-error-card-v2.css +200 -0
  111. package/src/v2/components/tool-error-card-v2.stories.tsx +91 -0
  112. package/src/v2/components/tool-error-card-v2.tsx +166 -0
  113. package/sst-env.d.ts +10 -0
  114. package/tsconfig.json +19 -0
@@ -0,0 +1,56 @@
1
+ import { expect, test } from "bun:test"
2
+ import { createWorkerTransport } from "./markdown-worker-transport"
3
+
4
+ test("posts one request and retains only the latest queued snapshot per key", () => {
5
+ const posted: number[] = []
6
+ const superseded: number[] = []
7
+ const transport = createWorkerTransport<{ id: number; key: string }>({
8
+ post: (request) => posted.push(request.id),
9
+ supersede: (request) => superseded.push(request.id),
10
+ })
11
+
12
+ transport.send({ id: 1, key: "code" })
13
+ transport.send({ id: 2, key: "code" })
14
+ transport.send({ id: 3, key: "code" })
15
+
16
+ expect(posted).toEqual([1])
17
+ expect(superseded).toEqual([2])
18
+ expect(transport.queued()).toBe(1)
19
+ transport.complete("code", 1)
20
+ expect(posted).toEqual([1, 3])
21
+ expect(transport.queued()).toBe(0)
22
+ })
23
+
24
+ test("ignores a disposed request response after the key is reused", () => {
25
+ const posted: number[] = []
26
+ const transport = createWorkerTransport<{ id: number; key: string }>({
27
+ post: (request) => posted.push(request.id),
28
+ supersede: () => {},
29
+ })
30
+
31
+ transport.send({ id: 1, key: "code" })
32
+ transport.dispose("code")
33
+ transport.send({ id: 2, key: "code" })
34
+ transport.send({ id: 3, key: "code" })
35
+ transport.complete("code", 1)
36
+
37
+ expect(posted).toEqual([1, 2])
38
+ expect(transport.queued()).toBe(1)
39
+ transport.complete("code", 2)
40
+ expect(posted).toEqual([1, 2, 3])
41
+ })
42
+
43
+ test("drops queued snapshots when a key is disposed", () => {
44
+ const superseded: number[] = []
45
+ const transport = createWorkerTransport<{ id: number; key: string }>({
46
+ post: () => {},
47
+ supersede: (request) => superseded.push(request.id),
48
+ })
49
+
50
+ transport.send({ id: 1, key: "code" })
51
+ transport.send({ id: 2, key: "code" })
52
+ transport.dispose("code")
53
+
54
+ expect(superseded).toEqual([2])
55
+ expect(transport.queued()).toBe(0)
56
+ })
@@ -0,0 +1,41 @@
1
+ export function createWorkerTransport<T extends { id: number; key: string }>(input: {
2
+ post: (request: T) => void
3
+ supersede: (request: T) => void
4
+ }) {
5
+ const active = new Map<string, T>()
6
+ const queued = new Map<string, T>()
7
+
8
+ return {
9
+ send(request: T) {
10
+ if (!active.has(request.key)) {
11
+ active.set(request.key, request)
12
+ input.post(request)
13
+ return
14
+ }
15
+ const previous = queued.get(request.key)
16
+ if (previous) input.supersede(previous)
17
+ queued.set(request.key, request)
18
+ },
19
+ complete(key: string, id: number) {
20
+ if (active.get(key)?.id !== id) return
21
+ active.delete(key)
22
+ const next = queued.get(key)
23
+ if (!next) return
24
+ queued.delete(key)
25
+ active.set(key, next)
26
+ input.post(next)
27
+ },
28
+ dispose(key: string) {
29
+ active.delete(key)
30
+ const request = queued.get(key)
31
+ if (request) input.supersede(request)
32
+ queued.delete(key)
33
+ },
34
+ reset() {
35
+ queued.forEach(input.supersede)
36
+ queued.clear()
37
+ active.clear()
38
+ },
39
+ queued: () => queued.size,
40
+ }
41
+ }
@@ -0,0 +1,122 @@
1
+ import MarkdownShikiWorkerUrl from "./markdown-shiki.worker.ts?worker&url"
2
+ import { OpenCodeTheme } from "@neurocode-ai/ui/context/marked"
3
+ import {
4
+ applyMarkdownWorkerResponse,
5
+ shouldReleaseMarkdownWorkerState,
6
+ type MarkdownWorkerRequest,
7
+ type MarkdownWorkerResponse,
8
+ type MarkdownWorkerState,
9
+ } from "./markdown-worker-protocol"
10
+ import { createWorkerTransport } from "./markdown-worker-transport"
11
+
12
+ type Pending = {
13
+ key: string
14
+ complete: boolean
15
+ resolve: (state: MarkdownWorkerState) => void
16
+ reject: (error: Error) => void
17
+ }
18
+
19
+ let worker: Worker | undefined
20
+ let disabled: Error | undefined
21
+ let nextID = 0
22
+ const pending = new Map<number, Pending>()
23
+ const states = new Map<string, MarkdownWorkerState>()
24
+ const keys = new Set<string>()
25
+ const latest = new Map<string, number>()
26
+ const transport = createWorkerTransport<Extract<MarkdownWorkerRequest, { type: "highlight" }>>({
27
+ post: (request) => worker!.postMessage(request),
28
+ supersede: (request) => {
29
+ const result = pending.get(request.id)
30
+ if (!result) return
31
+ pending.delete(request.id)
32
+ result.reject(new MarkdownWorkerSupersededError())
33
+ },
34
+ })
35
+
36
+ export function highlightStreamingCode(key: string, text: string, language: string, complete = false) {
37
+ const instance = getWorker()
38
+ const id = ++nextID
39
+ latest.set(key, id)
40
+ keys.delete(key)
41
+ keys.add(key)
42
+ if (keys.size > 200) disposeStreamingCode(keys.values().next().value!)
43
+ return new Promise<MarkdownWorkerState>((resolve, reject) => {
44
+ pending.set(id, { key, complete, resolve, reject })
45
+ transport.send({ type: "highlight", id, key, text, language, complete })
46
+ })
47
+ }
48
+
49
+ export function disposeStreamingCode(key: string) {
50
+ keys.delete(key)
51
+ latest.delete(key)
52
+ states.delete(key)
53
+ transport.dispose(key)
54
+ pending.forEach((request, id) => {
55
+ if (request.key !== key) return
56
+ pending.delete(id)
57
+ request.reject(new MarkdownWorkerDisposedError())
58
+ })
59
+ worker?.postMessage({ type: "dispose", key } satisfies MarkdownWorkerRequest)
60
+ }
61
+
62
+ export class MarkdownWorkerDisposedError extends Error {}
63
+ export class MarkdownWorkerSupersededError extends Error {}
64
+ export class MarkdownWorkerUnavailableError extends Error {}
65
+
66
+ function getWorker() {
67
+ if (worker) return worker
68
+ if (disabled) throw new MarkdownWorkerUnavailableError(disabled.message)
69
+ try {
70
+ worker = new Worker(MarkdownShikiWorkerUrl, { type: "module" })
71
+ } catch (error) {
72
+ disabled = error instanceof Error ? error : new Error(String(error))
73
+ throw new MarkdownWorkerUnavailableError(disabled.message)
74
+ }
75
+ worker.onmessage = (event: MessageEvent<MarkdownWorkerResponse>) => {
76
+ const result = pending.get(event.data.id)
77
+ if (!result) {
78
+ transport.complete(event.data.key, event.data.id)
79
+ return
80
+ }
81
+ pending.delete(event.data.id)
82
+ if (!keys.has(event.data.key)) {
83
+ result.reject(new MarkdownWorkerDisposedError())
84
+ transport.complete(event.data.key, event.data.id)
85
+ return
86
+ }
87
+ if (event.data.type === "superseded") {
88
+ result.reject(new MarkdownWorkerSupersededError())
89
+ transport.complete(event.data.key, event.data.id)
90
+ return
91
+ }
92
+ if (event.data.type === "error") {
93
+ result.reject(new Error(event.data.message))
94
+ transport.complete(event.data.key, event.data.id)
95
+ return
96
+ }
97
+ const state = applyMarkdownWorkerResponse(states.get(event.data.key), event.data)
98
+ if (shouldReleaseMarkdownWorkerState(result.complete, latest.get(event.data.key), event.data.id)) {
99
+ states.delete(event.data.key)
100
+ keys.delete(event.data.key)
101
+ latest.delete(event.data.key)
102
+ } else states.set(event.data.key, state)
103
+ result.resolve(state)
104
+ transport.complete(event.data.key, event.data.id)
105
+ }
106
+ const fail = (message: string) => {
107
+ const error = new Error(message)
108
+ disabled = error
109
+ transport.reset()
110
+ pending.forEach((request) => request.reject(error))
111
+ pending.clear()
112
+ states.clear()
113
+ keys.clear()
114
+ latest.clear()
115
+ worker?.terminate()
116
+ worker = undefined
117
+ }
118
+ worker.onerror = (event) => fail(event.message || "Markdown highlighting worker failed")
119
+ worker.onmessageerror = () => fail("Markdown worker response failed")
120
+ worker.postMessage({ type: "init", theme: OpenCodeTheme } satisfies MarkdownWorkerRequest)
121
+ return worker
122
+ }
@@ -0,0 +1,377 @@
1
+ [data-component="markdown"] {
2
+ --markdown-inline-code-path-color: var(--v2-text-text-code-accent);
3
+ --markdown-inline-code-color: var(--v2-text-text-code-accent);
4
+ --markdown-inline-code-bg-mix: 8%;
5
+ --markdown-shell-code-background: var(--v2-background-bg-layer-02);
6
+ --markdown-shell-code-border: var(--v2-border-border-muted);
7
+ --markdown-shell-code-color: var(--v2-text-text-base);
8
+ --markdown-shell-code-token-color: currentColor;
9
+
10
+ /* Reset & Base Typography */
11
+ min-width: 0;
12
+ max-width: 100%;
13
+ overflow-wrap: break-word;
14
+ color: var(--v2-text-text-base);
15
+ font-family: var(--font-family-sans);
16
+ font-size: var(--font-size-base); /* 14px */
17
+ line-height: 160%;
18
+
19
+ /* Spacing for flow */
20
+ > *:first-child {
21
+ margin-top: 0;
22
+ }
23
+ > *:last-child {
24
+ margin-bottom: 0;
25
+ }
26
+ > [data-markdown-block]:first-child > *:first-child {
27
+ margin-top: 0;
28
+ }
29
+ > [data-markdown-block]:last-child > *:last-child {
30
+ margin-bottom: 0;
31
+ }
32
+
33
+ /* Headings: Sized by level, distinguished by color and spacing */
34
+ h1 {
35
+ font-size: 17px;
36
+ color: var(--v2-text-text-base);
37
+ font-weight: 600;
38
+ margin-top: 28px;
39
+ margin-bottom: 12px;
40
+ line-height: var(--line-height-large);
41
+ }
42
+
43
+ h2 {
44
+ font-size: 15px;
45
+ color: var(--v2-text-text-base);
46
+ font-weight: 600;
47
+ margin-top: 24px;
48
+ margin-bottom: 10px;
49
+ line-height: var(--line-height-large);
50
+ }
51
+
52
+ h3 {
53
+ font-size: 13px;
54
+ color: var(--v2-text-text-base);
55
+ font-weight: var(--font-weight-medium);
56
+ margin-top: 20px;
57
+ margin-bottom: 8px;
58
+ line-height: var(--line-height-large);
59
+ }
60
+
61
+ h4,
62
+ h5,
63
+ h6 {
64
+ font-size: 13px;
65
+ color: var(--v2-text-text-muted);
66
+ font-weight: var(--font-weight-medium);
67
+ margin-top: 20px;
68
+ margin-bottom: 8px;
69
+ line-height: var(--line-height-large);
70
+ }
71
+
72
+ /* Emphasis & Strong: Neutral strong color */
73
+ strong,
74
+ b {
75
+ color: var(--v2-text-text-base);
76
+ font-weight: var(--font-weight-medium);
77
+ }
78
+
79
+ /* Paragraphs */
80
+ p {
81
+ margin-bottom: 12px;
82
+ }
83
+
84
+ /* Links */
85
+ a {
86
+ color: var(--text-interactive-base);
87
+ text-decoration: none;
88
+ font-weight: inherit;
89
+ }
90
+
91
+ a:hover {
92
+ text-decoration: underline;
93
+ text-underline-offset: 2px;
94
+ }
95
+
96
+ /* Lists */
97
+ ul,
98
+ ol {
99
+ margin-top: 8px;
100
+ margin-bottom: 12px;
101
+ margin-left: 0;
102
+ padding-left: 32px;
103
+ list-style-position: outside;
104
+ }
105
+
106
+ ul {
107
+ list-style-type: disc;
108
+ }
109
+
110
+ ol {
111
+ list-style-type: decimal;
112
+ padding-left: 2.25rem;
113
+ }
114
+
115
+ li {
116
+ margin-bottom: 8px;
117
+ }
118
+
119
+ li > p:first-child {
120
+ display: inline;
121
+ margin: 0;
122
+ }
123
+
124
+ li:has(> input[type="checkbox"]),
125
+ li:has(> p > input[type="checkbox"]) {
126
+ position: relative;
127
+ list-style-type: none;
128
+ }
129
+
130
+ li > input[type="checkbox"],
131
+ li > p > input[type="checkbox"] {
132
+ position: absolute;
133
+ top: 4px;
134
+ left: -20px;
135
+ margin: 0;
136
+ }
137
+
138
+ li > p + p {
139
+ display: block;
140
+ margin-top: 0.5rem;
141
+ }
142
+
143
+ li::marker {
144
+ color: var(--v2-text-text-muted);
145
+ }
146
+
147
+ /* Nested lists spacing */
148
+ li > ul,
149
+ li > ol {
150
+ margin-top: 0.25rem;
151
+ margin-bottom: 0.25rem;
152
+ padding-left: 1rem; /* Minimal indent for nesting only */
153
+ }
154
+
155
+ li > ol {
156
+ padding-left: 1.75rem;
157
+ }
158
+
159
+ /* Blockquotes */
160
+ blockquote {
161
+ border-left: 0.5px solid var(--v2-border-border-base);
162
+ margin: 1rem 0;
163
+ padding-left: 0.5rem;
164
+ color: var(--v2-text-text-muted);
165
+ font-style: normal;
166
+ }
167
+
168
+ /* Horizontal Rule - Invisible spacing only */
169
+ hr {
170
+ border: none;
171
+ height: 0;
172
+ margin: 32px 0;
173
+ }
174
+
175
+ .shiki {
176
+ background: var(--color-background-stronger);
177
+ color: var(--text-base);
178
+ font-size: 13px;
179
+ padding: 12px;
180
+ border-radius: 6px;
181
+ border: 0.5px solid var(--v2-border-border-base);
182
+ }
183
+
184
+ [data-component="markdown-code"] {
185
+ position: relative;
186
+ }
187
+
188
+ [data-component="markdown-code"][data-code-kind="shell"] .shiki {
189
+ background: var(--markdown-shell-code-background);
190
+ border-color: var(--markdown-shell-code-border);
191
+ color: var(--markdown-shell-code-color);
192
+ }
193
+
194
+ [data-component="markdown-code"][data-code-kind="shell"] code {
195
+ color: var(--markdown-shell-code-color);
196
+ }
197
+
198
+ [data-component="markdown-code"][data-code-kind="shell"] code span {
199
+ color: var(--markdown-shell-code-token-color) !important;
200
+ }
201
+
202
+ [data-slot="markdown-copy-button"] {
203
+ position: absolute;
204
+ top: 4px;
205
+ right: 4px;
206
+ opacity: 0;
207
+ transition: opacity 0.15s ease;
208
+ z-index: 1;
209
+ }
210
+
211
+ [data-component="markdown-code"]:hover [data-slot="markdown-copy-button"],
212
+ [data-slot="markdown-copy-button"]:focus-within {
213
+ opacity: 1;
214
+ }
215
+
216
+ [data-slot="markdown-copy-button"] [data-check-icon] {
217
+ display: none;
218
+ }
219
+
220
+ [data-slot="markdown-copy-button"][data-copied="true"] [data-copy-icon] {
221
+ display: none;
222
+ }
223
+
224
+ [data-slot="markdown-copy-button"][data-copied="true"] [data-check-icon] {
225
+ display: inline-flex;
226
+ }
227
+
228
+ pre {
229
+ margin-top: 12px;
230
+ margin-bottom: 24px;
231
+ overflow: auto;
232
+
233
+ scrollbar-width: none;
234
+ &::-webkit-scrollbar {
235
+ display: none;
236
+ }
237
+ }
238
+
239
+ :not(pre) > code {
240
+ font-family: var(--font-family-mono);
241
+ font-feature-settings: var(--font-family-mono--font-feature-settings);
242
+ color: var(--v2-text-text-base);
243
+ font-weight: var(--font-weight-medium);
244
+ padding: 2px 4px;
245
+ border-radius: 4px;
246
+ background: color-mix(in oklch, var(--v2-text-text-base) var(--markdown-inline-code-bg-mix), transparent);
247
+ }
248
+
249
+ :not(pre) > code[data-inline-code-kind="path"] {
250
+ color: var(--markdown-inline-code-path-color);
251
+ background: color-mix(
252
+ in oklch,
253
+ var(--markdown-inline-code-path-color) var(--markdown-inline-code-bg-mix),
254
+ transparent
255
+ );
256
+ }
257
+
258
+ a.external-link > code[data-inline-code-kind="url"] {
259
+ color: var(--markdown-inline-code-color);
260
+ background: none;
261
+ text-decoration: underline;
262
+ text-decoration-color: color-mix(in oklch, var(--markdown-inline-code-color) 25%, transparent);
263
+ text-underline-offset: 2px;
264
+ }
265
+
266
+ /* Tables */
267
+ table {
268
+ width: 100%;
269
+ border-collapse: collapse;
270
+ margin: 16px 0;
271
+ font-size: var(--font-size-base);
272
+ display: block;
273
+ overflow-x: auto;
274
+ }
275
+
276
+ th,
277
+ td {
278
+ /* Minimal borders for structure, matching TUI "lines" roughly but keeping it web-clean */
279
+ border-bottom: 0.5px solid var(--v2-border-border-muted);
280
+ padding: 12px;
281
+ text-align: left;
282
+ vertical-align: top;
283
+ }
284
+
285
+ th {
286
+ color: var(--v2-text-text-base);
287
+ font-weight: var(--font-weight-medium);
288
+ border-bottom: 0.5px solid var(--v2-border-border-base);
289
+ }
290
+
291
+ /* Images */
292
+ img {
293
+ max-width: 100%;
294
+ height: auto;
295
+ border-radius: 4px;
296
+ margin: 1.5rem 0;
297
+ display: block;
298
+ }
299
+ }
300
+
301
+ body[data-new-layout] [data-component="markdown"] {
302
+ [data-slot="markdown-copy-button"]::after {
303
+ box-sizing: border-box;
304
+ display: inline-flex;
305
+ flex-direction: row;
306
+ align-items: center;
307
+ gap: 6px;
308
+ padding: 5px 6px;
309
+ border: 0;
310
+ border-radius: 4px;
311
+ background: var(--v2-background-bg-layer-01);
312
+ box-shadow: var(--v2-elevation-floating);
313
+ color: var(--v2-text-text-base);
314
+ user-select: none;
315
+ font-size: 11px;
316
+ font-weight: 530;
317
+ line-height: 12px;
318
+ letter-spacing: 0.05px;
319
+ font-variant-numeric: tabular-nums;
320
+ font-variation-settings: "slnt" 0;
321
+ }
322
+ }
323
+
324
+ body:not([data-new-layout]) [data-component="markdown"] {
325
+ color: var(--text-strong);
326
+
327
+ h1,
328
+ h2,
329
+ h3,
330
+ h4,
331
+ h5,
332
+ h6 {
333
+ font-size: 14px;
334
+ color: var(--text-strong);
335
+ font-weight: var(--font-weight-medium);
336
+ }
337
+
338
+ strong,
339
+ b {
340
+ color: var(--text-strong);
341
+ }
342
+
343
+ li::marker {
344
+ color: var(--text-weak);
345
+ }
346
+
347
+ blockquote {
348
+ border-left: 2px solid var(--border-weak-base);
349
+ color: var(--text-weak);
350
+ }
351
+
352
+ .shiki {
353
+ border-color: var(--border-weak-base);
354
+ }
355
+
356
+ :not(pre) > code {
357
+ color: var(--syntax-string);
358
+ padding: 0;
359
+ border-radius: 0;
360
+ background: none;
361
+ }
362
+
363
+ th,
364
+ td {
365
+ border-bottom: 1px solid var(--border-weaker-base);
366
+ }
367
+
368
+ th {
369
+ color: var(--text-strong);
370
+ border-bottom-color: var(--border-weak-base);
371
+ }
372
+ }
373
+
374
+ [data-component="markdown"] a.external-link:hover > code {
375
+ text-decoration: underline;
376
+ text-underline-offset: 2px;
377
+ }
@@ -0,0 +1,53 @@
1
+ // @ts-nocheck
2
+ import * as mod from "./markdown"
3
+ import { create } from "@neurocode-ai/ui/storybook/scaffold"
4
+ import { markdown } from "@neurocode-ai/ui/storybook/fixtures"
5
+
6
+ const docs = `### Overview
7
+ Render sanitized Markdown with code blocks, inline code, and safe links.
8
+
9
+ Pair with \`Code\` for standalone code views.
10
+
11
+ ### API
12
+ - Required: \`text\` Markdown string.
13
+ - Uses the Marked context provider for parsing and sanitization.
14
+
15
+ ### Variants and states
16
+ - Code blocks include copy buttons when rendered.
17
+
18
+ ### Behavior
19
+ - Sanitizes HTML and auto-converts inline URL code to links.
20
+ - Adds copy buttons to code blocks.
21
+
22
+ ### Accessibility
23
+ - Copy buttons include aria-labels from i18n.
24
+ - TODO: confirm link target behavior in sanitized output.
25
+
26
+ ### Theming/tokens
27
+ - Uses \`data-component="markdown"\` and related slots for styling.
28
+
29
+ `
30
+
31
+ const story = create({
32
+ title: "UI/Markdown",
33
+ mod,
34
+ args: {
35
+ text: markdown,
36
+ },
37
+ })
38
+
39
+ export default {
40
+ title: "UI/Markdown",
41
+ id: "components-markdown",
42
+ component: story.meta.component,
43
+ tags: ["autodocs"],
44
+ parameters: {
45
+ docs: {
46
+ description: {
47
+ component: docs,
48
+ },
49
+ },
50
+ },
51
+ }
52
+
53
+ export const Basic = story.Basic