smoodly 0.0.23 → 0.0.25

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 (135) hide show
  1. package/README.md +192 -43
  2. package/bin/smoodly.js +6 -0
  3. package/dist/admin/auth.d.ts +10 -5
  4. package/dist/admin/auth.js +14 -13
  5. package/dist/admin/client-ops.js +2 -1
  6. package/dist/admin/editor/EditorView.js +20 -6
  7. package/dist/admin/editor/InsertPicker.d.ts +14 -0
  8. package/dist/admin/editor/InsertPicker.js +37 -7
  9. package/dist/admin/editor/InspectorFields.js +1 -1
  10. package/dist/admin/editor/Outline.js +2 -2
  11. package/dist/admin/editor/PageSettings.js +6 -0
  12. package/dist/admin/editor/SeoSettings.js +1 -1
  13. package/dist/admin/editor/locale-url.d.ts +7 -4
  14. package/dist/admin/editor/locale-url.js +14 -7
  15. package/dist/admin/editor/usePageLocale.js +9 -1
  16. package/dist/admin/forms/CropEditor.d.ts +4 -2
  17. package/dist/admin/forms/CropEditor.js +6 -3
  18. package/dist/admin/forms/DatePicker.d.ts +9 -0
  19. package/dist/admin/forms/DatePicker.js +133 -0
  20. package/dist/admin/forms/FieldWidget.js +11 -5
  21. package/dist/admin/forms/ImagePreview.d.ts +9 -0
  22. package/dist/admin/forms/ImagePreview.js +11 -0
  23. package/dist/admin/forms/MediaWidget.d.ts +8 -2
  24. package/dist/admin/forms/MediaWidget.js +13 -13
  25. package/dist/admin/forms/RichtextElementCard.js +1 -1
  26. package/dist/admin/forms/RichtextImageCard.js +14 -9
  27. package/dist/admin/forms/date-picker.d.ts +44 -0
  28. package/dist/admin/forms/date-picker.js +117 -0
  29. package/dist/admin/forms/elements-context.d.ts +3 -0
  30. package/dist/admin/forms/elements-context.js +7 -2
  31. package/dist/admin/forms/time-zone-context.d.ts +7 -0
  32. package/dist/admin/forms/time-zone-context.js +15 -0
  33. package/dist/admin/forms/useRichtextEditor.js +10 -1
  34. package/dist/admin/index.d.ts +2 -0
  35. package/dist/admin/next/bridge.d.ts +2 -1
  36. package/dist/admin/next/bridge.js +35 -5
  37. package/dist/admin/next/index.d.ts +2 -0
  38. package/dist/admin/ops-impl.js +73 -1
  39. package/dist/admin/ops.d.ts +22 -2
  40. package/dist/admin/serialize.d.ts +2 -0
  41. package/dist/admin/serialize.js +1 -0
  42. package/dist/admin/shell/AdminApp.js +48 -3
  43. package/dist/admin/shell/EntriesList.js +12 -21
  44. package/dist/admin/shell/EntryForm.js +63 -5
  45. package/dist/admin/shell/GlobalForm.js +22 -2
  46. package/dist/admin/shell/GlobalList.js +4 -18
  47. package/dist/admin/shell/IconRail.d.ts +3 -1
  48. package/dist/admin/shell/IconRail.js +17 -7
  49. package/dist/admin/shell/InviteModal.d.ts +6 -0
  50. package/dist/admin/shell/InviteModal.js +28 -0
  51. package/dist/admin/shell/ListView.js +2 -2
  52. package/dist/admin/shell/LoginView.js +34 -5
  53. package/dist/admin/shell/MediaScreen.js +11 -1
  54. package/dist/admin/shell/MembersPage.d.ts +2 -1
  55. package/dist/admin/shell/MembersPage.js +90 -25
  56. package/dist/admin/shell/PageDialogs.d.ts +26 -0
  57. package/dist/admin/shell/PageDialogs.js +38 -0
  58. package/dist/admin/shell/PagesList.js +100 -70
  59. package/dist/admin/shell/RouteLink.js +1 -1
  60. package/dist/admin/shell/SchemaNotice.d.ts +4 -0
  61. package/dist/admin/shell/SchemaNotice.js +4 -0
  62. package/dist/admin/shell/SetPasswordView.d.ts +7 -0
  63. package/dist/admin/shell/SetPasswordView.js +41 -0
  64. package/dist/admin/shell/SettingsView.d.ts +5 -1
  65. package/dist/admin/shell/SettingsView.js +17 -11
  66. package/dist/admin/shell/live-url.d.ts +10 -0
  67. package/dist/admin/shell/live-url.js +19 -0
  68. package/dist/admin/shell/locale-memory.d.ts +8 -0
  69. package/dist/admin/shell/locale-memory.js +23 -0
  70. package/dist/admin/shell/members-list.d.ts +15 -1
  71. package/dist/admin/shell/members-list.js +24 -7
  72. package/dist/admin/shell/page-dialogs.d.ts +9 -0
  73. package/dist/admin/shell/page-dialogs.js +19 -0
  74. package/dist/admin/shell/router.d.ts +10 -7
  75. package/dist/admin/shell/router.js +18 -9
  76. package/dist/admin/shell/session.d.ts +6 -0
  77. package/dist/admin/shell/session.js +17 -2
  78. package/dist/admin/shell/settings.d.ts +0 -6
  79. package/dist/admin/shell/settings.js +4 -5
  80. package/dist/admin/supabase-auth.js +6 -5
  81. package/dist/admin/tree-ops.js +3 -0
  82. package/dist/admin/ui/ActionMenu.js +26 -6
  83. package/dist/admin/ui/Combobox.d.ts +5 -0
  84. package/dist/admin/ui/Combobox.js +1 -1
  85. package/dist/admin/ui/ConfirmDialog.d.ts +8 -0
  86. package/dist/admin/ui/ConfirmDialog.js +30 -0
  87. package/dist/admin/ui/Dialog.d.ts +13 -0
  88. package/dist/admin/ui/Dialog.js +54 -0
  89. package/dist/admin/ui/HistoryModal.d.ts +2 -1
  90. package/dist/admin/ui/HistoryModal.js +7 -2
  91. package/dist/admin/ui/ResizeHandle.js +4 -4
  92. package/dist/admin/ui/confirm.d.ts +35 -0
  93. package/dist/admin/ui/confirm.js +57 -0
  94. package/dist/admin/ui/primitives.d.ts +19 -4
  95. package/dist/admin/ui/primitives.js +11 -5
  96. package/dist/admin/ui/resize.d.ts +7 -4
  97. package/dist/admin/ui/resize.js +7 -4
  98. package/dist/admin/ui/theme.d.ts +1 -1
  99. package/dist/admin/ui/theme.js +368 -203
  100. package/dist/admin/validate.d.ts +4 -0
  101. package/dist/admin/validate.js +28 -0
  102. package/dist/cli/load-collections.d.ts +2 -0
  103. package/dist/cli/load-collections.js +26 -0
  104. package/dist/cli/main.d.ts +13 -0
  105. package/dist/cli/main.js +62 -0
  106. package/dist/cli/migrate.d.ts +23 -0
  107. package/dist/cli/migrate.js +64 -0
  108. package/dist/config.d.ts +4 -0
  109. package/dist/config.js +9 -0
  110. package/dist/fields.d.ts +2 -0
  111. package/dist/fields.js +2 -0
  112. package/dist/index.d.ts +6 -3
  113. package/dist/index.js +4 -2
  114. package/dist/marks.d.ts +3 -0
  115. package/dist/marks.js +3 -0
  116. package/dist/member-store.d.ts +30 -1
  117. package/dist/member-store.js +37 -6
  118. package/dist/migrations.d.ts +37 -0
  119. package/dist/migrations.js +73 -0
  120. package/dist/permissions.d.ts +7 -0
  121. package/dist/permissions.js +19 -0
  122. package/dist/schema-version.d.ts +17 -0
  123. package/dist/schema-version.js +32 -0
  124. package/dist/sql-space.js +6 -0
  125. package/dist/sql.js +10 -0
  126. package/dist/store.d.ts +3 -0
  127. package/dist/store.js +4 -0
  128. package/dist/supabase-member-store.d.ts +8 -1
  129. package/dist/supabase-member-store.js +62 -2
  130. package/dist/supabase-store.d.ts +3 -0
  131. package/dist/supabase-store.js +23 -0
  132. package/dist/time-zone.d.ts +6 -0
  133. package/dist/time-zone.js +27 -0
  134. package/migrations/20260926120000_smoodly_v1.sql +535 -0
  135. package/package.json +13 -3
