@agent-native/core-corpus 0.212.1 → 0.214.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/onboarding-journeys/SKILL.md +29 -14
  3. package/corpus/templates/analytics/CHANGELOG.md +11 -0
  4. package/corpus/templates/analytics/actions/estimate-onboarding-journey-cost.ts +23 -6
  5. package/corpus/templates/analytics/actions/get-onboarding-journey.ts +20 -5
  6. package/corpus/templates/analytics/actions/provider-api-register.ts +2 -7
  7. package/corpus/templates/analytics/changelog/2026-10-21-analytics-recognizes-questions-about-connected-data-product-.md +6 -0
  8. package/corpus/templates/analytics/changelog/2026-10-21-onboarding-journeys-now-honor-larger-event-row-budgets.md +6 -0
  9. package/corpus/templates/analytics/changelog/2026-10-21-preserve-unknown-replay-availability-in-partial-journeys.md +6 -0
  10. package/corpus/templates/analytics/package.json +6 -0
  11. package/corpus/templates/analytics/scripts/journey-capture-plan.ts +18 -7
  12. package/corpus/templates/analytics/scripts/journey-capture.ts +3 -1
  13. package/corpus/templates/analytics/server/lib/db-admin-connections.ts +19 -12
  14. package/corpus/templates/analytics/server/lib/journey-tree.ts +24 -9
  15. package/corpus/templates/analytics/server/lib/onboarding-journey-cost.ts +163 -94
  16. package/corpus/templates/analytics/server/lib/onboarding-journey.ts +162 -74
  17. package/corpus/templates/assets/server/lib/org-admin.ts +20 -16
  18. package/corpus/templates/brain/server/lib/zoom-governance.ts +11 -6
  19. package/corpus/templates/clips/CHANGELOG.md +1 -0
  20. package/corpus/templates/clips/changelog/2026-10-16-rewind-status-says-when-it-remembers-audio.md +6 -0
  21. package/corpus/templates/clips/desktop/src/app.tsx +7 -4
  22. package/corpus/templates/clips/desktop/src/components/RewindCaptureStatus.tsx +25 -0
  23. package/corpus/templates/clips/desktop/src/lib/rewind-status.ts +6 -1
  24. package/corpus/templates/clips/desktop/src/shared/config.ts +3 -0
  25. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2 -0
  26. package/corpus/templates/clips/server/lib/recordings.ts +4 -19
  27. package/corpus/templates/community/qr/package.json +1 -1
  28. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +15 -1
  29. package/corpus/templates/content/CHANGELOG.md +13 -0
  30. package/corpus/templates/content/actions/_content-space-access.ts +95 -6
  31. package/corpus/templates/content/actions/_database-utils.ts +20 -16
  32. package/corpus/templates/content/actions/_database-view-setup.ts +305 -11
  33. package/corpus/templates/content/actions/_github-source-freshness.ts +72 -17
  34. package/corpus/templates/content/actions/_github-source-metadata.ts +63 -17
  35. package/corpus/templates/content/actions/_github-source.ts +141 -77
  36. package/corpus/templates/content/actions/_property-utils.ts +3 -0
  37. package/corpus/templates/content/actions/check-github-connection.ts +15 -0
  38. package/corpus/templates/content/actions/create-github-collection.ts +180 -0
  39. package/corpus/templates/content/actions/disconnect-content-database-source.ts +5 -0
  40. package/corpus/templates/content/actions/link-github-folder.ts +104 -36
  41. package/corpus/templates/content/actions/refresh-content-database-source.ts +37 -4
  42. package/corpus/templates/content/actions/sync-github-folder.ts +23 -5
  43. package/corpus/templates/content/actions/update-content-database-view.ts +18 -2
  44. package/corpus/templates/content/actions/view-screen.ts +12 -0
  45. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +263 -35
  46. package/corpus/templates/content/app/components/editor/database/GitHubLinkDialog.tsx +938 -0
  47. package/corpus/templates/content/app/components/editor/database/GitHubSourcePanel.tsx +228 -0
  48. package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +561 -0
  49. package/corpus/templates/content/app/components/editor/database/GitHubUnlinkDialog.tsx +96 -0
  50. package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +410 -0
  51. package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +426 -0
  52. package/corpus/templates/content/app/components/editor/database/ListView.tsx +136 -18
  53. package/corpus/templates/content/app/components/editor/database/github-availability.ts +147 -0
  54. package/corpus/templates/content/app/components/editor/database/github-source-text.ts +280 -0
  55. package/corpus/templates/content/app/components/editor/database/navigation-state.ts +3 -0
  56. package/corpus/templates/content/app/components/editor/database/settings.tsx +3 -3
  57. package/corpus/templates/content/app/components/editor/database/view-config.ts +4 -0
  58. package/corpus/templates/content/app/components/layout/Layout.tsx +2 -0
  59. package/corpus/templates/content/app/global.css +13 -0
  60. package/corpus/templates/content/app/hooks/content-action-refresh.ts +6 -0
  61. package/corpus/templates/content/app/hooks/use-content-database.ts +3 -1
  62. package/corpus/templates/content/app/hooks/use-github-folder.ts +199 -0
  63. package/corpus/templates/content/app/i18n-data.ts +186 -1
  64. package/corpus/templates/content/app/lib/github-link-dialog.ts +86 -0
  65. package/corpus/templates/content/changelog/2026-10-18-connected-ai-assistants-can-link-a-new-collection-to-a-github.md +1 -1
  66. package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +6 -0
  67. package/corpus/templates/content/changelog/2026-10-19-list-rows-can-be-customized.md +6 -0
  68. package/corpus/templates/content/changelog/2026-10-19-list-views-fit-phone-screens.md +6 -0
  69. package/corpus/templates/content/changelog/2026-10-21-organization-favorites-include-documents-shared-with-the-cur.md +6 -0
  70. package/corpus/templates/content/docs/product/capabilities/content.source.sync-policy.md +32 -7
  71. package/corpus/templates/content/docs/product/encyclopedia.md +3 -3
  72. package/corpus/templates/content/docs/product/features/content.feature.trust-your-connected-sources.md +2 -2
  73. package/corpus/templates/content/docs/product/roadmap.md +1 -1
  74. package/corpus/templates/content/parity/matrix.md +42 -42
  75. package/corpus/templates/content/parity/matrix.ts +14 -0
  76. package/corpus/templates/content/server/lib/github-source-client.ts +50 -1
  77. package/corpus/templates/content/shared/api.ts +21 -0
  78. package/corpus/templates/content/shared/database-item-layout.ts +591 -0
  79. package/corpus/templates/content/shared/github-source.ts +68 -0
  80. package/corpus/templates/design/CHANGELOG.md +1 -0
  81. package/corpus/templates/design/actions/connect-assets-mcp.ts +5 -9
  82. package/corpus/templates/design/changelog/2026-10-21-journey-canvases-accept-partial-analytics-samples-when-repla.md +6 -0
  83. package/corpus/templates/design/shared/journey-canvas.ts +6 -4
  84. package/corpus/templates/dispatch/.agents/skills/a2a-protocol/SKILL.md +16 -13
  85. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  86. package/corpus/templates/dispatch/changelog/2026-10-21-customer-ai-credit-questions-route-to-account-health-instead.md +6 -0
  87. package/corpus/templates/factory/server/lib/require-workspace-member.ts +5 -17
  88. package/corpus/templates/mail/server/lib/queued-drafts.ts +9 -4
  89. package/corpus/templates/slides/server/workspace-defaults.ts +12 -15
  90. package/package.json +1 -1
