directus-extension-csi-gallery-thumbnails 0.1.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +65 -25
  2. package/dist/index.js +1 -1
  3. package/package.json +7 -2
package/README.md CHANGED
@@ -1,27 +1,37 @@
1
- # CSI Gallery Thumbnails display
1
+ # CSI Image Thumbnails display
2
2
 
3
- A small Directus display that shows the first few images of a multi-image
4
- field as thumbnails, so table views show pictures instead of a count
5
- ("3 items").
3
+ A Directus display for single-image fields and complete galleries. Both follow
4
+ the same Compact, Cozy, and Comfortable table spacing. Gallery cells show
5
+ pictures instead of a count ("3 items").
6
6
 
7
7
  Directus's built-in Related Values display can only show a count for a
8
8
  multi-image field; its thumbnails appear after clicking the count. This display
9
- draws the thumbnails in the cell itself: the first four by default, then `+N`.
9
+ draws every thumbnail in the cell itself, wrapping onto additional lines when the column is narrow.
10
10
  Non-image files show a short type label such as `PDF`. Empty galleries show `--`.
11
11
 
12
- Thumbnails use the same `system-small-cover` preset and cache buster as
13
- Directus's own Image display, and load with the editor's session, so files in
14
- private folders work too.
12
+ Thumbnails follow Directus 12.3.1 table spacing: **Compact 32px**, **Cozy
13
+ 56px**, **Comfortable 80px**. Rows containing this display expand to fit the
14
+ images, including wrapped galleries. Other tables keep their native row height.
15
+ The display matches the native row's 31/45/60px CSS variable values, without
16
+ relying on their generated names; verify these values when upgrading Directus.
17
+
18
+ Images use 160px contain transformations for crisp 80px previews without
19
+ cropping, retain the modified-on cache buster, and load with the editor's
20
+ session so private files work too. Directus must allow these asset
21
+ transformations. Existing saved `limit` options are ignored; every photo is
22
+ shown. Empty and non-image file behavior is preserved.
15
23
 
16
24
  ## Fields
17
25
 
18
- | Field | Path to the file |
19
- | --- | --- |
20
- | `product_families.gallery` (Files) | `directus_files_id` (default) |
21
- | `products.gallery_assets` (Asset Library) | `asset_library_id.file` |
26
+ | Field | Path to the file |
27
+ | ------------------------------------------- | ----------------------------- |
28
+ | `products.thumbnail` (single file) | No option needed |
29
+ | `product_families.hero_image` (single file) | No option needed |
30
+ | `product_families.gallery` (Files) | `directus_files_id` (default) |
31
+ | `products.gallery_assets` (Asset Library) | `asset_library_id.file` |
22
32
 
23
- Set the field's display to **Gallery Thumbnails** and fill in **Path to the
24
- file**: the dot path from one gallery row (the junction item) to its
33
+ Set the field's display to **Image Thumbnails**. Single-file fields need no
34
+ options. For galleries, fill in **Path to the file**: the dot path from one gallery row (the junction item) to its
25
35
  `directus_files` record.
26
36
 
27
37
  ## Build and test
@@ -34,18 +44,48 @@ pnpm --filter directus-extension-csi-gallery-thumbnails build
34
44
 
35
45
  The build creates `dist/index.js`.
36
46
 
47
+ ## Image enlargement (0.3.0)
48
+
49
+ Hover over a thumbnail for a 400px preview after a short delay. Keyboard focus
50
+ also previews an image. The preview stays within the viewport and closes when
51
+ scrolling, resizing, leaving the thumbnail, or pressing Escape. Click a thumbnail
52
+ (or press Enter/Space) to open a persistent, larger image dialog. Close with its
53
+ button, Escape, or the backdrop; focus returns to the thumbnail. Touch users can
54
+ tap to open the dialog. Neither action navigates to the product record.
55
+
56
+ Larger images load only on interaction: 800px for hover and 1600px for the dialog,
57
+ using uncropped transformations and the existing Directus session. Empty galleries,
58
+ non-image files, thumbnail spacing and wrapping retain their existing behavior.
59
+
37
60
  ## Installation
