@elytracms/studio 0.0.65 → 0.0.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/components/account/self-account-view-source.js +9 -2
  2. package/dist/components/account/self-account-view-source.js.map +1 -1
  3. package/dist/components/app-views/default-modules.d.ts +28 -1
  4. package/dist/components/app-views/default-modules.js +47 -2
  5. package/dist/components/app-views/default-modules.js.map +1 -1
  6. package/dist/components/auth-context.js +32 -1
  7. package/dist/components/auth-context.js.map +1 -1
  8. package/dist/components/builder/blocks-model.js +4 -2
  9. package/dist/components/builder/blocks-model.js.map +1 -1
  10. package/dist/components/builder/canvas-dnd.js +4 -2
  11. package/dist/components/builder/canvas-dnd.js.map +1 -1
  12. package/dist/components/builder/insert-context.js +15 -6
  13. package/dist/components/builder/insert-context.js.map +1 -1
  14. package/dist/components/builder/page-tree.js +4 -2
  15. package/dist/components/builder/page-tree.js.map +1 -1
  16. package/dist/components/builder/property-inspector.js +2 -1
  17. package/dist/components/builder/property-inspector.js.map +1 -1
  18. package/dist/components/cms/asset-field.js +21 -5
  19. package/dist/components/cms/asset-field.js.map +1 -1
  20. package/dist/components/cms/bulk-actions.d.ts +22 -3
  21. package/dist/components/cms/bulk-actions.js +27 -3
  22. package/dist/components/cms/bulk-actions.js.map +1 -1
  23. package/dist/components/cms/cms-store.js +19 -0
  24. package/dist/components/cms/cms-store.js.map +1 -1
  25. package/dist/components/cms/detail-editor.js +18 -19
  26. package/dist/components/cms/detail-editor.js.map +1 -1
  27. package/dist/components/cms/prop-fields.js +23 -16
  28. package/dist/components/cms/prop-fields.js.map +1 -1
  29. package/dist/components/cms/publish-clean.d.ts +21 -0
  30. package/dist/components/cms/publish-clean.js +30 -0
  31. package/dist/components/cms/publish-clean.js.map +1 -0
  32. package/dist/components/cms/use-live-url.d.ts +24 -1
  33. package/dist/components/cms/use-live-url.js +26 -4
  34. package/dist/components/cms/use-live-url.js.map +1 -1
  35. package/dist/components/cms/use-publish-controller.js +8 -6
  36. package/dist/components/cms/use-publish-controller.js.map +1 -1
  37. package/dist/components/collection-view/cms-collection-view-source.js +15 -7
  38. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  39. package/dist/lib/auth/auth-adapter.d.ts +22 -0
  40. package/dist/lib/auth/auth-adapter.js.map +1 -1
  41. package/dist/lib/auth/convex-auth-adapter.d.ts +42 -1
  42. package/dist/lib/auth/convex-auth-adapter.js +69 -0
  43. package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
  44. package/dist/lib/auth/dev-auto-login-adapter.d.ts +2 -1
  45. package/dist/lib/auth/dev-auto-login-adapter.js +6 -0
  46. package/dist/lib/auth/dev-auto-login-adapter.js.map +1 -1
  47. package/dist/lib/auth/index.d.ts +1 -1
  48. package/dist/lib/auth/index.js.map +1 -1
  49. package/dist/lib/auth/local-auth-adapter.d.ts +2 -1
  50. package/dist/lib/auth/local-auth-adapter.js +7 -0
  51. package/dist/lib/auth/local-auth-adapter.js.map +1 -1
  52. package/dist/lib/auth-errors.d.ts +45 -3
  53. package/dist/lib/auth-errors.js +46 -5
  54. package/dist/lib/auth-errors.js.map +1 -1
  55. package/dist/lib/clipboard/paste-target.js +5 -2
  56. package/dist/lib/clipboard/paste-target.js.map +1 -1
  57. package/dist/lib/dnd/drop-rules.d.ts +13 -0
  58. package/dist/lib/dnd/drop-rules.js +15 -0
  59. package/dist/lib/dnd/drop-rules.js.map +1 -1
  60. package/dist/lib/i18n/messages.de.js +3 -0
  61. package/dist/lib/i18n/messages.de.js.map +1 -1
  62. package/dist/lib/i18n/messages.en.d.ts +2 -0
  63. package/dist/lib/i18n/messages.en.js +8 -0
  64. package/dist/lib/i18n/messages.en.js.map +1 -1
  65. package/dist/lib/persistence/index.js +6 -3
  66. package/dist/lib/persistence/index.js.map +1 -1
  67. package/dist/lib/session-expiry.d.ts +19 -6
  68. package/dist/lib/session-expiry.js +13 -1
  69. package/dist/lib/session-expiry.js.map +1 -1
  70. package/dist/lib/session-revalidation.d.ts +81 -0
  71. package/dist/lib/session-revalidation.js +94 -0
  72. package/dist/lib/session-revalidation.js.map +1 -0
  73. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import type { ChangePasswordResult, SignInResult } from './auth/auth-adapter.js';
1
+ import type { ChangePasswordResult, SignInResult, UpdateProfileResult } from './auth/auth-adapter.js';
2
2
  import type { OperationError } from './operations/index.js';
3
3
  /**
4
4
  * Does this operation failure mean "the session is no longer valid"?
@@ -7,8 +7,17 @@ import type { OperationError } from './operations/index.js';
7
7
  * studio-level failures (`validation`, `unauthorized` from the role hook,
8
8
  * `not-found`, `conflict`) are never session expiry. Within those, detect the
9
9
  * Convex unauthenticated shape: an embedded `{"code":"Unauthenticated"}`
10
- * payload, the bare `Unauthenticated` server-error marker, or the OIDC claim
11
- * message it ships with.
10
+ * payload, or the SAME positive-rejection markers the EC-415 probe classifier
11
+ * ({@link isUnauthenticatedTransportText}, `lib/auth/convex-auth-adapter.ts`)
12
+ * pins — the bare `Unauthenticated` server-error marker, the OIDC claim
13
+ * message it ships with, and (EC-427) `InvalidAuthHeader`: the backend
14
+ * positively rejecting a malformed/unverifiable token, not just an
15
+ * expired-claims one. Reusing that one exported classifier (rather than a
16
+ * second copy of the regex ladder here) is the drift guard — the probe and
17
+ * this reactive predicate can never recognize a different set of real 401
18
+ * bodies (`lib/auth-errors.test.ts` proves agreement over every pinned
19
+ * fixture). `lib/auth/**` itself is untouched: the classifier was already
20
+ * exported for this reuse.
12
21
  */
13
22
  export declare function isSessionExpiredError(error: OperationError): boolean;
