@elytracms/studio 0.0.64 → 0.0.65
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/convex/assets.ts +11 -0
- package/convex/delivery.ts +5 -0
- package/convex/schema.ts +23 -0
- package/dist/components/assets/asset-thumb.d.ts +4 -2
- package/dist/components/assets/asset-thumb.js +26 -4
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-module.js +48 -2
- package/dist/components/assets/assets-module.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +11 -1
- package/dist/components/assets/assets-store.js +18 -1
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +11 -0
- package/dist/components/builder/blocks-model.js +26 -8
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.js +90 -8
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-inline-editing.d.ts +7 -0
- package/dist/components/builder/canvas-inline-editing.js +3 -1
- package/dist/components/builder/canvas-inline-editing.js.map +1 -1
- package/dist/components/builder/canvas-overlay-geometry.d.ts +72 -0
- package/dist/components/builder/canvas-overlay-geometry.js +80 -0
- package/dist/components/builder/canvas-overlay-geometry.js.map +1 -0
- package/dist/components/builder/canvas-rect-tracking.d.ts +124 -0
- package/dist/components/builder/canvas-rect-tracking.js +198 -0
- package/dist/components/builder/canvas-rect-tracking.js.map +1 -0
- package/dist/components/builder/canvas.js +61 -12
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +22 -3
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/delete-key.d.ts +38 -0
- package/dist/components/builder/delete-key.js +46 -0
- package/dist/components/builder/delete-key.js.map +1 -0
- package/dist/components/builder/drag-session.d.ts +8 -0
- package/dist/components/builder/drag-session.js +15 -0
- package/dist/components/builder/drag-session.js.map +1 -1
- package/dist/components/builder/insert-affordance-context.d.ts +19 -0
- package/dist/components/builder/insert-affordance-context.js +7 -0
- package/dist/components/builder/insert-affordance-context.js.map +1 -0
- package/dist/components/builder/insert-context.d.ts +40 -0
- package/dist/components/builder/insert-context.js +86 -3
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/node-title.d.ts +60 -0
- package/dist/components/builder/node-title.js +135 -0
- package/dist/components/builder/node-title.js.map +1 -0
- package/dist/components/builder/page-tree.js +21 -3
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/prop-selection.d.ts +52 -0
- package/dist/components/builder/prop-selection.js +38 -0
- package/dist/components/builder/prop-selection.js.map +1 -0
- package/dist/components/builder/prop-sync-context.d.ts +49 -0
- package/dist/components/builder/prop-sync-context.js +23 -0
- package/dist/components/builder/prop-sync-context.js.map +1 -0
- package/dist/components/builder/property-inspector.js +23 -1
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +1 -1
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/selection-overlay.d.ts +79 -0
- package/dist/components/builder/selection-overlay.js +244 -0
- package/dist/components/builder/selection-overlay.js.map +1 -0
- package/dist/components/builder/spacing-handles.d.ts +57 -1
- package/dist/components/builder/spacing-handles.js +142 -26
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/asset-picker.js +11 -5
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/rich-text/block-style.d.ts +48 -0
- package/dist/components/cms/rich-text/block-style.js +30 -0
- package/dist/components/cms/rich-text/block-style.js.map +1 -0
- package/dist/components/cms/rich-text/embed-picker.js +4 -3
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/extensions.d.ts +2 -0
- package/dist/components/cms/rich-text/extensions.js +4 -0
- package/dist/components/cms/rich-text/extensions.js.map +1 -1
- package/dist/components/cms/rich-text/font-size-control.d.ts +31 -0
- package/dist/components/cms/rich-text/font-size-control.js +40 -0
- package/dist/components/cms/rich-text/font-size-control.js.map +1 -0
- package/dist/components/cms/rich-text/font-size.d.ts +16 -0
- package/dist/components/cms/rich-text/font-size.js +40 -0
- package/dist/components/cms/rich-text/font-size.js.map +1 -0
- package/dist/components/cms/rich-text/toolbar-state.d.ts +3 -0
- package/dist/components/cms/rich-text/toolbar-state.js +3 -0
- package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar.d.ts +13 -0
- package/dist/components/cms/rich-text/toolbar.js +17 -5
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/components/component-detail.js +3 -3
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.js +1 -1
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/components/registry-browser.js +2 -2
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.js +3 -2
- package/dist/components/components/taxonomy.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +1 -1
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/lib/assets/backfill-thumbs.d.ts +67 -0
- package/dist/lib/assets/backfill-thumbs.js +120 -0
- package/dist/lib/assets/backfill-thumbs.js.map +1 -0
- package/dist/lib/assets/delete-asset.d.ts +2 -1
- package/dist/lib/assets/delete-asset.js +12 -0
- package/dist/lib/assets/delete-asset.js.map +1 -1
- package/dist/lib/assets/index.d.ts +2 -0
- package/dist/lib/assets/index.js +2 -0
- package/dist/lib/assets/index.js.map +1 -1
- package/dist/lib/assets/thumbnail.d.ts +145 -0
- package/dist/lib/assets/thumbnail.js +220 -0
- package/dist/lib/assets/thumbnail.js.map +1 -0
- package/dist/lib/assets/upload.d.ts +9 -0
- package/dist/lib/assets/upload.js +31 -0
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +16 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +14 -0
- package/dist/lib/i18n/messages.en.js +16 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +4 -0
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +9 -5
- package/dist/lib/persona/labels.js +13 -8
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/references/references-store.js +7 -1
- package/dist/lib/references/references-store.js.map +1 -1
- package/package.json +2 -2
- package/src/styles/studio.css +13 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnail.js","sourceRoot":"","sources":["../../../src/lib/assets/thumbnail.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAA;AAGnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,yEAAyE;AACzE,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAA;AAEjC,mFAAmF;AACnF,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAA;AAEhC,oEAAoE;AACpE,MAAM,CAAC,MAAM,aAAa,GAAG,EAAE,CAAA;AAE/B,6EAA6E;AAC7E,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAA;AAmBhC,gGAAgG;AAChG,SAAS,WAAW,CAAC,KAAa,EAAE,MAAc,EAAE,OAAe;IACjE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5D,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,EAAE,CAAA;AAC3G,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,MAAmB;IAC3C,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,CAAA;IAC1F,uEAAuE;IACvE,IAAI,MAAM,CAAC,WAAW,KAAK,eAAe;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;IACrF,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAAA;IAChC,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,CAAA;IACpG,+EAA+E;IAC/E,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,cAAc;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,CAAA;IAC/F,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,CAAA;AAC1E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAmB;IAC1C,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,CAAA;IAC1F,IAAI,MAAM,CAAC,WAAW,KAAK,eAAe;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;IACrF,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAAA;IAChC,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,CAAA;IACpG,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,CAAA;AACzE,CAAC;AAuCD,+DAA+D;AAC/D,SAAS,YAAY,CAAC,MAAyB,EAAE,IAAY,EAAE,OAAe;IAC5E,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAA;AACxF,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,gBAAgB,CAC7B,MAAmB,EACnB,MAAoB;IAEpB,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAA;IAC/C,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAA;IAC3B,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAA;IAC7B,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;IACnC,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAA;IAC1B,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;IACxD,IAAI,IAAI,GAAG,MAAM,YAAY,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;IACnE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QACxC,IAAI,GAAG,MAAM,YAAY,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;IACjE,CAAC;IACD,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAA;IAC3B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,CAAA;IACtD,sDAAsD;IACtD,IAAI,KAAK,CAAC,UAAU,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAC5C,OAAO;QACL,KAAK;QACL,yEAAyE;QACzE,WAAW,EAAE,IAAI,CAAC,IAAI,IAAI,YAAY;QACtC,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EAAE,MAAM,CAAC,MAAM;KACtB,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAmB,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;IAC5E,MAAM,GAAG,GAAoB,EAAE,CAAA;IAC/B,IAAI,CAAC;QACH,IAAI,OAAO,iBAAiB,KAAK,UAAU,IAAI,OAAO,QAAQ,KAAK,WAAW;YAAE,OAAO,GAAG,CAAA;QAC1F,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,MAAM,CAAC,CAAA;QAC9C,IAAI,CAAC;YACH,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;gBAClB,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;gBAC7D,IAAI,OAAO;oBAAE,GAAG,CAAC,KAAK,GAAG,OAAO,CAAA;YAClC,CAAC;YACD,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;gBACjB,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;gBAC5D,IAAI,OAAO;oBAAE,GAAG,CAAC,IAAI,GAAG,OAAO,CAAA;YACjC,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,MAAM,CAAC,KAAK,EAAE,EAAE,CAAA;QAClB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,8DAA8D;QAC9D,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA;AAED,uFAAuF;AACvF,SAAS,aAAa,CAAC,KAAiB;IACtC,8EAA8E;IAC9E,IAAI,OAAO,IAAI,KAAK,UAAU;QAAE,OAAO,SAAS,CAAA;IAChD,IAAI,MAAM,GAAG,EAAE,CAAA;IACf,MAAM,KAAK,GAAG,MAAM,CAAA;IACpB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC;QAC7C,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAA;IAChE,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAA;AACrB,CAAC;AAED,6FAA6F;AAC7F,SAAS,SAAS,CAAC,OAAuB;IACxC,MAAM,GAAG,GAAG,aAAa,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IACxC,OAAO,GAAG,CAAC,CAAC,CAAC,QAAQ,OAAO,CAAC,WAAW,WAAW,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AACtE,CAAC;AAuBD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAA2B,EAC3B,MAAY,EACZ,IAAiB,EACjB,OAAoB,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IAE/C,MAAM,SAAS,GAAc,IAAI,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;IAC/G,MAAM,QAAQ,GAAa,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;IAE3G,MAAM,OAAO,GAAmB,EAAE,CAAA;IAClC,IAAI,SAAS,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAChC,OAAO,CAAC,KAAK,GAAG,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,CAAA;IAC9F,CAAC;IACD,IAAI,QAAQ,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,CAAC,IAAI,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,CAAA;IAC1F,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI;QAAE,OAAO,EAAE,CAAA;IAE9C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,oBAAoB,CAAA;IAClD,IAAI,OAAwB,CAAA;IAC5B,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,MAAM,GAAsB,EAAE,CAAA;IACpC,+EAA+E;IAC/E,gDAAgD;IAChD,IAAI,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACtC,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;YACzF,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;YACrF,MAAM,CAAC,KAAK,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,CAAA;QAC9F,CAAC;QAAC,MAAM,CAAC;YACP,iCAAiC;QACnC,CAAC;IACH,CAAC;IACD,+EAA+E;IAC/E,kFAAkF;IAClF,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QACjB,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QACvC,IAAI,OAAO,IAAI,OAAO,CAAC,MAAM,IAAI,qBAAqB;YAAE,MAAM,CAAC,IAAI,GAAG,OAAO,CAAA;IAC/E,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["import { ASSET_LQIP_MAX_LENGTH } from '@elytracms/core/persistence'\nimport type { AssetThumbVariant, BlobStorageAdapter } from '@elytracms/core/persistence'\n\n/**\n * Client-side preview generation (EC-400 thumb + EC-420 LQIP). Every studio image\n * surface used to stream the full-size Convex serve URL in a plain `<img>`, so a\n * page of big originals cost heavy egress. At the moment an upload already has the\n * bytes in hand — and again from the assets rail's \"Generate previews\" backfill —\n * ONE `createImageBitmap` decode yields BOTH variants:\n *\n * - **thumb** (EC-400): a ≤480px-long-edge re-encode stored as a SECOND blob;\n * editorial-only (studio grids/pickers/table cells render it instead of the\n * original). NEVER leaves the studio.\n * - **lqip** (EC-420): a ≤24px, low-quality re-encode base64'd into a tiny `data:`\n * URI (hard-capped at {@link ASSET_LQIP_MAX_LENGTH} chars) and stored INLINE on\n * the record. Unlike the thumb it rides the delivery wire (AD-2): the\n * `@elytracms/next` image seam feeds it to `next/image` as `placeholder=\"blur\"`.\n *\n * The decisions (`planThumb` / `planLqip`) are pure, node-testable cores; the\n * canvas re-encode (`canvasVariantEncoder`) is a thin, injectable DOM shell that\n * decodes once and draws each requested target; the orchestrator\n * (`generateVariants`) plans → encodes (one decode) → stores the thumb blob +\n * builds the lqip data URI, and is testable in node with a fake encoder over an\n * in-memory blob store.\n *\n * Ratified spec: thumb ≤480px, quality ~0.8 (EC-400 AD-2). lqip ≤24px, quality\n * ~0.45 (EC-420 AD-1); UNLIKE the thumb it is generated even for images already\n * ≤480px (any raster benefits from blur-up). Both prefer `image/webp` with an\n * `image/jpeg` fallback (the ACTUAL returned blob type is recorded — Safari has\n * historically ignored the requested type). Skips are silent and valid — not\n * validation issues; a generation failure never fails the upload.\n */\n\n/** The ratified maximum thumbnail long-edge, in pixels (EC-400 AD-2). */\nexport const THUMB_MAX_EDGE = 480\n\n/** The ratified re-encode quality for the lossy thumbnail codecs (EC-400 AD-2). */\nexport const THUMB_QUALITY = 0.8\n\n/** The ratified maximum LQIP long-edge, in pixels (EC-420 AD-1). */\nexport const LQIP_MAX_EDGE = 24\n\n/** The ratified re-encode quality for the LQIP placeholder (EC-420 AD-1). */\nexport const LQIP_QUALITY = 0.45\n\n/** The re-encode target size a variant needs, or the reason it is skipped. */\nexport type ThumbPlan =\n | { kind: 'skip'; reason: 'non-image' | 'vector' | 'no-dimensions' | 'already-small' }\n | { kind: 'encode'; width: number; height: number }\n\n/** The LQIP plan — like {@link ThumbPlan} but WITHOUT the `already-small` skip (AD-3). */\nexport type LqipPlan =\n | { kind: 'skip'; reason: 'non-image' | 'vector' | 'no-dimensions' }\n | { kind: 'encode'; width: number; height: number }\n\n/** The metadata `planThumb` / `planLqip` and the encoders reason over. */\nexport interface ThumbSource {\n contentType: string\n width?: number\n height?: number\n}\n\n/** Fit `width`×`height` so its long edge is ≤ `maxEdge` (never upscaling); short edge ≥ 1px. */\nfunction fitLongEdge(width: number, height: number, maxEdge: number): { width: number; height: number } {\n const scale = Math.min(1, maxEdge / Math.max(width, height))\n return { width: Math.max(1, Math.round(width * scale)), height: Math.max(1, Math.round(height * scale)) }\n}\n\n/**\n * Decide whether an asset needs a downscaled thumb, and to what size (EC-400\n * AD-2/AD-3). Pure and deterministic — the node-tested core of the skip rules.\n * A returned `encode` plan preserves aspect ratio, fitting the long edge to\n * {@link THUMB_MAX_EDGE}; the short edge rounds to at least one pixel.\n */\nexport function planThumb(source: ThumbSource): ThumbPlan {\n if (!source.contentType.startsWith('image/')) return { kind: 'skip', reason: 'non-image' }\n // SVG is vector — rasterizing loses fidelity and saves nothing (AD-3).\n if (source.contentType === 'image/svg+xml') return { kind: 'skip', reason: 'vector' }\n const { width, height } = source\n if (!width || !height || width <= 0 || height <= 0) return { kind: 'skip', reason: 'no-dimensions' }\n // Already small enough — a variant would be a no-op; consumers just use `url`.\n if (Math.max(width, height) <= THUMB_MAX_EDGE) return { kind: 'skip', reason: 'already-small' }\n return { kind: 'encode', ...fitLongEdge(width, height, THUMB_MAX_EDGE) }\n}\n\n/**\n * Decide whether an asset needs an LQIP blur placeholder, and to what size (EC-420\n * AD-1/AD-3). Pure and deterministic. Mirrors {@link planThumb}'s skips (non-image,\n * SVG, no dimensions) but — UNLIKE the thumb — has NO `already-small` skip: any\n * raster benefits from delivery blur-up, so even a ≤480px image gets one (fit to\n * {@link LQIP_MAX_EDGE}, never upscaled).\n */\nexport function planLqip(source: ThumbSource): LqipPlan {\n if (!source.contentType.startsWith('image/')) return { kind: 'skip', reason: 'non-image' }\n if (source.contentType === 'image/svg+xml') return { kind: 'skip', reason: 'vector' }\n const { width, height } = source\n if (!width || !height || width <= 0 || height <= 0) return { kind: 'skip', reason: 'no-dimensions' }\n return { kind: 'encode', ...fitLongEdge(width, height, LQIP_MAX_EDGE) }\n}\n\n/** The bytes + ACTUAL mime type + dimensions an encoder produced for one variant. */\nexport interface EncodedVariant {\n bytes: Uint8Array\n /** The blob's real type — NOT necessarily the requested one (AD-2 Safari note). */\n contentType: string\n width: number\n height: number\n}\n\n/** A re-encode target the encoder draws into (per-variant quality). */\nexport interface EncodeTarget {\n width: number\n height: number\n quality: number\n}\n\n/** Which variants to draw+encode from the single decode, at their target sizes. */\nexport interface VariantTargets {\n thumb?: EncodeTarget\n lqip?: EncodeTarget\n}\n\n/** The encoded output of one decode: whichever requested variants encoded. */\nexport interface EncodedVariants {\n thumb?: EncodedVariant\n lqip?: EncodedVariant\n}\n\n/**\n * Turn source image bytes into downscaled preview bytes for BOTH variants from a\n * single decode. Returns whichever variants encoded (an empty object when the\n * source cannot be decoded/encoded) — never throws; the caller treats a missing\n * variant as \"no preview\", never a failure. Injectable so the orchestrator + the\n * upload/backfill flows unit-test in node without a browser.\n */\nexport type VariantEncoder = (source: Blob, targets: VariantTargets) => Promise<EncodedVariants>\n\n/** Promisify the callback-style `HTMLCanvasElement.toBlob`. */\nfunction canvasToBlob(canvas: HTMLCanvasElement, type: string, quality: number): Promise<Blob | null> {\n return new Promise((resolve) => canvas.toBlob((blob) => resolve(blob), type, quality))\n}\n\n/**\n * Draw an already-decoded bitmap into a target-sized 2D canvas and encode it.\n * Prefers `image/webp`; when the browser silently substitutes another type\n * (Safari), retries `image/jpeg` and records whatever type actually came back.\n * Resolves `undefined` on any failure (a valid skip, never a throw).\n */\nasync function encodeFromBitmap(\n bitmap: ImageBitmap,\n target: EncodeTarget,\n): Promise<EncodedVariant | undefined> {\n const canvas = document.createElement('canvas')\n canvas.width = target.width\n canvas.height = target.height\n const ctx = canvas.getContext('2d')\n if (!ctx) return undefined\n ctx.drawImage(bitmap, 0, 0, target.width, target.height)\n let blob = await canvasToBlob(canvas, 'image/webp', target.quality)\n if (!blob || blob.type !== 'image/webp') {\n blob = await canvasToBlob(canvas, 'image/jpeg', target.quality)\n }\n if (!blob) return undefined\n const bytes = new Uint8Array(await blob.arrayBuffer())\n // Zero-length re-encodes are worthless; discard them.\n if (bytes.byteLength === 0) return undefined\n return {\n bytes,\n // Record the ACTUAL type — never assume the requested one landed (AD-2).\n contentType: blob.type || 'image/jpeg',\n width: target.width,\n height: target.height,\n }\n}\n\n/**\n * The default browser encoder: ONE `createImageBitmap` decode → per-variant 2D\n * canvas → `toBlob` (EC-420: the same decode yields thumb AND lqip). A thin DOM\n * shell, deliberately untested in node (the orchestrator is tested with a fake\n * encoder). Any failure resolves an empty object, so a preview never breaks an\n * upload.\n */\nexport const canvasVariantEncoder: VariantEncoder = async (source, targets) => {\n const out: EncodedVariants = {}\n try {\n if (typeof createImageBitmap !== 'function' || typeof document === 'undefined') return out\n const bitmap = await createImageBitmap(source)\n try {\n if (targets.thumb) {\n const encoded = await encodeFromBitmap(bitmap, targets.thumb)\n if (encoded) out.thumb = encoded\n }\n if (targets.lqip) {\n const encoded = await encodeFromBitmap(bitmap, targets.lqip)\n if (encoded) out.lqip = encoded\n }\n } finally {\n bitmap.close?.()\n }\n } catch {\n // Undecodable — a silent, valid skip, never a thrown failure.\n return out\n }\n return out\n}\n\n/** Base64-encode bytes for a `data:` URI. `undefined` when no encoder is available. */\nfunction bytesToBase64(bytes: Uint8Array): string | undefined {\n // `btoa` exists in browsers and Node ≥16; absence (exotic runtime) → no lqip.\n if (typeof btoa !== 'function') return undefined\n let binary = ''\n const chunk = 0x8000\n for (let i = 0; i < bytes.length; i += chunk) {\n binary += String.fromCharCode(...bytes.subarray(i, i + chunk))\n }\n return btoa(binary)\n}\n\n/** Build a `data:<type>;base64,<payload>` URI from encoded bytes; `undefined` on failure. */\nfunction toDataUri(encoded: EncodedVariant): string | undefined {\n const b64 = bytesToBase64(encoded.bytes)\n return b64 ? `data:${encoded.contentType};base64,${b64}` : undefined\n}\n\n/** Blob storage + the (optionally injected) encoder the orchestrator needs. */\nexport interface VariantGenerationDeps {\n blobs: BlobStorageAdapter\n /** Defaults to {@link canvasVariantEncoder}; tests inject a deterministic fake. */\n encode?: VariantEncoder\n}\n\n/** Which variants the orchestrator should PRODUCE (backfill fills only the missing ones). */\nexport interface VariantWant {\n thumb?: boolean\n lqip?: boolean\n}\n\n/** The stored/inlined variants a generation run produced (either may be absent). */\nexport interface GeneratedVariants {\n /** The stored downscaled thumb (a second blob's key/url + dims), or absent. */\n thumb?: AssetThumbVariant\n /** The LQIP blur-placeholder `data:` URI (≤ cap), or absent. */\n lqip?: string\n}\n\n/**\n * Plan → encode (ONE decode) → store/inline BOTH variants (EC-400 + EC-420). The\n * thumb rides a SECOND `store()` call (the same blob boundary the upload uses); the\n * lqip is inlined as a capped base64 `data:` URI needing no blob store at all.\n * `want` narrows which variants to produce — the per-variant backfill (AD-4) asks\n * for only the missing one so an existing thumb/lqip is never regenerated. Returns\n * whichever variants succeeded; any skip/failure simply omits that variant (never\n * throws — a preview must never fail an upload).\n */\nexport async function generateVariants(\n deps: VariantGenerationDeps,\n source: Blob,\n meta: ThumbSource,\n want: VariantWant = { thumb: true, lqip: true },\n): Promise<GeneratedVariants> {\n const thumbPlan: ThumbPlan = want.thumb === false ? { kind: 'skip', reason: 'already-small' } : planThumb(meta)\n const lqipPlan: LqipPlan = want.lqip === false ? { kind: 'skip', reason: 'no-dimensions' } : planLqip(meta)\n\n const targets: VariantTargets = {}\n if (thumbPlan.kind === 'encode') {\n targets.thumb = { width: thumbPlan.width, height: thumbPlan.height, quality: THUMB_QUALITY }\n }\n if (lqipPlan.kind === 'encode') {\n targets.lqip = { width: lqipPlan.width, height: lqipPlan.height, quality: LQIP_QUALITY }\n }\n if (!targets.thumb && !targets.lqip) return {}\n\n const encode = deps.encode ?? canvasVariantEncoder\n let encoded: EncodedVariants\n try {\n encoded = await encode(source, targets)\n } catch {\n return {}\n }\n\n const result: GeneratedVariants = {}\n // Thumb: a SECOND stored blob (needs a key-minting backend; a put-only backend\n // simply gets no thumb — the lqip still ships).\n if (encoded.thumb && deps.blobs.store) {\n try {\n const storageKey = await deps.blobs.store(encoded.thumb.bytes, encoded.thumb.contentType)\n const url = (await deps.blobs.resolveUrl?.(storageKey)) ?? deps.blobs.url(storageKey)\n result.thumb = { storageKey, url, width: encoded.thumb.width, height: encoded.thumb.height }\n } catch {\n // No thumb — never fail the run.\n }\n }\n // Lqip: inline data URI, no blob. Enforce the hard cap; over-cap → skip (AD-1)\n // rather than silently persist an oversized value the schema would reject anyway.\n if (encoded.lqip) {\n const dataUri = toDataUri(encoded.lqip)\n if (dataUri && dataUri.length <= ASSET_LQIP_MAX_LENGTH) result.lqip = dataUri\n }\n return result\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AssetRecord, AssetRepository, BlobStorageAdapter } from '@elytracms/core/persistence';
|
|
2
2
|
import type { ImageDimensions } from './image-dimensions.js';
|
|
3
|
+
import type { GeneratedVariants } from './thumbnail.js';
|
|
3
4
|
import type { UploadFailure, UploadState } from './upload-state.js';
|
|
4
5
|
/**
|
|
5
6
|
* Upload orchestration (EC-151): request upload URL → transfer bytes →
|
|
@@ -31,6 +32,14 @@ export interface UploadDeps {
|
|
|
31
32
|
transport?: UploadTransport;
|
|
32
33
|
/** Injectable dimension detection; defaults to `createImageBitmap`-based. */
|
|
33
34
|
detectDimensions?: (file: UploadableFile) => Promise<ImageDimensions | undefined>;
|
|
35
|
+
/**
|
|
36
|
+
* Injectable preview generation (EC-400 thumb + EC-420 lqip). Defaults to a
|
|
37
|
+
* single `createImageBitmap` + canvas decode that stores the thumb via a second
|
|
38
|
+
* `blobs.store()` and inlines the lqip data URI. Returns `{}` (or a partial) for
|
|
39
|
+
* skipped/failed variants — a preview NEVER fails the upload (AD-2). Tests inject
|
|
40
|
+
* a deterministic implementation.
|
|
41
|
+
*/
|
|
42
|
+
generateVariants?: (file: UploadableFile, dimensions: ImageDimensions | undefined) => Promise<GeneratedVariants>;
|
|
34
43
|
}
|
|
35
44
|
export interface UploadRequest {
|
|
36
45
|
projectId: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { detectImageDimensions } from './image-dimensions.js';
|
|
2
|
+
import { generateVariants } from './thumbnail.js';
|
|
2
3
|
import { initialUploadState, uploadReducer } from './upload-state.js';
|
|
3
4
|
/**
|
|
4
5
|
* Default browser transport: XMLHttpRequest POST for real byte progress
|
|
@@ -40,6 +41,23 @@ async function defaultDetectDimensions(file) {
|
|
|
40
41
|
return undefined;
|
|
41
42
|
return detectImageDimensions(file);
|
|
42
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Default preview step (EC-400 thumb + EC-420 lqip): decode the real `File` (which
|
|
46
|
+
* IS a `Blob` in the browser) ONCE and produce both variants — store the thumb on
|
|
47
|
+
* `blobs`, inline the lqip data URI. Guards the `Blob` narrowing so a non-browser
|
|
48
|
+
* `UploadableFile` (or a backend without `store()`) simply yields no thumb — never
|
|
49
|
+
* an error that could fail the upload.
|
|
50
|
+
*/
|
|
51
|
+
function defaultGenerateVariants(blobs) {
|
|
52
|
+
return async (file, dimensions) => {
|
|
53
|
+
if (typeof Blob === 'undefined' || !(file instanceof Blob))
|
|
54
|
+
return {};
|
|
55
|
+
return generateVariants({ blobs }, file, {
|
|
56
|
+
contentType: file.type,
|
|
57
|
+
...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
}
|
|
43
61
|
function failureOf(stage, error) {
|
|
44
62
|
return {
|
|
45
63
|
stage,
|
|
@@ -104,6 +122,17 @@ export async function runUpload(deps, request, previous) {
|
|
|
104
122
|
try {
|
|
105
123
|
const detect = deps.detectDimensions ?? defaultDetectDimensions;
|
|
106
124
|
const dimensions = await detect(file);
|
|
125
|
+
// EC-400 + EC-420: produce the thumb + lqip previews from the bytes already in
|
|
126
|
+
// hand (one decode). A preview failure is swallowed — the asset simply ships
|
|
127
|
+
// without them (AD-2, scope §4); absence = today's behaviour, never an error.
|
|
128
|
+
const generate = deps.generateVariants ?? defaultGenerateVariants(blobs);
|
|
129
|
+
let variants;
|
|
130
|
+
try {
|
|
131
|
+
variants = await generate(file, dimensions);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
variants = {};
|
|
135
|
+
}
|
|
107
136
|
const url = (await blobs.resolveUrl?.(storageKey)) ?? blobs.url(storageKey);
|
|
108
137
|
const asset = await assets.upsertAsset(projectId, {
|
|
109
138
|
filename: file.name,
|
|
@@ -112,6 +141,8 @@ export async function runUpload(deps, request, previous) {
|
|
|
112
141
|
storageKey,
|
|
113
142
|
url,
|
|
114
143
|
...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),
|
|
144
|
+
...(variants.thumb ? { thumb: variants.thumb } : {}),
|
|
145
|
+
...(variants.lqip ? { lqip: variants.lqip } : {}),
|
|
115
146
|
...(request.alt !== undefined ? { alt: request.alt } : {}),
|
|
116
147
|
});
|
|
117
148
|
dispatch({ type: 'confirmed', assetId: asset.id });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../../../src/lib/assets/upload.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssetRecord, AssetRepository, BlobStorageAdapter } from '@elytracms/core/persistence'\nimport { detectImageDimensions } from './image-dimensions'\nimport type { ImageDimensions } from './image-dimensions'\nimport { initialUploadState, uploadReducer } from './upload-state'\nimport type { UploadEvent, UploadFailure, UploadState } from './upload-state'\n\n/**\n * Upload orchestration (EC-151): request upload URL → transfer bytes →\n * confirm by writing the EC-044 metadata record (dimensions, mime, serve URL).\n * Storage is reached *only* through the {@link BlobStorageAdapter}; every\n * effect is injectable so the whole flow unit-tests without a browser or a\n * live Convex deployment.\n */\n\n/** The structural slice of a browser `File` the upload flow needs. */\nexport interface UploadableFile {\n name: string\n type: string\n size: number\n arrayBuffer(): Promise<ArrayBuffer>\n}\n\n/** Sends bytes to an upload URL, reporting progress; returns the assigned storage key. */\nexport type UploadTransport = (\n uploadUrl: string,\n file: UploadableFile,\n onProgress: (loaded: number, total: number) => void,\n) => Promise<{ storageKey: string }>\n\n/**\n * Default browser transport: XMLHttpRequest POST for real byte progress\n * (fetch has no upload progress without streams support everywhere).\n */\nexport const xhrUploadTransport: UploadTransport = (uploadUrl, file, onProgress) =>\n new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest()\n xhr.open('POST', uploadUrl)\n if (file.type) xhr.setRequestHeader('Content-Type', file.type)\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable) onProgress(event.loaded, event.total)\n }\n xhr.onerror = () => reject(new Error('Network error while uploading bytes.'))\n xhr.onabort = () => reject(new Error('Upload was aborted.'))\n xhr.onload = () => {\n if (xhr.status < 200 || xhr.status >= 300) {\n reject(new Error(`Upload failed with HTTP ${xhr.status}.`))\n return\n }\n try {\n const payload = JSON.parse(xhr.responseText) as { storageId?: unknown }\n if (typeof payload.storageId === 'string' && payload.storageId.length > 0) {\n resolve({ storageKey: payload.storageId })\n } else {\n reject(new Error('Upload response did not include a storageId.'))\n }\n } catch {\n reject(new Error('Upload response was not valid JSON.'))\n }\n }\n void file.arrayBuffer().then(\n (bytes) => xhr.send(bytes),\n () => reject(new Error('Could not read file bytes.')),\n )\n })\n\nexport interface UploadDeps {\n blobs: BlobStorageAdapter\n assets: AssetRepository\n /** Client-direct transport; only used when `blobs.createUploadUrl` exists. */\n transport?: UploadTransport\n /** Injectable dimension detection; defaults to `createImageBitmap`-based. */\n detectDimensions?: (file: UploadableFile) => Promise<ImageDimensions | undefined>\n}\n\nexport interface UploadRequest {\n projectId: string\n file: UploadableFile\n alt?: string\n /** Observe every state transition (drives progress UI). */\n onState?: (state: UploadState) => void\n}\n\nexport type UploadResult =\n | { ok: true; asset: AssetRecord; state: UploadState }\n | { ok: false; failure: UploadFailure; state: UploadState }\n\nasync function defaultDetectDimensions(file: UploadableFile): Promise<ImageDimensions | undefined> {\n if (typeof Blob === 'undefined' || !(file instanceof Blob)) return undefined\n return detectImageDimensions(file as Blob & { type: string })\n}\n\nfunction failureOf(stage: UploadFailure['stage'], error: unknown): UploadFailure {\n return {\n stage,\n message: error instanceof Error ? error.message : String(error),\n retryable: true,\n }\n}\n\n/**\n * Run one upload attempt. Resolves with a discriminated result — it never\n * throws, and a failed transfer leaves no metadata record behind. Pass the\n * previous `state` to retry (the attempt counter carries over).\n */\nexport async function runUpload(\n deps: UploadDeps,\n request: UploadRequest,\n previous?: UploadState,\n): Promise<UploadResult> {\n let state = previous ?? initialUploadState\n const dispatch = (event: UploadEvent): void => {\n state = uploadReducer(state, event)\n request.onState?.(state)\n }\n dispatch(state.phase === 'failed' ? { type: 'retry' } : { type: 'start' })\n\n const { blobs, assets } = deps\n const { projectId, file } = request\n\n // --- prepare + transfer ---------------------------------------------------\n let storageKey: string\n try {\n if (blobs.createUploadUrl && deps.transport) {\n const uploadUrl = await blobs.createUploadUrl()\n dispatch({ type: 'url-ready' })\n try {\n const sent = await deps.transport(uploadUrl, file, (loaded, total) =>\n dispatch({ type: 'progress', loaded, total }),\n )\n storageKey = sent.storageKey\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('transfer', error) })\n return { ok: false, failure: failureOf('transfer', error), state }\n }\n } else {\n // Backend without client-direct uploads: push bytes through the adapter.\n dispatch({ type: 'url-ready' })\n const bytes = new Uint8Array(await file.arrayBuffer())\n try {\n if (blobs.store) {\n storageKey = await blobs.store(bytes, file.type)\n } else {\n storageKey = `${projectId}/${file.name}`\n await blobs.put(storageKey, bytes, file.type)\n }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('transfer', error) })\n return { ok: false, failure: failureOf('transfer', error), state }\n }\n }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('prepare', error) })\n return { ok: false, failure: failureOf('prepare', error), state }\n }\n dispatch({ type: 'transferred' })\n\n // --- confirm: metadata record linked to the stored blob --------------------\n try {\n const detect = deps.detectDimensions ?? defaultDetectDimensions\n const dimensions = await detect(file)\n const url = (await blobs.resolveUrl?.(storageKey)) ?? blobs.url(storageKey)\n const asset = await assets.upsertAsset(projectId, {\n filename: file.name,\n contentType: file.type || 'application/octet-stream',\n byteSize: file.size,\n storageKey,\n url,\n ...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),\n ...(request.alt !== undefined ? { alt: request.alt } : {}),\n })\n dispatch({ type: 'confirmed', assetId: asset.id })\n return { ok: true, asset, state }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('confirm', error) })\n return { ok: false, failure: failureOf('confirm', error), state }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../../../src/lib/assets/upload.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AssetRecord,\n AssetRepository,\n BlobStorageAdapter,\n} from '@elytracms/core/persistence'\nimport { detectImageDimensions } from './image-dimensions'\nimport type { ImageDimensions } from './image-dimensions'\nimport { generateVariants } from './thumbnail'\nimport type { GeneratedVariants } from './thumbnail'\nimport { initialUploadState, uploadReducer } from './upload-state'\nimport type { UploadEvent, UploadFailure, UploadState } from './upload-state'\n\n/**\n * Upload orchestration (EC-151): request upload URL → transfer bytes →\n * confirm by writing the EC-044 metadata record (dimensions, mime, serve URL).\n * Storage is reached *only* through the {@link BlobStorageAdapter}; every\n * effect is injectable so the whole flow unit-tests without a browser or a\n * live Convex deployment.\n */\n\n/** The structural slice of a browser `File` the upload flow needs. */\nexport interface UploadableFile {\n name: string\n type: string\n size: number\n arrayBuffer(): Promise<ArrayBuffer>\n}\n\n/** Sends bytes to an upload URL, reporting progress; returns the assigned storage key. */\nexport type UploadTransport = (\n uploadUrl: string,\n file: UploadableFile,\n onProgress: (loaded: number, total: number) => void,\n) => Promise<{ storageKey: string }>\n\n/**\n * Default browser transport: XMLHttpRequest POST for real byte progress\n * (fetch has no upload progress without streams support everywhere).\n */\nexport const xhrUploadTransport: UploadTransport = (uploadUrl, file, onProgress) =>\n new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest()\n xhr.open('POST', uploadUrl)\n if (file.type) xhr.setRequestHeader('Content-Type', file.type)\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable) onProgress(event.loaded, event.total)\n }\n xhr.onerror = () => reject(new Error('Network error while uploading bytes.'))\n xhr.onabort = () => reject(new Error('Upload was aborted.'))\n xhr.onload = () => {\n if (xhr.status < 200 || xhr.status >= 300) {\n reject(new Error(`Upload failed with HTTP ${xhr.status}.`))\n return\n }\n try {\n const payload = JSON.parse(xhr.responseText) as { storageId?: unknown }\n if (typeof payload.storageId === 'string' && payload.storageId.length > 0) {\n resolve({ storageKey: payload.storageId })\n } else {\n reject(new Error('Upload response did not include a storageId.'))\n }\n } catch {\n reject(new Error('Upload response was not valid JSON.'))\n }\n }\n void file.arrayBuffer().then(\n (bytes) => xhr.send(bytes),\n () => reject(new Error('Could not read file bytes.')),\n )\n })\n\nexport interface UploadDeps {\n blobs: BlobStorageAdapter\n assets: AssetRepository\n /** Client-direct transport; only used when `blobs.createUploadUrl` exists. */\n transport?: UploadTransport\n /** Injectable dimension detection; defaults to `createImageBitmap`-based. */\n detectDimensions?: (file: UploadableFile) => Promise<ImageDimensions | undefined>\n /**\n * Injectable preview generation (EC-400 thumb + EC-420 lqip). Defaults to a\n * single `createImageBitmap` + canvas decode that stores the thumb via a second\n * `blobs.store()` and inlines the lqip data URI. Returns `{}` (or a partial) for\n * skipped/failed variants — a preview NEVER fails the upload (AD-2). Tests inject\n * a deterministic implementation.\n */\n generateVariants?: (\n file: UploadableFile,\n dimensions: ImageDimensions | undefined,\n ) => Promise<GeneratedVariants>\n}\n\nexport interface UploadRequest {\n projectId: string\n file: UploadableFile\n alt?: string\n /** Observe every state transition (drives progress UI). */\n onState?: (state: UploadState) => void\n}\n\nexport type UploadResult =\n | { ok: true; asset: AssetRecord; state: UploadState }\n | { ok: false; failure: UploadFailure; state: UploadState }\n\nasync function defaultDetectDimensions(file: UploadableFile): Promise<ImageDimensions | undefined> {\n if (typeof Blob === 'undefined' || !(file instanceof Blob)) return undefined\n return detectImageDimensions(file as Blob & { type: string })\n}\n\n/**\n * Default preview step (EC-400 thumb + EC-420 lqip): decode the real `File` (which\n * IS a `Blob` in the browser) ONCE and produce both variants — store the thumb on\n * `blobs`, inline the lqip data URI. Guards the `Blob` narrowing so a non-browser\n * `UploadableFile` (or a backend without `store()`) simply yields no thumb — never\n * an error that could fail the upload.\n */\nfunction defaultGenerateVariants(blobs: BlobStorageAdapter) {\n return async (\n file: UploadableFile,\n dimensions: ImageDimensions | undefined,\n ): Promise<GeneratedVariants> => {\n if (typeof Blob === 'undefined' || !(file instanceof Blob)) return {}\n return generateVariants({ blobs }, file, {\n contentType: file.type,\n ...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),\n })\n }\n}\n\nfunction failureOf(stage: UploadFailure['stage'], error: unknown): UploadFailure {\n return {\n stage,\n message: error instanceof Error ? error.message : String(error),\n retryable: true,\n }\n}\n\n/**\n * Run one upload attempt. Resolves with a discriminated result — it never\n * throws, and a failed transfer leaves no metadata record behind. Pass the\n * previous `state` to retry (the attempt counter carries over).\n */\nexport async function runUpload(\n deps: UploadDeps,\n request: UploadRequest,\n previous?: UploadState,\n): Promise<UploadResult> {\n let state = previous ?? initialUploadState\n const dispatch = (event: UploadEvent): void => {\n state = uploadReducer(state, event)\n request.onState?.(state)\n }\n dispatch(state.phase === 'failed' ? { type: 'retry' } : { type: 'start' })\n\n const { blobs, assets } = deps\n const { projectId, file } = request\n\n // --- prepare + transfer ---------------------------------------------------\n let storageKey: string\n try {\n if (blobs.createUploadUrl && deps.transport) {\n const uploadUrl = await blobs.createUploadUrl()\n dispatch({ type: 'url-ready' })\n try {\n const sent = await deps.transport(uploadUrl, file, (loaded, total) =>\n dispatch({ type: 'progress', loaded, total }),\n )\n storageKey = sent.storageKey\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('transfer', error) })\n return { ok: false, failure: failureOf('transfer', error), state }\n }\n } else {\n // Backend without client-direct uploads: push bytes through the adapter.\n dispatch({ type: 'url-ready' })\n const bytes = new Uint8Array(await file.arrayBuffer())\n try {\n if (blobs.store) {\n storageKey = await blobs.store(bytes, file.type)\n } else {\n storageKey = `${projectId}/${file.name}`\n await blobs.put(storageKey, bytes, file.type)\n }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('transfer', error) })\n return { ok: false, failure: failureOf('transfer', error), state }\n }\n }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('prepare', error) })\n return { ok: false, failure: failureOf('prepare', error), state }\n }\n dispatch({ type: 'transferred' })\n\n // --- confirm: metadata record linked to the stored blob --------------------\n try {\n const detect = deps.detectDimensions ?? defaultDetectDimensions\n const dimensions = await detect(file)\n // EC-400 + EC-420: produce the thumb + lqip previews from the bytes already in\n // hand (one decode). A preview failure is swallowed — the asset simply ships\n // without them (AD-2, scope §4); absence = today's behaviour, never an error.\n const generate = deps.generateVariants ?? defaultGenerateVariants(blobs)\n let variants: GeneratedVariants\n try {\n variants = await generate(file, dimensions)\n } catch {\n variants = {}\n }\n const url = (await blobs.resolveUrl?.(storageKey)) ?? blobs.url(storageKey)\n const asset = await assets.upsertAsset(projectId, {\n filename: file.name,\n contentType: file.type || 'application/octet-stream',\n byteSize: file.size,\n storageKey,\n url,\n ...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),\n ...(variants.thumb ? { thumb: variants.thumb } : {}),\n ...(variants.lqip ? { lqip: variants.lqip } : {}),\n ...(request.alt !== undefined ? { alt: request.alt } : {}),\n })\n dispatch({ type: 'confirmed', assetId: asset.id })\n return { ok: true, asset, state }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('confirm', error) })\n return { ok: false, failure: failureOf('confirm', error), state }\n }\n}\n"]}
|
|
@@ -574,6 +574,13 @@ export const deMessages = {
|
|
|
574
574
|
'assets.delete.confirmAnyway': 'Trotzdem löschen',
|
|
575
575
|
'assets.focalPoint.noPreview': 'Keine Bildvorschau — laden Sie ein Bildmedium hoch oder wählen Sie eines aus, um einen Fokuspunkt festzulegen.',
|
|
576
576
|
'assets.focalPoint.setLabel': 'Fokuspunkt festlegen',
|
|
577
|
+
// Vorschauen erzeugen — EC-400 Thumbnail-Nachrüstung.
|
|
578
|
+
'assets.generatePreviews.button': 'Vorschauen erzeugen',
|
|
579
|
+
'assets.generatePreviews.hint': 'Vorhandene Bilder auf leichtere Vorschau-Thumbnails herunterskalieren und so Egress sparen. Bereits vorhandene werden übersprungen.',
|
|
580
|
+
'assets.generatePreviews.running': 'Vorschauen werden erzeugt… {done}/{total}',
|
|
581
|
+
'assets.generatePreviews.done': '{count} Vorschauen erzeugt',
|
|
582
|
+
'assets.generatePreviews.none': 'Vorschauen bereits aktuell',
|
|
583
|
+
'assets.generatePreviews.cancelled': 'Abgebrochen — {count} erzeugt',
|
|
577
584
|
// ─────────────────────────────────────────────────────────────────────────
|
|
578
585
|
// Users (section)
|
|
579
586
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -885,6 +892,10 @@ export const deMessages = {
|
|
|
885
892
|
'builder.canvas.undo': 'Rückgängig',
|
|
886
893
|
'builder.canvas.redo': 'Wiederholen',
|
|
887
894
|
'builder.canvas.insert': 'Einfügen',
|
|
895
|
+
// In-canvas "+" insertion affordances at block boundaries (EC-405).
|
|
896
|
+
'builder.canvas.insertBefore': 'Block davor einfügen',
|
|
897
|
+
'builder.canvas.insertAfter': 'Block danach einfügen',
|
|
898
|
+
'builder.canvas.insertInto': 'Block darin einfügen',
|
|
888
899
|
'builder.canvasTheme.legacyNotice': 'Die canvas.css dieses Projekts bündelt noch das Studio-Designsystem mit dem Site-Theme (die alte zusammengeführte Form). Richten Sie canvas.css stattdessen auf den eigenen Full-Tailwind-Theme-Einstieg der Site — siehe Theming-Leitfaden (docs/theming.md). Bis dahin funktioniert alles weiter.',
|
|
889
900
|
'builder.canvasTheme.stylesheetErrorNotice': 'Das Canvas-Stylesheet konnte nicht geladen werden — Blöcke werden ungestylt dargestellt. Prüfen Sie den canvas.css-Build des Studios.',
|
|
890
901
|
'builder.canvasTheme.noThemeTokensNotice': 'Das Canvas-Stylesheet definiert keine Theme-Tokens. canvas.css sollte den Full-Tailwind-Theme-Einstieg der Site importieren (siehe docs/theming.md).',
|
|
@@ -918,6 +929,11 @@ export const deMessages = {
|
|
|
918
929
|
'richText.blockquote': 'Zitat',
|
|
919
930
|
'richText.codeBlock': 'Codeblock',
|
|
920
931
|
'richText.horizontalRule': 'Trennlinie',
|
|
932
|
+
'richText.fontSize.label': 'Schriftgröße',
|
|
933
|
+
'richText.fontSize.small': 'Klein',
|
|
934
|
+
'richText.fontSize.normal': 'Normal',
|
|
935
|
+
'richText.fontSize.large': 'Groß',
|
|
936
|
+
'richText.fontSize.xlarge': 'Sehr groß',
|
|
921
937
|
'richText.undo': 'Rückgängig',
|
|
922
938
|
'richText.redo': 'Wiederholen',
|
|
923
939
|
'richText.link.add': 'Link',
|