38
61
 
39
- Like the other CSI extensions, it installs from the Directus Marketplace, which
40
- indexes the npm package:
62
+ Publish the package using the workflow below, then upgrade the pinned version
63
+ in `directus/deploy/Dockerfile` and deploy the Beanstalk bundle following
64
+ [the deployment runbook](../../deploy/README.md). The production extension is
65
+ baked into the image; a Marketplace install does not replace that local package.
66
+
67
+ ## Single-image activation
68
+
69
+ After 0.2.0 is installed, change only the display metadata on
70
+ `products.thumbnail` and `product_families.hero_image` to
71
+ `display=csi-gallery-thumbnails`, with `display_options=null`. Preserve their
72
+ interfaces and file references. The extension uses Directus's field type to
73
+ fetch the related file directly; it does not change the stored image.
74
+
75
+ ## Publishing without browser authentication
76
+
77
+ Configure an npm trusted publisher for this package with GitHub owner
78
+ `Nuwave-Marketing`, repository `csi-big-commerce-catalyst`, workflow filename
79
+ `publish-gallery-thumbnails.yml`, and **Allow npm publish** enabled. Leave the
80
+ environment name empty; dist-tag management is not needed.
41
81
 
42
- 1. Publish the package to npm (`npm publish` from this folder after a build).
43
- 2. Once the Marketplace lists it, install it with
44
- `POST /extensions/registry/install {"extension": "<marketplace id>"}`.
45
- 3. Set the field displays above.
82
+ After merging a version change to `main`, run **Publish Directus Image
83
+ Thumbnails** in GitHub Actions on `main` and enter the package version. The
84
+ workflow checks that version, tests and builds the extension using locked
85
+ dependencies, and publishes using GitHub's short-lived OIDC identity. It needs
86
+ no npm login, saved publishing token, or passkey prompt for routine releases.
87
+ An npm owner still authenticates when initially configuring the connection.
88
+ The workflow refuses releases from other branches.
46
89
 
