smoodly 0.0.52 → 0.0.54
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.
- package/dist/admin/client-ops.js +1 -1
- package/dist/admin/editor/CanvasOverlay.d.ts +3 -1
- package/dist/admin/editor/CanvasOverlay.js +2 -2
- package/dist/admin/editor/EditorView.js +243 -63
- package/dist/admin/editor/InsertPicker.d.ts +8 -1
- package/dist/admin/editor/InsertPicker.js +2 -2
- package/dist/admin/editor/InspectorFields.js +1 -1
- package/dist/admin/editor/Outline.d.ts +2 -3
- package/dist/admin/editor/PageInspector.js +1 -1
- package/dist/admin/editor/StructureView.d.ts +31 -0
- package/dist/admin/editor/StructureView.js +58 -0
- package/dist/admin/editor/card-preview.d.ts +10 -0
- package/dist/admin/editor/card-preview.js +50 -0
- package/dist/admin/editor/global-edit.js +29 -22
- package/dist/admin/editor/history.d.ts +39 -0
- package/dist/admin/editor/history.js +64 -0
- package/dist/admin/editor/section-clipboard.d.ts +40 -0
- package/dist/admin/editor/section-clipboard.js +106 -0
- package/dist/admin/editor/undo-keys.d.ts +19 -0
- package/dist/admin/editor/undo-keys.js +27 -0
- package/dist/admin/editor/useInlineSession.d.ts +4 -1
- package/dist/admin/editor/useInlineSession.js +11 -2
- package/dist/admin/editor/view-memory.d.ts +9 -0
- package/dist/admin/editor/view-memory.js +17 -0
- package/dist/admin/files.generated.js +1 -1
- package/dist/admin/handler.d.ts +1 -1
- package/dist/admin/index.d.ts +2 -2
- package/dist/admin/ops.d.ts +4 -10
- package/dist/admin/page-data.d.ts +1 -1
- package/dist/admin/session-flow.d.ts +24 -2
- package/dist/admin/session-flow.js +29 -2
- package/dist/admin/shell/AdminApp.d.ts +1 -1
- package/dist/admin/shell/IconRail.js +5 -4
- package/dist/admin/shell/presence.d.ts +1 -1
- package/dist/admin/shell/session.d.ts +1 -2
- package/dist/admin/shell/session.js +10 -9
- package/dist/admin/ui/ActionMenu.d.ts +2 -0
- package/dist/admin/ui/ActionMenu.js +1 -1
- package/dist/admin/ui/primitives.d.ts +1 -0
- package/dist/admin/ui/primitives.js +3 -2
- package/dist/admin/ui/theme.js +70 -0
- package/dist/admin/ui/useSortable.js +3 -1
- package/dist/api-client.js +1 -1
- package/dist/assemble.js +1 -1
- package/dist/astro/cache.d.ts +1 -1
- package/dist/astro/index.js +11 -6
- package/dist/astro/runtime.d.ts +1 -1
- package/dist/astro/runtime.js +1 -1
- package/dist/astro/shell.d.ts +1 -1
- package/dist/astro/shell.js +1 -1
- package/dist/content/content-api.d.ts +1 -0
- package/dist/content/content-api.js +3 -1
- package/dist/css.js +1 -1
- package/dist/entry-store.d.ts +5 -5
- package/dist/entry-store.js +7 -7
- package/dist/image/markup.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/media-cut.d.ts +1 -1
- package/dist/media-cut.js +2 -2
- package/dist/media.d.ts +1 -1
- package/dist/media.js +2 -2
- package/dist/member-store.d.ts +0 -29
- package/dist/member-store.js +0 -4
- package/dist/paths.d.ts +1 -1
- package/dist/paths.js +1 -1
- package/dist/revalidate.d.ts +1 -1
- package/dist/revalidate.js +1 -1
- package/dist/space-transfer.d.ts +1 -1
- package/dist/space-transfer.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/admin/auth-config.d.ts +0 -7
- package/dist/admin/auth-config.js +0 -1
- package/dist/admin/browser/fetch-op.d.ts +0 -10
- package/dist/admin/browser/fetch-op.js +0 -45
- package/dist/admin/browser/main.d.ts +0 -1
- package/dist/admin/browser/main.js +0 -27
package/dist/admin/handler.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { PresenceConfig } from "../env.ts";
|
|
|
3
3
|
import { type KeySet } from "../signed-message.ts";
|
|
4
4
|
import { type AdminFiles } from "./files.ts";
|
|
5
5
|
import type { AdminEffects } from "./ops.ts";
|
|
6
|
-
import type { SignInConfig } from "./
|
|
6
|
+
import type { SignInConfig } from "./session-flow.ts";
|
|
7
7
|
export type AdminHandlerDeps = {
|
|
8
8
|
config: ResolvedConfig;
|
|
9
9
|
/** Where the browser sends a person to sign in, and the dashboard it links to: public values only. Absent, the page
|
package/dist/admin/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export type { AdminEffects, AdminOps, AdminOpCall, AdminOpAction, OpResult, OpError, PageCreateInput, PageGetResult, GlobalListItem, GlobalGetResult, AssetUsageItem, MembersListResult } from "./ops.ts";
|
|
2
|
-
export type {
|
|
2
|
+
export type { MemberRecord } from "../member-store.ts";
|
|
3
3
|
export type { MemberRole } from "../permissions.ts";
|
|
4
4
|
export { fixedNodeSpecs, fixedNodeOf, type FixedNode } from "./fixed-nodes.ts";
|
|
5
5
|
export { initialGlobalFields } from "./global-items.ts";
|
|
6
6
|
export { makeClientOps } from "./client-ops.ts";
|
|
7
|
-
export type { SignInConfig } from "./
|
|
7
|
+
export type { SignInConfig } from "./session-flow.ts";
|
|
8
8
|
export { serializeAdminConfig } from "./serialize.ts";
|
|
9
9
|
export type { AdminRegistry, AdminSection, AdminCollection, AdminPageTemplate, AdminGlobal, AdminField } from "./serialize.ts";
|
|
10
10
|
export * from "./tree-ops.ts";
|
package/dist/admin/ops.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { PageTree } from "../page-context.ts";
|
|
|
3
3
|
import type { EntryRecord, EntryUsage } from "../entry-store.ts";
|
|
4
4
|
import type { AssetRecord, AssetMeta } from "../media.ts";
|
|
5
5
|
import type { AssetPage, AssetQuery } from "../asset-store.ts";
|
|
6
|
-
import type {
|
|
6
|
+
import type { MemberRecord } from "../member-store.ts";
|
|
7
7
|
import type { MemberRole } from "../permissions.ts";
|
|
8
8
|
import type { FieldError } from "./validate.ts";
|
|
9
9
|
import type { DeployState } from "../deploy-state.ts";
|
|
@@ -126,10 +126,9 @@ export type AssetUsageItem = {
|
|
|
126
126
|
title: string;
|
|
127
127
|
href: string | null;
|
|
128
128
|
};
|
|
129
|
-
/** The Members page's read (spec 2026-09-25 §4): the rows
|
|
129
|
+
/** The Members page's read (spec 2026-09-25 §4): the rows; the dashboard manages them. */
|
|
130
130
|
export type MembersListResult = {
|
|
131
131
|
members: MemberRecord[];
|
|
132
|
-
consoleUrl: string | null;
|
|
133
132
|
};
|
|
134
133
|
/** `database` null = no smoodly_meta table (older than v1). Behind blocks the shell; ahead is silent (spec 2026-09-26 §5). */
|
|
135
134
|
/** The Deploy tile's state (spec 2026-10-09 static mode §6); `hook` false = SMOODLY_DEPLOY_HOOK is unset, Deploy disabled. */
|
|
@@ -254,18 +253,13 @@ export type AdminOps = {
|
|
|
254
253
|
usage(id: string): Promise<OpResult<AssetUsageItem[]>>;
|
|
255
254
|
};
|
|
256
255
|
members: {
|
|
257
|
-
/** The space's members
|
|
256
|
+
/** The space's members, a read-only list; admins only. Invites, roles and removals are the dashboard's
|
|
257
|
+
* (spec 2026-10-16 sign-in and the dashboard §6). */
|
|
258
258
|
list(): Promise<OpResult<MembersListResult>>;
|
|
259
259
|
/** The caller's role; every member. The shell hides Settings from editors with it. */
|
|
260
260
|
me(): Promise<OpResult<{
|
|
261
261
|
role: MemberRole;
|
|
262
262
|
}>>;
|
|
263
|
-
/** Admins only, self-host only: invites (or adds an existing account), refuses a duplicate row (spec 2026-09-25 members management §3). */
|
|
264
|
-
invite(input: InviteInput): Promise<OpResult<InviteResult>>;
|
|
265
|
-
/** Admins only, self-host only; never your own row, never the last admin. */
|
|
266
|
-
setRole(userId: string, role: MemberRole): Promise<OpResult<MemberRecord>>;
|
|
267
|
-
/** Admins only, self-host only; the same two guards. The Auth account stays. */
|
|
268
|
-
remove(userId: string): Promise<OpResult<null>>;
|
|
269
263
|
};
|
|
270
264
|
/** Redirects by hand (spec 2026-09-26 redirects §5): open to every member, as renaming is; nothing here expires a cache. */
|
|
271
265
|
redirects: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PresenceConfig } from "../env.ts";
|
|
2
2
|
import type { AdminRegistry } from "./serialize.ts";
|
|
3
|
-
import type { SignInConfig } from "./
|
|
3
|
+
import type { SignInConfig } from "./session-flow.ts";
|
|
4
4
|
export type AdminPageData = {
|
|
5
5
|
registry: AdminRegistry;
|
|
6
6
|
/** Where the browser sends a person to sign in and the dashboard it links to: public values only. */
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { MemberRole } from "../permissions.ts";
|
|
2
|
-
|
|
2
|
+
/** Where the admin sends a person to sign in, and the dashboard its account menu and Members page link to (spec
|
|
3
|
+
* 2026-10-16 sign-in and the dashboard §1, §6): `GET /v1/auth` answers it from the Worker's env; the handler and the
|
|
4
|
+
* page data carry it. Public values only. */
|
|
5
|
+
export type SignInConfig = {
|
|
6
|
+
signInUrl: string;
|
|
7
|
+
consoleUrl: string;
|
|
8
|
+
};
|
|
3
9
|
export declare const STORAGE_KEY: (space: string) => string;
|
|
4
10
|
/** The PKCE verifier, in this tab's `sessionStorage` only: the code is good for the browser tab that asked for it. */
|
|
5
11
|
export declare const VERIFIER_KEY = "smoodly-verifier";
|
|
@@ -26,10 +32,26 @@ export declare function readSession(local: Storage, space: string): StoredSessio
|
|
|
26
32
|
export declare function signInAddress(config: SignInConfig, space: string, returnUrl: string, stores: Pick<SessionStores, "tab">): Promise<string>;
|
|
27
33
|
/** The one-time code the sign-in page put in the fragment, or null. */
|
|
28
34
|
export declare function codeFrom(hash: string): string | null;
|
|
29
|
-
/**
|
|
35
|
+
/** What the admin shows when the service cannot be reached at sign-in: a refusal with a way back, never a blank screen. */
|
|
36
|
+
export declare const UNREACHABLE = "smoodly: couldn't reach Smoodly to sign you in \u2014 check the connection and sign in again.";
|
|
37
|
+
/** The code and this tab's verifier for a session, stored; or why not. The verifier is spent either way. A service
|
|
38
|
+
* that cannot be reached is a refusal too (UNREACHABLE), so the shell shows it rather than waiting forever. */
|
|
30
39
|
export declare function exchange(api: string, space: string, code: string, stores: SessionStores, doFetch?: typeof fetch): Promise<StoredSession | {
|
|
31
40
|
refused: string;
|
|
32
41
|
}>;
|
|
42
|
+
/** The load-time step with no code in the fragment: the stored session adopted as it is while its pass has more than
|
|
43
|
+
* RENEW_BEFORE_MS left, else renewed; a renewal the service refuses sends the person to sign in; one that cannot reach
|
|
44
|
+
* the service keeps a pass that is still valid (the next timer tries again) and refuses with UNREACHABLE only once the
|
|
45
|
+
* pass has expired. No stored session: sign in. */
|
|
46
|
+
export declare function resume(api: string, space: string, stores: Pick<SessionStores, "local">, now: number, doFetch?: typeof fetch, lock?: TabLock): Promise<{
|
|
47
|
+
kind: "adopt";
|
|
48
|
+
session: StoredSession;
|
|
49
|
+
} | {
|
|
50
|
+
kind: "sign_in";
|
|
51
|
+
} | {
|
|
52
|
+
kind: "refused";
|
|
53
|
+
message: string;
|
|
54
|
+
}>;
|
|
33
55
|
/** Runs `fn` while holding a lock no other tab of this origin holds (Web Locks); with no Web Locks (an insecure
|
|
34
56
|
* context), at once. */
|
|
35
57
|
export type TabLock = <T>(name: string, fn: () => Promise<T>) => Promise<T>;
|
|
@@ -38,18 +38,45 @@ async function post(doFetch, url, body) {
|
|
|
38
38
|
const json = (await res.json().catch(() => null));
|
|
39
39
|
return { status: res.status, data: json?.ok ? json.data : null, message: typeof json?.message === "string" ? json.message : null };
|
|
40
40
|
}
|
|
41
|
-
/**
|
|
41
|
+
/** What the admin shows when the service cannot be reached at sign-in: a refusal with a way back, never a blank screen. */
|
|
42
|
+
export const UNREACHABLE = "smoodly: couldn't reach Smoodly to sign you in — check the connection and sign in again.";
|
|
43
|
+
/** The code and this tab's verifier for a session, stored; or why not. The verifier is spent either way. A service
|
|
44
|
+
* that cannot be reached is a refusal too (UNREACHABLE), so the shell shows it rather than waiting forever. */
|
|
42
45
|
export async function exchange(api, space, code, stores, doFetch = fetch) {
|
|
43
46
|
const verifier = stores.tab.getItem(VERIFIER_KEY);
|
|
44
47
|
stores.tab.removeItem(VERIFIER_KEY);
|
|
45
48
|
if (!verifier)
|
|
46
49
|
return { refused: "smoodly: this sign-in was started in another tab or has already been used — sign in again." };
|
|
47
|
-
|
|
50
|
+
let answer;
|
|
51
|
+
try {
|
|
52
|
+
answer = await post(doFetch, sessionUrl(api, space, "session"), { code, verifier });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return { refused: UNREACHABLE };
|
|
56
|
+
}
|
|
48
57
|
if (answer.status !== 200 || !isSession(answer.data))
|
|
49
58
|
return { refused: answer.message ?? `smoodly: the sign-in was refused (${answer.status}).` };
|
|
50
59
|
writeSession(stores.local, space, answer.data);
|
|
51
60
|
return answer.data;
|
|
52
61
|
}
|
|
62
|
+
/** The load-time step with no code in the fragment: the stored session adopted as it is while its pass has more than
|
|
63
|
+
* RENEW_BEFORE_MS left, else renewed; a renewal the service refuses sends the person to sign in; one that cannot reach
|
|
64
|
+
* the service keeps a pass that is still valid (the next timer tries again) and refuses with UNREACHABLE only once the
|
|
65
|
+
* pass has expired. No stored session: sign in. */
|
|
66
|
+
export async function resume(api, space, stores, now, doFetch = fetch, lock = tabLock) {
|
|
67
|
+
const stored = readSession(stores.local, space);
|
|
68
|
+
if (!stored)
|
|
69
|
+
return { kind: "sign_in" };
|
|
70
|
+
if (stored.expiresAt - now > RENEW_BEFORE_MS)
|
|
71
|
+
return { kind: "adopt", session: stored };
|
|
72
|
+
try {
|
|
73
|
+
const renewed = await renew(api, space, stores, stored.refresh, doFetch, lock);
|
|
74
|
+
return renewed ? { kind: "adopt", session: renewed } : { kind: "sign_in" };
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return stored.expiresAt > now ? { kind: "adopt", session: stored } : { kind: "refused", message: UNREACHABLE };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
53
80
|
const tabLock = (name, fn) => {
|
|
54
81
|
const locks = globalThis.navigator?.locks;
|
|
55
82
|
return locks ? locks.request(name, fn) : fn();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AdminOpAction } from "../ops.ts";
|
|
2
2
|
import type { AdminRegistry } from "../serialize.ts";
|
|
3
|
-
import type { SignInConfig } from "../
|
|
3
|
+
import type { SignInConfig } from "../session-flow.ts";
|
|
4
4
|
import type { PresenceConfig } from "../../env.ts";
|
|
5
5
|
export declare function AdminApp({ registry, op, signIn, api, presenceConfig }: {
|
|
6
6
|
registry: AdminRegistry;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
// packages/smoodly/src/admin/shell/IconRail.tsx
|
|
4
|
-
// The admin's primary navigation: the design
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// The admin's primary navigation: the design handoff's 72px IconRail (the
|
|
5
|
+
// cool-blue look's components/navigation/IconRail, in the tree until 2026-10-19;
|
|
6
|
+
// the shipped tokens are design_handoff_smoodly_neutral/), which replaced the
|
|
7
|
+
// 200px SideNav so the visual editor gets the width. Ported markup-for-markup;
|
|
8
|
+
// the handoff's click handlers are the shell's router.
|
|
8
9
|
//
|
|
9
10
|
// Items are registry-derived, never separately configured: Pages, then
|
|
10
11
|
// Collections — one rail item whose flyout lists every collection — then
|
|
@@ -15,7 +15,7 @@ export type PresenceState = {
|
|
|
15
15
|
status: PresenceStatus;
|
|
16
16
|
others: PresenceEntry[];
|
|
17
17
|
};
|
|
18
|
-
/** The channel presence uses:
|
|
18
|
+
/** The channel presence uses: the service's socket (presence-socket.ts). */
|
|
19
19
|
export type PresenceChannel = {
|
|
20
20
|
subscribe(onStatus: (status: string) => void): void;
|
|
21
21
|
/** The session renewed (its new pass): reconnect with a fresh ticket. The space's object admits a live session of a
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { type StoredSession } from "../session-flow.ts";
|
|
1
|
+
import { type SignInConfig, type StoredSession } from "../session-flow.ts";
|
|
3
2
|
export type SessionUser = StoredSession["user"];
|
|
4
3
|
export type SessionState = {
|
|
5
4
|
status: "loading";
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// straight back in while their smoodly.io session lives. Another tab's renewal or sign out is adopted from the
|
|
8
8
|
// `storage` event. The pure steps are admin/session-flow.ts.
|
|
9
9
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
10
|
-
import { codeFrom, endSession, exchange, readSession, renew, RENEW_BEFORE_MS, signInAddress, STORAGE_KEY } from "../session-flow.js";
|
|
10
|
+
import { codeFrom, endSession, exchange, readSession, renew, RENEW_BEFORE_MS, resume, signInAddress, STORAGE_KEY } from "../session-flow.js";
|
|
11
11
|
/** A renewal's timer fires up to this much later, so two tabs rarely post the same refresh at once. */
|
|
12
12
|
const JITTER_MS = 30000;
|
|
13
13
|
export function useSpaceSession({ apiUrl, spaceId, signIn }) {
|
|
@@ -69,16 +69,17 @@ export function useSpaceSession({ apiUrl, spaceId, signIn }) {
|
|
|
69
69
|
adopt(got);
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
// The stored session, as it is or renewed; a service that cannot be reached keeps a still-valid pass and
|
|
73
|
+
// refuses, with a way back, only once it has expired (session-flow.ts resume).
|
|
74
|
+
const step = await resume(apiUrl, spaceId, stores(), Date.now());
|
|
75
|
+
if (!live)
|
|
75
76
|
return;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
77
|
+
if (step.kind === "adopt")
|
|
78
|
+
adopt(step.session);
|
|
79
|
+
else if (step.kind === "refused")
|
|
80
|
+
setState({ status: "signed_out", reason: "refused", message: step.message });
|
|
80
81
|
else
|
|
81
|
-
await
|
|
82
|
+
await signInAgain();
|
|
82
83
|
})();
|
|
83
84
|
const onStorage = (e) => {
|
|
84
85
|
if (e.key !== STORAGE_KEY(spaceId))
|
|
@@ -3,6 +3,8 @@ export type ActionMenuItem = {
|
|
|
3
3
|
run(): void;
|
|
4
4
|
disabled?: boolean;
|
|
5
5
|
danger?: boolean;
|
|
6
|
+
/** The reason on a disabled entry (a paste the zone refuses), shown as the button's title. */
|
|
7
|
+
title?: string;
|
|
6
8
|
};
|
|
7
9
|
/** The button's box in viewport pixels, `right` measured from the viewport's right edge. */
|
|
8
10
|
type Anchor = {
|
|
@@ -68,5 +68,5 @@ export function ActionMenu({ label, items, disabled, small = false }) {
|
|
|
68
68
|
document.removeEventListener("scroll", onScroll, true);
|
|
69
69
|
};
|
|
70
70
|
}, [open]);
|
|
71
|
-
return (_jsxs("div", { ref: ref, style: { position: "relative", flex: "none" }, children: [_jsx("button", { ref: button, type: "button", className: cx("sm-dotmenu", small && "sm-dotmenu--sm"), "aria-label": label, title: label, "aria-haspopup": "menu", "aria-expanded": open !== null, disabled: disabled, onClick: toggle, children: "\u00B7\u00B7\u00B7" }), open && (_jsx("div", { ref: menu, role: "menu", className: "sm-picker sm-picker--menu", style: place ?? { top: open.bottom + 6, right: open.right, visibility: "hidden" }, children: items.map((item) => (_jsx("button", { type: "button", role: "menuitem", className: cx("sm-picker__item", item.danger && "sm-picker__item--danger"), disabled: item.disabled, onClick: () => { setOpen(null); item.run(); }, children: item.label }, item.label))) }))] }));
|
|
71
|
+
return (_jsxs("div", { ref: ref, style: { position: "relative", flex: "none" }, children: [_jsx("button", { ref: button, type: "button", className: cx("sm-dotmenu", small && "sm-dotmenu--sm"), "aria-label": label, title: label, "aria-haspopup": "menu", "aria-expanded": open !== null, disabled: disabled, onClick: toggle, children: "\u00B7\u00B7\u00B7" }), open && (_jsx("div", { ref: menu, role: "menu", className: "sm-picker sm-picker--menu", style: place ?? { top: open.bottom + 6, right: open.right, visibility: "hidden" }, children: items.map((item) => (_jsx("button", { type: "button", role: "menuitem", className: cx("sm-picker__item", item.danger && "sm-picker__item--danger"), disabled: item.disabled, title: item.title, onClick: () => { setOpen(null); item.run(); }, children: item.label }, item.label))) }))] }));
|
|
72
72
|
}
|
|
@@ -103,6 +103,7 @@ export declare function Segmented<T extends string>({ options, value, onChange,
|
|
|
103
103
|
disabled?: boolean;
|
|
104
104
|
title?: string;
|
|
105
105
|
className?: string;
|
|
106
|
+
"aria-label"?: string;
|
|
106
107
|
} | undefined;
|
|
107
108
|
}): import("react").JSX.Element;
|
|
108
109
|
export declare function StatusBadge({ status }: {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
// The design system's primitives, ported 1:1 from the handoff's
|
|
4
|
-
// components/ set (
|
|
4
|
+
// components/ set (the cool-blue look, in the tree until 2026-10-19; the shipped
|
|
5
|
+
// tokens are design_handoff_smoodly_neutral/). Structure and dynamic
|
|
5
6
|
// values live here; every static rule, and all hover/focus state, lives in
|
|
6
7
|
// theme.tsx's stylesheet.
|
|
7
8
|
import { Fragment, useEffect, useRef } from "react";
|
|
@@ -102,7 +103,7 @@ export function Tabs({ tabs, value, onChange }) {
|
|
|
102
103
|
export function Segmented({ options, value, onChange, stretch, disabled, label, option, className }) {
|
|
103
104
|
return (_jsx("div", { className: cx("sm-seg", stretch && "sm-seg--stretch", disabled && "sm-seg--disabled", className), children: options.map((o) => {
|
|
104
105
|
const extra = option?.(o);
|
|
105
|
-
return (_jsx("button", { type: "button", className: cx("sm-seg__opt", extra?.className), "aria-pressed": o === value, disabled: disabled || extra?.disabled, title: extra?.title, onClick: () => onChange(o), children: label ? label(o) : o }, o));
|
|
106
|
+
return (_jsx("button", { type: "button", className: cx("sm-seg__opt", extra?.className), "aria-pressed": o === value, disabled: disabled || extra?.disabled, title: extra?.title, "aria-label": extra?.["aria-label"], onClick: () => onChange(o), children: label ? label(o) : o }, o));
|
|
106
107
|
}) }));
|
|
107
108
|
}
|
|
108
109
|
/* ── data ────────────────────────────────────────────────────────────── */
|
package/dist/admin/ui/theme.js
CHANGED
|
@@ -638,6 +638,8 @@ body:has(.sm-admin){margin:0}
|
|
|
638
638
|
.sm-seg{display:flex;gap:2px;padding:3px;background:var(--seg-bg);border-radius:var(--radius-btn)}
|
|
639
639
|
/* A locale switch: each option wide enough to read as a tab, not a chip. */
|
|
640
640
|
.sm-seg--locale .sm-seg__opt{min-width:64px}
|
|
641
|
+
/* The Canvas | Structure switch: two icon options. */
|
|
642
|
+
.sm-seg--view .sm-seg__opt{padding:4px 8px;display:grid;place-items:center}
|
|
641
643
|
.sm-seg--stretch{width:100%}
|
|
642
644
|
.sm-seg--disabled{opacity:.6}
|
|
643
645
|
.sm-seg__opt:disabled{cursor:default}
|
|
@@ -768,6 +770,11 @@ body:has(.sm-admin){margin:0}
|
|
|
768
770
|
position:relative;background:var(--page-bg);border-radius:var(--page-radius);
|
|
769
771
|
box-shadow:var(--shadow-page);min-height:100%;
|
|
770
772
|
}
|
|
773
|
+
/* Structure (spec 2026-10-19 editor at launch §1): the structure layer takes the scroller; the frame stays mounted
|
|
774
|
+
and hidden under it: the wrapper is a zero-height absolute box that clips the frame (so the scroller does not grow by the
|
|
775
|
+
page's height), while the frame's own box keeps its width and height for the bridge. After .sm-page: it overrides its position. */
|
|
776
|
+
.sm-canvas__scroll--structure{position:relative}
|
|
777
|
+
.sm-page.sm-canvas__hidden{visibility:hidden;position:absolute;left:0;top:0;width:100%;height:0;min-height:0;pointer-events:none}
|
|
771
778
|
.sm-canvas__loading{
|
|
772
779
|
height:240px;display:grid;place-items:center;color:var(--text-subtle);font-family:var(--meta-font);
|
|
773
780
|
font-size:var(--meta-size);letter-spacing:var(--caps-tracking);text-transform:var(--caps-transform);
|
|
@@ -834,6 +841,67 @@ body:has(.sm-admin){margin:0}
|
|
|
834
841
|
/* A row with a grip: the grip takes the left padding's place and the label follows it. */
|
|
835
842
|
.sm-outline-row--grip{padding-left:2px}
|
|
836
843
|
.sm-outline-row--grip .sm-outline-row__label{flex:1}
|
|
844
|
+
/* The structure view (spec 2026-10-19 editor at launch §1): the canvas column as rows of cards, the
|
|
845
|
+
outline's boxes grown up — a zone's caps label, a card per section on the outline's white, the
|
|
846
|
+
outline's selected fill and drift stripes. The add affordance between cards is the "+" pill at
|
|
847
|
+
label size, faint until its row is hovered. */
|
|
848
|
+
.sm-structure{max-width:720px;margin:0 auto;padding:var(--canvas-pad);padding-bottom:40px;display:flex;flex-direction:column;gap:20px}
|
|
849
|
+
.sm-structure__group{display:flex;flex-direction:column;gap:4px}
|
|
850
|
+
.sm-structure__zone{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:24px}
|
|
851
|
+
.sm-structure__zone .sm-sectionlabel{flex:1;min-width:0}
|
|
852
|
+
.sm-structure__rows{display:flex;flex-direction:column}
|
|
853
|
+
.sm-structure__row{display:flex;flex-direction:column}
|
|
854
|
+
.sm-structure__card{
|
|
855
|
+
display:grid;grid-template-columns:24px 32px minmax(0,1fr) auto auto;gap:10px;align-items:center;
|
|
856
|
+
width:100%;min-height:56px;padding:8px 6px 8px 2px;text-align:left;font:inherit;
|
|
857
|
+
border-radius:var(--outline-row-radius);background:var(--outline-row-bg);border:var(--outline-row-border);
|
|
858
|
+
color:var(--text);cursor:pointer;
|
|
859
|
+
}
|
|
860
|
+
.sm-structure__card:hover{background:var(--control-hover)}
|
|
861
|
+
.sm-structure__card:hover .sm-grip{opacity:1}
|
|
862
|
+
.sm-structure__card:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
|
|
863
|
+
.sm-structure__card--selected,.sm-structure__card--selected:hover{background:var(--nav-active-bg);color:var(--nav-active-fg);border-color:transparent}
|
|
864
|
+
.sm-structure__card--locked{background:var(--surface-inset);color:var(--text-muted);border-color:transparent}
|
|
865
|
+
.sm-structure__card--broken{box-shadow:inset 3px 0 0 var(--danger)}
|
|
866
|
+
.sm-structure__card--orphan{box-shadow:inset 3px 0 0 var(--line-dashed)}
|
|
867
|
+
.sm-structure__gripslot{width:24px;height:24px}
|
|
868
|
+
.sm-structure__icon{
|
|
869
|
+
width:32px;height:32px;display:grid;place-items:center;border-radius:var(--radius-sm);
|
|
870
|
+
background:var(--surface-inset);font-size:var(--text-lg);line-height:1;flex:none;
|
|
871
|
+
}
|
|
872
|
+
.sm-structure__card--selected .sm-structure__icon,.sm-structure__card--locked .sm-structure__icon{background:color-mix(in srgb,currentColor 10%,transparent)}
|
|
873
|
+
.sm-structure__text{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
874
|
+
.sm-structure__title{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:flex;align-items:center;gap:6px;min-width:0}
|
|
875
|
+
.sm-structure__badge{
|
|
876
|
+
font-size:var(--text-sm);font-weight:500;letter-spacing:var(--caps-tracking);text-transform:var(--caps-transform);
|
|
877
|
+
color:var(--text-muted);flex:none;
|
|
878
|
+
}
|
|
879
|
+
.sm-structure__card--selected .sm-structure__badge{color:inherit;opacity:.8}
|
|
880
|
+
.sm-structure__preview{font-size:var(--text-sm);color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:400}
|
|
881
|
+
.sm-structure__card--selected .sm-structure__preview{color:inherit;opacity:.8}
|
|
882
|
+
.sm-structure__thumb{width:44px;height:30px;border-radius:4px;overflow:hidden;background:var(--surface-inset);flex:none;display:block}
|
|
883
|
+
.sm-structure__img{width:100%;height:100%;object-fit:cover;display:block}
|
|
884
|
+
.sm-structure__menu{display:flex;flex:none}
|
|
885
|
+
.sm-structure__slot{display:flex;justify-content:center;padding:4px 0}
|
|
886
|
+
.sm-structure__add{
|
|
887
|
+
display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 10px 0 4px;border-radius:999px;border:0;
|
|
888
|
+
background:var(--plus-bg);color:var(--plus-fg);font:500 var(--text-sm)/1 var(--font-sans);cursor:pointer;
|
|
889
|
+
box-shadow:var(--plus-shadow);white-space:nowrap;opacity:.35;transition:opacity .12s,background .12s;
|
|
890
|
+
}
|
|
891
|
+
.sm-structure__add-icon{
|
|
892
|
+
width:16px;height:16px;flex:none;border-radius:50%;background:var(--plus-icon-bg);color:var(--plus-icon-fg);
|
|
893
|
+
display:grid;place-items:center;font-size:12px;line-height:1;
|
|
894
|
+
}
|
|
895
|
+
.sm-structure__row:hover .sm-structure__add,.sm-structure__slot:hover .sm-structure__add,.sm-structure__add:focus-visible,.sm-structure__empty .sm-structure__add{opacity:1}
|
|
896
|
+
.sm-structure__add:hover{background:var(--plus-hover)}
|
|
897
|
+
/* The lifted row keeps the hook's transform and z-index; its card carries the shadow, its add slot hides. */
|
|
898
|
+
.sm-structure__row.sm-drag--lifted{background:transparent;box-shadow:none}
|
|
899
|
+
.sm-structure__row.sm-drag--lifted .sm-structure__card{box-shadow:var(--shadow-pop)}
|
|
900
|
+
.sm-structure__row.sm-drag--lifted .sm-structure__slot{visibility:hidden}
|
|
901
|
+
.sm-structure__empty{
|
|
902
|
+
display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:var(--outline-row-h);padding:4px 12px;
|
|
903
|
+
border:1px dashed var(--line-dashed);border-radius:var(--outline-row-radius);color:var(--text-muted);font-weight:500;
|
|
904
|
+
}
|
|
837
905
|
|
|
838
906
|
.sm-picker{
|
|
839
907
|
width:300px;background:var(--menu-bg);border:var(--menu-border);border-radius:var(--radius-md);
|
|
@@ -847,6 +915,8 @@ body:has(.sm-admin){margin:0}
|
|
|
847
915
|
/* after the base item rule: same specificity, so source order decides (ActionMenu states) */
|
|
848
916
|
.sm-picker__item:disabled{opacity:.4;cursor:default}
|
|
849
917
|
.sm-picker__item:disabled:hover{background:transparent}
|
|
918
|
+
/* The clipboard's entry leads the picker (spec 2026-10-19 editor at launch §3), set off from the sections under it. */
|
|
919
|
+
.sm-picker__item--paste{border-bottom:1px solid var(--line);margin-bottom:4px;border-radius:var(--radius-sm) var(--radius-sm) 0 0}
|
|
850
920
|
.sm-picker__item--danger{color:var(--danger)}
|
|
851
921
|
.sm-picker__icon{
|
|
852
922
|
width:26px;height:26px;flex:none;border-radius:var(--radius-sm);background:var(--accent-soft);
|
|
@@ -172,7 +172,9 @@ export function useSortable(options) {
|
|
|
172
172
|
window.addEventListener("keydown", key);
|
|
173
173
|
};
|
|
174
174
|
const onKeyDown = (index) => (e) => {
|
|
175
|
-
|
|
175
|
+
// The row's own Enter/Space handler must not see the grip's keys; every other key (the editor's Cmd+Z, review 2026-10-20) bubbles on.
|
|
176
|
+
if (e.key === "Enter" || e.key === " " || e.key === "ArrowUp" || e.key === "ArrowDown")
|
|
177
|
+
e.stopPropagation();
|
|
176
178
|
if (e.key !== "ArrowUp" && e.key !== "ArrowDown")
|
|
177
179
|
return;
|
|
178
180
|
e.preventDefault();
|
package/dist/api-client.js
CHANGED
|
@@ -65,7 +65,7 @@ export function createSmoodlyApi(options) {
|
|
|
65
65
|
return (await call("/target-paths", { kind: target.kind, id: target.id })).data;
|
|
66
66
|
},
|
|
67
67
|
async load(target, locale, path, o) {
|
|
68
|
-
const draft = !!o?.draft || o?.version
|
|
68
|
+
const draft = !!o?.draft || !!o?.version;
|
|
69
69
|
const r = await call("/load", { kind: target.kind, id: target.id, locale, path, draft: o?.draft ? "1" : undefined, version: o?.version });
|
|
70
70
|
return withTags(r.data, r.tags, draft);
|
|
71
71
|
},
|
package/dist/assemble.js
CHANGED
|
@@ -49,7 +49,7 @@ export function checkEntryQuery(schema, collection, o) {
|
|
|
49
49
|
if (OBJECT_FIELD_TYPES.has(schema.fields[key].type ?? "")) {
|
|
50
50
|
throw new Error(`smoodly: getEntries("${collection}"): "${key}" holds an object — filter on a scalar field instead.`);
|
|
51
51
|
}
|
|
52
|
-
// Any-of reads the text form of the whole field (
|
|
52
|
+
// Any-of reads the text form of the whole field (an array as its JSON text), which for
|
|
53
53
|
// an array is its JSON — `["a"]` never matches "a" — so refuse rather than match nothing.
|
|
54
54
|
if (Array.isArray(o.where[key]) && ARRAY_FIELD_TYPES.has(schema.fields[key].type ?? "")) {
|
|
55
55
|
throw new Error(`smoodly: getEntries("${collection}"): "${key}" is array-valued — any-of is for scalar fields; pass one value to match entries containing it.`);
|
package/dist/astro/cache.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type Settled = {
|
|
|
17
17
|
draft: boolean;
|
|
18
18
|
/** The request is under the preview address, cookie or not. */
|
|
19
19
|
preview: boolean;
|
|
20
|
-
/** The shell rendered a page read from the
|
|
20
|
+
/** The shell rendered a page read from the service and gave it a lifetime. */
|
|
21
21
|
cacheable: boolean;
|
|
22
22
|
/** When the request began, in milliseconds. */
|
|
23
23
|
started: number;
|
package/dist/astro/index.js
CHANGED
|
@@ -79,11 +79,16 @@ export default function smoodly(options = {}) {
|
|
|
79
79
|
let staticMedia = new Map();
|
|
80
80
|
/** astro.config's `i18n`, which the build holds the config's `routing` against. */
|
|
81
81
|
let i18n;
|
|
82
|
-
/** The project's
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
/** The project's config at build, from its config file (the one `options.config` names), loaded once and shared by
|
|
83
|
+
* every hook; and its API client, from the env the setup hook loaded, made once. Two steps, since the routing
|
|
84
|
+
* check wants the config before any env is needed. */
|
|
85
|
+
let builtConfig = null;
|
|
86
|
+
const buildConfig = () => (builtConfig ?? (builtConfig = options.config ? loadConfigFile(join(root, options.config), root) : loadConfig(root)));
|
|
87
|
+
let builtApi = null;
|
|
88
|
+
const buildApi = () => (builtApi ?? (builtApi = (async () => {
|
|
89
|
+
const loaded = await buildConfig();
|
|
85
90
|
return { loaded, api: createSmoodlyApi({ ...apiEnv(process.env), config: loaded }) };
|
|
86
|
-
};
|
|
91
|
+
})()));
|
|
87
92
|
/** A build read that fails names the service's address and stops the build before any page renders (a static build
|
|
88
93
|
* with the Worker down would otherwise fail page by page). */
|
|
89
94
|
const fromApi = async (read) => {
|
|
@@ -115,7 +120,7 @@ export default function smoodly(options = {}) {
|
|
|
115
120
|
injectRoute(route);
|
|
116
121
|
addMiddleware({ entrypoint: here(`./middleware${ext}`), order: "pre" });
|
|
117
122
|
if (output === "server" && !config.cache?.provider) {
|
|
118
|
-
logger.warn("smoodly: no cache provider in astro.config — every page renders from the
|
|
123
|
+
logger.warn("smoodly: no cache provider in astro.config — every page renders from the service. Add `cache: { provider: memoryCache() }` (from \"astro/config\").");
|
|
119
124
|
}
|
|
120
125
|
// astro.config is evaluated before Astro loads .env, so `site: process.env.SITE_URL` there sees only the
|
|
121
126
|
// shell's value; the env files were read above, and SITE_URL from them sets `site` here. A value
|
|
@@ -149,7 +154,7 @@ export default function smoodly(options = {}) {
|
|
|
149
154
|
// The schema push (spec 2026-10-16 the control plane §4) comes first, in either output: the space then holds the
|
|
150
155
|
// registry this build renders with. A failed push never fails the build; the site runs against the schema the space has.
|
|
151
156
|
"astro:build:start": async ({ logger }) => {
|
|
152
|
-
const loaded =
|
|
157
|
+
const loaded = await buildConfig();
|
|
153
158
|
// The runtime's startup check, at build in either output: a deploy that builds must not refuse every request.
|
|
154
159
|
astroRouting(i18n, loaded.locales, loaded.routing);
|
|
155
160
|
try {
|
package/dist/astro/runtime.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type { PageHead } from "./head.ts";
|
|
|
19
19
|
export type { ViewFiles } from "./views-check.ts";
|
|
20
20
|
import { type Shell, type SmoodlyLocals } from "./shell.ts";
|
|
21
21
|
/** The shell's decision with the project's bound site: what routes/page.astro calls, with `Astro.cache`.
|
|
22
|
-
* A page read from the
|
|
22
|
+
* A page read from the service gets its lifetime here, before its view renders. The first call compares
|
|
23
23
|
* the project's view files to the registry and names every disagreement once. */
|
|
24
24
|
export declare function shell(pathname: string, search: string, state: SmoodlyLocals, views: ViewFiles, cache?: unknown): Promise<Shell>;
|
|
25
25
|
/** getStaticPaths for the static page shell (spec 2026-10-09 static mode §3): every published address of the bound site. */
|
package/dist/astro/runtime.js
CHANGED
|
@@ -25,7 +25,7 @@ import { missingViews } from "./views-check.js";
|
|
|
25
25
|
import { resolveShell } from "./shell.js";
|
|
26
26
|
let viewsChecked = false;
|
|
27
27
|
/** The shell's decision with the project's bound site: what routes/page.astro calls, with `Astro.cache`.
|
|
28
|
-
* A page read from the
|
|
28
|
+
* A page read from the service gets its lifetime here, before its view renders. The first call compares
|
|
29
29
|
* the project's view files to the registry and names every disagreement once. */
|
|
30
30
|
export async function shell(pathname, search, state, views, cache) {
|
|
31
31
|
if (!viewsChecked) {
|
package/dist/astro/shell.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type SmoodlyLocals = {
|
|
|
14
14
|
editor: boolean;
|
|
15
15
|
/** History's stored version, inside a preview only. */
|
|
16
16
|
version?: string;
|
|
17
|
-
/** Set by the shell when it rendered a page read from the
|
|
17
|
+
/** Set by the shell when it rendered a page read from the service: the response may be cached under its tags. */
|
|
18
18
|
cacheable: boolean;
|
|
19
19
|
/** Set by the shell when it rendered a page at all: in a preview, only that response is a canvas. */
|
|
20
20
|
shell: boolean;
|
package/dist/astro/shell.js
CHANGED
|
@@ -52,7 +52,7 @@ export async function resolveShell(deps, pathname, search, state, views) {
|
|
|
52
52
|
console.warn(`smoodly: no src/page-types/${hit.template}.astro — the record at ${hit.path} is a 404.`);
|
|
53
53
|
return NOT_FOUND;
|
|
54
54
|
}
|
|
55
|
-
// Only a page read from the
|
|
55
|
+
// Only a page read from the service has a tag to expire it by.
|
|
56
56
|
state.cacheable = !hit.record.id.startsWith("code:") && !state.draft;
|
|
57
57
|
state.shell = true;
|
|
58
58
|
const zoneNames = Object.keys(schema.zones ?? {});
|
|
@@ -22,6 +22,7 @@ export declare class SmoodlyContentError extends Error {
|
|
|
22
22
|
/** What content() writes through: the operations, the paths index's lookup, an upload. */
|
|
23
23
|
export type ContentTransport = {
|
|
24
24
|
op(call: AdminOpCall): Promise<OpResult<unknown>>;
|
|
25
|
+
/** The target at an address, as a draft read: what the script is writing, never an edge copy. */
|
|
25
26
|
locate(locale: string, path: string): Promise<PathHit | null>;
|
|
26
27
|
upload(bytes: Uint8Array, filename: string, mime: string | undefined, meta: UploadMeta): Promise<ImageValue>;
|
|
27
28
|
};
|
|
@@ -45,7 +45,9 @@ export function apiTransport(options) {
|
|
|
45
45
|
const json = (body) => ({ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
46
46
|
return {
|
|
47
47
|
op: (call) => send("/op", json({ path: call.path, args: call.args })),
|
|
48
|
-
|
|
48
|
+
// A draft read (DESIGN §1 Content API › Writes): the caller holds the secret key and asks about what it is writing,
|
|
49
|
+
// and a draft read is never stored at the edge, so a script sees its own create at once.
|
|
50
|
+
locate: (locale, path) => must(send(`/locate?${new URLSearchParams({ locale, path, draft: "1" })}`)),
|
|
49
51
|
// The bytes as the body with their length, the rest in the query (spec 2026-10-18 media on R2 and Images §4).
|
|
50
52
|
async upload(bytes, filename, mime, meta) {
|
|
51
53
|
const type = mime ?? mimeOf(filename);
|
package/dist/css.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The custom-CSS box (spec 2026-09-14 §4): a per-section snippet the
|
|
2
2
|
// wrapper nests under the section's own [data-smd-node] selector. CSS
|
|
3
3
|
// can do two things beyond styling — pull a remote stylesheet and beacon
|
|
4
|
-
// to a host — so those, the two
|
|
4
|
+
// to a host — so those, the two old code hooks (`expression()`, `behavior:`) and the tag-closing
|
|
5
5
|
// sequence are refused at save and stripped at render, in that order.
|
|
6
6
|
// The checks run on the snippet with its CSS escapes decoded, so
|
|
7
7
|
// `\75 rl(` cannot spell `url(` past them; and the braces must balance,
|
package/dist/entry-store.d.ts
CHANGED
|
@@ -58,8 +58,8 @@ export type Where = Record<string, WhereValue | WhereValue[]>;
|
|
|
58
58
|
* adapters read the text form of the whole array, never its elements). */
|
|
59
59
|
export declare const ARRAY_FIELD_TYPES: ReadonlySet<string>;
|
|
60
60
|
/** The memory adapter's and the tests' reading of `where`: a scalar matches an
|
|
61
|
-
* equal value or an array holding it (
|
|
62
|
-
* compared as text
|
|
61
|
+
* equal value or an array holding it (containment); an array is any-of,
|
|
62
|
+
* compared as text (textForm). */
|
|
63
63
|
export declare function matchesWhere(fields: Record<string, unknown>, where: Where): boolean;
|
|
64
64
|
export type ListOptions = {
|
|
65
65
|
/** Equality / any-of on shared fields, executed by the store (spec 2026-09-11 §3). */
|
|
@@ -256,8 +256,8 @@ export declare function entryTitleIn(record: EntryRecord, locale: string, schema
|
|
|
256
256
|
* directions (the SQLite list compiler sorts nulls last to match);
|
|
257
257
|
* the creation tiebreak follows `direction` for non-manual orders.
|
|
258
258
|
* Declared fields compare numerically when both values are numbers,
|
|
259
|
-
* otherwise as strings via `localeCompare` — the
|
|
260
|
-
*
|
|
261
|
-
* follow-ups for the mixed-type
|
|
259
|
+
* otherwise as strings via `localeCompare` — the order the SQLite store's
|
|
260
|
+
* `COLLATE NOCASE` gives for one type (ASCII only; see MAINTAINERS.md's
|
|
261
|
+
* follow-ups for the mixed-type, JSON-null and non-ASCII edges).
|
|
262
262
|
*/
|
|
263
263
|
export declare function compareEntries(order: EntryOrder, seq: (e: EntryRecord) => number): (a: EntryRecord, b: EntryRecord) => number;
|
package/dist/entry-store.js
CHANGED
|
@@ -15,12 +15,12 @@ import { slugOf, unsupportedLocale } from "./paths.js";
|
|
|
15
15
|
* containment, and any-of on one is refused by the site layer (both
|
|
16
16
|
* adapters read the text form of the whole array, never its elements). */
|
|
17
17
|
export const ARRAY_FIELD_TYPES = new Set(["checkboxes", "refList", "list", "tags"]);
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
18
|
+
/** A field's text form: a scalar's text, an array's or object's JSON text (no scalar
|
|
19
|
+
* equals either form), the same in every store. */
|
|
20
20
|
const textForm = (value) => (value !== null && typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
21
21
|
/** The memory adapter's and the tests' reading of `where`: a scalar matches an
|
|
22
|
-
* equal value or an array holding it (
|
|
23
|
-
* compared as text
|
|
22
|
+
* equal value or an array holding it (containment); an array is any-of,
|
|
23
|
+
* compared as text (textForm). */
|
|
24
24
|
export function matchesWhere(fields, where) {
|
|
25
25
|
return Object.entries(where).every(([key, want]) => {
|
|
26
26
|
const have = fields[key];
|
|
@@ -134,9 +134,9 @@ export function entryTitleIn(record, locale, schema) {
|
|
|
134
134
|
* directions (the SQLite list compiler sorts nulls last to match);
|
|
135
135
|
* the creation tiebreak follows `direction` for non-manual orders.
|
|
136
136
|
* Declared fields compare numerically when both values are numbers,
|
|
137
|
-
* otherwise as strings via `localeCompare` — the
|
|
138
|
-
*
|
|
139
|
-
* follow-ups for the mixed-type
|
|
137
|
+
* otherwise as strings via `localeCompare` — the order the SQLite store's
|
|
138
|
+
* `COLLATE NOCASE` gives for one type (ASCII only; see MAINTAINERS.md's
|
|
139
|
+
* follow-ups for the mixed-type, JSON-null and non-ASCII edges).
|
|
140
140
|
*/
|
|
141
141
|
export function compareEntries(order, seq) {
|
|
142
142
|
return (a, b) => {
|