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)}
-
diff --git a/web/src/lib/components/repo/pierre.ts b/web/src/lib/components/repo/pierre.ts index c50b9909e..8e0042294 100644 --- a/web/src/lib/components/repo/pierre.ts +++ b/web/src/lib/components/repo/pierre.ts @@ -13,12 +13,8 @@ import type { WorkerPoolManager } from "@pierre/diffs/worker"; // pierre's two diff layouts, threaded from the ?diff= url param down to the // FileDiff instances -export type DiffStyle = NonNullable["diffStyle"]>; +export type DiffStyle = NonNullable["diffStyle"]>; -// pierre's theming hook is a registered shiki css-variable theme. the token -// colors come from --diffs-token-* properties in pierre.css, they flip -// with the media query. the name is pinned by app.css's @utility or tailwind -// tree-shakes those rules away const PIERRE_THEME_NAME = "tangled"; registerCustomCSSVariableTheme(PIERRE_THEME_NAME, { @@ -73,12 +69,25 @@ export const pierreCodeViewOptions = (style: DiffStyle) => ({ unsafeCSS: diffShadowCSS }); -export const pierreFileOptions = (wrap: boolean): FileOptions => ({ +export const pierreFileOptions = (wrap: boolean): FileOptions => ({ ...PIERRE_BASE_OPTIONS, disableFileHeader: true, overflow: wrap ? "wrap" : "scroll" }); +export const prerenderFile = async ( + name: string, + contents: string | null +): Promise => { + if (browser || contents === null) return undefined; + const { preloadFile } = await import("@pierre/diffs/ssr"); + const { prerenderedHTML } = await preloadFile({ + file: { name, contents }, + options: pierreFileOptions(false) + }); + return prerenderedHTML; +}; + // lazy dynamic imports: pierre's worker bundle trips vite's dev dependency optimizer let workerPool: WorkerPoolManager | undefined; let workerPoolFailed = false; diff --git a/web/src/lib/components/strings/StringForm.stories.svelte b/web/src/lib/components/strings/StringForm.stories.svelte index bc3c68485..9676cb27b 100644 --- a/web/src/lib/components/strings/StringForm.stories.svelte +++ b/web/src/lib/components/strings/StringForm.stories.svelte @@ -14,6 +14,7 @@ filename: "hello.rs", description: "a tiny snippet", content: 'fn main() {\n\tprintln!("hello");\n}', + bobbinUrl: "https://bobbin.tangled.local", onsaved: noop, oncancel: noop } diff --git a/web/src/lib/components/strings/StringForm.svelte b/web/src/lib/components/strings/StringForm.svelte index 10b1223bf..5f9193274 100644 --- a/web/src/lib/components/strings/StringForm.svelte +++ b/web/src/lib/components/strings/StringForm.svelte @@ -6,25 +6,30 @@ import Card from "$lib/components/ui/Card.svelte"; import Input from "$lib/components/ui/Input.svelte"; import Spinner from "$lib/components/ui/Spinner.svelte"; - import Textarea from "$lib/components/ui/Textarea.svelte"; import ErrorAlert from "$lib/components/ui/Error.svelte"; import Separator from "$lib/components/ui/Separator.svelte"; import ArrowUp from "$icon/arrow-up"; + import Check from "$icon/check"; import X from "$icon/x"; import { getAuth } from "$lib/auth.svelte"; import AccountSelector from "$lib/components/auth/AccountSelector.svelte"; + import { contentStats } from "$lib/api/blob"; + import { createBobbinClient } from "$lib/api/client"; import { putString } from "$lib/api/strings"; import type { StringRecord } from "$lib/api/records"; import { createAction } from "$lib/action.svelte"; import Field from "$lib/components/ui/Field.svelte"; + import PierreEditor from "$lib/components/editor/PierreEditor.svelte"; interface Props { mode?: "create" | "edit"; rkey?: string; + swapRecord?: string; createdAt?: string; filename?: string; description?: string; content?: string; + bobbinUrl: string; onsaved: (rkey: string, handle: string) => void; oncancel?: () => void; } @@ -32,10 +37,12 @@ let { mode = "create", rkey, + swapRecord, createdAt, filename: initialFilename = "", description: initialDescription = "", content: initialContent = "", + bobbinUrl, onsaved, oncancel }: Props = $props(); @@ -54,30 +61,41 @@ let filename = $state(untrack(() => initialFilename)); let description = $state(untrack(() => initialDescription)); let content = $state(untrack(() => initialContent)); + let lastCid = $state(untrack(() => swapRecord)); + let lastRkey = $state(untrack(() => rkey)); - const lineCount = $derived(content === "" ? 0 : content.split("\n").length); - const byteCount = $derived(new TextEncoder().encode(content).length); + const stats = $derived(contentStats(content)); const publish = createAction(async (event: SubmitEvent) => { event.preventDefault(); if (!auth?.currentDid) return; + if (filename.trim() === "") throw new Error("Filename is required"); + if (content.length === 0) throw new Error("Content is required"); const agent = await auth.agentFor(acting.did as Did); - const targetRkey = rkey ?? tidNow(); + const targetRkey = lastRkey ?? tidNow(); const record: StringRecord = { $type: "sh.tangled.string", - filename, - description, + filename: filename.trim(), + description: description.trim(), contents: content, createdAt: createdAt ?? new Date().toISOString() }; - await putString(agent, targetRkey, record); + const written = await putString( + agent, + createBobbinClient({ serviceUrl: bobbinUrl }), + targetRkey, + record, + lastCid + ); + lastRkey = targetRkey; + lastCid = written.cid; onsaved(targetRkey, acting.handle); });
-
+
-