47
- Self-hosted Directus currently loses registry-installed extensions on every
48
- container restart or instance replacement, so reinstall it with the others
49
- until the hosting team bakes the CSI extensions into the Directus image. While
50
- it is missing, the two fields show no thumbnails; switching their display back
51
- to Related Values restores the click-to-open list.
90
+ The repository is private, so npm currently does not generate public provenance
91
+ for this workflow. Trusted publishing still authenticates the release.
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{defineDisplay as e}from"@directus/extensions-sdk";import{defineComponent as t,reactive as n,computed as i,openBlock as a,createElementBlock as r,Fragment as l,renderList as s,unref as o,toDisplayString as u,createCommentVNode as d}from"vue";const f="directus_files_id",c=["id","type","title","modified_on"];function m(e){return("string"==typeof e?e.trim().replace(/^\.+|\.+$/g,""):"")||f}function p(e){return e.type?.startsWith("image/")??!1}function h(e,t){const n=new URL(`assets/${t.id}`,e);return n.searchParams.set("key","system-small-cover"),t.modified_on&&n.searchParams.set("v",t.modified_on),n.href}function y(e){const t=e.type?.split("/")[1]?.split(/[+.;-]/)[0];return(t||"file").slice(0,4).toUpperCase()}const v={key:0,class:"null"},g=["title"],b=["alt","src","onError"],_={key:1,class:"thumb file"},x={key:0,class:"more"};var k=t({__name:"display",props:{value:{},filePath:{},limit:{}},setup(e){const t=e,f=function(e){const t=e.split("/"),n=t.indexOf("admin");return`${t.slice(0,n).join("/")}/`}(window.location.href),c=n(new Set),k=i(()=>function(e,t){if(!Array.isArray(e))return[];const n=m(t);return e.flatMap(e=>{const t=function(e,t){return t.split(".").reduce((e,t)=>{if(e&&"object"==typeof e)return e[t]},e)}(e,n);if(!t||"object"!=typeof t)return[];const{id:i,type:a,title:r,modified_on:l}=t;return"string"==typeof i&&i?[{id:i,type:"string"==typeof a?a:null,title:"string"==typeof r?r:null,modified_on:"string"==typeof l?l:null}]:[]})}(t.value,t.filePath)),w=i(()=>k.value.slice(0,function(e){const t="number"==typeof e?e:Number.parseInt(String(e??""),10);return!Number.isFinite(t)||t<1?4:Math.min(Math.floor(t),20)}(t.limit))),j=i(()=>k.value.length-w.value.length),A=i(()=>`${k.value.length} ${1===k.value.length?"file":"files"}`);return(e,t)=>0===k.value.length?(a(),r("span",v,"--")):(a(),r("span",{key:1,class:"gallery-thumbnails",title:A.value},[(a(!0),r(l,null,s(w.value,e=>(a(),r(l,{key:e.id},[o(p)(e)&&!c.has(e.id)?(a(),r("img",{key:0,alt:e.title??"",src:o(h)(o(f),e),class:"thumb",loading:"lazy",onError:t=>{return n=e.id,void c.add(n);var n}},null,40,b)):(a(),r("span",_,u(o(y)(e)),1))],64))),128)),j.value>0?(a(),r("span",x,"+"+u(j.value),1)):d("v-if",!0)],8,g))}}),w=[],j=[];!function(e,t){if(e&&"undefined"!=typeof document){var n,i=!0===t.prepend?"prepend":"append",a=!0===t.singleTag,r="string"==typeof t.container?document.querySelector(t.container):document.getElementsByTagName("head")[0];if(a){var l=w.indexOf(r);-1===l&&(l=w.push(r)-1,j[l]={}),n=j[l]&&j[l][i]?j[l][i]:j[l][i]=s()}else n=s();65279===e.charCodeAt(0)&&(e=e.substring(1)),n.styleSheet?n.styleSheet.cssText+=e:n.appendChild(document.createTextNode(e))}function s(){var e=document.createElement("style");if(e.setAttribute("type","text/css"),t.attributes)for(var n=Object.keys(t.attributes),a=0;a<n.length;a++)e.setAttribute(n[a],t.attributes[n[a]]);var l="prepend"===i?"afterbegin":"beforeend";return r.insertAdjacentElement(l,e),e}}("\n.gallery-thumbnails[data-v-dfa8e593] {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n max-inline-size: 100%;\n vertical-align: middle;\n}\n.thumb[data-v-dfa8e593] {\n flex: none;\n inline-size: 32px;\n block-size: 32px;\n border-radius: var(--theme--border-radius);\n object-fit: cover;\n background-color: var(--theme--background-normal);\n}\n.file[data-v-dfa8e593] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--theme--foreground-subdued);\n font-size: 9px;\n font-weight: 600;\n}\n.more[data-v-dfa8e593],\n.null[data-v-dfa8e593] {\n color: var(--theme--foreground-subdued);\n}\n.more[data-v-dfa8e593] {\n flex: none;\n font-size: 12px;\n}\n",{});var A=e({id:"csi-gallery-thumbnails",name:"Gallery Thumbnails",icon:"photo_library",description:"Show the first few images of a multi-image field as thumbnails.",component:((e,t)=>{const n=e.__vccOpts||e;for(const[e,i]of t)n[e]=i;return n})(k,[["__scopeId","data-v-dfa8e593"]]),types:["alias"],localTypes:["files","m2m","o2m"],fields:e=>function(e){const t=m(e);return c.map(e=>`${t}.${e}`)}(e?.filePath),options:[{field:"filePath",name:"Path to the file",type:"string",meta:{interface:"input",width:"half",note:"From one gallery row to its file. Files field: directus_files_id. Asset Library: asset_library_id.file",options:{placeholder:f,font:"monospace"}},schema:{default_value:f}},{field:"limit",name:"Thumbnails to show",type:"integer",meta:{interface:"input",width:"half",options:{min:1,max:20}},schema:{default_value:4}}]});export{A as default};
1
+ import{defineDisplay as e}from"@directus/extensions-sdk";import{defineComponent as n,reactive as t,computed as i,ref as a,watch as r,onBeforeUnmount as o,openBlock as l,createElementBlock as s,Fragment as d,renderList as u,unref as c,withKeys as p,withModifiers as f,createElementVNode as m,toDisplayString as g,createBlock as h,Teleport as v,normalizeStyle as b,createCommentVNode as y,nextTick as x}from"vue";const w="directus_files_id",k=["id","type","title","modified_on"];function z(e){return("string"==typeof e?e.trim().replace(/^\.+|\.+$/g,""):"")||w}function _(e){return e.type?.startsWith("image/")??!1}function j(e,n){return C(e,n,160,"contain")}function C(e,n,t,i="inside"){const a=new URL(`assets/${n.id}`,e);return a.searchParams.set("width",String(t)),a.searchParams.set("height",String(t)),a.searchParams.set("fit",i),a.searchParams.set("format","auto"),n.modified_on&&a.searchParams.set("v",n.modified_on),a.href}function E(e){const n=e.type?.split("/")[1]?.split(/[+.;-]/)[0];return(n||"file").slice(0,4).toUpperCase()}const P={key:0,class:"csi-gallery-thumbnails null"},T={key:1,class:"csi-gallery-thumbnails"},$=["aria-label","onPointerenter","onFocus","onClick","onKeydown"],M=["alt","src","onError"],S={key:1,class:"thumb file"},A=["src"],L={key:1},I=["src","alt"],O={key:1,role:"status"};var F=n({__name:"display",props:{value:{},type:{},filePath:{}},setup(e){const n=e,w=function(e){const n=e.split("/"),t=n.indexOf("admin");return`${n.slice(0,t).join("/")}/`}(window.location.href),k=t(new Set),F=i(()=>function(e,n,t){const i="uuid"===t?[e]:Array.isArray(e)?e:[],a=z(n);return i.flatMap(e=>{const n="uuid"===t?e:function(e,n){return n.split(".").reduce((e,n)=>{if(e&&"object"==typeof e)return e[n]},e)}(e,a);if(!n||"object"!=typeof n)return[];const{id:i,type:r,title:o,modified_on:l}=n;return"string"==typeof i&&i?[{id:i,type:"string"==typeof r?r:null,title:"string"==typeof o?o:null,modified_on:"string"==typeof l?l:null}]:[]})}(n.value,n.filePath,n.type)),B=a(null),D=a({}),K=a(!1),N=a(null),R=a(!1),U=a(null);let W;function q(){clearTimeout(W),B.value=null,window.removeEventListener("scroll",q,!0),window.removeEventListener("resize",q)}function H(e,n){if(q(),N.value||!e.isConnected)return;const t=function(e,n,t){const i=12,a=Math.max(1,Math.min(400,n-24,t-24)),r=e.right+i+a<=n-i?e.right+i:e.left-i-a;return{left:Math.max(i,Math.min(r,n-a-i)),top:Math.max(i,Math.min(e.top,t-a-i)),width:a,height:a}}(e.getBoundingClientRect(),window.innerWidth,window.innerHeight);D.value=Object.fromEntries(Object.entries(t).map(([e,n])=>[e,`${n}px`])),K.value=!1,B.value=n,window.addEventListener("scroll",q,!0),window.addEventListener("resize",q)}function V(){U.value?.close(),N.value=null}function G(e){e.target===U.value&&V()}return r(F,()=>{q(),V()}),o(()=>{q(),V()}),(e,n)=>(l(),s(d,null,[0===F.value.length?(l(),s("span",P,"--")):(l(),s("span",T,[(l(!0),s(d,null,u(F.value,(e,t)=>(l(),s(d,{key:`${e.id}-${t}`},[c(_)(e)&&!k.has(e.id)?(l(),s("button",{key:0,type:"button",class:"image-button","aria-label":`Enlarge ${e.title||`image ${t+1}`}`,"aria-haspopup":"dialog",onPointerenter:n=>function(e,n){if("touch"===e.pointerType)return;q();const t=e.currentTarget;W=setTimeout(()=>H(t,n),150)}(n,e),onPointerleave:q,onFocus:n=>function(e,n){H(e.currentTarget,n)}(n,e),onBlur:q,onClick:f(n=>async function(e){q(),R.value=!1,N.value=e,await x(),U.value?.showModal()}(e),["stop"]),onDblclick:n[0]||(n[0]=f(()=>{},["stop"])),onKeydown:[n[1]||(n[1]=p(f(()=>{},["stop"]),["enter"])),n[2]||(n[2]=p(f(()=>{},["stop"]),["space"])),p(f(q,["stop"]),["esc"])]},[m("img",{alt:e.title??"",src:c(j)(c(w),e),class:"thumb",loading:"lazy",onError:n=>{return t=e.id,k.add(t),void(B.value?.id===t&&q());var t}},null,40,M)],40,$)):(l(),s("span",S,g(c(E)(e)),1))],64))),128))])),(l(),h(v,{to:"body"},[B.value?(l(),s("div",{key:0,class:"csi-image-preview",style:b(D.value),"aria-hidden":"true"},[K.value?(l(),s("span",L,"Image preview unavailable")):(l(),s("img",{key:B.value.id,src:c(C)(c(w),B.value,800),alt:"",onError:n[3]||(n[3]=e=>K.value=!0)},null,40,A))],4)):y("v-if",!0),m("dialog",{ref_key:"dialog",ref:U,class:"csi-image-dialog","aria-label":"Image preview",onClick:f(G,["stop"]),onClose:n[5]||(n[5]=e=>N.value=null),onCancel:n[6]||(n[6]=e=>N.value=null)},[N.value?(l(),s(d,{key:0},[m("header",null,[m("span",null,g(N.value.title||"Image preview"),1),m("button",{type:"button",autofocus:"","aria-label":"Close image preview",onClick:V}," × ")]),R.value?(l(),s("p",O,"Image preview unavailable")):(l(),s("img",{key:N.value.id,src:c(C)(c(w),N.value,1600),alt:N.value.title??"",onError:n[4]||(n[4]=e=>R.value=!0)},null,40,I))],64)):y("v-if",!0)],544)]))],64))}}),B=[],D=[];!function(e,n){if(e&&"undefined"!=typeof document){var t,i=!0===n.prepend?"prepend":"append",a=!0===n.singleTag,r="string"==typeof n.container?document.querySelector(n.container):document.getElementsByTagName("head")[0];if(a){var o=B.indexOf(r);-1===o&&(o=B.push(r)-1,D[o]={}),t=D[o]&&D[o][i]?D[o][i]:D[o][i]=l()}else t=l();65279===e.charCodeAt(0)&&(e=e.substring(1)),t.styleSheet?t.styleSheet.cssText+=e:t.appendChild(document.createTextNode(e))}function l(){var e=document.createElement("style");if(e.setAttribute("type","text/css"),n.attributes)for(var t=Object.keys(n.attributes),a=0;a<t.length;a++)e.setAttribute(t[a],n.attributes[t[a]]);var o="prepend"===i?"afterbegin":"beforeend";return r.insertAdjacentElement(o,e),e}}('\n.csi-gallery-thumbnails[data-v-dad23590] {\n display: inline-flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 4px;\n max-inline-size: 100%;\n min-inline-size: 0;\n vertical-align: middle;\n}\n.image-button[data-v-dad23590] {\n display: inline-flex;\n flex: none;\n padding: 0;\n border: 0;\n border-radius: var(--theme--border-radius);\n background: transparent;\n cursor: zoom-in;\n}\n.image-button[data-v-dad23590]:focus-visible {\n outline: 2px solid var(--theme--primary, #6644ff);\n outline-offset: 2px;\n}\n.thumb[data-v-dad23590] {\n flex: none;\n inline-size: var(--csi-gallery-thumbnail-size, 32px);\n block-size: var(--csi-gallery-thumbnail-size, 32px);\n border-radius: var(--theme--border-radius);\n object-fit: contain;\n background-color: var(--theme--background-normal);\n}\n.file[data-v-dad23590] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--theme--foreground-subdued);\n font-size: 9px;\n font-weight: 600;\n}\n.null[data-v-dad23590] {\n color: var(--theme--foreground-subdued);\n}\n\n/* Directus 12.3 table rows expose their native spacing as Vue CSS variables:\n 29/43/58px plus a 2px border. Match those values without depending on the\n generated variable name. Leave the native variables intact so switching\n spacing continues to work; only rows containing this display grow. */\n.table-row:has(.csi-gallery-thumbnails) {\n --csi-gallery-thumbnail-size: 32px;\n block-size: auto;\n min-block-size: calc(var(--csi-gallery-thumbnail-size) + 16px);\n}\n.table-row[style*=": 45px;"]:has(.csi-gallery-thumbnails) {\n --csi-gallery-thumbnail-size: 56px;\n}\n.table-row[style*=": 60px;"]:has(.csi-gallery-thumbnails) {\n --csi-gallery-thumbnail-size: 80px;\n}\n.csi-image-preview[data-v-dad23590] {\n position: fixed;\n z-index: 10000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 8px;\n box-sizing: border-box;\n border: 1px solid var(--theme--border-color, #dde4e8);\n border-radius: var(--theme--border-radius, 6px);\n background: var(--theme--background, #fff);\n color: var(--theme--foreground, #26333f);\n box-shadow: 0 8px 32px #0003;\n pointer-events: none;\n}\n.csi-image-preview img[data-v-dad23590] {\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n.csi-image-dialog[data-v-dad23590] {\n width: min(960px, calc(100vw - 32px));\n max-height: calc(100dvh - 32px);\n padding: 16px;\n box-sizing: border-box;\n border: 1px solid var(--theme--border-color, #dde4e8);\n border-radius: var(--theme--border-radius, 6px);\n background: var(--theme--background, #fff);\n color: var(--theme--foreground, #26333f);\n}\n.csi-image-dialog[data-v-dad23590]::backdrop {\n background: #0009;\n}\n.csi-image-dialog header[data-v-dad23590] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 12px;\n}\n.csi-image-dialog header span[data-v-dad23590] {\n overflow-wrap: anywhere;\n}\n.csi-image-dialog button[data-v-dad23590] {\n flex: none;\n width: 36px;\n height: 36px;\n border: 0;\n border-radius: 6px;\n font: 28px/1 sans-serif;\n cursor: pointer;\n color: inherit;\n background: var(--theme--background-normal, #f1f4f6);\n}\n.csi-image-dialog > img[data-v-dad23590] {\n display: block;\n width: 100%;\n height: min(700px, 70dvh);\n object-fit: contain;\n}\n',{});var K=e({id:"csi-gallery-thumbnails",name:"Image Thumbnails",icon:"photo_library",description:"Show every image with hover previews and click-to-enlarge.",component:((e,n)=>{const t=e.__vccOpts||e;for(const[e,i]of n)t[e]=i;return t})(F,[["__scopeId","data-v-dad23590"]]),types:["alias","uuid"],localTypes:["files","m2m","o2m","file"],fields:(e,n)=>function(e,n){if("uuid"===n)return[...k];const t=z(e);return k.map(e=>`${t}.${e}`)}(e?.filePath,n.type),options:e=>"uuid"===e.field?.type?[]:[{field:"filePath",name:"Path to the file",type:"string",meta:{interface:"input",width:"half",note:"From one gallery row to its file. Files field: directus_files_id. Asset Library: asset_library_id.file",options:{placeholder:w,font:"monospace"}},schema:{default_value:w}}]});export{K as default};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "directus-extension-csi-gallery-thumbnails",
3
- "version": "0.1.0",
4
- "description": "Directus display that shows a row of image thumbnails for multi-file gallery fields in table views.",
3
+ "version": "0.3.0",
4
+ "description": "Directus display for single images and complete galleries, sized to table spacing.",
5
5
  "keywords": [
6
6
  "directus",
7
7
  "directus-extension",
@@ -11,6 +11,11 @@
11
11
  ],
12
12
  "icon": "photo_library",
13
13
  "license": "UNLICENSED",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/Nuwave-Marketing/csi-big-commerce-catalyst.git",
17
+ "directory": "directus/extensions/csi-gallery-thumbnails"
18
+ },
14
19
  "publishConfig": {
15
20
  "access": "public"
16
21
  },