@@ -0,0 +1,19 @@
1
+ // Roles and what each may do (spec 2026-09-25 members management §1–§2).
2
+ // The ONE place a role string is read: the ops and the shell ask can(),
3
+ // the gate normalizes what the table holds. A third role is one row in
4
+ // GRANTS; a stored matrix, if ever wanted, replaces GRANTS behind can().
5
+ export const MEMBER_ROLES = ["admin", "editor"];
6
+ const GRANTS = {
7
+ admin: ["content.write", "settings", "members.manage"],
8
+ editor: ["content.write"],
9
+ };
10
+ export function can(role, capability) {
11
+ return GRANTS[role].includes(capability);
12
+ }
13
+ export const isMemberRole = (v) => typeof v === "string" && MEMBER_ROLES.includes(v);
14
+ /** What the table holds → a role: `owner` (the pre-2026-09-25 word) is admin; anything unknown reads as editor, the safer side. */
15
+ export function normalizeRole(stored) {
16
+ if (stored === "owner")
17
+ return "admin";
18
+ return isMemberRole(stored) ? stored : "editor";
19
+ }
@@ -0,0 +1,17 @@
1
+ /** The minimum schema version the package needs. Moves ONLY in a release
2
+ * that adds a table, column, function or policy the package reads; a
3
+ * unit test holds it equal to the highest file in `migrations/`. */
4
+ export declare const SCHEMA_VERSION = 1;
5
+ export declare const SCHEMA_VERSION_KEY = "schema_version";
6
+ export declare const CORE_FILE_RE: RegExp;
7
+ export declare const VIEWS_FILE_RE: RegExp;
8
+ /** The version a core file leaves behind, from its name; null for anything else. */
9
+ export declare function coreFileVersion(name: string): number | null;
10
+ /** The last statement of every core file. */
11
+ export declare function schemaVersionSQL(version: number): string;
12
+ /** Behind blocks the admin and warns on the site (§5). Null = no
13
+ * `smoodly_meta` table, a database older than version 1. Ahead is the
14
+ * normal state of a cloud app between our migration and its package
15
+ * bump, and is fine. */
16
+ export declare const schemaBehind: (database: number | null, required?: number) => boolean;
17
+ export declare function schemaBehindMessage(database: number | null, required?: number): string;
@@ -0,0 +1,32 @@
1
+ // packages/smoodly/src/schema-version.ts
2
+ // The schema version this package needs (spec 2026-09-26 §1) and the two
3
+ // things every side agrees on: the `smoodly_meta` row that records a
4
+ // database's version, and the names of the files the package ships. Pure
5
+ // and framework-free — the SQL emitters, the CLI, the stores, the admin
6
+ // op and the shell all read it from here.
7
+ /** The minimum schema version the package needs. Moves ONLY in a release
8
+ * that adds a table, column, function or policy the package reads; a
9
+ * unit test holds it equal to the highest file in `migrations/`. */
10
+ export const SCHEMA_VERSION = 1;
11
+ export const SCHEMA_VERSION_KEY = "schema_version";
12
+ export const CORE_FILE_RE = /^(\d{14})_smoodly_v(\d+)\.sql$/;
13
+ export const VIEWS_FILE_RE = /^(\d{14})_smoodly_views\.sql$/;
14
+ /** The version a core file leaves behind, from its name; null for anything else. */
15
+ export function coreFileVersion(name) {
16
+ const m = CORE_FILE_RE.exec(name);
17
+ return m ? Number(m[2]) : null;
18
+ }
19
+ /** The last statement of every core file. */
20
+ export function schemaVersionSQL(version) {
21
+ return (`insert into "smoodly_meta" ("key", "value") values ('${SCHEMA_VERSION_KEY}', '${version}')\n` +
22
+ ` on conflict ("key") do update set "value" = excluded."value";`);
23
+ }
24
+ /** Behind blocks the admin and warns on the site (§5). Null = no
25
+ * `smoodly_meta` table, a database older than version 1. Ahead is the
26
+ * normal state of a cloud app between our migration and its package
27
+ * bump, and is fine. */
28
+ export const schemaBehind = (database, required = SCHEMA_VERSION) => database === null || database < required;
29
+ export function schemaBehindMessage(database, required = SCHEMA_VERSION) {
30
+ return (`smoodly: the database schema is at version ${database ?? "none"}; this Smoodly needs version ${required}. ` +
31
+ "Run `npx smoodly migrate`, then `supabase db push` (hosted) or `supabase db reset` (local).");
32
+ }
package/dist/sql-space.js CHANGED
@@ -193,6 +193,12 @@ $$;`.trim());
193
193
  statements.push(`drop policy if exists "smoodly_read" on "spaces";\n` +
194
194
  `create policy "smoodly_read" on "spaces" for select\n` +
195
195
  ` using (${write});`);
196
+ // The package reads its schema version with the write key (the
197
+ // service role bypasses anyway); nothing else sees the table. No
198
+ // space_id: the schema is one per database (spec 2026-09-26 §1).
199
+ statements.push(`drop policy if exists "smoodly_read" on "smoodly_meta";\n` +
200
+ `create policy "smoodly_read" on "smoodly_meta" for select\n` +
201
+ ` using ((select "smoodly_key_kind"()) = 'write' and not (select "smoodly_space_key_revoked"()));`);
196
202
  statements.push(storageSQL());
197
203
  return statements.join("\n\n");
198
204
  }
package/dist/sql.js CHANGED
@@ -286,6 +286,15 @@ $$;
286
286
  -- to anything but a write key. The generated per-collection views
287
287
  -- (collectionSQL) run with security_invoker = true, so they are subject
288
288
  -- to this same smoodly_read policy rather than their owner's privileges.
289
+ -- smoodly_meta: which version of this schema the database holds (spec
290
+ -- 2026-09-26 §1). One row per database, not per space; every core
291
+ -- migration file upserts the version row as its last statement, and the
292
+ -- package compares it with SCHEMA_VERSION (behind blocks the admin).
293
+ create table if not exists "smoodly_meta" (
294
+ "key" text primary key,
295
+ "value" text not null
296
+ );
297
+
289
298
  alter table "pages" enable row level security;
290
299
  alter table "page_locales" enable row level security;
291
300
  alter table "page_versions" enable row level security;
@@ -295,6 +304,7 @@ alter table "entry_versions" enable row level security;
295
304
  alter table "refs" enable row level security;
296
305
  alter table "paths" enable row level security;
297
306
  alter table "assets" enable row level security;
307
+ alter table "smoodly_meta" enable row level security;
298
308
  `.trim(),
