From 7d674aa6dac5e4f90f3e77e458d1b531c8b25a7d Mon Sep 17 00:00:00 2001
From: dawn
Date: Thu, 10 Sep 2026 07:50:39 +0300
Subject: [PATCH] web: implement strings view and edit
Signed-off-by: dawn
---
web/boneyard.config.json | 6 +
web/package.json | 2 +-
...diffs.patch => @pierre__diffs@1.4.1.patch} | 44 +-
web/pnpm-lock.yaml | 40 +-
web/pnpm-workspace.yaml | 2 +-
web/src/lib/api/awaitRecord.ts | 19 +
web/src/lib/api/blob.test.ts | 36 ++
web/src/lib/api/blob.ts | 51 +-
web/src/lib/api/graph.ts | 23 +-
web/src/lib/api/load.ts | 2 +
web/src/lib/api/records.ts | 3 +
web/src/lib/api/repoIndex.ts | 4 +-
web/src/lib/api/strings.test.ts | 219 ++++++++
web/src/lib/api/strings.ts | 64 ++-
.../strings-stringview--code-file.bones.json | 483 ++++++++++++++++++
...view--markdown-skeleton-fixture.bones.json | 209 ++++++++
.../components/comment/CommentSection.svelte | 67 +++
web/src/lib/components/comment/comments.ts | 107 +++-
.../lib/components/editor/PierreEditor.svelte | 106 ++++
.../profile/StringCardContent.svelte | 4 +-
.../components/profile/tabs/StarredTab.svelte | 4 +-
.../components/repo/BlobHeader.stories.svelte | 28 +-
web/src/lib/components/repo/BlobHeader.svelte | 97 ++--
.../components/repo/BlobView.stories.svelte | 16 +-
web/src/lib/components/repo/BlobView.svelte | 39 +-
.../lib/components/repo/DiffCodeView.svelte | 5 +-
.../lib/components/repo/FilePathChip.svelte | 11 +
web/src/lib/components/repo/PierreFile.svelte | 4 +-
.../components/repo/RepoCardContent.svelte | 9 +-
web/src/lib/components/repo/RepoCrumbs.svelte | 7 +-
web/src/lib/components/repo/RepoHeader.svelte | 10 +-
.../components/repo/StarButton.stories.svelte | 9 +-
web/src/lib/components/repo/StarButton.svelte | 126 +++--
.../repo/issues/IssueThreadView.svelte | 72 +--
web/src/lib/components/repo/pierre.ts | 21 +-
.../strings/StringForm.stories.svelte | 1 +
.../lib/components/strings/StringForm.svelte | 54 +-
.../components/strings/StringHeader.svelte | 98 ++++
.../strings/StringView.stories.svelte | 86 ++++
.../lib/components/strings/StringView.svelte | 62 +++
web/src/lib/components/strings/urls.ts | 7 +
web/src/lib/optimistic.svelte.ts | 40 ++
web/src/pierre.css | 2 +
.../[repo]/blob/[ref]/[...path]/+page.svelte | 3 +-
.../[repo]/blob/[ref]/[...path]/+page.ts | 28 +-
.../[handle]/[repo]/issues/[aturi]/+page.ts | 46 +-
.../strings/[handle]/[rkey]/+page.svelte | 27 +
.../routes/strings/[handle]/[rkey]/+page.ts | 90 ++++
.../[handle]/[rkey]/StringPageContent.svelte | 126 +++++
.../strings/[handle]/[rkey]/raw/+server.ts | 39 ++
.../[handle]/[rkey]/stringPage.test.ts | 107 ++++
web/src/routes/strings/new/+page.svelte | 5 +-
web/vite.config.ts | 8 +-
53 files changed, 2460 insertions(+), 318 deletions(-)
rename web/patches/{@pierre__diffs.patch => @pierre__diffs@1.4.1.patch} (56%)
create mode 100644 web/src/lib/api/blob.test.ts
create mode 100644 web/src/lib/api/strings.test.ts
create mode 100644 web/src/lib/bones/strings-stringview--code-file.bones.json
create mode 100644 web/src/lib/bones/strings-stringview--markdown-skeleton-fixture.bones.json
create mode 100644 web/src/lib/components/comment/CommentSection.svelte
create mode 100644 web/src/lib/components/editor/PierreEditor.svelte
create mode 100644 web/src/lib/components/repo/FilePathChip.svelte
create mode 100644 web/src/lib/components/strings/StringHeader.svelte
create mode 100644 web/src/lib/components/strings/StringView.stories.svelte
create mode 100644 web/src/lib/components/strings/StringView.svelte
create mode 100644 web/src/lib/components/strings/urls.ts
create mode 100644 web/src/routes/strings/[handle]/[rkey]/+page.svelte
create mode 100644 web/src/routes/strings/[handle]/[rkey]/+page.ts
create mode 100644 web/src/routes/strings/[handle]/[rkey]/StringPageContent.svelte
create mode 100644 web/src/routes/strings/[handle]/[rkey]/raw/+server.ts
create mode 100644 web/src/routes/strings/[handle]/[rkey]/stringPage.test.ts
diff --git a/web/boneyard.config.json b/web/boneyard.config.json
index 36bcb0d3b..8c96f54d0 100644
--- a/web/boneyard.config.json
+++ b/web/boneyard.config.json
@@ -144,6 +144,12 @@
},
"repo-difffilecard--skeleton-fixture": {
"route": "/iframe.html?id=repo-difffilecard--skeleton-fixture&viewMode=story"
+ },
+ "strings-stringview--code-file": {
+ "route": "/iframe.html?id=strings-stringview--code-file&viewMode=story"
+ },
+ "strings-stringview--markdown-skeleton-fixture": {
+ "route": "/iframe.html?id=strings-stringview--markdown-skeleton-fixture&viewMode=story"
}
}
}
diff --git a/web/package.json b/web/package.json
index 62d1343ee..3ed86d4c0 100644
--- a/web/package.json
+++ b/web/package.json
@@ -74,7 +74,7 @@
"@mdit/plugin-alert": "^1.0.1",
"@mdit/plugin-footnote": "^1.0.1",
"@mdit/plugin-tasklist": "^1.0.1",
- "@pierre/diffs": "^1.2.12",
+ "@pierre/diffs": "^1.4.1",
"@tanstack/svelte-virtual": "^3.13.35",
"ansi_up": "^6.0.6",
"boneyard-js": "1.9.0",
diff --git a/web/patches/@pierre__diffs.patch b/web/patches/@pierre__diffs@1.4.1.patch
similarity index 56%
rename from web/patches/@pierre__diffs.patch
rename to web/patches/@pierre__diffs@1.4.1.patch
index 15d70fdb8..2107040db 100644
--- a/web/patches/@pierre__diffs.patch
+++ b/web/patches/@pierre__diffs@1.4.1.patch
@@ -1,8 +1,8 @@
diff --git a/dist/components/VirtualizedFile.js b/dist/components/VirtualizedFile.js
-index abb195e6bf38e3a8f1e5c72f630f9c9809554774..45bc44ff026e3cb2c3020611e864acd2ea216934 100644
+index 89e2d37f700c20b9a824cf85922439416d7683e9..a2383b5fab5e6a9e086de040b74c14764a4dc0f0 100644
--- a/dist/components/VirtualizedFile.js
+++ b/dist/components/VirtualizedFile.js
-@@ -431,12 +431,17 @@ var VirtualizedFile = class extends File {
+@@ -556,12 +556,17 @@ var VirtualizedFile = class extends File {
const startingLine = Math.max(0, Math.min(idealStartHunk, totalHunksInFile)) * hunkLineCount;
const clampedTotalLines = idealStartHunk < 0 ? totalLines + idealStartHunk * hunkLineCount : totalLines;
const bufferBefore = startingLine === 0 ? 0 : fileAnnotationHeight + startingLine * lineHeight;
@@ -24,10 +24,10 @@ index abb195e6bf38e3a8f1e5c72f630f9c9809554774..45bc44ff026e3cb2c3020611e864acd2
}
const overflowHunks = totalHunks;
diff --git a/dist/components/VirtualizedFileDiff.js b/dist/components/VirtualizedFileDiff.js
-index 3d48b53247ae3bd1f8b8c2b668efe084d723cdf5..c80c91b9c118f79f4826e64cafe65ff755c2da2d 100644
+index e268d0076b69a3e623846de82b3020c64916522a..5bce3dfa4a6c3ab4571eea7e7c2e52b53fc38054 100644
--- a/dist/components/VirtualizedFileDiff.js
+++ b/dist/components/VirtualizedFileDiff.js
-@@ -734,13 +734,18 @@ var VirtualizedFileDiff = class extends FileDiff {
+@@ -1038,13 +1038,18 @@ var VirtualizedFileDiff = class extends FileDiff {
const clampedTotalLines = idealStartHunk < 0 ? totalLines + idealStartHunk * hunkLineCount : totalLines;
const codeBufferBefore = hunkOffsets[startHunk] ?? 0;
const bufferBefore = startingLine === 0 ? 0 : fileAnnotationHeight + codeBufferBefore;
@@ -48,3 +48,39 @@ index 3d48b53247ae3bd1f8b8c2b668efe084d723cdf5..c80c91b9c118f79f4826e64cafe65ff7
};
}
};
+diff --git a/dist/editor/tokenizer.js b/dist/editor/tokenizer.js
+index 804fb157d278ec26787fc0aede422ad205096780..8da1f56f3a672aee29046e0feab4c5b74b5163c8 100644
+--- a/dist/editor/tokenizer.js
++++ b/dist/editor/tokenizer.js
+@@ -170,12 +170,12 @@ var EditorTokenizer = class {
+ }`);
+ this.#themeName = themeName;
+ this.#themeType = themeType ?? theme.type;
+- this.#colorMap = colorMap;
++ this.#colorMap = resolveColorMap(colorMap, theme);
+ }
+ #ensureActiveTheme() {
+ if (this.#themeName === "") return;
+- const { colorMap } = this.#highlighter.setTheme(this.#themeName);
+- this.#colorMap = colorMap;
++ const { theme, colorMap } = this.#highlighter.setTheme(this.#themeName);
++ this.#colorMap = resolveColorMap(colorMap, theme);
+ }
+ cleanUp() {
+ this.#isCleanedUp = true;
+@@ -544,6 +544,15 @@ var EditorTokenizer = class {
+ this.#postTokenizeMessage(jobId);
+ }
+ };
++// tangled patch: shiki encodes a css-variable theme's colors as sentinel hexes
++// and keeps the reverse map on theme.colorReplacements; codeToHast applies it
++// through applyColorReplacements, while this tokenizer wrote the sentinels
++// straight into inline styles and painted rgba(0, 0, 0, 0.004)
++function resolveColorMap(colorMap, theme) {
++ const replacements = theme?.colorReplacements;
++ if (replacements == null) return colorMap;
++ return colorMap.map((color) => replacements[color?.toLowerCase()] ?? color);
++}
+ function tokenizeLine(grammar, colorMap, lineText, stateStack, timeLimit, collectBracketIgnoredRanges = true, resolveTokens = true) {
+ const result = grammar.tokenizeLine2(lineText, stateStack, timeLimit);
+ if (result.stoppedEarly) console.warn(`[diffs] Time limit reached when tokenizing line: ${lineText.substring(0, 100)}`);
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index d4bb9d4ce..e21fda9ae 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -5,7 +5,7 @@ settings:
excludeLinksFromLockfile: false
patchedDependencies:
- '@pierre/diffs': 81549e7b5525627e265441b26345e5ba9aa0ff1ffc0a91570f9e169dc9343349
+ '@pierre/diffs@1.4.1': b08596641b783a2c462c36e037f02db2505428aaa2f439c216330d5a09553f3f
boneyard-js@1.9.0: e9bda6b0720bed34eefee605bd041784ac2849ae4d27120d9815663d342f694f
importers:
@@ -46,8 +46,8 @@ importers:
specifier: ^1.0.1
version: 1.0.1(markdown-it@14.3.0)
'@pierre/diffs':
- specifier: ^1.2.12
- version: 1.2.12(patch_hash=81549e7b5525627e265441b26345e5ba9aa0ff1ffc0a91570f9e169dc9343349)(@shikijs/themes@4.3.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ specifier: ^1.4.1
+ version: 1.4.1(patch_hash=b08596641b783a2c462c36e037f02db2505428aaa2f439c216330d5a09553f3f)(@shikijs/themes@4.3.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@tanstack/svelte-virtual':
specifier: ^3.13.35
version: 3.13.35(svelte@5.56.9(@typescript-eslint/types@8.64.0))
@@ -1132,20 +1132,25 @@ packages:
cpu: [x64]
os: [win32]
- '@pierre/diffs@1.2.12':
- resolution: {integrity: sha512-pY/gmgWL03WnagqCyCnBi3QtRXUv4hCIY6FYqd5b1ZGaoI6a4Bsji8j+yRl2RfzPh/8Hf19rCl1GE80G6a1cLQ==}
+ '@pierre/diffs@1.4.1':
+ resolution: {integrity: sha512-rzvY9FeeYdGtVcErjNsW6tnrjpMb0DvGtgCFNuMoYT8wufE+gO1ZXAzKAc2VPRBS3Rl5HELEQ38WDlis4qZHkQ==}
peerDependencies:
react: ^18.3.1 || ^19.0.0
react-dom: ^18.3.1 || ^19.0.0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-dom:
+ optional: true
- '@pierre/theme@1.1.0':
- resolution: {integrity: sha512-GC2OWTAfTIIWWYhPCygwG8t2EtePQkRfON4MI2rwIkJylmiyqIttJID2dCL8sUD8cNdEvYkEyfEHHKMeCiDLoQ==}
+ '@pierre/theme@2.0.0':
+ resolution: {integrity: sha512-yNDd9GYLQl1mEUJR8AneJ5e4ohLIHQd/wZLWr4fagt78vS2RwwZNW530vVgHqXFAyFVcFlRmGUD5ramXH46OXw==}
engines: {vscode: ^1.0.0}
- '@pierre/theming@0.0.2':
- resolution: {integrity: sha512-QM1M4stXfnzfaE8I8YbjXSApV8c+2dBsXJj8eYg9WTpBR/cTmCZIcfGnN4p13iRrYu2Br/R/OJfEL7uR8Qjctw==}
+ '@pierre/theming@1.0.1':
+ resolution: {integrity: sha512-WCI5Qd7iprDpISL9fBYOLe8RV53+b7mFNA3bPzl60/2CKCSrsKN8zEcep6Y3BAzvARlmca50zGjDodqPGiTUKA==}
peerDependencies:
- '@pierre/theme': ^1.1.0
+ '@pierre/theme': ^1.1.0 || ^2.0.0
'@shikijs/themes': ^3.0.0 || ^4.0.0
react: ^18.3.1 || ^19.0.0
react-dom: ^18.3.1 || ^19.0.0
@@ -4356,25 +4361,26 @@ snapshots:
'@oxc-resolver/binding-win32-x64-msvc@11.24.2':
optional: true
- '@pierre/diffs@1.2.12(patch_hash=81549e7b5525627e265441b26345e5ba9aa0ff1ffc0a91570f9e169dc9343349)(@shikijs/themes@4.3.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ '@pierre/diffs@1.4.1(patch_hash=b08596641b783a2c462c36e037f02db2505428aaa2f439c216330d5a09553f3f)(@shikijs/themes@4.3.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
- '@pierre/theme': 1.1.0
- '@pierre/theming': 0.0.2(@pierre/theme@1.1.0)(@shikijs/themes@4.3.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.3.1)
+ '@pierre/theme': 2.0.0
+ '@pierre/theming': 1.0.1(@pierre/theme@2.0.0)(@shikijs/themes@4.3.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.3.1)
'@shikijs/transformers': 4.3.1
diff: 9.0.0
hast-util-to-html: 9.0.5
lru_map: 0.4.1
+ shiki: 4.3.1
+ optionalDependencies:
react: 19.2.8
react-dom: 19.2.8(react@19.2.8)
- shiki: 4.3.1
transitivePeerDependencies:
- '@shikijs/themes'
- '@pierre/theme@1.1.0': {}
+ '@pierre/theme@2.0.0': {}
- '@pierre/theming@0.0.2(@pierre/theme@1.1.0)(@shikijs/themes@4.3.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.3.1)':
+ '@pierre/theming@1.0.1(@pierre/theme@2.0.0)(@shikijs/themes@4.3.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.3.1)':
optionalDependencies:
- '@pierre/theme': 1.1.0
+ '@pierre/theme': 2.0.0
'@shikijs/themes': 4.3.1
react: 19.2.8
react-dom: 19.2.8(react@19.2.8)
diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml
index 01400475f..27d375c68 100644
--- a/web/pnpm-workspace.yaml
+++ b/web/pnpm-workspace.yaml
@@ -2,5 +2,5 @@ allowBuilds:
esbuild: true
workerd: true
patchedDependencies:
- "@pierre/diffs": patches/@pierre__diffs.patch
+ "@pierre/diffs@1.4.1": patches/@pierre__diffs@1.4.1.patch
boneyard-js@1.9.0: patches/boneyard-js@1.9.0.patch
diff --git a/web/src/lib/api/awaitRecord.ts b/web/src/lib/api/awaitRecord.ts
index 667c64300..f796a9424 100644
--- a/web/src/lib/api/awaitRecord.ts
+++ b/web/src/lib/api/awaitRecord.ts
@@ -1,6 +1,7 @@
import { ok } from "@atcute/client";
import type { Cid, ResourceUri } from "@atcute/lexicons/syntax";
import type { BobbinContext } from "$lib/api/client";
+import { orNull } from "$lib/api/load";
import { mainSchema as awaitRecordSchema } from "$lib/api/lexicons/types/sh/tangled/bobbin/awaitRecord";
export interface RecordRevision {
@@ -28,3 +29,21 @@ export const awaitIndexedRecord = async (
throw new Error(result.detail || result.reason || "Bobbin rejected the record.");
}
};
+
+const DELETION_BUDGET_MS = 5000;
+const DELETION_POLL_MS = 150;
+
+export const awaitDeletedRecord = async (
+ ctx: BobbinContext,
+ uri: ResourceUri,
+ { budgetMs = DELETION_BUDGET_MS, pollMs = DELETION_POLL_MS } = {}
+): Promise => {
+ const deadline = Date.now() + budgetMs;
+ for (;;) {
+ const result = await orNull(awaitRecord(ctx, { uri }));
+ if (result === null) return false;
+ if (result.status !== "indexed") return true;
+ if (Date.now() >= deadline) return false;
+ await new Promise((done) => setTimeout(done, pollMs));
+ }
+};
diff --git a/web/src/lib/api/blob.test.ts b/web/src/lib/api/blob.test.ts
new file mode 100644
index 000000000..dc67ced94
--- /dev/null
+++ b/web/src/lib/api/blob.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, it } from "vitest";
+import { contentStats, formatBytes, MAX_INLINE_SIZE } from "$lib/api/blob";
+
+describe("contentStats", () => {
+ it("counts an empty string as no lines and no bytes", () => {
+ expect(contentStats("")).toEqual({ lines: 0, bytes: 0, sizeLabel: "0 B" });
+ });
+
+ it("treats a trailing newline as a terminator, matching the repo blob header", () => {
+ expect(contentStats("a\nb").lines).toBe(2);
+ expect(contentStats("a\nb\n").lines).toBe(2);
+ expect(contentStats("a\n").lines).toBe(1);
+ });
+
+ it("sizes utf-8 rather than utf-16 code units", () => {
+ expect(contentStats("é").bytes).toBe(2);
+ expect(contentStats("hi").bytes).toBe(2);
+ });
+
+ it("keeps the 1MiB inline cap meaningful for the string loader", () => {
+ expect(contentStats("x".repeat(MAX_INLINE_SIZE)).bytes).toBeLessThanOrEqual(
+ MAX_INLINE_SIZE
+ );
+ expect(contentStats("x".repeat(MAX_INLINE_SIZE + 1)).bytes).toBeGreaterThan(
+ MAX_INLINE_SIZE
+ );
+ });
+});
+
+describe("formatBytes", () => {
+ it("promotes a rounded spill into the next unit", () => {
+ expect(formatBytes(999)).toBe("999 B");
+ expect(formatBytes(1000)).toBe("1.0 kB");
+ expect(formatBytes(999_999)).toBe("1.0 MB");
+ });
+});
diff --git a/web/src/lib/api/blob.ts b/web/src/lib/api/blob.ts
index 3f6847875..849560dc5 100644
--- a/web/src/lib/api/blob.ts
+++ b/web/src/lib/api/blob.ts
@@ -1,9 +1,10 @@
import { error } from "@sveltejs/kit";
import { ClientResponseError } from "$lib/api/client";
import { blob as gitBlob, gitTarget } from "$lib/api/gitclient";
-import { toHttpError } from "$lib/api/load";
+import { orNull, toHttpError } from "$lib/api/load";
import { toTreeCommitSummary, type CommitSummary } from "$lib/api/repo";
-import { isMarkdownFile } from "$lib/markup";
+import { isMarkdownFile, renderDocument, type MarkupContext } from "$lib/markup";
+import type { StringRecord } from "$lib/api/records";
import { renderReadme, type RepoLoadEvent, type RepoParent } from "$lib/api/repoIndex";
export type BlobKind = "code" | "markup" | "svg" | "image" | "video" | "submodule" | "other";
@@ -22,6 +23,7 @@ export interface RepoBlobView {
defaultView: BlobViewMode;
submodule: { name: string; url: string } | null;
lastCommit: CommitSummary | null;
+ prerenderedHTML?: string;
}
// same 1MiB inline cap as the appview's maxBlobSize
@@ -178,3 +180,48 @@ export const loadRepoBlob = async (
lastCommit
};
};
+
+const textEncoder = new TextEncoder();
+
+export const contentStats = (
+ contents: string
+): { lines: number; bytes: number; sizeLabel: string } => {
+ const bytes = textEncoder.encode(contents).length;
+ return { lines: countLines(contents), bytes, sizeLabel: formatBytes(bytes) };
+};
+
+export interface StringBlobSource {
+ rkey: string;
+ code: boolean;
+}
+
+export const loadStringBlob = async (
+ record: StringRecord,
+ { rkey, code }: StringBlobSource,
+ markup: MarkupContext
+): Promise => {
+ const { filename, contents } = record;
+ const stats = contentStats(contents);
+ const fileTooLarge = stats.bytes > MAX_INLINE_SIZE;
+ const text = fileTooLarge ? null : contents;
+ const isMarkdown = isMarkdownFile(filename);
+
+ const renderedHtml =
+ text !== null && isMarkdown ? await orNull(renderDocument(filename, text, markup)) : null;
+ const kind = isMarkdown && renderedHtml !== null ? "markup" : "code";
+ const defaultView = kind === "markup" && !code ? "rendered" : "code";
+
+ return {
+ ref: rkey,
+ path: filename,
+ kind,
+ sizeLabel: stats.sizeLabel,
+ lines: text === null ? null : stats.lines,
+ contents: text,
+ renderedHtml,
+ fileTooLarge,
+ defaultView,
+ submodule: null,
+ lastCommit: null
+ };
+};
diff --git a/web/src/lib/api/graph.ts b/web/src/lib/api/graph.ts
index 42da71319..b507413c6 100644
--- a/web/src/lib/api/graph.ts
+++ b/web/src/lib/api/graph.ts
@@ -1,4 +1,4 @@
-import type { Did, Nsid } from "@atcute/lexicons/syntax";
+import type { Did, Nsid, ResourceUri } from "@atcute/lexicons/syntax";
import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
import { ClientResponseError, type BobbinContext } from "$lib/api/client";
import { jsonGet } from "$lib/api/_request";
@@ -73,13 +73,24 @@ export const deleteFollow = async (agent: OAuthUserAgent, rkey: string): Promise
await deleteRecord(agent, FOLLOW_COLLECTION, rkey);
};
-export const createStar = async (agent: OAuthUserAgent, repoDid: string): Promise => {
+export type StarSubject =
+ | { kind: "repo"; did: string; ownerHandle: string; name: string }
+ | { kind: "string"; uri: string };
+
+export const createStar = async (agent: OAuthUserAgent, subject: StarSubject): Promise => {
+ const record =
+ subject.kind === "repo"
+ ? {
+ $type: "sh.tangled.feed.star#repo" as const,
+ did: subject.did as Did
+ }
+ : {
+ $type: "sh.tangled.feed.star#string" as const,
+ uri: subject.uri as ResourceUri
+ };
const { uri } = await createRecord(agent, STAR_COLLECTION, {
$type: "sh.tangled.feed.star",
- subject: {
- $type: "sh.tangled.feed.star#repo",
- did: repoDid as Did
- },
+ subject: record,
createdAt: new Date().toISOString()
});
return rkeyFromUri(uri);
diff --git a/web/src/lib/api/load.ts b/web/src/lib/api/load.ts
index 362e8e8a0..0f996e641 100644
--- a/web/src/lib/api/load.ts
+++ b/web/src/lib/api/load.ts
@@ -48,6 +48,8 @@ export const stream = (promise: Promise): Promise => {
return promise;
};
+export const orNull = (promise: Promise): Promise => promise.catch(() => null);
+
// per-request promise de-dupe cache.
export interface RequestCache {
run(key: string, load: () => Promise): Promise;
diff --git a/web/src/lib/api/records.ts b/web/src/lib/api/records.ts
index 57858662b..93a74690d 100644
--- a/web/src/lib/api/records.ts
+++ b/web/src/lib/api/records.ts
@@ -132,3 +132,6 @@ export const listStars = (
filter: { limit?: number; cursor?: string; offset?: number; order?: "asc" | "desc" } = {},
init?: XrpcRequestInit
) => jsonGet(ctx, "sh.tangled.feed.listStars", { subject, ...filter }, init);
+
+export const getString = (ctx: BobbinContext, uri: string, init?: XrpcRequestInit) =>
+ jsonGet>(ctx, "sh.tangled.string.getString", { uri }, init);
diff --git a/web/src/lib/api/repoIndex.ts b/web/src/lib/api/repoIndex.ts
index 646d3822c..cc3277eb1 100644
--- a/web/src/lib/api/repoIndex.ts
+++ b/web/src/lib/api/repoIndex.ts
@@ -10,7 +10,7 @@ import {
tags as gitTags,
tree as gitTree
} from "$lib/api/gitclient";
-import { parallel, stream } from "$lib/api/load";
+import { orNull, parallel, stream } from "$lib/api/load";
import {
sortBranches,
sortTreeEntries,
@@ -41,8 +41,6 @@ export interface RepoLoadEvent {
url: URL;
}
-const orNull = (promise: Promise): Promise => promise.catch(() => null);
-
interface Attempt {
value: T | null;
error: unknown | null;
diff --git a/web/src/lib/api/strings.test.ts b/web/src/lib/api/strings.test.ts
new file mode 100644
index 000000000..39d8fdea6
--- /dev/null
+++ b/web/src/lib/api/strings.test.ts
@@ -0,0 +1,219 @@
+import { describe, expect, it, vi } from "vitest";
+import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
+import type { BobbinContext } from "$lib/api/client";
+
+const agent = { sub: "did:plc:alice" } as unknown as OAuthUserAgent;
+
+const load = async () => {
+ vi.resetModules();
+ const writes: Record[] = [];
+ const waits: Record[] = [];
+ vi.doMock("$app/environment", () => ({ browser: true }));
+ vi.doMock("$lib/auth/agent", () => ({
+ createClient: () => ({
+ call: async (_schema: unknown, options: { input: Record }) => {
+ writes.push(options.input);
+ return {
+ ok: true,
+ data: { uri: "at://did:plc:alice/sh.tangled.string/3l7abc", cid: "bafytest" }
+ };
+ }
+ })
+ }));
+ const strings = await import("./strings");
+ const graph = await import("./graph");
+ const records = await import("./records");
+ const bobbin = {
+ xrpc: {
+ call: async (_schema: unknown, options: { params: Record }) => {
+ waits.push(options.params);
+ return {
+ ok: true,
+ data: options.params.cid
+ ? { status: "indexed", cid: options.params.cid }
+ : { status: "deleted" }
+ };
+ }
+ }
+ } as unknown as BobbinContext;
+ return { strings, graph, records, writes, waits, bobbin };
+};
+
+describe("strings api", () => {
+ it("puts a string record into sh.tangled.string collection with optional swapRecord", async () => {
+ const { strings, writes, waits, bobbin } = await load();
+ await strings.putString(
+ agent,
+ bobbin,
+ "3l7abc",
+ {
+ $type: "sh.tangled.string",
+ filename: "main.rs",
+ description: "test snippet",
+ contents: "fn main() {}",
+ createdAt: "2026-05-01T00:00:00Z"
+ },
+ "bafyswap"
+ );
+
+ expect(writes).toHaveLength(1);
+ expect(writes[0]).toMatchObject({
+ repo: "did:plc:alice",
+ collection: "sh.tangled.string",
+ rkey: "3l7abc",
+ swapRecord: "bafyswap",
+ record: {
+ $type: "sh.tangled.string",
+ filename: "main.rs",
+ contents: "fn main() {}"
+ }
+ });
+ expect(waits).toEqual([
+ { uri: "at://did:plc:alice/sh.tangled.string/3l7abc", cid: "bafytest" }
+ ]);
+ });
+
+ it("surfaces a write the appview did not index", async () => {
+ const { strings, bobbin } = await load();
+ (bobbin as unknown as { xrpc: { call: unknown } }).xrpc.call = async () => ({
+ ok: true,
+ data: { status: "rejected", detail: "unknown collection" }
+ });
+
+ await expect(
+ strings.putString(agent, bobbin, "3l7abc", {
+ $type: "sh.tangled.string",
+ filename: "main.rs",
+ description: "",
+ contents: "fn main() {}",
+ createdAt: "2026-05-01T00:00:00Z"
+ })
+ ).rejects.toThrow("unknown collection");
+ });
+
+ it("deletes a string record from sh.tangled.string collection with optional swapRecord", async () => {
+ const { strings, writes, waits, bobbin } = await load();
+ await strings.deleteString(
+ agent,
+ bobbin,
+ "at://did:plc:alice/sh.tangled.string/3l7abc",
+ "bafyswap"
+ );
+
+ expect(writes).toHaveLength(1);
+ expect(writes[0]).toMatchObject({
+ repo: "did:plc:alice",
+ collection: "sh.tangled.string",
+ rkey: "3l7abc",
+ swapRecord: "bafyswap"
+ });
+ expect(waits).toEqual([{ uri: "at://did:plc:alice/sh.tangled.string/3l7abc" }]);
+ });
+
+ it("reports an unconfirmed delete instead of failing the delete", async () => {
+ vi.useFakeTimers();
+ try {
+ const { strings, bobbin } = await load();
+ (bobbin as unknown as { xrpc: { call: unknown } }).xrpc.call = async () => ({
+ ok: true,
+ data: { status: "indexed", cid: "bafytest" }
+ });
+
+ const deleted = strings.deleteString(
+ agent,
+ bobbin,
+ "at://did:plc:alice/sh.tangled.string/3l7abc"
+ );
+ await vi.advanceTimersByTimeAsync(6000);
+ await expect(deleted).resolves.toBe(false);
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+
+ it("confirms a delete once the remembered write stops answering", async () => {
+ const { strings, bobbin } = await load();
+ let asks = 0;
+ (bobbin as unknown as { xrpc: { call: unknown } }).xrpc.call = async () => {
+ asks += 1;
+ return asks === 1
+ ? { ok: true, data: { status: "indexed", cid: "bafytest" } }
+ : { ok: true, data: { status: "deleted" } };
+ };
+
+ await expect(
+ strings.deleteString(agent, bobbin, "at://did:plc:alice/sh.tangled.string/3l7abc")
+ ).resolves.toBe(true);
+ expect(asks).toBe(2);
+ });
+
+ it("stops asking after the budget when bobbin keeps answering with the write", async () => {
+ const { bobbin } = await load();
+ let asks = 0;
+ (bobbin as unknown as { xrpc: { call: unknown } }).xrpc.call = async () => {
+ asks += 1;
+ return { ok: true, data: { status: "indexed", cid: "bafytest" } };
+ };
+
+ const { awaitDeletedRecord } = await import("./awaitRecord");
+ await expect(
+ awaitDeletedRecord(bobbin, "at://did:plc:alice/sh.tangled.string/3l7abc", {
+ budgetMs: 5,
+ pollMs: 1
+ })
+ ).resolves.toBe(false);
+ expect(asks).toBeGreaterThan(1);
+ });
+
+ it("creates a star on a string AT-URI", async () => {
+ const { graph, writes } = await load();
+ const rkey = await graph.createStar(agent, {
+ kind: "string",
+ uri: "at://did:plc:bob/sh.tangled.string/3l7xyz"
+ });
+
+ expect(writes).toHaveLength(1);
+ expect(writes[0]).toMatchObject({
+ repo: "did:plc:alice",
+ collection: "sh.tangled.feed.star",
+ record: {
+ $type: "sh.tangled.feed.star",
+ subject: {
+ $type: "sh.tangled.feed.star#string",
+ uri: "at://did:plc:bob/sh.tangled.string/3l7xyz"
+ }
+ }
+ });
+ expect(rkey).toBe("3l7abc");
+ });
+
+ it("fetches a string via getString", async () => {
+ const { records } = await load();
+ const mockFetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: async () => ({
+ uri: "at://did:plc:alice/sh.tangled.string/3l7abc",
+ cid: "bafytest",
+ value: {
+ $type: "sh.tangled.string",
+ filename: "main.rs",
+ description: "test snippet",
+ contents: "fn main() {}",
+ createdAt: "2026-05-01T00:00:00Z"
+ }
+ })
+ });
+ const ctx: BobbinContext = {
+ serviceUrl: "https://bobbin.tangled.local",
+ fetch: mockFetch,
+ xrpc: {} as unknown as BobbinContext["xrpc"]
+ };
+
+ const result = await records.getString(ctx, "at://did:plc:alice/sh.tangled.string/3l7abc");
+ expect(mockFetch).toHaveBeenCalledTimes(1);
+ const url = new URL(mockFetch.mock.calls[0][0]);
+ expect(url.pathname).toBe("/xrpc/sh.tangled.string.getString");
+ expect(url.searchParams.get("uri")).toBe("at://did:plc:alice/sh.tangled.string/3l7abc");
+ expect(result.value.filename).toBe("main.rs");
+ });
+});
diff --git a/web/src/lib/api/strings.ts b/web/src/lib/api/strings.ts
index 23587b35f..61dcfb198 100644
--- a/web/src/lib/api/strings.ts
+++ b/web/src/lib/api/strings.ts
@@ -1,15 +1,65 @@
-import type { Nsid } from "@atcute/lexicons/syntax";
+import { error } from "@sveltejs/kit";
+import type { Did, Nsid, ResourceUri } from "@atcute/lexicons/syntax";
import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
-import type { StringRecord } from "$lib/api/records";
-import { putRecord } from "$lib/api/write";
+import { awaitDeletedRecord, awaitIndexedRecord } from "$lib/api/awaitRecord";
+import { ClientResponseError, type BobbinContext } from "$lib/api/client";
+import { resolveMiniDoc } from "$lib/api/identity";
+import { orNull } from "$lib/api/load";
+import { getString, type RecordView, type StringRecord } from "$lib/api/records";
+import { rkeyFromUri } from "$lib/api/uri";
+import { deleteRecord, putRecord, type WrittenRecord } from "$lib/api/write";
const STRING_COLLECTION = "sh.tangled.string" as Nsid;
-// strings live at a generated tid rkey; put upserts the record at `rkey`.
+export interface StringSubject {
+ did: Did;
+ handle: string;
+ uri: ResourceUri;
+ record: RecordView;
+}
+
+export const loadStringSubject = async (
+ ctx: BobbinContext,
+ handle: string,
+ rkey: string
+): Promise => {
+ const doc = await orNull(resolveMiniDoc(ctx, handle));
+ if (!doc) error(404, "String not found");
+
+ const uri = `at://${doc.did}/${STRING_COLLECTION}/${rkey}` as ResourceUri;
+ const record = await orNull(getString(ctx, uri));
+ if (!record) error(404, "String not found");
+
+ return { did: doc.did as Did, handle: doc.handle, uri, record };
+};
+
export const putString = async (
agent: OAuthUserAgent,
+ bobbin: BobbinContext,
rkey: string,
- record: StringRecord
-): Promise => {
- await putRecord(agent, STRING_COLLECTION, rkey, record);
+ record: StringRecord,
+ swapRecord?: string
+): Promise => {
+ const written = await putRecord(agent, STRING_COLLECTION, rkey, record, swapRecord);
+ try {
+ await awaitIndexedRecord(bobbin, written);
+ } catch (cause) {
+ if (cause instanceof ClientResponseError) {
+ throw new Error(`Saved, but the appview has not caught up yet (${cause.message}).`, {
+ cause
+ });
+ }
+ throw cause;
+ }
+ return written;
+};
+
+export const deleteString = async (
+ agent: OAuthUserAgent,
+ bobbin: BobbinContext,
+ uri: ResourceUri,
+ swapRecord?: string
+): Promise => {
+ await deleteRecord(agent, STRING_COLLECTION, rkeyFromUri(uri), swapRecord);
+ return awaitDeletedRecord(bobbin, uri);
};
diff --git a/web/src/lib/bones/strings-stringview--code-file.bones.json b/web/src/lib/bones/strings-stringview--code-file.bones.json
new file mode 100644
index 000000000..32e741d94
--- /dev/null
+++ b/web/src/lib/bones/strings-stringview--code-file.bones.json
@@ -0,0 +1,483 @@
+{
+ "breakpoints": {
+ "375": {
+ "name": "strings-stringview--code-file",
+ "viewportWidth": 343,
+ "width": 343,
+ "height": 942,
+ "bones": [
+ [4.9563, 10, 4.25, 24, 999],
+ [12.0, 14, 25.0, 16, 4],
+ [4.9563, 40, 50.0, 14, 4],
+ [0, 50, 100, 892, 4, true, 15, true],
+ [4.9563, 67, 57.0426, 34, 4, true, 15, true],
+ [8.7464, 75, 14.5089, 17, 4],
+ [27.059, 75, 8.1496, 17, 4],
+ [39.0124, 75, 5.9585, 17, 4],
+ [48.7746, 72, 13.8165, 24, 0],
+ [65.4974, 74, 29.5463, 20, 4],
+ [4.9563, 113, 90.0875, 20, 4],
+ [7.2886, 158, 4.6647, 16, 4],
+ [14.2857, 157, 10.2132, 17, 4],
+ [56.2682, 149, 38.7755, 34, 4, true, 15, true],
+ [56.2682, 150, 9.9125, 32, "4px 0px 0px 4px"],
+ [65.8892, 150, 9.9125, 32, 0],
+ [75.5102, 150, 9.9125, 32, 0],
+ [85.1312, 150, 9.9125, 32, "0px 4px 4px 0px"],
+ [4.9563, 199, 90.0875, 1, 0],
+ [4.9563, 224, 12.828, 21, 0, true, 2, false],
+ [12.3041, 225, 2.4508, 18, 4],
+ [4.9563, 245, 12.828, 21, 0, true, 2, false],
+ [12.3041, 246, 2.4508, 18, 4],
+ [4.9563, 266, 12.828, 21, 0, true, 2, false],
+ [12.3041, 267, 2.4508, 18, 4],
+ [4.9563, 287, 12.828, 21, 0, true, 2, false],
+ [12.3041, 288, 2.4508, 18, 4],
+ [4.9563, 308, 12.828, 21, 0, true, 2, false],
+ [12.3041, 309, 2.4508, 18, 4],
+ [4.9563, 329, 12.828, 21, 0, true, 2, false],
+ [12.3041, 330, 2.4508, 18, 4],
+ [4.9563, 350, 12.828, 21, 0, true, 2, false],
+ [12.3041, 351, 2.4508, 18, 4],
+ [4.9563, 371, 12.828, 21, 0, true, 2, false],
+ [12.3041, 372, 2.4508, 18, 4],
+ [4.9563, 392, 12.828, 21, 0, true, 2, false],
+ [12.3041, 393, 2.4508, 18, 4],
+ [4.9563, 413, 12.828, 21, 0, true, 2, false],
+ [9.8533, 414, 4.9016, 18, 4],
+ [4.9563, 434, 12.828, 21, 0, true, 2, false],
+ [9.8533, 435, 4.9016, 18, 4],
+ [4.9563, 455, 12.828, 21, 0, true, 2, false],
+ [9.8533, 456, 4.9016, 18, 4],
+ [4.9563, 476, 12.828, 21, 0, true, 2, false],
+ [9.8533, 477, 4.9016, 18, 4],
+ [4.9563, 497, 12.828, 21, 0, true, 2, false],
+ [9.8533, 498, 4.9016, 18, 4],
+ [4.9563, 518, 12.828, 21, 0, true, 2, false],
+ [9.8533, 519, 4.9016, 18, 4],
+ [4.9563, 539, 12.828, 21, 0, true, 2, false],
+ [9.8533, 540, 4.9016, 18, 4],
+ [4.9563, 560, 12.828, 21, 0, true, 2, false],
+ [9.8533, 561, 4.9016, 18, 4],
+ [4.9563, 581, 12.828, 21, 0, true, 2, false],
+ [9.8533, 582, 4.9016, 18, 4],
+ [4.9563, 602, 12.828, 21, 0, true, 2, false],
+ [9.8533, 603, 4.9016, 18, 4],
+ [4.9563, 623, 12.828, 21, 0, true, 2, false],
+ [9.8533, 624, 4.9016, 18, 4],
+ [4.9563, 644, 12.828, 21, 0, true, 2, false],
+ [9.8533, 645, 4.9016, 18, 4],
+ [4.9563, 665, 12.828, 21, 0, true, 2, false],
+ [9.8533, 666, 4.9016, 18, 4],
+ [4.9563, 686, 12.828, 21, 0, true, 2, false],
+ [9.8533, 687, 4.9016, 18, 4],
+ [4.9563, 707, 12.828, 21, 0, true, 2, false],
+ [9.8533, 708, 4.9016, 18, 4],
+ [4.9563, 728, 12.828, 21, 0, true, 2, false],
+ [9.8533, 729, 4.9016, 18, 4],
+ [4.9563, 749, 12.828, 21, 0, true, 2, false],
+ [9.8533, 750, 4.9016, 18, 4],
+ [4.9563, 770, 12.828, 21, 0, true, 2, false],
+ [9.8533, 771, 4.9016, 18, 4],
+ [4.9563, 791, 12.828, 21, 0, true, 2, false],
+ [9.8533, 792, 4.9016, 18, 4],
+ [4.9563, 812, 12.828, 21, 0, true, 2, false],
+ [9.8533, 813, 4.9016, 18, 4],
+ [4.9563, 833, 12.828, 21, 0, true, 2, false],
+ [9.8533, 834, 4.9016, 18, 4],
+ [4.9563, 854, 12.828, 21, 0, true, 2, false],
+ [9.8533, 855, 4.9016, 18, 4],
+ [4.9563, 875, 12.828, 21, 0, true, 2, false],
+ [9.8533, 876, 4.9016, 18, 4],
+ [4.9563, 896, 12.828, 21, 0, true, 2, false],
+ [9.8533, 897, 4.9016, 18, 4],
+ [20.2305, 225, 78.3755, 18, 4],
+ [20.2305, 246, 66.1261, 18, 4],
+ [20.2305, 267, 79.7695, 18, 4],
+ [20.2305, 309, 61.2336, 18, 4],
+ [20.2305, 330, 36.7484, 18, 4],
+ [20.2305, 351, 56.3366, 18, 4],
+ [20.2305, 372, 2.4508, 18, 4],
+ [20.2305, 414, 75.9247, 18, 4],
+ [20.2305, 435, 71.0414, 18, 4],
+ [20.2305, 456, 66.1352, 18, 4],
+ [20.2305, 519, 79.7695, 18, 4],
+ [20.2305, 540, 79.7695, 18, 4],
+ [20.2305, 561, 68.5815, 18, 4],
+ [20.2305, 582, 12.2495, 18, 4],
+ [20.2305, 603, 4.9016, 18, 4],
+ [20.2305, 645, 73.4785, 18, 4],
+ [20.2305, 666, 75.943, 18, 4],
+ [20.2305, 687, 66.1352, 18, 4],
+ [20.2305, 708, 4.9016, 18, 4],
+ [20.2305, 750, 66.1306, 18, 4],
+ [20.2305, 771, 58.7782, 18, 4],
+ [20.2305, 813, 17.1465, 18, 4],
+ [20.2305, 834, 31.8468, 18, 4],
+ [20.2305, 855, 53.8903, 18, 4],
+ [20.2305, 876, 68.5906, 18, 4],
+ [20.2305, 897, 7.3478, 18, 4]
+ ]
+ },
+ "768": {
+ "name": "strings-stringview--code-file",
+ "viewportWidth": 736,
+ "width": 736,
+ "height": 897,
+ "bones": [
+ [4.9563, 10, 4.25, 24, 999],
+ [12.0, 14, 25.0, 16, 4],
+ [4.9563, 40, 50.0, 14, 4],
+ [0, 50, 100, 847, 4, true, 15, true],
+ [2.3098, 67, 26.8003, 34, 4, true, 15, true],
+ [4.0761, 75, 6.7616, 17, 4],
+ [12.6104, 75, 3.798, 17, 4],
+ [18.181, 75, 2.7768, 17, 4],
+ [22.7306, 72, 6.4389, 24, 0],
+ [30.7405, 74, 13.7695, 20, 4],
+ [47.7709, 74, 18.3933, 20, 4],
+ [69.4251, 76, 2.1739, 16, 4],
+ [72.686, 75, 4.7597, 17, 4],
+ [79.6196, 67, 18.0707, 34, 4, true, 15, true],
+ [79.6196, 68, 4.6196, 32, "4px 0px 0px 4px"],
+ [84.1033, 68, 4.6196, 32, 0],
+ [88.587, 68, 4.6196, 32, 0],
+ [93.0707, 68, 4.6196, 32, "0px 4px 4px 0px"],
+ [2.3098, 117, 95.3804, 21, 4],
+ [2.3098, 154, 95.3804, 1, 0],
+ [2.3098, 179, 5.9783, 21, 0, true, 2, false],
+ [5.7341, 180, 1.1422, 18, 4],
+ [2.3098, 200, 5.9783, 21, 0, true, 2, false],
+ [5.7341, 201, 1.1422, 18, 4],
+ [2.3098, 221, 5.9783, 21, 0, true, 2, false],
+ [5.7341, 222, 1.1422, 18, 4],
+ [2.3098, 242, 5.9783, 21, 0, true, 2, false],
+ [5.7341, 243, 1.1422, 18, 4],
+ [2.3098, 263, 5.9783, 21, 0, true, 2, false],
+ [5.7341, 264, 1.1422, 18, 4],
+ [2.3098, 284, 5.9783, 21, 0, true, 2, false],
+ [5.7341, 285, 1.1422, 18, 4],
+ [2.3098, 305, 5.9783, 21, 0, true, 2, false],
+ [5.7341, 306, 1.1422, 18, 4],
+ [2.3098, 326, 5.9783, 21, 0, true, 2, false],
+ [5.7341, 327, 1.1422, 18, 4],
+ [2.3098, 347, 5.9783, 21, 0, true, 2, false],
+ [5.7341, 348, 1.1422, 18, 4],
+ [2.3098, 368, 5.9783, 21, 0, true, 2, false],
+ [4.592, 369, 2.2843, 18, 4],
+ [2.3098, 389, 5.9783, 21, 0, true, 2, false],
+ [4.592, 390, 2.2843, 18, 4],
+ [2.3098, 410, 5.9783, 21, 0, true, 2, false],
+ [4.592, 411, 2.2843, 18, 4],
+ [2.3098, 431, 5.9783, 21, 0, true, 2, false],
+ [4.592, 432, 2.2843, 18, 4],
+ [2.3098, 452, 5.9783, 21, 0, true, 2, false],
+ [4.592, 453, 2.2843, 18, 4],
+ [2.3098, 473, 5.9783, 21, 0, true, 2, false],
+ [4.592, 474, 2.2843, 18, 4],
+ [2.3098, 494, 5.9783, 21, 0, true, 2, false],
+ [4.592, 495, 2.2843, 18, 4],
+ [2.3098, 515, 5.9783, 21, 0, true, 2, false],
+ [4.592, 516, 2.2843, 18, 4],
+ [2.3098, 536, 5.9783, 21, 0, true, 2, false],
+ [4.592, 537, 2.2843, 18, 4],
+ [2.3098, 557, 5.9783, 21, 0, true, 2, false],
+ [4.592, 558, 2.2843, 18, 4],
+ [2.3098, 578, 5.9783, 21, 0, true, 2, false],
+ [4.592, 579, 2.2843, 18, 4],
+ [2.3098, 599, 5.9783, 21, 0, true, 2, false],
+ [4.592, 600, 2.2843, 18, 4],
+ [2.3098, 620, 5.9783, 21, 0, true, 2, false],
+ [4.592, 621, 2.2843, 18, 4],
+ [2.3098, 641, 5.9783, 21, 0, true, 2, false],
+ [4.592, 642, 2.2843, 18, 4],
+ [2.3098, 662, 5.9783, 21, 0, true, 2, false],
+ [4.592, 663, 2.2843, 18, 4],
+ [2.3098, 683, 5.9783, 21, 0, true, 2, false],
+ [4.592, 684, 2.2843, 18, 4],
+ [2.3098, 704, 5.9783, 21, 0, true, 2, false],
+ [4.592, 705, 2.2843, 18, 4],
+ [2.3098, 725, 5.9783, 21, 0, true, 2, false],
+ [4.592, 726, 2.2843, 18, 4],
+ [2.3098, 746, 5.9783, 21, 0, true, 2, false],
+ [4.592, 747, 2.2843, 18, 4],
+ [2.3098, 767, 5.9783, 21, 0, true, 2, false],
+ [4.592, 768, 2.2843, 18, 4],
+ [2.3098, 788, 5.9783, 21, 0, true, 2, false],
+ [4.592, 789, 2.2843, 18, 4],
+ [2.3098, 809, 5.9783, 21, 0, true, 2, false],
+ [4.592, 810, 2.2843, 18, 4],
+ [2.3098, 830, 5.9783, 21, 0, true, 2, false],
+ [4.592, 831, 2.2843, 18, 4],
+ [2.3098, 851, 5.9783, 21, 0, true, 2, false],
+ [4.592, 852, 2.2843, 18, 4],
+ [9.4281, 180, 37.6677, 18, 4],
+ [9.4281, 201, 60.4938, 18, 4],
+ [9.4281, 222, 38.8099, 18, 4],
+ [9.4281, 264, 28.5369, 18, 4],
+ [9.4281, 285, 17.1259, 18, 4],
+ [9.4281, 306, 26.2547, 18, 4],
+ [9.4281, 327, 1.1422, 18, 4],
+ [9.4281, 369, 51.3693, 18, 4],
+ [9.4281, 390, 57.0758, 18, 4],
+ [9.4281, 411, 63.9245, 18, 4],
+ [9.4281, 432, 90.5719, 18, 4],
+ [9.4281, 474, 69.6332, 18, 4],
+ [9.4281, 495, 60.498, 18, 4],
+ [9.4281, 516, 70.7711, 18, 4],
+ [9.4281, 537, 5.7086, 18, 4],
+ [9.4281, 558, 2.2843, 18, 4],
+ [9.4281, 600, 36.5277, 18, 4],
+ [9.4281, 621, 46.8071, 18, 4],
+ [9.4281, 642, 59.3559, 18, 4],
+ [9.4281, 663, 2.2843, 18, 4],
+ [9.4281, 705, 45.6564, 18, 4],
+ [9.4281, 726, 27.3926, 18, 4],
+ [9.4281, 768, 7.9908, 18, 4],
+ [9.4281, 789, 14.8416, 18, 4],
+ [9.4281, 810, 25.1146, 18, 4],
+ [9.4281, 831, 71.9153, 18, 4],
+ [9.4281, 852, 3.4243, 18, 4]
+ ]
+ },
+ "1024": {
+ "name": "strings-stringview--code-file",
+ "viewportWidth": 992,
+ "width": 992,
+ "height": 897,
+ "bones": [
+ [4.9563, 10, 4.25, 24, 999],
+ [12.0, 14, 25.0, 16, 4],
+ [4.9563, 40, 50.0, 14, 4],
+ [0, 50, 100, 847, 4, true, 15, true],
+ [1.7137, 67, 21.2387, 34, 4, true, 15, true],
+ [3.0242, 75, 5.0167, 17, 4],
+ [9.3561, 75, 2.8179, 17, 4],
+ [13.4892, 75, 2.0602, 17, 4],
+ [16.8646, 72, 4.7773, 24, 0],
+ [24.162, 74, 10.2161, 20, 4],
+ [69.6195, 74, 13.6467, 20, 4],
+ [84.879, 67, 13.4073, 34, 4, true, 15, true],
+ [84.879, 68, 3.4274, 32, "4px 0px 0px 4px"],
+ [88.2056, 68, 3.4274, 32, 0],
+ [91.5323, 68, 3.4274, 32, 0],
+ [94.8589, 68, 3.4274, 32, "0px 4px 4px 0px"],
+ [1.7137, 117, 96.5726, 21, 4],
+ [1.7137, 154, 96.5726, 1, 0],
+ [1.7137, 179, 4.4355, 21, 0, true, 2, false],
+ [4.2543, 180, 0.8474, 18, 4],
+ [1.7137, 200, 4.4355, 21, 0, true, 2, false],
+ [4.2543, 201, 0.8474, 18, 4],
+ [1.7137, 221, 4.4355, 21, 0, true, 2, false],
+ [4.2543, 222, 0.8474, 18, 4],
+ [1.7137, 242, 4.4355, 21, 0, true, 2, false],
+ [4.2543, 243, 0.8474, 18, 4],
+ [1.7137, 263, 4.4355, 21, 0, true, 2, false],
+ [4.2543, 264, 0.8474, 18, 4],
+ [1.7137, 284, 4.4355, 21, 0, true, 2, false],
+ [4.2543, 285, 0.8474, 18, 4],
+ [1.7137, 305, 4.4355, 21, 0, true, 2, false],
+ [4.2543, 306, 0.8474, 18, 4],
+ [1.7137, 326, 4.4355, 21, 0, true, 2, false],
+ [4.2543, 327, 0.8474, 18, 4],
+ [1.7137, 347, 4.4355, 21, 0, true, 2, false],
+ [4.2543, 348, 0.8474, 18, 4],
+ [1.7137, 368, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 369, 1.6948, 18, 4],
+ [1.7137, 389, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 390, 1.6948, 18, 4],
+ [1.7137, 410, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 411, 1.6948, 18, 4],
+ [1.7137, 431, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 432, 1.6948, 18, 4],
+ [1.7137, 452, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 453, 1.6948, 18, 4],
+ [1.7137, 473, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 474, 1.6948, 18, 4],
+ [1.7137, 494, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 495, 1.6948, 18, 4],
+ [1.7137, 515, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 516, 1.6948, 18, 4],
+ [1.7137, 536, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 537, 1.6948, 18, 4],
+ [1.7137, 557, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 558, 1.6948, 18, 4],
+ [1.7137, 578, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 579, 1.6948, 18, 4],
+ [1.7137, 599, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 600, 1.6948, 18, 4],
+ [1.7137, 620, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 621, 1.6948, 18, 4],
+ [1.7137, 641, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 642, 1.6948, 18, 4],
+ [1.7137, 662, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 663, 1.6948, 18, 4],
+ [1.7137, 683, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 684, 1.6948, 18, 4],
+ [1.7137, 704, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 705, 1.6948, 18, 4],
+ [1.7137, 725, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 726, 1.6948, 18, 4],
+ [1.7137, 746, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 747, 1.6948, 18, 4],
+ [1.7137, 767, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 768, 1.6948, 18, 4],
+ [1.7137, 788, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 789, 1.6948, 18, 4],
+ [1.7137, 809, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 810, 1.6948, 18, 4],
+ [1.7137, 830, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 831, 1.6948, 18, 4],
+ [1.7137, 851, 4.4355, 21, 0, true, 2, false],
+ [3.4069, 852, 1.6948, 18, 4],
+ [6.995, 180, 27.947, 18, 4],
+ [6.995, 201, 44.8825, 18, 4],
+ [6.995, 222, 28.7944, 18, 4],
+ [6.995, 264, 21.1725, 18, 4],
+ [6.995, 285, 12.7063, 18, 4],
+ [6.995, 306, 19.4793, 18, 4],
+ [6.995, 327, 0.8474, 18, 4],
+ [6.995, 369, 38.1127, 18, 4],
+ [6.995, 390, 42.3466, 18, 4],
+ [6.995, 411, 47.4279, 18, 4],
+ [6.995, 432, 68.6004, 18, 4],
+ [6.995, 474, 51.6633, 18, 4],
+ [6.995, 495, 44.8856, 18, 4],
+ [6.995, 516, 52.5076, 18, 4],
+ [6.995, 537, 4.2354, 18, 4],
+ [6.995, 558, 1.6948, 18, 4],
+ [6.995, 600, 27.1012, 18, 4],
+ [6.995, 621, 34.7278, 18, 4],
+ [6.995, 642, 44.0382, 18, 4],
+ [6.995, 663, 1.6948, 18, 4],
+ [6.995, 705, 33.8741, 18, 4],
+ [6.995, 726, 20.3235, 18, 4],
+ [6.995, 768, 5.9287, 18, 4],
+ [6.995, 789, 11.0115, 18, 4],
+ [6.995, 810, 18.6334, 18, 4],
+ [6.995, 831, 53.3565, 18, 4],
+ [6.995, 852, 2.5406, 18, 4]
+ ]
+ },
+ "1440": {
+ "name": "strings-stringview--code-file",
+ "viewportWidth": 1408,
+ "width": 1408,
+ "height": 897,
+ "bones": [
+ [4.9563, 10, 4.25, 24, 999],
+ [12.0, 14, 25.0, 16, 4],
+ [4.9563, 40, 50.0, 14, 4],
+ [0, 50, 100, 847, 4, true, 15, true],
+ [1.2074, 67, 14.9636, 34, 4, true, 15, true],
+ [2.1307, 75, 3.5345, 17, 4],
+ [6.5918, 75, 1.9853, 17, 4],
+ [9.5037, 75, 1.4515, 17, 4],
+ [11.8819, 72, 3.3658, 24, 0],
+ [17.0233, 74, 7.1977, 20, 4],
+ [78.5955, 74, 9.6147, 20, 4],
+ [89.3466, 67, 9.446, 34, 4, true, 15, true],
+ [89.3466, 68, 2.4148, 32, "4px 0px 0px 4px"],
+ [91.6903, 68, 2.4148, 32, 0],
+ [94.0341, 68, 2.4148, 32, 0],
+ [96.3778, 68, 2.4148, 32, "0px 4px 4px 0px"],
+ [1.2074, 117, 97.5852, 21, 4],
+ [1.2074, 154, 97.5852, 1, 0],
+ [1.2074, 179, 3.125, 21, 0, true, 2, false],
+ [2.9974, 180, 0.597, 18, 4],
+ [1.2074, 200, 3.125, 21, 0, true, 2, false],
+ [2.9974, 201, 0.597, 18, 4],
+ [1.2074, 221, 3.125, 21, 0, true, 2, false],
+ [2.9974, 222, 0.597, 18, 4],
+ [1.2074, 242, 3.125, 21, 0, true, 2, false],
+ [2.9974, 243, 0.597, 18, 4],
+ [1.2074, 263, 3.125, 21, 0, true, 2, false],
+ [2.9974, 264, 0.597, 18, 4],
+ [1.2074, 284, 3.125, 21, 0, true, 2, false],
+ [2.9974, 285, 0.597, 18, 4],
+ [1.2074, 305, 3.125, 21, 0, true, 2, false],
+ [2.9974, 306, 0.597, 18, 4],
+ [1.2074, 326, 3.125, 21, 0, true, 2, false],
+ [2.9974, 327, 0.597, 18, 4],
+ [1.2074, 347, 3.125, 21, 0, true, 2, false],
+ [2.9974, 348, 0.597, 18, 4],
+ [1.2074, 368, 3.125, 21, 0, true, 2, false],
+ [2.4003, 369, 1.1941, 18, 4],
+ [1.2074, 389, 3.125, 21, 0, true, 2, false],
+ [2.4003, 390, 1.1941, 18, 4],
+ [1.2074, 410, 3.125, 21, 0, true, 2, false],
+ [2.4003, 411, 1.1941, 18, 4],
+ [1.2074, 431, 3.125, 21, 0, true, 2, false],
+ [2.4003, 432, 1.1941, 18, 4],
+ [1.2074, 452, 3.125, 21, 0, true, 2, false],
+ [2.4003, 453, 1.1941, 18, 4],
+ [1.2074, 473, 3.125, 21, 0, true, 2, false],
+ [2.4003, 474, 1.1941, 18, 4],
+ [1.2074, 494, 3.125, 21, 0, true, 2, false],
+ [2.4003, 495, 1.1941, 18, 4],
+ [1.2074, 515, 3.125, 21, 0, true, 2, false],
+ [2.4003, 516, 1.1941, 18, 4],
+ [1.2074, 536, 3.125, 21, 0, true, 2, false],
+ [2.4003, 537, 1.1941, 18, 4],
+ [1.2074, 557, 3.125, 21, 0, true, 2, false],
+ [2.4003, 558, 1.1941, 18, 4],
+ [1.2074, 578, 3.125, 21, 0, true, 2, false],
+ [2.4003, 579, 1.1941, 18, 4],
+ [1.2074, 599, 3.125, 21, 0, true, 2, false],
+ [2.4003, 600, 1.1941, 18, 4],
+ [1.2074, 620, 3.125, 21, 0, true, 2, false],
+ [2.4003, 621, 1.1941, 18, 4],
+ [1.2074, 641, 3.125, 21, 0, true, 2, false],
+ [2.4003, 642, 1.1941, 18, 4],
+ [1.2074, 662, 3.125, 21, 0, true, 2, false],
+ [2.4003, 663, 1.1941, 18, 4],
+ [1.2074, 683, 3.125, 21, 0, true, 2, false],
+ [2.4003, 684, 1.1941, 18, 4],
+ [1.2074, 704, 3.125, 21, 0, true, 2, false],
+ [2.4003, 705, 1.1941, 18, 4],
+ [1.2074, 725, 3.125, 21, 0, true, 2, false],
+ [2.4003, 726, 1.1941, 18, 4],
+ [1.2074, 746, 3.125, 21, 0, true, 2, false],
+ [2.4003, 747, 1.1941, 18, 4],
+ [1.2074, 767, 3.125, 21, 0, true, 2, false],
+ [2.4003, 768, 1.1941, 18, 4],
+ [1.2074, 788, 3.125, 21, 0, true, 2, false],
+ [2.4003, 789, 1.1941, 18, 4],
+ [1.2074, 809, 3.125, 21, 0, true, 2, false],
+ [2.4003, 810, 1.1941, 18, 4],
+ [1.2074, 830, 3.125, 21, 0, true, 2, false],
+ [2.4003, 831, 1.1941, 18, 4],
+ [1.2074, 851, 3.125, 21, 0, true, 2, false],
+ [2.4003, 852, 1.1941, 18, 4],
+ [4.9283, 180, 19.6899, 18, 4],
+ [4.9283, 201, 31.6218, 18, 4],
+ [4.9283, 222, 20.287, 18, 4],
+ [4.9283, 264, 14.917, 18, 4],
+ [4.9283, 285, 8.9522, 18, 4],
+ [4.9283, 306, 13.724, 18, 4],
+ [4.9283, 327, 0.597, 18, 4],
+ [4.9283, 369, 26.8521, 18, 4],
+ [4.9283, 390, 29.8351, 18, 4],
+ [4.9283, 411, 33.4151, 18, 4],
+ [4.9283, 432, 48.3321, 18, 4],
+ [4.9283, 474, 36.3991, 18, 4],
+ [4.9283, 495, 31.624, 18, 4],
+ [4.9283, 516, 36.994, 18, 4],
+ [4.9283, 537, 2.9841, 18, 4],
+ [4.9283, 558, 1.1941, 18, 4],
+ [4.9283, 600, 19.094, 18, 4],
+ [4.9283, 621, 24.4673, 18, 4],
+ [4.9283, 642, 31.0269, 18, 4],
+ [4.9283, 663, 1.1941, 18, 4],
+ [4.9283, 705, 23.8659, 18, 4],
+ [4.9283, 726, 14.3188, 18, 4],
+ [4.9283, 768, 4.177, 18, 4],
+ [4.9283, 789, 7.7581, 18, 4],
+ [4.9283, 810, 13.1281, 18, 4],
+ [4.9283, 831, 37.5921, 18, 4],
+ [4.9283, 852, 1.79, 18, 4]
+ ]
+ }
+ },
+ "_hash": "6d305f413d4a0710c021a0ecbbd173af"
+}
diff --git a/web/src/lib/bones/strings-stringview--markdown-skeleton-fixture.bones.json b/web/src/lib/bones/strings-stringview--markdown-skeleton-fixture.bones.json
new file mode 100644
index 000000000..906441116
--- /dev/null
+++ b/web/src/lib/bones/strings-stringview--markdown-skeleton-fixture.bones.json
@@ -0,0 +1,209 @@
+{
+ "breakpoints": {
+ "375": {
+ "name": "strings-stringview--markdown-skeleton-fixture",
+ "viewportWidth": 343,
+ "width": 343,
+ "height": 1205,
+ "bones": [
+ [4.9563, 10, 4.25, 24, 999],
+ [12.0, 14, 25.0, 16, 4],
+ [4.9563, 40, 50.0, 14, 4],
+ [0, 50, 100, 1155, 4, true, 15, true],
+ [4.9563, 67, 47.6995, 34, 4, true, 15, true],
+ [8.7464, 75, 14.5089, 17, 4],
+ [27.059, 72, 21.8067, 24, 0],
+ [84.416, 76, 10.6277, 15, 4],
+ [4.9563, 113, 90.0875, 20, 4],
+ [4.9563, 149, 19.5335, 34, 4, true, 15, true],
+ [4.9563, 150, 9.9125, 32, "4px 0px 0px 4px"],
+ [14.5773, 150, 9.9125, 32, "0px 4px 4px 0px"],
+ [56.2682, 149, 38.7755, 34, 4, true, 15, true],
+ [56.2682, 150, 9.9125, 32, "4px 0px 0px 4px"],
+ [65.8892, 150, 9.9125, 32, 0],
+ [75.5102, 150, 9.9125, 32, 0],
+ [85.1312, 150, 9.9125, 32, "0px 4px 4px 0px"],
+ [4.9563, 199, 90.0875, 1, 0],
+ [4.9563, 228, 34.2247, 29, 4],
+ [4.9563, 278, 35.7963, 17, 4],
+ [4.9563, 310, 85.2679, 17, 4],
+ [4.9563, 345, 33.3637, 24, 4],
+ [4.9563, 391, 75.4054, 17, 4],
+ [4.9563, 415, 76.4623, 17, 4],
+ [4.9563, 439, 17.4016, 17, 4],
+ [11.9534, 471, 55.6122, 17, 4],
+ [11.9534, 499, 79.3914, 17, 4],
+ [11.9534, 527, 65.707, 17, 4],
+ [11.9534, 555, 45.604, 17, 4],
+ [4.9563, 587, 23.6516, 17, 4],
+ [4.9563, 622, 21.3648, 24, 4],
+ [4.9563, 668, 84.4616, 17, 4],
+ [4.9563, 692, 86.1516, 17, 4],
+ [4.9563, 716, 65.7844, 17, 4],
+ [4.9563, 751, 30.3207, 24, 4],
+ [4.9563, 797, 89.0716, 17, 4],
+ [4.9563, 821, 83.2817, 17, 4],
+ [4.9563, 845, 13.9668, 17, 4],
+ [4.9563, 880, 41.527, 24, 4],
+ [4.9563, 926, 83.4548, 17, 4],
+ [4.9563, 950, 76.6536, 17, 4],
+ [4.9563, 974, 68.6316, 17, 4],
+ [74.754, 974, 2.6922, 17, 4],
+ [4.9563, 1006, 84.1017, 17, 4],
+ [4.9563, 1030, 43.8912, 17, 4],
+ [50.0137, 1030, 41.937, 17, 4],
+ [4.9563, 1054, 19.611, 17, 4],
+ [4.9563, 1089, 22.5128, 24, 4],
+ [4.9563, 1135, 83.5687, 17, 4],
+ [4.9563, 1159, 20.8546, 17, 4]
+ ]
+ },
+ "768": {
+ "name": "strings-stringview--markdown-skeleton-fixture",
+ "viewportWidth": 736,
+ "width": 736,
+ "height": 944,
+ "bones": [
+ [4.9563, 10, 4.25, 24, 999],
+ [12.0, 14, 25.0, 16, 4],
+ [4.9563, 40, 50.0, 14, 4],
+ [0, 50, 100, 894, 4, true, 15, true],
+ [2.3098, 67, 22.2295, 34, 4, true, 15, true],
+ [4.0761, 75, 6.7616, 17, 4],
+ [12.6104, 72, 10.1626, 24, 0],
+ [26.1698, 76, 4.9529, 15, 4],
+ [49.0808, 74, 18.1747, 20, 4],
+ [69.4293, 67, 9.1033, 34, 4, true, 15, true],
+ [69.4293, 68, 4.6196, 32, "4px 0px 0px 4px"],
+ [73.913, 68, 4.6196, 32, "0px 4px 4px 0px"],
+ [79.6196, 67, 18.0707, 34, 4, true, 15, true],
+ [79.6196, 68, 4.6196, 32, "4px 0px 0px 4px"],
+ [84.1033, 68, 4.6196, 32, 0],
+ [88.587, 68, 4.6196, 32, 0],
+ [93.0707, 68, 4.6196, 32, "0px 4px 4px 0px"],
+ [2.3098, 117, 95.3804, 21, 4],
+ [2.3098, 154, 95.3804, 1, 0],
+ [2.3098, 183, 15.9498, 29, 4],
+ [2.3098, 233, 16.6822, 17, 4],
+ [2.3098, 265, 39.7376, 17, 4],
+ [2.3098, 300, 15.5486, 24, 4],
+ [2.3098, 346, 79.904, 17, 4],
+ [5.5707, 378, 25.9171, 17, 4],
+ [5.5707, 406, 36.999, 17, 4],
+ [5.5707, 434, 30.6216, 17, 4],
+ [5.5707, 462, 21.253, 17, 4],
+ [2.3098, 494, 11.0224, 17, 4],
+ [2.3098, 529, 9.9567, 24, 4],
+ [2.3098, 575, 91.5103, 17, 4],
+ [2.3098, 599, 19.1682, 17, 4],
+ [2.3098, 634, 14.1304, 24, 4],
+ [2.3098, 680, 87.8524, 17, 4],
+ [2.3098, 715, 19.3529, 24, 4],
+ [2.3098, 761, 95.0344, 17, 4],
+ [2.3098, 785, 12.0797, 17, 4],
+ [14.9329, 785, 1.2547, 17, 4],
+ [2.3098, 817, 60.1584, 17, 4],
+ [63.0116, 817, 29.1928, 17, 4],
+ [2.3098, 852, 10.4917, 24, 4],
+ [2.3098, 898, 49.1742, 17, 4]
+ ]
+ },
+ "1024": {
+ "name": "strings-stringview--markdown-skeleton-fixture",
+ "viewportWidth": 992,
+ "width": 992,
+ "height": 896,
+ "bones": [
+ [4.9563, 10, 4.25, 24, 999],
+ [12.0, 14, 25.0, 16, 4],
+ [4.9563, 40, 50.0, 14, 4],
+ [0, 50, 100, 846, 4, true, 15, true],
+ [1.7137, 67, 16.4929, 34, 4, true, 15, true],
+ [3.0242, 75, 5.0167, 17, 4],
+ [9.3561, 72, 7.54, 24, 0],
+ [19.4163, 76, 3.6747, 15, 4],
+ [62.2212, 74, 13.4844, 20, 4],
+ [77.3185, 67, 6.754, 34, 4, true, 15, true],
+ [77.3185, 68, 3.4274, 32, "4px 0px 0px 4px"],
+ [80.6452, 68, 3.4274, 32, "0px 4px 4px 0px"],
+ [84.879, 67, 13.4073, 34, 4, true, 15, true],
+ [84.879, 68, 3.4274, 32, "4px 0px 0px 4px"],
+ [88.2056, 68, 3.4274, 32, 0],
+ [91.5323, 68, 3.4274, 32, 0],
+ [94.8589, 68, 3.4274, 32, "0px 4px 4px 0px"],
+ [1.7137, 117, 96.5726, 21, 4],
+ [1.7137, 154, 96.5726, 1, 0],
+ [1.7137, 183, 11.8337, 29, 4],
+ [1.7137, 233, 12.3771, 17, 4],
+ [1.7137, 265, 29.4827, 17, 4],
+ [1.7137, 300, 11.536, 24, 4],
+ [1.7137, 346, 59.2836, 17, 4],
+ [4.1331, 378, 19.2288, 17, 4],
+ [4.1331, 406, 27.4509, 17, 4],
+ [4.1331, 434, 22.7193, 17, 4],
+ [4.1331, 462, 15.7683, 17, 4],
+ [1.7137, 494, 8.1779, 17, 4],
+ [1.7137, 529, 7.3872, 24, 4],
+ [1.7137, 575, 82.4943, 17, 4],
+ [1.7137, 610, 10.4839, 24, 4],
+ [1.7137, 656, 65.1808, 17, 4],
+ [1.7137, 691, 14.3586, 24, 4],
+ [1.7137, 737, 79.8482, 17, 4],
+ [81.9651, 737, 0.9309, 17, 4],
+ [1.7137, 769, 44.6336, 17, 4],
+ [46.7506, 769, 21.6592, 17, 4],
+ [1.7137, 804, 7.7841, 24, 4],
+ [1.7137, 850, 36.4841, 17, 4]
+ ]
+ },
+ "1440": {
+ "name": "strings-stringview--markdown-skeleton-fixture",
+ "viewportWidth": 1408,
+ "width": 1408,
+ "height": 896,
+ "bones": [
+ [4.9563, 10, 4.25, 24, 999],
+ [12.0, 14, 25.0, 16, 4],
+ [4.9563, 40, 50.0, 14, 4],
+ [0, 50, 100, 846, 4, true, 15, true],
+ [1.2074, 67, 11.62, 34, 4, true, 15, true],
+ [2.1307, 75, 3.5345, 17, 4],
+ [6.5918, 72, 5.3123, 24, 0],
+ [13.6796, 76, 2.589, 15, 4],
+ [73.3831, 74, 9.5004, 20, 4],
+ [84.0199, 67, 4.7585, 34, 4, true, 15, true],
+ [84.0199, 68, 2.4148, 32, "4px 0px 0px 4px"],
+ [86.3636, 68, 2.4148, 32, "0px 4px 4px 0px"],
+ [89.3466, 67, 9.446, 34, 4, true, 15, true],
+ [89.3466, 68, 2.4148, 32, "4px 0px 0px 4px"],
+ [91.6903, 68, 2.4148, 32, 0],
+ [94.0341, 68, 2.4148, 32, 0],
+ [96.3778, 68, 2.4148, 32, "0px 4px 4px 0px"],
+ [1.2074, 117, 97.5852, 21, 4],
+ [1.2074, 154, 97.5852, 1, 0],
+ [1.2074, 183, 8.3374, 29, 4],
+ [1.2074, 233, 8.7203, 17, 4],
+ [1.2074, 265, 20.7719, 17, 4],
+ [1.2074, 300, 8.1277, 24, 4],
+ [1.2074, 346, 41.768, 17, 4],
+ [2.9119, 378, 13.5476, 17, 4],
+ [2.9119, 406, 19.3404, 17, 4],
+ [2.9119, 434, 16.0067, 17, 4],
+ [2.9119, 462, 11.1095, 17, 4],
+ [1.2074, 494, 5.7617, 17, 4],
+ [1.2074, 529, 5.2046, 24, 4],
+ [1.2074, 575, 58.121, 17, 4],
+ [1.2074, 610, 7.3864, 24, 4],
+ [1.2074, 656, 45.9229, 17, 4],
+ [1.2074, 691, 10.1163, 24, 4],
+ [1.2074, 737, 56.2567, 17, 4],
+ [57.7481, 737, 0.6559, 17, 4],
+ [1.2074, 769, 31.4464, 17, 4],
+ [32.9379, 769, 15.2599, 17, 4],
+ [1.2074, 804, 5.4843, 24, 4],
+ [1.2074, 850, 25.7047, 17, 4]
+ ]
+ }
+ },
+ "_hash": "5871f2ecc4af645e5c6f54e02ecc37bc"
+}
diff --git a/web/src/lib/components/comment/CommentSection.svelte b/web/src/lib/components/comment/CommentSection.svelte
new file mode 100644
index 000000000..12dc3fe58
--- /dev/null
+++ b/web/src/lib/components/comment/CommentSection.svelte
@@ -0,0 +1,67 @@
+
+
+
+
+
+ {#if auth.currentDid}
+
+ {:else}
+
+ {/if}
+
diff --git a/web/src/lib/components/comment/comments.ts b/web/src/lib/components/comment/comments.ts
index 525f3069d..dbd5ee42c 100644
--- a/web/src/lib/components/comment/comments.ts
+++ b/web/src/lib/components/comment/comments.ts
@@ -1,4 +1,11 @@
-import type { CommentRecord } from "$lib/api/comment";
+import type { BobbinContext } from "$lib/api/client";
+import type { CommentListPage, CommentRecord } from "$lib/api/comment";
+import { COMMENT_AUTHOR_DOCS } from "$lib/api/descriptors";
+import { authorOf, enrich, target, type Sidecar } from "$lib/api/enrich";
+import { INVALID_HANDLE } from "$lib/api/identity";
+import { orNull } from "$lib/api/load";
+import { rkeyFromUri } from "$lib/api/uri";
+import { renderMarkup, type MarkupContext } from "$lib/markup";
import type { CommitSummary } from "$lib/api/repo";
import type { ReactionGroup } from "$lib/components/reaction/reactions";
@@ -38,7 +45,7 @@ export function deletedComment(uri: string, createdAt: string, cid?: string): Co
return {
uri,
cid,
- rkey: uri.split("/").pop() ?? "",
+ rkey: rkeyFromUri(uri),
authorDid: "",
authorHandle: "",
createdAt,
@@ -135,3 +142,99 @@ export function filterCommentsByCommit(
return !!commit && filter(commit);
});
}
+
+export const toThreadInputs = async (
+ items: CommentListPage["items"],
+ docs: Sidecar,
+ markupCtx: MarkupContext,
+ extra?: (uri: string) => Partial
+): Promise =>
+ Promise.all(
+ items.map(async (item): Promise => {
+ const author = authorOf(docs, item.uri, COMMENT_AUTHOR_DOCS);
+ const body = item.value.body?.text ?? "";
+ return {
+ comment: {
+ uri: item.uri,
+ cid: item.cid,
+ rkey: rkeyFromUri(item.uri),
+ authorDid: author.did,
+ authorHandle: author.handle ?? INVALID_HANDLE,
+ createdAt: item.value.createdAt,
+ body,
+ bodyHtml: body ? await orNull(renderMarkup(body, markupCtx)) : null,
+ ...extra?.(item.uri)
+ },
+ replyTo: item.value.replyTo?.uri ?? null,
+ replyToCid: item.value.replyTo?.cid
+ };
+ })
+ );
+
+export interface CommentItems {
+ items: CommentListPage["items"];
+ docs: Sidecar;
+}
+
+export const listCommentItems = async (
+ ctx: BobbinContext,
+ subjectUri: string
+): Promise => {
+ const result = await orNull(
+ enrich(ctx, {
+ xrpc: "sh.tangled.feed.listComments",
+ params: { subject: subjectUri, order: "asc", limit: 100 },
+ enrich: [target(COMMENT_AUTHOR_DOCS, ["items[].uri"])]
+ })
+ );
+
+ return { items: result?.output?.items ?? [], docs: result?.data ?? {} };
+};
+
+export const loadCommentThreads = async (
+ ctx: BobbinContext,
+ subjectUri: string,
+ markupCtx: MarkupContext,
+ extra?: (uri: string) => Partial
+): Promise => {
+ const { items, docs } = await listCommentItems(ctx, subjectUri);
+ return buildCommentThreads(await toThreadInputs(items, docs, markupCtx, extra));
+};
+
+export const insertComment = (
+ threads: CommentThread[],
+ { comment, replyTo }: ThreadInput
+): CommentThread[] => {
+ if (replyTo === null) {
+ return [...threads, { self: comment, replies: [] }];
+ }
+ return threads.map((thread) =>
+ thread.self.uri === replyTo ? { ...thread, replies: [...thread.replies, comment] } : thread
+ );
+};
+
+export const updateComment = (
+ threads: CommentThread[],
+ { comment }: ThreadInput
+): CommentThread[] => {
+ return threads.map((thread) => ({
+ self: thread.self.uri === comment.uri ? comment : thread.self,
+ replies: thread.replies.map((reply) => (reply.uri === comment.uri ? comment : reply))
+ }));
+};
+
+export const removeComment = (threads: CommentThread[], uri: string): CommentThread[] => {
+ return threads
+ .map((thread) => {
+ if (thread.self.uri === uri) {
+ return thread.replies.length
+ ? {
+ ...thread,
+ self: { ...thread.self, body: "", bodyHtml: null, deleted: true }
+ }
+ : null;
+ }
+ return { ...thread, replies: thread.replies.filter((r) => r.uri !== uri) };
+ })
+ .filter((t): t is CommentThread => t !== null);
+};
diff --git a/web/src/lib/components/editor/PierreEditor.svelte b/web/src/lib/components/editor/PierreEditor.svelte
new file mode 100644
index 000000000..e09761289
--- /dev/null
+++ b/web/src/lib/components/editor/PierreEditor.svelte
@@ -0,0 +1,106 @@
+
+
+
+
+
diff --git a/web/src/lib/components/profile/StringCardContent.svelte b/web/src/lib/components/profile/StringCardContent.svelte
index 918dc829f..dab6f6992 100644
--- a/web/src/lib/components/profile/StringCardContent.svelte
+++ b/web/src/lib/components/profile/StringCardContent.svelte
@@ -1,5 +1,5 @@
@@ -196,7 +202,13 @@ here.
/>
-
+
diff --git a/web/src/lib/components/repo/BlobView.svelte b/web/src/lib/components/repo/BlobView.svelte
index 631defced..dad6f0688 100644
--- a/web/src/lib/components/repo/BlobView.svelte
+++ b/web/src/lib/components/repo/BlobView.svelte
@@ -1,28 +1,40 @@
+
+
+ {@render children()}
+
diff --git a/web/src/lib/components/repo/PierreFile.svelte b/web/src/lib/components/repo/PierreFile.svelte
index f21843c66..6048acf8b 100644
--- a/web/src/lib/components/repo/PierreFile.svelte
+++ b/web/src/lib/components/repo/PierreFile.svelte
@@ -15,7 +15,7 @@
// server-prerendered shadow dom, only set for the initial ssr
prerenderedHTML?: string;
// forwarded to pierre, fires after every (async-safe) render pass
- onPostRender?: FileOptions["onPostRender"];
+ onPostRender?: FileOptions["onPostRender"];
// one run per stretch of lines sharing a commit, absent when blame is off
blame?: Blame;
}
@@ -27,7 +27,7 @@
const hover = new BlameHover(() => wrapper);
- const options = (forWrap: boolean): FileOptions => ({
+ const options = (forWrap: boolean): FileOptions => ({
...pierreFileOptions(forWrap),
onPostRender,
onLineEnter: ({ lineNumber, lineElement, event }) => {
diff --git a/web/src/lib/components/repo/RepoCardContent.svelte b/web/src/lib/components/repo/RepoCardContent.svelte
index e5d5af119..12dab2356 100644
--- a/web/src/lib/components/repo/RepoCardContent.svelte
+++ b/web/src/lib/components/repo/RepoCardContent.svelte
@@ -46,9 +46,12 @@
{#if starButton}
import { resolve } from "$app/paths";
+ import FilePathChip from "$lib/components/repo/FilePathChip.svelte";
import { encodePathSegments } from "$lib/components/repo/urls";
interface Props {
@@ -27,9 +28,7 @@
});
-
+
{#each crumbs as crumb (crumb.href)}
{#if crumb.last}
{crumb.name}
@@ -41,4 +40,4 @@
/
{/if}
{/each}
-
+
diff --git a/web/src/lib/components/repo/RepoHeader.svelte b/web/src/lib/components/repo/RepoHeader.svelte
index 0e7c4d054..0150ebff8 100644
--- a/web/src/lib/components/repo/RepoHeader.svelte
+++ b/web/src/lib/components/repo/RepoHeader.svelte
@@ -30,11 +30,15 @@
{#snippet repoActions(className: string)}