emdash 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapters-BKSf3T9R.d.mts → adapters-BktHA7EO.d.mts} +1 -1
- package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
- package/dist/{apply-x0eMK1lX.mjs → apply-UsrFuO7l.mjs} +207 -355
- package/dist/apply-UsrFuO7l.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +118 -4
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +6 -7
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +14 -57
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +15 -10
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +8 -5
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +70 -121
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +25 -10
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-Chbr2GoP.mjs → byline-C3vnhIpU.mjs} +4 -4
- package/dist/{byline-Chbr2GoP.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
- package/dist/bylines-esI7ioa9.mjs +113 -0
- package/dist/bylines-esI7ioa9.mjs.map +1 -0
- package/dist/cache-fTzxgMFJ.mjs +65 -0
- package/dist/cache-fTzxgMFJ.mjs.map +1 -0
- package/dist/{chunks-HGz06Soa.mjs → chunks-Da2-b-oA.mjs} +8 -2
- package/dist/{chunks-HGz06Soa.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
- package/dist/cli/index.mjs +456 -90
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +3 -3
- package/dist/client/index.mjs.map +1 -1
- package/dist/{config-BXwuX8Bx.mjs → config-CVssduLe.mjs} +1 -1
- package/dist/{config-BXwuX8Bx.mjs.map → config-CVssduLe.mjs.map} +1 -1
- package/dist/{content-BcQPYxdV.mjs → content-C7G4QXkK.mjs} +42 -14
- package/dist/content-C7G4QXkK.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/libsql.d.mts.map +1 -1
- package/dist/db/libsql.mjs +7 -2
- package/dist/db/libsql.mjs.map +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.d.mts.map +1 -1
- package/dist/db/sqlite.mjs +8 -3
- package/dist/db/sqlite.mjs.map +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs → db-errors-B7P2pSCn.mjs} +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
- package/dist/{default-DCVqE5ib.mjs → default-pHuz9WF6.mjs} +1 -1
- package/dist/{default-DCVqE5ib.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
- package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
- package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
- package/dist/{error-zG5T1UGA.mjs → error-DqnRMM5z.mjs} +1 -1
- package/dist/{error-zG5T1UGA.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
- package/dist/{index-DIb-CzNx.d.mts → index-DjPMOfO0.d.mts} +162 -87
- package/dist/index-DjPMOfO0.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +27 -24
- package/dist/{load-CyEoextb.mjs → load-sXRuM7Us.mjs} +2 -2
- package/dist/{load-CyEoextb.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
- package/dist/{loader-CndGj8kM.mjs → loader-Bx2_9-5e.mjs} +53 -8
- package/dist/loader-Bx2_9-5e.mjs.map +1 -0
- package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-CXAbd1vH.mjs} +33 -3
- package/dist/manifest-schema-CXAbd1vH.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-BnAOqItE.mjs → mode-YhqNVef_.mjs} +1 -1
- package/dist/{mode-BnAOqItE.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
- package/dist/options-nPxWnrya.mjs +117 -0
- package/dist/options-nPxWnrya.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
- package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
- package/dist/{placeholder-D29tWZ7o.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
- package/dist/{placeholder-D29tWZ7o.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
- package/dist/public-url-B1AxbbbQ.mjs +51 -0
- package/dist/public-url-B1AxbbbQ.mjs.map +1 -0
- package/dist/{query-fqEdLFms.mjs → query-Bo-msrmu.mjs} +114 -16
- package/dist/query-Bo-msrmu.mjs.map +1 -0
- package/dist/{redirect-D_pshWdf.mjs → redirect-C5H7VGIX.mjs} +11 -6
- package/dist/redirect-C5H7VGIX.mjs.map +1 -0
- package/dist/{registry-C3Mr0ODu.mjs → registry-Beb7wxFc.mjs} +39 -5
- package/dist/registry-Beb7wxFc.mjs.map +1 -0
- package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-C-tIpYIw.mjs} +1 -1
- package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
- package/dist/runner-Clwe4Mme.d.mts +44 -0
- package/dist/runner-Clwe4Mme.d.mts.map +1 -0
- package/dist/{runner-tQ7BJ4T7.mjs → runner-DMnlIkh4.mjs} +616 -191
- package/dist/runner-DMnlIkh4.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-BoZYFuUk.mjs → search-DkN-BqsS.mjs} +270 -152
- package/dist/search-DkN-BqsS.mjs.map +1 -0
- package/dist/secrets-CZ8rxLX3.mjs +314 -0
- package/dist/secrets-CZ8rxLX3.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +13 -11
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/taxonomies-CTtewrSQ.mjs +407 -0
- package/dist/taxonomies-CTtewrSQ.mjs.map +1 -0
- package/dist/taxonomy-DSxx2K2L.mjs +218 -0
- package/dist/taxonomy-DSxx2K2L.mjs.map +1 -0
- package/dist/{tokens-D9vnZqYS.mjs → tokens-CyRDPVW2.mjs} +1 -1
- package/dist/{tokens-D9vnZqYS.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts → transport-DX_5rpsq.d.mts} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
- package/dist/{transport-C9ugt2Nr.mjs → transport-xpzIjCIB.mjs} +6 -5
- package/dist/{transport-C9ugt2Nr.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
- package/dist/{types-BrA0xf5I.d.mts → types-B_CXXnzh.d.mts} +1 -1
- package/dist/{types-BrA0xf5I.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
- package/dist/{types-DIMwPFub.d.mts → types-C-aFbqmA.d.mts} +1 -1
- package/dist/{types-DIMwPFub.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
- package/dist/types-CoO6mpV3.mjs +68 -0
- package/dist/types-CoO6mpV3.mjs.map +1 -0
- package/dist/{types-i36XcA_X.d.mts → types-D19uBYWn.d.mts} +83 -7
- package/dist/types-D19uBYWn.d.mts.map +1 -0
- package/dist/{types-BmPPSUEx.d.mts → types-Dl1fgFjn.d.mts} +24 -2
- package/dist/{types-BmPPSUEx.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
- package/dist/{types-CS8FIX7L.d.mts → types-Dtx1mSMX.d.mts} +9 -1
- package/dist/types-Dtx1mSMX.d.mts.map +1 -0
- package/dist/{types-Bm1dn-q3.mjs → types-Eg829jj9.mjs} +1 -1
- package/dist/{types-Bm1dn-q3.mjs.map → types-Eg829jj9.mjs.map} +1 -1
- package/dist/{types-CgqmmMJB.mjs → types-K-EkEQCI.mjs} +1 -1
- package/dist/{types-CgqmmMJB.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
- package/dist/{validate-CxVsLehf.mjs → validate-CBIbxM3L.mjs} +14 -10
- package/dist/validate-CBIbxM3L.mjs.map +1 -0
- package/dist/{validate-DHxmpFJt.d.mts → validate-DHGwADqO.d.mts} +18 -5
- package/dist/validate-DHGwADqO.d.mts.map +1 -0
- package/dist/{validation-C-ZpN2GI.mjs → validation-B1NYiEos.mjs} +6 -6
- package/dist/{validation-C-ZpN2GI.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
- package/dist/version-CMD42IRC.mjs +7 -0
- package/dist/{version-Bbq8TCrz.mjs.map → version-CMD42IRC.mjs.map} +1 -1
- package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-BNJDQBSZ.mjs} +11 -6
- package/dist/{zod-generator-CpwccCIv.mjs.map → zod-generator-BNJDQBSZ.mjs.map} +1 -1
- package/locals.d.ts +1 -6
- package/package.json +9 -8
- package/src/api/handlers/comments.ts +6 -4
- package/src/api/handlers/content.ts +40 -1
- package/src/api/handlers/dashboard.ts +29 -36
- package/src/api/handlers/device-flow.ts +5 -0
- package/src/api/handlers/marketplace.ts +11 -4
- package/src/api/handlers/menus.ts +256 -75
- package/src/api/handlers/oauth-authorization.ts +72 -33
- package/src/api/handlers/revision.ts +23 -14
- package/src/api/handlers/taxonomies.ts +273 -100
- package/src/api/public-url.ts +48 -2
- package/src/api/schemas/comments.ts +2 -2
- package/src/api/schemas/common.ts +7 -0
- package/src/api/schemas/content.ts +17 -0
- package/src/api/schemas/menus.ts +23 -0
- package/src/api/schemas/sections.ts +3 -3
- package/src/api/schemas/taxonomies.ts +39 -0
- package/src/api/schemas/users.ts +1 -1
- package/src/api/types.ts +5 -1
- package/src/astro/integration/index.ts +17 -0
- package/src/astro/integration/routes.ts +10 -0
- package/src/astro/integration/runtime.ts +30 -0
- package/src/astro/integration/virtual-modules.ts +32 -2
- package/src/astro/integration/vite-config.ts +6 -1
- package/src/astro/middleware/auth.ts +13 -6
- package/src/astro/middleware/redirect.ts +29 -16
- package/src/astro/middleware/request-context.ts +15 -5
- package/src/astro/middleware.ts +23 -9
- package/src/astro/routes/api/auth/invite/complete.ts +6 -1
- package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
- package/src/astro/routes/api/auth/signup/complete.ts +6 -1
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
- package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
- package/src/astro/routes/api/import/wordpress/prepare.ts +7 -8
- package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
- package/src/astro/routes/api/manifest.ts +62 -45
- package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
- package/src/astro/routes/api/menus/[name]/items.ts +16 -6
- package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
- package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
- package/src/astro/routes/api/menus/[name].ts +19 -10
- package/src/astro/routes/api/menus/index.ts +9 -6
- package/src/astro/routes/api/openapi.json.ts +27 -10
- package/src/astro/routes/api/redirects/404s/index.ts +10 -4
- package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
- package/src/astro/routes/api/redirects/[id].ts +10 -4
- package/src/astro/routes/api/redirects/index.ts +7 -3
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
- package/src/astro/routes/api/schema/collections/index.ts +1 -1
- package/src/astro/routes/api/search/index.ts +10 -2
- package/src/astro/routes/api/sections/[slug].ts +10 -4
- package/src/astro/routes/api/sections/index.ts +7 -3
- package/src/astro/routes/api/setup/admin-verify.ts +6 -1
- package/src/astro/routes/api/snapshot.ts +44 -18
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
- package/src/astro/routes/api/taxonomies/index.ts +9 -7
- package/src/astro/routes/api/themes/preview.ts +11 -5
- package/src/astro/types.ts +23 -3
- package/src/auth/allowed-origins.ts +168 -0
- package/src/auth/passkey-config.ts +35 -13
- package/src/bylines/index.ts +37 -88
- package/src/cli/commands/auth.ts +28 -6
- package/src/cli/commands/bundle-utils.ts +11 -2
- package/src/cli/commands/bundle.ts +28 -8
- package/src/cli/commands/content.ts +13 -0
- package/src/cli/commands/export-seed.ts +82 -21
- package/src/cli/commands/login.ts +8 -1
- package/src/cli/commands/plugin-init.ts +216 -90
- package/src/cli/commands/publish.ts +24 -0
- package/src/cli/commands/secrets.ts +183 -0
- package/src/cli/credentials.ts +1 -1
- package/src/cli/index.ts +5 -1
- package/src/client/index.ts +4 -4
- package/src/client/transport.ts +17 -7
- package/src/components/Break.astro +2 -2
- package/src/components/EmDashHead.astro +18 -13
- package/src/components/Embed.astro +1 -1
- package/src/components/Gallery.astro +1 -1
- package/src/components/Image.astro +1 -1
- package/src/components/InlinePortableTextEditor.tsx +104 -18
- package/src/config/secrets.ts +528 -0
- package/src/database/dialect-helpers.ts +50 -0
- package/src/database/migrations/034_published_at_index.ts +1 -1
- package/src/database/migrations/035_bounded_404_log.ts +56 -39
- package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
- package/src/database/migrations/runner.ts +158 -23
- package/src/database/repositories/content.ts +47 -12
- package/src/database/repositories/redirect.ts +14 -3
- package/src/database/repositories/taxonomy.ts +212 -82
- package/src/database/types.ts +10 -2
- package/src/db/libsql.ts +1 -3
- package/src/db/sqlite.ts +2 -5
- package/src/emdash-runtime.ts +84 -159
- package/src/i18n/resolve.ts +37 -0
- package/src/index.ts +9 -0
- package/src/loader.ts +73 -3
- package/src/mcp/server.ts +180 -54
- package/src/menus/index.ts +143 -124
- package/src/menus/types.ts +15 -1
- package/src/page/site-identity.ts +58 -0
- package/src/plugins/adapt-sandbox-entry.ts +22 -10
- package/src/plugins/context.ts +13 -10
- package/src/plugins/define-plugin.ts +40 -12
- package/src/plugins/hooks.ts +23 -19
- package/src/plugins/index.ts +9 -0
- package/src/plugins/manifest-schema.ts +37 -2
- package/src/plugins/types.ts +151 -11
- package/src/preview/urls.ts +23 -3
- package/src/query.ts +148 -5
- package/src/redirects/cache.ts +38 -18
- package/src/schema/registry.ts +56 -0
- package/src/schema/zod-generator.ts +39 -7
- package/src/seed/apply.ts +142 -54
- package/src/seed/types.ts +14 -1
- package/src/seed/validate.ts +27 -13
- package/src/settings/index.ts +80 -6
- package/src/settings/types.ts +23 -1
- package/src/taxonomies/index.ts +237 -210
- package/src/taxonomies/types.ts +10 -0
- package/dist/apply-x0eMK1lX.mjs.map +0 -1
- package/dist/bylines-CRNsVG88.mjs +0 -157
- package/dist/bylines-CRNsVG88.mjs.map +0 -1
- package/dist/cache-BkKBuIvS.mjs +0 -56
- package/dist/cache-BkKBuIvS.mjs.map +0 -1
- package/dist/chunk-ClPoSABd.mjs +0 -21
- package/dist/content-BcQPYxdV.mjs.map +0 -1
- package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
- package/dist/index-DIb-CzNx.d.mts.map +0 -1
- package/dist/loader-CndGj8kM.mjs.map +0 -1
- package/dist/manifest-schema-DH9xhc6t.mjs.map +0 -1
- package/dist/query-fqEdLFms.mjs.map +0 -1
- package/dist/redirect-D_pshWdf.mjs.map +0 -1
- package/dist/registry-C3Mr0ODu.mjs.map +0 -1
- package/dist/runner-OURCaApa.d.mts +0 -34
- package/dist/runner-OURCaApa.d.mts.map +0 -1
- package/dist/runner-tQ7BJ4T7.mjs.map +0 -1
- package/dist/search-BoZYFuUk.mjs.map +0 -1
- package/dist/taxonomies-B4IAshV8.mjs +0 -308
- package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
- package/dist/types-CS8FIX7L.d.mts.map +0 -1
- package/dist/types-i36XcA_X.d.mts.map +0 -1
- package/dist/validate-CxVsLehf.mjs.map +0 -1
- package/dist/validate-DHxmpFJt.d.mts.map +0 -1
- package/dist/version-Bbq8TCrz.mjs +0 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-context.mjs","names":[],"sources":["../../../src/visual-editing/toolbar.ts","../../../src/astro/middleware/request-context.ts"],"sourcesContent":["/**\n * EmDash Visual Editing Toolbar\n *\n * A floating pill injected via middleware for authenticated editors.\n * Renders as a plain HTML string with inline styles and a <script> tag.\n * No dependencies — works on any page with a </body> tag.\n */\n\ninterface ToolbarConfig {\n\teditMode: boolean;\n\tisPreview: boolean;\n}\n\nexport function renderToolbar(config: ToolbarConfig): string {\n\tconst { editMode, isPreview } = config;\n\n\treturn `\n<!-- EmDash Visual Editing Toolbar -->\n<div id=\"emdash-toolbar\" data-edit-mode=\"${editMode}\" data-preview=\"${isPreview}\">\n <div class=\"emdash-tb-inner\">\n <span class=\"emdash-tb-logo\">EmDash</span>\n\n <div class=\"emdash-tb-divider\"></div>\n\n <label class=\"emdash-tb-toggle\" title=\"Toggle edit mode\">\n <input type=\"checkbox\" id=\"emdash-edit-toggle\" ${editMode ? \"checked\" : \"\"} />\n <span class=\"emdash-tb-toggle-track\">\n <span class=\"emdash-tb-toggle-thumb\"></span>\n </span>\n <span class=\"emdash-tb-toggle-label\">Edit</span>\n </label>\n\n <span class=\"emdash-tb-status\" id=\"emdash-tb-status\"></span>\n\n <span class=\"emdash-tb-save-status\" id=\"emdash-tb-save-status\"></span>\n\n <a class=\"emdash-tb-admin\" id=\"emdash-tb-admin\" href=\"#\" target=\"emdash-admin\" style=\"display:none\" title=\"Open in admin\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/><polyline points=\"15 3 21 3 21 9\"/><line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/></svg>\n </a>\n\n <button class=\"emdash-tb-publish\" id=\"emdash-tb-publish\" style=\"display:none\">Publish</button>\n </div>\n</div>\n\n<style>\n #emdash-toolbar {\n position: fixed;\n bottom: 16px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 999999;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n font-size: 13px;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n }\n\n .emdash-tb-inner {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 16px;\n background: #1a1a1a;\n color: #e0e0e0;\n border-radius: 999px;\n box-shadow: 0 4px 24px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.08);\n white-space: nowrap;\n user-select: none;\n }\n\n .emdash-tb-logo {\n font-weight: 600;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: #fff;\n opacity: 0.7;\n }\n\n .emdash-tb-divider {\n width: 1px;\n height: 16px;\n background: rgba(255,255,255,0.15);\n }\n\n /* Toggle switch */\n .emdash-tb-toggle {\n display: flex;\n align-items: center;\n gap: 6px;\n cursor: pointer;\n }\n\n .emdash-tb-toggle input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .emdash-tb-toggle-track {\n position: relative;\n width: 32px;\n height: 18px;\n background: #444;\n border-radius: 9px;\n transition: background 0.2s;\n }\n\n .emdash-tb-toggle input:checked + .emdash-tb-toggle-track {\n background: #3b82f6;\n }\n\n .emdash-tb-toggle-thumb {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 14px;\n height: 14px;\n background: #fff;\n border-radius: 50%;\n transition: transform 0.2s;\n }\n\n .emdash-tb-toggle input:checked + .emdash-tb-toggle-track .emdash-tb-toggle-thumb {\n transform: translateX(14px);\n }\n\n .emdash-tb-toggle-label {\n font-size: 12px;\n color: #aaa;\n }\n\n .emdash-tb-toggle input:checked ~ .emdash-tb-toggle-label {\n color: #fff;\n }\n\n /* Status area — flex for multiple badges */\n .emdash-tb-status {\n display: inline-flex;\n gap: 6px;\n align-items: center;\n }\n\n /* Badges */\n .emdash-tb-badge {\n display: inline-flex;\n align-items: center;\n padding: 3px 8px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n }\n\n .emdash-tb-badge--preview {\n background: rgba(139,92,246,0.2);\n color: #a78bfa;\n }\n\n .emdash-tb-badge--draft {\n background: rgba(245,158,11,0.2);\n color: #fbbf24;\n }\n\n .emdash-tb-badge--published {\n background: rgba(34,197,94,0.2);\n color: #4ade80;\n }\n\n .emdash-tb-badge--pending {\n background: rgba(59,130,246,0.2);\n color: #60a5fa;\n }\n\n .emdash-tb-badge--unsaved {\n background: rgba(245,158,11,0.2);\n color: #fbbf24;\n }\n\n .emdash-tb-badge--saving {\n background: rgba(148,163,184,0.2);\n color: #94a3b8;\n }\n\n .emdash-tb-badge--saved {\n background: rgba(34,197,94,0.2);\n color: #4ade80;\n transition: opacity 0.3s;\n }\n\n .emdash-tb-badge--error {\n background: rgba(239,68,68,0.2);\n color: #f87171;\n }\n\n /* Admin link */\n .emdash-tb-admin {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #888;\n text-decoration: none;\n padding: 2px;\n border-radius: 4px;\n transition: color 0.15s;\n }\n\n .emdash-tb-admin:hover {\n color: #fff;\n }\n\n /* Publish button */\n .emdash-tb-publish {\n padding: 4px 12px;\n background: #3b82f6;\n color: #fff;\n border: none;\n border-radius: 999px;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.15s;\n font-family: inherit;\n }\n\n .emdash-tb-publish:hover {\n background: #2563eb;\n }\n\n .emdash-tb-publish:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n /* Edit mode: editable hover styles — uses :has() to check toolbar state */\n body:has(#emdash-toolbar[data-edit-mode=\"true\"]) [data-emdash-ref] {\n transition: box-shadow 0.15s, background-color 0.15s;\n }\n\n body:has(#emdash-toolbar[data-edit-mode=\"true\"]) [data-emdash-ref]:hover {\n box-shadow: 0 0 0 2px rgba(59,130,246,0.5);\n border-radius: 4px;\n background-color: rgba(59,130,246,0.04);\n cursor: text;\n }\n\n /* Active editing state — override hover pencil cursor */\n [data-emdash-editing] {\n box-shadow: 0 0 0 2px #3b82f6 !important;\n border-radius: 4px !important;\n background-color: rgba(59,130,246,0.04) !important;\n cursor: text !important;\n }\n\n /* Suppress browser focus ring on contenteditable and tiptap editor */\n [data-emdash-editing]:focus,\n [data-emdash-ref] .tiptap:focus,\n [data-emdash-ref] .ProseMirror:focus {\n outline: none !important;\n }\n\n /* Image editor popover */\n .emdash-img-popover {\n position: fixed;\n z-index: 1000000;\n background: #1a1a1a;\n border-radius: 12px;\n box-shadow: 0 8px 32px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.08);\n color: #e0e0e0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n font-size: 13px;\n width: 320px;\n overflow: hidden;\n animation: emdash-img-fadein 0.15s ease-out;\n }\n\n @keyframes emdash-img-fadein {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: translateY(0); }\n }\n\n .emdash-img-popover-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 12px;\n border-bottom: 1px solid rgba(255,255,255,0.08);\n }\n\n .emdash-img-popover-title {\n font-weight: 600;\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: #999;\n }\n\n .emdash-img-popover-close {\n background: none;\n border: none;\n color: #666;\n cursor: pointer;\n padding: 2px;\n line-height: 1;\n font-size: 16px;\n border-radius: 4px;\n transition: color 0.15s;\n }\n\n .emdash-img-popover-close:hover {\n color: #fff;\n }\n\n .emdash-img-popover-body {\n padding: 12px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n }\n\n .emdash-img-preview {\n width: 100%;\n max-height: 160px;\n object-fit: contain;\n border-radius: 6px;\n background: #111;\n }\n\n .emdash-img-empty {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 80px;\n border: 2px dashed rgba(255,255,255,0.15);\n border-radius: 6px;\n color: #666;\n font-size: 12px;\n }\n\n .emdash-img-field {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n .emdash-img-field label {\n font-size: 11px;\n font-weight: 600;\n color: #888;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n }\n\n .emdash-img-field input[type=\"text\"] {\n background: #111;\n border: 1px solid rgba(255,255,255,0.12);\n border-radius: 6px;\n color: #e0e0e0;\n padding: 6px 8px;\n font-size: 13px;\n font-family: inherit;\n outline: none;\n transition: border-color 0.15s;\n }\n\n .emdash-img-field input[type=\"text\"]:focus {\n border-color: #3b82f6;\n }\n\n .emdash-img-actions {\n display: flex;\n gap: 6px;\n }\n\n .emdash-img-btn {\n flex: 1;\n padding: 6px 10px;\n border: 1px solid rgba(255,255,255,0.12);\n border-radius: 6px;\n background: #222;\n color: #e0e0e0;\n font-size: 12px;\n font-family: inherit;\n cursor: pointer;\n transition: background 0.15s, border-color 0.15s;\n text-align: center;\n white-space: nowrap;\n }\n\n .emdash-img-btn:hover {\n background: #333;\n border-color: rgba(255,255,255,0.2);\n }\n\n .emdash-img-btn--primary {\n background: #3b82f6;\n border-color: #3b82f6;\n color: #fff;\n }\n\n .emdash-img-btn--primary:hover {\n background: #2563eb;\n border-color: #2563eb;\n }\n\n .emdash-img-btn--danger {\n color: #f87171;\n border-color: rgba(248,113,113,0.3);\n }\n\n .emdash-img-btn--danger:hover {\n background: rgba(248,113,113,0.1);\n border-color: rgba(248,113,113,0.5);\n }\n\n /* Media browser within the popover */\n .emdash-img-browser {\n border-top: 1px solid rgba(255,255,255,0.08);\n padding: 12px;\n }\n\n .emdash-img-browser-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 8px;\n }\n\n .emdash-img-browser-title {\n font-size: 12px;\n font-weight: 600;\n color: #999;\n }\n\n .emdash-img-browser-back {\n background: none;\n border: none;\n color: #3b82f6;\n cursor: pointer;\n font-size: 12px;\n font-family: inherit;\n padding: 2px 4px;\n }\n\n .emdash-img-browser-back:hover {\n text-decoration: underline;\n }\n\n .emdash-img-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 6px;\n max-height: 240px;\n overflow-y: auto;\n }\n\n .emdash-img-grid-item {\n aspect-ratio: 1;\n border-radius: 4px;\n overflow: hidden;\n cursor: pointer;\n border: 2px solid transparent;\n transition: border-color 0.15s;\n background: #111;\n }\n\n .emdash-img-grid-item:hover {\n border-color: rgba(59,130,246,0.5);\n }\n\n .emdash-img-grid-item--selected {\n border-color: #3b82f6;\n }\n\n .emdash-img-grid-item img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n }\n\n .emdash-img-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 80px;\n color: #666;\n font-size: 12px;\n }\n\n .emdash-img-drop {\n border: 2px dashed #3b82f6;\n background: rgba(59,130,246,0.05);\n }\n\n .emdash-img-uploading {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 0;\n color: #999;\n font-size: 12px;\n }\n\n .emdash-img-popover-backdrop {\n position: fixed;\n inset: 0;\n z-index: 999999;\n }\n</style>\n\n<script>\n(function() {\n var toolbar = document.getElementById(\"emdash-toolbar\");\n var toggle = document.getElementById(\"emdash-edit-toggle\");\n var statusEl = document.getElementById(\"emdash-tb-status\");\n var saveStatusEl = document.getElementById(\"emdash-tb-save-status\");\n var publishBtn = document.getElementById(\"emdash-tb-publish\");\n if (!toolbar || !toggle || !statusEl || !publishBtn || !saveStatusEl) return;\n\n var isEditMode = toolbar.getAttribute(\"data-edit-mode\") === \"true\";\n\n // CSRF-protected fetch — adds X-EmDash-Request header to all API calls\n function ecFetch(url, init) {\n init = init || {};\n init.headers = Object.assign({ \"X-EmDash-Request\": \"1\" }, init.headers || {});\n return fetch(url, init);\n }\n\n // --- Save status tracking ---\n var saveState = \"idle\"; // idle | unsaved | saving | saved | error\n var saveHideTimer = null;\n var pendingSavePromise = null;\n\n function setSaveState(state) {\n saveState = state;\n clearTimeout(saveHideTimer);\n\n switch (state) {\n case \"unsaved\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--unsaved\">Unsaved</span>';\n break;\n case \"saving\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--saving\">Saving\\u2026</span>';\n break;\n case \"saved\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--saved\">Saved</span>';\n saveHideTimer = setTimeout(function() {\n saveStatusEl.innerHTML = \"\";\n saveState = \"idle\";\n }, 2000);\n break;\n case \"error\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--error\">Save failed</span>';\n saveHideTimer = setTimeout(function() {\n saveStatusEl.innerHTML = \"\";\n saveState = \"idle\";\n }, 3000);\n break;\n default:\n saveStatusEl.innerHTML = \"\";\n }\n }\n\n // Listen for save events from inline editors (e.g. PT editor)\n document.addEventListener(\"emdash:save\", function(e) {\n var detail = e.detail || {};\n if (detail.state) {\n setSaveState(detail.state);\n }\n });\n\n document.addEventListener(\"emdash:content-changed\", function(e) {\n var detail = e.detail || {};\n if (detail.collection && detail.id) {\n showUnpublishedChanges(detail.collection, detail.id);\n }\n });\n\n // --- Entry status ---\n var entryRef = null;\n\n function updateStatus() {\n if (!isEditMode) {\n statusEl.innerHTML = \"\";\n publishBtn.style.display = \"none\";\n return;\n }\n\n var first = document.querySelector(\"[data-emdash-ref]\");\n if (!first) {\n statusEl.innerHTML = \"\";\n publishBtn.style.display = \"none\";\n return;\n }\n\n try {\n var ref = JSON.parse(first.getAttribute(\"data-emdash-ref\"));\n entryRef = ref;\n if (!ref.status) return;\n\n // Show admin link\n var adminLink = document.getElementById(\"emdash-tb-admin\");\n if (adminLink) {\n adminLink.href = \"/_emdash/admin/content/\" + encodeURIComponent(ref.collection) + \"/\" + encodeURIComponent(ref.id);\n adminLink.style.display = \"\";\n }\n\n if (ref.status === \"draft\") {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--draft\">Draft</span>';\n publishBtn.style.display = \"\";\n publishBtn.onclick = function() { publish(ref.collection, ref.id); };\n } else if (ref.status === \"published\" && ref.hasDraft) {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--pending\">Unpublished changes</span>';\n publishBtn.style.display = \"\";\n publishBtn.onclick = function() { publish(ref.collection, ref.id); };\n } else if (ref.status === \"published\") {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--published\">Published</span>';\n publishBtn.style.display = \"none\";\n }\n } catch (e) {\n // ignore parse errors\n }\n }\n\n // Publish action\n function publish(collection, id) {\n if (pendingSavePromise) {\n pendingSavePromise.then(function() { publish(collection, id); });\n return;\n }\n\n publishBtn.disabled = true;\n publishBtn.textContent = \"Publishing\\u2026\";\n\n ecFetch(\"/_emdash/api/content/\" + encodeURIComponent(collection) + \"/\" + encodeURIComponent(id) + \"/publish\", {\n method: \"POST\",\n credentials: \"same-origin\",\n })\n .then(function(res) {\n if (res.ok) {\n if (document.startViewTransition) {\n document.startViewTransition(function() { location.reload(); });\n } else {\n location.reload();\n }\n } else {\n publishBtn.disabled = false;\n publishBtn.textContent = \"Publish\";\n console.error(\"Publish failed:\", res.status);\n }\n })\n .catch(function(err) {\n publishBtn.disabled = false;\n publishBtn.textContent = \"Publish\";\n console.error(\"Publish failed:\", err);\n });\n }\n\n // Edit mode toggle\n toggle.addEventListener(\"change\", function() {\n if (toggle.checked) {\n document.cookie = \"emdash-edit-mode=true;path=/;samesite=lax\";\n } else {\n document.cookie = \"emdash-edit-mode=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT\";\n }\n\n if (document.startViewTransition) {\n document.startViewTransition(function() { location.replace(location.href); });\n } else {\n location.replace(location.href);\n }\n });\n\n // --- Inline editing ---\n\n // Cached manifest (fetched once on first edit click)\n var manifestCache = null;\n var manifestPromise = null;\n\n function fetchManifest() {\n if (manifestCache) return Promise.resolve(manifestCache);\n if (manifestPromise) return manifestPromise;\n manifestPromise = ecFetch(\"/_emdash/api/manifest\", { credentials: \"same-origin\" })\n .then(function(r) { return r.json(); })\n .then(function(m) {\n // The manifest endpoint wraps the payload in a { data } envelope (ApiResponse shape).\n // Unwrap it so getFieldKind can read manifest.collections directly.\n manifestCache = m && m.data ? m.data : m;\n return manifestCache;\n });\n return manifestPromise;\n }\n\n function getFieldKind(manifest, collection, field) {\n var col = manifest.collections && manifest.collections[collection];\n if (!col || !col.fields) return null;\n var f = col.fields[field];\n return f ? f.kind : null;\n }\n\n // Load manifest early so the first click can resolve field kinds without racing the event.\n if (isEditMode) {\n fetchManifest();\n }\n\n // Save a single field value\n function saveField(collection, id, field, value) {\n setSaveState(\"saving\");\n return ecFetch(\"/_emdash/api/content/\" + encodeURIComponent(collection) + \"/\" + encodeURIComponent(id), {\n method: \"PUT\",\n credentials: \"same-origin\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ data: { [field]: value } }),\n })\n .then(function(res) {\n if (res.ok) {\n setSaveState(\"saved\");\n // A save creates/updates a draft — show unpublished changes\n showUnpublishedChanges(collection, id);\n } else {\n setSaveState(\"error\");\n console.error(\"Save failed:\", res.status);\n }\n })\n .catch(function(err) {\n setSaveState(\"error\");\n console.error(\"Save failed:\", err);\n });\n }\n\n function showUnpublishedChanges(collection, id) {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--pending\">Unpublished changes</span>';\n publishBtn.style.display = \"\";\n publishBtn.disabled = false;\n publishBtn.textContent = \"Publish\";\n publishBtn.onclick = function() { publish(collection, id); };\n }\n\n // Plain text inline editing (contenteditable)\n var currentlyEditing = null;\n\n function startTextEdit(element, annotation) {\n if (currentlyEditing === element) return;\n if (currentlyEditing) endCurrentEdit();\n\n currentlyEditing = element;\n var originalText = element.textContent || \"\";\n\n element.setAttribute(\"data-emdash-editing\", \"\");\n element.contentEditable = \"plaintext-only\";\n element.focus();\n\n // Select all text\n var range = document.createRange();\n range.selectNodeContents(element);\n var sel = window.getSelection();\n sel.removeAllRanges();\n sel.addRange(range);\n\n // Track dirty state via input events\n function handleInput() {\n var current = (element.textContent || \"\").trim();\n if (current !== originalText.trim()) {\n setSaveState(\"unsaved\");\n } else {\n setSaveState(\"idle\");\n }\n }\n\n function handleBlur() {\n element.removeEventListener(\"blur\", handleBlur);\n element.removeEventListener(\"keydown\", handleKeydown);\n element.removeEventListener(\"input\", handleInput);\n element.contentEditable = \"false\";\n element.removeAttribute(\"data-emdash-editing\");\n currentlyEditing = null;\n\n var newValue = (element.textContent || \"\").trim();\n if (newValue !== originalText.trim()) {\n pendingSavePromise = saveField(annotation.collection, annotation.id, annotation.field, newValue).then(function() {\n pendingSavePromise = null;\n }, function() {\n pendingSavePromise = null;\n });\n } else {\n setSaveState(\"idle\");\n }\n }\n\n function handleKeydown(e) {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n element.blur();\n }\n if (e.key === \"Escape\") {\n element.textContent = originalText;\n setSaveState(\"idle\");\n element.blur();\n }\n }\n\n element.addEventListener(\"input\", handleInput);\n element.addEventListener(\"blur\", handleBlur);\n element.addEventListener(\"keydown\", handleKeydown);\n }\n\n function endCurrentEdit() {\n if (currentlyEditing) {\n currentlyEditing.blur();\n }\n }\n\n // Fallback: open admin\n function openAdmin(annotation) {\n var url = \"/_emdash/admin/content/\" + encodeURIComponent(annotation.collection) + \"/\" + encodeURIComponent(annotation.id);\n if (annotation.field) {\n url += \"?field=\" + encodeURIComponent(annotation.field);\n }\n window.open(url, \"emdash-admin\");\n }\n\n // --- Inline image editing ---\n var activeImagePopover = null;\n\n function closeImagePopover() {\n if (activeImagePopover) {\n activeImagePopover.backdrop.remove();\n activeImagePopover.popover.remove();\n if (activeImagePopover.escapeHandler) {\n document.removeEventListener(\"keydown\", activeImagePopover.escapeHandler);\n }\n activeImagePopover = null;\n }\n }\n\n function startImageEdit(element, annotation) {\n closeImagePopover();\n\n // Find the current image value by fetching the entry\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n // Find img element inside the annotated container (or the element itself if it's an img)\n var imgEl = element.tagName === \"IMG\" ? element : element.querySelector(\"img\");\n\n // Fetch current field value from the content API\n ecFetch(\"/_emdash/api/content/\" + encodeURIComponent(collection) + \"/\" + encodeURIComponent(id), {\n credentials: \"same-origin\"\n })\n .then(function(r) { return r.json(); })\n .then(function(entry) {\n var currentValue = entry.data && entry.data[field];\n showImagePopover(element, imgEl, annotation, currentValue);\n })\n .catch(function() {\n // If fetch fails, still show popover with what we can infer from DOM\n showImagePopover(element, imgEl, annotation, null);\n });\n }\n\n function showImagePopover(element, imgEl, annotation, currentValue) {\n closeImagePopover();\n\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n // Position near the element\n var rect = element.getBoundingClientRect();\n var viewportH = window.innerHeight;\n var viewportW = window.innerWidth;\n\n // Create backdrop for click-outside-to-close\n var backdrop = document.createElement(\"div\");\n backdrop.className = \"emdash-img-popover-backdrop\";\n backdrop.addEventListener(\"click\", function(e) {\n if (e.target === backdrop) closeImagePopover();\n });\n\n // Create popover\n var popover = document.createElement(\"div\");\n popover.className = \"emdash-img-popover\";\n\n var currentSrc = currentValue ? (currentValue.previewUrl || currentValue.src) : (imgEl ? imgEl.src : null);\n var currentAlt = currentValue ? (currentValue.alt || \"\") : (imgEl ? (imgEl.alt || \"\") : \"\");\n\n // Build popover HTML\n var html = '';\n html += '<div class=\"emdash-img-popover-header\">';\n html += ' <span class=\"emdash-img-popover-title\">Image</span>';\n html += ' <button class=\"emdash-img-popover-close\" data-action=\"close\">×</button>';\n html += '</div>';\n html += '<div class=\"emdash-img-popover-body\" id=\"emdash-img-main\">';\n\n if (currentSrc) {\n html += '<img class=\"emdash-img-preview\" src=\"' + escapeAttr(currentSrc) + '\" alt=\"\" />';\n } else {\n html += '<div class=\"emdash-img-empty\">No image selected</div>';\n }\n\n html += '<div class=\"emdash-img-field\">';\n html += ' <label for=\"emdash-img-alt\">Alt text</label>';\n html += ' <input type=\"text\" id=\"emdash-img-alt\" value=\"' + escapeAttr(currentAlt) + '\" placeholder=\"Describe the image\" />';\n html += '</div>';\n\n html += '<div class=\"emdash-img-actions\">';\n html += ' <button class=\"emdash-img-btn emdash-img-btn--primary\" data-action=\"browse\">Replace</button>';\n html += ' <label class=\"emdash-img-btn\" style=\"cursor:pointer\">';\n html += ' Upload';\n html += ' <input type=\"file\" accept=\"image/*\" id=\"emdash-img-upload\" style=\"display:none\" />';\n html += ' </label>';\n if (currentSrc) {\n html += ' <button class=\"emdash-img-btn emdash-img-btn--danger\" data-action=\"remove\">Remove</button>';\n }\n html += '</div>';\n html += '</div>';\n\n popover.innerHTML = html;\n\n backdrop.appendChild(popover);\n document.body.appendChild(backdrop);\n\n // Position the popover\n positionPopover(popover, rect, viewportW, viewportH);\n\n // Escape key handler\n function handleEscape(e) {\n if (e.key === \"Escape\") {\n closeImagePopover();\n document.removeEventListener(\"keydown\", handleEscape);\n }\n }\n document.addEventListener(\"keydown\", handleEscape);\n\n activeImagePopover = {\n backdrop: backdrop,\n popover: popover,\n annotation: annotation,\n currentValue: currentValue,\n element: element,\n imgEl: imgEl,\n escapeHandler: handleEscape\n };\n\n // Event handlers\n popover.querySelector('[data-action=\"close\"]').addEventListener(\"click\", closeImagePopover);\n\n popover.querySelector('[data-action=\"browse\"]').addEventListener(\"click\", function() {\n showMediaBrowser(popover, annotation, currentValue, element, imgEl);\n });\n\n var uploadInput = popover.querySelector(\"#emdash-img-upload\");\n uploadInput.addEventListener(\"change\", function(e) {\n var file = e.target.files && e.target.files[0];\n if (file) handleImageUpload(file, popover, annotation, element, imgEl);\n });\n\n var removeBtn = popover.querySelector('[data-action=\"remove\"]');\n if (removeBtn) {\n removeBtn.addEventListener(\"click\", function() {\n saveField(collection, id, field, null).then(function() {\n if (imgEl) {\n imgEl.style.display = \"none\";\n }\n closeImagePopover();\n });\n });\n }\n\n // Save alt text on change (debounced)\n var altInput = popover.querySelector(\"#emdash-img-alt\");\n var altTimer = null;\n altInput.addEventListener(\"input\", function() {\n clearTimeout(altTimer);\n altTimer = setTimeout(function() {\n var newAlt = altInput.value;\n if (currentValue) {\n var updated = Object.assign({}, currentValue, { alt: newAlt });\n saveField(collection, id, field, updated);\n if (imgEl) imgEl.alt = newAlt;\n }\n }, 500);\n });\n\n // Handle drag and drop on the popover body\n var body = popover.querySelector(\".emdash-img-popover-body\");\n body.addEventListener(\"dragover\", function(e) {\n e.preventDefault();\n body.classList.add(\"emdash-img-drop\");\n });\n body.addEventListener(\"dragleave\", function() {\n body.classList.remove(\"emdash-img-drop\");\n });\n body.addEventListener(\"drop\", function(e) {\n e.preventDefault();\n body.classList.remove(\"emdash-img-drop\");\n var file = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];\n if (file && file.type.startsWith(\"image/\")) {\n handleImageUpload(file, popover, annotation, element, imgEl);\n }\n });\n }\n\n function positionPopover(popover, targetRect, viewportW, viewportH) {\n var popoverW = 320;\n var gap = 8;\n\n // Try to place to the right of the element\n var left = targetRect.right + gap;\n var top = targetRect.top;\n\n // If it overflows right, place to the left\n if (left + popoverW > viewportW - 16) {\n left = targetRect.left - popoverW - gap;\n }\n // If it still overflows (narrow viewport), center below\n if (left < 16) {\n left = Math.max(16, (viewportW - popoverW) / 2);\n top = targetRect.bottom + gap;\n }\n // Clamp vertically\n if (top + 400 > viewportH - 80) { // 80 for toolbar\n top = Math.max(16, viewportH - 480);\n }\n if (top < 16) top = 16;\n\n popover.style.left = left + \"px\";\n popover.style.top = top + \"px\";\n }\n\n function escapeAttr(str) {\n return String(str || \"\").replace(/&/g, \"&\").replace(/\"/g, \""\").replace(/</g, \"<\").replace(/>/g, \">\");\n }\n\n function showMediaBrowser(popover, annotation, currentValue, element, imgEl) {\n var mainBody = popover.querySelector(\"#emdash-img-main\");\n if (mainBody) mainBody.style.display = \"none\";\n\n // Remove existing browser if any\n var existing = popover.querySelector(\".emdash-img-browser\");\n if (existing) existing.remove();\n\n var browser = document.createElement(\"div\");\n browser.className = \"emdash-img-browser\";\n\n browser.innerHTML = '<div class=\"emdash-img-browser-header\">' +\n '<span class=\"emdash-img-browser-title\">Media Library</span>' +\n '<button class=\"emdash-img-browser-back\">Back</button>' +\n '</div>' +\n '<div class=\"emdash-img-loading\">Loading\\u2026</div>';\n\n popover.appendChild(browser);\n\n browser.querySelector(\".emdash-img-browser-back\").addEventListener(\"click\", function() {\n browser.remove();\n if (mainBody) mainBody.style.display = \"\";\n });\n\n // Fetch media\n ecFetch(\"/_emdash/api/media?mimeType=image/&limit=30\", { credentials: \"same-origin\" })\n .then(function(r) { return r.json(); })\n .then(function(data) {\n var items = data.items || [];\n var loadingEl = browser.querySelector(\".emdash-img-loading\");\n if (loadingEl) loadingEl.remove();\n\n if (items.length === 0) {\n var empty = document.createElement(\"div\");\n empty.className = \"emdash-img-loading\";\n empty.textContent = \"No images found\";\n browser.appendChild(empty);\n return;\n }\n\n var grid = document.createElement(\"div\");\n grid.className = \"emdash-img-grid\";\n\n items.forEach(function(item) {\n var thumb = document.createElement(\"div\");\n thumb.className = \"emdash-img-grid-item\";\n if (currentValue && currentValue.id === item.id) {\n thumb.classList.add(\"emdash-img-grid-item--selected\");\n }\n var thumbUrl = item.url || item.previewUrl || (\"/_emdash/api/media/file/\" + item.storageKey);\n thumb.innerHTML = '<img src=\"' + escapeAttr(thumbUrl) + '\" alt=\"' + escapeAttr(item.alt || item.filename || \"\") + '\" loading=\"lazy\" />';\n\n thumb.addEventListener(\"click\", function() {\n selectMediaItem(item, annotation, element, imgEl);\n });\n\n grid.appendChild(thumb);\n });\n\n browser.appendChild(grid);\n })\n .catch(function(err) {\n var loadingEl = browser.querySelector(\".emdash-img-loading\");\n if (loadingEl) loadingEl.textContent = \"Failed to load media\";\n console.error(\"Media fetch error:\", err);\n });\n }\n\n function selectMediaItem(item, annotation, element, imgEl) {\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n var isLocal = !item.provider || item.provider === \"local\";\n var itemUrl = item.url || item.previewUrl || (\"/_emdash/api/media/file/\" + item.storageKey);\n\n var newValue = {\n id: item.id,\n provider: item.provider || \"local\",\n src: isLocal ? itemUrl : undefined,\n previewUrl: isLocal ? undefined : itemUrl,\n alt: item.alt || \"\",\n width: item.width,\n height: item.height,\n meta: item.meta\n };\n\n // Clean undefined fields\n Object.keys(newValue).forEach(function(k) {\n if (newValue[k] === undefined) delete newValue[k];\n });\n\n saveField(collection, id, field, newValue).then(function() {\n // Update the image in the DOM\n if (imgEl) {\n imgEl.src = itemUrl;\n imgEl.alt = item.alt || \"\";\n imgEl.style.display = \"\";\n }\n closeImagePopover();\n });\n }\n\n function handleImageUpload(file, popover, annotation, element, imgEl) {\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n // Show uploading state\n var mainBody = popover.querySelector(\"#emdash-img-main\");\n var browserEl = popover.querySelector(\".emdash-img-browser\");\n if (browserEl) browserEl.remove();\n if (mainBody) {\n mainBody.innerHTML = '<div class=\"emdash-img-uploading\">' +\n '<span>Uploading ' + escapeAttr(file.name) + '\\u2026</span>' +\n '</div>';\n mainBody.style.display = \"\";\n }\n\n // Detect dimensions before upload\n var dimPromise = new Promise(function(resolve) {\n if (!file.type.startsWith(\"image/\")) return resolve({});\n var img = new Image();\n img.onload = function() {\n resolve({ width: img.naturalWidth, height: img.naturalHeight });\n URL.revokeObjectURL(img.src);\n };\n img.onerror = function() {\n resolve({});\n URL.revokeObjectURL(img.src);\n };\n img.src = URL.createObjectURL(file);\n });\n\n dimPromise.then(function(dims) {\n // Generate a thumbnail for large images to avoid OOM in server-side\n // blurhash generation on memory-constrained runtimes (Workers).\n // Thumbnail fits within a 64x64 box (scale by max dimension) so that\n // extreme aspect ratios don't explode into a huge canvas client-side.\n var thumbPromise;\n if (dims.width && dims.height && dims.width * dims.height * 4 > 32 * 1024 * 1024) {\n thumbPromise = new Promise(function(resolve) {\n try {\n var maxDim = Math.max(dims.width, dims.height);\n var scale = Math.min(1, 64 / maxDim);\n var thumbW = Math.max(1, Math.round(dims.width * scale));\n var thumbH = Math.max(1, Math.round(dims.height * scale));\n var canvas = document.createElement(\"canvas\");\n canvas.width = thumbW;\n canvas.height = thumbH;\n var ctx = canvas.getContext(\"2d\");\n if (ctx) {\n var img = new Image();\n img.onload = function() {\n try {\n ctx.drawImage(img, 0, 0, thumbW, thumbH);\n canvas.toBlob(function(blob) {\n URL.revokeObjectURL(img.src);\n resolve(blob);\n }, \"image/png\");\n } catch (e) {\n URL.revokeObjectURL(img.src);\n resolve(null);\n }\n };\n img.onerror = function() {\n URL.revokeObjectURL(img.src);\n resolve(null);\n };\n img.src = URL.createObjectURL(file);\n } else {\n resolve(null);\n }\n } catch (e) {\n resolve(null);\n }\n });\n } else {\n thumbPromise = Promise.resolve(null);\n }\n\n return thumbPromise.then(function(thumbnail) {\n var formData = new FormData();\n formData.append(\"file\", file);\n if (dims.width) formData.append(\"width\", String(dims.width));\n if (dims.height) formData.append(\"height\", String(dims.height));\n if (thumbnail) formData.append(\"thumbnail\", thumbnail, \"thumb.png\");\n\n return ecFetch(\"/_emdash/api/media\", {\n method: \"POST\",\n credentials: \"same-origin\",\n body: formData\n });\n });\n })\n .then(function(r) { return r.json(); })\n .then(function(data) {\n if (!data.item) throw new Error(\"Upload failed\");\n var item = data.item;\n selectMediaItem(item, annotation, element, imgEl);\n })\n .catch(function(err) {\n console.error(\"Upload error:\", err);\n setSaveState(\"error\");\n closeImagePopover();\n });\n }\n\n // Click handler for edit mode\n if (isEditMode) {\n document.addEventListener(\"click\", function(e) {\n var target = e.target;\n\n // Don't intercept clicks on elements currently being edited\n if (target.hasAttribute && target.hasAttribute(\"data-emdash-editing\")) return;\n\n // Walk up to find annotated element\n while (target && target !== document.body) {\n if (target.hasAttribute && target.hasAttribute(\"data-emdash-editing\")) return;\n\n var ref = target.getAttribute && target.getAttribute(\"data-emdash-ref\");\n if (ref) {\n try {\n var annotation = JSON.parse(ref);\n\n // Entry-level annotation (no field) — keep walking for a field-level ancestor\n if (!annotation.field) {\n target = target.parentElement;\n continue;\n }\n\n function dispatchInline(kind) {\n closeImagePopover();\n // Portable Text is edited in-page by InlinePortableTextEditor — do not open admin\n if (kind === \"portableText\") {\n return;\n }\n e.preventDefault();\n e.stopPropagation();\n if (kind === \"string\" || kind === \"text\") {\n startTextEdit(target, annotation);\n } else if (kind === \"image\") {\n startImageEdit(target, annotation);\n } else {\n openAdmin(annotation);\n }\n }\n\n if (manifestCache) {\n dispatchInline(getFieldKind(manifestCache, annotation.collection, annotation.field));\n } else {\n fetchManifest().then(function(manifest) {\n dispatchInline(getFieldKind(manifest, annotation.collection, annotation.field));\n });\n }\n } catch (err) {\n console.error(\"Failed to parse emdash ref:\", err);\n }\n return;\n }\n target = target.parentElement;\n }\n }, true);\n }\n\n updateStatus();\n})();\n</script>\n`;\n}\n","/**\n * EmDash Request Context Middleware\n *\n * Sets up AsyncLocalStorage-based request context for query functions.\n * Skips ALS entirely for logged-out users with no CMS signals (fast path).\n *\n * Handles:\n * - Preview tokens: _preview query param with signed HMAC token\n * - Edit mode: emdash-edit-mode cookie (for visual editing)\n * - Toolbar injection: floating pill for authenticated editors\n */\n\nimport { defineMiddleware } from \"astro:middleware\";\n\nimport { verifyPreviewToken, parseContentId } from \"../../preview/tokens.js\";\nimport { getRequestContext, runWithContext } from \"../../request-context.js\";\nimport { renderToolbar } from \"../../visual-editing/toolbar.js\";\n\n/**\n * Inject toolbar HTML into a response if it's an HTML page.\n * Returns the original response if not HTML.\n */\nasync function injectToolbar(response: Response, toolbarHtml: string): Promise<Response> {\n\tconst contentType = response.headers.get(\"content-type\");\n\tif (!contentType?.includes(\"text/html\")) return response;\n\n\tconst html = await response.text();\n\tif (!html.includes(\"</body>\")) return new Response(html, response);\n\n\tconst injected = html.replace(\"</body>\", `${toolbarHtml}</body>`);\n\treturn new Response(injected, {\n\t\tstatus: response.status,\n\t\theaders: response.headers,\n\t});\n}\n\nexport const onRequest = defineMiddleware(async (context, next) => {\n\tconst { cookies, url } = context;\n\n\t// Skip /_emdash routes (admin has its own UI, no rendering context needed)\n\tif (url.pathname.startsWith(\"/_emdash\")) {\n\t\treturn next();\n\t}\n\n\t// Check for authenticated editor (role >= 30)\n\tconst { user } = context.locals;\n\tconst isEditor = !!user && user.role >= 30;\n\n\t// Playground mode: the playground middleware (from @emdash-cms/cloudflare) stashes\n\t// the per-session DO database on locals.__playgroundDb. We set it via ALS here\n\t// (same module instance as the loader) so getDb() picks it up correctly.\n\t//\n\t// `dbIsIsolated: true` tells schema-derived caches (manifest, taxonomy defs,\n\t// byline/term existence probes) to bypass module-scope memoization — each\n\t// playground session is its own database with its own schema, so a cached\n\t// value from another session would be wrong.\n\tconst playgroundDb = context.locals.__playgroundDb;\n\tif (playgroundDb) {\n\t\t// Check if playground user has toggled edit mode on\n\t\tconst hasEditCookie = cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\t\treturn runWithContext({ editMode: hasEditCookie, db: playgroundDb, dbIsIsolated: true }, () =>\n\t\t\tnext(),\n\t\t);\n\t}\n\n\t// Fast path: check for CMS signals before doing any work\n\tconst hasEditCookie = cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\tconst hasPreviewToken = url.searchParams.has(\"_preview\");\n\n\t// No CMS signals and not an editor → skip everything (zero overhead)\n\tif (!hasEditCookie && !hasPreviewToken && !isEditor) {\n\t\treturn next();\n\t}\n\n\t// Determine edit mode: cookie AND authenticated editor\n\tconst editMode = hasEditCookie && isEditor;\n\n\t// Read locale from Astro's i18n routing\n\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Astro context includes currentLocale when i18n is configured\n\tconst locale = (context as { currentLocale?: string }).currentLocale;\n\n\t// Verify preview token if present\n\tlet preview: { collection: string; id: string } | undefined;\n\tif (hasPreviewToken) {\n\t\tconst secret = import.meta.env.EMDASH_PREVIEW_SECRET || import.meta.env.PREVIEW_SECRET || \"\";\n\n\t\tif (secret) {\n\t\t\tconst result = await verifyPreviewToken({ url, secret });\n\t\t\tif (result.valid) {\n\t\t\t\tconst { collection, id } = parseContentId(result.payload.cid);\n\t\t\t\tpreview = { collection, id };\n\t\t\t}\n\t\t}\n\t}\n\n\t// If we have CMS signals, wrap in ALS context\n\tconst needsContext = hasEditCookie || hasPreviewToken;\n\n\tif (needsContext) {\n\t\t// Merge with any outer ALS context (e.g. the per-request D1 session db\n\t\t// set by the runtime middleware). `storage.run()` replaces the store\n\t\t// wholesale, so without the spread the outer `db` would be lost and\n\t\t// loaders would fall back to the singleton non-session dialect.\n\t\tconst parent = getRequestContext();\n\t\treturn runWithContext({ ...parent, editMode, preview, locale }, async () => {\n\t\t\tlet response = await next();\n\n\t\t\t// Preview responses must not be cached -- draft content could leak past token expiry.\n\t\t\t// Clone the response before modifying headers — the original may be immutable.\n\t\t\tif (preview) {\n\t\t\t\tresponse = new Response(response.body, response);\n\t\t\t\tresponse.headers.set(\"Cache-Control\", \"private, no-store\");\n\t\t\t}\n\n\t\t\t// Inject toolbar for authenticated editors\n\t\t\tif (isEditor) {\n\t\t\t\tconst toolbarHtml = renderToolbar({\n\t\t\t\t\teditMode,\n\t\t\t\t\tisPreview: !!preview,\n\t\t\t\t});\n\t\t\t\treturn injectToolbar(response, toolbarHtml);\n\t\t\t}\n\n\t\t\treturn response;\n\t\t});\n\t}\n\n\t// Editor without CMS signals — no ALS needed, but inject toolbar\n\tif (isEditor) {\n\t\tconst response = await next();\n\t\tconst toolbarHtml = renderToolbar({\n\t\t\teditMode: false,\n\t\t\tisPreview: false,\n\t\t});\n\t\treturn injectToolbar(response, toolbarHtml);\n\t}\n\n\treturn next();\n});\n\nexport default onRequest;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"request-context.mjs","names":[],"sources":["../../../src/visual-editing/toolbar.ts","../../../src/astro/middleware/request-context.ts"],"sourcesContent":["/**\n * EmDash Visual Editing Toolbar\n *\n * A floating pill injected via middleware for authenticated editors.\n * Renders as a plain HTML string with inline styles and a <script> tag.\n * No dependencies — works on any page with a </body> tag.\n */\n\ninterface ToolbarConfig {\n\teditMode: boolean;\n\tisPreview: boolean;\n}\n\nexport function renderToolbar(config: ToolbarConfig): string {\n\tconst { editMode, isPreview } = config;\n\n\treturn `\n<!-- EmDash Visual Editing Toolbar -->\n<div id=\"emdash-toolbar\" data-edit-mode=\"${editMode}\" data-preview=\"${isPreview}\">\n <div class=\"emdash-tb-inner\">\n <span class=\"emdash-tb-logo\">EmDash</span>\n\n <div class=\"emdash-tb-divider\"></div>\n\n <label class=\"emdash-tb-toggle\" title=\"Toggle edit mode\">\n <input type=\"checkbox\" id=\"emdash-edit-toggle\" ${editMode ? \"checked\" : \"\"} />\n <span class=\"emdash-tb-toggle-track\">\n <span class=\"emdash-tb-toggle-thumb\"></span>\n </span>\n <span class=\"emdash-tb-toggle-label\">Edit</span>\n </label>\n\n <span class=\"emdash-tb-status\" id=\"emdash-tb-status\"></span>\n\n <span class=\"emdash-tb-save-status\" id=\"emdash-tb-save-status\"></span>\n\n <a class=\"emdash-tb-admin\" id=\"emdash-tb-admin\" href=\"#\" target=\"emdash-admin\" style=\"display:none\" title=\"Open in admin\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/><polyline points=\"15 3 21 3 21 9\"/><line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/></svg>\n </a>\n\n <button class=\"emdash-tb-publish\" id=\"emdash-tb-publish\" style=\"display:none\">Publish</button>\n </div>\n</div>\n\n<style>\n #emdash-toolbar {\n position: fixed;\n bottom: 16px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 999999;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n font-size: 13px;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n }\n\n .emdash-tb-inner {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 16px;\n background: #1a1a1a;\n color: #e0e0e0;\n border-radius: 999px;\n box-shadow: 0 4px 24px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.08);\n white-space: nowrap;\n user-select: none;\n }\n\n .emdash-tb-logo {\n font-weight: 600;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: #fff;\n opacity: 0.7;\n }\n\n .emdash-tb-divider {\n width: 1px;\n height: 16px;\n background: rgba(255,255,255,0.15);\n }\n\n /* Toggle switch */\n .emdash-tb-toggle {\n display: flex;\n align-items: center;\n gap: 6px;\n cursor: pointer;\n }\n\n .emdash-tb-toggle input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .emdash-tb-toggle-track {\n position: relative;\n width: 32px;\n height: 18px;\n background: #444;\n border-radius: 9px;\n transition: background 0.2s;\n }\n\n .emdash-tb-toggle input:checked + .emdash-tb-toggle-track {\n background: #3b82f6;\n }\n\n .emdash-tb-toggle-thumb {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 14px;\n height: 14px;\n background: #fff;\n border-radius: 50%;\n transition: transform 0.2s;\n }\n\n .emdash-tb-toggle input:checked + .emdash-tb-toggle-track .emdash-tb-toggle-thumb {\n transform: translateX(14px);\n }\n\n .emdash-tb-toggle-label {\n font-size: 12px;\n color: #aaa;\n }\n\n .emdash-tb-toggle input:checked ~ .emdash-tb-toggle-label {\n color: #fff;\n }\n\n /* Status area — flex for multiple badges */\n .emdash-tb-status {\n display: inline-flex;\n gap: 6px;\n align-items: center;\n }\n\n /* Badges */\n .emdash-tb-badge {\n display: inline-flex;\n align-items: center;\n padding: 3px 8px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n }\n\n .emdash-tb-badge--preview {\n background: rgba(139,92,246,0.2);\n color: #a78bfa;\n }\n\n .emdash-tb-badge--draft {\n background: rgba(245,158,11,0.2);\n color: #fbbf24;\n }\n\n .emdash-tb-badge--published {\n background: rgba(34,197,94,0.2);\n color: #4ade80;\n }\n\n .emdash-tb-badge--pending {\n background: rgba(59,130,246,0.2);\n color: #60a5fa;\n }\n\n .emdash-tb-badge--unsaved {\n background: rgba(245,158,11,0.2);\n color: #fbbf24;\n }\n\n .emdash-tb-badge--saving {\n background: rgba(148,163,184,0.2);\n color: #94a3b8;\n }\n\n .emdash-tb-badge--saved {\n background: rgba(34,197,94,0.2);\n color: #4ade80;\n transition: opacity 0.3s;\n }\n\n .emdash-tb-badge--error {\n background: rgba(239,68,68,0.2);\n color: #f87171;\n }\n\n /* Admin link */\n .emdash-tb-admin {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #888;\n text-decoration: none;\n padding: 2px;\n border-radius: 4px;\n transition: color 0.15s;\n }\n\n .emdash-tb-admin:hover {\n color: #fff;\n }\n\n /* Publish button */\n .emdash-tb-publish {\n padding: 4px 12px;\n background: #3b82f6;\n color: #fff;\n border: none;\n border-radius: 999px;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.15s;\n font-family: inherit;\n }\n\n .emdash-tb-publish:hover {\n background: #2563eb;\n }\n\n .emdash-tb-publish:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n /* Edit mode: editable hover styles — uses :has() to check toolbar state */\n body:has(#emdash-toolbar[data-edit-mode=\"true\"]) [data-emdash-ref] {\n transition: box-shadow 0.15s, background-color 0.15s;\n }\n\n body:has(#emdash-toolbar[data-edit-mode=\"true\"]) [data-emdash-ref]:hover {\n box-shadow: 0 0 0 2px rgba(59,130,246,0.5);\n border-radius: 4px;\n background-color: rgba(59,130,246,0.04);\n cursor: text;\n }\n\n /* Active editing state — override hover pencil cursor */\n [data-emdash-editing] {\n box-shadow: 0 0 0 2px #3b82f6 !important;\n border-radius: 4px !important;\n background-color: rgba(59,130,246,0.04) !important;\n cursor: text !important;\n }\n\n /* Suppress browser focus ring on contenteditable and tiptap editor */\n [data-emdash-editing]:focus,\n [data-emdash-ref] .tiptap:focus,\n [data-emdash-ref] .ProseMirror:focus {\n outline: none !important;\n }\n\n /* Image editor popover */\n .emdash-img-popover {\n position: fixed;\n z-index: 1000000;\n background: #1a1a1a;\n border-radius: 12px;\n box-shadow: 0 8px 32px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.08);\n color: #e0e0e0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n font-size: 13px;\n width: 320px;\n overflow: hidden;\n animation: emdash-img-fadein 0.15s ease-out;\n }\n\n @keyframes emdash-img-fadein {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: translateY(0); }\n }\n\n .emdash-img-popover-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 12px;\n border-bottom: 1px solid rgba(255,255,255,0.08);\n }\n\n .emdash-img-popover-title {\n font-weight: 600;\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: #999;\n }\n\n .emdash-img-popover-close {\n background: none;\n border: none;\n color: #666;\n cursor: pointer;\n padding: 2px;\n line-height: 1;\n font-size: 16px;\n border-radius: 4px;\n transition: color 0.15s;\n }\n\n .emdash-img-popover-close:hover {\n color: #fff;\n }\n\n .emdash-img-popover-body {\n padding: 12px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n }\n\n .emdash-img-preview {\n width: 100%;\n max-height: 160px;\n object-fit: contain;\n border-radius: 6px;\n background: #111;\n }\n\n .emdash-img-empty {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 80px;\n border: 2px dashed rgba(255,255,255,0.15);\n border-radius: 6px;\n color: #666;\n font-size: 12px;\n }\n\n .emdash-img-field {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n .emdash-img-field label {\n font-size: 11px;\n font-weight: 600;\n color: #888;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n }\n\n .emdash-img-field input[type=\"text\"] {\n background: #111;\n border: 1px solid rgba(255,255,255,0.12);\n border-radius: 6px;\n color: #e0e0e0;\n padding: 6px 8px;\n font-size: 13px;\n font-family: inherit;\n outline: none;\n transition: border-color 0.15s;\n }\n\n .emdash-img-field input[type=\"text\"]:focus {\n border-color: #3b82f6;\n }\n\n .emdash-img-actions {\n display: flex;\n gap: 6px;\n }\n\n .emdash-img-btn {\n flex: 1;\n padding: 6px 10px;\n border: 1px solid rgba(255,255,255,0.12);\n border-radius: 6px;\n background: #222;\n color: #e0e0e0;\n font-size: 12px;\n font-family: inherit;\n cursor: pointer;\n transition: background 0.15s, border-color 0.15s;\n text-align: center;\n white-space: nowrap;\n }\n\n .emdash-img-btn:hover {\n background: #333;\n border-color: rgba(255,255,255,0.2);\n }\n\n .emdash-img-btn--primary {\n background: #3b82f6;\n border-color: #3b82f6;\n color: #fff;\n }\n\n .emdash-img-btn--primary:hover {\n background: #2563eb;\n border-color: #2563eb;\n }\n\n .emdash-img-btn--danger {\n color: #f87171;\n border-color: rgba(248,113,113,0.3);\n }\n\n .emdash-img-btn--danger:hover {\n background: rgba(248,113,113,0.1);\n border-color: rgba(248,113,113,0.5);\n }\n\n /* Media browser within the popover */\n .emdash-img-browser {\n border-top: 1px solid rgba(255,255,255,0.08);\n padding: 12px;\n }\n\n .emdash-img-browser-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 8px;\n }\n\n .emdash-img-browser-title {\n font-size: 12px;\n font-weight: 600;\n color: #999;\n }\n\n .emdash-img-browser-back {\n background: none;\n border: none;\n color: #3b82f6;\n cursor: pointer;\n font-size: 12px;\n font-family: inherit;\n padding: 2px 4px;\n }\n\n .emdash-img-browser-back:hover {\n text-decoration: underline;\n }\n\n .emdash-img-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 6px;\n max-height: 240px;\n overflow-y: auto;\n }\n\n .emdash-img-grid-item {\n aspect-ratio: 1;\n border-radius: 4px;\n overflow: hidden;\n cursor: pointer;\n border: 2px solid transparent;\n transition: border-color 0.15s;\n background: #111;\n }\n\n .emdash-img-grid-item:hover {\n border-color: rgba(59,130,246,0.5);\n }\n\n .emdash-img-grid-item--selected {\n border-color: #3b82f6;\n }\n\n .emdash-img-grid-item img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n }\n\n .emdash-img-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 80px;\n color: #666;\n font-size: 12px;\n }\n\n .emdash-img-drop {\n border: 2px dashed #3b82f6;\n background: rgba(59,130,246,0.05);\n }\n\n .emdash-img-uploading {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 0;\n color: #999;\n font-size: 12px;\n }\n\n .emdash-img-popover-backdrop {\n position: fixed;\n inset: 0;\n z-index: 999999;\n }\n</style>\n\n<script>\n(function() {\n var toolbar = document.getElementById(\"emdash-toolbar\");\n var toggle = document.getElementById(\"emdash-edit-toggle\");\n var statusEl = document.getElementById(\"emdash-tb-status\");\n var saveStatusEl = document.getElementById(\"emdash-tb-save-status\");\n var publishBtn = document.getElementById(\"emdash-tb-publish\");\n if (!toolbar || !toggle || !statusEl || !publishBtn || !saveStatusEl) return;\n\n var isEditMode = toolbar.getAttribute(\"data-edit-mode\") === \"true\";\n\n // CSRF-protected fetch — adds X-EmDash-Request header to all API calls\n function ecFetch(url, init) {\n init = init || {};\n init.headers = Object.assign({ \"X-EmDash-Request\": \"1\" }, init.headers || {});\n return fetch(url, init);\n }\n\n // --- Save status tracking ---\n var saveState = \"idle\"; // idle | unsaved | saving | saved | error\n var saveHideTimer = null;\n var pendingSavePromise = null;\n\n function setSaveState(state) {\n saveState = state;\n clearTimeout(saveHideTimer);\n\n switch (state) {\n case \"unsaved\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--unsaved\">Unsaved</span>';\n break;\n case \"saving\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--saving\">Saving\\u2026</span>';\n break;\n case \"saved\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--saved\">Saved</span>';\n saveHideTimer = setTimeout(function() {\n saveStatusEl.innerHTML = \"\";\n saveState = \"idle\";\n }, 2000);\n break;\n case \"error\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--error\">Save failed</span>';\n saveHideTimer = setTimeout(function() {\n saveStatusEl.innerHTML = \"\";\n saveState = \"idle\";\n }, 3000);\n break;\n default:\n saveStatusEl.innerHTML = \"\";\n }\n }\n\n // Listen for save events from inline editors (e.g. PT editor)\n document.addEventListener(\"emdash:save\", function(e) {\n var detail = e.detail || {};\n if (detail.state) {\n setSaveState(detail.state);\n }\n });\n\n document.addEventListener(\"emdash:content-changed\", function(e) {\n var detail = e.detail || {};\n if (detail.collection && detail.id) {\n showUnpublishedChanges(detail.collection, detail.id);\n }\n });\n\n // --- Entry status ---\n var entryRef = null;\n\n function updateStatus() {\n if (!isEditMode) {\n statusEl.innerHTML = \"\";\n publishBtn.style.display = \"none\";\n return;\n }\n\n var first = document.querySelector(\"[data-emdash-ref]\");\n if (!first) {\n statusEl.innerHTML = \"\";\n publishBtn.style.display = \"none\";\n return;\n }\n\n try {\n var ref = JSON.parse(first.getAttribute(\"data-emdash-ref\"));\n entryRef = ref;\n if (!ref.status) return;\n\n // Show admin link\n var adminLink = document.getElementById(\"emdash-tb-admin\");\n if (adminLink) {\n adminLink.href = \"/_emdash/admin/content/\" + encodeURIComponent(ref.collection) + \"/\" + encodeURIComponent(ref.id);\n adminLink.style.display = \"\";\n }\n\n if (ref.status === \"draft\") {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--draft\">Draft</span>';\n publishBtn.style.display = \"\";\n publishBtn.onclick = function() { publish(ref.collection, ref.id); };\n } else if (ref.status === \"published\" && ref.hasDraft) {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--pending\">Unpublished changes</span>';\n publishBtn.style.display = \"\";\n publishBtn.onclick = function() { publish(ref.collection, ref.id); };\n } else if (ref.status === \"published\") {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--published\">Published</span>';\n publishBtn.style.display = \"none\";\n }\n } catch (e) {\n // ignore parse errors\n }\n }\n\n // Publish action\n function publish(collection, id) {\n if (pendingSavePromise) {\n pendingSavePromise.then(function() { publish(collection, id); });\n return;\n }\n\n publishBtn.disabled = true;\n publishBtn.textContent = \"Publishing\\u2026\";\n\n ecFetch(\"/_emdash/api/content/\" + encodeURIComponent(collection) + \"/\" + encodeURIComponent(id) + \"/publish\", {\n method: \"POST\",\n credentials: \"same-origin\",\n })\n .then(function(res) {\n if (res.ok) {\n if (document.startViewTransition) {\n document.startViewTransition(function() { location.reload(); });\n } else {\n location.reload();\n }\n } else {\n publishBtn.disabled = false;\n publishBtn.textContent = \"Publish\";\n console.error(\"Publish failed:\", res.status);\n }\n })\n .catch(function(err) {\n publishBtn.disabled = false;\n publishBtn.textContent = \"Publish\";\n console.error(\"Publish failed:\", err);\n });\n }\n\n // Edit mode toggle\n toggle.addEventListener(\"change\", function() {\n if (toggle.checked) {\n document.cookie = \"emdash-edit-mode=true;path=/;samesite=lax\";\n } else {\n document.cookie = \"emdash-edit-mode=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT\";\n }\n\n if (document.startViewTransition) {\n document.startViewTransition(function() { location.replace(location.href); });\n } else {\n location.replace(location.href);\n }\n });\n\n // --- Inline editing ---\n\n // Cached manifest (fetched once on first edit click)\n var manifestCache = null;\n var manifestPromise = null;\n\n function fetchManifest() {\n if (manifestCache) return Promise.resolve(manifestCache);\n if (manifestPromise) return manifestPromise;\n manifestPromise = ecFetch(\"/_emdash/api/manifest\", { credentials: \"same-origin\" })\n .then(function(r) { return r.json(); })\n .then(function(m) {\n // The manifest endpoint wraps the payload in a { data } envelope (ApiResponse shape).\n // Unwrap it so getFieldKind can read manifest.collections directly.\n manifestCache = m && m.data ? m.data : m;\n return manifestCache;\n });\n return manifestPromise;\n }\n\n function getFieldKind(manifest, collection, field) {\n var col = manifest.collections && manifest.collections[collection];\n if (!col || !col.fields) return null;\n var f = col.fields[field];\n return f ? f.kind : null;\n }\n\n // Load manifest early so the first click can resolve field kinds without racing the event.\n if (isEditMode) {\n fetchManifest();\n }\n\n // Save a single field value\n function saveField(collection, id, field, value) {\n setSaveState(\"saving\");\n return ecFetch(\"/_emdash/api/content/\" + encodeURIComponent(collection) + \"/\" + encodeURIComponent(id), {\n method: \"PUT\",\n credentials: \"same-origin\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ data: { [field]: value } }),\n })\n .then(function(res) {\n if (res.ok) {\n setSaveState(\"saved\");\n // A save creates/updates a draft — show unpublished changes\n showUnpublishedChanges(collection, id);\n } else {\n setSaveState(\"error\");\n console.error(\"Save failed:\", res.status);\n }\n })\n .catch(function(err) {\n setSaveState(\"error\");\n console.error(\"Save failed:\", err);\n });\n }\n\n function showUnpublishedChanges(collection, id) {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--pending\">Unpublished changes</span>';\n publishBtn.style.display = \"\";\n publishBtn.disabled = false;\n publishBtn.textContent = \"Publish\";\n publishBtn.onclick = function() { publish(collection, id); };\n }\n\n // Plain text inline editing (contenteditable)\n var currentlyEditing = null;\n\n function startTextEdit(element, annotation) {\n if (currentlyEditing === element) return;\n if (currentlyEditing) endCurrentEdit();\n\n currentlyEditing = element;\n var originalText = element.textContent || \"\";\n\n element.setAttribute(\"data-emdash-editing\", \"\");\n element.contentEditable = \"plaintext-only\";\n element.focus();\n\n // Select all text\n var range = document.createRange();\n range.selectNodeContents(element);\n var sel = window.getSelection();\n sel.removeAllRanges();\n sel.addRange(range);\n\n // Track dirty state via input events\n function handleInput() {\n var current = (element.textContent || \"\").trim();\n if (current !== originalText.trim()) {\n setSaveState(\"unsaved\");\n } else {\n setSaveState(\"idle\");\n }\n }\n\n function handleBlur() {\n element.removeEventListener(\"blur\", handleBlur);\n element.removeEventListener(\"keydown\", handleKeydown);\n element.removeEventListener(\"input\", handleInput);\n element.contentEditable = \"false\";\n element.removeAttribute(\"data-emdash-editing\");\n currentlyEditing = null;\n\n var newValue = (element.textContent || \"\").trim();\n if (newValue !== originalText.trim()) {\n pendingSavePromise = saveField(annotation.collection, annotation.id, annotation.field, newValue).then(function() {\n pendingSavePromise = null;\n }, function() {\n pendingSavePromise = null;\n });\n } else {\n setSaveState(\"idle\");\n }\n }\n\n function handleKeydown(e) {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n element.blur();\n }\n if (e.key === \"Escape\") {\n element.textContent = originalText;\n setSaveState(\"idle\");\n element.blur();\n }\n }\n\n element.addEventListener(\"input\", handleInput);\n element.addEventListener(\"blur\", handleBlur);\n element.addEventListener(\"keydown\", handleKeydown);\n }\n\n function endCurrentEdit() {\n if (currentlyEditing) {\n currentlyEditing.blur();\n }\n }\n\n // Fallback: open admin\n function openAdmin(annotation) {\n var url = \"/_emdash/admin/content/\" + encodeURIComponent(annotation.collection) + \"/\" + encodeURIComponent(annotation.id);\n if (annotation.field) {\n url += \"?field=\" + encodeURIComponent(annotation.field);\n }\n window.open(url, \"emdash-admin\");\n }\n\n // --- Inline image editing ---\n var activeImagePopover = null;\n\n function closeImagePopover() {\n if (activeImagePopover) {\n activeImagePopover.backdrop.remove();\n activeImagePopover.popover.remove();\n if (activeImagePopover.escapeHandler) {\n document.removeEventListener(\"keydown\", activeImagePopover.escapeHandler);\n }\n activeImagePopover = null;\n }\n }\n\n function startImageEdit(element, annotation) {\n closeImagePopover();\n\n // Find the current image value by fetching the entry\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n // Find img element inside the annotated container (or the element itself if it's an img)\n var imgEl = element.tagName === \"IMG\" ? element : element.querySelector(\"img\");\n\n // Fetch current field value from the content API\n ecFetch(\"/_emdash/api/content/\" + encodeURIComponent(collection) + \"/\" + encodeURIComponent(id), {\n credentials: \"same-origin\"\n })\n .then(function(r) { return r.json(); })\n .then(function(entry) {\n var currentValue = entry.data && entry.data[field];\n showImagePopover(element, imgEl, annotation, currentValue);\n })\n .catch(function() {\n // If fetch fails, still show popover with what we can infer from DOM\n showImagePopover(element, imgEl, annotation, null);\n });\n }\n\n function showImagePopover(element, imgEl, annotation, currentValue) {\n closeImagePopover();\n\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n // Position near the element\n var rect = element.getBoundingClientRect();\n var viewportH = window.innerHeight;\n var viewportW = window.innerWidth;\n\n // Create backdrop for click-outside-to-close\n var backdrop = document.createElement(\"div\");\n backdrop.className = \"emdash-img-popover-backdrop\";\n backdrop.addEventListener(\"click\", function(e) {\n if (e.target === backdrop) closeImagePopover();\n });\n\n // Create popover\n var popover = document.createElement(\"div\");\n popover.className = \"emdash-img-popover\";\n\n var currentSrc = currentValue ? (currentValue.previewUrl || currentValue.src) : (imgEl ? imgEl.src : null);\n var currentAlt = currentValue ? (currentValue.alt || \"\") : (imgEl ? (imgEl.alt || \"\") : \"\");\n\n // Build popover HTML\n var html = '';\n html += '<div class=\"emdash-img-popover-header\">';\n html += ' <span class=\"emdash-img-popover-title\">Image</span>';\n html += ' <button class=\"emdash-img-popover-close\" data-action=\"close\">×</button>';\n html += '</div>';\n html += '<div class=\"emdash-img-popover-body\" id=\"emdash-img-main\">';\n\n if (currentSrc) {\n html += '<img class=\"emdash-img-preview\" src=\"' + escapeAttr(currentSrc) + '\" alt=\"\" />';\n } else {\n html += '<div class=\"emdash-img-empty\">No image selected</div>';\n }\n\n html += '<div class=\"emdash-img-field\">';\n html += ' <label for=\"emdash-img-alt\">Alt text</label>';\n html += ' <input type=\"text\" id=\"emdash-img-alt\" value=\"' + escapeAttr(currentAlt) + '\" placeholder=\"Describe the image\" />';\n html += '</div>';\n\n html += '<div class=\"emdash-img-actions\">';\n html += ' <button class=\"emdash-img-btn emdash-img-btn--primary\" data-action=\"browse\">Replace</button>';\n html += ' <label class=\"emdash-img-btn\" style=\"cursor:pointer\">';\n html += ' Upload';\n html += ' <input type=\"file\" accept=\"image/*\" id=\"emdash-img-upload\" style=\"display:none\" />';\n html += ' </label>';\n if (currentSrc) {\n html += ' <button class=\"emdash-img-btn emdash-img-btn--danger\" data-action=\"remove\">Remove</button>';\n }\n html += '</div>';\n html += '</div>';\n\n popover.innerHTML = html;\n\n backdrop.appendChild(popover);\n document.body.appendChild(backdrop);\n\n // Position the popover\n positionPopover(popover, rect, viewportW, viewportH);\n\n // Escape key handler\n function handleEscape(e) {\n if (e.key === \"Escape\") {\n closeImagePopover();\n document.removeEventListener(\"keydown\", handleEscape);\n }\n }\n document.addEventListener(\"keydown\", handleEscape);\n\n activeImagePopover = {\n backdrop: backdrop,\n popover: popover,\n annotation: annotation,\n currentValue: currentValue,\n element: element,\n imgEl: imgEl,\n escapeHandler: handleEscape\n };\n\n // Event handlers\n popover.querySelector('[data-action=\"close\"]').addEventListener(\"click\", closeImagePopover);\n\n popover.querySelector('[data-action=\"browse\"]').addEventListener(\"click\", function() {\n showMediaBrowser(popover, annotation, currentValue, element, imgEl);\n });\n\n var uploadInput = popover.querySelector(\"#emdash-img-upload\");\n uploadInput.addEventListener(\"change\", function(e) {\n var file = e.target.files && e.target.files[0];\n if (file) handleImageUpload(file, popover, annotation, element, imgEl);\n });\n\n var removeBtn = popover.querySelector('[data-action=\"remove\"]');\n if (removeBtn) {\n removeBtn.addEventListener(\"click\", function() {\n saveField(collection, id, field, null).then(function() {\n if (imgEl) {\n imgEl.style.display = \"none\";\n }\n closeImagePopover();\n });\n });\n }\n\n // Save alt text on change (debounced)\n var altInput = popover.querySelector(\"#emdash-img-alt\");\n var altTimer = null;\n altInput.addEventListener(\"input\", function() {\n clearTimeout(altTimer);\n altTimer = setTimeout(function() {\n var newAlt = altInput.value;\n if (currentValue) {\n var updated = Object.assign({}, currentValue, { alt: newAlt });\n saveField(collection, id, field, updated);\n if (imgEl) imgEl.alt = newAlt;\n }\n }, 500);\n });\n\n // Handle drag and drop on the popover body\n var body = popover.querySelector(\".emdash-img-popover-body\");\n body.addEventListener(\"dragover\", function(e) {\n e.preventDefault();\n body.classList.add(\"emdash-img-drop\");\n });\n body.addEventListener(\"dragleave\", function() {\n body.classList.remove(\"emdash-img-drop\");\n });\n body.addEventListener(\"drop\", function(e) {\n e.preventDefault();\n body.classList.remove(\"emdash-img-drop\");\n var file = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];\n if (file && file.type.startsWith(\"image/\")) {\n handleImageUpload(file, popover, annotation, element, imgEl);\n }\n });\n }\n\n function positionPopover(popover, targetRect, viewportW, viewportH) {\n var popoverW = 320;\n var gap = 8;\n\n // Try to place to the right of the element\n var left = targetRect.right + gap;\n var top = targetRect.top;\n\n // If it overflows right, place to the left\n if (left + popoverW > viewportW - 16) {\n left = targetRect.left - popoverW - gap;\n }\n // If it still overflows (narrow viewport), center below\n if (left < 16) {\n left = Math.max(16, (viewportW - popoverW) / 2);\n top = targetRect.bottom + gap;\n }\n // Clamp vertically\n if (top + 400 > viewportH - 80) { // 80 for toolbar\n top = Math.max(16, viewportH - 480);\n }\n if (top < 16) top = 16;\n\n popover.style.left = left + \"px\";\n popover.style.top = top + \"px\";\n }\n\n function escapeAttr(str) {\n return String(str || \"\").replace(/&/g, \"&\").replace(/\"/g, \""\").replace(/</g, \"<\").replace(/>/g, \">\");\n }\n\n function showMediaBrowser(popover, annotation, currentValue, element, imgEl) {\n var mainBody = popover.querySelector(\"#emdash-img-main\");\n if (mainBody) mainBody.style.display = \"none\";\n\n // Remove existing browser if any\n var existing = popover.querySelector(\".emdash-img-browser\");\n if (existing) existing.remove();\n\n var browser = document.createElement(\"div\");\n browser.className = \"emdash-img-browser\";\n\n browser.innerHTML = '<div class=\"emdash-img-browser-header\">' +\n '<span class=\"emdash-img-browser-title\">Media Library</span>' +\n '<button class=\"emdash-img-browser-back\">Back</button>' +\n '</div>' +\n '<div class=\"emdash-img-loading\">Loading\\u2026</div>';\n\n popover.appendChild(browser);\n\n browser.querySelector(\".emdash-img-browser-back\").addEventListener(\"click\", function() {\n browser.remove();\n if (mainBody) mainBody.style.display = \"\";\n });\n\n // Fetch media\n ecFetch(\"/_emdash/api/media?mimeType=image/&limit=30\", { credentials: \"same-origin\" })\n .then(function(r) { return r.json(); })\n .then(function(data) {\n var items = data.items || [];\n var loadingEl = browser.querySelector(\".emdash-img-loading\");\n if (loadingEl) loadingEl.remove();\n\n if (items.length === 0) {\n var empty = document.createElement(\"div\");\n empty.className = \"emdash-img-loading\";\n empty.textContent = \"No images found\";\n browser.appendChild(empty);\n return;\n }\n\n var grid = document.createElement(\"div\");\n grid.className = \"emdash-img-grid\";\n\n items.forEach(function(item) {\n var thumb = document.createElement(\"div\");\n thumb.className = \"emdash-img-grid-item\";\n if (currentValue && currentValue.id === item.id) {\n thumb.classList.add(\"emdash-img-grid-item--selected\");\n }\n var thumbUrl = item.url || item.previewUrl || (\"/_emdash/api/media/file/\" + item.storageKey);\n thumb.innerHTML = '<img src=\"' + escapeAttr(thumbUrl) + '\" alt=\"' + escapeAttr(item.alt || item.filename || \"\") + '\" loading=\"lazy\" />';\n\n thumb.addEventListener(\"click\", function() {\n selectMediaItem(item, annotation, element, imgEl);\n });\n\n grid.appendChild(thumb);\n });\n\n browser.appendChild(grid);\n })\n .catch(function(err) {\n var loadingEl = browser.querySelector(\".emdash-img-loading\");\n if (loadingEl) loadingEl.textContent = \"Failed to load media\";\n console.error(\"Media fetch error:\", err);\n });\n }\n\n function selectMediaItem(item, annotation, element, imgEl) {\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n var isLocal = !item.provider || item.provider === \"local\";\n var itemUrl = item.url || item.previewUrl || (\"/_emdash/api/media/file/\" + item.storageKey);\n\n var newValue = {\n id: item.id,\n provider: item.provider || \"local\",\n src: isLocal ? itemUrl : undefined,\n previewUrl: isLocal ? undefined : itemUrl,\n alt: item.alt || \"\",\n width: item.width,\n height: item.height,\n meta: item.meta\n };\n\n // Clean undefined fields\n Object.keys(newValue).forEach(function(k) {\n if (newValue[k] === undefined) delete newValue[k];\n });\n\n saveField(collection, id, field, newValue).then(function() {\n // Update the image in the DOM\n if (imgEl) {\n imgEl.src = itemUrl;\n imgEl.alt = item.alt || \"\";\n imgEl.style.display = \"\";\n }\n closeImagePopover();\n });\n }\n\n function handleImageUpload(file, popover, annotation, element, imgEl) {\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n // Show uploading state\n var mainBody = popover.querySelector(\"#emdash-img-main\");\n var browserEl = popover.querySelector(\".emdash-img-browser\");\n if (browserEl) browserEl.remove();\n if (mainBody) {\n mainBody.innerHTML = '<div class=\"emdash-img-uploading\">' +\n '<span>Uploading ' + escapeAttr(file.name) + '\\u2026</span>' +\n '</div>';\n mainBody.style.display = \"\";\n }\n\n // Detect dimensions before upload\n var dimPromise = new Promise(function(resolve) {\n if (!file.type.startsWith(\"image/\")) return resolve({});\n var img = new Image();\n img.onload = function() {\n resolve({ width: img.naturalWidth, height: img.naturalHeight });\n URL.revokeObjectURL(img.src);\n };\n img.onerror = function() {\n resolve({});\n URL.revokeObjectURL(img.src);\n };\n img.src = URL.createObjectURL(file);\n });\n\n dimPromise.then(function(dims) {\n // Generate a thumbnail for large images to avoid OOM in server-side\n // blurhash generation on memory-constrained runtimes (Workers).\n // Thumbnail fits within a 64x64 box (scale by max dimension) so that\n // extreme aspect ratios don't explode into a huge canvas client-side.\n var thumbPromise;\n if (dims.width && dims.height && dims.width * dims.height * 4 > 32 * 1024 * 1024) {\n thumbPromise = new Promise(function(resolve) {\n try {\n var maxDim = Math.max(dims.width, dims.height);\n var scale = Math.min(1, 64 / maxDim);\n var thumbW = Math.max(1, Math.round(dims.width * scale));\n var thumbH = Math.max(1, Math.round(dims.height * scale));\n var canvas = document.createElement(\"canvas\");\n canvas.width = thumbW;\n canvas.height = thumbH;\n var ctx = canvas.getContext(\"2d\");\n if (ctx) {\n var img = new Image();\n img.onload = function() {\n try {\n ctx.drawImage(img, 0, 0, thumbW, thumbH);\n canvas.toBlob(function(blob) {\n URL.revokeObjectURL(img.src);\n resolve(blob);\n }, \"image/png\");\n } catch (e) {\n URL.revokeObjectURL(img.src);\n resolve(null);\n }\n };\n img.onerror = function() {\n URL.revokeObjectURL(img.src);\n resolve(null);\n };\n img.src = URL.createObjectURL(file);\n } else {\n resolve(null);\n }\n } catch (e) {\n resolve(null);\n }\n });\n } else {\n thumbPromise = Promise.resolve(null);\n }\n\n return thumbPromise.then(function(thumbnail) {\n var formData = new FormData();\n formData.append(\"file\", file);\n if (dims.width) formData.append(\"width\", String(dims.width));\n if (dims.height) formData.append(\"height\", String(dims.height));\n if (thumbnail) formData.append(\"thumbnail\", thumbnail, \"thumb.png\");\n\n return ecFetch(\"/_emdash/api/media\", {\n method: \"POST\",\n credentials: \"same-origin\",\n body: formData\n });\n });\n })\n .then(function(r) { return r.json(); })\n .then(function(data) {\n if (!data.item) throw new Error(\"Upload failed\");\n var item = data.item;\n selectMediaItem(item, annotation, element, imgEl);\n })\n .catch(function(err) {\n console.error(\"Upload error:\", err);\n setSaveState(\"error\");\n closeImagePopover();\n });\n }\n\n // Click handler for edit mode\n if (isEditMode) {\n document.addEventListener(\"click\", function(e) {\n var target = e.target;\n\n // Don't intercept clicks on elements currently being edited\n if (target.hasAttribute && target.hasAttribute(\"data-emdash-editing\")) return;\n\n // Walk up to find annotated element\n while (target && target !== document.body) {\n if (target.hasAttribute && target.hasAttribute(\"data-emdash-editing\")) return;\n\n var ref = target.getAttribute && target.getAttribute(\"data-emdash-ref\");\n if (ref) {\n try {\n var annotation = JSON.parse(ref);\n\n // Entry-level annotation (no field) — keep walking for a field-level ancestor\n if (!annotation.field) {\n target = target.parentElement;\n continue;\n }\n\n function dispatchInline(kind) {\n closeImagePopover();\n // Portable Text is edited in-page by InlinePortableTextEditor — do not open admin\n if (kind === \"portableText\") {\n return;\n }\n e.preventDefault();\n e.stopPropagation();\n if (kind === \"string\" || kind === \"text\") {\n startTextEdit(target, annotation);\n } else if (kind === \"image\") {\n startImageEdit(target, annotation);\n } else {\n openAdmin(annotation);\n }\n }\n\n if (manifestCache) {\n dispatchInline(getFieldKind(manifestCache, annotation.collection, annotation.field));\n } else {\n fetchManifest().then(function(manifest) {\n dispatchInline(getFieldKind(manifest, annotation.collection, annotation.field));\n });\n }\n } catch (err) {\n console.error(\"Failed to parse emdash ref:\", err);\n }\n return;\n }\n target = target.parentElement;\n }\n }, true);\n }\n\n updateStatus();\n})();\n</script>\n`;\n}\n","/**\n * EmDash Request Context Middleware\n *\n * Sets up AsyncLocalStorage-based request context for query functions.\n * Skips ALS entirely for logged-out users with no CMS signals (fast path).\n *\n * Handles:\n * - Preview tokens: _preview query param with signed HMAC token\n * - Edit mode: emdash-edit-mode cookie (for visual editing)\n * - Toolbar injection: floating pill for authenticated editors\n */\n\nimport { defineMiddleware } from \"astro:middleware\";\n\nimport { resolveSecretsCached } from \"#config/secrets.js\";\n\nimport { verifyPreviewToken, parseContentId } from \"../../preview/tokens.js\";\nimport { getRequestContext, runWithContext } from \"../../request-context.js\";\nimport { renderToolbar } from \"../../visual-editing/toolbar.js\";\n\n/**\n * Inject toolbar HTML into a response if it's an HTML page.\n * Returns the original response if not HTML.\n */\nasync function injectToolbar(response: Response, toolbarHtml: string): Promise<Response> {\n\tconst contentType = response.headers.get(\"content-type\");\n\tif (!contentType?.includes(\"text/html\")) return response;\n\n\tconst html = await response.text();\n\tif (!html.includes(\"</body>\")) return new Response(html, response);\n\n\tconst injected = html.replace(\"</body>\", `${toolbarHtml}</body>`);\n\treturn new Response(injected, {\n\t\tstatus: response.status,\n\t\theaders: response.headers,\n\t});\n}\n\nexport const onRequest = defineMiddleware(async (context, next) => {\n\tconst { cookies, url } = context;\n\n\t// Skip /_emdash routes (admin has its own UI, no rendering context needed)\n\tif (url.pathname.startsWith(\"/_emdash\")) {\n\t\treturn next();\n\t}\n\n\t// Check for authenticated editor (role >= 30)\n\tconst { user } = context.locals;\n\tconst isEditor = !!user && user.role >= 30;\n\n\t// Playground mode: the playground middleware (from @emdash-cms/cloudflare) stashes\n\t// the per-session DO database on locals.__playgroundDb. We set it via ALS here\n\t// (same module instance as the loader) so getDb() picks it up correctly.\n\t//\n\t// `dbIsIsolated: true` tells schema-derived caches (manifest, taxonomy defs,\n\t// byline/term existence probes) to bypass module-scope memoization — each\n\t// playground session is its own database with its own schema, so a cached\n\t// value from another session would be wrong.\n\tconst playgroundDb = context.locals.__playgroundDb;\n\tif (playgroundDb) {\n\t\t// Check if playground user has toggled edit mode on\n\t\tconst hasEditCookie = cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\t\treturn runWithContext({ editMode: hasEditCookie, db: playgroundDb, dbIsIsolated: true }, () =>\n\t\t\tnext(),\n\t\t);\n\t}\n\n\t// Fast path: check for CMS signals before doing any work\n\tconst hasEditCookie = cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\tconst hasPreviewToken = url.searchParams.has(\"_preview\");\n\n\t// No CMS signals and not an editor → skip everything (zero overhead)\n\tif (!hasEditCookie && !hasPreviewToken && !isEditor) {\n\t\treturn next();\n\t}\n\n\t// Determine edit mode: cookie AND authenticated editor\n\tconst editMode = hasEditCookie && isEditor;\n\n\t// Read locale from Astro's i18n routing\n\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Astro context includes currentLocale when i18n is configured\n\tconst locale = (context as { currentLocale?: string }).currentLocale;\n\n\t// Verify preview token if present.\n\t// The preview secret is resolved via `resolveSecretsCached`: env wins,\n\t// otherwise a DB-stored value is read (or generated on first need).\n\t// `emdash.db` is set by the runtime middleware which runs first; the\n\t// only path where it's missing is a runtime-init failure.\n\tlet preview: { collection: string; id: string } | undefined;\n\tif (hasPreviewToken) {\n\t\tconst db = context.locals.emdash?.db;\n\t\tif (db) {\n\t\t\tconst { previewSecret } = await resolveSecretsCached(db);\n\t\t\tconst result = await verifyPreviewToken({ url, secret: previewSecret });\n\t\t\tif (result.valid) {\n\t\t\t\tconst { collection, id } = parseContentId(result.payload.cid);\n\t\t\t\tpreview = { collection, id };\n\t\t\t}\n\t\t} else {\n\t\t\tconsole.warn(\n\t\t\t\t\"[emdash] Preview token present but EmDash runtime not initialized; preview disabled.\",\n\t\t\t);\n\t\t}\n\t}\n\n\t// If we have CMS signals, wrap in ALS context\n\tconst needsContext = hasEditCookie || hasPreviewToken;\n\n\tif (needsContext) {\n\t\t// Merge with any outer ALS context (e.g. the per-request D1 session db\n\t\t// set by the runtime middleware). `storage.run()` replaces the store\n\t\t// wholesale, so without the spread the outer `db` would be lost and\n\t\t// loaders would fall back to the singleton non-session dialect.\n\t\tconst parent = getRequestContext();\n\t\treturn runWithContext({ ...parent, editMode, preview, locale }, async () => {\n\t\t\tlet response = await next();\n\n\t\t\t// Preview responses must not be cached -- draft content could leak past token expiry.\n\t\t\t// Clone the response before modifying headers — the original may be immutable.\n\t\t\tif (preview) {\n\t\t\t\tresponse = new Response(response.body, response);\n\t\t\t\tresponse.headers.set(\"Cache-Control\", \"private, no-store\");\n\t\t\t}\n\n\t\t\t// Inject toolbar for authenticated editors\n\t\t\tif (isEditor) {\n\t\t\t\tconst toolbarHtml = renderToolbar({\n\t\t\t\t\teditMode,\n\t\t\t\t\tisPreview: !!preview,\n\t\t\t\t});\n\t\t\t\treturn injectToolbar(response, toolbarHtml);\n\t\t\t}\n\n\t\t\treturn response;\n\t\t});\n\t}\n\n\t// Editor without CMS signals — no ALS needed, but inject toolbar\n\tif (isEditor) {\n\t\tconst response = await next();\n\t\tconst toolbarHtml = renderToolbar({\n\t\t\teditMode: false,\n\t\t\tisPreview: false,\n\t\t});\n\t\treturn injectToolbar(response, toolbarHtml);\n\t}\n\n\treturn next();\n});\n\nexport default onRequest;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.mts","names":[],"sources":["../../src/astro/middleware.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"middleware.d.mts","names":[],"sources":["../../src/astro/middleware.ts"],"mappings":";;;;;;AAwOA;;;cAAa,SAAA,EAsUX,KAAA,CAtUoB,iBAAA"}
|
|
@@ -1,33 +1,37 @@
|
|
|
1
|
+
import { r as runMigrations } from "../runner-DMnlIkh4.mjs";
|
|
1
2
|
import { getRequestContext, runWithContext } from "../request-context.mjs";
|
|
2
3
|
import { createRecorder, flushRecorder, isInstrumentationEnabled, kyselyLogOption } from "../database/instrumentation.mjs";
|
|
3
4
|
import "../connection-2igzM-AT.mjs";
|
|
4
5
|
import { t as validateIdentifier } from "../validate-VPnKoIzW.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { At as
|
|
8
|
-
import { r as RevisionRepository } from "../content-
|
|
6
|
+
import { o as isSqlite } from "../dialect-helpers-BKCvISIQ.mjs";
|
|
7
|
+
import { i as setI18nConfig } from "../config-CVssduLe.mjs";
|
|
8
|
+
import { At as handleContentTranslations, Ct as handleContentGetIncludingTrashed, Dt as handleContentPublish, Et as handleContentPermanentDelete, G as sanitizeHeadersForSandbox, H as resolveExclusiveHooks, I as PluginRouteRegistry, K as getTrustedProxyHeaders, L as DEV_CONSOLE_EMAIL_PLUGIN_ID, Mt as handleContentUnschedule, Nt as handleContentUpdate, Ot as handleContentRestore, Pt as validateRev, R as devConsoleEmailDeliver, St as handleContentGet, Tt as handleContentListTrashed, U as CronExecutor, V as createHookPipeline, W as extractRequestMeta, X as after, _t as handleContentCountTrashed, bt as handleContentDiscardDraft, ct as handleMediaGet, dt as handleRevisionGet, ft as handleRevisionList, gt as handleContentCountScheduled, ht as handleContentCompare, it as PluginStateRepository, jt as handleContentUnpublish, kt as handleContentSchedule, lt as handleMediaList, ot as handleMediaCreate, pt as handleRevisionRestore, q as definePlugin, st as handleMediaDelete, tt as loadBundleFromR2, ut as handleMediaUpdate, vt as handleContentCreate, wt as handleContentList, xt as handleContentDuplicate, yt as handleContentDelete, z as EmailPipeline } from "../search-DkN-BqsS.mjs";
|
|
9
|
+
import { r as RevisionRepository } from "../content-C7G4QXkK.mjs";
|
|
9
10
|
import "../base64-MBPo9ozB.mjs";
|
|
10
11
|
import "../types-BIgulNsW.mjs";
|
|
11
12
|
import { t as MediaRepository } from "../media-D8FbNsl0.mjs";
|
|
12
|
-
import
|
|
13
|
-
import "../
|
|
14
|
-
import "../
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
17
|
-
import { r as hashString } from "../zod-generator-
|
|
18
|
-
import { i as FTSManager, n as SchemaRegistry } from "../registry-
|
|
19
|
-
import {
|
|
20
|
-
import "../request-cache-
|
|
21
|
-
import "../
|
|
22
|
-
import
|
|
23
|
-
import "../
|
|
24
|
-
import
|
|
25
|
-
import "../
|
|
26
|
-
import "../
|
|
27
|
-
import "../
|
|
13
|
+
import "../taxonomy-DSxx2K2L.mjs";
|
|
14
|
+
import { t as OptionsRepository } from "../options-nPxWnrya.mjs";
|
|
15
|
+
import "../redirect-C5H7VGIX.mjs";
|
|
16
|
+
import "../byline-C3vnhIpU.mjs";
|
|
17
|
+
import { n as normalizeMediaValue } from "../placeholder-Ci0RLeCk.mjs";
|
|
18
|
+
import { r as hashString } from "../zod-generator-BNJDQBSZ.mjs";
|
|
19
|
+
import { i as FTSManager, n as SchemaRegistry } from "../registry-Beb7wxFc.mjs";
|
|
20
|
+
import { r as getDb } from "../loader-Bx2_9-5e.mjs";
|
|
21
|
+
import { n as requestCached } from "../request-cache-C-tIpYIw.mjs";
|
|
22
|
+
import "../apply-UsrFuO7l.mjs";
|
|
23
|
+
import "../taxonomies-CTtewrSQ.mjs";
|
|
24
|
+
import { r as normalizeManifestRoute } from "../manifest-schema-CXAbd1vH.mjs";
|
|
25
|
+
import "../types-CoO6mpV3.mjs";
|
|
26
|
+
import "../error-DqnRMM5z.mjs";
|
|
27
|
+
import { a as invalidateUrlPatternCache } from "../query-Bo-msrmu.mjs";
|
|
28
|
+
import "../tokens-CyRDPVW2.mjs";
|
|
29
|
+
import "../bylines-esI7ioa9.mjs";
|
|
30
|
+
import "../load-sXRuM7Us.mjs";
|
|
28
31
|
import "../index.mjs";
|
|
29
|
-
import { n as VERSION, t as COMMIT } from "../version-
|
|
30
|
-
import { t as getAuthMode } from "../mode-
|
|
32
|
+
import { n as VERSION, t as COMMIT } from "../version-CMD42IRC.mjs";
|
|
33
|
+
import { t as getAuthMode } from "../mode-YhqNVef_.mjs";
|
|
34
|
+
import { a as validateEncryptionKeyAtStartup } from "../secrets-CZ8rxLX3.mjs";
|
|
31
35
|
import { Kysely, sql } from "kysely";
|
|
32
36
|
import { defineMiddleware } from "astro:middleware";
|
|
33
37
|
import virtualConfig from "virtual:emdash/config";
|
|
@@ -390,9 +394,6 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
390
394
|
cronScheduler;
|
|
391
395
|
enabledPlugins;
|
|
392
396
|
pluginStates;
|
|
393
|
-
_cachedManifest = null;
|
|
394
|
-
_manifestPromise = null;
|
|
395
|
-
_manifestCacheKey;
|
|
396
397
|
/**
|
|
397
398
|
* Set to true after FTS indexes have been verified for this worker
|
|
398
399
|
* lifetime so we don't re-scan on every admin request. See
|
|
@@ -445,7 +446,6 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
445
446
|
this.pipelineFactoryOptions = parts.pipelineFactoryOptions;
|
|
446
447
|
this.runtimeDeps = parts.runtimeDeps;
|
|
447
448
|
this.pipelineRef = parts.pipelineRef;
|
|
448
|
-
this._manifestCacheKey = parts.manifestCacheKey;
|
|
449
449
|
}
|
|
450
450
|
/**
|
|
451
451
|
* Get the sandbox runner instance (for marketplace install/update)
|
|
@@ -486,7 +486,6 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
486
486
|
this.enabledPlugins.delete(pluginId);
|
|
487
487
|
await this.rebuildHookPipeline();
|
|
488
488
|
}
|
|
489
|
-
this.invalidateManifest();
|
|
490
489
|
}
|
|
491
490
|
/**
|
|
492
491
|
* Rebuild the hook pipeline from the current set of enabled plugins.
|
|
@@ -610,6 +609,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
610
609
|
}
|
|
611
610
|
};
|
|
612
611
|
const db = await phase("rt.db", "DB init + migrations", () => EmDashRuntime.getDatabase(deps));
|
|
612
|
+
await phase("rt.secrets", "Validate encryption key", () => validateEncryptionKeyAtStartup());
|
|
613
613
|
const storage = EmDashRuntime.getStorage(deps);
|
|
614
614
|
let pluginStates = /* @__PURE__ */ new Map();
|
|
615
615
|
await phase("rt.plugins", "Plugin states", async () => {
|
|
@@ -643,7 +643,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
643
643
|
const devConsolePlugin = definePlugin({
|
|
644
644
|
id: DEV_CONSOLE_EMAIL_PLUGIN_ID,
|
|
645
645
|
version: "0.0.0",
|
|
646
|
-
capabilities: ["email:
|
|
646
|
+
capabilities: ["hooks.email-transport:register"],
|
|
647
647
|
hooks: { "email:deliver": {
|
|
648
648
|
exclusive: true,
|
|
649
649
|
handler: devConsoleEmailDeliver
|
|
@@ -658,7 +658,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
658
658
|
const defaultModeratorPlugin = definePlugin({
|
|
659
659
|
id: DEFAULT_COMMENT_MODERATOR_PLUGIN_ID,
|
|
660
660
|
version: "0.0.0",
|
|
661
|
-
capabilities: ["read
|
|
661
|
+
capabilities: ["users:read"],
|
|
662
662
|
hooks: { "comment:moderate": {
|
|
663
663
|
exclusive: true,
|
|
664
664
|
handler: defaultCommentModerate
|
|
@@ -731,13 +731,6 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
731
731
|
console.warn("[cron] Failed to initialize cron system:", error);
|
|
732
732
|
}
|
|
733
733
|
});
|
|
734
|
-
const manifestCacheKey = await hashString([
|
|
735
|
-
COMMIT,
|
|
736
|
-
...deps.plugins.map((p) => `${p.id}@${p.version ?? ""}`).toSorted(),
|
|
737
|
-
...deps.sandboxedPluginEntries.map((e) => `${e.id}@${e.version}`).toSorted(),
|
|
738
|
-
virtualConfig?.i18n?.defaultLocale ?? "",
|
|
739
|
-
(virtualConfig?.i18n?.locales ?? []).toSorted().join(",")
|
|
740
|
-
].join("|"));
|
|
741
734
|
return new EmDashRuntime({
|
|
742
735
|
db,
|
|
743
736
|
storage,
|
|
@@ -756,8 +749,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
756
749
|
allPipelinePlugins,
|
|
757
750
|
pipelineFactoryOptions,
|
|
758
751
|
runtimeDeps: deps,
|
|
759
|
-
pipelineRef
|
|
760
|
-
manifestCacheKey
|
|
752
|
+
pipelineRef
|
|
761
753
|
});
|
|
762
754
|
}
|
|
763
755
|
/**
|
|
@@ -798,10 +790,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
798
790
|
dialect: deps.createDialect(dbConfig.config),
|
|
799
791
|
log: kyselyLogOption()
|
|
800
792
|
});
|
|
801
|
-
|
|
802
|
-
if (applied.length > 0) try {
|
|
803
|
-
await new OptionsRepository(db).delete("emdash:manifest_cache");
|
|
804
|
-
} catch {}
|
|
793
|
+
await runMigrations(db);
|
|
805
794
|
try {
|
|
806
795
|
const [collectionCount, setupOption] = await Promise.all([db.selectFrom("_emdash_collections").select((eb) => eb.fn.countAll().as("count")).executeTakeFirstOrThrow(), db.selectFrom("options").select("value").where("name", "=", "emdash:setup_complete").executeTakeFirst()]);
|
|
807
796
|
const setupDone = (() => {
|
|
@@ -812,9 +801,9 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
812
801
|
}
|
|
813
802
|
})();
|
|
814
803
|
if (collectionCount.count === 0 && !setupDone) {
|
|
815
|
-
const { applySeed } = await import("../apply-
|
|
816
|
-
const { loadSeed } = await import("../load-
|
|
817
|
-
const { validateSeed } = await import("../validate-
|
|
804
|
+
const { applySeed } = await import("../apply-UsrFuO7l.mjs").then((n) => n.n);
|
|
805
|
+
const { loadSeed } = await import("../load-sXRuM7Us.mjs").then((n) => n.r);
|
|
806
|
+
const { validateSeed } = await import("../validate-CBIbxM3L.mjs").then((n) => n.n);
|
|
818
807
|
const seed = await loadSeed();
|
|
819
808
|
if (validateSeed(seed).valid) {
|
|
820
809
|
await applySeed(db, seed, { onConflict: "skip" });
|
|
@@ -953,60 +942,42 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
953
942
|
});
|
|
954
943
|
}
|
|
955
944
|
/**
|
|
956
|
-
*
|
|
957
|
-
*
|
|
958
|
-
*
|
|
945
|
+
* Build the admin manifest from the live database.
|
|
946
|
+
*
|
|
947
|
+
* Used by the admin UI (sidebar collections, content editor field
|
|
948
|
+
* dispatch, manifest endpoint) and by WordPress import — it's never
|
|
949
|
+
* read on a public request, so this isn't on any anonymous hot path.
|
|
959
950
|
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
951
|
+
* No cross-request cache. The previous worker-isolate cache produced
|
|
952
|
+
* a class of cross-isolate staleness bugs (#776, #873, #876, #877)
|
|
953
|
+
* because Cloudflare Workers keeps multiple warm isolates per region
|
|
954
|
+
* and there's no fan-out primitive to invalidate them in step. The
|
|
955
|
+
* cache existed to amortize an N+1 schema query pattern; now that
|
|
956
|
+
* `listCollectionsWithFields()` does the same work in two queries,
|
|
957
|
+
* the rebuild is fast enough to pay on every admin request.
|
|
958
|
+
*
|
|
959
|
+
* Within a single request, `requestCached` deduplicates concurrent
|
|
960
|
+
* callers (the manifest endpoint and an admin SSR template, say).
|
|
962
961
|
*/
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
if (this._cachedManifest) return this._cachedManifest;
|
|
966
|
-
try {
|
|
967
|
-
const cached = await new OptionsRepository(this.db).get("emdash:manifest_cache");
|
|
968
|
-
if (cached && cached.key === this._manifestCacheKey && cached.manifest) {
|
|
969
|
-
this._cachedManifest = cached.manifest;
|
|
970
|
-
return cached.manifest;
|
|
971
|
-
}
|
|
972
|
-
} catch {}
|
|
973
|
-
if (!this._manifestPromise) {
|
|
974
|
-
let manifestPromise;
|
|
975
|
-
const isCurrentLoad = () => this._manifestPromise === manifestPromise;
|
|
976
|
-
manifestPromise = this._loadManifest(isCurrentLoad);
|
|
977
|
-
this._manifestPromise = manifestPromise;
|
|
978
|
-
}
|
|
979
|
-
return this._manifestPromise;
|
|
980
|
-
}
|
|
981
|
-
async _loadManifest(isCurrentLoad) {
|
|
982
|
-
try {
|
|
983
|
-
const manifest = await this._buildManifest();
|
|
984
|
-
if (isCurrentLoad()) {
|
|
985
|
-
this._cachedManifest = manifest;
|
|
986
|
-
try {
|
|
987
|
-
await new OptionsRepository(this.db).set("emdash:manifest_cache", {
|
|
988
|
-
key: this._manifestCacheKey,
|
|
989
|
-
manifest
|
|
990
|
-
});
|
|
991
|
-
} catch {}
|
|
992
|
-
}
|
|
993
|
-
return manifest;
|
|
994
|
-
} finally {
|
|
995
|
-
if (isCurrentLoad()) this._manifestPromise = null;
|
|
996
|
-
}
|
|
962
|
+
getManifest() {
|
|
963
|
+
return requestCached("emdash:manifest", () => this._buildManifest());
|
|
997
964
|
}
|
|
998
965
|
/**
|
|
999
|
-
* Build the manifest from database
|
|
966
|
+
* Build the manifest from the database.
|
|
967
|
+
*
|
|
968
|
+
* Constant query shapes via `listCollectionsWithFields()` — one query
|
|
969
|
+
* for collections, one batched query for fields (chunked at
|
|
970
|
+
* `SQL_BATCH_SIZE` collection IDs to stay under D1's bound-parameter
|
|
971
|
+
* limit). Typical sites stay well under the chunk threshold, so this
|
|
972
|
+
* is two queries in practice; never N+1.
|
|
1000
973
|
*/
|
|
1001
974
|
async _buildManifest() {
|
|
1002
975
|
const manifestCollections = {};
|
|
1003
976
|
try {
|
|
1004
|
-
const
|
|
1005
|
-
const dbCollections = await registry.listCollections();
|
|
977
|
+
const dbCollections = await new SchemaRegistry(this.db).listCollectionsWithFields();
|
|
1006
978
|
for (const collection of dbCollections) {
|
|
1007
|
-
const collectionWithFields = await registry.getCollectionWithFields(collection.slug);
|
|
1008
979
|
const fields = {};
|
|
1009
|
-
|
|
980
|
+
for (const field of collection.fields) {
|
|
1010
981
|
const entry = {
|
|
1011
982
|
kind: FIELD_TYPE_TO_KIND[field.type] ?? "string",
|
|
1012
983
|
label: field.label,
|
|
@@ -1116,22 +1087,6 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1116
1087
|
};
|
|
1117
1088
|
}
|
|
1118
1089
|
/**
|
|
1119
|
-
* Invalidate cached data derived from the manifest/schema.
|
|
1120
|
-
* Called when collections, fields, plugins, or taxonomy defs change.
|
|
1121
|
-
*/
|
|
1122
|
-
invalidateManifest() {
|
|
1123
|
-
this._cachedManifest = null;
|
|
1124
|
-
this._manifestPromise = null;
|
|
1125
|
-
invalidateUrlPatternCache();
|
|
1126
|
-
try {
|
|
1127
|
-
new OptionsRepository(this.db).delete("emdash:manifest_cache").catch((error) => {
|
|
1128
|
-
console.error("Failed to delete persisted manifest cache", error);
|
|
1129
|
-
});
|
|
1130
|
-
} catch (error) {
|
|
1131
|
-
console.error("Failed to initialize manifest cache invalidation", error);
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
/**
|
|
1135
1090
|
* Verify and repair FTS indexes on demand. Runs at most once per worker
|
|
1136
1091
|
* lifetime.
|
|
1137
1092
|
*
|
|
@@ -1233,7 +1188,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1233
1188
|
if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(body.data, collection, true)).content;
|
|
1234
1189
|
processedData = await this.runSandboxedBeforeSave(processedData, collection, true);
|
|
1235
1190
|
processedData = await this.normalizeMediaFields(collection, processedData);
|
|
1236
|
-
const { validateContentData } = await import("../validation-
|
|
1191
|
+
const { validateContentData } = await import("../validation-B1NYiEos.mjs");
|
|
1237
1192
|
const validation = await validateContentData(this.db, collection, processedData, { partial: false });
|
|
1238
1193
|
if (!validation.ok) return {
|
|
1239
1194
|
success: false,
|
|
@@ -1249,7 +1204,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1249
1204
|
return result;
|
|
1250
1205
|
}
|
|
1251
1206
|
async handleContentUpdate(collection, id, body) {
|
|
1252
|
-
const { ContentRepository } = await import("../content-
|
|
1207
|
+
const { ContentRepository } = await import("../content-C7G4QXkK.mjs").then((n) => n.n);
|
|
1253
1208
|
const repo = new ContentRepository(this.db);
|
|
1254
1209
|
const resolvedItem = await repo.findByIdOrSlug(collection, id);
|
|
1255
1210
|
const resolvedId = resolvedItem?.id ?? id;
|
|
@@ -1276,7 +1231,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1276
1231
|
if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(bodyWithoutRev.data, collection, false)).content;
|
|
1277
1232
|
processedData = await this.runSandboxedBeforeSave(processedData, collection, false);
|
|
1278
1233
|
processedData = await this.normalizeMediaFields(collection, processedData);
|
|
1279
|
-
const { validateContentData } = await import("../validation-
|
|
1234
|
+
const { validateContentData } = await import("../validation-B1NYiEos.mjs");
|
|
1280
1235
|
const validation = await validateContentData(this.db, collection, processedData, { partial: true });
|
|
1281
1236
|
if (!validation.ok) return {
|
|
1282
1237
|
success: false,
|
|
@@ -1369,8 +1324,8 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1369
1324
|
async handleContentDuplicate(collection, id, authorId) {
|
|
1370
1325
|
return handleContentDuplicate(this.db, collection, id, authorId);
|
|
1371
1326
|
}
|
|
1372
|
-
async handleContentPublish(collection, id) {
|
|
1373
|
-
const result = await handleContentPublish(this.db, collection, id);
|
|
1327
|
+
async handleContentPublish(collection, id, options = {}) {
|
|
1328
|
+
const result = await handleContentPublish(this.db, collection, id, options);
|
|
1374
1329
|
if (result.success && result.data) this.runAfterPublishHooks(contentItemToRecord(result.data.item), collection);
|
|
1375
1330
|
return result;
|
|
1376
1331
|
}
|
|
@@ -1944,7 +1899,8 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
1944
1899
|
const hasEditCookie = cookies.get("emdash-edit-mode")?.value === "true";
|
|
1945
1900
|
const hasPreviewToken = url.searchParams.has("_preview");
|
|
1946
1901
|
const playgroundDb = locals.__playgroundDb;
|
|
1947
|
-
const
|
|
1902
|
+
const hasSessionCookie = cookies.get("astro-session") !== void 0;
|
|
1903
|
+
const sessionUser = context.isPrerendered || !hasSessionCookie ? null : await context.session?.get("user");
|
|
1948
1904
|
if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
|
|
1949
1905
|
if (!sessionUser && !playgroundDb) {
|
|
1950
1906
|
const timings = [];
|
|
@@ -1952,7 +1908,7 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
1952
1908
|
if (!setupVerified) {
|
|
1953
1909
|
const t0 = performance.now();
|
|
1954
1910
|
try {
|
|
1955
|
-
const { getDb } = await import("../loader-
|
|
1911
|
+
const { getDb } = await import("../loader-Bx2_9-5e.mjs").then((n) => n.i);
|
|
1956
1912
|
await (await getDb()).selectFrom("_emdash_migrations").selectAll().limit(1).execute();
|
|
1957
1913
|
setupVerified = true;
|
|
1958
1914
|
} catch {
|
|
@@ -2042,14 +1998,6 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
2042
1998
|
});
|
|
2043
1999
|
for (const sub of initSubTimings) timings.push(sub);
|
|
2044
2000
|
setupVerified = true;
|
|
2045
|
-
t0 = performance.now();
|
|
2046
|
-
const manifest = await runtime.getManifest();
|
|
2047
|
-
timings.push({
|
|
2048
|
-
name: "manifest",
|
|
2049
|
-
dur: performance.now() - t0,
|
|
2050
|
-
desc: "Manifest"
|
|
2051
|
-
});
|
|
2052
|
-
locals.emdashManifest = manifest;
|
|
2053
2001
|
locals.emdash = {
|
|
2054
2002
|
handleContentList: runtime.handleContentList.bind(runtime),
|
|
2055
2003
|
handleContentGet: runtime.handleContentGet.bind(runtime),
|
|
@@ -2092,7 +2040,8 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
2092
2040
|
email: runtime.email,
|
|
2093
2041
|
configuredPlugins: runtime.configuredPlugins,
|
|
2094
2042
|
config,
|
|
2095
|
-
|
|
2043
|
+
getManifest: runtime.getManifest.bind(runtime),
|
|
2044
|
+
invalidateUrlPatternCache,
|
|
2096
2045
|
getSandboxRunner: runtime.getSandboxRunner.bind(runtime),
|
|
2097
2046
|
syncMarketplacePlugins: runtime.syncMarketplacePlugins.bind(runtime),
|
|
2098
2047
|
setPluginStatus: runtime.setPluginStatus.bind(runtime)
|