@brett_lamy/docstream 0.5.2 → 0.5.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brett_lamy/docstream",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "GitBook-aware readonly markdown and AI stream renderer.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -70,6 +70,7 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "@brett_lamy/viz-engine": ">=0.2.0",
73
+ "framer-motion": "^13.1.1",
73
74
  "hast-util-to-html": "^9.0.5",
74
75
  "lowlight": "^3.3.0",
75
76
  "lucide-react": "^1.17.0",
@@ -1,4 +1,5 @@
1
1
  import { useState, type ReactNode } from "react"
2
+ import { LayoutGroup, motion } from "framer-motion"
2
3
  import {
3
4
  AlertTriangle,
4
5
  CheckCircle2,
@@ -92,18 +93,30 @@ function Tabs({ block, liveRenderer, sourceRenderer }: { block: Extract<Block, {
92
93
  const [active, setActive] = useState(0)
93
94
  return (
94
95
  <div className="docs-tabs">
95
- <div className="docs-tabs-header">
96
- {block.tabs.map((t, i) => (
97
- <button
98
- key={i}
99
- className={i === active ? "docs-tab-active" : ""}
100
- onClick={() => setActive(i)}
101
- >
102
- {t.title}
103
- </button>
104
- ))}
105
- </div>
106
- <div className="docs-tabs-body">
96
+ <LayoutGroup>
97
+ <div className="docs-tabs-header" role="tablist" aria-label="Content tabs">
98
+ {block.tabs.map((t, i) => (
99
+ <button
100
+ key={i}
101
+ type="button"
102
+ role="tab"
103
+ aria-selected={i === active}
104
+ className={i === active ? "docs-tab-active" : ""}
105
+ onClick={() => setActive(i)}
106
+ >
107
+ {i === active ? (
108
+ <motion.span
109
+ className="docs-tab-indicator"
110
+ layoutId="docs-tab-indicator"
111
+ transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
112
+ />
113
+ ) : null}
114
+ <span className="docs-tab-label">{t.title}</span>
115
+ </button>
116
+ ))}
117
+ </div>
118
+ </LayoutGroup>
119
+ <div className="docs-tabs-body" role="tabpanel">
107
120
  <Blocks blocks={block.tabs[active]?.children ?? []} liveRenderer={liveRenderer} sourceRenderer={sourceRenderer} />
108
121
  </div>
109
122
  </div>
@@ -225,7 +225,7 @@ export function parseBlocks(lines: string[]): Block[] {
225
225
  const code = inner.find((b) => b.type === "code")
226
226
  if (code && code.type === "code") {
227
227
  code.title = tag.attrs.title ?? null
228
- code.lineNumbers = tag.attrs.lineNumbers === "true"
228
+ code.lineNumbers = booleanAttr(tag.attrs, "lineNumbers") ?? booleanAttr(tag.attrs, "showLineNumbers") ?? true
229
229
  code.live = tag.attrs.live === "true"
230
230
  code.entry = tag.attrs.entry ?? null
231
231
  blocks.push(code)
@@ -428,13 +428,14 @@ export function parseBlocks(lines: string[]): Block[] {
428
428
  }
429
429
 
430
430
  // fenced code (``` or ~~~)
431
- const fence = trimmed.match(/^(`{3,}|~{3,})(\S*)\s*$/)
431
+ const fence = trimmed.match(/^(`{3,}|~{3,})([^\s`]*)?(?:\s+(.*?))?\s*$/)
432
432
  if (fence) {
433
433
  flushParagraph()
434
434
  const marker = fence[1][0]
435
+ const attrs = parseAttrs(fence[3])
435
436
  const code: string[] = []
436
437
  i++
437
- while (i < lines.length && !lines[i].trim().startsWith(marker.repeat(3))) {
438
+ while (i < lines.length && !lines[i].trim().startsWith(fence[1])) {
438
439
  code.push(lines[i])
439
440
  i++
440
441
  }
@@ -442,9 +443,11 @@ export function parseBlocks(lines: string[]): Block[] {
442
443
  blocks.push({
443
444
  type: "code",
444
445
  language: fence[2] || null,
445
- title: null,
446
- lineNumbers: false,
446
+ title: attrs.title ?? null,
447
+ lineNumbers: booleanAttr(attrs, "lineNumbers") ?? booleanAttr(attrs, "showLineNumbers") ?? true,
447
448
  code: code.join("\n"),
449
+ ...(booleanAttr(attrs, "live") === undefined ? {} : { live: booleanAttr(attrs, "live") }),
450
+ ...(attrs.entry ? { entry: attrs.entry } : {}),
448
451
  })
449
452
  continue
450
453
  }
@@ -459,7 +462,7 @@ export function parseBlocks(lines: string[]): Block[] {
459
462
  code.push(lines[i].slice(4))
460
463
  i++
461
464
  }
462
- blocks.push({ type: "code", language: null, title: null, lineNumbers: false, code: code.join("\n") })
465
+ blocks.push({ type: "code", language: null, title: null, lineNumbers: true, code: code.join("\n") })
463
466
  continue
464
467
  }
465
468
 
@@ -34,18 +34,14 @@ function serializeBlock(b: Block): string {
34
34
  }
35
35
 
36
36
  case "code": {
37
- const fence = "```" + (b.language ?? "")
38
- const body = `${fence}\n${b.code}\n\`\`\``
39
- if (b.title || b.lineNumbers || b.live || b.entry) {
40
- const attrs = [
41
- b.title ? ` title="${b.title}"` : "",
42
- b.lineNumbers ? ` lineNumbers="true"` : "",
43
- b.live ? ` live="true"` : "",
44
- b.entry ? ` entry="${b.entry}"` : "",
45
- ].join("")
46
- return `{% code${attrs} %}\n${body}\n{% endcode %}`
47
- }
48
- return body
37
+ const attrs = [
38
+ b.title ? `title="${b.title}"` : "",
39
+ !b.lineNumbers ? `lineNumbers="false"` : "",
40
+ b.live ? `live="true"` : "",
41
+ b.entry ? `entry="${b.entry}"` : "",
42
+ ].filter(Boolean)
43
+ const info = [b.language ?? "", ...attrs].filter(Boolean).join(" ")
44
+ return `\`\`\`${info}\n${b.code}\n\`\`\``
49
45
  }
50
46
 
51
47
  case "hint":
package/src/styles.css CHANGED
@@ -176,6 +176,7 @@
176
176
 
177
177
  .docs-hint {
178
178
  display: flex;
179
+ align-items: center;
179
180
  gap: 12px;
180
181
  padding: 14px 16px;
181
182
  border-radius: 12px;
@@ -186,7 +187,6 @@
186
187
  width: 18px;
187
188
  height: 18px;
188
189
  flex-shrink: 0;
189
- margin-top: 3px;
190
190
  }
191
191
 
192
192
  .docs-hint > div {
@@ -194,6 +194,14 @@
194
194
  min-width: 0;
195
195
  }
196
196
 
197
+ .docs-hint > div > :first-child {
198
+ margin-top: 0;
199
+ }
200
+
201
+ .docs-hint > div > :last-child {
202
+ margin-bottom: 0;
203
+ }
204
+
197
205
  .docs-hint-info {
198
206
  border-color: var(--gb-info-border);
199
207
  background: var(--gb-info-bg);
@@ -234,31 +242,77 @@
234
242
  border: 1px solid var(--gb-border);
235
243
  border-radius: 12px;
236
244
  overflow: hidden;
245
+ background: var(--gb-panel);
237
246
  }
238
247
 
239
248
  .docs-tabs-header {
240
249
  display: flex;
250
+ width: fit-content;
251
+ max-width: calc(100% - 24px);
241
252
  gap: 2px;
242
- padding: 6px 8px 0;
253
+ margin: 10px auto 0;
254
+ padding: 4px;
255
+ overflow-x: auto;
256
+ border: 1px solid var(--gb-border);
257
+ border-radius: 999px;
243
258
  background: var(--gb-muted);
244
- border-bottom: 1px solid var(--gb-border);
245
259
  }
246
260
 
247
261
  .docs-tabs-header button {
262
+ position: relative;
263
+ z-index: 0;
264
+ flex: 0 0 auto;
265
+ border: 0;
266
+ border-radius: 999px;
248
267
  padding: 7px 14px;
268
+ background: transparent;
269
+ font: inherit;
249
270
  font-size: 13.5px;
250
- border-radius: 8px 8px 0 0;
271
+ line-height: 1.25;
251
272
  color: var(--gb-muted-foreground);
273
+ cursor: pointer;
274
+ -webkit-tap-highlight-color: transparent;
275
+ transition: color 160ms ease;
252
276
  }
253
277
 
254
278
  .docs-tabs-header .docs-tab-active {
255
- background: var(--background, #ffffff);
256
- color: var(--foreground, #18181b);
257
- font-weight: 500;
279
+ color: var(--gb-panel-foreground);
280
+ font-weight: 600;
281
+ }
282
+
283
+ .docs-tabs-header button:hover:not(.docs-tab-active) {
284
+ color: var(--gb-panel-foreground);
285
+ }
286
+
287
+ .docs-tabs-header button:focus-visible {
288
+ outline: 2px solid var(--gb-primary);
289
+ outline-offset: 2px;
290
+ }
291
+
292
+ .docs-tab-indicator {
293
+ position: absolute;
294
+ inset: 0;
295
+ z-index: -1;
296
+ border: 1px solid var(--gb-border);
297
+ border-radius: inherit;
298
+ background: var(--gb-panel);
299
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08), 0 1px 6px rgba(15, 23, 42, 0.06);
300
+ }
301
+
302
+ .docs-tab-label {
303
+ position: relative;
258
304
  }
259
305
 
260
306
  .docs-tabs-body {
307
+ margin-top: 10px;
261
308
  padding: 14px 16px;
309
+ border-top: 1px solid var(--gb-border);
310
+ }
311
+
312
+ @media (prefers-reduced-motion: reduce) {
313
+ .docs-tabs-header button {
314
+ transition: none;
315
+ }
262
316
  }
263
317
 
264
318
  .docs-expandable {