14
23
  type SignInFailure = Extract<SignInResult, {
@@ -69,4 +78,37 @@ export declare function changePasswordErrorMessageKey(failure: {
69
78
  reason: string;
70
79
  message: string;
71
80
  }): string | null;
81
+ type UpdateProfileFailure = Extract<UpdateProfileResult, {
82
+ ok: false;
83
+ }>;
84
+ /**
85
+ * Engine-owned update-profile-failure → message-catalog-key mapping (EC-416) —
86
+ * the {@link SIGN_IN_ERROR_KEYS} / {@link CHANGE_PASSWORD_ERROR_KEYS} design
87
+ * extended to the account identity form (`self-account-view-source.ts`'s
88
+ * `save`, which calls `AuthAdapter.updateProfile`). The reasons are the
89
+ * {@link UpdateProfileResult} failure union. Unlike change-password,
90
+ * `updateProfile` is NOT richly classified: the `users:updateProfile` Convex
91
+ * mutation never throws a coded `ConvexError`, so `ConvexAuthAdapter` collapses
92
+ * every failure (missing `users:me` row, thrown mutation error, network
93
+ * failure) into one generic `unavailable` message — there is no
94
+ * `classifyUpdateProfileError` to extend, and adding one would mean editing the
95
+ * no-touch `lib/auth/**` mutation to throw coded errors, which is out of scope
96
+ * here.
97
+ *
98
+ * `not-authenticated` reuses `account.password.error.notSignedIn` rather than
99
+ * a new key: `ConvexAuthAdapter.updateProfile` and `.changePassword` both
100
+ * return the byte-identical constant `'You are not signed in.'` for a missing
101
+ * token, so a second key would only duplicate the same English (and German)
102
+ * copy under a different name.
103
+ */
104
+ export declare const UPDATE_PROFILE_ERROR_KEYS: Record<UpdateProfileFailure['reason'], readonly string[]>;
105
+ /**
106
+ * Resolve a failed {@link UpdateProfileResult} to its message-catalog key, or
107
+ * `null` when the failure is not an engine-known shape (render
108
+ * `result.message` raw — the honest fallback).
109
+ */
110
+ export declare function updateProfileErrorMessageKey(failure: {
111
+ reason: string;
112
+ message: string;
113
+ }): string | null;
72
114
  export {};
@@ -1,3 +1,4 @@
1
+ import { isUnauthenticatedTransportText } from './auth/convex-auth-adapter.js';
1
2
  import { EN_MESSAGES } from './i18n/messages.en.js';
2
3
  /**
3
4
  * Session-expiry detection + plain-language mapping (EC-177 gap 9).
@@ -48,17 +49,24 @@ function embeddedJsonCode(message) {
48
49
  * studio-level failures (`validation`, `unauthorized` from the role hook,
49
50
  * `not-found`, `conflict`) are never session expiry. Within those, detect the
50
51
  * Convex unauthenticated shape: an embedded `{"code":"Unauthenticated"}`
51
- * payload, the bare `Unauthenticated` server-error marker, or the OIDC claim
52
- * message it ships with.
52
+ * payload, or the SAME positive-rejection markers the EC-415 probe classifier
53
+ * ({@link isUnauthenticatedTransportText}, `lib/auth/convex-auth-adapter.ts`)
54
+ * pins — the bare `Unauthenticated` server-error marker, the OIDC claim
55
+ * message it ships with, and (EC-427) `InvalidAuthHeader`: the backend
56
+ * positively rejecting a malformed/unverifiable token, not just an
57
+ * expired-claims one. Reusing that one exported classifier (rather than a
58
+ * second copy of the regex ladder here) is the drift guard — the probe and
59
+ * this reactive predicate can never recognize a different set of real 401
60
+ * bodies (`lib/auth-errors.test.ts` proves agreement over every pinned
61
+ * fixture). `lib/auth/**` itself is untouched: the classifier was already
62
+ * exported for this reuse.
53
63
  */
54
64
  export function isSessionExpiredError(error) {
55
65
  if (error.code !== 'unknown' && error.code !== 'backend-unavailable')
56
66
  return false;
57
67
  if (embeddedJsonCode(error.message) === 'Unauthenticated')
58
68
  return true;
59
- if (/\bUnauthenticated\b/.test(error.message))
60
- return true;
61
- return /could not verify oidc token/i.test(error.message);
69
+ return isUnauthenticatedTransportText(error.message);
62
70
  }
63
71
  /**
64
72
  * Engine-owned sign-in-failure → message-catalog-key mapping (EC-397). The
@@ -146,6 +154,39 @@ export const CHANGE_PASSWORD_ERROR_KEYS = {
146
154
  export function changePasswordErrorMessageKey(failure) {
147
155
  return errorMessageKey(CHANGE_PASSWORD_ERROR_KEYS, failure);
148
156
  }
157
+ /**
158
+ * Engine-owned update-profile-failure → message-catalog-key mapping (EC-416) —
159
+ * the {@link SIGN_IN_ERROR_KEYS} / {@link CHANGE_PASSWORD_ERROR_KEYS} design
160
+ * extended to the account identity form (`self-account-view-source.ts`'s
161
+ * `save`, which calls `AuthAdapter.updateProfile`). The reasons are the
162
+ * {@link UpdateProfileResult} failure union. Unlike change-password,
163
+ * `updateProfile` is NOT richly classified: the `users:updateProfile` Convex
164
+ * mutation never throws a coded `ConvexError`, so `ConvexAuthAdapter` collapses
165
+ * every failure (missing `users:me` row, thrown mutation error, network
166
+ * failure) into one generic `unavailable` message — there is no
167
+ * `classifyUpdateProfileError` to extend, and adding one would mean editing the
168
+ * no-touch `lib/auth/**` mutation to throw coded errors, which is out of scope
169
+ * here.
170
+ *
171
+ * `not-authenticated` reuses `account.password.error.notSignedIn` rather than
172
+ * a new key: `ConvexAuthAdapter.updateProfile` and `.changePassword` both
173
+ * return the byte-identical constant `'You are not signed in.'` for a missing
174
+ * token, so a second key would only duplicate the same English (and German)
175
+ * copy under a different name.
176
+ */
177
+ export const UPDATE_PROFILE_ERROR_KEYS = {
178
+ 'not-authenticated': ['account.password.error.notSignedIn'],
179
+ unavailable: ['account.profile.error.unavailable'],
180
+ unsupported: ['account.profile.error.playgroundUnsupported'],
181
+ };
182
+ /**
183
+ * Resolve a failed {@link UpdateProfileResult} to its message-catalog key, or
184
+ * `null` when the failure is not an engine-known shape (render
185
+ * `result.message` raw — the honest fallback).
186
+ */
187
+ export function updateProfileErrorMessageKey(failure) {
188
+ return errorMessageKey(UPDATE_PROFILE_ERROR_KEYS, failure);
189
+ }
149
190
  // `signInHref` (the banner's navigating "Sign in again" action) was retired by
150
191
  // EC-403: re-auth happens in place via the blocking overlay — a navigation
151
192
  // would destroy exactly the unsaved editor state the treatment protects.
@@ -1 +1 @@
1
- {"version":3,"file":"auth-errors.js","sourceRoot":"","sources":["../../src/lib/auth-errors.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ChangePasswordResult, SignInResult } from './auth/auth-adapter'\nimport { EN_MESSAGES } from './i18n/messages.en'\nimport type { OperationError } from './operations'\n\n/**\n * Session-expiry detection + plain-language mapping (EC-177 gap 9).\n *\n * An expired Convex session surfaces as a raw backend blob thrown by the\n * transport — e.g. `{\"code\":\"Unauthenticated\",\"message\":\"Could not verify\n * OIDC token claim…\"}` — which `toOperationError` can only normalize to an\n * `unknown`/`backend-unavailable` `OperationError` whose message *is* that\n * blob. Rendering it verbatim is editor-hostile, and the real problem (edits\n * silently stop persisting) is invisible.\n *\n * This module is the one chokepoint that recognizes that shape on the\n * structured `OperationError` (the seam every studio failure already flows\n * through), so UI surfaces never string-match raw messages themselves. Since\n * EC-403 an expired session is an app-level state: `lib/session-expiry.ts`\n * runs this predicate over every failed operation on the lifecycle bus and\n * raises the blocking in-place re-auth overlay (the\n * `error.sessionExpiredTitle` / `error.sessionExpired` catalog keys) — the\n * shared banner only uses it to route expiry AWAY from per-surface rendering.\n */\n\n/**\n * Extract the `code` of a JSON object embedded in a transport error message.\n * Convex auth failures carry `{\"code\":\"Unauthenticated\", …}`, sometimes\n * wrapped in client prefixes (`[CONVEX M(graph:save)] … Server Error: {…}`).\n */\nfunction embeddedJsonCode(message: string): string | undefined {\n const start = message.indexOf('{')\n const end = message.lastIndexOf('}')\n if (start === -1 || end <= start) return undefined\n try {\n const parsed: unknown = JSON.parse(message.slice(start, end + 1))\n if (parsed !== null && typeof parsed === 'object') {\n const code = (parsed as { code?: unknown }).code\n if (typeof code === 'string') return code\n }\n } catch {\n // Not a JSON payload — fall through to the marker checks.\n }\n return undefined\n}\n\n/**\n * Does this operation failure mean \"the session is no longer valid\"?\n *\n * Only the transport-shaped codes can carry the backend auth blob: structured\n * studio-level failures (`validation`, `unauthorized` from the role hook,\n * `not-found`, `conflict`) are never session expiry. Within those, detect the\n * Convex unauthenticated shape: an embedded `{\"code\":\"Unauthenticated\"}`\n * payload, the bare `Unauthenticated` server-error marker, or the OIDC claim\n * message it ships with.\n */\nexport function isSessionExpiredError(error: OperationError): boolean {\n if (error.code !== 'unknown' && error.code !== 'backend-unavailable') return false\n if (embeddedJsonCode(error.message) === 'Unauthenticated') return true\n if (/\\bUnauthenticated\\b/.test(error.message)) return true\n return /could not verify oidc token/i.test(error.message)\n}\n\ntype SignInFailure = Extract<SignInResult, { ok: false }>\n\n/**\n * Engine-owned sign-in-failure → message-catalog-key mapping (EC-397). The\n * auth adapters (no-touch `lib/auth/**`) return plain-English `message`\n * constants on the EC-362/EC-363 classified reasons; this table names the\n * catalog key that carries each of those messages, so the sign-in screen\n * localizes failures through the SAME catalog as the rest of the chrome\n * (a German catalog covers them with no consumer-side mapper — the\n * pre-EC-375 Naturkinder `authErrorMessage` is retired by this seam).\n *\n * A reason can carry several distinct engine messages (`unavailable` spans\n * the rate-limit, the honest server-error generic, and the playground\n * sign-up refusal), so each reason lists its candidate keys and a failure is\n * recognized by **exact EN-message match** against the catalog entry. That\n * makes English rendering byte-identical by construction and keeps the\n * fallback honest: any unrecognized message (a future reason, a reworded\n * adapter constant, a non-engine adapter) falls through to the raw\n * `result.message` — never a mistranslated or stale mapping.\n * `lib/auth-errors.test.ts` runs the real classifier over every branch to\n * pin that adapter copy and catalog never drift apart silently.\n */\nexport const SIGN_IN_ERROR_KEYS: Record<SignInFailure['reason'], readonly string[]> = {\n 'invalid-credentials': [\n 'auth.signIn.error.invalidCredentials',\n 'auth.signIn.error.invalidCredentialsSignUp',\n ],\n 'account-exists': ['auth.signIn.error.accountExists'],\n 'deployment-misconfigured': ['auth.signIn.error.deploymentMisconfigured'],\n 'not-invited': ['auth.signIn.error.notInvited'],\n unavailable: [\n 'auth.signIn.error.tooManyAttempts',\n 'auth.signIn.error.serverError',\n 'auth.signIn.error.serverErrorSignUp',\n 'auth.signIn.error.unavailable',\n 'auth.signIn.error.playgroundSignUp',\n ],\n}\n\n/**\n * Resolve a failure `{ reason, message }` against a reason → candidate-keys\n * table by **exact EN-message match** (the EC-397 recognition rule): the\n * matched key's catalog entry carries the same English, so EN rendering stays\n * byte-identical, and anything unrecognized returns `null` — the caller\n * renders the raw message, never a stale mapping.\n */\nfunction errorMessageKey(\n table: Record<string, readonly string[] | undefined>,\n failure: { reason: string; message: string },\n): string | null {\n const candidates = table[failure.reason]\n return candidates?.find((key) => EN_MESSAGES[key]?.message === failure.message) ?? null\n}\n\n/**\n * Resolve a failed {@link SignInResult} to its message-catalog key, or `null`\n * when the failure is not an engine-known shape (render `result.message` raw\n * — the honest fallback). Pure; accepts the failure structurally so callers\n * and tests can probe unknown future reasons without casts.\n */\nexport function signInErrorMessageKey(failure: {\n reason: string\n message: string\n}): string | null {\n return errorMessageKey(SIGN_IN_ERROR_KEYS, failure)\n}\n\ntype ChangePasswordFailure = Extract<ChangePasswordResult, { ok: false }>\n\n/**\n * Engine-owned change-password-failure → message-catalog-key mapping (EC-412)\n * — the {@link SIGN_IN_ERROR_KEYS} design extended to the account credential\n * panel. The reasons are the {@link ChangePasswordResult} failure union;\n * `unavailable` again spans several deliberate messages (the\n * deployment-misconfigured remedy, the EC-377 rate limit, and the honest\n * server-error generic), so recognition is by reason + exact EN-message match\n * and every unrecognized shape falls through to the raw `result.message`.\n * `lib/auth-errors.test.ts` runs the real `classifyChangePasswordError` over\n * every branch to pin adapter copy and catalog together.\n */\nexport const CHANGE_PASSWORD_ERROR_KEYS: Record<\n ChangePasswordFailure['reason'],\n readonly string[]\n> = {\n 'invalid-credentials': ['account.password.error.currentIncorrect'],\n 'weak-password': ['account.password.error.weakPassword'],\n 'not-authenticated': ['account.password.error.notSignedIn'],\n unavailable: [\n 'account.password.error.deploymentMisconfigured',\n 'account.password.error.tooManyAttempts',\n 'account.password.error.serverError',\n ],\n unsupported: ['account.password.error.playgroundUnsupported'],\n}\n\n/**\n * Resolve a failed {@link ChangePasswordResult} to its message-catalog key, or\n * `null` when the failure is not an engine-known shape (render\n * `result.message` raw — the honest fallback).\n */\nexport function changePasswordErrorMessageKey(failure: {\n reason: string\n message: string\n}): string | null {\n return errorMessageKey(CHANGE_PASSWORD_ERROR_KEYS, failure)\n}\n\n// `signInHref` (the banner's navigating \"Sign in again\" action) was retired by\n// EC-403: re-auth happens in place via the blocking overlay — a navigation\n// would destroy exactly the unsaved editor state the treatment protects.\n"]}
1
+ {"version":3,"file":"auth-errors.js","sourceRoot":"","sources":["../../src/lib/auth-errors.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ChangePasswordResult, SignInResult, UpdateProfileResult } from './auth/auth-adapter'\nimport { isUnauthenticatedTransportText } from './auth/convex-auth-adapter'\nimport { EN_MESSAGES } from './i18n/messages.en'\nimport type { OperationError } from './operations'\n\n/**\n * Session-expiry detection + plain-language mapping (EC-177 gap 9).\n *\n * An expired Convex session surfaces as a raw backend blob thrown by the\n * transport — e.g. `{\"code\":\"Unauthenticated\",\"message\":\"Could not verify\n * OIDC token claim…\"}` — which `toOperationError` can only normalize to an\n * `unknown`/`backend-unavailable` `OperationError` whose message *is* that\n * blob. Rendering it verbatim is editor-hostile, and the real problem (edits\n * silently stop persisting) is invisible.\n *\n * This module is the one chokepoint that recognizes that shape on the\n * structured `OperationError` (the seam every studio failure already flows\n * through), so UI surfaces never string-match raw messages themselves. Since\n * EC-403 an expired session is an app-level state: `lib/session-expiry.ts`\n * runs this predicate over every failed operation on the lifecycle bus and\n * raises the blocking in-place re-auth overlay (the\n * `error.sessionExpiredTitle` / `error.sessionExpired` catalog keys) — the\n * shared banner only uses it to route expiry AWAY from per-surface rendering.\n */\n\n/**\n * Extract the `code` of a JSON object embedded in a transport error message.\n * Convex auth failures carry `{\"code\":\"Unauthenticated\", …}`, sometimes\n * wrapped in client prefixes (`[CONVEX M(graph:save)] … Server Error: {…}`).\n */\nfunction embeddedJsonCode(message: string): string | undefined {\n const start = message.indexOf('{')\n const end = message.lastIndexOf('}')\n if (start === -1 || end <= start) return undefined\n try {\n const parsed: unknown = JSON.parse(message.slice(start, end + 1))\n if (parsed !== null && typeof parsed === 'object') {\n const code = (parsed as { code?: unknown }).code\n if (typeof code === 'string') return code\n }\n } catch {\n // Not a JSON payload — fall through to the marker checks.\n }\n return undefined\n}\n\n/**\n * Does this operation failure mean \"the session is no longer valid\"?\n *\n * Only the transport-shaped codes can carry the backend auth blob: structured\n * studio-level failures (`validation`, `unauthorized` from the role hook,\n * `not-found`, `conflict`) are never session expiry. Within those, detect the\n * Convex unauthenticated shape: an embedded `{\"code\":\"Unauthenticated\"}`\n * payload, or the SAME positive-rejection markers the EC-415 probe classifier\n * ({@link isUnauthenticatedTransportText}, `lib/auth/convex-auth-adapter.ts`)\n * pins — the bare `Unauthenticated` server-error marker, the OIDC claim\n * message it ships with, and (EC-427) `InvalidAuthHeader`: the backend\n * positively rejecting a malformed/unverifiable token, not just an\n * expired-claims one. Reusing that one exported classifier (rather than a\n * second copy of the regex ladder here) is the drift guard — the probe and\n * this reactive predicate can never recognize a different set of real 401\n * bodies (`lib/auth-errors.test.ts` proves agreement over every pinned\n * fixture). `lib/auth/**` itself is untouched: the classifier was already\n * exported for this reuse.\n */\nexport function isSessionExpiredError(error: OperationError): boolean {\n if (error.code !== 'unknown' && error.code !== 'backend-unavailable') return false\n if (embeddedJsonCode(error.message) === 'Unauthenticated') return true\n return isUnauthenticatedTransportText(error.message)\n}\n\ntype SignInFailure = Extract<SignInResult, { ok: false }>\n\n/**\n * Engine-owned sign-in-failure → message-catalog-key mapping (EC-397). The\n * auth adapters (no-touch `lib/auth/**`) return plain-English `message`\n * constants on the EC-362/EC-363 classified reasons; this table names the\n * catalog key that carries each of those messages, so the sign-in screen\n * localizes failures through the SAME catalog as the rest of the chrome\n * (a German catalog covers them with no consumer-side mapper — the\n * pre-EC-375 Naturkinder `authErrorMessage` is retired by this seam).\n *\n * A reason can carry several distinct engine messages (`unavailable` spans\n * the rate-limit, the honest server-error generic, and the playground\n * sign-up refusal), so each reason lists its candidate keys and a failure is\n * recognized by **exact EN-message match** against the catalog entry. That\n * makes English rendering byte-identical by construction and keeps the\n * fallback honest: any unrecognized message (a future reason, a reworded\n * adapter constant, a non-engine adapter) falls through to the raw\n * `result.message` — never a mistranslated or stale mapping.\n * `lib/auth-errors.test.ts` runs the real classifier over every branch to\n * pin that adapter copy and catalog never drift apart silently.\n */\nexport const SIGN_IN_ERROR_KEYS: Record<SignInFailure['reason'], readonly string[]> = {\n 'invalid-credentials': [\n 'auth.signIn.error.invalidCredentials',\n 'auth.signIn.error.invalidCredentialsSignUp',\n ],\n 'account-exists': ['auth.signIn.error.accountExists'],\n 'deployment-misconfigured': ['auth.signIn.error.deploymentMisconfigured'],\n 'not-invited': ['auth.signIn.error.notInvited'],\n unavailable: [\n 'auth.signIn.error.tooManyAttempts',\n 'auth.signIn.error.serverError',\n 'auth.signIn.error.serverErrorSignUp',\n 'auth.signIn.error.unavailable',\n 'auth.signIn.error.playgroundSignUp',\n ],\n}\n\n/**\n * Resolve a failure `{ reason, message }` against a reason → candidate-keys\n * table by **exact EN-message match** (the EC-397 recognition rule): the\n * matched key's catalog entry carries the same English, so EN rendering stays\n * byte-identical, and anything unrecognized returns `null` — the caller\n * renders the raw message, never a stale mapping.\n */\nfunction errorMessageKey(\n table: Record<string, readonly string[] | undefined>,\n failure: { reason: string; message: string },\n): string | null {\n const candidates = table[failure.reason]\n return candidates?.find((key) => EN_MESSAGES[key]?.message === failure.message) ?? null\n}\n\n/**\n * Resolve a failed {@link SignInResult} to its message-catalog key, or `null`\n * when the failure is not an engine-known shape (render `result.message` raw\n * — the honest fallback). Pure; accepts the failure structurally so callers\n * and tests can probe unknown future reasons without casts.\n */\nexport function signInErrorMessageKey(failure: {\n reason: string\n message: string\n}): string | null {\n return errorMessageKey(SIGN_IN_ERROR_KEYS, failure)\n}\n\ntype ChangePasswordFailure = Extract<ChangePasswordResult, { ok: false }>\n\n/**\n * Engine-owned change-password-failure → message-catalog-key mapping (EC-412)\n * — the {@link SIGN_IN_ERROR_KEYS} design extended to the account credential\n * panel. The reasons are the {@link ChangePasswordResult} failure union;\n * `unavailable` again spans several deliberate messages (the\n * deployment-misconfigured remedy, the EC-377 rate limit, and the honest\n * server-error generic), so recognition is by reason + exact EN-message match\n * and every unrecognized shape falls through to the raw `result.message`.\n * `lib/auth-errors.test.ts` runs the real `classifyChangePasswordError` over\n * every branch to pin adapter copy and catalog together.\n */\nexport const CHANGE_PASSWORD_ERROR_KEYS: Record<\n ChangePasswordFailure['reason'],\n readonly string[]\n> = {\n 'invalid-credentials': ['account.password.error.currentIncorrect'],\n 'weak-password': ['account.password.error.weakPassword'],\n 'not-authenticated': ['account.password.error.notSignedIn'],\n unavailable: [\n 'account.password.error.deploymentMisconfigured',\n 'account.password.error.tooManyAttempts',\n 'account.password.error.serverError',\n ],\n unsupported: ['account.password.error.playgroundUnsupported'],\n}\n\n/**\n * Resolve a failed {@link ChangePasswordResult} to its message-catalog key, or\n * `null` when the failure is not an engine-known shape (render\n * `result.message` raw — the honest fallback).\n */\nexport function changePasswordErrorMessageKey(failure: {\n reason: string\n message: string\n}): string | null {\n return errorMessageKey(CHANGE_PASSWORD_ERROR_KEYS, failure)\n}\n\ntype UpdateProfileFailure = Extract<UpdateProfileResult, { ok: false }>\n\n/**\n * Engine-owned update-profile-failure → message-catalog-key mapping (EC-416) —\n * the {@link SIGN_IN_ERROR_KEYS} / {@link CHANGE_PASSWORD_ERROR_KEYS} design\n * extended to the account identity form (`self-account-view-source.ts`'s\n * `save`, which calls `AuthAdapter.updateProfile`). The reasons are the\n * {@link UpdateProfileResult} failure union. Unlike change-password,\n * `updateProfile` is NOT richly classified: the `users:updateProfile` Convex\n * mutation never throws a coded `ConvexError`, so `ConvexAuthAdapter` collapses\n * every failure (missing `users:me` row, thrown mutation error, network\n * failure) into one generic `unavailable` message — there is no\n * `classifyUpdateProfileError` to extend, and adding one would mean editing the\n * no-touch `lib/auth/**` mutation to throw coded errors, which is out of scope\n * here.\n *\n * `not-authenticated` reuses `account.password.error.notSignedIn` rather than\n * a new key: `ConvexAuthAdapter.updateProfile` and `.changePassword` both\n * return the byte-identical constant `'You are not signed in.'` for a missing\n * token, so a second key would only duplicate the same English (and German)\n * copy under a different name.\n */\nexport const UPDATE_PROFILE_ERROR_KEYS: Record<UpdateProfileFailure['reason'], readonly string[]> = {\n 'not-authenticated': ['account.password.error.notSignedIn'],\n unavailable: ['account.profile.error.unavailable'],\n unsupported: ['account.profile.error.playgroundUnsupported'],\n}\n\n/**\n * Resolve a failed {@link UpdateProfileResult} to its message-catalog key, or\n * `null` when the failure is not an engine-known shape (render\n * `result.message` raw — the honest fallback).\n */\nexport function updateProfileErrorMessageKey(failure: {\n reason: string\n message: string\n}): string | null {\n return errorMessageKey(UPDATE_PROFILE_ERROR_KEYS, failure)\n}\n\n// `signInHref` (the banner's navigating \"Sign in again\" action) was retired by\n// EC-403: re-auth happens in place via the blocking overlay — a navigation\n// would destroy exactly the unsaved editor state the treatment protects.\n"]}
@@ -1,12 +1,15 @@
1
1
  import { getNode, getParentInfo } from '@elytracms/core/project-graph';
2
+ import { manifestRules, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules';
2
3
  import { shortId } from '@elytracms/studio/lib/persona/labels';
3
4
  export function resolvePasteTarget(args) {
4
5
  const { root, getManifest } = args;
5
6
  const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined;
6
7
  const node = anchor ?? root;
8
+ // EC-386: the shared slot rule (`primaryInsertSlot` over the merged
9
+ // `manifestRules` vocabulary) — a blocks-prop region (e.g. a Section's `body`)
10
+ // receives the paste exactly like the insert dialog and a DnD `into`.
7
11
  const manifest = getManifest(node.componentId);
8
- const childSlot = manifest?.slots.find((s) => s.name === 'children') ??
9
- manifest?.slots.find((s) => s.name !== 'outlet');
12
+ const childSlot = primaryInsertSlot(manifestRules(manifest));
10
13
  if (childSlot) {
11
14
  return {
12
15
  target: { parentId: node.id, slot: childSlot.name },
@@ -1 +1 @@
1
- {"version":3,"file":"paste-target.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-target.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAGtE,OAAO,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AAgB9D,MAAM,UAAU,kBAAkB,CAAC,IAKlC;IACC,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,IAAI,CAAA;IAClC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAChF,MAAM,IAAI,GAAG,MAAM,IAAI,IAAI,CAAA;IAE3B,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAC9C,MAAM,SAAS,GACb,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC;QAClD,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAA;IAClD,IAAI,SAAS,EAAE,CAAC;QACd,OAAO;YACL,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE;YACnD,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,SAAS,CAAC,IAAI,EAAE;SACrE,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAA;IACzC,IAAI,IAAI,EAAE,CAAC;QACT,OAAO;YACL,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE;YAC5E,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE;SACvE,CAAA;IACH,CAAC;IAED,8EAA8E;IAC9E,OAAO,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAA;AACtF,CAAC","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode, InsertTarget } from '@elytracms/core/project-graph'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Paste landing-spot resolution (EC-175) — the same placement rule the Insert\n * dialog uses (`insert-context.ts`): a pasted subtree lands *into*\n * the anchor node's child slot when it has one, else *after* the anchor as a\n * sibling, else appended to the page root. Pure and selection-driven so the\n * canvas, the page tree, and keyboard paste all share one rule.\n */\n\nexport interface PasteTarget {\n target: InsertTarget\n /** Human label of where the paste lands (notices/debugging). */\n destinationLabel: string\n}\n\nexport function resolvePasteTarget(args: {\n root: ComponentNode\n /** The anchor node (usually the selection); `null` falls back to the root. */\n anchorId: string | null\n getManifest: (componentId: string) => ComponentManifest | undefined\n}): PasteTarget {\n const { root, getManifest } = args\n const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined\n const node = anchor ?? root\n\n const manifest = getManifest(node.componentId)\n const childSlot =\n manifest?.slots.find((s) => s.name === 'children') ??\n manifest?.slots.find((s) => s.name !== 'outlet')\n if (childSlot) {\n return {\n target: { parentId: node.id, slot: childSlot.name },\n destinationLabel: `${shortId(node.componentId)} · ${childSlot.name}`,\n }\n }\n\n const info = getParentInfo(root, node.id)\n if (info) {\n return {\n target: { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,\n }\n }\n\n // Leaf root with no declared slots (degenerate graphs): append to `children`.\n return { target: { parentId: root.id, slot: 'children' }, destinationLabel: 'root' }\n}\n"]}
1
+ {"version":3,"file":"paste-target.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-target.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAGtE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAA;AACvF,OAAO,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AAgB9D,MAAM,UAAU,kBAAkB,CAAC,IAKlC;IACC,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,IAAI,CAAA;IAClC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAChF,MAAM,IAAI,GAAG,MAAM,IAAI,IAAI,CAAA;IAE3B,oEAAoE;IACpE,+EAA+E;IAC/E,sEAAsE;IACtE,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAC9C,MAAM,SAAS,GAAG,iBAAiB,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC5D,IAAI,SAAS,EAAE,CAAC;QACd,OAAO;YACL,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE;YACnD,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,SAAS,CAAC,IAAI,EAAE;SACrE,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAA;IACzC,IAAI,IAAI,EAAE,CAAC;QACT,OAAO;YACL,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE;YAC5E,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE;SACvE,CAAA;IACH,CAAC;IAED,8EAA8E;IAC9E,OAAO,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAA;AACtF,CAAC","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode, InsertTarget } from '@elytracms/core/project-graph'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { manifestRules, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Paste landing-spot resolution (EC-175) — the same placement rule the Insert\n * dialog uses (`insert-context.ts`): a pasted subtree lands *into*\n * the anchor node's child slot when it has one, else *after* the anchor as a\n * sibling, else appended to the page root. Pure and selection-driven so the\n * canvas, the page tree, and keyboard paste all share one rule.\n */\n\nexport interface PasteTarget {\n target: InsertTarget\n /** Human label of where the paste lands (notices/debugging). */\n destinationLabel: string\n}\n\nexport function resolvePasteTarget(args: {\n root: ComponentNode\n /** The anchor node (usually the selection); `null` falls back to the root. */\n anchorId: string | null\n getManifest: (componentId: string) => ComponentManifest | undefined\n}): PasteTarget {\n const { root, getManifest } = args\n const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined\n const node = anchor ?? root\n\n // EC-386: the shared slot rule (`primaryInsertSlot` over the merged\n // `manifestRules` vocabulary) — a blocks-prop region (e.g. a Section's `body`)\n // receives the paste exactly like the insert dialog and a DnD `into`.\n const manifest = getManifest(node.componentId)\n const childSlot = primaryInsertSlot(manifestRules(manifest))\n if (childSlot) {\n return {\n target: { parentId: node.id, slot: childSlot.name },\n destinationLabel: `${shortId(node.componentId)} · ${childSlot.name}`,\n }\n }\n\n const info = getParentInfo(root, node.id)\n if (info) {\n return {\n target: { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,\n }\n }\n\n // Leaf root with no declared slots (degenerate graphs): append to `children`.\n return { target: { parentId: root.id, slot: 'children' }, destinationLabel: 'root' }\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import type { ComponentNode, GridContainerOptions, InsertTarget } from '@elytracms/core/project-graph';
2
+ import type { ComponentManifest } from '@elytracms/core/component-registry';
2
3
  /**
3
4
  * Drag & drop eligibility rules (EC-160 + EC-162, vision §3.1) — pure
4
5
  * graph-level functions with **no DOM or UI dependencies**. The canvas and the
@@ -25,6 +26,18 @@ export interface ManifestRules {
25
26
  }
26
27
  /** Resolve a component id to its manifest slot rules (undefined = unknown component). */
27
28
  export type ManifestLookup = (componentId: string) => ManifestRules | undefined;
29
+ /**
30
+ * The canonical `ManifestRules` view of a full manifest (EC-386): the declared
31
+ * `slots` merged with the blocks-prop-derived slots (`manifestSlots`, EC-190
32
+ * "slots are blocks props" — a `blocks`-type prop IS a named child region whose
33
+ * subtrees live in `node.slots[propName]`, carrying the prop's `allow` leash).
34
+ * Every surface that feeds a real `ComponentManifest` into a {@link ManifestLookup}
35
+ * or {@link primaryInsertSlot} must adapt through this — resolving against raw
36
+ * `manifest.slots` silently degrades a `project.Section.body`-style region to a
37
+ * sibling insert. One derivation, so tree insert, DnD, paste, and the canvas
38
+ * affordances can never disagree about which regions exist or what they allow.
39
+ */
40
+ export declare function manifestRules(manifest: ComponentManifest | undefined): ManifestRules | undefined;
28
41
  /** What is being dragged. */
29
42
  export type DragSubject =
30
43
  /** An existing node of the current page — a completed drop is a `move`. */
@@ -1,4 +1,19 @@
1
1
  import { DEFAULT_SLOT, GRID_COMPONENT_ID, OUTLET_SLOT, STACK_COMPONENT_ID, getNode, getParentInfo, gridContainerNode, isDescendantOf, isStaticProp, removeNode, updateNode, } from '@elytracms/core/project-graph';
2
+ import { manifestSlots } from '@elytracms/core/component-registry';
3
+ /**
4
+ * The canonical `ManifestRules` view of a full manifest (EC-386): the declared
5
+ * `slots` merged with the blocks-prop-derived slots (`manifestSlots`, EC-190
6
+ * "slots are blocks props" — a `blocks`-type prop IS a named child region whose
7
+ * subtrees live in `node.slots[propName]`, carrying the prop's `allow` leash).
8
+ * Every surface that feeds a real `ComponentManifest` into a {@link ManifestLookup}
9
+ * or {@link primaryInsertSlot} must adapt through this — resolving against raw
10
+ * `manifest.slots` silently degrades a `project.Section.body`-style region to a
11
+ * sibling insert. One derivation, so tree insert, DnD, paste, and the canvas
12
+ * affordances can never disagree about which regions exist or what they allow.
13
+ */
14
+ export function manifestRules(manifest) {
15
+ return manifest ? { slots: manifestSlots(manifest) } : undefined;
16
+ }
2
17
  function rejected(code, reason) {
3
18
  return { eligible: false, code, reason };
4
19
  }
@@ -1 +1 @@
1
- {"version":3,"file":"drop-rules.js","sourceRoot":"","sources":["../../../src/lib/dnd/drop-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DEFAULT_SLOT,\n GRID_COMPONENT_ID,\n OUTLET_SLOT,\n STACK_COMPONENT_ID,\n getNode,\n getParentInfo,\n gridContainerNode,\n isDescendantOf,\n isStaticProp,\n removeNode,\n updateNode,\n} from '@elytracms/core/project-graph'\nimport type { ComponentNode, GridContainerOptions, InsertTarget } from '@elytracms/core/project-graph'\n\n/**\n * Drag & drop eligibility rules (EC-160 + EC-162, vision §3.1) — pure\n * graph-level functions with **no DOM or UI dependencies**. The canvas and the\n * page tree both resolve drops through `resolveDrop`, so the two surfaces can\n * never disagree about what a manifest slot allow-list permits, which children\n * a component accepts, or whether a move would create a cycle.\n *\n * The drop-zone model is position-based: `into` a slot, `before`/`after` a\n * sibling along the parent's flow axis, and the EC-162 gesture positions\n * `left`/`right` — dropping *beside* an element wraps both in a two-column\n * `Grid` container (see `applyWrap`).\n */\n\n// ---------------------------------------------------------------------------\n// Inputs\n// ---------------------------------------------------------------------------\n\n/**\n * The manifest slice the rules evaluate. Structural (not the full\n * `ComponentManifest`) so tests and future callers can feed minimal lookups.\n */\nexport interface SlotRule {\n name: string\n /** Restrict which component ids may be inserted (omit to allow any). */\n allow?: string[]\n}\n\nexport interface ManifestRules {\n slots: SlotRule[]\n}\n\n/** Resolve a component id to its manifest slot rules (undefined = unknown component). */\nexport type ManifestLookup = (componentId: string) => ManifestRules | undefined\n\n/** What is being dragged. */\nexport type DragSubject =\n /** An existing node of the current page — a completed drop is a `move`. */\n | { kind: 'node'; nodeId: string; componentId: string }\n /** A new subtree (blocks-panel tile) — a completed drop is an `insert`. */\n | { kind: 'component'; componentId: string }\n\n/**\n * Where, relative to a reference node, the drop would land. `into`, `before`\n * and `after` are the EC-160 positions; `left`/`right` are the EC-162 gesture\n * positions — \"beside\" the reference, wrapping both elements in a two-column\n * `Grid` container.\n */\nexport type DropPosition = 'into' | 'before' | 'after' | 'left' | 'right'\n\n/** The side of the reference an EC-162 wrap gesture drops on. */\nexport type WrapSide = 'left' | 'right'\n\nexport interface DropLocation {\n /** The reference node id (the hovered node/row). */\n nodeId: string\n position: DropPosition\n}\n\n// ---------------------------------------------------------------------------\n// Outcomes\n// ---------------------------------------------------------------------------\n\nexport type DropRejectionCode =\n /** The reference node does not exist in the tree. */\n | 'missing-target'\n /** `into` on a component that declares no insertable slot. */\n | 'no-slot'\n /** The target slot's manifest allow-list rejects the dragged component. */\n | 'not-allowed'\n /** Moving a node into itself or its own subtree (cycle). */\n | 'own-subtree'\n /** The page root cannot be moved, and nothing can drop beside it. */\n | 'root'\n /** Structurally valid but changes nothing (dropping a node onto its own position). */\n | 'no-op'\n\nexport interface DropRejection {\n eligible: false\n code: DropRejectionCode\n reason: string\n}\n\n/** An eligible drop: exactly one builder action (move, insert, or wrap) commits it. */\nexport type DropPlan =\n | { eligible: true; kind: 'move'; nodeId: string; target: InsertTarget }\n | { eligible: true; kind: 'insert'; target: InsertTarget }\n /**\n * EC-162 wrap gesture: the reference node and the dragged subject become the\n * two columns of a new `Grid` container, created atomically (`applyWrap`).\n * `nodeId` is present when the subject is an existing node (a move-wrap);\n * absent for blocks-panel components (an insert-wrap via `buildNode`).\n */\n | { eligible: true; kind: 'wrap'; referenceId: string; side: WrapSide; nodeId?: string }\n\nexport type DropResolution = DropPlan | DropRejection\n\nfunction rejected(code: DropRejectionCode, reason: string): DropRejection {\n return { eligible: false, code, reason }\n}\n\n// ---------------------------------------------------------------------------\n// Slot rules\n// ---------------------------------------------------------------------------\n\n/**\n * The slot an `into` drop inserts into: the default `children` slot when\n * declared, else the first authored slot that is not the layout `outlet`.\n * Mirrors the shell's insert-context resolution so click-insert and\n * drag-insert land in the same place.\n */\nexport function primaryInsertSlot(manifest: ManifestRules | undefined): SlotRule | undefined {\n if (!manifest) return undefined\n return (\n manifest.slots.find((s) => s.name === DEFAULT_SLOT) ??\n manifest.slots.find((s) => s.name !== OUTLET_SLOT)\n )\n}\n\n/** True when a component can receive `into` drops at all (declares an insertable slot). */\nexport function canNestInto(getManifest: ManifestLookup, componentId: string): boolean {\n return primaryInsertSlot(getManifest(componentId)) !== undefined\n}\n\n/** True when a slot's allow-list (if any) permits the component id. */\nexport function slotAllows(slot: SlotRule | undefined, componentId: string): boolean {\n if (!slot?.allow) return true\n return slot.allow.includes(componentId)\n}\n\n// ---------------------------------------------------------------------------\n// Resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a prospective drop into either an executable plan (one `move` or\n * `insert` builder action → one operations command → one undo step) or a\n * structured rejection the UI shows *during* the drag. Pure: same inputs,\n * same answer, on both surfaces.\n */\nexport function resolveDrop(args: {\n root: ComponentNode\n getManifest: ManifestLookup\n subject: DragSubject\n location: DropLocation\n}): DropResolution {\n const { root, getManifest, subject, location } = args\n\n if (subject.kind === 'node' && subject.nodeId === root.id) {\n return rejected('root', 'The page root cannot be moved.')\n }\n\n const reference = getNode(root, location.nodeId)\n if (!reference) {\n return rejected('missing-target', `Node \"${location.nodeId}\" is not on this page.`)\n }\n\n // EC-162: dropping beside the reference wraps both in a two-column Grid.\n if (location.position === 'left' || location.position === 'right') {\n return resolveWrap({ root, getManifest, subject, reference, side: location.position })\n }\n\n // Resolve the concrete insertion point: parent node, slot, index.\n let parentId: string\n let slotRule: SlotRule | undefined\n let slotName: string\n let index: number\n\n if (location.position === 'into') {\n const manifest = getManifest(reference.componentId)\n const slot = primaryInsertSlot(manifest)\n if (!slot) {\n return rejected('no-slot', `\"${reference.componentId}\" does not accept children.`)\n }\n parentId = reference.id\n slotRule = slot\n slotName = slot.name\n index = (reference.slots?.[slot.name] ?? []).length\n } else {\n const info = getParentInfo(root, reference.id)\n if (!info) {\n return rejected('root', 'Nothing can be dropped beside the page root.')\n }\n parentId = info.parent.id\n slotName = info.slot\n // Unknown parent manifests impose no allow-list; validation owns that issue.\n slotRule = getManifest(info.parent.componentId)?.slots.find((s) => s.name === info.slot)\n index = info.index + (location.position === 'after' ? 1 : 0)\n }\n\n if (!slotAllows(slotRule, subject.componentId)) {\n return rejected(\n 'not-allowed',\n `Slot \"${slotName}\" does not allow \"${subject.componentId}\".`,\n )\n }\n\n const target: InsertTarget = { parentId, slot: slotName, index }\n\n if (subject.kind === 'component') {\n return { eligible: true, kind: 'insert', target }\n }\n\n // Cycle prevention: a node can never land inside its own subtree.\n if (subject.nodeId === parentId || isDescendantOf(root, subject.nodeId, parentId)) {\n return rejected('own-subtree', 'A node cannot be dropped into its own subtree.')\n }\n\n // No-op detection: the final position (after same-slot removal compensation,\n // matching `moveNode`) equals the node's current position.\n const current = getParentInfo(root, subject.nodeId)\n if (current && current.parent.id === parentId && current.slot === slotName) {\n const finalIndex = index > current.index ? index - 1 : index\n if (finalIndex === current.index) {\n return rejected('no-op', 'Dropping here would not move the node.')\n }\n }\n\n return { eligible: true, kind: 'move', nodeId: subject.nodeId, target }\n}\n\n// ---------------------------------------------------------------------------\n// Wrap gesture (EC-162): drop beside → two-column Grid container\n// ---------------------------------------------------------------------------\n\n/**\n * The container a \"drop beside\" gesture creates. Canonical choice (EC-161\n * semantics): a `Grid` with two columns, **not** a horizontal `Stack` —\n * EC-161 gives `Grid` the canonical column vocabulary (`columns` is a\n * validated graph value the contextual inspector refines from 2 → n), while\n * a horizontal Stack expresses flow order, not column tracks. The values\n * mirror the Grid manifest defaults (`columns: 2`, `gap: '4'`), so the\n * gesture's graph is exactly what composing the same layout manually would\n * produce.\n */\nexport const WRAP_GRID_OPTIONS: GridContainerOptions = { columns: 2, gap: '4' }\n\n/** Eligibility for a `left`/`right` (wrap-beside) drop. */\nfunction resolveWrap(args: {\n root: ComponentNode\n getManifest: ManifestLookup\n subject: DragSubject\n reference: ComponentNode\n side: WrapSide\n}): DropResolution {\n const { root, getManifest, subject, reference, side } = args\n\n // The Grid takes the reference's place in its parent slot — so the\n // reference needs a parent (the root is the page anchor, never wrapped)\n // and that slot's allow-list must accept a Grid.\n const info = getParentInfo(root, reference.id)\n if (!info) {\n return rejected('root', 'The page root cannot be wrapped in a container.')\n }\n const parentSlotRule = getManifest(info.parent.componentId)?.slots.find(\n (s) => s.name === info.slot,\n )\n if (!slotAllows(parentSlotRule, GRID_COMPONENT_ID)) {\n return rejected('not-allowed', `Slot \"${info.slot}\" does not allow a Grid container.`)\n }\n\n // Both columns land in the Grid's children slot.\n const gridSlot = primaryInsertSlot(getManifest(GRID_COMPONENT_ID))\n for (const componentId of [reference.componentId, subject.componentId]) {\n if (!slotAllows(gridSlot, componentId)) {\n return rejected('not-allowed', `A Grid container does not allow \"${componentId}\".`)\n }\n }\n\n if (subject.kind === 'component') {\n return { eligible: true, kind: 'wrap', referenceId: reference.id, side }\n }\n if (subject.nodeId === reference.id) {\n return rejected('no-op', 'A node cannot be placed beside itself.')\n }\n // The reference must not live inside the dragged subtree (the subject is\n // lifted out first — taking the reference with it would orphan the wrap).\n if (isDescendantOf(root, subject.nodeId, reference.id)) {\n return rejected('own-subtree', 'A node cannot wrap an element of its own subtree.')\n }\n return { eligible: true, kind: 'wrap', referenceId: reference.id, side, nodeId: subject.nodeId }\n}\n\n/** What lands beside the reference: an existing node (moved) or a built subtree (inserted). */\nexport type WrapPartner =\n | { kind: 'node'; nodeId: string }\n | { kind: 'subtree'; node: ComponentNode }\n\nexport interface WrapResult {\n root: ComponentNode\n /** The id of the created Grid container. */\n containerId: string\n}\n\n/**\n * Execute a wrap plan as **one** structural edit: lift the partner out of the\n * tree (when it is an existing node) and replace the reference with\n * `gridContainerNode(containerId, WRAP_GRID_OPTIONS, [left, right])` in a\n * single pass. The caller commits the result as one builder action → one\n * operations command → one undo step; EC-161 normalization then runs on the\n * *committed* tree, so the two-child container can never be observed (or\n * persisted) in a half-built state. Returns `null` for structurally invalid\n * input (missing nodes, root reference, reference inside the partner).\n *\n * Determinism (EC-162): the produced container subtree is exactly the\n * `gridContainerNode` constructor output — identical to manual composition.\n */\nexport function applyWrap(args: {\n root: ComponentNode\n referenceId: string\n side: WrapSide\n partner: WrapPartner\n containerId: string\n}): WrapResult | null {\n const { root, referenceId, side, partner, containerId } = args\n if (referenceId === root.id || !getNode(root, referenceId)) return null\n\n let working = root\n let partnerNode: ComponentNode\n if (partner.kind === 'node') {\n if (partner.nodeId === referenceId || partner.nodeId === root.id) return null\n const node = getNode(root, partner.nodeId)\n if (!node || isDescendantOf(root, partner.nodeId, referenceId)) return null\n partnerNode = node\n working = removeNode(root, partner.nodeId)\n } else {\n partnerNode = partner.node\n }\n\n const next = updateNode(working, referenceId, (reference) =>\n gridContainerNode(\n containerId,\n WRAP_GRID_OPTIONS,\n side === 'left' ? [partnerNode, reference] : [reference, partnerNode],\n ),\n )\n return { root: next, containerId }\n}\n\n// ---------------------------------------------------------------------------\n// Pointer → position bands (shared hit-testing model)\n// ---------------------------------------------------------------------------\n\n/** The axis along which a container lays out its children. */\nexport type FlowAxis = 'vertical' | 'horizontal'\n\nfunction staticPropValue(node: ComponentNode, name: string): unknown {\n const prop = node.props?.[name]\n return isStaticProp(prop) ? prop.value : undefined\n}\n\n/**\n * The flow axis of a container — the axis its children stack along, and\n * therefore the hit-band axis for drops over those children (EC-162 ledger\n * fix: bands follow the parent's flow direction, on both surfaces). Mirrors\n * the renderer: a horizontal Stack flows horizontally; a Grid flows\n * horizontally when it actually has ≥ 2 column tracks (an absent/invalid\n * `columns` renders one column). Everything else is block flow (vertical).\n */\nexport function containerFlowAxis(node: ComponentNode | undefined): FlowAxis {\n if (!node) return 'vertical'\n if (node.componentId === STACK_COMPONENT_ID) {\n return staticPropValue(node, 'direction') === 'horizontal' ? 'horizontal' : 'vertical'\n }\n if (node.componentId === GRID_COMPONENT_ID) {\n const columns = staticPropValue(node, 'columns')\n return typeof columns === 'number' && columns >= 2 ? 'horizontal' : 'vertical'\n }\n return 'vertical'\n}\n\n/** Cross-axis side-band width (fraction of the box) that triggers the wrap gesture. */\nconst WRAP_BAND = 0.18\n\n/**\n * Map a 2D pointer offset within a drop target's box onto a `DropPosition`\n * (EC-162). `flow` is the flow axis of the target's *parent* — before/after\n * bands run along it. On vertically flowing targets, narrow side bands on the\n * cross axis produce `left`/`right` (the wrap-into-columns gesture, when\n * `wrappable`); children of horizontal containers get left/right bands for\n * plain reorder instead, so \"beside\" already means before/after there.\n */\nexport function dropPositionForPoint(args: {\n /** Pointer offset across the target's width, 0..1. */\n x: number\n /** Pointer offset across the target's height, 0..1. */\n y: number\n /** Flow axis of the target's parent (the axis siblings stack along). */\n flow: FlowAxis\n nestable: boolean\n edges: boolean\n /** Enable the wrap side-bands (the gesture surface opts in). */\n wrappable?: boolean\n}): DropPosition {\n const { x, y, flow, nestable, edges, wrappable = false } = args\n if (!edges) return 'into'\n if (wrappable && flow === 'vertical') {\n if (x < WRAP_BAND) return 'left'\n if (x > 1 - WRAP_BAND) return 'right'\n }\n return dropPositionForPointer({ ratio: flow === 'horizontal' ? x : y, nestable, edges })\n}\n\n/**\n * Map a pointer offset within a drop target's box onto a `DropPosition`.\n * `ratio` is the pointer's offset along the target's main axis, 0..1 — the\n * caller picks the axis (the parent's flow axis; see `containerFlowAxis`).\n *\n * - nestable targets with siblings: edge quarters insert before/after, the\n * middle half nests into the slot\n * - leaf targets: top/bottom halves insert before/after\n * - targets without siblings (the root): always `into`\n */\nexport function dropPositionForPointer(args: {\n ratio: number\n /** Whether `into` is structurally possible (target declares an insert slot). */\n nestable: boolean\n /** Whether before/after exist (target has a parent slot). */\n edges: boolean\n}): DropPosition {\n const { ratio, nestable, edges } = args\n if (!edges) return 'into'\n if (nestable) {\n if (ratio < 0.25) return 'before'\n if (ratio > 0.75) return 'after'\n return 'into'\n }\n return ratio < 0.5 ? 'before' : 'after'\n}\n\n// ---------------------------------------------------------------------------\n// Indicator state (shared visual contract)\n// ---------------------------------------------------------------------------\n\n/**\n * The visual state a surface shows for the current resolution. `blocked` is\n * the explicit rejection state (never a silent failed drop); `null` means show\n * nothing (a no-op drop is inert, not an error).\n */\nexport type DropIndicator = DropPosition | 'blocked' | null\n\nexport function dropIndicatorFor(\n resolution: DropResolution,\n position: DropPosition,\n): DropIndicator {\n if (resolution.eligible) return position\n return resolution.code === 'no-op' ? null : 'blocked'\n}\n"]}
1
+ {"version":3,"file":"drop-rules.js","sourceRoot":"","sources":["../../../src/lib/dnd/drop-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DEFAULT_SLOT,\n GRID_COMPONENT_ID,\n OUTLET_SLOT,\n STACK_COMPONENT_ID,\n getNode,\n getParentInfo,\n gridContainerNode,\n isDescendantOf,\n isStaticProp,\n removeNode,\n updateNode,\n} from '@elytracms/core/project-graph'\nimport type { ComponentNode, GridContainerOptions, InsertTarget } from '@elytracms/core/project-graph'\nimport { manifestSlots } from '@elytracms/core/component-registry'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\n\n/**\n * Drag & drop eligibility rules (EC-160 + EC-162, vision §3.1) — pure\n * graph-level functions with **no DOM or UI dependencies**. The canvas and the\n * page tree both resolve drops through `resolveDrop`, so the two surfaces can\n * never disagree about what a manifest slot allow-list permits, which children\n * a component accepts, or whether a move would create a cycle.\n *\n * The drop-zone model is position-based: `into` a slot, `before`/`after` a\n * sibling along the parent's flow axis, and the EC-162 gesture positions\n * `left`/`right` — dropping *beside* an element wraps both in a two-column\n * `Grid` container (see `applyWrap`).\n */\n\n// ---------------------------------------------------------------------------\n// Inputs\n// ---------------------------------------------------------------------------\n\n/**\n * The manifest slice the rules evaluate. Structural (not the full\n * `ComponentManifest`) so tests and future callers can feed minimal lookups.\n */\nexport interface SlotRule {\n name: string\n /** Restrict which component ids may be inserted (omit to allow any). */\n allow?: string[]\n}\n\nexport interface ManifestRules {\n slots: SlotRule[]\n}\n\n/** Resolve a component id to its manifest slot rules (undefined = unknown component). */\nexport type ManifestLookup = (componentId: string) => ManifestRules | undefined\n\n/**\n * The canonical `ManifestRules` view of a full manifest (EC-386): the declared\n * `slots` merged with the blocks-prop-derived slots (`manifestSlots`, EC-190\n * \"slots are blocks props\" — a `blocks`-type prop IS a named child region whose\n * subtrees live in `node.slots[propName]`, carrying the prop's `allow` leash).\n * Every surface that feeds a real `ComponentManifest` into a {@link ManifestLookup}\n * or {@link primaryInsertSlot} must adapt through this — resolving against raw\n * `manifest.slots` silently degrades a `project.Section.body`-style region to a\n * sibling insert. One derivation, so tree insert, DnD, paste, and the canvas\n * affordances can never disagree about which regions exist or what they allow.\n */\nexport function manifestRules(manifest: ComponentManifest | undefined): ManifestRules | undefined {\n return manifest ? { slots: manifestSlots(manifest) } : undefined\n}\n\n/** What is being dragged. */\nexport type DragSubject =\n /** An existing node of the current page — a completed drop is a `move`. */\n | { kind: 'node'; nodeId: string; componentId: string }\n /** A new subtree (blocks-panel tile) — a completed drop is an `insert`. */\n | { kind: 'component'; componentId: string }\n\n/**\n * Where, relative to a reference node, the drop would land. `into`, `before`\n * and `after` are the EC-160 positions; `left`/`right` are the EC-162 gesture\n * positions — \"beside\" the reference, wrapping both elements in a two-column\n * `Grid` container.\n */\nexport type DropPosition = 'into' | 'before' | 'after' | 'left' | 'right'\n\n/** The side of the reference an EC-162 wrap gesture drops on. */\nexport type WrapSide = 'left' | 'right'\n\nexport interface DropLocation {\n /** The reference node id (the hovered node/row). */\n nodeId: string\n position: DropPosition\n}\n\n// ---------------------------------------------------------------------------\n// Outcomes\n// ---------------------------------------------------------------------------\n\nexport type DropRejectionCode =\n /** The reference node does not exist in the tree. */\n | 'missing-target'\n /** `into` on a component that declares no insertable slot. */\n | 'no-slot'\n /** The target slot's manifest allow-list rejects the dragged component. */\n | 'not-allowed'\n /** Moving a node into itself or its own subtree (cycle). */\n | 'own-subtree'\n /** The page root cannot be moved, and nothing can drop beside it. */\n | 'root'\n /** Structurally valid but changes nothing (dropping a node onto its own position). */\n | 'no-op'\n\nexport interface DropRejection {\n eligible: false\n code: DropRejectionCode\n reason: string\n}\n\n/** An eligible drop: exactly one builder action (move, insert, or wrap) commits it. */\nexport type DropPlan =\n | { eligible: true; kind: 'move'; nodeId: string; target: InsertTarget }\n | { eligible: true; kind: 'insert'; target: InsertTarget }\n /**\n * EC-162 wrap gesture: the reference node and the dragged subject become the\n * two columns of a new `Grid` container, created atomically (`applyWrap`).\n * `nodeId` is present when the subject is an existing node (a move-wrap);\n * absent for blocks-panel components (an insert-wrap via `buildNode`).\n */\n | { eligible: true; kind: 'wrap'; referenceId: string; side: WrapSide; nodeId?: string }\n\nexport type DropResolution = DropPlan | DropRejection\n\nfunction rejected(code: DropRejectionCode, reason: string): DropRejection {\n return { eligible: false, code, reason }\n}\n\n// ---------------------------------------------------------------------------\n// Slot rules\n// ---------------------------------------------------------------------------\n\n/**\n * The slot an `into` drop inserts into: the default `children` slot when\n * declared, else the first authored slot that is not the layout `outlet`.\n * Mirrors the shell's insert-context resolution so click-insert and\n * drag-insert land in the same place.\n */\nexport function primaryInsertSlot(manifest: ManifestRules | undefined): SlotRule | undefined {\n if (!manifest) return undefined\n return (\n manifest.slots.find((s) => s.name === DEFAULT_SLOT) ??\n manifest.slots.find((s) => s.name !== OUTLET_SLOT)\n )\n}\n\n/** True when a component can receive `into` drops at all (declares an insertable slot). */\nexport function canNestInto(getManifest: ManifestLookup, componentId: string): boolean {\n return primaryInsertSlot(getManifest(componentId)) !== undefined\n}\n\n/** True when a slot's allow-list (if any) permits the component id. */\nexport function slotAllows(slot: SlotRule | undefined, componentId: string): boolean {\n if (!slot?.allow) return true\n return slot.allow.includes(componentId)\n}\n\n// ---------------------------------------------------------------------------\n// Resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a prospective drop into either an executable plan (one `move` or\n * `insert` builder action → one operations command → one undo step) or a\n * structured rejection the UI shows *during* the drag. Pure: same inputs,\n * same answer, on both surfaces.\n */\nexport function resolveDrop(args: {\n root: ComponentNode\n getManifest: ManifestLookup\n subject: DragSubject\n location: DropLocation\n}): DropResolution {\n const { root, getManifest, subject, location } = args\n\n if (subject.kind === 'node' && subject.nodeId === root.id) {\n return rejected('root', 'The page root cannot be moved.')\n }\n\n const reference = getNode(root, location.nodeId)\n if (!reference) {\n return rejected('missing-target', `Node \"${location.nodeId}\" is not on this page.`)\n }\n\n // EC-162: dropping beside the reference wraps both in a two-column Grid.\n if (location.position === 'left' || location.position === 'right') {\n return resolveWrap({ root, getManifest, subject, reference, side: location.position })\n }\n\n // Resolve the concrete insertion point: parent node, slot, index.\n let parentId: string\n let slotRule: SlotRule | undefined\n let slotName: string\n let index: number\n\n if (location.position === 'into') {\n const manifest = getManifest(reference.componentId)\n const slot = primaryInsertSlot(manifest)\n if (!slot) {\n return rejected('no-slot', `\"${reference.componentId}\" does not accept children.`)\n }\n parentId = reference.id\n slotRule = slot\n slotName = slot.name\n index = (reference.slots?.[slot.name] ?? []).length\n } else {\n const info = getParentInfo(root, reference.id)\n if (!info) {\n return rejected('root', 'Nothing can be dropped beside the page root.')\n }\n parentId = info.parent.id\n slotName = info.slot\n // Unknown parent manifests impose no allow-list; validation owns that issue.\n slotRule = getManifest(info.parent.componentId)?.slots.find((s) => s.name === info.slot)\n index = info.index + (location.position === 'after' ? 1 : 0)\n }\n\n if (!slotAllows(slotRule, subject.componentId)) {\n return rejected(\n 'not-allowed',\n `Slot \"${slotName}\" does not allow \"${subject.componentId}\".`,\n )\n }\n\n const target: InsertTarget = { parentId, slot: slotName, index }\n\n if (subject.kind === 'component') {\n return { eligible: true, kind: 'insert', target }\n }\n\n // Cycle prevention: a node can never land inside its own subtree.\n if (subject.nodeId === parentId || isDescendantOf(root, subject.nodeId, parentId)) {\n return rejected('own-subtree', 'A node cannot be dropped into its own subtree.')\n }\n\n // No-op detection: the final position (after same-slot removal compensation,\n // matching `moveNode`) equals the node's current position.\n const current = getParentInfo(root, subject.nodeId)\n if (current && current.parent.id === parentId && current.slot === slotName) {\n const finalIndex = index > current.index ? index - 1 : index\n if (finalIndex === current.index) {\n return rejected('no-op', 'Dropping here would not move the node.')\n }\n }\n\n return { eligible: true, kind: 'move', nodeId: subject.nodeId, target }\n}\n\n// ---------------------------------------------------------------------------\n// Wrap gesture (EC-162): drop beside → two-column Grid container\n// ---------------------------------------------------------------------------\n\n/**\n * The container a \"drop beside\" gesture creates. Canonical choice (EC-161\n * semantics): a `Grid` with two columns, **not** a horizontal `Stack` —\n * EC-161 gives `Grid` the canonical column vocabulary (`columns` is a\n * validated graph value the contextual inspector refines from 2 → n), while\n * a horizontal Stack expresses flow order, not column tracks. The values\n * mirror the Grid manifest defaults (`columns: 2`, `gap: '4'`), so the\n * gesture's graph is exactly what composing the same layout manually would\n * produce.\n */\nexport const WRAP_GRID_OPTIONS: GridContainerOptions = { columns: 2, gap: '4' }\n\n/** Eligibility for a `left`/`right` (wrap-beside) drop. */\nfunction resolveWrap(args: {\n root: ComponentNode\n getManifest: ManifestLookup\n subject: DragSubject\n reference: ComponentNode\n side: WrapSide\n}): DropResolution {\n const { root, getManifest, subject, reference, side } = args\n\n // The Grid takes the reference's place in its parent slot — so the\n // reference needs a parent (the root is the page anchor, never wrapped)\n // and that slot's allow-list must accept a Grid.\n const info = getParentInfo(root, reference.id)\n if (!info) {\n return rejected('root', 'The page root cannot be wrapped in a container.')\n }\n const parentSlotRule = getManifest(info.parent.componentId)?.slots.find(\n (s) => s.name === info.slot,\n )\n if (!slotAllows(parentSlotRule, GRID_COMPONENT_ID)) {\n return rejected('not-allowed', `Slot \"${info.slot}\" does not allow a Grid container.`)\n }\n\n // Both columns land in the Grid's children slot.\n const gridSlot = primaryInsertSlot(getManifest(GRID_COMPONENT_ID))\n for (const componentId of [reference.componentId, subject.componentId]) {\n if (!slotAllows(gridSlot, componentId)) {\n return rejected('not-allowed', `A Grid container does not allow \"${componentId}\".`)\n }\n }\n\n if (subject.kind === 'component') {\n return { eligible: true, kind: 'wrap', referenceId: reference.id, side }\n }\n if (subject.nodeId === reference.id) {\n return rejected('no-op', 'A node cannot be placed beside itself.')\n }\n // The reference must not live inside the dragged subtree (the subject is\n // lifted out first — taking the reference with it would orphan the wrap).\n if (isDescendantOf(root, subject.nodeId, reference.id)) {\n return rejected('own-subtree', 'A node cannot wrap an element of its own subtree.')\n }\n return { eligible: true, kind: 'wrap', referenceId: reference.id, side, nodeId: subject.nodeId }\n}\n\n/** What lands beside the reference: an existing node (moved) or a built subtree (inserted). */\nexport type WrapPartner =\n | { kind: 'node'; nodeId: string }\n | { kind: 'subtree'; node: ComponentNode }\n\nexport interface WrapResult {\n root: ComponentNode\n /** The id of the created Grid container. */\n containerId: string\n}\n\n/**\n * Execute a wrap plan as **one** structural edit: lift the partner out of the\n * tree (when it is an existing node) and replace the reference with\n * `gridContainerNode(containerId, WRAP_GRID_OPTIONS, [left, right])` in a\n * single pass. The caller commits the result as one builder action → one\n * operations command → one undo step; EC-161 normalization then runs on the\n * *committed* tree, so the two-child container can never be observed (or\n * persisted) in a half-built state. Returns `null` for structurally invalid\n * input (missing nodes, root reference, reference inside the partner).\n *\n * Determinism (EC-162): the produced container subtree is exactly the\n * `gridContainerNode` constructor output — identical to manual composition.\n */\nexport function applyWrap(args: {\n root: ComponentNode\n referenceId: string\n side: WrapSide\n partner: WrapPartner\n containerId: string\n}): WrapResult | null {\n const { root, referenceId, side, partner, containerId } = args\n if (referenceId === root.id || !getNode(root, referenceId)) return null\n\n let working = root\n let partnerNode: ComponentNode\n if (partner.kind === 'node') {\n if (partner.nodeId === referenceId || partner.nodeId === root.id) return null\n const node = getNode(root, partner.nodeId)\n if (!node || isDescendantOf(root, partner.nodeId, referenceId)) return null\n partnerNode = node\n working = removeNode(root, partner.nodeId)\n } else {\n partnerNode = partner.node\n }\n\n const next = updateNode(working, referenceId, (reference) =>\n gridContainerNode(\n containerId,\n WRAP_GRID_OPTIONS,\n side === 'left' ? [partnerNode, reference] : [reference, partnerNode],\n ),\n )\n return { root: next, containerId }\n}\n\n// ---------------------------------------------------------------------------\n// Pointer → position bands (shared hit-testing model)\n// ---------------------------------------------------------------------------\n\n/** The axis along which a container lays out its children. */\nexport type FlowAxis = 'vertical' | 'horizontal'\n\nfunction staticPropValue(node: ComponentNode, name: string): unknown {\n const prop = node.props?.[name]\n return isStaticProp(prop) ? prop.value : undefined\n}\n\n/**\n * The flow axis of a container — the axis its children stack along, and\n * therefore the hit-band axis for drops over those children (EC-162 ledger\n * fix: bands follow the parent's flow direction, on both surfaces). Mirrors\n * the renderer: a horizontal Stack flows horizontally; a Grid flows\n * horizontally when it actually has ≥ 2 column tracks (an absent/invalid\n * `columns` renders one column). Everything else is block flow (vertical).\n */\nexport function containerFlowAxis(node: ComponentNode | undefined): FlowAxis {\n if (!node) return 'vertical'\n if (node.componentId === STACK_COMPONENT_ID) {\n return staticPropValue(node, 'direction') === 'horizontal' ? 'horizontal' : 'vertical'\n }\n if (node.componentId === GRID_COMPONENT_ID) {\n const columns = staticPropValue(node, 'columns')\n return typeof columns === 'number' && columns >= 2 ? 'horizontal' : 'vertical'\n }\n return 'vertical'\n}\n\n/** Cross-axis side-band width (fraction of the box) that triggers the wrap gesture. */\nconst WRAP_BAND = 0.18\n\n/**\n * Map a 2D pointer offset within a drop target's box onto a `DropPosition`\n * (EC-162). `flow` is the flow axis of the target's *parent* — before/after\n * bands run along it. On vertically flowing targets, narrow side bands on the\n * cross axis produce `left`/`right` (the wrap-into-columns gesture, when\n * `wrappable`); children of horizontal containers get left/right bands for\n * plain reorder instead, so \"beside\" already means before/after there.\n */\nexport function dropPositionForPoint(args: {\n /** Pointer offset across the target's width, 0..1. */\n x: number\n /** Pointer offset across the target's height, 0..1. */\n y: number\n /** Flow axis of the target's parent (the axis siblings stack along). */\n flow: FlowAxis\n nestable: boolean\n edges: boolean\n /** Enable the wrap side-bands (the gesture surface opts in). */\n wrappable?: boolean\n}): DropPosition {\n const { x, y, flow, nestable, edges, wrappable = false } = args\n if (!edges) return 'into'\n if (wrappable && flow === 'vertical') {\n if (x < WRAP_BAND) return 'left'\n if (x > 1 - WRAP_BAND) return 'right'\n }\n return dropPositionForPointer({ ratio: flow === 'horizontal' ? x : y, nestable, edges })\n}\n\n/**\n * Map a pointer offset within a drop target's box onto a `DropPosition`.\n * `ratio` is the pointer's offset along the target's main axis, 0..1 — the\n * caller picks the axis (the parent's flow axis; see `containerFlowAxis`).\n *\n * - nestable targets with siblings: edge quarters insert before/after, the\n * middle half nests into the slot\n * - leaf targets: top/bottom halves insert before/after\n * - targets without siblings (the root): always `into`\n */\nexport function dropPositionForPointer(args: {\n ratio: number\n /** Whether `into` is structurally possible (target declares an insert slot). */\n nestable: boolean\n /** Whether before/after exist (target has a parent slot). */\n edges: boolean\n}): DropPosition {\n const { ratio, nestable, edges } = args\n if (!edges) return 'into'\n if (nestable) {\n if (ratio < 0.25) return 'before'\n if (ratio > 0.75) return 'after'\n return 'into'\n }\n return ratio < 0.5 ? 'before' : 'after'\n}\n\n// ---------------------------------------------------------------------------\n// Indicator state (shared visual contract)\n// ---------------------------------------------------------------------------\n\n/**\n * The visual state a surface shows for the current resolution. `blocked` is\n * the explicit rejection state (never a silent failed drop); `null` means show\n * nothing (a no-op drop is inert, not an error).\n */\nexport type DropIndicator = DropPosition | 'blocked' | null\n\nexport function dropIndicatorFor(\n resolution: DropResolution,\n position: DropPosition,\n): DropIndicator {\n if (resolution.eligible) return position\n return resolution.code === 'no-op' ? null : 'blocked'\n}\n"]}
@@ -643,6 +643,9 @@ export const deMessages = {
643
643
  'account.password.error.deploymentMisconfigured': 'Diese Bereitstellung kann Ihr Passwort nicht ändern: Der Auth-Signaturschlüssel ist nicht gesetzt. Führen Sie `npx @convex-dev/auth` aus.',
644
644
  'account.password.error.serverError': 'Passwortänderung fehlgeschlagen (unerwarteter Serverfehler) — prüfen Sie die Logs der Bereitstellung.',
645
645
  'account.password.error.playgroundUnsupported': 'Die Passwortänderung ist im Playground-Modus nicht verfügbar.',
646
+ // EC-416 — update-profile failures (not-authenticated reuses notSignedIn above).
647
+ 'account.profile.error.unavailable': 'Die Profilaktualisierung ist derzeit nicht verfügbar.',
648
+ 'account.profile.error.playgroundUnsupported': 'Die Profilbearbeitung ist im Playground-Modus nicht verfügbar.',
646
649
  // ─────────────────────────────────────────────────────────────────────────
647
650
  // Redirects (section)
648
651
  // ─────────────────────────────────────────────────────────────────────────