@elytracms/studio 0.0.62 → 0.0.63
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/components/account/credential-panel.js +6 -1
- package/dist/components/account/credential-panel.js.map +1 -1
- package/dist/components/auth-context.d.ts +14 -0
- package/dist/components/auth-context.js +32 -2
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +7 -0
- package/dist/components/builder/blocks-model.js +5 -0
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.js +2 -2
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/fixture-thumbnail.d.ts +15 -0
- package/dist/components/builder/fixture-thumbnail.js +14 -0
- package/dist/components/builder/fixture-thumbnail.js.map +1 -1
- package/dist/components/cms/cms-index.js +6 -3
- package/dist/components/cms/cms-index.js.map +1 -1
- package/dist/components/deployment-setup-gate.js +40 -8
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/operation-error.d.ts +7 -5
- package/dist/components/operation-error.js +19 -36
- package/dist/components/operation-error.js.map +1 -1
- package/dist/components/project-list.js +17 -1
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-switcher.d.ts +10 -0
- package/dist/components/project-switcher.js +13 -3
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/session-expired-overlay.d.ts +27 -0
- package/dist/components/session-expired-overlay.js +51 -0
- package/dist/components/session-expired-overlay.js.map +1 -0
- package/dist/components/sign-in-view.d.ts +13 -1
- package/dist/components/sign-in-view.js +27 -13
- package/dist/components/sign-in-view.js.map +1 -1
- package/dist/components/studio-project-screen.js +2 -2
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +40 -3
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/customization.d.ts +4 -1
- package/dist/customization.js +3 -0
- package/dist/customization.js.map +1 -1
- package/dist/lib/auth-errors.d.ts +58 -28
- package/dist/lib/auth-errors.js +93 -15
- package/dist/lib/auth-errors.js.map +1 -1
- package/dist/lib/i18n/index.d.ts +1 -0
- package/dist/lib/i18n/index.js.map +1 -1
- package/dist/lib/i18n/messages.de.d.ts +24 -0
- package/dist/lib/i18n/messages.de.js +1019 -0
- package/dist/lib/i18n/messages.de.js.map +1 -0
- package/dist/lib/i18n/messages.en.d.ts +880 -0
- package/dist/lib/i18n/messages.en.js +71 -3
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +7 -0
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.d.ts +8 -1
- package/dist/lib/persistence/deployment-readiness.js +16 -1
- package/dist/lib/persistence/deployment-readiness.js.map +1 -1
- package/dist/lib/session-expiry.d.ts +67 -0
- package/dist/lib/session-expiry.js +72 -0
- package/dist/lib/session-expiry.js.map +1 -0
- package/dist/lib/tab-title.d.ts +53 -0
- package/dist/lib/tab-title.js +77 -0
- package/dist/lib/tab-title.js.map +1 -0
- package/dist/lib/workspace/index.d.ts +1 -1
- package/dist/lib/workspace/index.js +1 -1
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.d.ts +27 -0
- package/dist/lib/workspace/switcher-source.js +30 -0
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/dist/router/index.d.ts +6 -0
- package/dist/router/index.js +6 -0
- package/dist/router/index.js.map +1 -1
- package/dist/router/launcher-visit.d.ts +13 -0
- package/dist/router/launcher-visit.js +34 -0
- package/dist/router/launcher-visit.js.map +1 -0
- package/dist/router/root.js +12 -2
- package/dist/router/root.js.map +1 -1
- package/dist/ui/dialog.d.ts +15 -0
- package/dist/ui/dialog.js +17 -0
- package/dist/ui/dialog.js.map +1 -1
- package/package.json +6 -2
|
@@ -3,6 +3,7 @@ import { useState } from 'react';
|
|
|
3
3
|
import { KeyRound, ShieldCheck } from 'lucide-react';
|
|
4
4
|
import { Button, Card, CardContent, Field, Heading, Input, Row, Stack, Text, toast, } from '@elytracms/studio/ui';
|
|
5
5
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
6
|
+
import { changePasswordErrorMessageKey } from '@elytracms/studio/lib/auth-errors';
|
|
6
7
|
import { useAuth } from '../auth-context.js';
|
|
7
8
|
/**
|
|
8
9
|
* The credential seam (EC-240 H-tail item 3) — part of the account **property
|
|
@@ -42,7 +43,11 @@ export function CredentialPanel() {
|
|
|
42
43
|
toast.success(translate('account.password.changed'));
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
+
// EC-412: engine-classified failures (the EC-377 reasons) localize through
|
|
47
|
+
// the message catalog; an unrecognized shape renders the adapter's raw
|
|
48
|
+
// English message — honest, never a stale mapping (the EC-397 pattern).
|
|
49
|
+
const errorKey = changePasswordErrorMessageKey(result);
|
|
50
|
+
setError(errorKey ? translate(errorKey) : result.message);
|
|
46
51
|
}
|
|
47
52
|
const submitting = phase === 'submitting';
|
|
48
53
|
return (_jsx(Card, { "data-testid": "credential-panel", children: _jsx(CardContent, { className: "pt-5", children: _jsxs(Stack, { gap: "4", children: [_jsxs(Stack, { gap: "1", children: [_jsxs(Row, { gap: "2", align: "center", children: [_jsx(ShieldCheck, { className: "size-4 text-muted-foreground" }), _jsx(Heading, { level: 3, size: "h4", children: translate('account.security.title') })] }), _jsx(Text, { size: "sm", tone: "muted", children: translate('account.security.method') })] }), _jsx("form", { onSubmit: handleSubmit, noValidate: true, children: _jsxs(Stack, { gap: "3", children: [_jsx(Field, { label: translate('account.password.current'), htmlFor: "account-current-password", required: true, children: _jsx(Input, { id: "account-current-password", type: "password", autoComplete: "current-password", value: current, onChange: (e) => setCurrent(e.target.value), disabled: submitting }) }), _jsx(Field, { label: translate('account.password.new'), htmlFor: "account-new-password", required: true, children: _jsx(Input, { id: "account-new-password", type: "password", autoComplete: "new-password", value: next, onChange: (e) => setNext(e.target.value), disabled: submitting }) }), _jsx(Field, { label: translate('account.password.confirm'), htmlFor: "account-confirm-password", required: true, error: error, children: _jsx(Input, { id: "account-confirm-password", type: "password", autoComplete: "new-password", value: confirm, onChange: (e) => setConfirm(e.target.value), disabled: submitting }) }), _jsx(Row, { children: _jsxs(Button, { type: "submit", variant: "outline", disabled: submitting || !current || !next || !confirm, children: [_jsx(KeyRound, {}), submitting
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credential-panel.js","sourceRoot":"","sources":["../../../src/components/account/credential-panel.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,EACL,MAAM,EACN,IAAI,EACJ,WAAW,EACX,KAAK,EACL,OAAO,EACP,KAAK,EACL,GAAG,EACH,KAAK,EACL,IAAI,EACJ,KAAK,GACN,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAEzC;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,IAAI,GAAG,OAAO,EAAE,CAAA;IACtB,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAC1C,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IACpC,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAC1C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAwB,MAAM,CAAC,CAAA;IACjE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IAEvD,KAAK,UAAU,YAAY,CAAC,KAAiC;QAC3D,KAAK,CAAC,cAAc,EAAE,CAAA;QACtB,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YACrB,QAAQ,CAAC,SAAS,CAAC,2BAA2B,CAAC,CAAC,CAAA;YAChD,OAAM;QACR,CAAC;QACD,QAAQ,CAAC,YAAY,CAAC,CAAA;QACtB,2EAA2E;QAC3E,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,EAAE,eAAe,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAA;QACzF,QAAQ,CAAC,MAAM,CAAC,CAAA;QAChB,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;YACd,UAAU,CAAC,EAAE,CAAC,CAAA;YACd,OAAO,CAAC,EAAE,CAAC,CAAA;YACX,UAAU,CAAC,EAAE,CAAC,CAAA;YACd,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,0BAA0B,CAAC,CAAC,CAAA;YACpD,OAAM;QACR,CAAC;QACD,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"credential-panel.js","sourceRoot":"","sources":["../../../src/components/account/credential-panel.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport type { FormEvent } from 'react'\nimport { KeyRound, ShieldCheck } from 'lucide-react'\nimport {\n Button,\n Card,\n CardContent,\n Field,\n Heading,\n Input,\n Row,\n Stack,\n Text,\n toast,\n} from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { changePasswordErrorMessageKey } from '@elytracms/studio/lib/auth-errors'\nimport { useAuth } from '../auth-context'\n\n/**\n * The credential seam (EC-240 H-tail item 3) — part of the account **property\n * surface**, never a modal. Two blocks:\n *\n * - **Sign-in method** (read-only): today every studio account is email + password.\n * OAuth \"connected accounts\" is a deferred extension.\n * - **Change password**: current + new + confirm, handed straight to\n * `AuthAdapter.changePassword` (provider-delegated). SEAM ONLY — the raw values\n * are never logged, stored, or inspected here; they leave in the adapter call.\n *\n * The current-password field is the gate around this sensitive action (no separate\n * confirm dialog needed).\n */\nexport function CredentialPanel() {\n const auth = useAuth()\n const [current, setCurrent] = useState('')\n const [next, setNext] = useState('')\n const [confirm, setConfirm] = useState('')\n const [phase, setPhase] = useState<'idle' | 'submitting'>('idle')\n const [error, setError] = useState<string | null>(null)\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault()\n setError(null)\n if (next !== confirm) {\n setError(translate('account.password.mismatch'))\n return\n }\n setPhase('submitting')\n // The raw values leave here ONLY into the provider adapter — never logged.\n const result = await auth.changePassword({ currentPassword: current, newPassword: next })\n setPhase('idle')\n if (result.ok) {\n setCurrent('')\n setNext('')\n setConfirm('')\n toast.success(translate('account.password.changed'))\n return\n }\n // EC-412: engine-classified failures (the EC-377 reasons) localize through\n // the message catalog; an unrecognized shape renders the adapter's raw\n // English message — honest, never a stale mapping (the EC-397 pattern).\n const errorKey = changePasswordErrorMessageKey(result)\n setError(errorKey ? translate(errorKey) : result.message)\n }\n\n const submitting = phase === 'submitting'\n\n return (\n <Card data-testid=\"credential-panel\">\n <CardContent className=\"pt-5\">\n <Stack gap=\"4\">\n <Stack gap=\"1\">\n <Row gap=\"2\" align=\"center\">\n <ShieldCheck className=\"size-4 text-muted-foreground\" />\n <Heading level={3} size=\"h4\">\n {translate('account.security.title')}\n </Heading>\n </Row>\n <Text size=\"sm\" tone=\"muted\">\n {translate('account.security.method')}\n </Text>\n </Stack>\n\n <form onSubmit={handleSubmit} noValidate>\n <Stack gap=\"3\">\n <Field\n label={translate('account.password.current')}\n htmlFor=\"account-current-password\"\n required\n >\n <Input\n id=\"account-current-password\"\n type=\"password\"\n autoComplete=\"current-password\"\n value={current}\n onChange={(e) => setCurrent(e.target.value)}\n disabled={submitting}\n />\n </Field>\n <Field\n label={translate('account.password.new')}\n htmlFor=\"account-new-password\"\n required\n >\n <Input\n id=\"account-new-password\"\n type=\"password\"\n autoComplete=\"new-password\"\n value={next}\n onChange={(e) => setNext(e.target.value)}\n disabled={submitting}\n />\n </Field>\n <Field\n label={translate('account.password.confirm')}\n htmlFor=\"account-confirm-password\"\n required\n error={error}\n >\n <Input\n id=\"account-confirm-password\"\n type=\"password\"\n autoComplete=\"new-password\"\n value={confirm}\n onChange={(e) => setConfirm(e.target.value)}\n disabled={submitting}\n />\n </Field>\n <Row>\n <Button\n type=\"submit\"\n variant=\"outline\"\n disabled={submitting || !current || !next || !confirm}\n >\n <KeyRound />\n {submitting\n ? translate('account.password.changing')\n : translate('account.password.change')}\n </Button>\n </Row>\n </Stack>\n </form>\n </Stack>\n </CardContent>\n </Card>\n )\n}\n"]}
|
|
@@ -7,10 +7,24 @@ import type { AuthAdapter, AuthSession, AuthStatus, ChangePasswordInput, ChangeP
|
|
|
7
7
|
* (`setStudioSession`) so every studio mutation runs through the role hook as
|
|
8
8
|
* the signed-in user. Auth state machine: `loading` (restoring) →
|
|
9
9
|
* `signed-in` | `signed-out` — all three are deterministic, renderable states.
|
|
10
|
+
*
|
|
11
|
+
* EC-403 adds the orthogonal `sessionExpired` flag: when the backend session
|
|
12
|
+
* dies mid-flight (any operation fails the session-expired predicate,
|
|
13
|
+
* reported through `lib/session-expiry.ts`), `status` deliberately STAYS
|
|
14
|
+
* `'signed-in'` — flipping it would let the route guard navigate to the
|
|
15
|
+
* sign-in screen and destroy the unsaved editor state. Instead the flag
|
|
16
|
+
* mounts the blocking in-place re-auth overlay; a successful sign-in applies
|
|
17
|
+
* the fresh session and clears it, and the tree never unmounted.
|
|
10
18
|
*/
|
|
11
19
|
export interface AuthStore {
|
|
12
20
|
status: AuthStatus;
|
|
13
21
|
session: AuthSession | null;
|
|
22
|
+
/**
|
|
23
|
+
* The backend session expired mid-flight (EC-403). While true, the shell
|
|
24
|
+
* blocks all editing behind the in-place re-auth overlay; `status` remains
|
|
25
|
+
* `'signed-in'` so no route guard navigation tears down unsaved state.
|
|
26
|
+
*/
|
|
27
|
+
sessionExpired: boolean;
|
|
14
28
|
signIn(email: string, password: string): Promise<SignInResult>;
|
|
15
29
|
/** Register the first/new user on the active backend, then sign in (EC-179). */
|
|
16
30
|
signUp(email: string, password: string): Promise<SignInResult>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { selectAuthAdapter, sessionPrincipal } from '@elytracms/studio/lib/auth';
|
|
4
|
+
import { onSessionExpiry, shouldBlockOnSessionExpiry } from '@elytracms/studio/lib/session-expiry';
|
|
4
5
|
import { setStudioSession } from '@elytracms/studio/lib/operations';
|
|
5
6
|
import { useOperationQuery } from '@elytracms/studio/lib/query';
|
|
6
7
|
import { createRequiredContext } from '@elytracms/studio/lib/required-context';
|
|
@@ -11,10 +12,20 @@ export function AuthProvider({ children, adapter }) {
|
|
|
11
12
|
const [auth] = useState(() => adapter ?? selectAuthAdapter());
|
|
12
13
|
const [status, setStatus] = useState('loading');
|
|
13
14
|
const [session, setSession] = useState(null);
|
|
15
|
+
const [sessionExpired, setSessionExpired] = useState(false);
|
|
16
|
+
// Latest values for the once-per-adapter expiry subscription below.
|
|
17
|
+
const statusRef = useRef(status);
|
|
18
|
+
statusRef.current = status;
|
|
19
|
+
const expiredRef = useRef(sessionExpired);
|
|
20
|
+
expiredRef.current = sessionExpired;
|
|
14
21
|
const apply = useCallback((next) => {
|
|
15
22
|
setStudioSession(next ? sessionPrincipal(next) : null);
|
|
16
23
|
setSession(next);
|
|
17
24
|
setStatus(next ? 'signed-in' : 'signed-out');
|
|
25
|
+
// Any applied session state resolves the expiry block (EC-403): a fresh
|
|
26
|
+
// sign-in replaces the dead session in place; an explicit sign-out hands
|
|
27
|
+
// over to the route guard.
|
|
28
|
+
setSessionExpired(false);
|
|
18
29
|
}, []);
|
|
19
30
|
// Restore the session once on the client (SSR renders the 'loading' state).
|
|
20
31
|
useEffect(() => {
|
|
@@ -27,9 +38,28 @@ export function AuthProvider({ children, adapter }) {
|
|
|
27
38
|
cancelled = true;
|
|
28
39
|
};
|
|
29
40
|
}, [auth, apply]);
|
|
41
|
+
// EC-403: raise the blocking re-auth state when the operations layer reports
|
|
42
|
+
// session expiry. `status` stays 'signed-in' (see AuthStore docs); the
|
|
43
|
+
// adapter-level signOut resets the DEAD backend session — the shared Convex
|
|
44
|
+
// client would otherwise send the expired JWT with the overlay's sign-in
|
|
45
|
+
// action itself and be rejected at the transport. Local React state is
|
|
46
|
+
// untouched by that reset, so the tree and unsaved edits live on.
|
|
47
|
+
useEffect(() => onSessionExpiry(() => {
|
|
48
|
+
if (!shouldBlockOnSessionExpiry({
|
|
49
|
+
status: statusRef.current,
|
|
50
|
+
alreadyBlocked: expiredRef.current,
|
|
51
|
+
})) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
setSessionExpired(true);
|
|
55
|
+
auth.signOut().catch(() => {
|
|
56
|
+
// Resetting a dead session is best-effort; the overlay still blocks.
|
|
57
|
+
});
|
|
58
|
+
}), [auth]);
|
|
30
59
|
const store = useMemo(() => ({
|
|
31
60
|
status,
|
|
32
61
|
session,
|
|
62
|
+
sessionExpired,
|
|
33
63
|
signIn: async (email, password) => {
|
|
34
64
|
const result = await auth.signIn({ email, password });
|
|
35
65
|
if (result.ok)
|
|
@@ -55,7 +85,7 @@ export function AuthProvider({ children, adapter }) {
|
|
|
55
85
|
return result;
|
|
56
86
|
},
|
|
57
87
|
changePassword: (input) => auth.changePassword(input),
|
|
58
|
-
}), [auth, status, session, apply]);
|
|
88
|
+
}), [auth, status, session, sessionExpired, apply]);
|
|
59
89
|
return _jsx(AuthContext.Provider, { value: store, children: children });
|
|
60
90
|
}
|
|
61
91
|
export { useAuth };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-context.js","sourceRoot":"","sources":["../../src/components/auth-context.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useContext, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { ProjectRole } from '@elytracms/core/persistence'\nimport { selectAuthAdapter, sessionPrincipal } from '@elytracms/studio/lib/auth'\nimport type {\n AuthAdapter,\n AuthSession,\n AuthStatus,\n ChangePasswordInput,\n ChangePasswordResult,\n SignInResult,\n UpdateProfileInput,\n UpdateProfileResult,\n} from '@elytracms/studio/lib/auth'\nimport { setStudioSession } from '@elytracms/studio/lib/operations'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\n\n/**\n * Session store (EC-150). Owns the one `AuthAdapter` the app talks to, keeps\n * the session in React state, and binds/unbinds the operations-layer principal\n * (`setStudioSession`) so every studio mutation runs through the role hook as\n * the signed-in user. Auth state machine: `loading` (restoring) →\n * `signed-in` | `signed-out` — all three are deterministic, renderable states.\n */\n\nexport interface AuthStore {\n status: AuthStatus\n session: AuthSession | null\n signIn(email: string, password: string): Promise<SignInResult>\n /** Register the first/new user on the active backend, then sign in (EC-179). */\n signUp(email: string, password: string): Promise<SignInResult>\n signOut(): Promise<void>\n /** Edit the signed-in user's OWN name/avatar (EC-240 H-tail); refreshes the session. */\n updateProfile(input: UpdateProfileInput): Promise<UpdateProfileResult>\n /** Change the signed-in user's OWN password (EC-240 H-tail); provider-delegated. */\n changePassword(input: ChangePasswordInput): Promise<ChangePasswordResult>\n}\n\nconst { Context: AuthContext, useRequired: useAuth } = createRequiredContext<AuthStore>('Auth', {\n article: 'an',\n})\n\nexport interface AuthProviderProps {\n children: ReactNode\n /** Test seam: inject an adapter; defaults to `selectAuthAdapter()`. */\n adapter?: AuthAdapter\n}\n\nexport function AuthProvider({ children, adapter }: AuthProviderProps) {\n const [auth] = useState<AuthAdapter>(() => adapter ?? selectAuthAdapter())\n const [status, setStatus] = useState<AuthStatus>('loading')\n const [session, setSession] = useState<AuthSession | null>(null)\n\n const apply = useCallback((next: AuthSession | null) => {\n setStudioSession(next ? sessionPrincipal(next) : null)\n setSession(next)\n setStatus(next ? 'signed-in' : 'signed-out')\n }, [])\n\n // Restore the session once on the client (SSR renders the 'loading' state).\n useEffect(() => {\n let cancelled = false\n void auth.currentSession().then((restored) => {\n if (!cancelled) apply(restored)\n })\n return () => {\n cancelled = true\n }\n }, [auth, apply])\n\n const store = useMemo<AuthStore>(\n () => ({\n status,\n session,\n signIn: async (email, password) => {\n const result = await auth.signIn({ email, password })\n if (result.ok) apply(result.session)\n return result\n },\n signUp: async (email, password) => {\n const result = await auth.signUp({ email, password })\n if (result.ok) apply(result.session)\n return result\n },\n signOut: async () => {\n await auth.signOut()\n apply(null)\n },\n updateProfile: async (input) => {\n const result = await auth.updateProfile(input)\n // A successful identity edit refreshes the session so the shell (and the\n // account surface) reflect the new name immediately.\n if (result.ok) apply(result.session)\n return result\n },\n changePassword: (input) => auth.changePassword(input),\n }),\n [auth, status, session, apply],\n )\n\n return <AuthContext.Provider value={store}>{children}</AuthContext.Provider>\n}\n\nexport { useAuth }\n\n/**\n * Like {@link useAuth}, but tolerant of provider-less renders. For shared\n * leaf surfaces (e.g. the operation-error banner's \"Sign in again\" action)\n * that are also rendered standalone in tests/styleguides: they degrade\n * gracefully instead of demanding the full provider tree.\n */\nexport function useOptionalAuth(): AuthStore | null {\n return useContext(AuthContext)\n}\n\n/**\n * The signed-in user's role on a project (for the shell's role badge).\n * Read through the one operations-backed query cache (EC-308/AD-17) via the\n * `members.find` query — no longer reaching past the spine into\n * `adapter.members` — and re-read when membership changes. `null` while loading\n * or when the user is not a member.\n */\nexport function useProjectRole(projectId: string | null): ProjectRole | null {\n const { session } = useAuth()\n const userId = session?.user.id ?? null\n const query = useOperationQuery<ProjectRole | null>({\n queryKey: ['projectRole', projectId ?? '∅', userId ?? '∅'],\n loader: async (operations) => {\n const result = await operations.members.find({\n projectId: projectId as string,\n userId: userId as string,\n })\n return result.ok ? (result.value?.role ?? null) : null\n },\n invalidateOn: ['members'],\n enabled: projectId !== null && userId !== null,\n })\n return query.data ?? null\n}\n"]}
|
|
1
|
+
{"version":3,"file":"auth-context.js","sourceRoot":"","sources":["../../src/components/auth-context.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { ProjectRole } from '@elytracms/core/persistence'\nimport { selectAuthAdapter, sessionPrincipal } from '@elytracms/studio/lib/auth'\nimport { onSessionExpiry, shouldBlockOnSessionExpiry } from '@elytracms/studio/lib/session-expiry'\nimport type {\n AuthAdapter,\n AuthSession,\n AuthStatus,\n ChangePasswordInput,\n ChangePasswordResult,\n SignInResult,\n UpdateProfileInput,\n UpdateProfileResult,\n} from '@elytracms/studio/lib/auth'\nimport { setStudioSession } from '@elytracms/studio/lib/operations'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\n\n/**\n * Session store (EC-150). Owns the one `AuthAdapter` the app talks to, keeps\n * the session in React state, and binds/unbinds the operations-layer principal\n * (`setStudioSession`) so every studio mutation runs through the role hook as\n * the signed-in user. Auth state machine: `loading` (restoring) →\n * `signed-in` | `signed-out` — all three are deterministic, renderable states.\n *\n * EC-403 adds the orthogonal `sessionExpired` flag: when the backend session\n * dies mid-flight (any operation fails the session-expired predicate,\n * reported through `lib/session-expiry.ts`), `status` deliberately STAYS\n * `'signed-in'` — flipping it would let the route guard navigate to the\n * sign-in screen and destroy the unsaved editor state. Instead the flag\n * mounts the blocking in-place re-auth overlay; a successful sign-in applies\n * the fresh session and clears it, and the tree never unmounted.\n */\n\nexport interface AuthStore {\n status: AuthStatus\n session: AuthSession | null\n /**\n * The backend session expired mid-flight (EC-403). While true, the shell\n * blocks all editing behind the in-place re-auth overlay; `status` remains\n * `'signed-in'` so no route guard navigation tears down unsaved state.\n */\n sessionExpired: boolean\n signIn(email: string, password: string): Promise<SignInResult>\n /** Register the first/new user on the active backend, then sign in (EC-179). */\n signUp(email: string, password: string): Promise<SignInResult>\n signOut(): Promise<void>\n /** Edit the signed-in user's OWN name/avatar (EC-240 H-tail); refreshes the session. */\n updateProfile(input: UpdateProfileInput): Promise<UpdateProfileResult>\n /** Change the signed-in user's OWN password (EC-240 H-tail); provider-delegated. */\n changePassword(input: ChangePasswordInput): Promise<ChangePasswordResult>\n}\n\nconst { Context: AuthContext, useRequired: useAuth } = createRequiredContext<AuthStore>('Auth', {\n article: 'an',\n})\n\nexport interface AuthProviderProps {\n children: ReactNode\n /** Test seam: inject an adapter; defaults to `selectAuthAdapter()`. */\n adapter?: AuthAdapter\n}\n\nexport function AuthProvider({ children, adapter }: AuthProviderProps) {\n const [auth] = useState<AuthAdapter>(() => adapter ?? selectAuthAdapter())\n const [status, setStatus] = useState<AuthStatus>('loading')\n const [session, setSession] = useState<AuthSession | null>(null)\n const [sessionExpired, setSessionExpired] = useState(false)\n\n // Latest values for the once-per-adapter expiry subscription below.\n const statusRef = useRef(status)\n statusRef.current = status\n const expiredRef = useRef(sessionExpired)\n expiredRef.current = sessionExpired\n\n const apply = useCallback((next: AuthSession | null) => {\n setStudioSession(next ? sessionPrincipal(next) : null)\n setSession(next)\n setStatus(next ? 'signed-in' : 'signed-out')\n // Any applied session state resolves the expiry block (EC-403): a fresh\n // sign-in replaces the dead session in place; an explicit sign-out hands\n // over to the route guard.\n setSessionExpired(false)\n }, [])\n\n // Restore the session once on the client (SSR renders the 'loading' state).\n useEffect(() => {\n let cancelled = false\n void auth.currentSession().then((restored) => {\n if (!cancelled) apply(restored)\n })\n return () => {\n cancelled = true\n }\n }, [auth, apply])\n\n // EC-403: raise the blocking re-auth state when the operations layer reports\n // session expiry. `status` stays 'signed-in' (see AuthStore docs); the\n // adapter-level signOut resets the DEAD backend session — the shared Convex\n // client would otherwise send the expired JWT with the overlay's sign-in\n // action itself and be rejected at the transport. Local React state is\n // untouched by that reset, so the tree and unsaved edits live on.\n useEffect(\n () =>\n onSessionExpiry(() => {\n if (\n !shouldBlockOnSessionExpiry({\n status: statusRef.current,\n alreadyBlocked: expiredRef.current,\n })\n ) {\n return\n }\n setSessionExpired(true)\n auth.signOut().catch(() => {\n // Resetting a dead session is best-effort; the overlay still blocks.\n })\n }),\n [auth],\n )\n\n const store = useMemo<AuthStore>(\n () => ({\n status,\n session,\n sessionExpired,\n signIn: async (email, password) => {\n const result = await auth.signIn({ email, password })\n if (result.ok) apply(result.session)\n return result\n },\n signUp: async (email, password) => {\n const result = await auth.signUp({ email, password })\n if (result.ok) apply(result.session)\n return result\n },\n signOut: async () => {\n await auth.signOut()\n apply(null)\n },\n updateProfile: async (input) => {\n const result = await auth.updateProfile(input)\n // A successful identity edit refreshes the session so the shell (and the\n // account surface) reflect the new name immediately.\n if (result.ok) apply(result.session)\n return result\n },\n changePassword: (input) => auth.changePassword(input),\n }),\n [auth, status, session, sessionExpired, apply],\n )\n\n return <AuthContext.Provider value={store}>{children}</AuthContext.Provider>\n}\n\nexport { useAuth }\n\n/**\n * Like {@link useAuth}, but tolerant of provider-less renders. For shared\n * leaf surfaces (e.g. the operation-error banner's \"Sign in again\" action)\n * that are also rendered standalone in tests/styleguides: they degrade\n * gracefully instead of demanding the full provider tree.\n */\nexport function useOptionalAuth(): AuthStore | null {\n return useContext(AuthContext)\n}\n\n/**\n * The signed-in user's role on a project (for the shell's role badge).\n * Read through the one operations-backed query cache (EC-308/AD-17) via the\n * `members.find` query — no longer reaching past the spine into\n * `adapter.members` — and re-read when membership changes. `null` while loading\n * or when the user is not a member.\n */\nexport function useProjectRole(projectId: string | null): ProjectRole | null {\n const { session } = useAuth()\n const userId = session?.user.id ?? null\n const query = useOperationQuery<ProjectRole | null>({\n queryKey: ['projectRole', projectId ?? '∅', userId ?? '∅'],\n loader: async (operations) => {\n const result = await operations.members.find({\n projectId: projectId as string,\n userId: userId as string,\n })\n return result.ok ? (result.value?.role ?? null) : null\n },\n invalidateOn: ['members'],\n enabled: projectId !== null && userId !== null,\n })\n return query.data ?? null\n}\n"]}
|
|
@@ -28,6 +28,13 @@ export interface BlockTile {
|
|
|
28
28
|
issues: string[];
|
|
29
29
|
/** Deterministic-id subtree rendered live in the tile thumbnail. */
|
|
30
30
|
previewNode: ComponentNode;
|
|
31
|
+
/**
|
|
32
|
+
* Curated preview image URL (EC-409, `manifest.previewImage`). When set, the
|
|
33
|
+
* tile renders this image instead of the live `previewNode` render — the
|
|
34
|
+
* escape hatch for components whose real render needs data/context (e.g.
|
|
35
|
+
* listings) and previews as "just text". Insertion is unaffected.
|
|
36
|
+
*/
|
|
37
|
+
previewImage?: string;
|
|
31
38
|
/** Build the subtree to insert (ids are reassigned by the builder store). */
|
|
32
39
|
build: () => ComponentNode;
|
|
33
40
|
}
|
|
@@ -114,6 +114,10 @@ export function tilesForManifest(studio, manifest) {
|
|
|
114
114
|
description: manifest.description,
|
|
115
115
|
issues: baseIssues,
|
|
116
116
|
previewNode,
|
|
117
|
+
// EC-409: the curated preview image (when declared) replaces the live
|
|
118
|
+
// render on EVERY tile of the manifest — it previews the component,
|
|
119
|
+
// not one fixture variation.
|
|
120
|
+
...(manifest.previewImage ? { previewImage: manifest.previewImage } : {}),
|
|
117
121
|
build: () => seededNodeFromManifest(manifest),
|
|
118
122
|
},
|
|
119
123
|
];
|
|
@@ -128,6 +132,7 @@ export function tilesForManifest(studio, manifest) {
|
|
|
128
132
|
description: manifest.description,
|
|
129
133
|
issues: [...baseIssues, ...issues],
|
|
130
134
|
previewNode: node,
|
|
135
|
+
...(manifest.previewImage ? { previewImage: manifest.previewImage } : {}),
|
|
131
136
|
build: () => fixtureSubtree(studio, manifest, fixture, 'tmp').node,
|
|
132
137
|
};
|
|
133
138
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blocks-model.js","sourceRoot":"","sources":["../../../src/components/builder/blocks-model.ts"],"names":[],"mappings":"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","sourcesContent":["import { cloneWithNewIds, isLayoutContainerId } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { ComponentFixture, ComponentManifest } from '@elytracms/core/component-registry'\nimport { isCanvasEligible, propValueValidator } from '@elytracms/core/component-registry'\nimport { componentStatus } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { nodeFromProps, previewSpecs } from '@elytracms/studio/lib/registry/preview'\nimport { LAYOUT_PRESETS, nodeFromManifest, seededNodeFromManifest } from './node-factory'\n\n/**\n * Blocks panel model (EC-164) — the pure, React-free derivation behind the\n * visual insert surface. Every registry manifest yields one tile per declared\n * fixture (or a single default-props tile), grouped by manifest `category`\n * and filtered by a free-text query over name / description / category — no\n * hand-maintained panel config. Broken fixtures degrade to tiles carrying\n * explicit `issues`, never to a missing entry or a crash.\n */\n\n/** One insertable tile in the blocks panel. */\nexport interface BlockTile {\n /** Stable identity (`<componentId>::<variant>` or `preset::<key>`). */\n key: string\n /** Component id of the node the tile inserts. */\n componentId: string\n title: string\n /** Variation label: the fixture name, `Default`, or a preset description. */\n variant: string\n description?: string\n /**\n * Validation problems detected while building the preview (unknown child\n * component, undeclared slot, invalid fixture prop, missing\n * implementation). A non-empty list marks the tile as degraded — it still\n * renders and still inserts.\n */\n issues: string[]\n /** Deterministic-id subtree rendered live in the tile thumbnail. */\n previewNode: ComponentNode\n /** Build the subtree to insert (ids are reassigned by the builder store). */\n build: () => ComponentNode\n}\n\n/** Tiles of one manifest `category`. */\nexport interface BlockGroup {\n category: string\n tiles: BlockTile[]\n}\n\nexport interface BlockFilter {\n /** Case-insensitive match over id, title, description, and category. */\n query?: string\n /** When set, only these component ids may be inserted (slot.allow). */\n slotAllow?: string[]\n}\n\n/**\n * Intersect two component-id allow-lists. `undefined` means \"no constraint from\n * this source\", so the result is the other list (copied); two present lists yield\n * their set intersection, preserving the first list's order. This layers a\n * composition field's top-level `allow` vocabulary over a slot's positional\n * `allow` at the canvas root (EC-188): a component must satisfy BOTH leashes to be\n * offered. An empty array stays empty (\"allow nothing\"); callers wanting the\n * validator's \"empty = allow any\" must coalesce `[]` to `undefined` first (which\n * `resolveInsertContext` does via its `length > 0` guard, mirroring\n * `validateCompositionValue`).\n */\nexport function intersectAllow(\n slotAllow: readonly string[] | undefined,\n fieldAllow: readonly string[] | undefined,\n): string[] | undefined {\n if (!slotAllow) return fieldAllow ? [...fieldAllow] : undefined\n if (!fieldAllow) return [...slotAllow]\n const inField = new Set(fieldAllow)\n return slotAllow.filter((id) => inField.has(id))\n}\n\nconst UNCATEGORIZED = 'other'\n\nfunction prefixedCounter(prefix: string): () => string {\n let n = 0\n return () => {\n n += 1\n return `${prefix}-${n}`\n }\n}\n\n/** A node built from a manifest's defaults with an explicit id. */\nfunction defaultNode(manifest: ComponentManifest, id: string): ComponentNode {\n return { ...nodeFromManifest(manifest), id }\n}\n\n/** Structured issues for a component that cannot render (EC-055 statuses). */\nfunction manifestIssues(studio: StudioRegistry, manifest: ComponentManifest): string[] {\n const status = componentStatus(studio, manifest)\n if (status === 'incompatible') {\n return [`No implementation is registered for \"${manifest.id}\".`]\n }\n if (status === 'invalid') {\n return studio.registry.issues\n .filter((issue) => issue.componentId === manifest.id)\n .map((issue) => issue.message)\n }\n return []\n}\n\n/**\n * Build the configured subtree a fixture describes: manifest defaults overlaid\n * with the fixture's props, plus one default-props child node per\n * `slotChildren` component id. Never throws — unknown components, undeclared\n * slots, and invalid prop values become entries in `issues` while the node is\n * still produced (the renderer shows its own visible fallbacks for them).\n */\nexport function fixtureSubtree(\n studio: StudioRegistry,\n manifest: ComponentManifest,\n fixture: ComponentFixture,\n baseId: string,\n): { node: ComponentNode; issues: string[] } {\n const issues: string[] = []\n const fixtureProps = fixture.props ?? {}\n\n for (const [name, value] of Object.entries(fixtureProps)) {\n const spec = manifest.props[name]\n if (!spec) {\n issues.push(`Fixture \"${fixture.name}\" sets unknown prop \"${name}\".`)\n continue\n }\n // EC-190: validate the fixture value with the prop's field-def validator.\n if (!(propValueValidator(manifest, name)?.(value) ?? true)) {\n issues.push(`Fixture \"${fixture.name}\" sets an invalid value for prop \"${name}\".`)\n }\n }\n\n const node = nodeFromProps(manifest, fixtureProps, baseId)\n\n if (fixture.slotChildren) {\n const declared = new Set(manifest.slots.map((slot) => slot.name))\n const slots: NonNullable<ComponentNode['slots']> = {}\n for (const [slotName, childIds] of Object.entries(fixture.slotChildren)) {\n if (!declared.has(slotName)) {\n issues.push(`Fixture \"${fixture.name}\" fills undeclared slot \"${slotName}\".`)\n }\n slots[slotName] = childIds.map((childId, index) => {\n const id = `${baseId}-${slotName}-${index}`\n const childManifest = studio.registry.getManifest(childId)\n if (!childManifest) {\n issues.push(\n `Fixture \"${fixture.name}\" references unknown component \"${childId}\" in slot \"${slotName}\".`,\n )\n // Keep the reference: the renderer shows a visible missing-component fallback.\n return { id, componentId: childId }\n }\n return defaultNode(childManifest, id)\n })\n }\n if (Object.keys(slots).length > 0) node.slots = slots\n }\n\n return { node, issues }\n}\n\n/** All tiles for one manifest: one per fixture, else a single default tile. */\nexport function tilesForManifest(studio: StudioRegistry, manifest: ComponentManifest): BlockTile[] {\n const title = manifest.title ?? manifest.id\n const baseIssues = manifestIssues(studio, manifest)\n\n if (!manifest.fixtures || manifest.fixtures.length === 0) {\n // No declared fixtures: insert the manifest defaults, seeding layout\n // containers so a bare Stack/Grid survives EC-161 normalization instead of\n // vanishing on commit (EC-383). Preview from the seeded node for containers\n // (WYSIWYG with the insert) and from the shared preview specs otherwise\n // (authored primitive previews or plain defaults).\n const previewNode = isLayoutContainerId(manifest.id)\n ? cloneWithNewIds(seededNodeFromManifest(manifest), prefixedCounter(`preview-${manifest.id}`))\n : previewSpecs(manifest)[0]!.node\n return [\n {\n key: `${manifest.id}::default`,\n componentId: manifest.id,\n title,\n variant: 'Default',\n description: manifest.description,\n issues: baseIssues,\n previewNode,\n build: () => seededNodeFromManifest(manifest),\n },\n ]\n }\n\n return manifest.fixtures.map((fixture, index) => {\n const { node, issues } = fixtureSubtree(\n studio,\n manifest,\n fixture,\n `preview-${manifest.id}-${index}`,\n )\n return {\n key: `${manifest.id}::${fixture.name}`,\n componentId: manifest.id,\n title,\n variant: fixture.name,\n description: manifest.description,\n issues: [...baseIssues, ...issues],\n previewNode: node,\n build: () => fixtureSubtree(studio, manifest, fixture, 'tmp').node,\n }\n })\n}\n\nfunction matches(manifest: ComponentManifest, query: string): boolean {\n const hay = [manifest.id, manifest.title, manifest.description, manifest.category]\n .filter(Boolean)\n .join(' ')\n .toLowerCase()\n return hay.includes(query)\n}\n\n/**\n * Derive the panel's category groups from the registry: filter by slot allow\n * list and query, expand manifests into tiles, group by `category`, and sort\n * categories alphabetically (registry order within a group). Deterministic.\n */\nexport function blockGroups(studio: StudioRegistry, filter: BlockFilter = {}): BlockGroup[] {\n const query = filter.query?.trim().toLowerCase()\n const allow = filter.slotAllow\n\n const groups = new Map<string, BlockTile[]>()\n for (const manifest of studio.registry.list()) {\n // EC-191: the canvas is client-rendered, so only canvas-eligible\n // (client-renderable) components are offered for insertion; server-only / RSC\n // components are usable in dev frames, not placed on the canvas.\n if (!isCanvasEligible(manifest)) continue\n if (allow && !allow.includes(manifest.id)) continue\n if (query && !matches(manifest, query)) continue\n const category = manifest.category ?? UNCATEGORIZED\n const tiles = groups.get(category) ?? []\n tiles.push(...tilesForManifest(studio, manifest))\n groups.set(category, tiles)\n }\n\n return [...groups.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([category, tiles]) => ({ category, tiles }))\n}\n\n/**\n * Layout helper presets (EC-067) as panel tiles. Preview nodes are re-id'd\n * deterministically so sibling React keys stay unique.\n */\nexport function presetTiles(slotAllow?: string[]): BlockTile[] {\n return LAYOUT_PRESETS.filter(\n (preset) => !slotAllow || slotAllow.includes(preset.build().componentId),\n ).map((preset) => ({\n key: `preset::${preset.key}`,\n componentId: preset.build().componentId,\n title: preset.label,\n variant: preset.description,\n description: preset.description,\n issues: [],\n previewNode: cloneWithNewIds(preset.build(), prefixedCounter(`preview-preset-${preset.key}`)),\n build: () => preset.build(),\n }))\n}\n"]}
|
|
1
|
+
{"version":3,"file":"blocks-model.js","sourceRoot":"","sources":["../../../src/components/builder/blocks-model.ts"],"names":[],"mappings":"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","sourcesContent":["import { cloneWithNewIds, isLayoutContainerId } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { ComponentFixture, ComponentManifest } from '@elytracms/core/component-registry'\nimport { isCanvasEligible, propValueValidator } from '@elytracms/core/component-registry'\nimport { componentStatus } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { nodeFromProps, previewSpecs } from '@elytracms/studio/lib/registry/preview'\nimport { LAYOUT_PRESETS, nodeFromManifest, seededNodeFromManifest } from './node-factory'\n\n/**\n * Blocks panel model (EC-164) — the pure, React-free derivation behind the\n * visual insert surface. Every registry manifest yields one tile per declared\n * fixture (or a single default-props tile), grouped by manifest `category`\n * and filtered by a free-text query over name / description / category — no\n * hand-maintained panel config. Broken fixtures degrade to tiles carrying\n * explicit `issues`, never to a missing entry or a crash.\n */\n\n/** One insertable tile in the blocks panel. */\nexport interface BlockTile {\n /** Stable identity (`<componentId>::<variant>` or `preset::<key>`). */\n key: string\n /** Component id of the node the tile inserts. */\n componentId: string\n title: string\n /** Variation label: the fixture name, `Default`, or a preset description. */\n variant: string\n description?: string\n /**\n * Validation problems detected while building the preview (unknown child\n * component, undeclared slot, invalid fixture prop, missing\n * implementation). A non-empty list marks the tile as degraded — it still\n * renders and still inserts.\n */\n issues: string[]\n /** Deterministic-id subtree rendered live in the tile thumbnail. */\n previewNode: ComponentNode\n /**\n * Curated preview image URL (EC-409, `manifest.previewImage`). When set, the\n * tile renders this image instead of the live `previewNode` render — the\n * escape hatch for components whose real render needs data/context (e.g.\n * listings) and previews as \"just text\". Insertion is unaffected.\n */\n previewImage?: string\n /** Build the subtree to insert (ids are reassigned by the builder store). */\n build: () => ComponentNode\n}\n\n/** Tiles of one manifest `category`. */\nexport interface BlockGroup {\n category: string\n tiles: BlockTile[]\n}\n\nexport interface BlockFilter {\n /** Case-insensitive match over id, title, description, and category. */\n query?: string\n /** When set, only these component ids may be inserted (slot.allow). */\n slotAllow?: string[]\n}\n\n/**\n * Intersect two component-id allow-lists. `undefined` means \"no constraint from\n * this source\", so the result is the other list (copied); two present lists yield\n * their set intersection, preserving the first list's order. This layers a\n * composition field's top-level `allow` vocabulary over a slot's positional\n * `allow` at the canvas root (EC-188): a component must satisfy BOTH leashes to be\n * offered. An empty array stays empty (\"allow nothing\"); callers wanting the\n * validator's \"empty = allow any\" must coalesce `[]` to `undefined` first (which\n * `resolveInsertContext` does via its `length > 0` guard, mirroring\n * `validateCompositionValue`).\n */\nexport function intersectAllow(\n slotAllow: readonly string[] | undefined,\n fieldAllow: readonly string[] | undefined,\n): string[] | undefined {\n if (!slotAllow) return fieldAllow ? [...fieldAllow] : undefined\n if (!fieldAllow) return [...slotAllow]\n const inField = new Set(fieldAllow)\n return slotAllow.filter((id) => inField.has(id))\n}\n\nconst UNCATEGORIZED = 'other'\n\nfunction prefixedCounter(prefix: string): () => string {\n let n = 0\n return () => {\n n += 1\n return `${prefix}-${n}`\n }\n}\n\n/** A node built from a manifest's defaults with an explicit id. */\nfunction defaultNode(manifest: ComponentManifest, id: string): ComponentNode {\n return { ...nodeFromManifest(manifest), id }\n}\n\n/** Structured issues for a component that cannot render (EC-055 statuses). */\nfunction manifestIssues(studio: StudioRegistry, manifest: ComponentManifest): string[] {\n const status = componentStatus(studio, manifest)\n if (status === 'incompatible') {\n return [`No implementation is registered for \"${manifest.id}\".`]\n }\n if (status === 'invalid') {\n return studio.registry.issues\n .filter((issue) => issue.componentId === manifest.id)\n .map((issue) => issue.message)\n }\n return []\n}\n\n/**\n * Build the configured subtree a fixture describes: manifest defaults overlaid\n * with the fixture's props, plus one default-props child node per\n * `slotChildren` component id. Never throws — unknown components, undeclared\n * slots, and invalid prop values become entries in `issues` while the node is\n * still produced (the renderer shows its own visible fallbacks for them).\n */\nexport function fixtureSubtree(\n studio: StudioRegistry,\n manifest: ComponentManifest,\n fixture: ComponentFixture,\n baseId: string,\n): { node: ComponentNode; issues: string[] } {\n const issues: string[] = []\n const fixtureProps = fixture.props ?? {}\n\n for (const [name, value] of Object.entries(fixtureProps)) {\n const spec = manifest.props[name]\n if (!spec) {\n issues.push(`Fixture \"${fixture.name}\" sets unknown prop \"${name}\".`)\n continue\n }\n // EC-190: validate the fixture value with the prop's field-def validator.\n if (!(propValueValidator(manifest, name)?.(value) ?? true)) {\n issues.push(`Fixture \"${fixture.name}\" sets an invalid value for prop \"${name}\".`)\n }\n }\n\n const node = nodeFromProps(manifest, fixtureProps, baseId)\n\n if (fixture.slotChildren) {\n const declared = new Set(manifest.slots.map((slot) => slot.name))\n const slots: NonNullable<ComponentNode['slots']> = {}\n for (const [slotName, childIds] of Object.entries(fixture.slotChildren)) {\n if (!declared.has(slotName)) {\n issues.push(`Fixture \"${fixture.name}\" fills undeclared slot \"${slotName}\".`)\n }\n slots[slotName] = childIds.map((childId, index) => {\n const id = `${baseId}-${slotName}-${index}`\n const childManifest = studio.registry.getManifest(childId)\n if (!childManifest) {\n issues.push(\n `Fixture \"${fixture.name}\" references unknown component \"${childId}\" in slot \"${slotName}\".`,\n )\n // Keep the reference: the renderer shows a visible missing-component fallback.\n return { id, componentId: childId }\n }\n return defaultNode(childManifest, id)\n })\n }\n if (Object.keys(slots).length > 0) node.slots = slots\n }\n\n return { node, issues }\n}\n\n/** All tiles for one manifest: one per fixture, else a single default tile. */\nexport function tilesForManifest(studio: StudioRegistry, manifest: ComponentManifest): BlockTile[] {\n const title = manifest.title ?? manifest.id\n const baseIssues = manifestIssues(studio, manifest)\n\n if (!manifest.fixtures || manifest.fixtures.length === 0) {\n // No declared fixtures: insert the manifest defaults, seeding layout\n // containers so a bare Stack/Grid survives EC-161 normalization instead of\n // vanishing on commit (EC-383). Preview from the seeded node for containers\n // (WYSIWYG with the insert) and from the shared preview specs otherwise\n // (authored primitive previews or plain defaults).\n const previewNode = isLayoutContainerId(manifest.id)\n ? cloneWithNewIds(seededNodeFromManifest(manifest), prefixedCounter(`preview-${manifest.id}`))\n : previewSpecs(manifest)[0]!.node\n return [\n {\n key: `${manifest.id}::default`,\n componentId: manifest.id,\n title,\n variant: 'Default',\n description: manifest.description,\n issues: baseIssues,\n previewNode,\n // EC-409: the curated preview image (when declared) replaces the live\n // render on EVERY tile of the manifest — it previews the component,\n // not one fixture variation.\n ...(manifest.previewImage ? { previewImage: manifest.previewImage } : {}),\n build: () => seededNodeFromManifest(manifest),\n },\n ]\n }\n\n return manifest.fixtures.map((fixture, index) => {\n const { node, issues } = fixtureSubtree(\n studio,\n manifest,\n fixture,\n `preview-${manifest.id}-${index}`,\n )\n return {\n key: `${manifest.id}::${fixture.name}`,\n componentId: manifest.id,\n title,\n variant: fixture.name,\n description: manifest.description,\n issues: [...baseIssues, ...issues],\n previewNode: node,\n ...(manifest.previewImage ? { previewImage: manifest.previewImage } : {}),\n build: () => fixtureSubtree(studio, manifest, fixture, 'tmp').node,\n }\n })\n}\n\nfunction matches(manifest: ComponentManifest, query: string): boolean {\n const hay = [manifest.id, manifest.title, manifest.description, manifest.category]\n .filter(Boolean)\n .join(' ')\n .toLowerCase()\n return hay.includes(query)\n}\n\n/**\n * Derive the panel's category groups from the registry: filter by slot allow\n * list and query, expand manifests into tiles, group by `category`, and sort\n * categories alphabetically (registry order within a group). Deterministic.\n */\nexport function blockGroups(studio: StudioRegistry, filter: BlockFilter = {}): BlockGroup[] {\n const query = filter.query?.trim().toLowerCase()\n const allow = filter.slotAllow\n\n const groups = new Map<string, BlockTile[]>()\n for (const manifest of studio.registry.list()) {\n // EC-191: the canvas is client-rendered, so only canvas-eligible\n // (client-renderable) components are offered for insertion; server-only / RSC\n // components are usable in dev frames, not placed on the canvas.\n if (!isCanvasEligible(manifest)) continue\n if (allow && !allow.includes(manifest.id)) continue\n if (query && !matches(manifest, query)) continue\n const category = manifest.category ?? UNCATEGORIZED\n const tiles = groups.get(category) ?? []\n tiles.push(...tilesForManifest(studio, manifest))\n groups.set(category, tiles)\n }\n\n return [...groups.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([category, tiles]) => ({ category, tiles }))\n}\n\n/**\n * Layout helper presets (EC-067) as panel tiles. Preview nodes are re-id'd\n * deterministically so sibling React keys stay unique.\n */\nexport function presetTiles(slotAllow?: string[]): BlockTile[] {\n return LAYOUT_PRESETS.filter(\n (preset) => !slotAllow || slotAllow.includes(preset.build().componentId),\n ).map((preset) => ({\n key: `preset::${preset.key}`,\n componentId: preset.build().componentId,\n title: preset.label,\n variant: preset.description,\n description: preset.description,\n issues: [],\n previewNode: cloneWithNewIds(preset.build(), prefixedCounter(`preview-preset-${preset.key}`)),\n build: () => preset.build(),\n }))\n}\n"]}
|
|
@@ -11,7 +11,7 @@ import { Code, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTit
|
|
|
11
11
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
12
12
|
import { useBuilder } from './builder-store.js';
|
|
13
13
|
import { blockGroups, presetTiles } from './blocks-model.js';
|
|
14
|
-
import { ScaledFixtureThumbnail } from './fixture-thumbnail.js';
|
|
14
|
+
import { PreviewImageThumbnail, ScaledFixtureThumbnail } from './fixture-thumbnail.js';
|
|
15
15
|
/**
|
|
16
16
|
* "Copy exact configuration" (EC-175, vision 3.3): the EC-174 fragment for a
|
|
17
17
|
* blocks-panel tile. Tiles backed by a declared manifest fixture go through
|
|
@@ -151,6 +151,6 @@ function BlockTileButton({ tile, onClick, onCopy, onDragStart, onDragEnd, }) {
|
|
|
151
151
|
}), title: translate('builder.blocksPanel.copyExactConfiguration'), "data-block-copy": tile.key, onClick: (event) => {
|
|
152
152
|
event.stopPropagation();
|
|
153
153
|
onCopy();
|
|
154
|
-
}, className: cn('absolute right-1.5 top-1.5 z-10 rounded-md border border-border bg-popover/90 p-1 text-muted-foreground', 'opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/tile:opacity-100'), children: _jsx(ClipboardCopy, { className: "size-3.5", "aria-hidden": true }) }), _jsx(ScaledFixtureThumbnail, { node: tile.previewNode }), _jsxs(Row, { gap: "1", className: "w-full min-w-0", children: [_jsx("span", { className: "truncate text-xs font-medium", children: tile.title }), _jsx("span", { className: "ml-auto shrink-0 text-[0.7rem] text-muted-foreground", children: tile.variant })] }), degraded ? (_jsxs(Row, { gap: "1", className: "w-full min-w-0 text-amber-700 dark:text-amber-400", children: [_jsx(AlertTriangle, { "aria-hidden": true, className: "size-3 shrink-0" }), _jsx("span", { className: "truncate text-[0.7rem]", title: tile.issues.join('\n'), children: tile.issues[0] })] })) : null] }));
|
|
154
|
+
}, className: cn('absolute right-1.5 top-1.5 z-10 rounded-md border border-border bg-popover/90 p-1 text-muted-foreground', 'opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/tile:opacity-100'), children: _jsx(ClipboardCopy, { className: "size-3.5", "aria-hidden": true }) }), tile.previewImage ? (_jsx(PreviewImageThumbnail, { src: tile.previewImage, alt: tile.title })) : (_jsx(ScaledFixtureThumbnail, { node: tile.previewNode })), _jsxs(Row, { gap: "1", className: "w-full min-w-0", children: [_jsx("span", { className: "truncate text-xs font-medium", children: tile.title }), _jsx("span", { className: "ml-auto shrink-0 text-[0.7rem] text-muted-foreground", children: tile.variant })] }), degraded ? (_jsxs(Row, { gap: "1", className: "w-full min-w-0 text-amber-700 dark:text-amber-400", children: [_jsx(AlertTriangle, { "aria-hidden": true, className: "size-3 shrink-0" }), _jsx("span", { className: "truncate text-[0.7rem]", title: tile.issues.join('\n'), children: tile.issues[0] })] })) : null] }));
|
|
155
155
|
}
|
|
156
156
|
//# sourceMappingURL=blocks-panel.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blocks-panel.js","sourceRoot":"","sources":["../../../src/components/builder/blocks-panel.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, ClipboardCopy, Search } from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport type { InsertTarget } from '@elytracms/core/project-graph'\nimport {\n buildFragmentFromRegistryFixture,\n buildFragmentFromSubtree,\n} from '@elytracms/core/clipboard'\nimport type { ClipboardFragment } from '@elytracms/core/clipboard'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useCollab } from '../collab/collab-context'\nimport { useStudioClipboard } from './clipboard-actions'\nimport { beginDrag, endDrag, onDragSessionEnd } from './drag-session'\nimport {\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { useBuilder } from './builder-store'\nimport { blockGroups, presetTiles } from './blocks-model'\nimport type { BlockTile } from './blocks-model'\nimport { ScaledFixtureThumbnail } from './fixture-thumbnail'\n\n/**\n * Blocks panel (EC-164): the visual insert surface. Every registry component\n * and each of its fixture variations is a tile with a live, scaled render —\n * the component library *is* the insert surface. Clicking a tile inserts the\n * fixture's configured subtree through the builder's `insert` action (one\n * history entry, mirrored onto the `graph.insertNode` operation).\n */\n\nexport interface InsertContext {\n target: InsertTarget\n /** When set, only these component ids may be inserted into the slot (slot.allow). */\n slotAllow?: string[]\n /** Human label of where the insert lands. */\n destinationLabel: string\n}\n\n/**\n * \"Copy exact configuration\" (EC-175, vision 3.3): the EC-174 fragment for a\n * blocks-panel tile. Tiles backed by a declared manifest fixture go through\n * `buildFragmentFromRegistryFixture` (the fixture's exact configuration);\n * preset/default tiles fragment the same subtree the tile inserts. Both paths\n * share `buildFragmentFromSubtree`, so the fragment shape — and the paste\n * path — is identical to copying a live canvas instance.\n */\nexport function tileFragment(\n studio: StudioRegistry,\n tile: BlockTile,\n projectId: string,\n): ClipboardFragment | null {\n const manifest = studio.registry.getManifest(tile.componentId)\n const fixture = manifest?.fixtures?.find((candidate) => candidate.name === tile.variant)\n if (manifest && fixture) {\n const built = buildFragmentFromRegistryFixture(manifest, fixture, {\n source: { projectId },\n })\n return built.ok ? built.fragment : null\n }\n return buildFragmentFromSubtree(tile.build(), { source: { projectId } })\n}\n\n/**\n * The Insert dialog never dismisses on pointer interactions outside its\n * content. Rows scrolled below the list's fold keep their layout position\n * directly above the modal overlay, so a coordinate-targeted click on a\n * clipped row (agents click what the accessibility tree lists; humans can hit\n * the boundary too) lands on the overlay — with default Radix behaviour that\n * pointerdown dismissed the dialog and silently dropped the insert. Cancel\n * stays explicit: Escape or the Close button.\n */\nexport function preventOutsideDismiss(event: { preventDefault: () => void }): void {\n event.preventDefault()\n}\n\nexport function BlocksPanel({\n context,\n open,\n onOpenChange,\n}: {\n context: InsertContext | null\n open: boolean\n onOpenChange: (open: boolean) => void\n}) {\n const builder = useBuilder()\n const studio = getStudioRegistry()\n const collab = useCollab()\n const clipboard = useStudioClipboard()\n const [query, setQuery] = useState('')\n\n useEffect(() => {\n if (open) setQuery('')\n }, [open])\n\n const slotAllow = context?.slotAllow\n const trimmed = query.trim()\n const groups = useMemo(\n () => blockGroups(studio, { query: trimmed || undefined, slotAllow }),\n [studio, trimmed, slotAllow],\n )\n // Layout helpers keep their dedicated group while browsing (hidden once a\n // query narrows the panel to registry matches), filtered by slot.allow.\n const presets = useMemo(\n () => (trimmed ? [] : presetTiles(slotAllow)),\n [trimmed, slotAllow],\n )\n\n // Drag-to-insert (EC-160): a tile is also a drag source. While the drag is\n // live the modal (content + overlay + Radix body pointer lock) gets out of\n // the way so the canvas and the page tree can receive the drop; the session\n // ending restores it — and a successful drop closes the panel. The drop\n // surfaces dispatch the single insert action; the panel only carries the\n // payload (`tile.build`, the same fixture-subtree model click-insert uses).\n const dragRestoreRef = useRef<(() => void) | null>(null)\n useEffect(\n () =>\n onDragSessionEnd((outcome, session) => {\n if (session.source !== 'blocks') return\n dragRestoreRef.current?.()\n dragRestoreRef.current = null\n if (outcome === 'dropped') onOpenChange(false)\n }),\n [onOpenChange],\n )\n\n if (!context) return null\n\n function insertTile(tile: BlockTile) {\n builder.dispatch({ type: 'insert', target: context!.target, node: tile.build() })\n onOpenChange(false)\n }\n\n // \"Copy exact configuration\" (EC-175): same fragment + paste path as\n // copying a live canvas instance.\n function copyTile(tile: BlockTile) {\n const fragment = tileFragment(studio, tile, collab.projectId)\n if (fragment) clipboard.copyFragment(fragment, `${tile.title} — ${tile.variant}`)\n }\n\n function handleTileDragStart(tile: BlockTile, e: React.DragEvent) {\n beginDrag(\n {\n subject: { kind: 'component', componentId: tile.componentId },\n source: 'blocks',\n label: `${tile.title} — ${tile.variant}`,\n buildNode: tile.build,\n },\n e.dataTransfer,\n )\n // Radix's modal layer locks pointer events on the body and the overlay\n // sits above every drop surface. Lift both for the duration of the drag —\n // after a tick, so the browser captures the tile as the drag image first.\n const content = (e.currentTarget as HTMLElement).closest('[role=\"dialog\"]') as HTMLElement | null\n const overlay = (content?.previousElementSibling ?? null) as HTMLElement | null\n const previousBodyPointerEvents = document.body.style.pointerEvents\n const hide = window.setTimeout(() => {\n document.body.style.pointerEvents = 'auto'\n for (const el of [content, overlay]) {\n if (el) {\n el.style.pointerEvents = 'none'\n el.style.opacity = '0'\n }\n }\n }, 0)\n dragRestoreRef.current = () => {\n window.clearTimeout(hide)\n document.body.style.pointerEvents = previousBodyPointerEvents\n for (const el of [content, overlay]) {\n if (el) {\n el.style.pointerEvents = ''\n el.style.opacity = ''\n }\n }\n }\n }\n\n function handleTileDragEnd() {\n // Trailing dragend after a handled drop is a no-op (endDrag is idempotent).\n endDrag('cancelled')\n }\n\n const empty = groups.length === 0 && presets.length === 0\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-3xl\" onInteractOutside={preventOutsideDismiss}>\n <DialogHeader>\n <DialogTitle>{translate('builder.blocksPanel.title')}</DialogTitle>\n <DialogDescription>\n Adds into <Code tone=\"muted\">{context.destinationLabel}</Code>.\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translate('builder.blocksPanel.searchPlaceholder')}\n aria-label={translate('builder.blocksPanel.searchAria')}\n className=\"pl-8\"\n />\n </div>\n\n {/* Viewport-bounded so the dialog never extends past the fold on short windows\n (a grid taller than the screen would put tiles outside the clickable area). */}\n <div className=\"max-h-[min(30rem,calc(100dvh-14rem))] overflow-auto pr-1\">\n {presets.length > 0 ? (\n <TileGroup\n heading={translate('builder.blocksPanel.layoutHelpers')}\n tiles={presets}\n onInsert={insertTile}\n onCopy={copyTile}\n onTileDragStart={handleTileDragStart}\n onTileDragEnd={handleTileDragEnd}\n />\n ) : null}\n {groups.map((group) => (\n <TileGroup\n key={group.category}\n heading={group.category}\n tiles={group.tiles}\n onInsert={insertTile}\n onCopy={copyTile}\n onTileDragStart={handleTileDragStart}\n onTileDragEnd={handleTileDragEnd}\n />\n ))}\n {empty ? (\n <Text size=\"sm\" tone=\"muted\" className=\"p-3\">\n {slotAllow\n ? translate('builder.blocksPanel.noAllowed')\n : translate('builder.blocksPanel.noMatch', { query })}\n </Text>\n ) : null}\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n\nfunction TileGroup({\n heading,\n tiles,\n onInsert,\n onCopy,\n onTileDragStart,\n onTileDragEnd,\n}: {\n heading: string\n tiles: BlockTile[]\n onInsert: (tile: BlockTile) => void\n onCopy: (tile: BlockTile) => void\n onTileDragStart: (tile: BlockTile, e: React.DragEvent) => void\n onTileDragEnd: () => void\n}) {\n return (\n <section className=\"mb-3\" aria-label={heading}>\n <Row gap=\"2\" className=\"px-1 py-1.5\">\n <Code tone=\"muted\" className=\"capitalize\">\n {heading}\n </Code>\n <Text size=\"xs\" tone=\"muted\">\n {tiles.length}\n </Text>\n </Row>\n <div className=\"grid grid-cols-3 gap-2\">\n {tiles.map((tile) => (\n <BlockTileButton\n key={tile.key}\n tile={tile}\n onClick={() => onInsert(tile)}\n onCopy={() => onCopy(tile)}\n onDragStart={(e) => onTileDragStart(tile, e)}\n onDragEnd={onTileDragEnd}\n />\n ))}\n </div>\n </section>\n )\n}\n\nfunction BlockTileButton({\n tile,\n onClick,\n onCopy,\n onDragStart,\n onDragEnd,\n}: {\n tile: BlockTile\n onClick: () => void\n onCopy: () => void\n onDragStart: (e: React.DragEvent) => void\n onDragEnd: () => void\n}) {\n const degraded = tile.issues.length > 0\n // Not a native <button>: live thumbnails are real renders and may contain\n // interactive elements (e.g. the Button primitive), and a nested <button>\n // is invalid HTML. The thumbnail itself is aria-hidden and pointer-inert.\n return (\n <div\n role=\"button\"\n tabIndex={0}\n draggable\n onDragStart={onDragStart}\n onDragEnd={onDragEnd}\n onClick={onClick}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n onClick()\n }\n }}\n data-block-tile={tile.key}\n data-block-degraded={degraded || undefined}\n aria-label={translate('builder.blocksPanel.insertTileAria', {\n title: tile.title,\n variant: tile.variant,\n })}\n title={tile.description}\n className={cn(\n 'group/tile relative flex cursor-pointer flex-col gap-1.5 rounded-lg border bg-card p-2 text-left outline-none transition-colors',\n 'hover:border-ring/60 hover:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring/60',\n degraded ? 'border-amber-500/50' : 'border-border',\n )}\n >\n {/* Copy exact configuration (EC-175/vision 3.3): the EC-174 fragment of\n this tile, pasteable anywhere the canvas/tree paste path reaches. */}\n <button\n type=\"button\"\n aria-label={translate('builder.blocksPanel.copyExactAria', {\n title: tile.title,\n variant: tile.variant,\n })}\n title={translate('builder.blocksPanel.copyExactConfiguration')}\n data-block-copy={tile.key}\n onClick={(event) => {\n event.stopPropagation()\n onCopy()\n }}\n className={cn(\n 'absolute right-1.5 top-1.5 z-10 rounded-md border border-border bg-popover/90 p-1 text-muted-foreground',\n 'opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/tile:opacity-100',\n )}\n >\n <ClipboardCopy className=\"size-3.5\" aria-hidden />\n </button>\n <ScaledFixtureThumbnail node={tile.previewNode} />\n <Row gap=\"1\" className=\"w-full min-w-0\">\n <span className=\"truncate text-xs font-medium\">{tile.title}</span>\n <span className=\"ml-auto shrink-0 text-[0.7rem] text-muted-foreground\">{tile.variant}</span>\n </Row>\n {degraded ? (\n <Row gap=\"1\" className=\"w-full min-w-0 text-amber-700 dark:text-amber-400\">\n <AlertTriangle aria-hidden className=\"size-3 shrink-0\" />\n <span className=\"truncate text-[0.7rem]\" title={tile.issues.join('\\n')}>\n {tile.issues[0]}\n </span>\n </Row>\n ) : null}\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"blocks-panel.js","sourceRoot":"","sources":["../../../src/components/builder/blocks-panel.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, ClipboardCopy, Search } from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport type { InsertTarget } from '@elytracms/core/project-graph'\nimport {\n buildFragmentFromRegistryFixture,\n buildFragmentFromSubtree,\n} from '@elytracms/core/clipboard'\nimport type { ClipboardFragment } from '@elytracms/core/clipboard'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useCollab } from '../collab/collab-context'\nimport { useStudioClipboard } from './clipboard-actions'\nimport { beginDrag, endDrag, onDragSessionEnd } from './drag-session'\nimport {\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { useBuilder } from './builder-store'\nimport { blockGroups, presetTiles } from './blocks-model'\nimport type { BlockTile } from './blocks-model'\nimport { PreviewImageThumbnail, ScaledFixtureThumbnail } from './fixture-thumbnail'\n\n/**\n * Blocks panel (EC-164): the visual insert surface. Every registry component\n * and each of its fixture variations is a tile with a live, scaled render —\n * the component library *is* the insert surface. Clicking a tile inserts the\n * fixture's configured subtree through the builder's `insert` action (one\n * history entry, mirrored onto the `graph.insertNode` operation).\n */\n\nexport interface InsertContext {\n target: InsertTarget\n /** When set, only these component ids may be inserted into the slot (slot.allow). */\n slotAllow?: string[]\n /** Human label of where the insert lands. */\n destinationLabel: string\n}\n\n/**\n * \"Copy exact configuration\" (EC-175, vision 3.3): the EC-174 fragment for a\n * blocks-panel tile. Tiles backed by a declared manifest fixture go through\n * `buildFragmentFromRegistryFixture` (the fixture's exact configuration);\n * preset/default tiles fragment the same subtree the tile inserts. Both paths\n * share `buildFragmentFromSubtree`, so the fragment shape — and the paste\n * path — is identical to copying a live canvas instance.\n */\nexport function tileFragment(\n studio: StudioRegistry,\n tile: BlockTile,\n projectId: string,\n): ClipboardFragment | null {\n const manifest = studio.registry.getManifest(tile.componentId)\n const fixture = manifest?.fixtures?.find((candidate) => candidate.name === tile.variant)\n if (manifest && fixture) {\n const built = buildFragmentFromRegistryFixture(manifest, fixture, {\n source: { projectId },\n })\n return built.ok ? built.fragment : null\n }\n return buildFragmentFromSubtree(tile.build(), { source: { projectId } })\n}\n\n/**\n * The Insert dialog never dismisses on pointer interactions outside its\n * content. Rows scrolled below the list's fold keep their layout position\n * directly above the modal overlay, so a coordinate-targeted click on a\n * clipped row (agents click what the accessibility tree lists; humans can hit\n * the boundary too) lands on the overlay — with default Radix behaviour that\n * pointerdown dismissed the dialog and silently dropped the insert. Cancel\n * stays explicit: Escape or the Close button.\n */\nexport function preventOutsideDismiss(event: { preventDefault: () => void }): void {\n event.preventDefault()\n}\n\nexport function BlocksPanel({\n context,\n open,\n onOpenChange,\n}: {\n context: InsertContext | null\n open: boolean\n onOpenChange: (open: boolean) => void\n}) {\n const builder = useBuilder()\n const studio = getStudioRegistry()\n const collab = useCollab()\n const clipboard = useStudioClipboard()\n const [query, setQuery] = useState('')\n\n useEffect(() => {\n if (open) setQuery('')\n }, [open])\n\n const slotAllow = context?.slotAllow\n const trimmed = query.trim()\n const groups = useMemo(\n () => blockGroups(studio, { query: trimmed || undefined, slotAllow }),\n [studio, trimmed, slotAllow],\n )\n // Layout helpers keep their dedicated group while browsing (hidden once a\n // query narrows the panel to registry matches), filtered by slot.allow.\n const presets = useMemo(\n () => (trimmed ? [] : presetTiles(slotAllow)),\n [trimmed, slotAllow],\n )\n\n // Drag-to-insert (EC-160): a tile is also a drag source. While the drag is\n // live the modal (content + overlay + Radix body pointer lock) gets out of\n // the way so the canvas and the page tree can receive the drop; the session\n // ending restores it — and a successful drop closes the panel. The drop\n // surfaces dispatch the single insert action; the panel only carries the\n // payload (`tile.build`, the same fixture-subtree model click-insert uses).\n const dragRestoreRef = useRef<(() => void) | null>(null)\n useEffect(\n () =>\n onDragSessionEnd((outcome, session) => {\n if (session.source !== 'blocks') return\n dragRestoreRef.current?.()\n dragRestoreRef.current = null\n if (outcome === 'dropped') onOpenChange(false)\n }),\n [onOpenChange],\n )\n\n if (!context) return null\n\n function insertTile(tile: BlockTile) {\n builder.dispatch({ type: 'insert', target: context!.target, node: tile.build() })\n onOpenChange(false)\n }\n\n // \"Copy exact configuration\" (EC-175): same fragment + paste path as\n // copying a live canvas instance.\n function copyTile(tile: BlockTile) {\n const fragment = tileFragment(studio, tile, collab.projectId)\n if (fragment) clipboard.copyFragment(fragment, `${tile.title} — ${tile.variant}`)\n }\n\n function handleTileDragStart(tile: BlockTile, e: React.DragEvent) {\n beginDrag(\n {\n subject: { kind: 'component', componentId: tile.componentId },\n source: 'blocks',\n label: `${tile.title} — ${tile.variant}`,\n buildNode: tile.build,\n },\n e.dataTransfer,\n )\n // Radix's modal layer locks pointer events on the body and the overlay\n // sits above every drop surface. Lift both for the duration of the drag —\n // after a tick, so the browser captures the tile as the drag image first.\n const content = (e.currentTarget as HTMLElement).closest('[role=\"dialog\"]') as HTMLElement | null\n const overlay = (content?.previousElementSibling ?? null) as HTMLElement | null\n const previousBodyPointerEvents = document.body.style.pointerEvents\n const hide = window.setTimeout(() => {\n document.body.style.pointerEvents = 'auto'\n for (const el of [content, overlay]) {\n if (el) {\n el.style.pointerEvents = 'none'\n el.style.opacity = '0'\n }\n }\n }, 0)\n dragRestoreRef.current = () => {\n window.clearTimeout(hide)\n document.body.style.pointerEvents = previousBodyPointerEvents\n for (const el of [content, overlay]) {\n if (el) {\n el.style.pointerEvents = ''\n el.style.opacity = ''\n }\n }\n }\n }\n\n function handleTileDragEnd() {\n // Trailing dragend after a handled drop is a no-op (endDrag is idempotent).\n endDrag('cancelled')\n }\n\n const empty = groups.length === 0 && presets.length === 0\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-3xl\" onInteractOutside={preventOutsideDismiss}>\n <DialogHeader>\n <DialogTitle>{translate('builder.blocksPanel.title')}</DialogTitle>\n <DialogDescription>\n Adds into <Code tone=\"muted\">{context.destinationLabel}</Code>.\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translate('builder.blocksPanel.searchPlaceholder')}\n aria-label={translate('builder.blocksPanel.searchAria')}\n className=\"pl-8\"\n />\n </div>\n\n {/* Viewport-bounded so the dialog never extends past the fold on short windows\n (a grid taller than the screen would put tiles outside the clickable area). */}\n <div className=\"max-h-[min(30rem,calc(100dvh-14rem))] overflow-auto pr-1\">\n {presets.length > 0 ? (\n <TileGroup\n heading={translate('builder.blocksPanel.layoutHelpers')}\n tiles={presets}\n onInsert={insertTile}\n onCopy={copyTile}\n onTileDragStart={handleTileDragStart}\n onTileDragEnd={handleTileDragEnd}\n />\n ) : null}\n {groups.map((group) => (\n <TileGroup\n key={group.category}\n heading={group.category}\n tiles={group.tiles}\n onInsert={insertTile}\n onCopy={copyTile}\n onTileDragStart={handleTileDragStart}\n onTileDragEnd={handleTileDragEnd}\n />\n ))}\n {empty ? (\n <Text size=\"sm\" tone=\"muted\" className=\"p-3\">\n {slotAllow\n ? translate('builder.blocksPanel.noAllowed')\n : translate('builder.blocksPanel.noMatch', { query })}\n </Text>\n ) : null}\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n\nfunction TileGroup({\n heading,\n tiles,\n onInsert,\n onCopy,\n onTileDragStart,\n onTileDragEnd,\n}: {\n heading: string\n tiles: BlockTile[]\n onInsert: (tile: BlockTile) => void\n onCopy: (tile: BlockTile) => void\n onTileDragStart: (tile: BlockTile, e: React.DragEvent) => void\n onTileDragEnd: () => void\n}) {\n return (\n <section className=\"mb-3\" aria-label={heading}>\n <Row gap=\"2\" className=\"px-1 py-1.5\">\n <Code tone=\"muted\" className=\"capitalize\">\n {heading}\n </Code>\n <Text size=\"xs\" tone=\"muted\">\n {tiles.length}\n </Text>\n </Row>\n <div className=\"grid grid-cols-3 gap-2\">\n {tiles.map((tile) => (\n <BlockTileButton\n key={tile.key}\n tile={tile}\n onClick={() => onInsert(tile)}\n onCopy={() => onCopy(tile)}\n onDragStart={(e) => onTileDragStart(tile, e)}\n onDragEnd={onTileDragEnd}\n />\n ))}\n </div>\n </section>\n )\n}\n\nfunction BlockTileButton({\n tile,\n onClick,\n onCopy,\n onDragStart,\n onDragEnd,\n}: {\n tile: BlockTile\n onClick: () => void\n onCopy: () => void\n onDragStart: (e: React.DragEvent) => void\n onDragEnd: () => void\n}) {\n const degraded = tile.issues.length > 0\n // Not a native <button>: live thumbnails are real renders and may contain\n // interactive elements (e.g. the Button primitive), and a nested <button>\n // is invalid HTML. The thumbnail itself is aria-hidden and pointer-inert.\n return (\n <div\n role=\"button\"\n tabIndex={0}\n draggable\n onDragStart={onDragStart}\n onDragEnd={onDragEnd}\n onClick={onClick}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n onClick()\n }\n }}\n data-block-tile={tile.key}\n data-block-degraded={degraded || undefined}\n aria-label={translate('builder.blocksPanel.insertTileAria', {\n title: tile.title,\n variant: tile.variant,\n })}\n title={tile.description}\n className={cn(\n 'group/tile relative flex cursor-pointer flex-col gap-1.5 rounded-lg border bg-card p-2 text-left outline-none transition-colors',\n 'hover:border-ring/60 hover:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring/60',\n degraded ? 'border-amber-500/50' : 'border-border',\n )}\n >\n {/* Copy exact configuration (EC-175/vision 3.3): the EC-174 fragment of\n this tile, pasteable anywhere the canvas/tree paste path reaches. */}\n <button\n type=\"button\"\n aria-label={translate('builder.blocksPanel.copyExactAria', {\n title: tile.title,\n variant: tile.variant,\n })}\n title={translate('builder.blocksPanel.copyExactConfiguration')}\n data-block-copy={tile.key}\n onClick={(event) => {\n event.stopPropagation()\n onCopy()\n }}\n className={cn(\n 'absolute right-1.5 top-1.5 z-10 rounded-md border border-border bg-popover/90 p-1 text-muted-foreground',\n 'opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/tile:opacity-100',\n )}\n >\n <ClipboardCopy className=\"size-3.5\" aria-hidden />\n </button>\n {/* EC-409: a curated `manifest.previewImage` replaces the live fixture\n render — the escape hatch for tiles that would preview as \"just\n text\" (data-dependent renders). Everything else about the tile —\n insert, drag, copy — is unchanged. */}\n {tile.previewImage ? (\n <PreviewImageThumbnail src={tile.previewImage} alt={tile.title} />\n ) : (\n <ScaledFixtureThumbnail node={tile.previewNode} />\n )}\n <Row gap=\"1\" className=\"w-full min-w-0\">\n <span className=\"truncate text-xs font-medium\">{tile.title}</span>\n <span className=\"ml-auto shrink-0 text-[0.7rem] text-muted-foreground\">{tile.variant}</span>\n </Row>\n {degraded ? (\n <Row gap=\"1\" className=\"w-full min-w-0 text-amber-700 dark:text-amber-400\">\n <AlertTriangle aria-hidden className=\"size-3 shrink-0\" />\n <span className=\"truncate text-[0.7rem]\" title={tile.issues.join('\\n')}>\n {tile.issues[0]}\n </span>\n </Row>\n ) : null}\n </div>\n )\n}\n"]}
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
2
2
|
import type { StudioRegistry } from '@elytracms/studio/lib/registry';
|
|
3
|
+
/**
|
|
4
|
+
* Curated preview-image tile (EC-409): renders `manifest.previewImage` in the
|
|
5
|
+
* SAME 16/10 tile frame as {@link ScaledFixtureThumbnail}, object-fit cover.
|
|
6
|
+
* The escape hatch for components whose live render needs data/context the
|
|
7
|
+
* tile can't have — fixtures stay the primary curation lever. `alt` is the
|
|
8
|
+
* component title (already-localized manifest data); the image is never a
|
|
9
|
+
* fetch the engine performs, just an `<img>` with the consumer's URL string.
|
|
10
|
+
*/
|
|
11
|
+
export declare function PreviewImageThumbnail({ src, alt, className, }: {
|
|
12
|
+
/** The manifest's `previewImage` URL string, passed through verbatim. */
|
|
13
|
+
src: string;
|
|
14
|
+
/** The component title (localized manifest data — no new catalog string). */
|
|
15
|
+
alt: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
3
18
|
export declare function ScaledFixtureThumbnail({ node, studio, designWidth, eager, className, }: {
|
|
4
19
|
/** The preview subtree to render live (deterministic ids). */
|
|
5
20
|
node: ComponentNode;
|
|
@@ -41,6 +41,20 @@ class ThumbnailErrorBoundary extends Component {
|
|
|
41
41
|
return this.props.children;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Curated preview-image tile (EC-409): renders `manifest.previewImage` in the
|
|
46
|
+
* SAME 16/10 tile frame as {@link ScaledFixtureThumbnail}, object-fit cover.
|
|
47
|
+
* The escape hatch for components whose live render needs data/context the
|
|
48
|
+
* tile can't have — fixtures stay the primary curation lever. `alt` is the
|
|
49
|
+
* component title (already-localized manifest data); the image is never a
|
|
50
|
+
* fetch the engine performs, just an `<img>` with the consumer's URL string.
|
|
51
|
+
*/
|
|
52
|
+
export function PreviewImageThumbnail({ src, alt, className, }) {
|
|
53
|
+
return (_jsx("div", { "data-block-thumbnail": true, "data-block-thumbnail-image": true, className: cn('relative w-full overflow-hidden rounded-md border border-border bg-background', className), style: { aspectRatio: '16 / 10' }, children: _jsx("img", { src: src, alt: alt,
|
|
54
|
+
// The surrounding tile is the drag source; a natively draggable <img>
|
|
55
|
+
// would hijack the tile drag.
|
|
56
|
+
draggable: false, className: "pointer-events-none h-full w-full select-none object-cover" }) }));
|
|
57
|
+
}
|
|
44
58
|
export function ScaledFixtureThumbnail({ node, studio, designWidth = DESIGN_WIDTH, eager = false, className, }) {
|
|
45
59
|
const registry = studio ?? getStudioRegistry();
|
|
46
60
|
const ref = useRef(null);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixture-thumbnail.js","sourceRoot":"","sources":["../../../src/components/builder/fixture-thumbnail.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Component, useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { AlertTriangle } from 'lucide-react'\nimport { renderNode } from '@elytracms/core/runtime-renderer'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { getStudioRegistry, previewContext } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Live, scaled fixture thumbnail (EC-164). Renders a preview node through the\n * runtime renderer — the exact render path the canvas, fixture runner, and\n * export use — inside a fixed-aspect tile, scaled down with a CSS transform.\n * Never a screenshot, never interactive (pointer events are disabled), and\n * never a crash: render errors degrade to a visible fallback inside the tile.\n *\n * Off-screen thumbnails mount lazily via IntersectionObserver so a large\n * registry doesn't render its whole library up front; pass `eager` to render\n * immediately (first-fold tiles, non-DOM tests).\n */\n\n/** Width the preview is laid out at before being scaled into the tile. */\nconst DESIGN_WIDTH = 420\n/** Fallback scale used for the single frame before the tile is measured. */\nconst FALLBACK_SCALE = 0.45\n\nfunction LiveRender({ node, studio }: { node: ComponentNode; studio: StudioRegistry }) {\n const ctx = useMemo(() => previewContext(studio), [studio])\n // Called inside a component render so the boundary catches sync throws too.\n return <>{renderNode(node, ctx)}</>\n}\n\nfunction ThumbnailFallback({ message }: { message: string }) {\n return (\n <div\n data-block-thumbnail-fallback\n className=\"flex h-full w-full items-start gap-1.5 p-2 text-[0.7rem] leading-snug text-amber-700 dark:text-amber-400\"\n >\n <AlertTriangle aria-hidden className=\"mt-px size-3 shrink-0\" />\n <span className=\"line-clamp-3\">{message}</span>\n </div>\n )\n}\n\ninterface BoundaryState {\n error: Error | null\n}\n\n/** Tile-local error boundary: one broken fixture never takes down the panel. */\nclass ThumbnailErrorBoundary extends Component<{ children: ReactNode }, BoundaryState> {\n override state: BoundaryState = { error: null }\n\n static getDerivedStateFromError(error: Error): BoundaryState {\n return { error }\n }\n\n override render(): ReactNode {\n if (this.state.error) {\n return (\n <ThumbnailFallback\n message={translate('builder.blocksPanel.previewFailed', { error: this.state.error.message })}\n />\n )\n }\n return this.props.children\n }\n}\n\nexport function ScaledFixtureThumbnail({\n node,\n studio,\n designWidth = DESIGN_WIDTH,\n eager = false,\n className,\n}: {\n /** The preview subtree to render live (deterministic ids). */\n node: ComponentNode\n /**\n * Registry the preview renders against. Defaults to the active project's live\n * registry (`getStudioRegistry()`) — the blocks panel's tiles and their\n * thumbnails share one registry. Pass an explicit registry to render against a\n * specific component set (deterministic previews/tests).\n */\n studio?: StudioRegistry\n /** Layout width of the unscaled preview, in CSS pixels. */\n designWidth?: number\n /** Mount the live render immediately instead of waiting for visibility. */\n eager?: boolean\n className?: string\n}) {\n const registry = studio ?? getStudioRegistry()\n const ref = useRef<HTMLDivElement>(null)\n const [inView, setInView] = useState(eager)\n const [scale, setScale] = useState<number | null>(null)\n\n // Lazy mount: render the live preview only once the tile scrolls into view.\n // IntersectionObserver always delivers an initial report (intersecting or\n // not) within a rendering frame — when no report arrives at all the page\n // isn't producing frames (occluded/headless automation windows), so a\n // safety-net timer mounts the preview rather than leaving tiles blank.\n // Frame-producing browsers report long before the timer and keep laziness.\n useEffect(() => {\n if (inView) return\n const el = ref.current\n if (!el || typeof IntersectionObserver === 'undefined') {\n setInView(true)\n return\n }\n let reported = false\n const observer = new IntersectionObserver((entries) => {\n reported = true\n if (entries.some((entry) => entry.isIntersecting)) setInView(true)\n })\n observer.observe(el)\n const fallback = setTimeout(() => {\n if (!reported) setInView(true)\n }, 400)\n return () => {\n observer.disconnect()\n clearTimeout(fallback)\n }\n }, [inView])\n\n // Fit-to-tile scale, kept in sync with the tile's measured width.\n useEffect(() => {\n const el = ref.current\n if (!el) return\n const update = () => {\n const width = el.clientWidth\n if (width > 0) setScale(width / designWidth)\n }\n update()\n if (typeof ResizeObserver === 'undefined') return\n const observer = new ResizeObserver(update)\n observer.observe(el)\n return () => observer.disconnect()\n }, [designWidth])\n\n return (\n <div\n ref={ref}\n aria-hidden\n data-block-thumbnail\n className={cn(\n 'relative w-full overflow-hidden rounded-md border border-border bg-background',\n className,\n )}\n style={{ aspectRatio: '16 / 10' }}\n >\n {inView ? (\n <ThumbnailErrorBoundary>\n <div\n className=\"pointer-events-none absolute left-0 top-0 origin-top-left select-none p-3\"\n style={{ width: designWidth, transform: `scale(${scale ?? FALLBACK_SCALE})` }}\n >\n <LiveRender node={node} studio={registry} />\n </div>\n </ThumbnailErrorBoundary>\n ) : null}\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fixture-thumbnail.js","sourceRoot":"","sources":["../../../src/components/builder/fixture-thumbnail.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Component, useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { AlertTriangle } from 'lucide-react'\nimport { renderNode } from '@elytracms/core/runtime-renderer'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { getStudioRegistry, previewContext } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Live, scaled fixture thumbnail (EC-164). Renders a preview node through the\n * runtime renderer — the exact render path the canvas, fixture runner, and\n * export use — inside a fixed-aspect tile, scaled down with a CSS transform.\n * Never a screenshot, never interactive (pointer events are disabled), and\n * never a crash: render errors degrade to a visible fallback inside the tile.\n *\n * Off-screen thumbnails mount lazily via IntersectionObserver so a large\n * registry doesn't render its whole library up front; pass `eager` to render\n * immediately (first-fold tiles, non-DOM tests).\n */\n\n/** Width the preview is laid out at before being scaled into the tile. */\nconst DESIGN_WIDTH = 420\n/** Fallback scale used for the single frame before the tile is measured. */\nconst FALLBACK_SCALE = 0.45\n\nfunction LiveRender({ node, studio }: { node: ComponentNode; studio: StudioRegistry }) {\n const ctx = useMemo(() => previewContext(studio), [studio])\n // Called inside a component render so the boundary catches sync throws too.\n return <>{renderNode(node, ctx)}</>\n}\n\nfunction ThumbnailFallback({ message }: { message: string }) {\n return (\n <div\n data-block-thumbnail-fallback\n className=\"flex h-full w-full items-start gap-1.5 p-2 text-[0.7rem] leading-snug text-amber-700 dark:text-amber-400\"\n >\n <AlertTriangle aria-hidden className=\"mt-px size-3 shrink-0\" />\n <span className=\"line-clamp-3\">{message}</span>\n </div>\n )\n}\n\ninterface BoundaryState {\n error: Error | null\n}\n\n/** Tile-local error boundary: one broken fixture never takes down the panel. */\nclass ThumbnailErrorBoundary extends Component<{ children: ReactNode }, BoundaryState> {\n override state: BoundaryState = { error: null }\n\n static getDerivedStateFromError(error: Error): BoundaryState {\n return { error }\n }\n\n override render(): ReactNode {\n if (this.state.error) {\n return (\n <ThumbnailFallback\n message={translate('builder.blocksPanel.previewFailed', { error: this.state.error.message })}\n />\n )\n }\n return this.props.children\n }\n}\n\n/**\n * Curated preview-image tile (EC-409): renders `manifest.previewImage` in the\n * SAME 16/10 tile frame as {@link ScaledFixtureThumbnail}, object-fit cover.\n * The escape hatch for components whose live render needs data/context the\n * tile can't have — fixtures stay the primary curation lever. `alt` is the\n * component title (already-localized manifest data); the image is never a\n * fetch the engine performs, just an `<img>` with the consumer's URL string.\n */\nexport function PreviewImageThumbnail({\n src,\n alt,\n className,\n}: {\n /** The manifest's `previewImage` URL string, passed through verbatim. */\n src: string\n /** The component title (localized manifest data — no new catalog string). */\n alt: string\n className?: string\n}) {\n return (\n <div\n data-block-thumbnail\n data-block-thumbnail-image\n className={cn(\n 'relative w-full overflow-hidden rounded-md border border-border bg-background',\n className,\n )}\n style={{ aspectRatio: '16 / 10' }}\n >\n {/* Deliberately NOT loading=\"lazy\": native lazy images need rendering\n frames to trigger (occluded/headless windows never load them — the\n same hazard the live thumbnail's safety-net timer covers), and a\n curated preview is a small asset on a handful of tiles. */}\n <img\n src={src}\n alt={alt}\n // The surrounding tile is the drag source; a natively draggable <img>\n // would hijack the tile drag.\n draggable={false}\n className=\"pointer-events-none h-full w-full select-none object-cover\"\n />\n </div>\n )\n}\n\nexport function ScaledFixtureThumbnail({\n node,\n studio,\n designWidth = DESIGN_WIDTH,\n eager = false,\n className,\n}: {\n /** The preview subtree to render live (deterministic ids). */\n node: ComponentNode\n /**\n * Registry the preview renders against. Defaults to the active project's live\n * registry (`getStudioRegistry()`) — the blocks panel's tiles and their\n * thumbnails share one registry. Pass an explicit registry to render against a\n * specific component set (deterministic previews/tests).\n */\n studio?: StudioRegistry\n /** Layout width of the unscaled preview, in CSS pixels. */\n designWidth?: number\n /** Mount the live render immediately instead of waiting for visibility. */\n eager?: boolean\n className?: string\n}) {\n const registry = studio ?? getStudioRegistry()\n const ref = useRef<HTMLDivElement>(null)\n const [inView, setInView] = useState(eager)\n const [scale, setScale] = useState<number | null>(null)\n\n // Lazy mount: render the live preview only once the tile scrolls into view.\n // IntersectionObserver always delivers an initial report (intersecting or\n // not) within a rendering frame — when no report arrives at all the page\n // isn't producing frames (occluded/headless automation windows), so a\n // safety-net timer mounts the preview rather than leaving tiles blank.\n // Frame-producing browsers report long before the timer and keep laziness.\n useEffect(() => {\n if (inView) return\n const el = ref.current\n if (!el || typeof IntersectionObserver === 'undefined') {\n setInView(true)\n return\n }\n let reported = false\n const observer = new IntersectionObserver((entries) => {\n reported = true\n if (entries.some((entry) => entry.isIntersecting)) setInView(true)\n })\n observer.observe(el)\n const fallback = setTimeout(() => {\n if (!reported) setInView(true)\n }, 400)\n return () => {\n observer.disconnect()\n clearTimeout(fallback)\n }\n }, [inView])\n\n // Fit-to-tile scale, kept in sync with the tile's measured width.\n useEffect(() => {\n const el = ref.current\n if (!el) return\n const update = () => {\n const width = el.clientWidth\n if (width > 0) setScale(width / designWidth)\n }\n update()\n if (typeof ResizeObserver === 'undefined') return\n const observer = new ResizeObserver(update)\n observer.observe(el)\n return () => observer.disconnect()\n }, [designWidth])\n\n return (\n <div\n ref={ref}\n aria-hidden\n data-block-thumbnail\n className={cn(\n 'relative w-full overflow-hidden rounded-md border border-border bg-background',\n className,\n )}\n style={{ aspectRatio: '16 / 10' }}\n >\n {inView ? (\n <ThumbnailErrorBoundary>\n <div\n className=\"pointer-events-none absolute left-0 top-0 origin-top-left select-none p-3\"\n style={{ width: designWidth, transform: `scale(${scale ?? FALLBACK_SCALE})` }}\n >\n <LiveRender node={node} studio={registry} />\n </div>\n </ThumbnailErrorBoundary>\n ) : null}\n </div>\n )\n}\n"]}
|