@@ -0,0 +1,147 @@
1
+ import type { ContentDatabaseSource } from "@shared/api";
2
+ import { GITHUB_SOURCE_TYPE, githubFolderName } from "@shared/github-source";
3
+
4
+ import { githubRelativeTime } from "./github-source-text";
5
+ import { dbText } from "./text";
6
+
7
+ /** The collection the Sources panel is about, as GitHub linking needs it. */
8
+ export interface GitHubCollectionPlace {
9
+ databaseId: string;
10
+ documentId: string;
11
+ /** Where a new collection beside this one goes. */
12
+ spaceId?: string;
13
+ parentId: string | null;
14
+ systemRole: string | null;
15
+ pageCount: number;
16
+ canEdit: boolean;
17
+ }
18
+
19
+ export type GitHubBlockedReason =
20
+ | "files"
21
+ | "system"
22
+ | "editors-only"
23
+ | "other-source"
24
+ | "has-pages";
25
+
26
+ /** Whether this collection can take a GitHub folder, and if not, why. */
27
+ export type GitHubAvailability =
28
+ | { kind: "linked"; source: ContentDatabaseSource }
29
+ | { kind: "available" }
30
+ | { kind: "connect" }
31
+ | {
32
+ kind: "blocked";
33
+ reason: GitHubBlockedReason;
34
+ /** The other app the collection's pages come from. */
35
+ sourceName?: string;
36
+ };
37
+
38
+ /** The app a source's pages come from, as people call it. */
39
+ export function sourceAppName(source: ContentDatabaseSource): string {
40
+ if (source.sourceType === "builder-cms") return "Builder";
41
+ if (source.sourceType === "notion-database") return "Notion";
42
+ return source.sourceName;
43
+ }
44
+
45
+ /**
46
+ * Checks the collection before the GitHub connection, so nobody is sent
47
+ * through GitHub's sign-in for a collection that could never link. The server
48
+ * checks again when linking, for changes made in between.
49
+ */
50
+ export function githubAvailability(
51
+ place: GitHubCollectionPlace,
52
+ sources: ContentDatabaseSource[],
53
+ connected: boolean | null | undefined,
54
+ ): GitHubAvailability {
55
+ const linked = sources.find(
56
+ (source) => source.sourceType === GITHUB_SOURCE_TYPE,
57
+ );
58
+ if (linked) return { kind: "linked", source: linked };
59
+ if (place.systemRole === "files") return { kind: "blocked", reason: "files" };
60
+ if (place.systemRole) return { kind: "blocked", reason: "system" };
61
+ if (!place.canEdit) return { kind: "blocked", reason: "editors-only" };
62
+ const other = sources[0];
63
+ if (other) {
64
+ return {
65
+ kind: "blocked",
66
+ reason: "other-source",
67
+ sourceName: sourceAppName(other),
68
+ };
69
+ }
70
+ if (place.pageCount > 0) return { kind: "blocked", reason: "has-pages" };
71
+ // An unknown connection is left to the dialog, which asks again.
72
+ return connected === false ? { kind: "connect" } : { kind: "available" };
73
+ }
74
+
75
+ /** Whether a blocked collection can point to a new one instead. */
76
+ export function githubOffersNewCollection(
77
+ availability: GitHubAvailability,
78
+ place: GitHubCollectionPlace,
79
+ ): boolean {
80
+ return (
81
+ availability.kind === "blocked" &&
82
+ availability.reason !== "editors-only" &&
83
+ !!place.spaceId
84
+ );
85
+ }
86
+
87
+ /** The line under "GitHub" in Sources, always visible. */
88
+ export function githubRowDescription(
89
+ availability: GitHubAvailability,
90
+ now = Date.now(),
91
+ ): string {
92
+ switch (availability.kind) {
93
+ case "linked": {
94
+ const status = availability.source.metadata.github;
95
+ const folder = status
96
+ ? githubFolderName(status)
97
+ : availability.source.sourceName;
98
+ return status?.syncedAt
99
+ ? dbText("githubRowLinked", {
100
+ folder,
101
+ time: githubRelativeTime(status.syncedAt, now),
102
+ })
103
+ : dbText("githubRowLinkedNotUpdated", { folder });
104
+ }
105
+ case "available":
106
+ return dbText("githubRowAvailable");
107
+ case "connect":
108
+ return dbText("githubRowConnectFirst");
109
+ case "blocked":
110
+ switch (availability.reason) {
111
+ case "files":
112
+ return dbText("githubRowFiles");
113
+ case "system":
114
+ return dbText("githubRowSystem");
115
+ case "editors-only":
116
+ return dbText("githubRowEditorsOnly");
117
+ case "other-source":
118
+ return dbText("githubRowOtherSource", {
119
+ source: availability.sourceName ?? "",
120
+ });
121
+ case "has-pages":
122
+ return dbText("githubRowEmptyOnly");
123
+ }
124
+ }
125
+ }
126
+
127
+ /** The full reason a blocked collection can't take a folder. */
128
+ export function githubBlockedExplanation(
129
+ reason: GitHubBlockedReason,
130
+ place: GitHubCollectionPlace,
131
+ sourceName?: string,
132
+ ): string {
133
+ switch (reason) {
134
+ case "files":
135
+ return dbText("githubExplainFiles");
136
+ case "system":
137
+ return dbText("githubExplainSystem");
138
+ case "editors-only":
139
+ return dbText("githubExplainEditorsOnly");
140
+ case "other-source":
141
+ return dbText("githubExplainOtherSource", { source: sourceName ?? "" });
142
+ case "has-pages":
143
+ return place.pageCount === 1
144
+ ? dbText("githubExplainHasPagesOne")
145
+ : dbText("githubExplainHasPages", { count: place.pageCount });
146
+ }
147
+ }
@@ -0,0 +1,280 @@
1
+ import { actionErrorMessage } from "@agent-native/core/client/hooks";
2
+ import {
3
+ GITHUB_SOURCE_MAX_FILES,
4
+ parseGitHubFolderUrl,
5
+ type GitHubLinkBlockedReason,
6
+ type GitHubSourceProblemCode,
7
+ type GitHubSourceSkipReason,
8
+ } from "@shared/github-source";
9
+
10
+ import { dbText, type DatabaseMessageKey } from "./text";
11
+
12
+ const PROBLEM_KEYS: Record<GitHubSourceProblemCode, DatabaseMessageKey> = {
13
+ connection: "githubProblemConnection",
14
+ forbidden: "githubProblemForbidden",
15
+ repository_not_found: "githubProblemRepositoryNotFound",
16
+ branch_not_found: "githubProblemBranchNotFound",
17
+ folder_not_found: "githubProblemFolderNotFound",
18
+ rate_limited: "githubProblemPaused",
19
+ too_large: "githubProblemTooLarge",
20
+ unavailable: "githubProblemPaused",
21
+ sync_failed: "githubProblemPaused",
22
+ storage_unavailable: "githubProblemStorageUnavailable",
23
+ };
24
+
25
+ const SKIP_KEYS: Record<GitHubSourceSkipReason, DatabaseMessageKey> = {
26
+ mdx: "githubSkipMdx",
27
+ "too-large": "githubSkipTooLarge",
28
+ "not-text": "githubSkipNotText",
29
+ };
30
+
31
+ const BLOCKED_KEYS: Record<GitHubLinkBlockedReason, DatabaseMessageKey> = {
32
+ "collection-has-rows": "githubLinkBlockedHasRows",
33
+ "already-linked": "githubLinkBlockedLinked",
34
+ "system-collection": "githubLinkBlockedSystem",
35
+ };
36
+
37
+ /** Where a linked folder lives, for problem text that names it. */
38
+ export interface GitHubProblemPlace {
39
+ branch?: string;
40
+ path?: string;
41
+ }
42
+
43
+ /** A linked folder's problem, as the status line and update toasts say it. */
44
+ export function githubProblemText(
45
+ code: GitHubSourceProblemCode,
46
+ place: GitHubProblemPlace = {},
47
+ ): string {
48
+ return dbText(PROBLEM_KEYS[code] ?? "githubProblemPaused", {
49
+ branch: place.branch ?? "",
50
+ folder: lastSegment(place.path ?? ""),
51
+ max: GITHUB_SOURCE_MAX_FILES.toLocaleString(pageLocale()),
52
+ });
53
+ }
54
+
55
+ /** Problems that mean nothing comes in until someone acts. */
56
+ export function githubProblemStopsUpdates(
57
+ code: GitHubSourceProblemCode,
58
+ ): boolean {
59
+ return (
60
+ code === "connection" ||
61
+ code === "forbidden" ||
62
+ code === "repository_not_found" ||
63
+ code === "branch_not_found" ||
64
+ code === "folder_not_found" ||
65
+ code === "too_large"
66
+ );
67
+ }
68
+
69
+ /**
70
+ * The problem a `GITHUB_<CODE>` action error stands for, so the dialog can say
71
+ * it in the reader's language. Null for any other error.
72
+ */
73
+ export function githubProblemFromErrorCode(
74
+ errorCode: unknown,
75
+ ): GitHubSourceProblemCode | null {
76
+ if (typeof errorCode !== "string" || !errorCode.startsWith("GITHUB_")) {
77
+ return null;
78
+ }
79
+ const code = errorCode.slice("GITHUB_".length).toLowerCase();
80
+ return code in PROBLEM_KEYS ? (code as GitHubSourceProblemCode) : null;
81
+ }
82
+
83
+ /** Whether an action error means GitHub needs connecting first. */
84
+ export function githubErrorNeedsConnection(error: unknown): boolean {
85
+ const code = (error as { errorCode?: unknown } | null)?.errorCode;
86
+ // A missing or ungranted connection comes from the shared provider runtime
87
+ // as `connection_required`; a rejected one, from GitHub.
88
+ return (
89
+ code === "connection_required" ||
90
+ githubProblemFromErrorCode(code) === "connection"
91
+ );
92
+ }
93
+
94
+ /** The collection-level reason a link was refused, from its action error. */
95
+ export function githubBlockedFromErrorCode(
96
+ errorCode: unknown,
97
+ ): GitHubLinkBlockedReason | null {
98
+ if (errorCode === "COLLECTION_NOT_EMPTY") return "collection-has-rows";
99
+ if (errorCode === "SOURCE_ALREADY_LINKED") return "already-linked";
100
+ if (errorCode === "COLLECTION_NOT_LINKABLE") return "system-collection";
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * A link-time error in plain words, naming what the person typed: a typo in
106
+ * the link reads as a typo, not as something that went missing.
107
+ */
108
+ export function githubLinkErrorText(error: unknown, url: string): string {
109
+ const errorCode = (error as { errorCode?: unknown } | null)?.errorCode;
110
+ const blocked = githubBlockedFromErrorCode(errorCode);
111
+ if (blocked) return githubBlockedText(blocked);
112
+ const problem = githubProblemFromErrorCode(errorCode);
113
+ if (!problem) {
114
+ return (
115
+ actionErrorMessage(error) ??
116
+ (error instanceof Error ? error.message : dbText("githubLinkUnavailable"))
117
+ );
118
+ }
119
+ const parsed = parseGitHubFolderUrl(url);
120
+ const repo = parsed.ok ? parsed.repo : "";
121
+ const [branch = "", ...folder] = parsed.ok ? parsed.refPath : [];
122
+ switch (problem) {
123
+ case "repository_not_found":
124
+ return dbText("githubLinkRepositoryNotFound", { repo });
125
+ case "forbidden":
126
+ return dbText("githubLinkForbidden", { repo });
127
+ case "branch_not_found":
128
+ return dbText("githubLinkBranchNotFound", { branch, repo });
129
+ case "folder_not_found":
130
+ return dbText("githubLinkFolderNotFound", {
131
+ folder: folder.join("/"),
132
+ repo,
133
+ });
134
+ case "too_large":
135
+ return dbText("githubLinkTooLarge", {
136
+ max: GITHUB_SOURCE_MAX_FILES.toLocaleString(pageLocale()),
137
+ });
138
+ default:
139
+ return dbText("githubLinkUnavailable");
140
+ }
141
+ }
142
+
143
+ /** What's wrong with a link before anything is sent, if anything. */
144
+ export function githubUrlProblem(
145
+ url: string,
146
+ ): { text: string; folderUrl: string | null; folder: string | null } | null {
147
+ const parsed = parseGitHubFolderUrl(url);
148
+ if (parsed.ok) return null;
149
+ if (parsed.reason !== "file") {
150
+ return {
151
+ text: dbText("githubLinkInvalidUrl"),
152
+ folderUrl: null,
153
+ folder: null,
154
+ };
155
+ }
156
+ const folder = fileLinkFolder(url);
157
+ return {
158
+ text: dbText("githubLinkFileUrl"),
159
+ folderUrl: folder?.url ?? null,
160
+ folder: folder?.name ?? null,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * The folder a file link points into: `…/blob/main/docs/faq.md` becomes
166
+ * `…/tree/main/docs`, named "docs", or the repository at its root.
167
+ */
168
+ export function fileLinkFolder(
169
+ url: string,
170
+ ): { url: string; name: string } | null {
171
+ let parsed: URL;
172
+ try {
173
+ parsed = new URL(
174
+ /^[a-z][a-z0-9+.-]*:\/\//i.test(url) ? url : `https://${url}`,
175
+ );
176
+ // coercion-ok: a link that isn't a URL has no folder to offer; the caller shows the invalid-link text.
177
+ } catch {
178
+ return null;
179
+ }
180
+ const segments = parsed.pathname.split("/").filter(Boolean);
181
+ const [owner, repo, kind, ...rest] = segments;
182
+ if (!owner || !repo || kind !== "blob" || rest.length < 2) return null;
183
+ const folderSegments = rest.slice(0, -1);
184
+ const folderUrl = `https://github.com/${owner}/${repo}/tree/${folderSegments.join("/")}`;
185
+ // The first segment is the branch, so a file at the root names the repository.
186
+ const name =
187
+ folderSegments.length > 1
188
+ ? decodeURIComponent(folderSegments[folderSegments.length - 1]!)
189
+ : decodeURIComponent(repo);
190
+ return parseGitHubFolderUrl(folderUrl).ok ? { url: folderUrl, name } : null;
191
+ }
192
+
193
+ export function githubSkipReasonText(reason: GitHubSourceSkipReason): string {
194
+ return dbText(SKIP_KEYS[reason] ?? "githubSkipNotText");
195
+ }
196
+
197
+ export function githubBlockedText(reason: GitHubLinkBlockedReason): string {
198
+ return dbText(BLOCKED_KEYS[reason] ?? "githubLinkBlockedLinked");
199
+ }
200
+
201
+ /** The seven-character commit name GitHub shows. */
202
+ export function shortCommit(sha: string): string {
203
+ return sha.slice(0, 7);
204
+ }
205
+
206
+ function lastSegment(path: string): string {
207
+ const trimmed = path.replace(/\/+$/, "");
208
+ return trimmed.slice(trimmed.lastIndexOf("/") + 1);
209
+ }
210
+
211
+ /** The page's language, for numbers, dates and lists. */
212
+ export function pageLocale(): string {
213
+ return typeof document === "undefined" || !document.documentElement.lang
214
+ ? "en-US"
215
+ : document.documentElement.lang;
216
+ }
217
+
218
+ // Not in the TypeScript lib this app targets, though every supported
219
+ // browser has it.
220
+ const ListFormat = (
221
+ Intl as typeof Intl & {
222
+ ListFormat?: new (
223
+ locale: string,
224
+ options: { style: "long"; type: "conjunction" },
225
+ ) => { format(items: string[]): string };
226
+ }
227
+ ).ListFormat;
228
+
229
+ /** "API reference, FAQ and Getting started", in the page's language. */
230
+ export function githubList(items: string[]): string {
231
+ if (!ListFormat) return items.join(", ");
232
+ return new ListFormat(pageLocale(), {
233
+ style: "long",
234
+ type: "conjunction",
235
+ }).format(items);
236
+ }
237
+
238
+ const RELATIVE_UNITS: Array<[Intl.RelativeTimeFormatUnit, number]> = [
239
+ ["year", 365 * 24 * 60 * 60],
240
+ ["month", 30 * 24 * 60 * 60],
241
+ ["week", 7 * 24 * 60 * 60],
242
+ ["day", 24 * 60 * 60],
243
+ ["hour", 60 * 60],
244
+ ["minute", 60],
245
+ ];
246
+
247
+ /** "2 minutes ago", "just now" and the like, in the page's language. */
248
+ export function githubRelativeTime(iso: string, now = Date.now()): string {
249
+ const at = Date.parse(iso);
250
+ if (!Number.isFinite(at)) return "";
251
+ const format = new Intl.RelativeTimeFormat(pageLocale(), {
252
+ numeric: "auto",
253
+ });
254
+ const seconds = Math.round((at - now) / 1000);
255
+ for (const [unit, size] of RELATIVE_UNITS) {
256
+ if (Math.abs(seconds) >= size) {
257
+ return format.format(Math.round(seconds / size), unit);
258
+ }
259
+ }
260
+ return dbText("githubJustNow");
261
+ }
262
+
263
+ /** A frontmatter example the way a writer reads it: "Yes", "Sep 15, 2026". */
264
+ export function githubExampleText(type: string, example: string): string {
265
+ if (type === "checkbox") {
266
+ if (example === "true") return dbText("githubYes");
267
+ if (example === "false") return dbText("githubNo");
268
+ }
269
+ if (type === "date") {
270
+ const at = Date.parse(example);
271
+ if (Number.isFinite(at)) {
272
+ return new Intl.DateTimeFormat(pageLocale(), {
273
+ dateStyle: "medium",
274
+ // Frontmatter dates are calendar days, so they mustn't shift by zone.
275
+ timeZone: /^\d{4}-\d{2}-\d{2}$/.test(example) ? "UTC" : undefined,
276
+ }).format(at);
277
+ }
278
+ }
279
+ return example;
280
+ }
@@ -172,6 +172,7 @@ export function databaseNavigationState({
172
172
  | "rowDensity"
173
173
  | "openPagesIn"
174
174
  | "formQuestions"
175
+ | "itemLayout"
175
176
  >;
176
177
  searchQuery?: string;
177
178
  sorts?: DatabaseSort[];
@@ -286,6 +287,8 @@ export function databaseNavigationState({
286
287
  : undefined,
287
288
  databaseFormQuestions:
288
289
  activeView.type === "form" ? activeView.formQuestions : undefined,
290
+ databaseItemLayout:
291
+ activeView.type === "list" ? (activeView.itemLayout ?? null) : undefined,
289
292
  databaseVisibleItemCount: visibleItemCount,
290
293
  databaseTotalItemCount: totalItemCount,
291
294
  databaseVisibleItems: databaseVisibleItemSummaries(
@@ -2066,7 +2066,7 @@ function SourceDetailsFieldPicker({
2066
2066
  className={cn(
2067
2067
  "flex size-4 shrink-0 items-center justify-center rounded border",
2068
2068
  selected.has(field.id)
2069
- ? "border-[#2383e2] bg-[#2383e2] text-white"
2069
+ ? "border-database-blue bg-database-blue text-database-blue-foreground"
2070
2070
  : "border-muted-foreground/40 text-transparent",
2071
2071
  )}
2072
2072
  >
@@ -2644,7 +2644,7 @@ function DatabaseSettingsSwitch({
2644
2644
  <span
2645
2645
  className={cn(
2646
2646
  "relative h-5 w-9 rounded-full transition-colors",
2647
- checked ? "bg-[#2383e2]" : "bg-muted-foreground/25",
2647
+ checked ? "bg-database-blue" : "bg-muted-foreground/25",
2648
2648
  )}
2649
2649
  >
2650
2650
  <span
@@ -2683,7 +2683,7 @@ function DatabaseSettingsLayoutPanel({
2683
2683
  className={cn(
2684
2684
  "flex h-16 flex-col items-center justify-center gap-1 rounded-md border text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
2685
2685
  activeView.type === type
2686
- ? "border-[#2383e2] bg-[#2383e2]/5 text-[#2383e2]"
2686
+ ? "border-database-blue bg-database-blue/5 text-database-blue"
2687
2687
  : "border-border text-muted-foreground hover:bg-muted hover:text-foreground",
2688
2688
  )}
2689
2689
  onClick={() => onViewTypeChange(type)}
@@ -3,6 +3,7 @@ import type {
3
3
  ContentDatabaseViewConfig,
4
4
  ContentDatabaseViewType,
5
5
  } from "@shared/api";
6
+ import { contentDatabaseItemLayoutField } from "@shared/database-item-layout";
6
7
 
7
8
  import {
8
9
  type DatabaseColumnCalculation,
@@ -60,6 +61,7 @@ export function createDatabaseView(
60
61
  rowDensity: normalizeClientDatabaseRowDensity(values.rowDensity),
61
62
  openPagesIn: normalizeClientDatabaseOpenPagesIn(values.openPagesIn),
62
63
  formQuestions: normalizeClientDatabaseFormQuestions(values.formQuestions),
64
+ ...contentDatabaseItemLayoutField(values.itemLayout),
63
65
  };
64
66
  }
65
67
 
@@ -219,6 +221,7 @@ export function duplicateDatabaseView(
219
221
  rowDensity: view.rowDensity,
220
222
  openPagesIn: view.openPagesIn,
221
223
  formQuestions: view.formQuestions,
224
+ itemLayout: view.itemLayout,
222
225
  },
223
226
  view.type,
224
227
  );
@@ -360,6 +363,7 @@ function normalizeClientDatabaseView(
360
363
  rowDensity: normalizeClientDatabaseRowDensity(value.rowDensity),
361
364
  openPagesIn: normalizeClientDatabaseOpenPagesIn(value.openPagesIn),
362
365
  formQuestions: normalizeClientDatabaseFormQuestions(value.formQuestions),
366
+ itemLayout: value.itemLayout,
363
367
  },
364
368
  type,
365
369
  );
@@ -26,6 +26,7 @@ import {
26
26
  import { useLocation, useNavigation } from "react-router";
27
27
  import { toast } from "sonner";
28
28
 
29
+ import { GitHubLinkDialogHost } from "@/components/editor/database/GitHubLinkDialog";
29
30
  import { DocumentEditor } from "@/components/editor/DocumentEditor";
30
31
  import { DocumentEditorSkeleton } from "@/components/editor/DocumentEditorSkeleton";
31
32
  import { DocumentSidebar } from "@/components/sidebar/DocumentSidebar";
@@ -497,6 +498,7 @@ export function Layout({ children }: LayoutProps) {
497
498
  </>
498
499
  )}
499
500
  </div>
501
+ <GitHubLinkDialogHost />
500
502
  </HeaderActionsProvider>
501
503
  );
502
504
  }
@@ -70,10 +70,20 @@
70
70
  --block-bg-purple: #ad46ff19;
71
71
  --block-bg-pink: #f6339a19;
72
72
  --block-bg-red: #fb2c3619;
73
+
74
+ /* Active filter and sort chips in collection views. */
75
+ --database-chip: #2383e2;
76
+ --database-chip-foreground: #0f5ea8;
73
77
  }
74
78
 
75
79
  @theme {
76
80
  --color-sidebar-muted: hsl(var(--sidebar-muted));
81
+ /* Checked, selected, and focused controls in collection views, the same in
82
+ light and dark mode. */
83
+ --color-database-blue: #2383e2;
84
+ --color-database-blue-foreground: #fff;
85
+ --color-database-chip: var(--database-chip);
86
+ --color-database-chip-foreground: var(--database-chip-foreground);
77
87
  /* Comment cards: resting in the margin, floating as a popover, and the
78
88
  lifted state for the hovered, focused, or selected thread. */
79
89
  --shadow-comment-card:
@@ -143,6 +153,9 @@
143
153
  --block-bg-purple: #ad46ff26;
144
154
  --block-bg-pink: #f6339a26;
145
155
  --block-bg-red: #fb2c3626;
156
+
157
+ --database-chip: #529cca;
158
+ --database-chip-foreground: #8ec7ff;
146
159
  }
147
160
 
148
161
  @layer base {
@@ -30,12 +30,14 @@ const DOCUMENT_MUTATIONS = new Set([
30
30
  "delete-document",
31
31
  "delete-document-property",
32
32
  "delete-content-database",
33
+ "disconnect-content-database-source",
33
34
  "execute-content-trash-purge",
34
35
  "permanently-delete-document",
35
36
  "duplicate-document-property",
36
37
  "edit-document",
37
38
  "execute-builder-source-batch",
38
39
  "execute-builder-source-execution",
40
+ "create-github-collection",
39
41
  "import-content-source",
40
42
  "link-github-folder",
41
43
  "migrate-content-database-rows",
@@ -72,12 +74,14 @@ const DATABASE_RESULT_MUTATIONS = new Set([
72
74
  "delete-content-database",
73
75
  "delete-document",
74
76
  "delete-document-property",
77
+ "disconnect-content-database-source",
75
78
  "duplicate-database-item",
76
79
  "duplicate-database-items",
77
80
  "duplicate-document-property",
78
81
  "edit-document",
79
82
  "execute-builder-source-batch",
80
83
  "execute-builder-source-execution",
84
+ "create-github-collection",
81
85
  "import-content-source",
82
86
  "link-github-folder",
83
87
  "migrate-content-database-rows",
@@ -103,6 +107,7 @@ const DATABASE_PRESENTATION_MUTATIONS = new Set([
103
107
  ]);
104
108
 
105
109
  const DATABASE_LIFECYCLE_MUTATIONS = new Set([
110
+ "create-github-collection",
106
111
  "delete-content-database",
107
112
  "execute-content-trash-purge",
108
113
  "permanently-delete-document",
@@ -115,6 +120,7 @@ const DATABASE_LIFECYCLE_MUTATIONS = new Set([
115
120
 
116
121
  const DOCUMENT_DISCOVERY_MUTATIONS = new Set([
117
122
  "create-document",
123
+ "create-github-collection",
118
124
  "import-content",
119
125
  "link-github-folder",
120
126
  "refresh-content-database-source",
@@ -1837,7 +1837,9 @@ export function useRefreshContentDatabaseSource(documentId: string) {
1837
1837
  RefreshContentDatabaseSourceRequest
1838
1838
  >("refresh-content-database-source", {
1839
1839
  skipActionQueryInvalidation: true,
1840
- onSuccess: () => {
1840
+ // A GitHub refresh that fails stores the problem on the source first, so
1841
+ // the status line rereads after a failure too.
1842
+ onSettled: () => {
1841
1843
  invalidateContentDatabaseSourceRefreshQueries(queryClient, documentId);
1842
1844
  },
1843
1845
  });