This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

web: add the repo index page

Signed-off-by: dawn <dawn@tangled.org>

author
dawn
date (Jul 31, 2026, 10:54 PM +0300) commit 1de57f9a parent 7282ec14 change-id yvvqlxww
+5853 -67
+22
web/.storybook/StoryAuthProvider.svelte
··· 1 + <script lang="ts"> 2 + import { setContext, untrack } from "svelte"; 3 + import type { Snippet } from "svelte"; 4 + import { AUTH_KEY, createAuth } from "$lib/auth.svelte"; 5 + 6 + interface Props { 7 + initial?: { did: string; handle: string } | null; 8 + children: Snippet; 9 + } 10 + 11 + let { initial = null, children }: Props = $props(); 12 + 13 + setContext( 14 + AUTH_KEY, 15 + createAuth( 16 + "https://bobbin.example.test", 17 + untrack(() => initial) 18 + ) 19 + ); 20 + </script> 21 + 22 + {@render children()}
+1 -1
web/eslint.config.js
··· 29 29 files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"], 30 30 languageOptions: { 31 31 parserOptions: { 32 - projectService: true, 32 + projectService: { allowDefaultProject: [".storybook/StoryAuthProvider.svelte"] }, 33 33 extraFileExtensions: [".svelte"], 34 34 parser: ts.parser 35 35 }
+2
web/lex.config.ts
··· 19 19 "../lexicons/label/**/*.json", 20 20 "../lexicons/markup/**/*.json", 21 21 "../lexicons/pipeline/**/*.json", 22 + "../lexicons/publicKey/**/*.json", 22 23 "../lexicons/pulls/**/*.json", 23 24 "../lexicons/query/**/*.json", 24 25 "../lexicons/repo/**/*.json", 26 + "../lexicons/search/**/*.json", 25 27 "../lexicons/spindle/**/*.json", 26 28 "../lexicons/string/**/*.json", 27 29 "../lexicons/sync/**/*.json",
+9 -1
web/src/lib/api/count.ts
··· 30 30 | "sh.tangled.repo.countArtifactsBy" 31 31 | "sh.tangled.repo.countCollaborators" 32 32 | "sh.tangled.repo.countCollaboratorsBy" 33 + | "sh.tangled.repo.countForks" 33 34 | "sh.tangled.repo.countIssues" 34 35 | "sh.tangled.repo.countIssuesBy" 35 36 | "sh.tangled.repo.countPulls" ··· 49 50 distinctAuthors: number; 50 51 } 51 52 53 + export interface CountFilter { 54 + author?: string; 55 + state?: "open" | "closed"; 56 + status?: "open" | "closed" | "merged"; 57 + } 58 + 52 59 export const count = ( 53 60 ctx: BobbinContext, 54 61 name: CountName, 55 62 subject: string, 63 + filter: CountFilter = {}, 56 64 init?: XrpcRequestInit 57 - ): Promise<CountResult> => jsonGet<CountResult>(ctx, name, { subject }, init); 65 + ): Promise<CountResult> => jsonGet<CountResult>(ctx, name, { subject, ...filter }, init);
+19
web/src/lib/api/graph.ts
··· 8 8 import { jsonGet } from "./_request"; 9 9 import { httpStatusFor } from "./load"; 10 10 import { rkeyFromUri } from "./uri"; 11 + import type * as ShTangledFeedGetStar from "./lexicons/types/sh/tangled/feed/getStar"; 11 12 import type * as ShTangledGraphFollow from "./lexicons/types/sh/tangled/graph/follow"; 12 13 import type * as ShTangledGraphGetFollow from "./lexicons/types/sh/tangled/graph/getFollow"; 13 14 import type * as ShTangledGraphVouch from "./lexicons/types/sh/tangled/graph/vouch"; ··· 30 31 "sh.tangled.graph.getFollow", 31 32 { actor: actor as Did, subject: subject as Did } satisfies ShTangledGraphGetFollow.$params 32 33 ); 34 + return rkeyFromUri(uri); 35 + } catch (cause) { 36 + if (cause instanceof ClientResponseError && httpStatusFor(cause) === 404) return null; 37 + throw cause; 38 + } 39 + }; 40 + 41 + // 404 means no star exists, other errors propagate 42 + export const getStarRkey = async ( 43 + ctx: BobbinContext, 44 + actor: string, 45 + subject: string 46 + ): Promise<string | null> => { 47 + try { 48 + const { uri } = await jsonGet<ShTangledFeedGetStar.$output>(ctx, "sh.tangled.feed.getStar", { 49 + actor: actor as Did, 50 + subject 51 + } satisfies ShTangledFeedGetStar.$params); 33 52 return rkeyFromUri(uri); 34 53 } catch (cause) { 35 54 if (cause instanceof ClientResponseError && httpStatusFor(cause) === 404) return null;
+1
web/src/lib/api/index.ts
··· 9 9 export * from "./load"; 10 10 export * from "./uri"; 11 11 export * from "./graph"; 12 + export * from "./repo";
+54
web/src/lib/api/lexicons/index.ts
··· 1 + export * as ShTangledActorGetProfile from "./types/sh/tangled/actor/getProfile.js"; 2 + export * as ShTangledActorGetProfiles from "./types/sh/tangled/actor/getProfiles.js"; 1 3 export * as ShTangledActorProfile from "./types/sh/tangled/actor/profile.js"; 2 4 export * as ShTangledCiCancelPipeline from "./types/sh/tangled/ci/cancelPipeline.js"; 3 5 export * as ShTangledCiGetPipeline from "./types/sh/tangled/ci/getPipeline.js"; ··· 7 9 export * as ShTangledCiTrigger from "./types/sh/tangled/ci/trigger.js"; 8 10 export * as ShTangledCiTriggerPipeline from "./types/sh/tangled/ci/triggerPipeline.js"; 9 11 export * as ShTangledFeedComment from "./types/sh/tangled/feed/comment.js"; 12 + export * as ShTangledFeedCountComments from "./types/sh/tangled/feed/countComments.js"; 13 + export * as ShTangledFeedCountCommentsBy from "./types/sh/tangled/feed/countCommentsBy.js"; 14 + export * as ShTangledFeedCountReactions from "./types/sh/tangled/feed/countReactions.js"; 15 + export * as ShTangledFeedCountReactionsBy from "./types/sh/tangled/feed/countReactionsBy.js"; 16 + export * as ShTangledFeedCountStars from "./types/sh/tangled/feed/countStars.js"; 17 + export * as ShTangledFeedCountStarsBy from "./types/sh/tangled/feed/countStarsBy.js"; 10 18 export * as ShTangledFeedGetStar from "./types/sh/tangled/feed/getStar.js"; 11 19 export * as ShTangledFeedListComments from "./types/sh/tangled/feed/listComments.js"; 12 20 export * as ShTangledFeedListCommentsBy from "./types/sh/tangled/feed/listCommentsBy.js"; ··· 16 24 export * as ShTangledFeedListStarsBy from "./types/sh/tangled/feed/listStarsBy.js"; 17 25 export * as ShTangledFeedReaction from "./types/sh/tangled/feed/reaction.js"; 18 26 export * as ShTangledFeedStar from "./types/sh/tangled/feed/star.js"; 27 + export * as ShTangledGitCountRefUpdates from "./types/sh/tangled/git/countRefUpdates.js"; 28 + export * as ShTangledGitCountRefUpdatesBy from "./types/sh/tangled/git/countRefUpdatesBy.js"; 19 29 export * as ShTangledGitListRefUpdates from "./types/sh/tangled/git/listRefUpdates.js"; 20 30 export * as ShTangledGitListRefUpdatesBy from "./types/sh/tangled/git/listRefUpdatesBy.js"; 21 31 export * as ShTangledGitListRefs from "./types/sh/tangled/git/listRefs.js"; ··· 35 45 export * as ShTangledGitTempListCommits from "./types/sh/tangled/git/temp/listCommits.js"; 36 46 export * as ShTangledGitTempListLanguages from "./types/sh/tangled/git/temp/listLanguages.js"; 37 47 export * as ShTangledGitTempListTags from "./types/sh/tangled/git/temp/listTags.js"; 48 + export * as ShTangledGraphCountFollows from "./types/sh/tangled/graph/countFollows.js"; 49 + export * as ShTangledGraphCountFollowsBy from "./types/sh/tangled/graph/countFollowsBy.js"; 50 + export * as ShTangledGraphCountVouches from "./types/sh/tangled/graph/countVouches.js"; 51 + export * as ShTangledGraphCountVouchesBy from "./types/sh/tangled/graph/countVouchesBy.js"; 38 52 export * as ShTangledGraphFollow from "./types/sh/tangled/graph/follow.js"; 39 53 export * as ShTangledGraphGetFollow from "./types/sh/tangled/graph/getFollow.js"; 40 54 export * as ShTangledGraphListFollows from "./types/sh/tangled/graph/listFollows.js"; ··· 44 58 export * as ShTangledGraphVouch from "./types/sh/tangled/graph/vouch.js"; 45 59 export * as ShTangledKnot from "./types/sh/tangled/knot.js"; 46 60 export * as ShTangledKnotAddMember from "./types/sh/tangled/knot/addMember.js"; 61 + export * as ShTangledKnotCountKnots from "./types/sh/tangled/knot/countKnots.js"; 62 + export * as ShTangledKnotCountMembers from "./types/sh/tangled/knot/countMembers.js"; 63 + export * as ShTangledKnotCountMembersBy from "./types/sh/tangled/knot/countMembersBy.js"; 47 64 export * as ShTangledKnotListKeys from "./types/sh/tangled/knot/listKeys.js"; 48 65 export * as ShTangledKnotListKnots from "./types/sh/tangled/knot/listKnots.js"; 49 66 export * as ShTangledKnotListMembers from "./types/sh/tangled/knot/listMembers.js"; ··· 52 69 export * as ShTangledKnotRemoveMember from "./types/sh/tangled/knot/removeMember.js"; 53 70 export * as ShTangledKnotSubscribeRepos from "./types/sh/tangled/knot/subscribeRepos.js"; 54 71 export * as ShTangledKnotVersion from "./types/sh/tangled/knot/version.js"; 72 + export * as ShTangledLabelCountDefinitions from "./types/sh/tangled/label/countDefinitions.js"; 73 + export * as ShTangledLabelCountOps from "./types/sh/tangled/label/countOps.js"; 74 + export * as ShTangledLabelCountOpsBy from "./types/sh/tangled/label/countOpsBy.js"; 55 75 export * as ShTangledLabelDefinition from "./types/sh/tangled/label/definition.js"; 56 76 export * as ShTangledLabelListDefinitions from "./types/sh/tangled/label/listDefinitions.js"; 57 77 export * as ShTangledLabelListOps from "./types/sh/tangled/label/listOps.js"; ··· 61 81 export * as ShTangledOwner from "./types/sh/tangled/owner.js"; 62 82 export * as ShTangledPipeline from "./types/sh/tangled/pipeline.js"; 63 83 export * as ShTangledPipelineCancelPipeline from "./types/sh/tangled/pipeline/cancelPipeline.js"; 84 + export * as ShTangledPipelineCountPipelines from "./types/sh/tangled/pipeline/countPipelines.js"; 85 + export * as ShTangledPipelineCountPipelinesBy from "./types/sh/tangled/pipeline/countPipelinesBy.js"; 86 + export * as ShTangledPipelineCountStatuses from "./types/sh/tangled/pipeline/countStatuses.js"; 87 + export * as ShTangledPipelineCountStatusesBy from "./types/sh/tangled/pipeline/countStatusesBy.js"; 64 88 export * as ShTangledPipelineListPipelines from "./types/sh/tangled/pipeline/listPipelines.js"; 65 89 export * as ShTangledPipelineListPipelinesBy from "./types/sh/tangled/pipeline/listPipelinesBy.js"; 66 90 export * as ShTangledPipelineListStatuses from "./types/sh/tangled/pipeline/listStatuses.js"; 67 91 export * as ShTangledPipelineListStatusesBy from "./types/sh/tangled/pipeline/listStatusesBy.js"; 68 92 export * as ShTangledPipelineStatus from "./types/sh/tangled/pipeline/status.js"; 69 93 export * as ShTangledPublicKey from "./types/sh/tangled/publicKey.js"; 94 + export * as ShTangledPublicKeyCountKeys from "./types/sh/tangled/publicKey/countKeys.js"; 95 + export * as ShTangledPublicKeyGetPublicKey from "./types/sh/tangled/publicKey/getPublicKey.js"; 70 96 export * as ShTangledPublicKeyListKeys from "./types/sh/tangled/publicKey/listKeys.js"; 71 97 export * as ShTangledQueryEnrichResponse from "./types/sh/tangled/query/enrichResponse.js"; 72 98 export * as ShTangledRepo from "./types/sh/tangled/repo.js"; ··· 79 105 export * as ShTangledRepoBranches from "./types/sh/tangled/repo/branches.js"; 80 106 export * as ShTangledRepoCollaborator from "./types/sh/tangled/repo/collaborator.js"; 81 107 export * as ShTangledRepoCompare from "./types/sh/tangled/repo/compare.js"; 108 + export * as ShTangledRepoCountArtifacts from "./types/sh/tangled/repo/countArtifacts.js"; 109 + export * as ShTangledRepoCountArtifactsBy from "./types/sh/tangled/repo/countArtifactsBy.js"; 110 + export * as ShTangledRepoCountCollaborators from "./types/sh/tangled/repo/countCollaborators.js"; 111 + export * as ShTangledRepoCountCollaboratorsBy from "./types/sh/tangled/repo/countCollaboratorsBy.js"; 112 + export * as ShTangledRepoCountForks from "./types/sh/tangled/repo/countForks.js"; 113 + export * as ShTangledRepoCountIssues from "./types/sh/tangled/repo/countIssues.js"; 114 + export * as ShTangledRepoCountIssuesBy from "./types/sh/tangled/repo/countIssuesBy.js"; 115 + export * as ShTangledRepoCountPulls from "./types/sh/tangled/repo/countPulls.js"; 116 + export * as ShTangledRepoCountPullsBy from "./types/sh/tangled/repo/countPullsBy.js"; 117 + export * as ShTangledRepoCountRepos from "./types/sh/tangled/repo/countRepos.js"; 82 118 export * as ShTangledRepoCreate from "./types/sh/tangled/repo/create.js"; 83 119 export * as ShTangledRepoDelete from "./types/sh/tangled/repo/delete.js"; 84 120 export * as ShTangledRepoDeleteBranch from "./types/sh/tangled/repo/deleteBranch.js"; ··· 87 123 export * as ShTangledRepoForkStatus from "./types/sh/tangled/repo/forkStatus.js"; 88 124 export * as ShTangledRepoForkSync from "./types/sh/tangled/repo/forkSync.js"; 89 125 export * as ShTangledRepoGetDefaultBranch from "./types/sh/tangled/repo/getDefaultBranch.js"; 126 + export * as ShTangledRepoGetIssue from "./types/sh/tangled/repo/getIssue.js"; 127 + export * as ShTangledRepoGetIssues from "./types/sh/tangled/repo/getIssues.js"; 128 + export * as ShTangledRepoGetPull from "./types/sh/tangled/repo/getPull.js"; 129 + export * as ShTangledRepoGetPulls from "./types/sh/tangled/repo/getPulls.js"; 130 + export * as ShTangledRepoGetRepo from "./types/sh/tangled/repo/getRepo.js"; 131 + export * as ShTangledRepoGetRepoByName from "./types/sh/tangled/repo/getRepoByName.js"; 132 + export * as ShTangledRepoGetRepoByRepoDid from "./types/sh/tangled/repo/getRepoByRepoDid.js"; 133 + export * as ShTangledRepoGetRepos from "./types/sh/tangled/repo/getRepos.js"; 90 134 export * as ShTangledRepoGetReposByRepoDids from "./types/sh/tangled/repo/getReposByRepoDids.js"; 91 135 export * as ShTangledRepoHiddenRef from "./types/sh/tangled/repo/hiddenRef.js"; 92 136 export * as ShTangledRepoIssue from "./types/sh/tangled/repo/issue.js"; 93 137 export * as ShTangledRepoIssueComment from "./types/sh/tangled/repo/issue/comment.js"; 138 + export * as ShTangledRepoIssueCountStates from "./types/sh/tangled/repo/issue/countStates.js"; 139 + export * as ShTangledRepoIssueCountStatesBy from "./types/sh/tangled/repo/issue/countStatesBy.js"; 94 140 export * as ShTangledRepoIssueListStates from "./types/sh/tangled/repo/issue/listStates.js"; 95 141 export * as ShTangledRepoIssueListStatesBy from "./types/sh/tangled/repo/issue/listStatesBy.js"; 96 142 export * as ShTangledRepoIssueState from "./types/sh/tangled/repo/issue/state.js"; ··· 112 158 export * as ShTangledRepoMergeCheck from "./types/sh/tangled/repo/mergeCheck.js"; 113 159 export * as ShTangledRepoPull from "./types/sh/tangled/repo/pull.js"; 114 160 export * as ShTangledRepoPullComment from "./types/sh/tangled/repo/pull/comment.js"; 161 + export * as ShTangledRepoPullCountStatuses from "./types/sh/tangled/repo/pull/countStatuses.js"; 162 + export * as ShTangledRepoPullCountStatusesBy from "./types/sh/tangled/repo/pull/countStatusesBy.js"; 115 163 export * as ShTangledRepoPullListStatuses from "./types/sh/tangled/repo/pull/listStatuses.js"; 116 164 export * as ShTangledRepoPullListStatusesBy from "./types/sh/tangled/repo/pull/listStatusesBy.js"; 117 165 export * as ShTangledRepoPullStatus from "./types/sh/tangled/repo/pull/status.js"; ··· 124 172 export * as ShTangledRepoTag from "./types/sh/tangled/repo/tag.js"; 125 173 export * as ShTangledRepoTags from "./types/sh/tangled/repo/tags.js"; 126 174 export * as ShTangledRepoTree from "./types/sh/tangled/repo/tree.js"; 175 + export * as ShTangledSearchQuery from "./types/sh/tangled/search/query.js"; 127 176 export * as ShTangledSpindle from "./types/sh/tangled/spindle.js"; 177 + export * as ShTangledSpindleCountMembers from "./types/sh/tangled/spindle/countMembers.js"; 178 + export * as ShTangledSpindleCountMembersBy from "./types/sh/tangled/spindle/countMembersBy.js"; 179 + export * as ShTangledSpindleCountSpindles from "./types/sh/tangled/spindle/countSpindles.js"; 128 180 export * as ShTangledSpindleListMembers from "./types/sh/tangled/spindle/listMembers.js"; 129 181 export * as ShTangledSpindleListMembersBy from "./types/sh/tangled/spindle/listMembersBy.js"; 130 182 export * as ShTangledSpindleListSpindles from "./types/sh/tangled/spindle/listSpindles.js"; 131 183 export * as ShTangledSpindleMember from "./types/sh/tangled/spindle/member.js"; 132 184 export * as ShTangledString from "./types/sh/tangled/string.js"; 185 + export * as ShTangledStringCountStrings from "./types/sh/tangled/string/countStrings.js"; 186 + export * as ShTangledStringGetString from "./types/sh/tangled/string/getString.js"; 133 187 export * as ShTangledStringListStrings from "./types/sh/tangled/string/listStrings.js"; 134 188 export * as ShTangledSyncListRepos from "./types/sh/tangled/sync/listRepos.js"; 135 189 export * as ShTangledSyncRequestCrawl from "./types/sh/tangled/sync/requestCrawl.js";
+38
web/src/lib/api/lexicons/types/sh/tangled/actor/getProfile.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.actor.getProfile", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * AT-URI of the sh.tangled.actor.profile record to fetch. 9 + */ 10 + actor: /*#__PURE__*/ v.resourceUriString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.cidString()), 16 + uri: /*#__PURE__*/ v.resourceUriString(), 17 + /** 18 + * Embedded sh.tangled.actor.profile record. 19 + */ 20 + value: /*#__PURE__*/ v.unknown(), 21 + }), 22 + }, 23 + }); 24 + 25 + type main$schematype = typeof _mainSchema; 26 + 27 + export interface mainSchema extends main$schematype {} 28 + 29 + export const mainSchema = _mainSchema as mainSchema; 30 + 31 + export interface $params extends v.InferInput<mainSchema["params"]> {} 32 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 33 + 34 + declare module "@atcute/lexicons/ambient" { 35 + interface XRPCQueries { 36 + "sh.tangled.actor.getProfile": mainSchema; 37 + } 38 + }
+56
web/src/lib/api/lexicons/types/sh/tangled/actor/getProfiles.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.actor.getProfiles", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * AT-URIs of the sh.tangled.actor.profile records to fetch. At most 50 per request. 9 + * @minLength 1 10 + * @maxLength 50 11 + */ 12 + actors: /*#__PURE__*/ v.constrain( 13 + /*#__PURE__*/ v.array(/*#__PURE__*/ v.resourceUriString()), 14 + [/*#__PURE__*/ v.arrayLength(1, 50)], 15 + ), 16 + }), 17 + output: { 18 + type: "lex", 19 + schema: /*#__PURE__*/ v.object({ 20 + get items() { 21 + return /*#__PURE__*/ v.array(recordViewSchema); 22 + }, 23 + }), 24 + }, 25 + }); 26 + const _recordViewSchema = /*#__PURE__*/ v.object({ 27 + $type: /*#__PURE__*/ v.optional( 28 + /*#__PURE__*/ v.literal("sh.tangled.actor.getProfiles#recordView"), 29 + ), 30 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.cidString()), 31 + uri: /*#__PURE__*/ v.resourceUriString(), 32 + /** 33 + * Embedded sh.tangled.actor.profile record. 34 + */ 35 + value: /*#__PURE__*/ v.unknown(), 36 + }); 37 + 38 + type main$schematype = typeof _mainSchema; 39 + type recordView$schematype = typeof _recordViewSchema; 40 + 41 + export interface mainSchema extends main$schematype {} 42 + export interface recordViewSchema extends recordView$schematype {} 43 + 44 + export const mainSchema = _mainSchema as mainSchema; 45 + export const recordViewSchema = _recordViewSchema as recordViewSchema; 46 + 47 + export interface RecordView extends v.InferInput<typeof recordViewSchema> {} 48 + 49 + export interface $params extends v.InferInput<mainSchema["params"]> {} 50 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 51 + 52 + declare module "@atcute/lexicons/ambient" { 53 + interface XRPCQueries { 54 + "sh.tangled.actor.getProfiles": mainSchema; 55 + } 56 + }
+8
web/src/lib/api/lexicons/types/sh/tangled/ci/queryPipelines.ts
··· 16 16 */ 17 17 cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 18 18 /** 19 + * Filter pipelines by trigger kind. When provided, pipelines matching any listed kind are returned; when omitted, every kind is returned. 20 + */ 21 + kinds: /*#__PURE__*/ v.optional( 22 + /*#__PURE__*/ v.array( 23 + /*#__PURE__*/ v.literalEnum(["manual", "pull_request", "push"]), 24 + ), 25 + ), 26 + /** 19 27 * Maximum number of pipelines to return 20 28 * @minimum 1 21 29 * @maximum 250
+42
web/src/lib/api/lexicons/types/sh/tangled/feed/countComments.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.feed.countComments", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Record AT-URI the comments attach to. 9 + */ 10 + subject: /*#__PURE__*/ v.resourceUriString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.feed.countComments": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/feed/countCommentsBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.feed.countCommentsBy", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Actor DID whose comment authorings to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.feed.countCommentsBy": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/feed/countReactions.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.feed.countReactions", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Record AT-URI the reactions target. 9 + */ 10 + subject: /*#__PURE__*/ v.resourceUriString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.feed.countReactions": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/feed/countReactionsBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.feed.countReactionsBy", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Actor DID whose reaction authorings to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.feed.countReactionsBy": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/feed/countStars.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.feed.countStars", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Repo DID to list star edges for. 9 + */ 10 + subject: /*#__PURE__*/ v.string(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.feed.countStars": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/feed/countStarsBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.feed.countStarsBy", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Actor DID whose star authorings to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.feed.countStarsBy": mainSchema; 41 + } 42 + }
+1 -1
web/src/lib/api/lexicons/types/sh/tangled/feed/listComments.ts
··· 39 39 "desc", 40 40 ), 41 41 /** 42 - * Record AT-URI the comments are attached to. 42 + * Record AT-URI the comments attach to. 43 43 */ 44 44 subject: /*#__PURE__*/ v.resourceUriString(), 45 45 }),
+42
web/src/lib/api/lexicons/types/sh/tangled/git/countRefUpdates.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.git.countRefUpdates", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Repo DID whose ref-update records to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.git.countRefUpdates": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/git/countRefUpdatesBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.git.countRefUpdatesBy", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Actor DID whose ref-update authorings to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.git.countRefUpdatesBy": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/graph/countFollows.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.graph.countFollows", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Followee DID whose inbound follows to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.graph.countFollows": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/graph/countFollowsBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.graph.countFollowsBy", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Actor DID whose follow authorings to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.graph.countFollowsBy": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/graph/countVouches.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.graph.countVouches", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Vouchee DID whose inbound vouches to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.graph.countVouches": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/graph/countVouchesBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.graph.countVouchesBy", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Actor DID whose vouch authorings to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.graph.countVouchesBy": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/knot/countKnots.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.knot.countKnots", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Owner DID whose knot records to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.knot.countKnots": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/knot/countMembers.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.knot.countMembers", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Knot identifier whose member records to list. 9 + */ 10 + subject: /*#__PURE__*/ v.string(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.knot.countMembers": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/knot/countMembersBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.knot.countMembersBy", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Actor DID whose knot-member authorings to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.knot.countMembersBy": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/label/countDefinitions.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.label.countDefinitions", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Scope identifier whose label definitions to list. 9 + */ 10 + subject: /*#__PURE__*/ v.string(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.label.countDefinitions": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/label/countOps.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.label.countOps", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Scope identifier whose label op records to list. 9 + */ 10 + subject: /*#__PURE__*/ v.string(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.label.countOps": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/label/countOpsBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.label.countOpsBy", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Actor DID whose label-op authorings to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.label.countOpsBy": mainSchema; 41 + } 42 + }
+45
web/src/lib/api/lexicons/types/sh/tangled/pipeline/countPipelines.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query( 6 + "sh.tangled.pipeline.countPipelines", 7 + { 8 + params: /*#__PURE__*/ v.object({ 9 + /** 10 + * Repo or spindle identifier whose pipeline records to list. 11 + */ 12 + subject: /*#__PURE__*/ v.string(), 13 + }), 14 + output: { 15 + type: "lex", 16 + schema: /*#__PURE__*/ v.object({ 17 + /** 18 + * Total number of matching records. 19 + * @minimum 0 20 + */ 21 + count: /*#__PURE__*/ v.integer(), 22 + /** 23 + * Number of distinct authors among the matching records. 24 + * @minimum 0 25 + */ 26 + distinctAuthors: /*#__PURE__*/ v.integer(), 27 + }), 28 + }, 29 + }, 30 + ); 31 + 32 + type main$schematype = typeof _mainSchema; 33 + 34 + export interface mainSchema extends main$schematype {} 35 + 36 + export const mainSchema = _mainSchema as mainSchema; 37 + 38 + export interface $params extends v.InferInput<mainSchema["params"]> {} 39 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 40 + 41 + declare module "@atcute/lexicons/ambient" { 42 + interface XRPCQueries { 43 + "sh.tangled.pipeline.countPipelines": mainSchema; 44 + } 45 + }
+45
web/src/lib/api/lexicons/types/sh/tangled/pipeline/countPipelinesBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query( 6 + "sh.tangled.pipeline.countPipelinesBy", 7 + { 8 + params: /*#__PURE__*/ v.object({ 9 + /** 10 + * Actor DID whose pipeline authorings to list. 11 + */ 12 + subject: /*#__PURE__*/ v.didString(), 13 + }), 14 + output: { 15 + type: "lex", 16 + schema: /*#__PURE__*/ v.object({ 17 + /** 18 + * Total number of matching records. 19 + * @minimum 0 20 + */ 21 + count: /*#__PURE__*/ v.integer(), 22 + /** 23 + * Number of distinct authors among the matching records. 24 + * @minimum 0 25 + */ 26 + distinctAuthors: /*#__PURE__*/ v.integer(), 27 + }), 28 + }, 29 + }, 30 + ); 31 + 32 + type main$schematype = typeof _mainSchema; 33 + 34 + export interface mainSchema extends main$schematype {} 35 + 36 + export const mainSchema = _mainSchema as mainSchema; 37 + 38 + export interface $params extends v.InferInput<mainSchema["params"]> {} 39 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 40 + 41 + declare module "@atcute/lexicons/ambient" { 42 + interface XRPCQueries { 43 + "sh.tangled.pipeline.countPipelinesBy": mainSchema; 44 + } 45 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/pipeline/countStatuses.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.pipeline.countStatuses", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Pipeline AT-URI whose status records to list. 9 + */ 10 + subject: /*#__PURE__*/ v.resourceUriString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.pipeline.countStatuses": mainSchema; 41 + } 42 + }
+45
web/src/lib/api/lexicons/types/sh/tangled/pipeline/countStatusesBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query( 6 + "sh.tangled.pipeline.countStatusesBy", 7 + { 8 + params: /*#__PURE__*/ v.object({ 9 + /** 10 + * Actor DID whose pipeline-status authorings to list. 11 + */ 12 + subject: /*#__PURE__*/ v.didString(), 13 + }), 14 + output: { 15 + type: "lex", 16 + schema: /*#__PURE__*/ v.object({ 17 + /** 18 + * Total number of matching records. 19 + * @minimum 0 20 + */ 21 + count: /*#__PURE__*/ v.integer(), 22 + /** 23 + * Number of distinct authors among the matching records. 24 + * @minimum 0 25 + */ 26 + distinctAuthors: /*#__PURE__*/ v.integer(), 27 + }), 28 + }, 29 + }, 30 + ); 31 + 32 + type main$schematype = typeof _mainSchema; 33 + 34 + export interface mainSchema extends main$schematype {} 35 + 36 + export const mainSchema = _mainSchema as mainSchema; 37 + 38 + export interface $params extends v.InferInput<mainSchema["params"]> {} 39 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 40 + 41 + declare module "@atcute/lexicons/ambient" { 42 + interface XRPCQueries { 43 + "sh.tangled.pipeline.countStatusesBy": mainSchema; 44 + } 45 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/publicKey/countKeys.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.publicKey.countKeys", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Owner DID whose public-key records to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.publicKey.countKeys": mainSchema; 41 + } 42 + }
+38
web/src/lib/api/lexicons/types/sh/tangled/publicKey/getPublicKey.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.publicKey.getPublicKey", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * AT-URI of the sh.tangled.publicKey record to fetch. 9 + */ 10 + publicKey: /*#__PURE__*/ v.resourceUriString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.cidString()), 16 + uri: /*#__PURE__*/ v.resourceUriString(), 17 + /** 18 + * Embedded sh.tangled.publicKey record. 19 + */ 20 + value: /*#__PURE__*/ v.unknown(), 21 + }), 22 + }, 23 + }); 24 + 25 + type main$schematype = typeof _mainSchema; 26 + 27 + export interface mainSchema extends main$schematype {} 28 + 29 + export const mainSchema = _mainSchema as mainSchema; 30 + 31 + export interface $params extends v.InferInput<mainSchema["params"]> {} 32 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 33 + 34 + declare module "@atcute/lexicons/ambient" { 35 + interface XRPCQueries { 36 + "sh.tangled.publicKey.getPublicKey": mainSchema; 37 + } 38 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/repo/countArtifacts.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.countArtifacts", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Repo or release identifier whose artifacts to list. 9 + */ 10 + subject: /*#__PURE__*/ v.string(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.repo.countArtifacts": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/repo/countArtifactsBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.countArtifactsBy", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Actor DID whose artifact authorings to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.repo.countArtifactsBy": mainSchema; 41 + } 42 + }
+45
web/src/lib/api/lexicons/types/sh/tangled/repo/countCollaborators.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query( 6 + "sh.tangled.repo.countCollaborators", 7 + { 8 + params: /*#__PURE__*/ v.object({ 9 + /** 10 + * Repo DID whose collaborator records to list. 11 + */ 12 + subject: /*#__PURE__*/ v.didString(), 13 + }), 14 + output: { 15 + type: "lex", 16 + schema: /*#__PURE__*/ v.object({ 17 + /** 18 + * Total number of matching records. 19 + * @minimum 0 20 + */ 21 + count: /*#__PURE__*/ v.integer(), 22 + /** 23 + * Number of distinct authors among the matching records. 24 + * @minimum 0 25 + */ 26 + distinctAuthors: /*#__PURE__*/ v.integer(), 27 + }), 28 + }, 29 + }, 30 + ); 31 + 32 + type main$schematype = typeof _mainSchema; 33 + 34 + export interface mainSchema extends main$schematype {} 35 + 36 + export const mainSchema = _mainSchema as mainSchema; 37 + 38 + export interface $params extends v.InferInput<mainSchema["params"]> {} 39 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 40 + 41 + declare module "@atcute/lexicons/ambient" { 42 + interface XRPCQueries { 43 + "sh.tangled.repo.countCollaborators": mainSchema; 44 + } 45 + }
+45
web/src/lib/api/lexicons/types/sh/tangled/repo/countCollaboratorsBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query( 6 + "sh.tangled.repo.countCollaboratorsBy", 7 + { 8 + params: /*#__PURE__*/ v.object({ 9 + /** 10 + * Actor DID whose collaborator authorings to list. 11 + */ 12 + subject: /*#__PURE__*/ v.didString(), 13 + }), 14 + output: { 15 + type: "lex", 16 + schema: /*#__PURE__*/ v.object({ 17 + /** 18 + * Total number of matching records. 19 + * @minimum 0 20 + */ 21 + count: /*#__PURE__*/ v.integer(), 22 + /** 23 + * Number of distinct authors among the matching records. 24 + * @minimum 0 25 + */ 26 + distinctAuthors: /*#__PURE__*/ v.integer(), 27 + }), 28 + }, 29 + }, 30 + ); 31 + 32 + type main$schematype = typeof _mainSchema; 33 + 34 + export interface mainSchema extends main$schematype {} 35 + 36 + export const mainSchema = _mainSchema as mainSchema; 37 + 38 + export interface $params extends v.InferInput<mainSchema["params"]> {} 39 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 40 + 41 + declare module "@atcute/lexicons/ambient" { 42 + interface XRPCQueries { 43 + "sh.tangled.repo.countCollaboratorsBy": mainSchema; 44 + } 45 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/repo/countForks.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.countForks", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Repo DID to count forks of. Repos that never got a DID cannot be counted. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.repo.countForks": mainSchema; 41 + } 42 + }
+52
web/src/lib/api/lexicons/types/sh/tangled/repo/countIssues.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.countIssues", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Restrict to issues authored by this user DID. 9 + */ 10 + author: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.didString()), 11 + /** 12 + * Restrict to issues whose latest derived state matches. 13 + */ 14 + state: /*#__PURE__*/ v.optional( 15 + /*#__PURE__*/ v.string<"closed" | "open" | (string & {})>(), 16 + ), 17 + /** 18 + * Repo DID to list issues for 19 + */ 20 + subject: /*#__PURE__*/ v.didString(), 21 + }), 22 + output: { 23 + type: "lex", 24 + schema: /*#__PURE__*/ v.object({ 25 + /** 26 + * Total number of matching records. 27 + * @minimum 0 28 + */ 29 + count: /*#__PURE__*/ v.integer(), 30 + /** 31 + * Number of distinct authors among the matching records. 32 + * @minimum 0 33 + */ 34 + distinctAuthors: /*#__PURE__*/ v.integer(), 35 + }), 36 + }, 37 + }); 38 + 39 + type main$schematype = typeof _mainSchema; 40 + 41 + export interface mainSchema extends main$schematype {} 42 + 43 + export const mainSchema = _mainSchema as mainSchema; 44 + 45 + export interface $params extends v.InferInput<mainSchema["params"]> {} 46 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 47 + 48 + declare module "@atcute/lexicons/ambient" { 49 + interface XRPCQueries { 50 + "sh.tangled.repo.countIssues": mainSchema; 51 + } 52 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/repo/countIssuesBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.countIssuesBy", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Actor DID whose issue authorings to list 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.repo.countIssuesBy": mainSchema; 41 + } 42 + }
+52
web/src/lib/api/lexicons/types/sh/tangled/repo/countPulls.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.countPulls", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Restrict to pulls authored by this user DID. 9 + */ 10 + author: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.didString()), 11 + /** 12 + * Restrict to pulls whose latest derived status matches. 13 + */ 14 + status: /*#__PURE__*/ v.optional( 15 + /*#__PURE__*/ v.string<"closed" | "merged" | "open" | (string & {})>(), 16 + ), 17 + /** 18 + * Repo DID to list pulls for 19 + */ 20 + subject: /*#__PURE__*/ v.didString(), 21 + }), 22 + output: { 23 + type: "lex", 24 + schema: /*#__PURE__*/ v.object({ 25 + /** 26 + * Total number of matching records. 27 + * @minimum 0 28 + */ 29 + count: /*#__PURE__*/ v.integer(), 30 + /** 31 + * Number of distinct authors among the matching records. 32 + * @minimum 0 33 + */ 34 + distinctAuthors: /*#__PURE__*/ v.integer(), 35 + }), 36 + }, 37 + }); 38 + 39 + type main$schematype = typeof _mainSchema; 40 + 41 + export interface mainSchema extends main$schematype {} 42 + 43 + export const mainSchema = _mainSchema as mainSchema; 44 + 45 + export interface $params extends v.InferInput<mainSchema["params"]> {} 46 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 47 + 48 + declare module "@atcute/lexicons/ambient" { 49 + interface XRPCQueries { 50 + "sh.tangled.repo.countPulls": mainSchema; 51 + } 52 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/repo/countPullsBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.countPullsBy", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Actor DID whose pull authorings to list 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.repo.countPullsBy": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/repo/countRepos.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.countRepos", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Owner DID whose repo records to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.repo.countRepos": mainSchema; 41 + } 42 + }
+4
web/src/lib/api/lexicons/types/sh/tangled/repo/forkSync.ts
··· 20 20 */ 21 21 name: /*#__PURE__*/ v.string(), 22 22 /** 23 + * DID of the repository 24 + */ 25 + repo: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.didString()), 26 + /** 23 27 * AT-URI of the source repository 24 28 */ 25 29 source: /*#__PURE__*/ v.resourceUriString(),
+38
web/src/lib/api/lexicons/types/sh/tangled/repo/getIssue.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.getIssue", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * AT-URI of the sh.tangled.repo.issue record to fetch. 9 + */ 10 + issue: /*#__PURE__*/ v.resourceUriString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.cidString()), 16 + uri: /*#__PURE__*/ v.resourceUriString(), 17 + /** 18 + * Embedded sh.tangled.repo.issue record. 19 + */ 20 + value: /*#__PURE__*/ v.unknown(), 21 + }), 22 + }, 23 + }); 24 + 25 + type main$schematype = typeof _mainSchema; 26 + 27 + export interface mainSchema extends main$schematype {} 28 + 29 + export const mainSchema = _mainSchema as mainSchema; 30 + 31 + export interface $params extends v.InferInput<mainSchema["params"]> {} 32 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 33 + 34 + declare module "@atcute/lexicons/ambient" { 35 + interface XRPCQueries { 36 + "sh.tangled.repo.getIssue": mainSchema; 37 + } 38 + }
+56
web/src/lib/api/lexicons/types/sh/tangled/repo/getIssues.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.getIssues", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * AT-URIs of the sh.tangled.repo.issue records to fetch. At most 50 per request. 9 + * @minLength 1 10 + * @maxLength 50 11 + */ 12 + issues: /*#__PURE__*/ v.constrain( 13 + /*#__PURE__*/ v.array(/*#__PURE__*/ v.resourceUriString()), 14 + [/*#__PURE__*/ v.arrayLength(1, 50)], 15 + ), 16 + }), 17 + output: { 18 + type: "lex", 19 + schema: /*#__PURE__*/ v.object({ 20 + get items() { 21 + return /*#__PURE__*/ v.array(recordViewSchema); 22 + }, 23 + }), 24 + }, 25 + }); 26 + const _recordViewSchema = /*#__PURE__*/ v.object({ 27 + $type: /*#__PURE__*/ v.optional( 28 + /*#__PURE__*/ v.literal("sh.tangled.repo.getIssues#recordView"), 29 + ), 30 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.cidString()), 31 + uri: /*#__PURE__*/ v.resourceUriString(), 32 + /** 33 + * Embedded sh.tangled.repo.issue record. 34 + */ 35 + value: /*#__PURE__*/ v.unknown(), 36 + }); 37 + 38 + type main$schematype = typeof _mainSchema; 39 + type recordView$schematype = typeof _recordViewSchema; 40 + 41 + export interface mainSchema extends main$schematype {} 42 + export interface recordViewSchema extends recordView$schematype {} 43 + 44 + export const mainSchema = _mainSchema as mainSchema; 45 + export const recordViewSchema = _recordViewSchema as recordViewSchema; 46 + 47 + export interface RecordView extends v.InferInput<typeof recordViewSchema> {} 48 + 49 + export interface $params extends v.InferInput<mainSchema["params"]> {} 50 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 51 + 52 + declare module "@atcute/lexicons/ambient" { 53 + interface XRPCQueries { 54 + "sh.tangled.repo.getIssues": mainSchema; 55 + } 56 + }
+38
web/src/lib/api/lexicons/types/sh/tangled/repo/getPull.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.getPull", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * AT-URI of the sh.tangled.repo.pull record to fetch. 9 + */ 10 + pull: /*#__PURE__*/ v.resourceUriString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.cidString()), 16 + uri: /*#__PURE__*/ v.resourceUriString(), 17 + /** 18 + * Embedded sh.tangled.repo.pull record. 19 + */ 20 + value: /*#__PURE__*/ v.unknown(), 21 + }), 22 + }, 23 + }); 24 + 25 + type main$schematype = typeof _mainSchema; 26 + 27 + export interface mainSchema extends main$schematype {} 28 + 29 + export const mainSchema = _mainSchema as mainSchema; 30 + 31 + export interface $params extends v.InferInput<mainSchema["params"]> {} 32 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 33 + 34 + declare module "@atcute/lexicons/ambient" { 35 + interface XRPCQueries { 36 + "sh.tangled.repo.getPull": mainSchema; 37 + } 38 + }
+56
web/src/lib/api/lexicons/types/sh/tangled/repo/getPulls.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.getPulls", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * AT-URIs of the sh.tangled.repo.pull records to fetch. At most 50 per request. 9 + * @minLength 1 10 + * @maxLength 50 11 + */ 12 + pulls: /*#__PURE__*/ v.constrain( 13 + /*#__PURE__*/ v.array(/*#__PURE__*/ v.resourceUriString()), 14 + [/*#__PURE__*/ v.arrayLength(1, 50)], 15 + ), 16 + }), 17 + output: { 18 + type: "lex", 19 + schema: /*#__PURE__*/ v.object({ 20 + get items() { 21 + return /*#__PURE__*/ v.array(recordViewSchema); 22 + }, 23 + }), 24 + }, 25 + }); 26 + const _recordViewSchema = /*#__PURE__*/ v.object({ 27 + $type: /*#__PURE__*/ v.optional( 28 + /*#__PURE__*/ v.literal("sh.tangled.repo.getPulls#recordView"), 29 + ), 30 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.cidString()), 31 + uri: /*#__PURE__*/ v.resourceUriString(), 32 + /** 33 + * Embedded sh.tangled.repo.pull record. 34 + */ 35 + value: /*#__PURE__*/ v.unknown(), 36 + }); 37 + 38 + type main$schematype = typeof _mainSchema; 39 + type recordView$schematype = typeof _recordViewSchema; 40 + 41 + export interface mainSchema extends main$schematype {} 42 + export interface recordViewSchema extends recordView$schematype {} 43 + 44 + export const mainSchema = _mainSchema as mainSchema; 45 + export const recordViewSchema = _recordViewSchema as recordViewSchema; 46 + 47 + export interface RecordView extends v.InferInput<typeof recordViewSchema> {} 48 + 49 + export interface $params extends v.InferInput<mainSchema["params"]> {} 50 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 51 + 52 + declare module "@atcute/lexicons/ambient" { 53 + interface XRPCQueries { 54 + "sh.tangled.repo.getPulls": mainSchema; 55 + } 56 + }
+38
web/src/lib/api/lexicons/types/sh/tangled/repo/getRepo.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.getRepo", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * AT-URI of the sh.tangled.repo record to fetch. 9 + */ 10 + repo: /*#__PURE__*/ v.resourceUriString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.cidString()), 16 + uri: /*#__PURE__*/ v.resourceUriString(), 17 + /** 18 + * Embedded sh.tangled.repo record. 19 + */ 20 + value: /*#__PURE__*/ v.unknown(), 21 + }), 22 + }, 23 + }); 24 + 25 + type main$schematype = typeof _mainSchema; 26 + 27 + export interface mainSchema extends main$schematype {} 28 + 29 + export const mainSchema = _mainSchema as mainSchema; 30 + 31 + export interface $params extends v.InferInput<mainSchema["params"]> {} 32 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 33 + 34 + declare module "@atcute/lexicons/ambient" { 35 + interface XRPCQueries { 36 + "sh.tangled.repo.getRepo": mainSchema; 37 + } 38 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/repo/getRepoByName.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.getRepoByName", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Name of the repo as it appears in its url. Repos without a name are reachable by their rkey. 9 + */ 10 + name: /*#__PURE__*/ v.string(), 11 + /** 12 + * DID of the account that owns the repo. 13 + */ 14 + owner: /*#__PURE__*/ v.didString(), 15 + }), 16 + output: { 17 + type: "lex", 18 + schema: /*#__PURE__*/ v.object({ 19 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.cidString()), 20 + uri: /*#__PURE__*/ v.resourceUriString(), 21 + /** 22 + * Embedded sh.tangled.repo record. 23 + */ 24 + value: /*#__PURE__*/ v.unknown(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.repo.getRepoByName": mainSchema; 41 + } 42 + }
+38
web/src/lib/api/lexicons/types/sh/tangled/repo/getRepoByRepoDid.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.getRepoByRepoDid", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Repo DID whose sh.tangled.repo record to fetch. 9 + */ 10 + repoDid: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.cidString()), 16 + uri: /*#__PURE__*/ v.resourceUriString(), 17 + /** 18 + * Embedded sh.tangled.repo record. 19 + */ 20 + value: /*#__PURE__*/ v.unknown(), 21 + }), 22 + }, 23 + }); 24 + 25 + type main$schematype = typeof _mainSchema; 26 + 27 + export interface mainSchema extends main$schematype {} 28 + 29 + export const mainSchema = _mainSchema as mainSchema; 30 + 31 + export interface $params extends v.InferInput<mainSchema["params"]> {} 32 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 33 + 34 + declare module "@atcute/lexicons/ambient" { 35 + interface XRPCQueries { 36 + "sh.tangled.repo.getRepoByRepoDid": mainSchema; 37 + } 38 + }
+56
web/src/lib/api/lexicons/types/sh/tangled/repo/getRepos.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.getRepos", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * AT-URIs of the sh.tangled.repo records to fetch. At most 50 per request. 9 + * @minLength 1 10 + * @maxLength 50 11 + */ 12 + repos: /*#__PURE__*/ v.constrain( 13 + /*#__PURE__*/ v.array(/*#__PURE__*/ v.resourceUriString()), 14 + [/*#__PURE__*/ v.arrayLength(1, 50)], 15 + ), 16 + }), 17 + output: { 18 + type: "lex", 19 + schema: /*#__PURE__*/ v.object({ 20 + get items() { 21 + return /*#__PURE__*/ v.array(recordViewSchema); 22 + }, 23 + }), 24 + }, 25 + }); 26 + const _recordViewSchema = /*#__PURE__*/ v.object({ 27 + $type: /*#__PURE__*/ v.optional( 28 + /*#__PURE__*/ v.literal("sh.tangled.repo.getRepos#recordView"), 29 + ), 30 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.cidString()), 31 + uri: /*#__PURE__*/ v.resourceUriString(), 32 + /** 33 + * Embedded sh.tangled.repo record. 34 + */ 35 + value: /*#__PURE__*/ v.unknown(), 36 + }); 37 + 38 + type main$schematype = typeof _mainSchema; 39 + type recordView$schematype = typeof _recordViewSchema; 40 + 41 + export interface mainSchema extends main$schematype {} 42 + export interface recordViewSchema extends recordView$schematype {} 43 + 44 + export const mainSchema = _mainSchema as mainSchema; 45 + export const recordViewSchema = _recordViewSchema as recordViewSchema; 46 + 47 + export interface RecordView extends v.InferInput<typeof recordViewSchema> {} 48 + 49 + export interface $params extends v.InferInput<mainSchema["params"]> {} 50 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 51 + 52 + declare module "@atcute/lexicons/ambient" { 53 + interface XRPCQueries { 54 + "sh.tangled.repo.getRepos": mainSchema; 55 + } 56 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/repo/issue/countStates.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.repo.issue.countStates", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Issue AT-URI whose state records to list. 9 + */ 10 + subject: /*#__PURE__*/ v.resourceUriString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.repo.issue.countStates": mainSchema; 41 + } 42 + }
+45
web/src/lib/api/lexicons/types/sh/tangled/repo/issue/countStatesBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query( 6 + "sh.tangled.repo.issue.countStatesBy", 7 + { 8 + params: /*#__PURE__*/ v.object({ 9 + /** 10 + * Actor DID whose issue-state authorings to list. 11 + */ 12 + subject: /*#__PURE__*/ v.didString(), 13 + }), 14 + output: { 15 + type: "lex", 16 + schema: /*#__PURE__*/ v.object({ 17 + /** 18 + * Total number of matching records. 19 + * @minimum 0 20 + */ 21 + count: /*#__PURE__*/ v.integer(), 22 + /** 23 + * Number of distinct authors among the matching records. 24 + * @minimum 0 25 + */ 26 + distinctAuthors: /*#__PURE__*/ v.integer(), 27 + }), 28 + }, 29 + }, 30 + ); 31 + 32 + type main$schematype = typeof _mainSchema; 33 + 34 + export interface mainSchema extends main$schematype {} 35 + 36 + export const mainSchema = _mainSchema as mainSchema; 37 + 38 + export interface $params extends v.InferInput<mainSchema["params"]> {} 39 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 40 + 41 + declare module "@atcute/lexicons/ambient" { 42 + interface XRPCQueries { 43 + "sh.tangled.repo.issue.countStatesBy": mainSchema; 44 + } 45 + }
+4
web/src/lib/api/lexicons/types/sh/tangled/repo/merge.ts
··· 39 39 * Patch content to merge 40 40 */ 41 41 patch: /*#__PURE__*/ v.string(), 42 + /** 43 + * DID of the repository 44 + */ 45 + repo: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.didString()), 42 46 }), 43 47 }, 44 48 output: null,
+4
web/src/lib/api/lexicons/types/sh/tangled/repo/mergeCheck.ts
··· 36 36 * Patch or pull request to check for merge conflicts 37 37 */ 38 38 patch: /*#__PURE__*/ v.string(), 39 + /** 40 + * DID of the repository 41 + */ 42 + repo: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.didString()), 39 43 }), 40 44 }, 41 45 output: {
+45
web/src/lib/api/lexicons/types/sh/tangled/repo/pull/countStatuses.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query( 6 + "sh.tangled.repo.pull.countStatuses", 7 + { 8 + params: /*#__PURE__*/ v.object({ 9 + /** 10 + * Pull AT-URI whose status records to list. 11 + */ 12 + subject: /*#__PURE__*/ v.resourceUriString(), 13 + }), 14 + output: { 15 + type: "lex", 16 + schema: /*#__PURE__*/ v.object({ 17 + /** 18 + * Total number of matching records. 19 + * @minimum 0 20 + */ 21 + count: /*#__PURE__*/ v.integer(), 22 + /** 23 + * Number of distinct authors among the matching records. 24 + * @minimum 0 25 + */ 26 + distinctAuthors: /*#__PURE__*/ v.integer(), 27 + }), 28 + }, 29 + }, 30 + ); 31 + 32 + type main$schematype = typeof _mainSchema; 33 + 34 + export interface mainSchema extends main$schematype {} 35 + 36 + export const mainSchema = _mainSchema as mainSchema; 37 + 38 + export interface $params extends v.InferInput<mainSchema["params"]> {} 39 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 40 + 41 + declare module "@atcute/lexicons/ambient" { 42 + interface XRPCQueries { 43 + "sh.tangled.repo.pull.countStatuses": mainSchema; 44 + } 45 + }
+45
web/src/lib/api/lexicons/types/sh/tangled/repo/pull/countStatusesBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query( 6 + "sh.tangled.repo.pull.countStatusesBy", 7 + { 8 + params: /*#__PURE__*/ v.object({ 9 + /** 10 + * Actor DID whose pull-status authorings to list. 11 + */ 12 + subject: /*#__PURE__*/ v.didString(), 13 + }), 14 + output: { 15 + type: "lex", 16 + schema: /*#__PURE__*/ v.object({ 17 + /** 18 + * Total number of matching records. 19 + * @minimum 0 20 + */ 21 + count: /*#__PURE__*/ v.integer(), 22 + /** 23 + * Number of distinct authors among the matching records. 24 + * @minimum 0 25 + */ 26 + distinctAuthors: /*#__PURE__*/ v.integer(), 27 + }), 28 + }, 29 + }, 30 + ); 31 + 32 + type main$schematype = typeof _mainSchema; 33 + 34 + export interface mainSchema extends main$schematype {} 35 + 36 + export const mainSchema = _mainSchema as mainSchema; 37 + 38 + export interface $params extends v.InferInput<mainSchema["params"]> {} 39 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 40 + 41 + declare module "@atcute/lexicons/ambient" { 42 + interface XRPCQueries { 43 + "sh.tangled.repo.pull.countStatusesBy": mainSchema; 44 + } 45 + }
+98
web/src/lib/api/lexicons/types/sh/tangled/search/query.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _hitSchema = /*#__PURE__*/ v.object({ 6 + $type: /*#__PURE__*/ v.optional( 7 + /*#__PURE__*/ v.literal("sh.tangled.search.query#hit"), 8 + ), 9 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.cidString()), 10 + /** 11 + * Collection of the matched record. 12 + */ 13 + nsid: /*#__PURE__*/ v.nsidString(), 14 + /** 15 + * Relevance score of the hit, a floating-point number where higher ranks first. 16 + */ 17 + score: /*#__PURE__*/ v.unknown(), 18 + uri: /*#__PURE__*/ v.resourceUriString(), 19 + /** 20 + * Embedded matched record. 21 + */ 22 + value: /*#__PURE__*/ v.unknown(), 23 + }); 24 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.search.query", { 25 + params: /*#__PURE__*/ v.object({ 26 + /** 27 + * Restrict to records authored by this DID. 28 + */ 29 + author: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.didString()), 30 + /** 31 + * Pagination cursor 32 + */ 33 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 34 + /** 35 + * @minimum 1 36 + * @maximum 1000 37 + * @default 50 38 + */ 39 + limit: /*#__PURE__*/ v.optional( 40 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.integer(), [ 41 + /*#__PURE__*/ v.integerRange(1, 1000), 42 + ]), 43 + 50, 44 + ), 45 + /** 46 + * Restrict to records of this collection. 47 + */ 48 + nsid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 49 + /** 50 + * Full-text search query. 51 + * @minLength 1 52 + */ 53 + q: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 54 + /*#__PURE__*/ v.stringLength(1), 55 + ]), 56 + /** 57 + * Restrict to records under this repo DID. 58 + */ 59 + repo: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.didString()), 60 + /** 61 + * Restrict to records created at or after this time. 62 + */ 63 + since: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 64 + /** 65 + * Restrict to records created at or before this time. 66 + */ 67 + until: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 68 + }), 69 + output: { 70 + type: "lex", 71 + schema: /*#__PURE__*/ v.object({ 72 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 73 + get hits() { 74 + return /*#__PURE__*/ v.array(hitSchema); 75 + }, 76 + }), 77 + }, 78 + }); 79 + 80 + type hit$schematype = typeof _hitSchema; 81 + type main$schematype = typeof _mainSchema; 82 + 83 + export interface hitSchema extends hit$schematype {} 84 + export interface mainSchema extends main$schematype {} 85 + 86 + export const hitSchema = _hitSchema as hitSchema; 87 + export const mainSchema = _mainSchema as mainSchema; 88 + 89 + export interface Hit extends v.InferInput<typeof hitSchema> {} 90 + 91 + export interface $params extends v.InferInput<mainSchema["params"]> {} 92 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 93 + 94 + declare module "@atcute/lexicons/ambient" { 95 + interface XRPCQueries { 96 + "sh.tangled.search.query": mainSchema; 97 + } 98 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/spindle/countMembers.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.spindle.countMembers", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Spindle identifier whose member records to list. 9 + */ 10 + subject: /*#__PURE__*/ v.string(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.spindle.countMembers": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/spindle/countMembersBy.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.spindle.countMembersBy", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Actor DID whose spindle-member authorings to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.spindle.countMembersBy": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/spindle/countSpindles.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.spindle.countSpindles", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Owner DID whose spindle records to list. 9 + */ 10 + subject: /*#__PURE__*/ v.didString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.spindle.countSpindles": mainSchema; 41 + } 42 + }
+42
web/src/lib/api/lexicons/types/sh/tangled/string/countStrings.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.string.countStrings", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Scope identifier whose string records to list. 9 + */ 10 + subject: /*#__PURE__*/ v.string(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + /** 16 + * Total number of matching records. 17 + * @minimum 0 18 + */ 19 + count: /*#__PURE__*/ v.integer(), 20 + /** 21 + * Number of distinct authors among the matching records. 22 + * @minimum 0 23 + */ 24 + distinctAuthors: /*#__PURE__*/ v.integer(), 25 + }), 26 + }, 27 + }); 28 + 29 + type main$schematype = typeof _mainSchema; 30 + 31 + export interface mainSchema extends main$schematype {} 32 + 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface $params extends v.InferInput<mainSchema["params"]> {} 36 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 37 + 38 + declare module "@atcute/lexicons/ambient" { 39 + interface XRPCQueries { 40 + "sh.tangled.string.countStrings": mainSchema; 41 + } 42 + }
+38
web/src/lib/api/lexicons/types/sh/tangled/string/getString.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query("sh.tangled.string.getString", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * AT-URI of the sh.tangled.string record to fetch. 9 + */ 10 + string: /*#__PURE__*/ v.resourceUriString(), 11 + }), 12 + output: { 13 + type: "lex", 14 + schema: /*#__PURE__*/ v.object({ 15 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.cidString()), 16 + uri: /*#__PURE__*/ v.resourceUriString(), 17 + /** 18 + * Embedded sh.tangled.string record. 19 + */ 20 + value: /*#__PURE__*/ v.unknown(), 21 + }), 22 + }, 23 + }); 24 + 25 + type main$schematype = typeof _mainSchema; 26 + 27 + export interface mainSchema extends main$schematype {} 28 + 29 + export const mainSchema = _mainSchema as mainSchema; 30 + 31 + export interface $params extends v.InferInput<mainSchema["params"]> {} 32 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 33 + 34 + declare module "@atcute/lexicons/ambient" { 35 + interface XRPCQueries { 36 + "sh.tangled.string.getString": mainSchema; 37 + } 38 + }
+7
web/src/lib/api/records.ts
··· 30 30 export const getRepoByRepoDid = (ctx: BobbinContext, repoDid: string, init?: XrpcRequestInit) => 31 31 jsonGet<RecordView<RepoRecord>>(ctx, "sh.tangled.repo.getRepoByRepoDid", { repoDid }, init); 32 32 33 + export const getRepoByName = ( 34 + ctx: BobbinContext, 35 + owner: string, 36 + name: string, 37 + init?: XrpcRequestInit 38 + ) => jsonGet<RecordView<RepoRecord>>(ctx, "sh.tangled.repo.getRepoByName", { owner, name }, init); 39 + 33 40 export const getProfile = (ctx: BobbinContext, did: string, init?: XrpcRequestInit) => 34 41 jsonGet<RecordView<ProfileRecord>>( 35 42 ctx,
+207
web/src/lib/api/repo.test.ts
··· 1 + import { describe, expect, it, vi } from "vitest"; 2 + import { 3 + repoNameOf, 4 + resolveRepoByName, 5 + sortTreeEntries, 6 + toBranchSummary, 7 + toCommitSummary, 8 + toTagSummary, 9 + toTreeEntrySummary, 10 + treeEntryKind, 11 + type BranchEntry, 12 + type LogCommit, 13 + type TagEntry, 14 + type TreeEntrySummary 15 + } from "./repo"; 16 + import { ClientResponseError, createBobbinClient, type BobbinContext } from "./client"; 17 + import type { RecordView, RepoRecord } from "./records"; 18 + 19 + const jsonResponse = (body: unknown): Response => 20 + new Response(JSON.stringify(body), { 21 + status: 200, 22 + headers: { "content-type": "application/json" } 23 + }); 24 + 25 + const makeCtx = (fetchMock: typeof globalThis.fetch): BobbinContext => 26 + createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock }); 27 + 28 + describe("repoNameOf", () => { 29 + const view = (uri: string, name?: string): RecordView<RepoRecord> => ({ 30 + uri: uri as RecordView<RepoRecord>["uri"], 31 + value: { $type: "sh.tangled.repo", createdAt: "", knot: "knot.test", name } 32 + }); 33 + 34 + it("prefers the record's cosmetic name over a tid rkey", () => { 35 + expect(repoNameOf(view("at://did:plc:o/sh.tangled.repo/3lzg6enurmo22", "infra"))).toBe("infra"); 36 + }); 37 + 38 + it("falls back to the rkey for unnamed records", () => { 39 + expect(repoNameOf(view("at://did:plc:o/sh.tangled.repo/infra"))).toBe("infra"); 40 + }); 41 + }); 42 + 43 + describe("treeEntryKind", () => { 44 + it("maps git file modes to entry kinds", () => { 45 + expect(treeEntryKind("0040000")).toBe("directory"); 46 + expect(treeEntryKind("040000")).toBe("directory"); 47 + expect(treeEntryKind("0100644")).toBe("file"); 48 + expect(treeEntryKind("0100755")).toBe("file"); 49 + expect(treeEntryKind("0120000")).toBe("symlink"); 50 + expect(treeEntryKind("0160000")).toBe("submodule"); 51 + }); 52 + }); 53 + 54 + describe("sortTreeEntries", () => { 55 + it("puts directories and submodules before files, then sorts by name", () => { 56 + const entry = (name: string, kind: TreeEntrySummary["kind"]): TreeEntrySummary => ({ 57 + name, 58 + kind, 59 + size: 0 60 + }); 61 + const sorted = sortTreeEntries([ 62 + entry("readme.md", "file"), 63 + entry("src", "directory"), 64 + entry(".gitignore", "file"), 65 + entry("vendor", "submodule") 66 + ]); 67 + expect(sorted.map((item) => item.name)).toEqual(["src", "vendor", ".gitignore", "readme.md"]); 68 + }); 69 + }); 70 + 71 + describe("toCommitSummary", () => { 72 + // `this` is where the hex hash lives, `hash` is a byte array on the wire 73 + const commit: LogCommit = { 74 + this: "0c4d0e9b07940033721395a434b5873f0fb9e6c8", 75 + author: { Name: "Ada", Email: "ada@example.com", When: "2026-07-01T10:00:00Z" }, 76 + committer: { Name: "Ada", Email: "ada@example.com", When: "2026-07-02T10:00:00Z" }, 77 + message: "web: add repo index\n\nWith a longer body.\n", 78 + change_id: "abc123" 79 + }; 80 + 81 + it("splits the subject from the body and shortens the hash", () => { 82 + const summary = toCommitSummary(commit); 83 + expect(summary).toMatchObject({ 84 + hash: "0c4d0e9b07940033721395a434b5873f0fb9e6c8", 85 + shortHash: "0c4d0e9b", 86 + subject: "web: add repo index", 87 + body: "With a longer body.", 88 + authorName: "Ada", 89 + when: "2026-07-02T10:00:00Z", 90 + changeId: "abc123" 91 + }); 92 + }); 93 + 94 + it("leaves the body empty for single-line messages", () => { 95 + const summary = toCommitSummary({ ...commit, message: "one liner\n" }); 96 + expect(summary.body).toBe(""); 97 + expect(summary.subject).toBe("one liner"); 98 + }); 99 + }); 100 + 101 + describe("toBranchSummary", () => { 102 + it("reads the nested reference and the go-git commit fields", () => { 103 + const branch: BranchEntry = { 104 + reference: { name: "master", hash: "ff3a3678" }, 105 + commit: { Committer: { Name: "Ada", Email: "a@b.c", When: "2026-07-02T10:00:00Z" } }, 106 + is_default: true 107 + }; 108 + expect(toBranchSummary(branch)).toEqual({ 109 + name: "master", 110 + hash: "ff3a3678", 111 + when: "2026-07-02T10:00:00Z", 112 + isDefault: true 113 + }); 114 + }); 115 + 116 + it("treats a missing is_default as not default", () => { 117 + expect(toBranchSummary({ reference: { name: "topic", hash: "abc" } }).isDefault).toBe(false); 118 + }); 119 + }); 120 + 121 + describe("toTagSummary", () => { 122 + // an annotated tag's own hash is the tag object, the commit is in Target 123 + it("reads the inlined reference and follows an annotated tag to its commit", () => { 124 + const tag: TagEntry = { 125 + name: "v1.0.0", 126 + hash: "63fa1d4b", 127 + message: "release", 128 + tag: { 129 + Tagger: { Name: "Ada", Email: "a@b.c", When: "2026-07-01T10:00:00Z" }, 130 + Target: [75, 78, 254, 37] 131 + } 132 + }; 133 + expect(toTagSummary(tag)).toEqual({ 134 + name: "v1.0.0", 135 + hash: "63fa1d4b", 136 + commitHash: "4b4efe25", 137 + when: "2026-07-01T10:00:00Z", 138 + message: "release" 139 + }); 140 + }); 141 + 142 + it("a lightweight tag is its own commit", () => { 143 + const summary = toTagSummary({ name: "v1.0.0", hash: "eebb477b" }); 144 + expect(summary.commitHash).toBe("eebb477b"); 145 + }); 146 + }); 147 + 148 + describe("toTreeEntrySummary", () => { 149 + it("carries the last commit over from snake_case", () => { 150 + expect( 151 + toTreeEntrySummary({ 152 + name: "flake.nix", 153 + mode: "0100644", 154 + size: 213, 155 + last_commit: { hash: "f0b11b85", message: "init", when: "2026-07-01T10:00:00Z" } 156 + }) 157 + ).toEqual({ 158 + name: "flake.nix", 159 + kind: "file", 160 + size: 213, 161 + lastCommitHash: "f0b11b85", 162 + lastCommitWhen: "2026-07-01T10:00:00Z", 163 + lastCommitMessage: "init" 164 + }); 165 + }); 166 + }); 167 + 168 + describe("resolveRepoByName", () => { 169 + const owner = "did:plc:owner"; 170 + 171 + it("asks bobbin for the owner and name", async () => { 172 + const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( 173 + jsonResponse({ 174 + uri: `at://${owner}/sh.tangled.repo/3lzg6enurmo22`, 175 + value: { $type: "sh.tangled.repo", knot: "knot.test", name: "infra" } 176 + }) 177 + ); 178 + const view = await resolveRepoByName(makeCtx(fetchMock), owner, "infra"); 179 + expect(view?.uri).toBe(`at://${owner}/sh.tangled.repo/3lzg6enurmo22`); 180 + const url = new URL(String(fetchMock.mock.calls[0][0])); 181 + expect(url.pathname).toBe("/xrpc/sh.tangled.repo.getRepoByName"); 182 + expect(url.searchParams.get("owner")).toBe(owner); 183 + expect(url.searchParams.get("name")).toBe("infra"); 184 + }); 185 + 186 + it("returns null when the owner has no such repo", async () => { 187 + const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( 188 + new Response(JSON.stringify({ error: "RecordNotFound" }), { 189 + status: 404, 190 + headers: { "content-type": "application/json" } 191 + }) 192 + ); 193 + expect(await resolveRepoByName(makeCtx(fetchMock), owner, "missing")).toBeNull(); 194 + }); 195 + 196 + it("propagates anything that is not a miss", async () => { 197 + const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( 198 + new Response(JSON.stringify({ error: "UpstreamFailed" }), { 199 + status: 502, 200 + headers: { "content-type": "application/json" } 201 + }) 202 + ); 203 + await expect(resolveRepoByName(makeCtx(fetchMock), owner, "infra")).rejects.toBeInstanceOf( 204 + ClientResponseError 205 + ); 206 + }); 207 + });
+232
web/src/lib/api/repo.ts
··· 1 + import { ClientResponseError, type BobbinContext, type XrpcRequestInit } from "./client"; 2 + import { getRepoByName, type RecordView, type RepoRecord } from "./records"; 3 + import { branches as knotBranches, log as knotLog, tags as knotTags } from "./knot"; 4 + import { httpStatusFor } from "./load"; 5 + import { rkeyFromUri } from "./uri"; 6 + import type * as Tree from "./lexicons/types/sh/tangled/repo/tree"; 7 + 8 + // log, branches and tags are `*/*` in the lexicons, so these shapes are copied 9 + // from core/types by hand. anything go-git touches keeps its go field names and 10 + // writes hashes as byte arrays, so the hex comes from a sibling field 11 + 12 + export interface GitSignature { 13 + Name: string; 14 + Email: string; 15 + When: string; 16 + } 17 + 18 + export interface GitCommit { 19 + Author?: GitSignature; 20 + Committer?: GitSignature; 21 + Message?: string; 22 + } 23 + 24 + // `this` and `parent` are the hex hashes, `hash` is the byte array 25 + export interface LogCommit { 26 + this?: string; 27 + parent?: string; 28 + author?: GitSignature; 29 + committer?: GitSignature; 30 + message?: string; 31 + tree?: string; 32 + change_id?: string; 33 + } 34 + 35 + export interface LogResponse { 36 + commits?: LogCommit[]; 37 + ref?: string; 38 + total?: number; 39 + page?: number; 40 + } 41 + 42 + export interface GitReference { 43 + name: string; 44 + hash: string; 45 + } 46 + 47 + export interface BranchEntry { 48 + reference: GitReference; 49 + commit?: GitCommit; 50 + is_default?: boolean; 51 + } 52 + 53 + export interface BranchesResponse { 54 + branches?: BranchEntry[]; 55 + total?: number; 56 + } 57 + 58 + export interface TagEntry { 59 + name: string; 60 + hash: string; 61 + message?: string; 62 + tag?: { 63 + Tagger?: GitSignature; 64 + Message?: string; 65 + /** the commit an annotated tag points at, bytes like every go-git hash */ 66 + Target?: number[]; 67 + }; 68 + } 69 + 70 + export interface TagsResponse { 71 + tags?: TagEntry[]; 72 + total?: number; 73 + } 74 + 75 + // newer repos get tid rkeys and keep their display name in the record 76 + export const repoNameOf = (view: RecordView<RepoRecord>): string => 77 + view.value.name ?? rkeyFromUri(view.uri); 78 + 79 + // a repo bobbin has never indexed is a miss, not an error 80 + export const resolveRepoByName = async ( 81 + ctx: BobbinContext, 82 + ownerDid: string, 83 + name: string, 84 + init?: XrpcRequestInit 85 + ): Promise<RecordView<RepoRecord> | null> => { 86 + try { 87 + return await getRepoByName(ctx, ownerDid, name, init); 88 + } catch (cause) { 89 + if (cause instanceof ClientResponseError && httpStatusFor(cause) === 404) return null; 90 + throw cause; 91 + } 92 + }; 93 + 94 + export interface CommitSummary { 95 + hash: string; 96 + shortHash: string; 97 + subject: string; 98 + body: string; 99 + authorName: string; 100 + authorEmail: string; 101 + when: string; 102 + changeId?: string; 103 + } 104 + 105 + const splitMessage = (message: string): [string, string] => { 106 + const separator = message.indexOf("\n\n"); 107 + if (separator === -1) return [message.trim(), ""]; 108 + return [message.slice(0, separator).trim(), message.slice(separator + 2).trim()]; 109 + }; 110 + 111 + export const toCommitSummary = (commit: LogCommit): CommitSummary => { 112 + const [subject, body] = splitMessage(commit.message ?? ""); 113 + const hash = commit.this ?? ""; 114 + return { 115 + hash, 116 + shortHash: hash.slice(0, 8), 117 + subject, 118 + body, 119 + authorName: commit.author?.Name ?? "", 120 + authorEmail: commit.author?.Email ?? "", 121 + when: commit.committer?.When ?? commit.author?.When ?? "", 122 + changeId: commit.change_id 123 + }; 124 + }; 125 + 126 + export interface BranchSummary { 127 + name: string; 128 + hash: string; 129 + when?: string; 130 + isDefault: boolean; 131 + } 132 + 133 + export const toBranchSummary = (branch: BranchEntry): BranchSummary => ({ 134 + name: branch.reference.name, 135 + hash: branch.reference.hash, 136 + when: branch.commit?.Committer?.When ?? branch.commit?.Author?.When, 137 + isDefault: branch.is_default === true 138 + }); 139 + 140 + export interface TagSummary { 141 + name: string; 142 + hash: string; 143 + /** an annotated tag has its own hash, this is the commit it points at */ 144 + commitHash: string; 145 + when?: string; 146 + message?: string; 147 + } 148 + 149 + const hexFromBytes = (bytes: number[]): string => 150 + bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(""); 151 + 152 + export const toTagSummary = (tag: TagEntry): TagSummary => { 153 + const target = tag.tag?.Target; 154 + return { 155 + name: tag.name, 156 + hash: tag.hash, 157 + commitHash: target?.length ? hexFromBytes(target) : tag.hash, 158 + when: tag.tag?.Tagger?.When, 159 + message: tag.message ?? tag.tag?.Message 160 + }; 161 + }; 162 + 163 + export type TreeEntryKind = "file" | "directory" | "symlink" | "submodule"; 164 + 165 + // modes come back octal and zero padded 166 + export const treeEntryKind = (mode: string): TreeEntryKind => { 167 + switch (mode.replace(/^0+/, "").padStart(6, "0")) { 168 + case "040000": 169 + return "directory"; 170 + case "120000": 171 + return "symlink"; 172 + case "160000": 173 + return "submodule"; 174 + default: 175 + return "file"; 176 + } 177 + }; 178 + 179 + export interface TreeEntrySummary { 180 + name: string; 181 + kind: TreeEntryKind; 182 + size: number; 183 + lastCommitHash?: string; 184 + lastCommitWhen?: string; 185 + lastCommitMessage?: string; 186 + } 187 + 188 + export const toTreeEntrySummary = (entry: Tree.TreeEntry): TreeEntrySummary => ({ 189 + name: entry.name, 190 + kind: treeEntryKind(entry.mode), 191 + size: entry.size, 192 + lastCommitHash: entry.last_commit?.hash, 193 + lastCommitWhen: entry.last_commit?.when, 194 + lastCommitMessage: entry.last_commit?.message 195 + }); 196 + 197 + export const tagsByCommitHash = ( 198 + commits: CommitSummary[], 199 + tags: TagSummary[] 200 + ): Record<string, string[]> => { 201 + const shown = new Set(commits.map((commit) => commit.hash)); 202 + return tags.reduce<Record<string, string[]>>((acc, tag) => { 203 + if (shown.has(tag.commitHash)) (acc[tag.commitHash] ??= []).push(tag.name); 204 + return acc; 205 + }, {}); 206 + }; 207 + 208 + export const sortTreeEntries = (entries: TreeEntrySummary[]): TreeEntrySummary[] => 209 + [...entries].sort((a, b) => { 210 + const aDir = a.kind === "directory" || a.kind === "submodule"; 211 + const bDir = b.kind === "directory" || b.kind === "submodule"; 212 + if (aDir !== bDir) return aDir ? -1 : 1; 213 + return a.name.localeCompare(b.name); 214 + }); 215 + 216 + export const logFor = ( 217 + ctx: BobbinContext, 218 + repo: string, 219 + ref: string, 220 + limit: number, 221 + init?: XrpcRequestInit 222 + ) => knotLog<LogResponse>(ctx, { repo, ref, limit }, init); 223 + 224 + export const branchesFor = ( 225 + ctx: BobbinContext, 226 + repo: string, 227 + limit: number, 228 + init?: XrpcRequestInit 229 + ) => knotBranches<BranchesResponse>(ctx, { repo, limit }, init); 230 + 231 + export const tagsFor = (ctx: BobbinContext, repo: string, limit: number, init?: XrpcRequestInit) => 232 + knotTags<TagsResponse>(ctx, { repo, limit }, init);
+34
web/src/lib/api/repoIndex.test.ts
··· 1 + import { describe, expect, it } from "vitest"; 2 + import { ClientResponseError } from "./client"; 3 + import { classifyRepoAvailability } from "./repoIndex"; 4 + 5 + const unsupported = () => 6 + new ClientResponseError({ status: 404, data: { error: "XRPCNotSupported" } }); 7 + 8 + describe("classifyRepoAvailability", () => { 9 + it("recognizes a legacy knot that does not expose repository endpoints", () => { 10 + const result = classifyRepoAvailability( 11 + [unsupported(), unsupported(), unsupported()].map((cause) => ({ value: null, error: cause })) 12 + ); 13 + 14 + expect(result).toEqual({ needsUpgrade: true, knotUnreachable: false }); 15 + }); 16 + 17 + it("keeps transport failures as unreachable", () => { 18 + const result = classifyRepoAvailability( 19 + ["tree", "log", "branches"].map(() => ({ value: null, error: new TypeError("offline") })) 20 + ); 21 + 22 + expect(result).toEqual({ needsUpgrade: false, knotUnreachable: true }); 23 + }); 24 + 25 + it("does not mistake a missing ref for an unavailable knot", () => { 26 + const result = classifyRepoAvailability([ 27 + { value: null, error: unsupported() }, 28 + { value: null, error: unsupported() }, 29 + { value: { branches: [] }, error: null } 30 + ]); 31 + 32 + expect(result).toEqual({ needsUpgrade: false, knotUnreachable: false }); 33 + }); 34 + });
+249
web/src/lib/api/repoIndex.ts
··· 1 + import { error } from "@sveltejs/kit"; 2 + import { ClientResponseError, createBobbinClient } from "$lib/api/client"; 3 + import { languages as knotLanguages, tree as knotTree } from "$lib/api/knot"; 4 + import { 5 + branches as knotMirrorBranches, 6 + createKnotMirrorClient, 7 + languages as knotMirrorLanguages, 8 + log as knotMirrorLog, 9 + tags as knotMirrorTags, 10 + tree as knotMirrorTree 11 + } from "$lib/api/knotmirror"; 12 + import { parallel } from "$lib/api/load"; 13 + import { 14 + branchesFor, 15 + logFor, 16 + sortTreeEntries, 17 + tagsByCommitHash, 18 + tagsFor, 19 + toBranchSummary, 20 + toCommitSummary, 21 + toTagSummary, 22 + toTreeCommitSummary, 23 + toTreeEntrySummary 24 + } from "$lib/api/repo"; 25 + import { renderDocument } from "$lib/markup"; 26 + import type { LanguageSlice, RepoInfo } from "$lib/components/repo/types"; 27 + import type * as Tree from "./lexicons/types/sh/tangled/repo/tree"; 28 + 29 + // `/tree/{ref}` is this same page at another ref, so they share a load 30 + 31 + const COMMIT_LIMIT = 10; 32 + const BRANCH_LIMIT = 5; 33 + const TAG_LIMIT = 5; 34 + // a knot only ever lists 100 refs, so any total we get is really a minimum 35 + const REF_LIMIT = 100; 36 + 37 + export interface RepoParent { 38 + publicConfig: { bobbinUrl: string; knotMirrorUrl: string; camoEnabled: boolean }; 39 + repo: RepoInfo; 40 + } 41 + 42 + export interface RepoLoadEvent { 43 + fetch: typeof globalThis.fetch; 44 + url: URL; 45 + } 46 + 47 + interface RepoDataSource { 48 + tree: (ref: string, path?: string) => Promise<Tree.$output>; 49 + log: (ref: string, limit: number) => Promise<Awaited<ReturnType<typeof logFor>>>; 50 + branches: (limit: number) => Promise<Awaited<ReturnType<typeof branchesFor>>>; 51 + tags: (limit: number) => Promise<Awaited<ReturnType<typeof tagsFor>>>; 52 + languages: (ref: string) => Promise<{ languages?: { name: string; size: number }[] }>; 53 + } 54 + 55 + const orNull = <T>(promise: Promise<T>): Promise<T | null> => promise.catch(() => null); 56 + 57 + interface Attempt<T> { 58 + value: T | null; 59 + error: unknown | null; 60 + } 61 + 62 + const attempt = <T>(promise: Promise<T>): Promise<Attempt<T>> => 63 + promise.then( 64 + (value) => ({ value, error: null }), 65 + (error) => ({ value: null, error }) 66 + ); 67 + 68 + const isUnsupported = (cause: unknown): boolean => 69 + cause instanceof ClientResponseError && cause.status === 404; 70 + 71 + export const classifyRepoAvailability = ( 72 + attempts: readonly { value: unknown | null; error: unknown | null }[] 73 + ) => { 74 + const needsUpgrade = attempts.every( 75 + (result) => result.value === null && isUnsupported(result.error) 76 + ); 77 + return { 78 + needsUpgrade, 79 + knotUnreachable: !needsUpgrade && attempts.every((result) => result.value === null) 80 + }; 81 + }; 82 + 83 + const toLanguageSlices = (languages: { name: string; size: number }[]): LanguageSlice[] => { 84 + const sized = languages.filter((language) => language.size > 0); 85 + const total = sized.reduce((sum, language) => sum + language.size, 0); 86 + if (total === 0) return []; 87 + 88 + const slices = sized.map((language) => { 89 + const share = (language.size / total) * 100; 90 + return { name: language.name, share, percentage: Math.floor(share) }; 91 + }); 92 + 93 + const short = 100 - slices.reduce((sum, slice) => sum + slice.percentage, 0); 94 + [...slices] 95 + .sort((a, b) => (b.share % 1) - (a.share % 1) || b.share - a.share) 96 + .slice(0, Math.max(0, short)) 97 + .forEach((slice) => { 98 + slice.percentage += 1; 99 + }); 100 + 101 + return slices.sort((a, b) => b.share - a.share); 102 + }; 103 + 104 + const refNames = (branches: { name: string }[], tags: { name: string }[]) => ({ 105 + branches: branches.map((branch) => branch.name), 106 + tags: tags.map((tag) => tag.name), 107 + capped: branches.length >= REF_LIMIT || tags.length >= REF_LIMIT 108 + }); 109 + 110 + const renderReadme = ( 111 + readme: { filename: string; contents: string } | null, 112 + parent: RepoParent, 113 + event: RepoLoadEvent, 114 + ref: string, 115 + dir?: string 116 + ) => 117 + readme 118 + ? renderDocument(readme.filename, readme.contents, { 119 + repo: `${parent.repo.ownerHandle}/${parent.repo.name}`, 120 + ref, 121 + dir, 122 + host: event.url.host, 123 + camo: parent.publicConfig.camoEnabled 124 + }) 125 + : Promise.resolve(null); 126 + 127 + // the knot sends a readme with empty fields when a directory has none 128 + const readmeOf = (tree: { readme?: { filename: string; contents: string } } | null) => { 129 + const readme = tree?.readme; 130 + return readme?.filename ? readme : null; 131 + }; 132 + 133 + const repoDataSource = (event: RepoLoadEvent, parent: RepoParent): RepoDataSource => { 134 + if (parent.publicConfig.knotMirrorUrl && parent.repo.repoDid) { 135 + const ctx = createKnotMirrorClient(parent.publicConfig.knotMirrorUrl, event.fetch); 136 + const repo = parent.repo.repoDid; 137 + return { 138 + tree: (ref, path) => knotMirrorTree(ctx, { repo, ref, path }), 139 + log: (ref, limit) => knotMirrorLog(ctx, { repo, ref, limit }), 140 + branches: (limit) => knotMirrorBranches(ctx, { repo, limit }), 141 + tags: (limit) => knotMirrorTags(ctx, { repo, limit }), 142 + languages: (ref) => knotMirrorLanguages(ctx, { repo, ref }) 143 + }; 144 + } 145 + 146 + const ctx = createBobbinClient({ serviceUrl: parent.publicConfig.bobbinUrl, fetch: event.fetch }); 147 + const repo = parent.repo.uri; 148 + return { 149 + tree: (ref, path) => knotTree(ctx, { repo, ref, path }), 150 + log: (ref, limit) => logFor(ctx, repo, ref, limit), 151 + branches: (limit) => branchesFor(ctx, repo, limit), 152 + tags: (limit) => tagsFor(ctx, repo, limit), 153 + languages: (ref) => knotLanguages(ctx, { repo, ref }) 154 + }; 155 + }; 156 + 157 + export interface RepoIndexOptions { 158 + requireRef?: boolean; 159 + } 160 + 161 + export const loadRepoIndex = async ( 162 + event: RepoLoadEvent, 163 + parent: RepoParent, 164 + ref: string, 165 + // a ref from the url has to resolve or a typo looks like a repo with no 166 + // files. the default branch renders whatever the knot managed to answer 167 + { requireRef = false }: RepoIndexOptions = {} 168 + ) => { 169 + const source = repoDataSource(event, parent); 170 + 171 + // each list falls back on its own, so half a page still renders 172 + const results = await parallel({ 173 + tree: attempt(source.tree(ref)), 174 + log: attempt(source.log(ref, COMMIT_LIMIT)), 175 + branches: attempt(source.branches(REF_LIMIT)), 176 + tags: attempt(source.tags(REF_LIMIT)), 177 + languages: attempt(source.languages(ref)) 178 + }); 179 + 180 + const branches = (results.branches.value?.branches ?? []).map(toBranchSummary); 181 + const tags = (results.tags.value?.tags ?? []).map(toTagSummary); 182 + const commits = (results.log.value?.commits ?? []).map(toCommitSummary); 183 + const files = sortTreeEntries((results.tree.value?.files ?? []).map(toTreeEntrySummary)); 184 + 185 + const languages = toLanguageSlices(results.languages.value?.languages ?? []); 186 + 187 + const readme = readmeOf(results.tree.value); 188 + const readmeHtml = await renderReadme(readme, parent, event, ref); 189 + 190 + const contentAttempts = [results.tree, results.log, results.branches]; 191 + const { needsUpgrade, knotUnreachable } = classifyRepoAvailability(contentAttempts); 192 + const isEmpty = !knotUnreachable && files.length === 0 && branches.length === 0; 193 + 194 + // there are refs but not this one, so it is not a real ref. an empty repo has 195 + // no refs at all and still gets a page 196 + if (requireRef && results.tree.value === null && branches.length > 0) { 197 + error(404, `${ref} does not exist in this repository`); 198 + } 199 + 200 + return { 201 + ref, 202 + isEmpty, 203 + needsUpgrade, 204 + knotUnreachable, 205 + files, 206 + readme, 207 + readmeHtml, 208 + commits, 209 + tagsByCommit: tagsByCommitHash(commits, tags), 210 + totalCommits: results.log.value?.total ?? commits.length, 211 + branches: branches.slice(0, BRANCH_LIMIT), 212 + totalBranches: branches.length, 213 + tags: tags.slice(0, TAG_LIMIT), 214 + totalTags: tags.length, 215 + // the switcher needs every ref, not just the visible slice 216 + refs: refNames(branches, tags), 217 + languages 218 + }; 219 + }; 220 + 221 + export const loadRepoTree = async ( 222 + event: RepoLoadEvent, 223 + parent: RepoParent, 224 + ref: string, 225 + path: string 226 + ) => { 227 + const source = repoDataSource(event, parent); 228 + 229 + // the tree is the whole page here, so a miss is just a 404 230 + const tree = await orNull(source.tree(ref, path)); 231 + const files = sortTreeEntries((tree?.files ?? []).map(toTreeEntrySummary)); 232 + // git cannot store an empty directory. so nothing here means the path is a 233 + // file, or was never there 234 + if (tree === null || files.length === 0) { 235 + error(404, `${path} does not exist at ${ref}`); 236 + } 237 + 238 + const readme = readmeOf(tree); 239 + const readmeHtml = await renderReadme(readme, parent, event, ref, path); 240 + 241 + return { 242 + ref, 243 + path, 244 + files, 245 + readme, 246 + readmeHtml, 247 + lastCommit: tree.lastCommit ? toTreeCommitSummary(tree.lastCommit) : null 248 + }; 249 + };
+39
web/src/lib/components/repo/BranchList.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import BranchList from "./BranchList.svelte"; 4 + 5 + const branches = [ 6 + { 7 + name: "main", 8 + hash: "0123456789abcdef0123456789abcdef01234567", 9 + when: "2026-07-28T09:00:00Z", 10 + isDefault: true 11 + }, 12 + { 13 + name: "feature/storybook", 14 + hash: "abcdef0123456789abcdef0123456789abcdef01", 15 + when: "2026-07-27T09:00:00Z", 16 + isDefault: false 17 + } 18 + ]; 19 + 20 + const { Story } = defineMeta({ 21 + title: "Repo/BranchList", 22 + component: BranchList, 23 + tags: ["autodocs"], 24 + args: { 25 + ownerHandle: "dawn", 26 + repoName: "tangled", 27 + currentRef: "main", 28 + branches 29 + } 30 + }); 31 + </script> 32 + 33 + <Story name="Current branch has no compare link" /> 34 + <Story name="Feature branch selected" args={{ currentRef: "feature/storybook" }} /> 35 + <Story 36 + name="Branch without timestamp" 37 + args={{ branches: [{ name: "main", hash: "0123456789abcdef", isDefault: true }] }} 38 + /> 39 + <Story name="No branches" args={{ branches: [] }} />
+57
web/src/lib/components/repo/BranchList.svelte
··· 1 + <script lang="ts"> 2 + import { resolve } from "$app/paths"; 3 + import GitCompare from "$icon/git-compare"; 4 + import Tag from "$lib/components/ui/Tag.svelte"; 5 + import TimeAgo from "$lib/components/ui/TimeAgo.svelte"; 6 + import type { BranchSummary } from "./types"; 7 + 8 + interface Props { 9 + ownerHandle: string; 10 + repoName: string; 11 + currentRef: string; 12 + branches: BranchSummary[]; 13 + } 14 + 15 + let { ownerHandle, repoName, currentRef, branches }: Props = $props(); 16 + 17 + const base = $derived(`/${ownerHandle}/${repoName}`); 18 + </script> 19 + 20 + <div class="flex flex-col gap-1"> 21 + {#each branches as branch (branch.name)} 22 + <div class="flex items-center justify-between overflow-hidden text-base"> 23 + <div class="flex min-w-0 flex-1 items-center gap-2"> 24 + <a 25 + href={resolve(`${base}/tree/${encodeURIComponent(branch.name)}` as "/")} 26 + class="truncate text-foreground-default no-underline hover:underline" 27 + > 28 + {branch.name} 29 + </a> 30 + {#if branch.when} 31 + <span aria-hidden="true" class="shrink-0 text-foreground-subtle select-none" 32 + >&middot;</span 33 + > 34 + <TimeAgo 35 + value={branch.when} 36 + class="shrink-0 text-xs whitespace-nowrap text-foreground-subtle" 37 + /> 38 + {/if} 39 + {#if branch.isDefault} 40 + <Tag color="gray" class="shrink-0 font-mono">Default</Tag> 41 + {/if} 42 + </div> 43 + {#if branch.name !== currentRef} 44 + <a 45 + href={resolve( 46 + `${base}/compare/${encodeURIComponent(currentRef)}...${encodeURIComponent(branch.name)}` as "/" 47 + )} 48 + class="ml-2 flex shrink-0 items-center gap-1 text-xs text-foreground-muted" 49 + title="Compare branches or tags" 50 + > 51 + <GitCompare class="size-3" aria-hidden="true" /> 52 + Compare 53 + </a> 54 + {/if} 55 + </div> 56 + {/each} 57 + </div>
+51
web/src/lib/components/repo/CloneDropdown.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta, type StoryContext } from "@storybook/addon-svelte-csf"; 3 + import { expect } from "storybook/test"; 4 + import CloneDropdown from "./CloneDropdown.svelte"; 5 + import type { RepoInfo } from "./types"; 6 + 7 + const repo: RepoInfo = { 8 + uri: "at://did:plc:repo/sh.tangled.repo.repo/3jzrepo", 9 + rkey: "3jzrepo", 10 + name: "tangled", 11 + ownerDid: "did:plc:owner", 12 + ownerHandle: "dawn", 13 + repoDid: "did:plc:repo", 14 + knot: "knot1.tangled.sh", 15 + defaultBranch: "main" 16 + }; 17 + 18 + const selfHostedRepo = { ...repo, knot: "https://git.example.test:8443" }; 19 + const repoWithoutDid = { ...repo, repoDid: undefined }; 20 + type PlayContext = Pick<StoryContext<Record<string, unknown>>, "canvas" | "userEvent">; 21 + 22 + const openMenu = async ({ canvas, userEvent }: PlayContext) => { 23 + await userEvent.click(canvas.getByRole("button", { name: "Clone this repository" })); 24 + await expect(canvas.getByRole("menu")).toBeVisible(); 25 + }; 26 + 27 + const selectPermalink = async ({ canvas, userEvent }: PlayContext) => { 28 + await openMenu({ canvas, userEvent }); 29 + await userEvent.click(canvas.getByRole("checkbox", { name: "Use permalink" })); 30 + await expect(canvas.getByText("git@tangled.org:did:plc:repo")).toBeVisible(); 31 + }; 32 + 33 + const { Story } = defineMeta({ 34 + title: "Repo/CloneDropdown", 35 + component: CloneDropdown, 36 + tags: ["autodocs"], 37 + args: { 38 + repo, 39 + ref: "main", 40 + bobbinUrl: "https://bobbin.example.test" 41 + } 42 + }); 43 + </script> 44 + 45 + <Story name="Opened clone menu" play={openMenu} /> 46 + <Story name="Permalink URLs" play={selectPermalink} /> 47 + <Story 48 + name="Self-hosted knot normalizes SSH host" 49 + args={{ repo: selfHostedRepo, ref: "feature/demo" }} 50 + /> 51 + <Story name="Repository without DID" args={{ repo: repoWithoutDid }} />
+128
web/src/lib/components/repo/CloneDropdown.svelte
··· 1 + <script lang="ts"> 2 + import { page } from "$app/state"; 3 + import Check from "$icon/check"; 4 + import Copy from "$icon/copy"; 5 + import Download from "$icon/download"; 6 + import Checkbox from "$lib/components/ui/Checkbox.svelte"; 7 + import Dropdown from "$lib/components/ui/Dropdown.svelte"; 8 + import type { RepoInfo } from "./types"; 9 + 10 + interface Props { 11 + repo: RepoInfo; 12 + ref: string; 13 + bobbinUrl: string; 14 + } 15 + 16 + let { repo, ref, bobbinUrl }: Props = $props(); 17 + 18 + // ssh for knot1 goes through the main domain, other knots serve their own. the 19 + // knot field is usually a bare host but a scheme and port are both allowed 20 + const sshHost = $derived( 21 + (repo.knot === "knot1.tangled.sh" ? "tangled.org" : repo.knot) 22 + .replace(/^[a-z]+:\/\//, "") 23 + .split(/[:/]/)[0] 24 + ); 25 + const origin = $derived(page.url.origin); 26 + 27 + let permalink = $state(false); 28 + let copied = $state<string | null>(null); 29 + 30 + const urls = $derived([ 31 + { 32 + label: "HTTPS", 33 + handle: `${origin}/${repo.ownerHandle}/${repo.name}`, 34 + perma: repo.repoDid ? `${origin}/${repo.repoDid}` : null 35 + }, 36 + { 37 + label: "SSH", 38 + handle: `git@${sshHost}:${repo.ownerHandle}/${repo.name}`, 39 + perma: repo.repoDid ? `git@${sshHost}:${repo.repoDid}` : null 40 + } 41 + ]); 42 + 43 + const archiveUrl = (format: "tar.gz" | "zip") => { 44 + const url = new URL("/xrpc/sh.tangled.repo.archive", `${bobbinUrl}/`); 45 + url.searchParams.set("repo", repo.uri); 46 + url.searchParams.set("ref", ref); 47 + url.searchParams.set("format", format); 48 + return url.toString(); 49 + }; 50 + 51 + const copy = async (label: string, value: string) => { 52 + try { 53 + await navigator.clipboard.writeText(value); 54 + copied = label; 55 + setTimeout(() => (copied = copied === label ? null : copied), 2000); 56 + } catch { 57 + // clipboard was denied, the text is still selectable 58 + } 59 + }; 60 + </script> 61 + 62 + <Dropdown 63 + variant="button" 64 + buttonVariant="primary" 65 + align="right" 66 + label="Clone this repository" 67 + menuClass="w-96 divide-y-0 p-4" 68 + > 69 + {#snippet trigger()} 70 + <Download class="size-4" aria-hidden="true" /> 71 + <span class="hidden md:inline">Code</span> 72 + {/snippet} 73 + 74 + <div class="flex items-center justify-between"> 75 + <h3 class="text-sm font-medium text-foreground-default">Clone this repository</h3> 76 + {#if repo.repoDid} 77 + <Checkbox bind:checked={permalink} class="text-xs text-foreground-muted"> 78 + Use permalink 79 + </Checkbox> 80 + {/if} 81 + </div> 82 + 83 + {#each urls as entry (entry.label)} 84 + {@const value = permalink && entry.perma ? entry.perma : entry.handle} 85 + <div class="mt-4"> 86 + <span class="mb-1 block text-xs font-medium text-foreground-muted">{entry.label}</span> 87 + <div 88 + class="flex items-stretch divide-x divide-border-default rounded border border-border-default" 89 + > 90 + <span 91 + class="flex-1 overflow-x-auto bg-background-inset px-3 py-2 font-mono text-sm whitespace-nowrap text-foreground-default select-all" 92 + >{value}</span 93 + > 94 + <button 95 + type="button" 96 + class="cursor-pointer px-3 py-2 text-foreground-subtle hover:text-foreground-default" 97 + title="Copy to clipboard" 98 + aria-label={`Copy ${entry.label} clone url`} 99 + onclick={() => copy(entry.label, value)} 100 + > 101 + {#if copied === entry.label} 102 + <Check class="size-4" aria-hidden="true" /> 103 + {:else} 104 + <Copy class="size-4" aria-hidden="true" /> 105 + {/if} 106 + </button> 107 + </div> 108 + </div> 109 + {/each} 110 + 111 + <p class="mt-2 text-xs text-foreground-subtle"> 112 + For self-hosted knots, clone URLs may differ based on your setup. 113 + </p> 114 + 115 + <!-- archives come from bobbin, so these are external links --> 116 + <div class="mt-4 flex gap-2"> 117 + {#each [{ format: "tar.gz" }, { format: "zip" }] as const as archive (archive.format)} 118 + <a 119 + href={archiveUrl(archive.format)} 120 + rel="external noopener noreferrer" 121 + class="flex flex-1 items-center justify-center gap-2 rounded border border-border-default px-2 py-1.5 text-sm text-foreground-default no-underline hover:bg-background-subtle hover:no-underline" 122 + > 123 + <Download class="size-4" aria-hidden="true" /> 124 + {archive.format === "zip" ? ".zip" : archive.format} 125 + </a> 126 + {/each} 127 + </div> 128 + </Dropdown>
+57
web/src/lib/components/repo/CommitList.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta, type StoryContext } from "@storybook/addon-svelte-csf"; 3 + import { expect } from "storybook/test"; 4 + import CommitList from "./CommitList.svelte"; 5 + import type { CommitSummary } from "./types"; 6 + 7 + const commits: CommitSummary[] = [ 8 + { 9 + hash: "0123456789abcdef0123456789abcdef01234567", 10 + shortHash: "01234567", 11 + subject: "add a storybook fixture", 12 + body: "this body is hidden until the disclosure button is pressed.", 13 + authorName: "dawn", 14 + authorEmail: "dawn@example.test", 15 + when: "2026-07-28T09:00:00Z", 16 + changeId: "kqpuwoxz" 17 + }, 18 + { 19 + hash: "abcdef0123456789abcdef0123456789abcdef01", 20 + shortHash: "abcdef01", 21 + subject: "document the repository view", 22 + body: "", 23 + authorName: "dawn", 24 + authorEmail: "dawn@example.test", 25 + when: "2026-07-27T09:00:00Z" 26 + } 27 + ]; 28 + const minimalCommit = { ...commits[1], authorName: "", when: "", changeId: undefined }; 29 + type PlayContext = Pick<StoryContext<Record<string, unknown>>, "canvas" | "userEvent">; 30 + 31 + const expandBody = async ({ canvas, userEvent }: PlayContext) => { 32 + const toggle = canvas.getByRole("button", { name: "Toggle commit body" }); 33 + await userEvent.click(toggle); 34 + await expect(toggle).toHaveAttribute("aria-expanded", "true"); 35 + await expect( 36 + canvas.getByText("this body is hidden until the disclosure button is pressed.") 37 + ).toBeVisible(); 38 + }; 39 + 40 + const { Story } = defineMeta({ 41 + title: "Repo/CommitList", 42 + component: CommitList, 43 + tags: ["autodocs"], 44 + args: { 45 + ownerHandle: "dawn", 46 + repoName: "tangled", 47 + commits, 48 + tagsByCommit: { 49 + "0123456789abcdef0123456789abcdef01234567": ["v1.0.0"] 50 + } 51 + } 52 + }); 53 + </script> 54 + 55 + <Story name="Disclosure body and tag" play={expandBody} /> 56 + <Story name="Missing optional metadata" args={{ commits: [minimalCommit] }} /> 57 + <Story name="No commits" args={{ commits: [] }} />
+69
web/src/lib/components/repo/CommitList.svelte
··· 1 + <script lang="ts"> 2 + import { resolve } from "$app/paths"; 3 + import Ellipsis from "$icon/ellipsis"; 4 + import Tag from "$lib/components/ui/Tag.svelte"; 5 + import TimeAgo from "$lib/components/ui/TimeAgo.svelte"; 6 + import type { CommitSummary } from "./types"; 7 + 8 + interface Props { 9 + ownerHandle: string; 10 + repoName: string; 11 + commits: CommitSummary[]; 12 + tagsByCommit?: Record<string, string[]>; 13 + } 14 + 15 + let { ownerHandle, repoName, commits, tagsByCommit = {} }: Props = $props(); 16 + 17 + const base = $derived(`/${ownerHandle}/${repoName}`); 18 + let expanded = $state<Record<string, boolean>>({}); 19 + </script> 20 + 21 + <div class="flex flex-col gap-6"> 22 + {#each commits as commit (commit.hash)} 23 + <div> 24 + <div class="flex items-start gap-1"> 25 + <a 26 + href={resolve(`${base}/commit/${commit.hash}` as "/")} 27 + class="text-base text-foreground-default no-underline hover:underline" 28 + > 29 + {commit.subject} 30 + </a> 31 + {#if commit.body} 32 + <button 33 + type="button" 34 + class="mt-1 shrink-0 cursor-pointer rounded bg-background-inset px-1 py-0.5 text-foreground-muted hover:bg-background-muted" 35 + aria-expanded={expanded[commit.hash] === true} 36 + aria-label="Toggle commit body" 37 + onclick={() => (expanded[commit.hash] = !expanded[commit.hash])} 38 + > 39 + <Ellipsis class="size-3" aria-hidden="true" /> 40 + </button> 41 + {/if} 42 + </div> 43 + 44 + {#if commit.body && expanded[commit.hash]} 45 + <p class="mt-1 pb-2 text-sm whitespace-pre-wrap text-foreground-muted">{commit.body}</p> 46 + {/if} 47 + 48 + <div class="mt-2 flex flex-wrap items-center gap-2 text-xs text-foreground-subtle"> 49 + <a 50 + href={resolve(`${base}/commit/${commit.hash}` as "/")} 51 + title={commit.changeId ? `jj change id: ${commit.changeId}` : undefined} 52 + class="rounded bg-background-inset px-2 py-0.5 font-mono text-foreground-muted no-underline hover:underline" 53 + > 54 + {commit.shortHash} 55 + </a> 56 + {#if commit.authorName} 57 + <span class="truncate">{commit.authorName}</span> 58 + {/if} 59 + {#if commit.when} 60 + <span aria-hidden="true">&middot;</span> 61 + <TimeAgo value={commit.when} /> 62 + {/if} 63 + {#each tagsByCommit[commit.hash] ?? [] as name (name)} 64 + <Tag color="gray" class="font-mono">{name}</Tag> 65 + {/each} 66 + </div> 67 + </div> 68 + {/each} 69 + </div>
+36
web/src/lib/components/repo/EmptyRepo.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import StoryAuthProvider from "../../../../.storybook/StoryAuthProvider.svelte"; 4 + import EmptyRepo from "./EmptyRepo.svelte"; 5 + import type { RepoInfo } from "./types"; 6 + 7 + const ownerDid = "did:plc:owner"; 8 + const ownerRepo: RepoInfo = { 9 + uri: "at://did:plc:repo/sh.tangled.repo.repo/3jzrepo", 10 + rkey: "3jzrepo", 11 + name: "tangled", 12 + ownerDid, 13 + ownerHandle: "dawn", 14 + repoDid: "did:plc:repo", 15 + knot: "knot1.tangled.sh", 16 + defaultBranch: "main" 17 + }; 18 + const otherRepo = { ...ownerRepo, ownerDid: "did:plc:other", ownerHandle: "other" }; 19 + 20 + const { Story } = defineMeta({ 21 + title: "Repo/EmptyRepo", 22 + component: EmptyRepo, 23 + tags: ["autodocs"] 24 + }); 25 + </script> 26 + 27 + <Story name="Owner instructions" asChild> 28 + <StoryAuthProvider initial={{ did: ownerDid, handle: "dawn" }}> 29 + <EmptyRepo repo={ownerRepo} /> 30 + </StoryAuthProvider> 31 + </Story> 32 + <Story name="Visitor" asChild> 33 + <StoryAuthProvider> 34 + <EmptyRepo repo={otherRepo} /> 35 + </StoryAuthProvider> 36 + </Story>
+56
web/src/lib/components/repo/EmptyRepo.svelte
··· 1 + <script lang="ts"> 2 + import { resolve } from "$app/paths"; 3 + import { getAuth } from "$lib/auth.svelte"; 4 + import type { RepoInfo } from "./types"; 5 + 6 + interface Props { 7 + repo: RepoInfo; 8 + } 9 + 10 + let { repo }: Props = $props(); 11 + 12 + const auth = getAuth(); 13 + const isOwner = $derived(auth.currentDid === repo.ownerDid); 14 + // same host rewrite as the clone urls 15 + const sshHost = $derived( 16 + (repo.knot === "knot1.tangled.sh" ? "tangled.org" : repo.knot) 17 + .replace(/^[a-z]+:\/\//, "") 18 + .split(/[:/]/)[0] 19 + ); 20 + const remote = $derived(`git@${sshHost}:${repo.repoDid ?? `${repo.ownerHandle}/${repo.name}`}`); 21 + </script> 22 + 23 + {#snippet bullet(n: number)} 24 + <span 25 + class="mr-2 inline-flex size-5 shrink-0 items-center justify-center rounded-full bg-background-inset align-middle font-mono text-xs" 26 + > 27 + {n} 28 + </span> 29 + {/snippet} 30 + 31 + {#if isOwner} 32 + <div class="flex w-full place-content-center"> 33 + <div class="flex w-fit flex-col gap-4 py-6 text-sm"> 34 + <p>This is an empty repository. To get started:</p> 35 + <p> 36 + {@render bullet(1)}First, generate a new 37 + <a 38 + href="https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key" 39 + rel="noopener" 40 + class="underline">SSH key pair</a 41 + >. 42 + </p> 43 + <p> 44 + {@render bullet(2)}Then add the public key from the 45 + <a href={resolve("/settings/keys")} class="underline">keys page</a> in your settings. 46 + </p> 47 + <p> 48 + {@render bullet(3)}Configure your remote to 49 + <code class="rounded bg-background-inset px-1 font-mono">{remote}</code> 50 + </p> 51 + <p>{@render bullet(4)}Push!</p> 52 + </div> 53 + </div> 54 + {:else} 55 + <p class="py-6 text-center text-foreground-subtle">This is an empty repository.</p> 56 + {/if}
+70
web/src/lib/components/repo/FileTree.svelte
··· 1 + <script lang="ts"> 2 + import { resolve } from "$app/paths"; 3 + import File from "$icon/file"; 4 + import FileSymlink from "$icon/file-symlink"; 5 + import Folder from "$icon/folder"; 6 + import FolderInput from "$icon/folder-input"; 7 + import TimeAgo from "$lib/components/ui/TimeAgo.svelte"; 8 + import type { TreeEntrySummary } from "./types"; 9 + 10 + interface Props { 11 + ownerHandle: string; 12 + repoName: string; 13 + ref: string; 14 + entries: TreeEntrySummary[]; 15 + path?: string; 16 + } 17 + 18 + let { ownerHandle, repoName, ref, entries, path = "" }: Props = $props(); 19 + 20 + const base = $derived(`/${ownerHandle}/${repoName}`); 21 + const encodedRef = $derived(encodeURIComponent(ref)); 22 + 23 + const linkFor = (entry: TreeEntrySummary) => { 24 + const target = path ? `${path}/${entry.name}` : entry.name; 25 + const kind = entry.kind === "directory" ? "tree" : "blob"; 26 + return `${base}/${kind}/${encodedRef}/${target}`; 27 + }; 28 + 29 + const iconFor = (entry: TreeEntrySummary) => { 30 + switch (entry.kind) { 31 + case "directory": 32 + return Folder; 33 + case "submodule": 34 + return FolderInput; 35 + case "symlink": 36 + return FileSymlink; 37 + default: 38 + return File; 39 + } 40 + }; 41 + </script> 42 + 43 + <div class="min-w-0 md:border-r md:border-border-default md:pr-2"> 44 + {#each entries as entry (entry.name)} 45 + {@const Glyph = iconFor(entry)} 46 + <div class="grid grid-cols-3 items-center gap-4 py-1"> 47 + <a 48 + href={resolve(linkFor(entry) as "/")} 49 + class="col-span-2 flex min-w-0 items-center gap-2 text-foreground-default no-underline hover:underline" 50 + > 51 + <Glyph 52 + class={`size-4 shrink-0 ${entry.kind === "directory" ? "fill-current" : ""}`} 53 + aria-hidden="true" 54 + /> 55 + <span class="truncate">{entry.name}</span> 56 + </a> 57 + <div class="col-span-1 text-right text-sm text-foreground-subtle"> 58 + {#if entry.lastCommitHash && entry.lastCommitWhen} 59 + <a 60 + href={resolve(`${base}/commit/${entry.lastCommitHash}` as "/")} 61 + class="text-foreground-subtle no-underline hover:underline" 62 + title={entry.lastCommitMessage} 63 + > 64 + <TimeAgo value={entry.lastCommitWhen} /> 65 + </a> 66 + {/if} 67 + </div> 68 + </div> 69 + {/each} 70 + </div>
+40
web/src/lib/components/repo/LanguageBar.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta, type StoryContext } from "@storybook/addon-svelte-csf"; 3 + import { expect } from "storybook/test"; 4 + import LanguageBar from "./LanguageBar.svelte"; 5 + 6 + const languages = [ 7 + { name: "TypeScript", percentage: 58, share: 58.2 }, 8 + { name: "Svelte", percentage: 31, share: 30.7 }, 9 + { name: "CSS", percentage: 10, share: 10.1 }, 10 + { name: "Other", percentage: 0, share: 0.4 } 11 + ]; 12 + type PlayContext = Pick< 13 + StoryContext<Record<string, unknown>>, 14 + "canvas" | "canvasElement" | "userEvent" 15 + >; 16 + 17 + const expandBreakdown = async ({ canvas, canvasElement, userEvent }: PlayContext) => { 18 + const details = canvasElement.querySelector("details"); 19 + const summary = details?.querySelector("summary"); 20 + if (!(summary instanceof HTMLElement)) throw new Error("language summary was not rendered"); 21 + await userEvent.click(summary); 22 + await expect(details).toHaveAttribute("open"); 23 + await expect(canvas.getByText("TypeScript")).toBeVisible(); 24 + }; 25 + 26 + const { Story } = defineMeta({ 27 + title: "Repo/LanguageBar", 28 + component: LanguageBar, 29 + tags: ["autodocs"], 30 + args: { languages } 31 + }); 32 + </script> 33 + 34 + <Story name="Collapsed language bar" /> 35 + <Story name="Expanded percentage breakdown" play={expandBreakdown} /> 36 + <Story 37 + name="Sub-percent slice" 38 + args={{ languages: [{ name: "Other", percentage: 0, share: 0.4 }] }} 39 + /> 40 + <Story name="No languages" args={{ languages: [] }} />
+44
web/src/lib/components/repo/LanguageBar.svelte
··· 1 + <script lang="ts"> 2 + import { LANGUAGE_COLORS, LANGUAGE_COLOR_FALLBACK } from "./language-colors"; 3 + import type { LanguageSlice } from "./types"; 4 + 5 + interface Props { 6 + languages: LanguageSlice[]; 7 + } 8 + 9 + let { languages }: Props = $props(); 10 + 11 + const colorFor = (name: string) => LANGUAGE_COLORS[name] ?? LANGUAGE_COLOR_FALLBACK; 12 + const percent = (language: LanguageSlice) => 13 + language.share < 1 ? "<1" : String(language.percentage); 14 + const label = (language: LanguageSlice) => `${language.name} ${percent(language)}%`; 15 + </script> 16 + 17 + <details class="group -mx-6 -mt-4 mb-4"> 18 + <summary 19 + class="flex h-4 origin-top scale-y-50 cursor-pointer gap-px overflow-hidden rounded-t transition-transform group-open:scale-y-100 hover:scale-y-100" 20 + > 21 + {#each languages as language (language.name)} 22 + <div 23 + title={label(language)} 24 + style={`background-color: ${colorFor(language.name)}; flex: ${language.share} 0 0px`} 25 + ></div> 26 + {/each} 27 + </summary> 28 + <div 29 + class="flex flex-wrap items-center justify-center gap-4 border-b border-border-default px-4 py-2" 30 + > 31 + {#each languages as language (language.name)} 32 + <div class="flex items-center gap-2 text-xs"> 33 + <span 34 + class="inline-block size-2.5 shrink-0 rounded-full" 35 + style={`background-color: ${colorFor(language.name)}`} 36 + ></span> 37 + <span> 38 + {language.name} 39 + <span class="text-foreground-subtle">{percent(language)}%</span> 40 + </span> 41 + </div> 42 + {/each} 43 + </div> 44 + </details>
+16
web/src/lib/components/repo/PanelHeader.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import Logs from "$icon/logs"; 4 + import PanelHeader from "./PanelHeader.svelte"; 5 + 6 + const { Story } = defineMeta({ 7 + title: "Repo/PanelHeader", 8 + component: PanelHeader, 9 + tags: ["autodocs"], 10 + args: { title: "Commits", href: "/dawn/tangled/commits/main", icon: Logs, count: 42 } 11 + }); 12 + </script> 13 + 14 + <Story name="With count" /> 15 + <Story name="Approximate count" args={{ title: "Branches", count: 100, approximate: true }} /> 16 + <Story name="Without count" args={{ title: "Files", count: undefined }} />
+29
web/src/lib/components/repo/PanelHeader.svelte
··· 1 + <script lang="ts"> 2 + import { resolve } from "$app/paths"; 3 + import type { Component } from "svelte"; 4 + import type { SvelteHTMLElements } from "svelte/elements"; 5 + 6 + interface Props { 7 + title: string; 8 + href: string; 9 + icon: Component<SvelteHTMLElements["svg"]>; 10 + count?: number; 11 + approximate?: boolean; 12 + } 13 + 14 + let { title, href, icon, count, approximate = false }: Props = $props(); 15 + const Glyph = $derived(icon); 16 + </script> 17 + 18 + <a 19 + href={resolve(href as "/")} 20 + class="flex items-center gap-2 pb-2 font-medium text-foreground-default no-underline hover:text-foreground-muted hover:no-underline" 21 + > 22 + <Glyph class="size-4 shrink-0" aria-hidden="true" /> 23 + {title} 24 + {#if count !== undefined} 25 + <span class="rounded bg-background-inset px-1 text-sm font-normal"> 26 + {approximate && count > 0 ? `${count}+` : count} 27 + </span> 28 + {/if} 29 + </a>
+22
web/src/lib/components/repo/Readme.svelte
··· 1 + <script lang="ts"> 2 + import FileText from "$icon/file-text"; 3 + 4 + interface Props { 5 + filename: string; 6 + contents: string; 7 + } 8 + 9 + let { filename, contents }: Props = $props(); 10 + </script> 11 + 12 + <div class="mt-4 w-full overflow-hidden rounded bg-background-default shadow-sm"> 13 + <div class="border-b border-border-default px-4 py-2"> 14 + <span class="flex items-center gap-2"> 15 + <FileText class="size-4 text-foreground-subtle" aria-hidden="true" /> 16 + <span class="font-mono text-sm text-foreground-muted">{filename}</span> 17 + </span> 18 + </div> 19 + <div class="overflow-x-auto px-6 py-4"> 20 + <pre class="font-mono text-sm whitespace-pre-wrap text-foreground-default">{contents}</pre> 21 + </div> 22 + </div>
+22
web/src/lib/components/repo/RefSelector.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import RefSelector from "./RefSelector.svelte"; 4 + 5 + const { Story } = defineMeta({ 6 + title: "Repo/RefSelector", 7 + component: RefSelector, 8 + tags: ["autodocs"], 9 + args: { 10 + ownerHandle: "dawn", 11 + repoName: "tangled", 12 + current: "main", 13 + branches: ["main", "feature/storybook"], 14 + tags: ["v1.0.0", "v0.9.0"] 15 + } 16 + }); 17 + </script> 18 + 19 + <Story name="Branch selected" /> 20 + <Story name="Tag selected" args={{ current: "v1.0.0" }} /> 21 + <Story name="Detached commit ref" args={{ current: "0123456789abcdef" }} /> 22 + <Story name="Repository has no tags" args={{ tags: [] }} />
+56
web/src/lib/components/repo/RefSelector.svelte
··· 1 + <script lang="ts"> 2 + import { goto } from "$app/navigation"; 3 + import { resolve } from "$app/paths"; 4 + import GitCompare from "$icon/git-compare"; 5 + import Select from "$lib/components/ui/Select.svelte"; 6 + 7 + interface Props { 8 + ownerHandle: string; 9 + repoName: string; 10 + current: string; 11 + branches: string[]; 12 + tags: string[]; 13 + } 14 + 15 + let { ownerHandle, repoName, current, branches, tags }: Props = $props(); 16 + 17 + const base = $derived(`/${ownerHandle}/${repoName}`); 18 + 19 + const onchange = (event: Event) => { 20 + const value = (event.currentTarget as HTMLSelectElement).value; 21 + if (value === current) return; 22 + void goto(resolve(`${base}/tree/${encodeURIComponent(value)}` as "/")); 23 + }; 24 + </script> 25 + 26 + <div class="flex min-w-0 flex-1 items-stretch gap-2"> 27 + <div class="max-w-32 sm:max-w-64"> 28 + <!-- the ref lives in the url so navigation updates it, no two way binding --> 29 + <Select value={current} {onchange} aria-label="Switch branch or tag"> 30 + <!-- a commit sha or a ref past the knot's cap still needs a label --> 31 + {#if !branches.includes(current) && !tags.includes(current)} 32 + <option value={current}>{current}</option> 33 + {/if} 34 + <optgroup label={`Branches (${branches.length})`}> 35 + {#each branches as branch (branch)} 36 + <option value={branch}>{branch}</option> 37 + {/each} 38 + </optgroup> 39 + <optgroup label={`Tags (${tags.length})`}> 40 + {#each tags as tag (tag)} 41 + <option value={tag}>{tag}</option> 42 + {:else} 43 + <option disabled>No tags found</option> 44 + {/each} 45 + </optgroup> 46 + </Select> 47 + </div> 48 + <a 49 + href={resolve(`${base}/compare?base=${encodeURIComponent(current)}` as "/")} 50 + class="flex items-center rounded border border-border-default px-2 text-foreground-muted no-underline hover:bg-background-subtle hover:no-underline" 51 + title="Compare branches or tags" 52 + aria-label="Compare branches or tags" 53 + > 54 + <GitCompare class="size-4" aria-hidden="true" /> 55 + </a> 56 + </div>
+47
web/src/lib/components/repo/RepoHeader.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import StoryAuthProvider from "../../../../.storybook/StoryAuthProvider.svelte"; 4 + import RepoHeader from "./RepoHeader.svelte"; 5 + import type { RepoInfo } from "./types"; 6 + 7 + const repo: RepoInfo = { 8 + uri: "at://did:plc:repo/sh.tangled.repo.repo/3jzrepo", 9 + rkey: "3jzrepo", 10 + name: "tangled", 11 + ownerDid: "did:plc:owner", 12 + ownerHandle: "dawn", 13 + repoDid: "did:plc:repo", 14 + knot: "knot1.tangled.sh", 15 + description: "social code collaboration for the at protocol", 16 + website: "https://tangled.org", 17 + topics: ["atproto", "git", "svelte"], 18 + defaultBranch: "main", 19 + source: { ownerHandle: "upstream", name: "tangled" } 20 + }; 21 + const counts = { stars: 128, issues: 7, pulls: 3, forks: 12 }; 22 + 23 + const { Story } = defineMeta({ 24 + title: "Repo/RepoHeader", 25 + component: RepoHeader, 26 + tags: ["autodocs"] 27 + }); 28 + </script> 29 + 30 + <Story name="Full metadata" asChild> 31 + <StoryAuthProvider> 32 + <RepoHeader {repo} {counts} /> 33 + </StoryAuthProvider> 34 + </Story> 35 + <Story name="No description or website" asChild> 36 + <StoryAuthProvider> 37 + <RepoHeader 38 + repo={{ ...repo, description: undefined, website: undefined, topics: [] }} 39 + {counts} 40 + /> 41 + </StoryAuthProvider> 42 + </Story> 43 + <Story name="Not a fork" asChild> 44 + <StoryAuthProvider> 45 + <RepoHeader repo={{ ...repo, source: undefined }} {counts} /> 46 + </StoryAuthProvider> 47 + </Story>
+130
web/src/lib/components/repo/RepoHeader.svelte
··· 1 + <script lang="ts"> 2 + import { resolve } from "$app/paths"; 3 + import GitFork from "$icon/git-fork"; 4 + import Globe from "$icon/globe"; 5 + import Star from "$icon/star"; 6 + import Avatar from "$lib/components/ui/Avatar.svelte"; 7 + import Button from "$lib/components/ui/Button.svelte"; 8 + import Tag from "$lib/components/ui/Tag.svelte"; 9 + import { getAuth } from "$lib/auth.svelte"; 10 + import StarButton from "./StarButton.svelte"; 11 + import type { RepoCounts, RepoInfo } from "./types"; 12 + 13 + interface Props { 14 + repo: RepoInfo; 15 + counts: RepoCounts; 16 + viewerStarRkey?: string | null; 17 + } 18 + 19 + let { repo, counts, viewerStarRkey }: Props = $props(); 20 + 21 + const auth = getAuth(); 22 + const signedIn = $derived(Boolean(auth.currentDid)); 23 + const base = $derived(`/${repo.ownerHandle}/${repo.name}`); 24 + const trimScheme = (url: string) => url.replace(/^https?:\/\//, "").replace(/\/$/, ""); 25 + </script> 26 + 27 + <section class="mb-2 px-4 py-2"> 28 + <div class="mb-2 flex flex-col items-start justify-between gap-4 sm:flex-row"> 29 + <div class="flex min-w-0 flex-col gap-2"> 30 + <div class="flex flex-wrap items-center gap-2 text-lg"> 31 + <a 32 + href={resolve(`/${repo.ownerHandle}` as "/")} 33 + class="flex items-center gap-2 text-foreground-default no-underline hover:underline" 34 + > 35 + <Avatar src={repo.ownerAvatar} handle={repo.ownerHandle} size="size-6" /> 36 + {repo.ownerHandle} 37 + </a> 38 + <span class="text-foreground-subtle select-none">/</span> 39 + <a 40 + href={resolve(`/${repo.ownerHandle}/${repo.name}` as "/")} 41 + class="font-bold text-foreground-default no-underline hover:underline" 42 + > 43 + {repo.name} 44 + </a> 45 + </div> 46 + 47 + {#if repo.source} 48 + <div class="flex flex-wrap items-center gap-1 text-sm text-foreground-muted"> 49 + <GitFork class="size-3 shrink-0" aria-hidden="true" /> 50 + <span>forked from</span> 51 + <a 52 + href={resolve(`/${repo.source.ownerHandle}/${repo.source.name}` as "/")} 53 + class="underline" 54 + > 55 + {repo.source.ownerHandle}/{repo.source.name} 56 + </a> 57 + </div> 58 + {/if} 59 + </div> 60 + 61 + <div class="flex shrink-0 items-start gap-2"> 62 + {#if signedIn} 63 + <StarButton 64 + repoDid={repo.repoDid ?? ""} 65 + repoOwnerHandle={repo.ownerHandle} 66 + repoName={repo.name} 67 + initialCount={counts.stars} 68 + initialRkey={viewerStarRkey} 69 + /> 70 + {:else} 71 + <a 72 + href={resolve(`${base}/stars` as "/")} 73 + class="inline-flex items-center gap-1.5 rounded border border-border-default px-2 py-1 text-sm text-foreground-muted no-underline hover:bg-background-subtle hover:no-underline" 74 + title="Starred by" 75 + > 76 + <Star class="size-4 shrink-0" aria-hidden="true" /> 77 + {counts.stars} 78 + </a> 79 + {/if} 80 + 81 + <div 82 + class="inline-flex max-h-8 items-stretch divide-x divide-border-default overflow-clip rounded border border-border-default" 83 + > 84 + <Button 85 + href={resolve(`${base}/fork` as "/")} 86 + variant="default" 87 + size="sm" 88 + class="min-h-7.5 rounded-none border-0 before:rounded-none before:rounded-l-sm" 89 + > 90 + <GitFork class="size-4 shrink-0" aria-hidden="true" /> 91 + <span>Fork</span> 92 + </Button> 93 + <Button 94 + href={resolve(`${base}/forks` as "/")} 95 + variant="ghost" 96 + size="sm" 97 + class="min-h-7.5 rounded-none" 98 + title="Forked by" 99 + > 100 + {counts.forks} 101 + </Button> 102 + </div> 103 + </div> 104 + </div> 105 + 106 + <div class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-foreground-muted"> 107 + {#if repo.description} 108 + <span>{repo.description}</span> 109 + {:else} 110 + <span class="italic">This repository has no description</span> 111 + {/if} 112 + 113 + {#if repo.website} 114 + <span class="flex items-center gap-1"> 115 + <Globe class="size-4 shrink-0" aria-hidden="true" /> 116 + <a href={repo.website} rel="external ugc nofollow noopener noreferrer" 117 + >{trimScheme(repo.website)}</a 118 + > 119 + </span> 120 + {/if} 121 + 122 + {#if repo.topics?.length} 123 + <div class="flex flex-wrap items-center gap-1"> 124 + {#each repo.topics as topic (topic)} 125 + <Tag color="gray">{topic}</Tag> 126 + {/each} 127 + </div> 128 + {/if} 129 + </div> 130 + </section>
+99
web/src/lib/components/repo/RepoIndexView.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import type { loadRepoIndex } from "$lib/api/repoIndex"; 4 + import StoryAuthProvider from "../../../../.storybook/StoryAuthProvider.svelte"; 5 + import RepoIndexView from "./RepoIndexView.svelte"; 6 + import type { RepoInfo } from "./types"; 7 + 8 + const repo: RepoInfo = { 9 + uri: "at://did:plc:repo/sh.tangled.repo.repo/3jzrepo", 10 + rkey: "3jzrepo", 11 + name: "tangled", 12 + ownerDid: "did:plc:owner", 13 + ownerHandle: "dawn", 14 + repoDid: "did:plc:repo", 15 + knot: "knot1.tangled.sh", 16 + defaultBranch: "main" 17 + }; 18 + 19 + const data = { 20 + ref: "main", 21 + isEmpty: false, 22 + needsUpgrade: false, 23 + knotUnreachable: false, 24 + files: [ 25 + { name: "src", kind: "directory" as const, size: 0 }, 26 + { name: "README.md", kind: "file" as const, size: 512 } 27 + ], 28 + readme: { filename: "README.md", contents: "# tangled" }, 29 + readmeHtml: "<h1>tangled</h1>", 30 + commits: [ 31 + { 32 + hash: "0123456789abcdef0123456789abcdef01234567", 33 + shortHash: "01234567", 34 + subject: "add the repository index", 35 + body: "", 36 + authorName: "dawn", 37 + authorEmail: "dawn@example.test", 38 + when: "2026-07-28T09:00:00Z" 39 + } 40 + ], 41 + tagsByCommit: {}, 42 + totalCommits: 42, 43 + branches: [ 44 + { 45 + name: "main", 46 + hash: "0123456789abcdef0123456789abcdef01234567", 47 + when: "2026-07-28T09:00:00Z", 48 + isDefault: true 49 + } 50 + ], 51 + totalBranches: 1, 52 + tags: [{ name: "v1.0.0", hash: "abcdef0123456789", commitHash: "0123456789abcdef" }], 53 + totalTags: 1, 54 + refs: { branches: ["main"], tags: ["v1.0.0"], capped: false }, 55 + languages: [ 56 + { name: "TypeScript", percentage: 70, share: 70 }, 57 + { name: "Svelte", percentage: 30, share: 30 } 58 + ] 59 + } satisfies Awaited<ReturnType<typeof loadRepoIndex>>; 60 + const cappedData = { 61 + ...data, 62 + totalBranches: 100, 63 + totalTags: 100, 64 + refs: { ...data.refs, capped: true } 65 + } satisfies Awaited<ReturnType<typeof loadRepoIndex>>; 66 + const emptyData = { 67 + ...data, 68 + isEmpty: true, 69 + files: [], 70 + readme: null, 71 + readmeHtml: null, 72 + commits: [], 73 + tagsByCommit: {}, 74 + totalCommits: 0, 75 + branches: [], 76 + totalBranches: 0, 77 + tags: [], 78 + totalTags: 0, 79 + refs: { branches: [], tags: [], capped: false }, 80 + languages: [] 81 + } satisfies Awaited<ReturnType<typeof loadRepoIndex>>; 82 + 83 + const { Story } = defineMeta({ 84 + title: "Repo/RepoIndexView", 85 + component: RepoIndexView, 86 + tags: ["autodocs"], 87 + args: { repo, data, bobbinUrl: "https://bobbin.example.test" } 88 + }); 89 + </script> 90 + 91 + <Story name="Populated repository" /> 92 + <Story name="Reference counts are lower bounds" args={{ data: cappedData }} /> 93 + <Story name="Knot needs upgrade" args={{ data: { ...data, needsUpgrade: true } }} /> 94 + <Story name="Knot is unreachable" args={{ data: { ...data, knotUnreachable: true } }} /> 95 + <Story name="Empty repository" asChild> 96 + <StoryAuthProvider> 97 + <RepoIndexView {repo} data={emptyData} bobbinUrl="https://bobbin.example.test" /> 98 + </StoryAuthProvider> 99 + </Story>
+140
web/src/lib/components/repo/RepoIndexView.svelte
··· 1 + <script lang="ts"> 2 + import GitBranch from "$icon/git-branch"; 3 + import Logs from "$icon/logs"; 4 + import Tags from "$icon/tags"; 5 + import TriangleAlert from "$icon/triangle-alert"; 6 + import BranchList from "./BranchList.svelte"; 7 + import CommitList from "./CommitList.svelte"; 8 + import EmptyRepo from "./EmptyRepo.svelte"; 9 + import FileTree from "./FileTree.svelte"; 10 + import LanguageBar from "./LanguageBar.svelte"; 11 + import PanelHeader from "./PanelHeader.svelte"; 12 + import Readme from "./Readme.svelte"; 13 + import RepoToolbar from "./RepoToolbar.svelte"; 14 + import TagList from "./TagList.svelte"; 15 + import type { loadRepoIndex } from "$lib/api/repoIndex"; 16 + import type { RepoInfo } from "./types"; 17 + 18 + interface Props { 19 + repo: RepoInfo; 20 + data: Awaited<ReturnType<typeof loadRepoIndex>>; 21 + bobbinUrl: string; 22 + } 23 + 24 + let { repo, data, bobbinUrl }: Props = $props(); 25 + 26 + const base = $derived(`/${repo.ownerHandle}/${repo.name}`); 27 + const encodedRef = $derived(encodeURIComponent(data.ref)); 28 + const refsCapped = $derived(data.refs.capped); 29 + </script> 30 + 31 + <section 32 + class="relative mx-auto w-full rounded bg-background-default px-6 py-4 text-foreground-default" 33 + > 34 + {#if data.needsUpgrade} 35 + <div class="flex h-96 items-center justify-center text-center text-foreground-danger"> 36 + <div> 37 + <span class="flex items-center justify-center gap-2"> 38 + <TriangleAlert class="size-5 shrink-0" aria-hidden="true" /> 39 + The knot hosting this repository needs an upgrade. 40 + </span> 41 + <p class="mt-2"> 42 + This repository is currently unavailable. 43 + <a 44 + href="https://docs.tangled.org/migrating-knots-and-spindles.html" 45 + rel="external noopener noreferrer" 46 + class="underline">Read the upgrade guide</a 47 + > 48 + </p> 49 + </div> 50 + </div> 51 + {:else if data.knotUnreachable} 52 + <div class="flex h-96 items-center justify-center text-center text-foreground-danger"> 53 + <span class="flex items-center gap-2"> 54 + <TriangleAlert class="size-5 shrink-0" aria-hidden="true" /> 55 + The knot hosting this repository is unreachable. 56 + </span> 57 + </div> 58 + {:else if data.isEmpty} 59 + <EmptyRepo {repo} /> 60 + {:else} 61 + {#if data.languages.length > 0} 62 + <LanguageBar languages={data.languages} /> 63 + {/if} 64 + 65 + <RepoToolbar 66 + {repo} 67 + ref={data.ref} 68 + refs={data.refs} 69 + totalCommits={data.totalCommits} 70 + totalBranches={data.totalBranches} 71 + totalTags={data.totalTags} 72 + {bobbinUrl} 73 + /> 74 + 75 + <div class="grid grid-cols-1 gap-2 md:grid-cols-2"> 76 + <div class="min-w-0 md:border-r md:border-border-default md:pr-2"> 77 + <FileTree 78 + ownerHandle={repo.ownerHandle} 79 + repoName={repo.name} 80 + ref={data.ref} 81 + entries={data.files} 82 + /> 83 + </div> 84 + 85 + <div class="hidden md:block"> 86 + {#if data.commits.length > 0} 87 + <div class="px-2 pb-4"> 88 + <PanelHeader 89 + title="Commits" 90 + icon={Logs} 91 + href={`${base}/commits/${encodedRef}`} 92 + count={data.totalCommits} 93 + /> 94 + <CommitList 95 + ownerHandle={repo.ownerHandle} 96 + repoName={repo.name} 97 + commits={data.commits} 98 + tagsByCommit={data.tagsByCommit} 99 + /> 100 + </div> 101 + {/if} 102 + 103 + {#if data.branches.length > 0} 104 + <div class="border-t border-border-default px-2 py-4"> 105 + <PanelHeader 106 + title="Branches" 107 + icon={GitBranch} 108 + href={`${base}/branches`} 109 + count={data.totalBranches} 110 + approximate={refsCapped} 111 + /> 112 + <BranchList 113 + ownerHandle={repo.ownerHandle} 114 + repoName={repo.name} 115 + currentRef={data.ref} 116 + branches={data.branches} 117 + /> 118 + </div> 119 + {/if} 120 + 121 + {#if data.tags.length > 0} 122 + <div class="border-t border-border-default px-2 py-4"> 123 + <PanelHeader 124 + title="Tags" 125 + icon={Tags} 126 + href={`${base}/tags`} 127 + count={data.totalTags} 128 + approximate={refsCapped} 129 + /> 130 + <TagList ownerHandle={repo.ownerHandle} repoName={repo.name} tags={data.tags} /> 131 + </div> 132 + {/if} 133 + </div> 134 + </div> 135 + {/if} 136 + </section> 137 + 138 + {#if data.readme} 139 + <Readme filename={data.readme.filename} contents={data.readme.contents} html={data.readmeHtml} /> 140 + {/if}
+31
web/src/lib/components/repo/RepoTabs.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import RepoTabs from "./RepoTabs.svelte"; 4 + import type { RepoInfo } from "./types"; 5 + 6 + const repo: RepoInfo = { 7 + uri: "at://did:plc:repo/sh.tangled.repo.repo/3jzrepo", 8 + rkey: "3jzrepo", 9 + name: "tangled", 10 + ownerDid: "did:plc:owner", 11 + ownerHandle: "dawn", 12 + knot: "knot1.tangled.sh", 13 + defaultBranch: "main" 14 + }; 15 + 16 + const { Story } = defineMeta({ 17 + title: "Repo/RepoTabs", 18 + component: RepoTabs, 19 + tags: ["autodocs"], 20 + args: { 21 + repo, 22 + counts: { stars: 128, issues: 7, pulls: 3, forks: 12 }, 23 + active: "overview" 24 + } 25 + }); 26 + </script> 27 + 28 + <Story name="Overview" /> 29 + <Story name="Issues" args={{ active: "issues" }} /> 30 + <Story name="Pulls" args={{ active: "pulls" }} /> 31 + <Story name="Pipelines" args={{ active: "pipelines" }} />
+39
web/src/lib/components/repo/RepoTabs.svelte
··· 1 + <script lang="ts"> 2 + import SquareChartGantt from "$icon/square-chart-gantt"; 3 + import CircleDot from "$icon/circle-dot"; 4 + import GitPullRequest from "$icon/git-pull-request"; 5 + import Layers2 from "$icon/layers-2"; 6 + import Tabs, { type TabDef } from "$lib/components/ui/Tabs.svelte"; 7 + import type { RepoCounts, RepoInfo } from "./types"; 8 + 9 + interface Props { 10 + repo: RepoInfo; 11 + counts: RepoCounts; 12 + active: string; 13 + } 14 + 15 + let { repo, counts, active }: Props = $props(); 16 + 17 + const base = $derived(`/${repo.ownerHandle}/${repo.name}`); 18 + 19 + const tabs = $derived<TabDef[]>([ 20 + { id: "overview", label: "Overview", icon: SquareChartGantt, href: base }, 21 + { 22 + id: "issues", 23 + label: "Issues", 24 + icon: CircleDot, 25 + count: counts.issues, 26 + href: `${base}/issues` 27 + }, 28 + { 29 + id: "pulls", 30 + label: "Pulls", 31 + icon: GitPullRequest, 32 + count: counts.pulls, 33 + href: `${base}/pulls` 34 + }, 35 + { id: "pipelines", label: "Pipelines", icon: Layers2, href: `${base}/pipelines` } 36 + ]); 37 + </script> 38 + 39 + <Tabs {tabs} {active} label="repository sections" />
+31
web/src/lib/components/repo/TagList.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import TagList from "./TagList.svelte"; 4 + 5 + const tags = [ 6 + { 7 + name: "v1.0.0", 8 + hash: "0123456789abcdef", 9 + commitHash: "0123456789abcdef", 10 + when: "2026-07-28T09:00:00Z", 11 + message: "first stable release" 12 + }, 13 + { 14 + name: "v0.9.0", 15 + hash: "abcdef0123456789", 16 + commitHash: "abcdef0123456789", 17 + when: "2026-07-20T09:00:00Z" 18 + } 19 + ]; 20 + 21 + const { Story } = defineMeta({ 22 + title: "Repo/TagList", 23 + component: TagList, 24 + tags: ["autodocs"], 25 + args: { ownerHandle: "dawn", repoName: "tangled", tags } 26 + }); 27 + </script> 28 + 29 + <Story name="Latest tag and release history" /> 30 + <Story name="Tag without timestamp" args={{ tags: [tags[1]] }} /> 31 + <Story name="No tags" args={{ tags: [] }} />
+39
web/src/lib/components/repo/TagList.svelte
··· 1 + <script lang="ts"> 2 + import { resolve } from "$app/paths"; 3 + import Tag from "$lib/components/ui/Tag.svelte"; 4 + import TimeAgo from "$lib/components/ui/TimeAgo.svelte"; 5 + import type { TagSummary } from "./types"; 6 + 7 + interface Props { 8 + ownerHandle: string; 9 + repoName: string; 10 + tags: TagSummary[]; 11 + } 12 + 13 + let { ownerHandle, repoName, tags }: Props = $props(); 14 + 15 + const base = $derived(`/${ownerHandle}/${repoName}`); 16 + </script> 17 + 18 + <div class="flex flex-col gap-1"> 19 + {#each tags as tag, index (tag.name)} 20 + <div> 21 + <div class="flex items-center gap-2 text-base"> 22 + <a 23 + href={resolve(`${base}/tags/${encodeURIComponent(tag.name)}` as "/")} 24 + class="truncate text-foreground-default no-underline hover:underline" 25 + > 26 + {tag.name} 27 + </a> 28 + </div> 29 + <div class="flex items-center gap-2"> 30 + {#if tag.when} 31 + <TimeAgo value={tag.when} class="text-xs text-foreground-subtle" /> 32 + {/if} 33 + {#if index === 0} 34 + <Tag color="gray" class="font-mono">Latest</Tag> 35 + {/if} 36 + </div> 37 + </div> 38 + {/each} 39 + </div>
+672
web/src/lib/components/repo/language-colors.ts
··· 1 + // language colours as the appview renders them, ported from go-enry's 2 + // LanguagesColor, itself extracted from github/linguist 537297cdae3a 3 + // the knot names languages with enry too, so these keys line up 4 + 5 + export const LANGUAGE_COLORS: Record<string, string> = { 6 + "1C Enterprise": "#814CCC", 7 + "2-Dimensional Array": "#38761D", 8 + "4D": "#004289", 9 + ABAP: "#E8274B", 10 + "ABAP CDS": "#555e25", 11 + "AGS Script": "#B9D9FF", 12 + AIDL: "#34EB6B", 13 + AL: "#3AA2B5", 14 + ALGOL: "#D1E0DB", 15 + AMPL: "#E6EFBB", 16 + ANTLR: "#9DC3FF", 17 + "API Blueprint": "#2ACCA8", 18 + APL: "#5A8164", 19 + "ASP.NET": "#9400ff", 20 + ATS: "#1ac620", 21 + ActionScript: "#882B0F", 22 + Ada: "#02f88c", 23 + "Adblock Filter List": "#800000", 24 + "Adobe Font Metrics": "#fa0f00", 25 + Agda: "#315665", 26 + Aiken: "#640ff8", 27 + Alloy: "#64C800", 28 + "Alpine Abuild": "#0D597F", 29 + "Altium Designer": "#A89663", 30 + AngelScript: "#C7D7DC", 31 + "Answer Set Programming": "#A9CC29", 32 + "Ant Build System": "#A9157E", 33 + Antlers: "#ff269e", 34 + ApacheConf: "#d12127", 35 + Apex: "#1797c0", 36 + "Apollo Guidance Computer": "#0B3D91", 37 + AppleScript: "#101F1F", 38 + Arc: "#aa2afe", 39 + AsciiDoc: "#73a0c5", 40 + AspectJ: "#a957b0", 41 + Assembly: "#6E4C13", 42 + Astro: "#ff5a03", 43 + Asymptote: "#ff0000", 44 + Augeas: "#9CC134", 45 + AutoHotkey: "#6594b9", 46 + AutoIt: "#1C3552", 47 + "Avro IDL": "#0040FF", 48 + Awk: "#c30e9b", 49 + "B (Formal Method)": "#8aa8c5", 50 + B4X: "#00e4ff", 51 + BASIC: "#ff0000", 52 + BQN: "#2b7067", 53 + Ballerina: "#FF5000", 54 + Batchfile: "#C1F12E", 55 + Beef: "#a52f4e", 56 + Berry: "#15A13C", 57 + BibTeX: "#778899", 58 + Bicep: "#519aba", 59 + Bikeshed: "#5562ac", 60 + Bison: "#6A463F", 61 + BitBake: "#00bce4", 62 + Blade: "#f7523f", 63 + BlitzBasic: "#00FFAE", 64 + BlitzMax: "#cd6400", 65 + Bluespec: "#12223c", 66 + "Bluespec BH": "#12223c", 67 + Boo: "#d4bec1", 68 + Boogie: "#c80fa0", 69 + Brainfuck: "#2F2530", 70 + BrighterScript: "#66AABB", 71 + Brightscript: "#662D91", 72 + Browserslist: "#ffd539", 73 + Bru: "#F4AA41", 74 + BuildStream: "#006bff", 75 + C: "#555555", 76 + "C#": "#178600", 77 + "C++": "#f34b7d", 78 + C3: "#2563eb", 79 + "CAP CDS": "#0092d1", 80 + CLIPS: "#00A300", 81 + CMake: "#DA3434", 82 + COLLADA: "#F1A42B", 83 + CQL: "#006091", 84 + CSON: "#244776", 85 + CSS: "#663399", 86 + CSV: "#237346", 87 + CUE: "#5886E1", 88 + CWeb: "#00007a", 89 + "Cabal Config": "#483465", 90 + Caddyfile: "#22b638", 91 + Cadence: "#00ef8b", 92 + Cairo: "#ff4a48", 93 + "Cairo Zero": "#ff4a48", 94 + CameLIGO: "#3be133", 95 + Cangjie: "#00868B", 96 + "Cap'n Proto": "#c42727", 97 + Carbon: "#222222", 98 + Ceylon: "#dfa535", 99 + Chapel: "#8dc63f", 100 + ChucK: "#3f8000", 101 + Circom: "#707575", 102 + Cirru: "#ccccff", 103 + Clarion: "#db901e", 104 + Clarity: "#5546ff", 105 + "Classic ASP": "#6a40fd", 106 + Clean: "#3F85AF", 107 + Click: "#E4E6F3", 108 + Clojure: "#db5855", 109 + "Closure Templates": "#0d948f", 110 + "Cloud Firestore Security Rules": "#FFA000", 111 + Clue: "#0009b5", 112 + CodeQL: "#140f46", 113 + CoffeeScript: "#244776", 114 + ColdFusion: "#ed2cd6", 115 + "ColdFusion CFC": "#ed2cd6", 116 + "Common Lisp": "#3fb68b", 117 + "Common Workflow Language": "#B5314C", 118 + "Component Pascal": "#B0CE4E", 119 + Cooklang: "#E15A29", 120 + Crystal: "#000100", 121 + Csound: "#1a1a1a", 122 + "Csound Document": "#1a1a1a", 123 + "Csound Score": "#1a1a1a", 124 + Cuda: "#3A4E3A", 125 + Curry: "#531242", 126 + Cylc: "#00b3fd", 127 + Cypher: "#34c0eb", 128 + Cython: "#fedf5b", 129 + D: "#ba595e", 130 + D2: "#526ee8", 131 + DM: "#447265", 132 + Dafny: "#FFEC25", 133 + "Darcs Patch": "#8eff23", 134 + Dart: "#00B4AB", 135 + Daslang: "#d3d3d3", 136 + DataWeave: "#003a52", 137 + "Debian Package Control File": "#D70751", 138 + DenizenScript: "#FBEE96", 139 + Dhall: "#dfafff", 140 + "DirectX 3D File": "#aace60", 141 + Dockerfile: "#384d54", 142 + Dogescript: "#cca760", 143 + Dotenv: "#e5d559", 144 + Dune: "#89421e", 145 + Dylan: "#6c616e", 146 + E: "#ccce35", 147 + ECL: "#8a1267", 148 + ECLiPSe: "#001d9d", 149 + EJS: "#a91e50", 150 + EQ: "#a78649", 151 + Earthly: "#2af0ff", 152 + Easybuild: "#069406", 153 + "Ecere Projects": "#913960", 154 + Ecmarkup: "#eb8131", 155 + Edge: "#0dffe0", 156 + EdgeQL: "#31A7FF", 157 + EditorConfig: "#fff1f2", 158 + Eiffel: "#4d6977", 159 + Elixir: "#6e4a7e", 160 + Elm: "#60B5CC", 161 + Elvish: "#55BB55", 162 + "Elvish Transcript": "#55BB55", 163 + "Emacs Lisp": "#c065db", 164 + EmberScript: "#FFF4F3", 165 + Erlang: "#B83998", 166 + Euphoria: "#FF790B", 167 + "F#": "#b845fc", 168 + "F*": "#572e30", 169 + "FIGlet Font": "#FFDDBB", 170 + FIRRTL: "#2f632f", 171 + FLUX: "#88ccff", 172 + Factor: "#636746", 173 + Fancy: "#7b9db4", 174 + Fantom: "#14253c", 175 + Faust: "#c37240", 176 + Fennel: "#fff3d7", 177 + "Filebench WML": "#F6B900", 178 + FlatBuffers: "#ed284a", 179 + Flix: "#d44a45", 180 + Fluent: "#ffcc33", 181 + Forth: "#341708", 182 + Fortran: "#4d41b1", 183 + "Fortran Free Form": "#4d41b1", 184 + FreeBASIC: "#141AC9", 185 + FreeMarker: "#0050b2", 186 + Frege: "#00cafe", 187 + Futhark: "#5f021f", 188 + "G-code": "#D08CF2", 189 + GAML: "#FFC766", 190 + GAMS: "#f49a22", 191 + GAP: "#0000cc", 192 + "GCC Machine Description": "#FFCFAB", 193 + GDScript: "#355570", 194 + GDShader: "#478CBF", 195 + GEDCOM: "#003058", 196 + GLSL: "#5686a5", 197 + GSC: "#FF6800", 198 + "Game Maker Language": "#71b417", 199 + "Gemfile.lock": "#701516", 200 + Gemini: "#ff6900", 201 + "Genero 4gl": "#63408e", 202 + "Genero per": "#d8df39", 203 + Genie: "#fb855d", 204 + Genshi: "#951531", 205 + "Gentoo Ebuild": "#9400ff", 206 + "Gentoo Eclass": "#9400ff", 207 + "Gerber Image": "#d20b00", 208 + Gherkin: "#5B2063", 209 + "Git Attributes": "#F44D27", 210 + "Git Commit": "#F44D27", 211 + "Git Config": "#F44D27", 212 + "Git Revision List": "#F44D27", 213 + Gleam: "#ffaff3", 214 + "Glimmer JS": "#F5835F", 215 + "Glimmer TS": "#3178c6", 216 + Glyph: "#c1ac7f", 217 + Gnuplot: "#f0a9f0", 218 + Go: "#00ADD8", 219 + "Go Checksums": "#00ADD8", 220 + "Go Module": "#00ADD8", 221 + "Go Template": "#00ADD8", 222 + "Go Workspace": "#00ADD8", 223 + "Godot Resource": "#355570", 224 + Golo: "#88562A", 225 + Gosu: "#82937f", 226 + Grace: "#615f8b", 227 + Gradle: "#02303a", 228 + "Gradle Kotlin DSL": "#02303a", 229 + "Grammatical Framework": "#ff0000", 230 + GraphQL: "#e10098", 231 + "Graphviz (DOT)": "#2596be", 232 + Groovy: "#4298b8", 233 + "Groovy Server Pages": "#4298b8", 234 + HAProxy: "#106da9", 235 + HCL: "#844FBA", 236 + HIP: "#4F3A4F", 237 + HLSL: "#aace60", 238 + HOCON: "#9ff8ee", 239 + HTML: "#e34c26", 240 + "HTML+ECR": "#2e1052", 241 + "HTML+EEX": "#6e4a7e", 242 + "HTML+ERB": "#701516", 243 + "HTML+PHP": "#4f5d95", 244 + "HTML+Razor": "#512be4", 245 + HTTP: "#005C9C", 246 + HXML: "#f68712", 247 + Hack: "#878787", 248 + Haml: "#ece2a9", 249 + Handlebars: "#f7931e", 250 + Harbour: "#0e60e3", 251 + Hare: "#9d7424", 252 + Haskell: "#5e5086", 253 + Haxe: "#df7900", 254 + HiveQL: "#dce200", 255 + HolyC: "#ffefaf", 256 + "Hosts File": "#308888", 257 + Hurl: "#FF0288", 258 + Hy: "#7790B2", 259 + IDL: "#a3522f", 260 + "IGOR Pro": "#0000cc", 261 + INI: "#d1dbe0", 262 + ISPC: "#2D68B1", 263 + Idris: "#b30000", 264 + "Ignore List": "#000000", 265 + "ImageJ Macro": "#99AAFF", 266 + Imba: "#16cec6", 267 + "Inno Setup": "#264b99", 268 + Io: "#a9188d", 269 + Ioke: "#078193", 270 + Isabelle: "#FEFE00", 271 + "Isabelle ROOT": "#FEFE00", 272 + J: "#9EEDFF", 273 + "JAR Manifest": "#b07219", 274 + JCL: "#d90e09", 275 + JFlex: "#DBCA00", 276 + JSON: "#292929", 277 + "JSON with Comments": "#292929", 278 + JSON5: "#267CB9", 279 + JSONLD: "#0c479c", 280 + JSONiq: "#40d47e", 281 + Jac: "#FC792D", 282 + Jai: "#ab8b4b", 283 + Janet: "#0886a5", 284 + Jasmin: "#d03600", 285 + Java: "#b07219", 286 + "Java Properties": "#2A6277", 287 + "Java Server Pages": "#2A6277", 288 + "Java Template Engine": "#2A6277", 289 + JavaScript: "#f1e05a", 290 + "JavaScript+ERB": "#f1e05a", 291 + "Jest Snapshot": "#15c213", 292 + "JetBrains MPS": "#21D789", 293 + Jinja: "#a52a22", 294 + Jison: "#56b3cb", 295 + "Jison Lex": "#56b3cb", 296 + Jolie: "#843179", 297 + Jsonnet: "#0064bd", 298 + Julia: "#a270ba", 299 + "Julia REPL": "#a270ba", 300 + "Jupyter Notebook": "#DA5B0B", 301 + Just: "#384d54", 302 + KCL: "#7ABABF", 303 + KDL: "#ffb3b3", 304 + KFramework: "#4195c5", 305 + KRL: "#28430A", 306 + "Kaitai Struct": "#773b37", 307 + KakouneScript: "#6f8042", 308 + KerboScript: "#41adf0", 309 + "KiCad Layout": "#2f4aab", 310 + "KiCad Legacy Layout": "#2f4aab", 311 + "KiCad Schematic": "#2f4aab", 312 + "KoLmafia ASH": "#B9D9B9", 313 + Koka: "#215166", 314 + Kotlin: "#A97BFF", 315 + LFE: "#4C3023", 316 + LLVM: "#185619", 317 + LOLCODE: "#cc9900", 318 + LSL: "#3d9970", 319 + LabVIEW: "#fede06", 320 + Lambdapi: "#8027a3", 321 + Langium: "#2c8c87", 322 + Lark: "#2980B9", 323 + Lasso: "#999999", 324 + Latte: "#f2a542", 325 + Leo: "#C4FFC2", 326 + Less: "#1d365d", 327 + Lex: "#DBCA00", 328 + LigoLANG: "#0e74ff", 329 + LilyPond: "#9ccc7c", 330 + Liquid: "#67b8de", 331 + Liquidsoap: "#990066", 332 + "Literate Agda": "#315665", 333 + "Literate CoffeeScript": "#244776", 334 + "Literate Haskell": "#5e5086", 335 + "LiveCode Script": "#0c5ba5", 336 + LiveScript: "#499886", 337 + Logtalk: "#295b9a", 338 + LookML: "#652B81", 339 + Lua: "#000080", 340 + Luau: "#00A2FF", 341 + M3U: "#179C7D", 342 + MATLAB: "#e16737", 343 + MAXScript: "#00a6a6", 344 + MDX: "#fcb32c", 345 + MLIR: "#5EC8DB", 346 + MQL4: "#62A8D6", 347 + MQL5: "#4A76B8", 348 + MTML: "#b7e1f4", 349 + Macaulay2: "#d8ffff", 350 + Makefile: "#427819", 351 + Mako: "#7e858d", 352 + Markdown: "#083fa1", 353 + Marko: "#42bff2", 354 + Mask: "#f97732", 355 + "Mathematical Programming System": "#0530ad", 356 + Max: "#c4a79c", 357 + MeTTa: "#6a5acd", 358 + Mercury: "#ff2b2b", 359 + Mermaid: "#ff3670", 360 + Meson: "#007800", 361 + Metal: "#8f14e9", 362 + MiniYAML: "#ff1111", 363 + MiniZinc: "#06a9e6", 364 + Mint: "#02b046", 365 + Mirah: "#c7a938", 366 + Modelica: "#de1d31", 367 + "Modula-2": "#10253f", 368 + "Modula-3": "#223388", 369 + Mojo: "#ff4c1f", 370 + "Monkey C": "#8D6747", 371 + MoonBit: "#b92381", 372 + MoonScript: "#ff4585", 373 + Motoko: "#fbb03b", 374 + "Motorola 68K Assembly": "#005daa", 375 + Move: "#4a137a", 376 + Mustache: "#724b3b", 377 + NCL: "#28431f", 378 + NMODL: "#00356B", 379 + "NPM Config": "#cb3837", 380 + NWScript: "#111522", 381 + Nasal: "#1d2c4e", 382 + Nearley: "#990000", 383 + Nemerle: "#3d3c6e", 384 + NetLinx: "#0aa0ff", 385 + "NetLinx+ERB": "#747faa", 386 + NetLogo: "#ff6375", 387 + NewLisp: "#87AED7", 388 + Nextflow: "#3ac486", 389 + Nginx: "#009639", 390 + Nickel: "#E0C3FC", 391 + Nim: "#ffc200", 392 + Nit: "#009917", 393 + Nix: "#7e7eff", 394 + Noir: "#2f1f49", 395 + Nu: "#c9df40", 396 + NumPy: "#9C8AF9", 397 + Nunjucks: "#3d8137", 398 + Nushell: "#4E9906", 399 + "OASv2-json": "#85ea2d", 400 + "OASv2-yaml": "#85ea2d", 401 + "OASv3-json": "#85ea2d", 402 + "OASv3-yaml": "#85ea2d", 403 + OCaml: "#ef7a08", 404 + "OMNeT++ MSG": "#a0e0a0", 405 + "OMNeT++ NED": "#08607c", 406 + ObjectScript: "#424893", 407 + "Objective-C": "#438eff", 408 + "Objective-C++": "#6866fb", 409 + "Objective-J": "#ff0c5a", 410 + Odin: "#60AFFE", 411 + Omgrofl: "#cabbff", 412 + Opal: "#f7ede0", 413 + "Open Policy Agent": "#7d9199", 414 + "OpenAPI Specification v2": "#85ea2d", 415 + "OpenAPI Specification v3": "#85ea2d", 416 + OpenCL: "#ed2e2d", 417 + "OpenEdge ABL": "#5ce600", 418 + OpenQASM: "#AA70FF", 419 + OpenSCAD: "#e5cd45", 420 + "Option List": "#476732", 421 + Org: "#77aa99", 422 + OverpassQL: "#cce2aa", 423 + Oxygene: "#cdd0e3", 424 + Oz: "#fab738", 425 + P4: "#7055b5", 426 + PDDL: "#0d00ff", 427 + "PEG.js": "#234d6b", 428 + PHP: "#4F5D95", 429 + PLSQL: "#dad8d8", 430 + PLpgSQL: "#336790", 431 + "POV-Ray SDL": "#6bac65", 432 + Pact: "#F7A8B8", 433 + Pan: "#cc0000", 434 + Papyrus: "#6600cc", 435 + Parrot: "#f3ca0a", 436 + Pascal: "#E3F171", 437 + Pawn: "#dbb284", 438 + Pep8: "#C76F5B", 439 + Perl: "#0298c3", 440 + PicoLisp: "#6067af", 441 + PigLatin: "#fcd7de", 442 + Pike: "#005390", 443 + "Pip Requirements": "#FFD343", 444 + Pkl: "#6b9543", 445 + PlantUML: "#fbbd16", 446 + PogoScript: "#d80074", 447 + Polar: "#ae81ff", 448 + Portugol: "#f8bd00", 449 + PostCSS: "#dc3a0c", 450 + PostScript: "#da291c", 451 + PowerBuilder: "#8f0f8d", 452 + PowerShell: "#012456", 453 + Praat: "#c8506d", 454 + Prisma: "#0c344b", 455 + Processing: "#0096D8", 456 + Procfile: "#3B2F63", 457 + Prolog: "#74283c", 458 + Promela: "#de0000", 459 + "Propeller Spin": "#7fa2a7", 460 + Pug: "#a86454", 461 + Puppet: "#302B6D", 462 + PureBasic: "#5a6986", 463 + PureScript: "#1D222D", 464 + Pyret: "#ee1e10", 465 + Python: "#3572A5", 466 + "Python console": "#3572A5", 467 + "Python traceback": "#3572A5", 468 + "Q#": "#fed659", 469 + QML: "#44a51c", 470 + "Qt Script": "#00b841", 471 + Quake: "#882233", 472 + QuakeC: "#975777", 473 + QuickBASIC: "#008080", 474 + R: "#198CE7", 475 + RAML: "#77d9fb", 476 + RAScript: "#2C97FA", 477 + RBS: "#701516", 478 + RDoc: "#701516", 479 + REXX: "#d90e09", 480 + RMarkdown: "#198ce7", 481 + RON: "#a62c00", 482 + "ROS Interface": "#22314e", 483 + RPGLE: "#2BDE21", 484 + RUNOFF: "#665a4e", 485 + Racket: "#3c5caa", 486 + Ragel: "#9d5200", 487 + Raku: "#0000fb", 488 + Rascal: "#fffaa0", 489 + ReScript: "#ed5051", 490 + Reason: "#ff5847", 491 + ReasonLIGO: "#ff5847", 492 + Rebol: "#358a5b", 493 + "Record Jar": "#0673ba", 494 + Red: "#f50000", 495 + "Regular Expression": "#009a00", 496 + "Ren'Py": "#ff7f7f", 497 + Rez: "#FFDAB3", 498 + Ring: "#2D54CB", 499 + Riot: "#A71E49", 500 + RobotFramework: "#00c0b5", 501 + Roc: "#7c38f5", 502 + "Rocq Prover": "#d0b68c", 503 + Roff: "#ecdebe", 504 + "Roff Manpage": "#ecdebe", 505 + Rouge: "#cc0088", 506 + "RouterOS Script": "#DE3941", 507 + Ruby: "#701516", 508 + Rust: "#dea584", 509 + SAS: "#B34936", 510 + SCSS: "#c6538c", 511 + SPARQL: "#0C4597", 512 + SQF: "#3F3F3F", 513 + SQL: "#e38c00", 514 + SQLPL: "#e38c00", 515 + "SRecode Template": "#348a34", 516 + STL: "#373b5e", 517 + SVG: "#ff9900", 518 + Sail: "#259dd5", 519 + SaltStack: "#646464", 520 + Sass: "#a53b70", 521 + Scala: "#c22d40", 522 + Scaml: "#bd181a", 523 + Scenic: "#fdc700", 524 + Scheme: "#1e4aec", 525 + Scilab: "#ca0f21", 526 + Self: "#0579aa", 527 + ShaderLab: "#222c37", 528 + Shell: "#89e051", 529 + "ShellCheck Config": "#cecfcb", 530 + Shen: "#120F14", 531 + "Simple File Verification": "#C9BFED", 532 + Singularity: "#64E6AD", 533 + Slang: "#1fbec9", 534 + Slash: "#007eff", 535 + Slice: "#003fa2", 536 + Slim: "#2b2b2b", 537 + Slint: "#2379F4", 538 + SmPL: "#c94949", 539 + Smalltalk: "#596706", 540 + Smarty: "#f0c040", 541 + Smithy: "#c44536", 542 + Snakemake: "#419179", 543 + Solidity: "#AA6746", 544 + SourcePawn: "#f69e1d", 545 + Squirrel: "#800000", 546 + Stan: "#b2011d", 547 + "Standard ML": "#dc566d", 548 + Starlark: "#76d275", 549 + Stata: "#1a5f91", 550 + StringTemplate: "#3fb34f", 551 + Stylus: "#ff6347", 552 + "SubRip Text": "#9e0101", 553 + SugarSS: "#2fcc9f", 554 + SuperCollider: "#46390b", 555 + SurrealQL: "#ff00a0", 556 + "Survex data": "#ffcc99", 557 + Svelte: "#ff3e00", 558 + Sway: "#00F58C", 559 + Sweave: "#198ce7", 560 + Swift: "#F05138", 561 + SystemVerilog: "#DAE1C2", 562 + "TI Program": "#A0AA87", 563 + "TL-Verilog": "#C40023", 564 + TLA: "#4b0079", 565 + TMDL: "#f0c913", 566 + TOML: "#9c4221", 567 + TSQL: "#e38c00", 568 + TSV: "#237346", 569 + TSX: "#3178c6", 570 + TXL: "#0178b8", 571 + Tact: "#48b5ff", 572 + Talon: "#333333", 573 + Tcl: "#e4cc98", 574 + TeX: "#3D6117", 575 + Teal: "#00B1BC", 576 + Terra: "#00004c", 577 + "Terraform Template": "#7b42bb", 578 + TextGrid: "#c8506d", 579 + "TextMate Properties": "#df66e4", 580 + Textile: "#ffe7ac", 581 + Thrift: "#D12127", 582 + Toit: "#c2c9fb", 583 + "Tor Config": "#59316b", 584 + "Tree-sitter Query": "#8ea64c", 585 + Turing: "#cf142b", 586 + Twig: "#c1d026", 587 + TypeScript: "#3178c6", 588 + TypeSpec: "#4A3665", 589 + Typst: "#239dad", 590 + "Unified Parallel C": "#4e3617", 591 + "Unity3D Asset": "#222c37", 592 + Uno: "#9933cc", 593 + UnrealScript: "#a54c4d", 594 + "Untyped Plutus Core": "#36adbd", 595 + UrWeb: "#ccccee", 596 + V: "#4f87c4", 597 + VBA: "#867db1", 598 + VBScript: "#15dcdc", 599 + VCL: "#148AA8", 600 + VHDL: "#adb2cb", 601 + Vala: "#a56de2", 602 + "Valve Data Format": "#f26025", 603 + "Velocity Template Language": "#507cff", 604 + Vento: "#ff0080", 605 + Verilog: "#b2b7f8", 606 + "Vim Help File": "#199f4b", 607 + "Vim Script": "#199f4b", 608 + "Vim Snippet": "#199f4b", 609 + "Visual Basic .NET": "#945db7", 610 + "Visual Basic 6.0": "#2c6353", 611 + Volt: "#1F1F1F", 612 + Vue: "#41b883", 613 + Vyper: "#9F4CF2", 614 + WDL: "#42f1f4", 615 + WGSL: "#1a5e9a", 616 + "Web Ontology Language": "#5b70bd", 617 + WebAssembly: "#04133b", 618 + "WebAssembly Interface Type": "#6250e7", 619 + Whiley: "#d5c397", 620 + Wikitext: "#fc5757", 621 + "Windows Registry Entries": "#52d5ff", 622 + "Witcher Script": "#ff0000", 623 + "Wolfram Language": "#dd1100", 624 + Wollok: "#a23738", 625 + "World of Warcraft Addon Data": "#f7e43f", 626 + Wren: "#383838", 627 + X10: "#4B6BEF", 628 + XC: "#99DA07", 629 + XML: "#0060ac", 630 + "XML Property List": "#0060ac", 631 + XQuery: "#5232e7", 632 + XSLT: "#EB8CEB", 633 + Xmake: "#22a079", 634 + Xojo: "#81bd41", 635 + Xonsh: "#285EEF", 636 + Xtend: "#24255d", 637 + YAML: "#cb171e", 638 + YARA: "#220000", 639 + YASnippet: "#32AB90", 640 + Yacc: "#4B6C4B", 641 + Yul: "#794932", 642 + ZAP: "#0d665e", 643 + ZIL: "#dc75e5", 644 + ZenScript: "#00BCD1", 645 + Zephir: "#118f9e", 646 + Zig: "#ec915c", 647 + Zimpl: "#d67711", 648 + Zmodel: "#ff7100", 649 + crontab: "#ead7ac", 650 + eC: "#913960", 651 + fish: "#4aae47", 652 + hoon: "#00b171", 653 + iCalendar: "#ec564c", 654 + jq: "#c7254e", 655 + kvlang: "#1da6e0", 656 + "mIRC Script": "#3d57c3", 657 + mcfunction: "#E22837", 658 + mdsvex: "#5f9ea0", 659 + mupad: "#244963", 660 + nanorc: "#2d004d", 661 + nesC: "#94B0C7", 662 + ooc: "#b0b77e", 663 + q: "#0040cd", 664 + reStructuredText: "#141414", 665 + sed: "#64b970", 666 + templ: "#66D0DD", 667 + vCard: "#ee2647", 668 + wisp: "#7582D1", 669 + xBase: "#403a40" 670 + }; 671 + 672 + export const LANGUAGE_COLOR_FALLBACK = "#cccccc";
+42
web/src/lib/components/repo/types.ts
··· 1 + import type { BranchSummary, CommitSummary, TagSummary, TreeEntrySummary } from "$lib/api/repo"; 2 + 3 + export type { BranchSummary, CommitSummary, TagSummary, TreeEntrySummary }; 4 + 5 + /** the repo every page under [handle]/[repo] is scoped to */ 6 + export interface RepoInfo { 7 + uri: string; 8 + rkey: string; 9 + name: string; 10 + ownerDid: string; 11 + ownerHandle: string; 12 + ownerAvatar?: string; 13 + repoDid?: string; 14 + knot: string; 15 + spindle?: string; 16 + description?: string; 17 + website?: string; 18 + topics?: string[]; 19 + source?: RepoSource; 20 + defaultBranch: string; 21 + } 22 + 23 + /** the repo this one was forked from */ 24 + export interface RepoSource { 25 + ownerHandle: string; 26 + name: string; 27 + } 28 + 29 + export interface RepoCounts { 30 + stars: number; 31 + /** open only, same as the appview's tabs. drop the filter for the total */ 32 + issues: number; 33 + /** open only, so neither merged nor closed */ 34 + pulls: number; 35 + forks: number; 36 + } 37 + 38 + export interface LanguageSlice { 39 + name: string; 40 + percentage: number; 41 + share: number; 42 + }
+5 -5
web/src/lib/components/ui/Checkbox.svelte
··· 27 27 28 28 const inputClasses = [ 29 29 "peer size-4 shrink-0 appearance-none rounded border border-border-default bg-background-default", 30 - "transition-colors duration-150", 30 + "transition-colors duration-150 checked:transition-none indeterminate:transition-none", 31 31 "hover:cursor-pointer hover:bg-background-inset", 32 - "checked:border-foreground-default checked:bg-foreground-default checked:hover:bg-gray-700", 33 - "indeterminate:border-foreground-default indeterminate:bg-foreground-default indeterminate:hover:bg-gray-700", 32 + "checked:border-foreground-default checked:bg-foreground-default checked:hover:bg-foreground-default", 33 + "indeterminate:border-foreground-default indeterminate:bg-foreground-default indeterminate:hover:bg-foreground-default", 34 34 "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground-default", 35 35 "disabled:hover:pointer-events-none disabled:hover:bg-background-default" 36 36 ].join(" "); 37 37 38 38 const checkClasses = [ 39 - "pointer-events-none absolute inset-0 m-auto hidden size-3.5 text-foreground-on-emphasis", 39 + "pointer-events-none absolute inset-0 m-auto hidden size-3.5 text-background-default", 40 40 "peer-checked:block peer-indeterminate:hidden", 41 41 "peer-disabled:text-foreground-disabled peer-disabled:hover:cursor-default" 42 42 ].join(" "); 43 43 44 44 const minusClasses = [ 45 - "pointer-events-none absolute inset-0 m-auto hidden size-3.5 text-foreground-on-emphasis", 45 + "pointer-events-none absolute inset-0 m-auto hidden size-3.5 text-background-default", 46 46 "peer-indeterminate:block", 47 47 "peer-disabled:text-foreground-disabled peer-disabled:hover:cursor-default" 48 48 ].join(" ");
+71 -59
web/src/lib/components/ui/Dropdown.svelte
··· 7 7 root: "relative inline-block", 8 8 plainTrigger: 9 9 "flex cursor-pointer items-center gap-1 border-0 bg-transparent p-0 text-inherit", 10 - menu: "absolute z-50 mt-2 w-56 divide-y divide-border-default overflow-hidden rounded border border-border-default bg-background-default text-sm shadow-menu" 10 + menu: "dropdown-menu fixed z-50 m-0 max-h-[calc(100dvh-1rem)] w-56 max-w-[calc(100vw-1rem)] divide-y divide-border-default overflow-auto rounded border border-border-default bg-background-default text-sm shadow-menu" 11 11 }, 12 12 variants: { 13 13 align: { 14 - left: { 15 - menu: "left-0" 16 - }, 17 - right: { 18 - menu: "right-0" 19 - } 20 - }, 21 - open: { 22 - true: { 23 - menu: "block" 24 - }, 25 - false: { 26 - menu: "hidden" 27 - } 14 + left: {}, 15 + right: {} 28 16 } 29 17 }, 30 18 defaultVariants: { 31 - align: "left", 32 - open: false 19 + align: "left" 33 20 } 34 21 }); 35 22 ··· 63 50 <script lang="ts"> 64 51 import type { Snippet } from "svelte"; 65 52 import { onMount, setContext, tick } from "svelte"; 66 - import Button from "./Button.svelte"; 53 + import Button, { type ButtonVariants } from "./Button.svelte"; 67 54 68 55 interface Props { 69 56 id?: string; 70 57 group?: string; 71 58 variant?: "plain" | "button"; 59 + buttonVariant?: ButtonVariants["variant"]; 72 60 align?: DropdownVariants["align"]; 73 61 label?: string; 62 + menuClass?: string; 74 63 trigger: Snippet; 75 64 children: Snippet; 76 65 } 77 66 78 - let { id, group, variant = "plain", align = "left", label, trigger, children }: Props = $props(); 67 + let { 68 + id, 69 + group, 70 + variant = "plain", 71 + buttonVariant = "default", 72 + align = "left", 73 + label, 74 + menuClass, 75 + trigger, 76 + children 77 + }: Props = $props(); 79 78 80 79 let open = $state(false); 81 - const classes = $derived(dropdown({ open, align })); 82 - let root = $state<HTMLElement>(); 80 + const classes = $derived(dropdown({ align })); 83 81 let triggerWrapper = $state<HTMLElement>(); 84 82 let triggerButtonEl = $state<HTMLElement>(); 83 + let menuElement = $state<HTMLElement>(); 85 84 const fallbackId = $props.id(); 86 85 const menuId = $derived(id ?? fallbackId); 87 86 87 + const triggerElement = () => 88 + variant === "button" 89 + ? triggerWrapper?.querySelector<HTMLElement>("button, a") 90 + : triggerButtonEl; 91 + 88 92 const focusTrigger = () => { 89 - if (variant === "button") { 90 - triggerWrapper?.querySelector<HTMLElement>("button, a")?.focus(); 91 - } else { 92 - triggerButtonEl?.focus(); 93 - } 93 + triggerElement()?.focus(); 94 94 }; 95 95 // intentionally not $state: only used for imperative focus, and reactive 96 96 // reads inside DropdownItem's register $effect would loop it forever 97 97 let items: HTMLElement[] = []; 98 98 99 99 const close = () => { 100 - open = false; 100 + if (open) menuElement?.hidePopover(); 101 101 }; 102 102 103 - const toggle = () => { 104 - open = !open; 105 - if (open) closeGroupExcept(group, close); 103 + const onToggle = (event: Event) => { 104 + const { newState } = event as ToggleEvent; 105 + open = newState === "open"; 106 + if (!open) return; 107 + 108 + closeGroupExcept(group, close); 106 109 }; 107 110 108 111 const focusItem = (index: number) => { ··· 114 117 const onTriggerKeydown = (event: KeyboardEvent) => { 115 118 if (!open && (event.key === "ArrowDown" || event.key === "ArrowUp")) { 116 119 event.preventDefault(); 117 - open = true; 118 - closeGroupExcept(group, close); 120 + triggerElement()?.click(); 119 121 } 120 122 }; 121 123 ··· 140 142 break; 141 143 case "Escape": 142 144 event.preventDefault(); 143 - close(); 145 + menuElement?.hidePopover(); 144 146 focusTrigger(); 145 147 break; 146 148 case "Tab": 147 - close(); 149 + menuElement?.hidePopover(); 148 150 break; 149 151 } 150 152 }; ··· 174 176 }); 175 177 176 178 onMount(() => { 177 - const unregister = registerDropdown(group, close); 178 - 179 - const onDocumentClick = (event: PointerEvent) => { 180 - if (open && event.target instanceof Node && root && !root.contains(event.target)) close(); 181 - }; 182 - const onKeydown = (event: KeyboardEvent) => { 183 - if (!open) return; 184 - if (event.key === "Escape") { 185 - close(); 186 - focusTrigger(); 187 - } 188 - }; 189 - 190 - document.addEventListener("pointerdown", onDocumentClick); 191 - document.addEventListener("keydown", onKeydown); 192 - 193 - return () => { 194 - unregister(); 195 - document.removeEventListener("pointerdown", onDocumentClick); 196 - document.removeEventListener("keydown", onKeydown); 197 - }; 179 + return registerDropdown(group, close); 198 180 }); 199 181 </script> 200 182 201 - <div bind:this={root} class={classes.root()}> 183 + <div class={classes.root()}> 202 184 {#if variant === "button"} 203 185 <span bind:this={triggerWrapper} class="contents"> 204 186 <Button 187 + variant={buttonVariant} 205 188 class="px-3" 206 - onclick={toggle} 189 + popovertarget={menuId} 190 + popovertargetaction="toggle" 207 191 onkeydown={onTriggerKeydown} 208 192 aria-haspopup="menu" 209 193 aria-expanded={open} ··· 218 202 bind:this={triggerButtonEl} 219 203 type="button" 220 204 class={classes.plainTrigger()} 221 - onclick={toggle} 205 + popovertarget={menuId} 206 + popovertargetaction="toggle" 222 207 onkeydown={onTriggerKeydown} 223 208 aria-haspopup="menu" 224 209 aria-expanded={open} ··· 229 214 </button> 230 215 {/if} 231 216 232 - <div id={menuId} class={classes.menu()} role="menu" tabindex={-1} onkeydown={onMenuKeydown}> 217 + <div 218 + bind:this={menuElement} 219 + id={menuId} 220 + data-align={align} 221 + popover="auto" 222 + ontoggle={onToggle} 223 + class={classes.menu({ class: menuClass })} 224 + role="menu" 225 + tabindex={-1} 226 + onkeydown={onMenuKeydown} 227 + > 233 228 {@render children()} 234 229 </div> 235 230 </div> 231 + 232 + <style> 233 + @supports (position-area: bottom) and (position-try: flip-block) { 234 + .dropdown-menu { 235 + margin: 0.5rem; 236 + position-try: flip-block; 237 + } 238 + 239 + .dropdown-menu[data-align="left"] { 240 + position-area: block-end span-inline-end; 241 + } 242 + 243 + .dropdown-menu[data-align="right"] { 244 + position-area: block-end span-inline-start; 245 + } 246 + } 247 + </style>
+25
web/src/lib/components/ui/TimeAgo.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import TimeAgo from "./TimeAgo.svelte"; 4 + 5 + const recent = "2026-07-28T08:00:00Z"; 6 + 7 + const { Story } = defineMeta({ 8 + title: "UI/TimeAgo", 9 + component: TimeAgo, 10 + tags: ["autodocs"], 11 + argTypes: { 12 + value: { control: "date" }, 13 + variant: { 14 + control: { type: "inline-radio" }, 15 + options: ["compact", "full"] 16 + } 17 + }, 18 + args: { value: recent, variant: "compact" } 19 + }); 20 + </script> 21 + 22 + <Story name="Compact relative time" /> 23 + <Story name="Full relative time" args={{ variant: "full" }} /> 24 + <Story name="Date object input" args={{ value: new Date(recent) }} /> 25 + <Story name="Invalid input renders nothing" args={{ value: "not-a-date" }} />
+18
web/src/lib/components/ui/TimeAgo.svelte
··· 1 + <script lang="ts"> 2 + import { compactRelativeTime, formatDate, relativeTime } from "$lib/format"; 3 + 4 + interface Props { 5 + value: string | Date; 6 + variant?: "compact" | "full"; 7 + class?: string; 8 + } 9 + 10 + let { value, variant = "compact", class: className }: Props = $props(); 11 + 12 + const text = $derived(variant === "compact" ? compactRelativeTime(value) : relativeTime(value)); 13 + const iso = $derived(typeof value === "string" ? value : value.toISOString()); 14 + </script> 15 + 16 + {#if text} 17 + <time datetime={iso} title={formatDate(value)} class={className}>{text}</time> 18 + {/if}
+116
web/src/routes/[handle]/[repo]/+layout.ts
··· 1 + import { error, redirect } from "@sveltejs/kit"; 2 + import { createBobbinClient } from "$lib/api/client"; 3 + import { count } from "$lib/api/count"; 4 + import { getStarRkey } from "$lib/api/graph"; 5 + import { IdentityCache, resolveMiniDoc } from "$lib/api/identity"; 6 + import { getDefaultBranch } from "$lib/api/knot"; 7 + import { parallel, toHttpError } from "$lib/api/load"; 8 + import { getRepo } from "$lib/api/records"; 9 + import { repoNameOf, resolveRepoByName } from "$lib/api/repo"; 10 + import { didFromUri, rkeyFromUri } from "$lib/api/uri"; 11 + import type { BobbinContext } from "$lib/api/client"; 12 + import type { RepoCounts, RepoInfo, RepoSource } from "$lib/components/repo/types"; 13 + import type { LayoutLoad } from "./$types"; 14 + 15 + const FALLBACK_BRANCH = "main"; 16 + 17 + const resolveSource = async ( 18 + ctx: BobbinContext, 19 + uri: string | undefined 20 + ): Promise<RepoSource | null> => { 21 + if (!uri?.startsWith("at://")) return null; 22 + try { 23 + const view = await getRepo(ctx, uri); 24 + const ownerDid = didFromUri(view.uri); 25 + const owner = await new IdentityCache(ctx).resolve(ownerDid).catch(() => null); 26 + return { ownerHandle: owner?.handle ?? ownerDid, name: repoNameOf(view) }; 27 + } catch { 28 + // a fork whose source is gone still renders, just without the attribution 29 + return null; 30 + } 31 + }; 32 + 33 + // the layout reset also cuts us off from `[handle]/+layout.ts`, so identity gets 34 + // resolved again here 35 + export const load: LayoutLoad = async (event) => { 36 + const parent = await event.parent(); 37 + const identifier = decodeURIComponent(event.params.handle); 38 + const name = decodeURIComponent(event.params.repo); 39 + 40 + // rejects bare words so unrelated paths 404 instead of resolving as actors 41 + if (!identifier.startsWith("did:") && !identifier.includes(".")) { 42 + error(404, "Not found"); 43 + } 44 + 45 + const ctx = createBobbinClient({ serviceUrl: parent.publicConfig.bobbinUrl, fetch: event.fetch }); 46 + const doc = await resolveMiniDoc(ctx, identifier).catch((cause) => 47 + toHttpError(cause, "Could not resolve user") 48 + ); 49 + 50 + // redirects dids and stale handles to the canonical handle url 51 + const canonical = doc.handle && !doc.handle.endsWith(".invalid") ? doc.handle : null; 52 + if (canonical && identifier.toLowerCase() !== canonical.toLowerCase()) { 53 + redirect(307, `/${canonical}/${event.params.repo}${event.url.search}`); 54 + } 55 + 56 + const view = await resolveRepoByName(ctx, doc.did, name).catch((cause) => 57 + toHttpError(cause, "Could not load repository") 58 + ); 59 + if (!view) error(404, `${doc.handle}/${name} does not exist`); 60 + 61 + const record = view.value; 62 + const repoDid = record.repoDid; 63 + const viewerDid = parent.auth?.did; 64 + 65 + const stats = await parallel({ 66 + // only the knot knows the default branch. a knot that is down or still 67 + // syncing shouldn't take out the whole layout 68 + defaultBranch: getDefaultBranch(ctx, { repo: view.uri }) 69 + .then((branch) => branch.name) 70 + .catch(() => null), 71 + stars: repoDid 72 + ? count(ctx, "sh.tangled.feed.countStars", repoDid).catch(() => null) 73 + : Promise.resolve(null), 74 + // the tabs count what is still open, closed ones matter on their own pages 75 + issues: repoDid 76 + ? count(ctx, "sh.tangled.repo.countIssues", repoDid, { state: "open" }).catch(() => null) 77 + : Promise.resolve(null), 78 + pulls: repoDid 79 + ? count(ctx, "sh.tangled.repo.countPulls", repoDid, { status: "open" }).catch(() => null) 80 + : Promise.resolve(null), 81 + forks: repoDid 82 + ? count(ctx, "sh.tangled.repo.countForks", repoDid).catch(() => null) 83 + : Promise.resolve(null), 84 + viewerStarRkey: 85 + viewerDid && repoDid 86 + ? getStarRkey(ctx, viewerDid, repoDid).catch(() => null) 87 + : Promise.resolve(null), 88 + source: resolveSource(ctx, record.source) 89 + }); 90 + 91 + const repo: RepoInfo = { 92 + uri: view.uri, 93 + rkey: rkeyFromUri(view.uri), 94 + name: repoNameOf(view), 95 + ownerDid: doc.did, 96 + ownerHandle: doc.handle, 97 + ownerAvatar: doc.avatar, 98 + repoDid, 99 + knot: record.knot, 100 + spindle: record.spindle, 101 + description: record.description, 102 + website: record.website, 103 + topics: record.topics, 104 + source: stats.source ?? undefined, 105 + defaultBranch: stats.defaultBranch ?? FALLBACK_BRANCH 106 + }; 107 + 108 + const counts: RepoCounts = { 109 + stars: stats.stars?.count ?? 0, 110 + issues: stats.issues?.count ?? 0, 111 + pulls: stats.pulls?.count ?? 0, 112 + forks: stats.forks?.count ?? 0 113 + }; 114 + 115 + return { repo, counts, viewerStarRkey: stats.viewerStarRkey }; 116 + };
+43
web/src/routes/[handle]/[repo]/+layout@.svelte
··· 1 + <script lang="ts"> 2 + // the @ resets out of the profile shell, repo pages want their own header and 3 + // tabs instead of the profile card 4 + import { page } from "$app/state"; 5 + import RepoHeader from "$lib/components/repo/RepoHeader.svelte"; 6 + import RepoTabs from "$lib/components/repo/RepoTabs.svelte"; 7 + 8 + let { children, data } = $props(); 9 + 10 + const fullName = $derived(`${data.repo.ownerHandle}/${data.repo.name}`); 11 + const repoUrl = $derived(`${page.url.origin}/${fullName}`); 12 + const fullNameWithoutAt = $derived(fullName.replace(/^@/, "")); 13 + 14 + const activeTab = $derived.by(() => { 15 + const segment = page.route.id?.split("/")[3] ?? ""; 16 + return ["issues", "pulls", "pipelines", "settings"].includes(segment) ? segment : "overview"; 17 + }); 18 + </script> 19 + 20 + <!-- todo: og and twitter card tags, the appview has repo/fragments/og.html --> 21 + <svelte:head> 22 + <title>{fullName} &middot; Tangled</title> 23 + <meta name="description" content={data.repo.description ?? "A repository on Tangled"} /> 24 + <meta name="vcs:clone" content={repoUrl} /> 25 + <meta name="forge:summary" content={repoUrl} /> 26 + <meta name="forge:dir" content={`${repoUrl}/tree/{ref}/{path}`} /> 27 + <meta name="forge:file" content={`${repoUrl}/blob/{ref}/{path}`} /> 28 + <meta name="forge:line" content={`${repoUrl}/blob/{ref}/{path}#L{line}`} /> 29 + <meta 30 + name="go-import" 31 + content={`tangled.sh/${fullNameWithoutAt} git https://tangled.sh/${fullName}`} 32 + /> 33 + <meta 34 + name="go-import" 35 + content={`tangled.org/${fullNameWithoutAt} git https://tangled.org/${fullName}`} 36 + /> 37 + </svelte:head> 38 + 39 + <section class="mx-auto w-full max-w-screen-lg py-6"> 40 + <RepoHeader repo={data.repo} counts={data.counts} viewerStarRkey={data.viewerStarRkey} /> 41 + <RepoTabs repo={data.repo} counts={data.counts} active={activeTab} /> 42 + {@render children()} 43 + </section>
+163
web/src/routes/[handle]/[repo]/+page.svelte
··· 1 + <script lang="ts"> 2 + import { resolve } from "$app/paths"; 3 + import GitBranch from "$icon/git-branch"; 4 + import GitCommitHorizontal from "$icon/git-commit-horizontal"; 5 + import Logs from "$icon/logs"; 6 + import SearchCode from "$icon/search-code"; 7 + import Tags from "$icon/tags"; 8 + import TriangleAlert from "$icon/triangle-alert"; 9 + import BranchList from "$lib/components/repo/BranchList.svelte"; 10 + import CloneDropdown from "$lib/components/repo/CloneDropdown.svelte"; 11 + import CommitList from "$lib/components/repo/CommitList.svelte"; 12 + import EmptyRepo from "$lib/components/repo/EmptyRepo.svelte"; 13 + import FileTree from "$lib/components/repo/FileTree.svelte"; 14 + import LanguageBar from "$lib/components/repo/LanguageBar.svelte"; 15 + import PanelHeader from "$lib/components/repo/PanelHeader.svelte"; 16 + import Readme from "$lib/components/repo/Readme.svelte"; 17 + import RefSelector from "$lib/components/repo/RefSelector.svelte"; 18 + import TagList from "$lib/components/repo/TagList.svelte"; 19 + 20 + let { data } = $props(); 21 + 22 + const repo = $derived(data.repo); 23 + const base = $derived(`/${repo.ownerHandle}/${repo.name}`); 24 + const encodedRef = $derived(encodeURIComponent(data.ref)); 25 + const refsCapped = $derived(data.refs.capped); 26 + </script> 27 + 28 + <section 29 + class="relative mx-auto w-full rounded bg-background-default px-6 py-4 text-foreground-default" 30 + > 31 + {#if data.knotUnreachable} 32 + <div class="flex h-96 items-center justify-center text-center text-foreground-danger"> 33 + <span class="flex items-center gap-2"> 34 + <TriangleAlert class="size-5 shrink-0" aria-hidden="true" /> 35 + The knot hosting this repository is unreachable. 36 + </span> 37 + </div> 38 + {:else if data.isEmpty} 39 + <EmptyRepo {repo} /> 40 + {:else} 41 + {#if data.languages.length > 0} 42 + <LanguageBar languages={data.languages} /> 43 + {/if} 44 + 45 + <div class="flex flex-wrap items-center justify-between gap-3 pb-5"> 46 + <RefSelector 47 + ownerHandle={repo.ownerHandle} 48 + repoName={repo.name} 49 + current={data.ref} 50 + branches={data.refs.branches} 51 + tags={data.refs.tags} 52 + /> 53 + 54 + <form 55 + class="order-last flex h-8 w-full items-center md:order-none md:w-64" 56 + method="GET" 57 + action={resolve(`${base}/search` as "/")} 58 + > 59 + <div class="relative flex h-full w-full items-center"> 60 + <SearchCode 61 + class="pointer-events-none absolute left-2 size-4 text-foreground-placeholder" 62 + aria-hidden="true" 63 + /> 64 + <input 65 + class="h-full w-full rounded border border-border-default bg-background-default py-1 pr-2 pl-8 text-sm outline-none focus:border-border-strong" 66 + type="text" 67 + name="q" 68 + placeholder="Find files or code..." 69 + aria-label="Search this repository" 70 + /> 71 + </div> 72 + </form> 73 + 74 + <div class="flex items-center gap-3"> 75 + <a 76 + href={resolve(`${base}/commits/${encodedRef}` as "/")} 77 + class="inline-flex items-center gap-1 text-sm font-medium text-foreground-default no-underline hover:underline md:hidden" 78 + > 79 + <GitCommitHorizontal class="size-4" aria-hidden="true" /> 80 + {data.totalCommits} 81 + </a> 82 + <a 83 + href={resolve(`${base}/branches` as "/")} 84 + class="inline-flex items-center gap-1 text-sm font-medium text-foreground-default no-underline hover:underline md:hidden" 85 + > 86 + <GitBranch class="size-4" aria-hidden="true" /> 87 + {data.totalBranches} 88 + </a> 89 + <a 90 + href={resolve(`${base}/tags` as "/")} 91 + class="inline-flex items-center gap-1 text-sm font-medium text-foreground-default no-underline hover:underline md:hidden" 92 + > 93 + <Tags class="size-4" aria-hidden="true" /> 94 + {data.totalTags} 95 + </a> 96 + <CloneDropdown {repo} ref={data.ref} bobbinUrl={data.publicConfig.bobbinUrl} /> 97 + </div> 98 + </div> 99 + 100 + <div class="grid grid-cols-1 gap-2 md:grid-cols-2"> 101 + <FileTree 102 + ownerHandle={repo.ownerHandle} 103 + repoName={repo.name} 104 + ref={data.ref} 105 + entries={data.files} 106 + /> 107 + 108 + <div class="hidden md:block"> 109 + {#if data.commits.length > 0} 110 + <div class="px-2 pb-4"> 111 + <PanelHeader 112 + title="Commits" 113 + icon={Logs} 114 + href={`${base}/commits/${encodedRef}`} 115 + count={data.totalCommits} 116 + /> 117 + <CommitList 118 + ownerHandle={repo.ownerHandle} 119 + repoName={repo.name} 120 + commits={data.commits} 121 + tagsByCommit={data.tagsByCommit} 122 + /> 123 + </div> 124 + {/if} 125 + 126 + {#if data.branches.length > 0} 127 + <div class="border-t border-border-default px-2 py-4"> 128 + <PanelHeader 129 + title="Branches" 130 + icon={GitBranch} 131 + href={`${base}/branches`} 132 + count={data.totalBranches} 133 + approximate={refsCapped} 134 + /> 135 + <BranchList 136 + ownerHandle={repo.ownerHandle} 137 + repoName={repo.name} 138 + currentRef={data.ref} 139 + branches={data.branches} 140 + /> 141 + </div> 142 + {/if} 143 + 144 + {#if data.tags.length > 0} 145 + <div class="border-t border-border-default px-2 py-4"> 146 + <PanelHeader 147 + title="Tags" 148 + icon={Tags} 149 + href={`${base}/tags`} 150 + count={data.totalTags} 151 + approximate={refsCapped} 152 + /> 153 + <TagList ownerHandle={repo.ownerHandle} repoName={repo.name} tags={data.tags} /> 154 + </div> 155 + {/if} 156 + </div> 157 + </div> 158 + {/if} 159 + </section> 160 + 161 + {#if data.readme} 162 + <Readme filename={data.readme.filename} contents={data.readme.contents} /> 163 + {/if}
+96
web/src/routes/[handle]/[repo]/+page.ts
··· 1 + import { createBobbinClient } from "$lib/api/client"; 2 + import { languages as knotLanguages, tree as knotTree } from "$lib/api/knot"; 3 + import { parallel } from "$lib/api/load"; 4 + import { 5 + branchesFor, 6 + logFor, 7 + sortTreeEntries, 8 + tagsByCommitHash, 9 + tagsFor, 10 + toBranchSummary, 11 + toCommitSummary, 12 + toTagSummary, 13 + toTreeEntrySummary 14 + } from "$lib/api/repo"; 15 + import type { LanguageSlice } from "$lib/components/repo/types"; 16 + import type { PageLoad } from "./$types"; 17 + 18 + const COMMIT_LIMIT = 10; 19 + const BRANCH_LIMIT = 5; 20 + const TAG_LIMIT = 5; 21 + // knots cap ref listings at 100, so a total is really an "at least" 22 + const REF_LIMIT = 100; 23 + 24 + const orNull = <T>(promise: Promise<T>): Promise<T | null> => promise.catch(() => null); 25 + 26 + const toLanguageSlices = (languages: { name: string; size: number }[]): LanguageSlice[] => { 27 + const sized = languages.filter((language) => language.size > 0); 28 + const total = sized.reduce((sum, language) => sum + language.size, 0); 29 + if (total === 0) return []; 30 + 31 + const slices = sized.map((language) => { 32 + const share = (language.size / total) * 100; 33 + return { name: language.name, share, percentage: Math.floor(share) }; 34 + }); 35 + 36 + const short = 100 - slices.reduce((sum, slice) => sum + slice.percentage, 0); 37 + [...slices] 38 + .sort((a, b) => (b.share % 1) - (a.share % 1) || b.share - a.share) 39 + .slice(0, Math.max(0, short)) 40 + .forEach((slice) => { 41 + slice.percentage += 1; 42 + }); 43 + 44 + return slices.sort((a, b) => b.share - a.share); 45 + }; 46 + 47 + export const load: PageLoad = async (event) => { 48 + const parent = await event.parent(); 49 + const ctx = createBobbinClient({ serviceUrl: parent.publicConfig.bobbinUrl, fetch: event.fetch }); 50 + const repo = parent.repo.uri; 51 + const ref = parent.repo.defaultBranch; 52 + 53 + // every list falls back on its own so a partial page still renders 54 + const results = await parallel({ 55 + tree: orNull(knotTree(ctx, { repo, ref })), 56 + log: orNull(logFor(ctx, repo, ref, COMMIT_LIMIT)), 57 + branches: orNull(branchesFor(ctx, repo, REF_LIMIT)), 58 + tags: orNull(tagsFor(ctx, repo, REF_LIMIT)), 59 + languages: orNull(knotLanguages(ctx, { repo, ref })) 60 + }); 61 + 62 + const branches = (results.branches?.branches ?? []).map(toBranchSummary); 63 + const tags = (results.tags?.tags ?? []).map(toTagSummary); 64 + const commits = (results.log?.commits ?? []).map(toCommitSummary); 65 + const files = sortTreeEntries((results.tree?.files ?? []).map(toTreeEntrySummary)); 66 + 67 + const languages = toLanguageSlices(results.languages?.languages ?? []); 68 + 69 + // nothing answered, so the knot is down or doesn't know this repo 70 + const knotUnreachable = 71 + results.tree === null && results.log === null && results.branches === null; 72 + // the knot answered but there is nothing there, so it was never pushed to 73 + const isEmpty = !knotUnreachable && files.length === 0 && branches.length === 0; 74 + 75 + return { 76 + ref, 77 + isEmpty, 78 + knotUnreachable, 79 + files, 80 + readme: results.tree?.readme ?? null, 81 + commits, 82 + tagsByCommit: tagsByCommitHash(commits, tags), 83 + totalCommits: results.log?.total ?? commits.length, 84 + branches: branches.slice(0, BRANCH_LIMIT), 85 + totalBranches: branches.length, 86 + tags: tags.slice(0, TAG_LIMIT), 87 + totalTags: tags.length, 88 + // the switcher needs every ref, not just the visible slice 89 + refs: { 90 + branches: branches.map((branch) => branch.name), 91 + tags: tags.map((tag) => tag.name), 92 + capped: branches.length >= REF_LIMIT || tags.length >= REF_LIMIT 93 + }, 94 + languages 95 + }; 96 + };