299
309
  spaceLayerSQL(),
300
310
  ].join("\n\n");
package/dist/store.d.ts CHANGED
@@ -54,6 +54,8 @@ export interface PageStore {
54
54
  getPage(id: string): Promise<PageRecord | null>;
55
55
  listPages(): Promise<PageRecord[]>;
56
56
  resolvePath(locale: string, path: string): Promise<PathTarget | null>;
57
+ /** The database's schema version from `smoodly_meta`; null when the table is missing (older than v1). Memory: always current (spec 2026-09-26 §5). */
58
+ schemaVersion(): Promise<number | null>;
57
59
  /** locale → path, for the locales the page exists in. */
58
60
  pathsOf(id: string): Promise<Record<string, string>>;
59
61
  /** Refused when the page already has the locale or its parent lacks it (the parent rule). */
@@ -194,6 +196,7 @@ export declare class MemoryPageStore implements PageStore {
194
196
  getPage(id: string): Promise<PageRecord | null>;
195
197
  listPages(): Promise<PageRecord[]>;
196
198
  resolvePath(locale: string, path: string): Promise<PathTarget | null>;
199
+ schemaVersion(): Promise<number>;
197
200
  pathsOf(id: string): Promise<Record<string, string>>;
198
201
  addLocale(id: string, locale: string, options: AddLocaleOptions, by?: string): Promise<PageRecord>;
199
202
  removeLocale(id: string, locale: string): Promise<PageRecord>;
package/dist/store.js CHANGED
@@ -13,6 +13,7 @@ import { ASSET_COLLECTION } from "./media.js";
13
13
  import { collectTreeRefs } from "./refs.js";
14
14
  import { customToolset, withCustomSchema } from "./custom/index.js";
15
15
  import { MemoryPathIndex } from "./path-index.js";
16
+ import { SCHEMA_VERSION } from "./schema-version.js";
16
17
  import { assertDepth, assertSegment, assertSupportedLocale, chainOf, isHomeNode, pagePathRows, unsupportedLocale, } from "./paths.js";
17
18
  export const DEFAULT_LOCALES = { default: "en", supported: ["en"] };
18
19
  /** The section list every adapter walks refs with: the registered ones plus the custom section's schema (spec 2026-09-14 §6). */
@@ -266,6 +267,9 @@ export class MemoryPageStore {
266
267
  resolvePath(locale, path) {
267
268
  return this.paths.resolve(locale, path);
268
269
  }
270
+ schemaVersion() {
271
+ return Promise.resolve(SCHEMA_VERSION);
272
+ }
269
273
  pathsOf(id) {
270
274
  return this.paths.pathsOf({ kind: "page", id });
271
275
  }
@@ -1,8 +1,15 @@
1
1
  import type { SupabaseClient } from "@supabase/supabase-js";
2
- import { type MemberRecord, type MemberStore } from "./member-store.ts";
2
+ import { type InviteInput, type InviteResult, type MemberRecord, type MemberStore } from "./member-store.ts";
3
+ import type { MemberRole } from "./permissions.ts";
3
4
  export declare class SupabaseMemberStore implements MemberStore {
4
5
  private db;
5
6
  constructor(db: SupabaseClient);
6
7
  list(): Promise<MemberRecord[]>;
7
8
  consoleUrl(): Promise<string | null>;
9
+ private toRecord;
10
+ /** The admin API has no lookup by email: page through the users (tens, not thousands). */
11
+ private userIdByEmail;
12
+ invite(input: InviteInput): Promise<InviteResult>;
13
+ setRole(userId: string, role: MemberRole): Promise<MemberRecord>;
14
+ remove(userId: string): Promise<void>;
8
15
  }
@@ -1,4 +1,4 @@
1
- import { sortMembers } from "./member-store.js";
1
+ import { memberErrors, sortMembers } from "./member-store.js";
2
2
  export class SupabaseMemberStore {
3
3
  constructor(db) {
4
4
  this.db = db;
@@ -7,7 +7,7 @@ export class SupabaseMemberStore {
7
7
  const res = await this.db.from("members").select("user_id, email, role");
8
8
  if (res.error)
9
9
  throw new Error(`smoodly: ${res.error.message}`);
10
- return sortMembers(res.data.map((r) => ({ userId: r.user_id, email: r.email, role: r.role })));
10
+ return sortMembers(res.data.map((r) => this.toRecord(r)));
11
11
  }
12
12
  async consoleUrl() {
13
13
  const res = await this.db.from("spaces").select("console_url").limit(1).maybeSingle();
@@ -15,4 +15,64 @@ export class SupabaseMemberStore {
15
15
  throw new Error(`smoodly: ${res.error.message}`);
16
16
  return res.data?.console_url ?? null;
17
17
  }
18
+ toRecord(row) {
19
+ return { userId: row.user_id, email: row.email, role: row.role };
20
+ }
21
+ /** The admin API has no lookup by email: page through the users (tens, not thousands). */
22
+ async userIdByEmail(email) {
23
+ const wanted = email.toLowerCase();
24
+ for (let page = 1; page <= 50; page++) {
25
+ const res = await this.db.auth.admin.listUsers({ page, perPage: 200 });
26
+ if (res.error)
27
+ throw new Error(`smoodly: ${res.error.message}`);
28
+ const hit = res.data.users.find((u) => u.email?.toLowerCase() === wanted);
29
+ if (hit)
30
+ return hit.id;
31
+ if (res.data.users.length < 200)
32
+ break;
33
+ }
34
+ throw memberErrors.noAccount(email);
35
+ }
36
+ async invite(input) {
37
+ // Self-host: the content client is the auth project's service role, so
38
+ // the admin API is reachable. A cloud write key never gets here — the
39
+ // ops refuse behind the console URL first.
40
+ const sent = await this.db.auth.admin.inviteUserByEmail(input.email, { redirectTo: input.redirectTo });
41
+ let userId;
42
+ let invited;
43
+ if (!sent.error) {
44
+ userId = sent.data.user.id;
45
+ invited = true;
46
+ }
47
+ else if (sent.error.code === "email_exists" || /already (been )?registered/i.test(sent.error.message)) {
48
+ userId = await this.userIdByEmail(input.email);
49
+ invited = false;
50
+ }
51
+ else {
52
+ throw new Error(`smoodly: ${sent.error.message}`);
53
+ }
54
+ const res = await this.db
55
+ .from("members")
56
+ .upsert({ user_id: userId, email: input.email, role: input.role }, { onConflict: "space_id,user_id" })
57
+ .select("user_id, email, role")
58
+ .single();
59
+ if (res.error)
60
+ throw new Error(`smoodly: ${res.error.message}`);
61
+ return { member: this.toRecord(res.data), invited };
62
+ }
63
+ async setRole(userId, role) {
64
+ const res = await this.db.from("members").update({ role }).eq("user_id", userId).select("user_id, email, role").maybeSingle();
65
+ if (res.error)
66
+ throw new Error(`smoodly: ${res.error.message}`);
67
+ if (!res.data)
68
+ throw memberErrors.notFound(userId);
69
+ return this.toRecord(res.data);
70
+ }
71
+ async remove(userId) {
72
+ const res = await this.db.from("members").delete().eq("user_id", userId).select("user_id");
73
+ if (res.error)
74
+ throw new Error(`smoodly: ${res.error.message}`);
75
+ if (!res.data || res.data.length === 0)
76
+ throw memberErrors.notFound(userId);
77
+ }
18
78
  }
@@ -48,7 +48,10 @@ export declare class SupabasePageStore implements PageStore {
48
48
  createPage(input: CreatePageInput, by?: string): Promise<PageRecord>;
49
49
  getPage(id: string): Promise<PageRecord | null>;
50
50
  listPages(): Promise<PageRecord[]>;
51
+ /** The site's one warning per process (spec 2026-09-26 §5): fired by the first locate, never blocking. */
52
+ private schemaChecked;
51
53
  resolvePath(locale: string, path: string): Promise<PathTarget | null>;
54
+ schemaVersion(): Promise<number | null>;
52
55
  pathsOf(id: string): Promise<{}>;
53
56
  addLocale(id: string, locale: string, options: AddLocaleOptions, by?: string): Promise<PageRecord>;
54
57
  removeLocale(id: string, locale: string): Promise<PageRecord>;
@@ -8,6 +8,7 @@
8
8
  // rewritten through ONE RPC per write, so a collision rolls back.
9
9
  import { assertPublishable, assertSupportedLocale, localeRow, pageErrors, pageRefEdges, pathOptions, refSections, } from "./store.js";
10
10
  import { SupabasePathIndex } from "./supabase-path-index.js";
11
+ import { SCHEMA_VERSION_KEY, schemaBehind, schemaBehindMessage } from "./schema-version.js";
11
12
  import { assertDepth, assertSegment, isHomeNode, pagePathRows } from "./paths.js";
12
13
  const SELECT = "*, page_locales(*)";
13
14
  const UNIQUE_VIOLATION = "23505";
@@ -40,6 +41,8 @@ export class SupabasePageStore {
40
41
  constructor(db, options = {}) {
41
42
  this.db = db;
42
43
  this.options = options;
44
+ /** The site's one warning per process (spec 2026-09-26 §5): fired by the first locate, never blocking. */
45
+ this.schemaChecked = false;
43
46
  this.options = { ...options, sections: refSections(options) };
44
47
  this.paths = options.paths ?? new SupabasePathIndex(db);
45
48
  this.opts = pathOptions(options);
@@ -300,8 +303,28 @@ export class SupabasePageStore {
300
303
  return res.data.map(toRecord);
301
304
  }
302
305
  resolvePath(locale, path) {
306
+ if (!this.schemaChecked) {
307
+ this.schemaChecked = true;
308
+ void this.schemaVersion()
309
+ .then((v) => {
310
+ if (schemaBehind(v))
311
+ console.warn(schemaBehindMessage(v));
312
+ })
313
+ .catch(() => { });
314
+ }
303
315
  return this.paths.resolve(locale, path);
304
316
  }
317
+ async schemaVersion() {
318
+ const res = await this.db.from("smoodly_meta").select("value").eq("key", SCHEMA_VERSION_KEY).maybeSingle();
319
+ if (res.error) {
320
+ // PGRST205: not in PostgREST's schema cache; 42P01: undefined table.
321
+ if (res.error.code === "PGRST205" || res.error.code === "42P01")
322
+ return null;
323
+ this.fail(res.error.message);
324
+ }
325
+ const n = Number(res.data?.value);
326
+ return Number.isInteger(n) ? n : null;
327
+ }
305
328
  pathsOf(id) {
306
329
  return UUID_RE.test(id) ? this.paths.pathsOf({ kind: "page", id }) : Promise.resolve({});
307
330
  }
@@ -0,0 +1,6 @@
1
+ /** Whether this runtime's Intl knows the zone: canonical names, the
2
+ * aliases its database still carries ("Europe/Kiev"), "UTC". An offset
3
+ * like "+3" or a bare city is refused. */
4
+ export declare function isValidZone(zone: string): boolean;
5
+ /** The zone the runtime resolves for itself — the editor's browser in the admin; "UTC" when it has none. */
6
+ export declare function browserZone(): string;
@@ -0,0 +1,27 @@
1
+ // The site's time zone (spec 2026-09-25 date pickers §5): an IANA name,
2
+ // never an offset, so daylight-saving rules come from the runtime's own
3
+ // zone database. Core and pure: defineConfig validates with it, the
4
+ // admin's picker falls back to the browser's zone through it.
5
+ /** Whether this runtime's Intl knows the zone: canonical names, the
6
+ * aliases its database still carries ("Europe/Kiev"), "UTC". An offset
7
+ * like "+3" or a bare city is refused. */
8
+ export function isValidZone(zone) {
9
+ if (zone === "")
10
+ return false;
11
+ try {
12
+ new Intl.DateTimeFormat("en", { timeZone: zone });
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ /** The zone the runtime resolves for itself — the editor's browser in the admin; "UTC" when it has none. */
20
+ export function browserZone() {
21
+ try {
22
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
23
+ }
24
+ catch {
25
+ return "UTC";
26
+ }
27
+ }