@object-ui/layout 2.0.0 → 3.0.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.
@@ -1,6 +1,6 @@
1
- (function(d,y){typeof exports=="object"&&typeof module<"u"?y(exports,require("@object-ui/core"),require("react"),require("@object-ui/components"),require("@object-ui/react"),require("react-router-dom")):typeof define=="function"&&define.amd?define(["exports","@object-ui/core","react","@object-ui/components","@object-ui/react","react-router-dom"],y):(d=typeof globalThis<"u"?globalThis:d||self,y(d.ObjectUILayout={},d.core,d.require$$0,d.components,d.react,d.reactRouterDom))})(this,(function(d,y,w,m,Q,I){"use strict";var _={exports:{}},x={};var L;function K(){if(L)return x;L=1;var r=Symbol.for("react.transitional.element"),c=Symbol.for("react.fragment");function a(s,l,n){var p=null;if(n!==void 0&&(p=""+n),l.key!==void 0&&(p=""+l.key),"key"in l){n={};for(var b in l)b!=="key"&&(n[b]=l[b])}else n=l;return l=n.ref,{$$typeof:r,type:s,key:p,ref:l!==void 0?l:null,props:n}}return x.Fragment=c,x.jsx=a,x.jsxs=a,x}var E={};var M;function ee(){return M||(M=1,process.env.NODE_ENV!=="production"&&(function(){function r(e){if(e==null)return null;if(typeof e=="function")return e.$$typeof===be?null:e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case P:return"Fragment";case le:return"Profiler";case se:return"StrictMode";case fe:return"Suspense";case de:return"SuspenseList";case pe:return"Activity"}if(typeof e=="object")switch(typeof e.tag=="number"&&console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),e.$$typeof){case oe:return"Portal";case ie:return e.displayName||"Context";case ce:return(e._context.displayName||"Context")+".Consumer";case ue:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case me:return t=e.displayName||null,t!==null?t:r(e.type)||"Memo";case k:t=e._payload,e=e._init;try{return r(e(t))}catch{}}return null}function c(e){return""+e}function a(e){try{c(e);var t=!1}catch{t=!0}if(t){t=console;var i=t.error,u=typeof Symbol=="function"&&Symbol.toStringTag&&e[Symbol.toStringTag]||e.constructor.name||"Object";return i.call(t,"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",u),c(e)}}function s(e){if(e===P)return"<>";if(typeof e=="object"&&e!==null&&e.$$typeof===k)return"<...>";try{var t=r(e);return t?"<"+t+">":"<...>"}catch{return"<...>"}}function l(){var e=A.A;return e===null?null:e.getOwner()}function n(){return Error("react-stack-top-frame")}function p(e){if(V.call(e,"key")){var t=Object.getOwnPropertyDescriptor(e,"key").get;if(t&&t.isReactWarning)return!1}return e.key!==void 0}function b(e,t){function i(){z||(z=!0,console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",t))}i.isReactWarning=!0,Object.defineProperty(e,"key",{get:i,configurable:!0})}function R(){var e=r(this.type);return H[e]||(H[e]=!0,console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.")),e=this.props.ref,e!==void 0?e:null}function g(e,t,i,u,T,O){var f=i.ref;return e={$$typeof:J,type:e,key:t,props:i,_owner:u},(f!==void 0?f:null)!==null?Object.defineProperty(e,"ref",{enumerable:!1,get:R}):Object.defineProperty(e,"ref",{enumerable:!1,value:null}),e._store={},Object.defineProperty(e._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:0}),Object.defineProperty(e,"_debugInfo",{configurable:!1,enumerable:!1,writable:!0,value:null}),Object.defineProperty(e,"_debugStack",{configurable:!1,enumerable:!1,writable:!0,value:T}),Object.defineProperty(e,"_debugTask",{configurable:!1,enumerable:!1,writable:!0,value:O}),Object.freeze&&(Object.freeze(e.props),Object.freeze(e)),e}function h(e,t,i,u,T,O){var f=t.children;if(f!==void 0)if(u)if(ye(f)){for(u=0;u<f.length;u++)W(f[u]);Object.freeze&&Object.freeze(f)}else console.error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");else W(f);if(V.call(t,"key")){f=r(e);var v=Object.keys(t).filter(function(he){return he!=="key"});u=0<v.length?"{key: someKey, "+v.join(": ..., ")+": ...}":"{key: someKey}",Z[f+u]||(v=0<v.length?"{"+v.join(": ..., ")+": ...}":"{}",console.error(`A props object containing a "key" prop is being spread into JSX:
1
+ (function(f,b){typeof exports=="object"&&typeof module<"u"?b(exports,require("@object-ui/core"),require("react"),require("@object-ui/components"),require("@object-ui/react"),require("react-router-dom")):typeof define=="function"&&define.amd?define(["exports","@object-ui/core","react","@object-ui/components","@object-ui/react","react-router-dom"],b):(f=typeof globalThis<"u"?globalThis:f||self,b(f.ObjectUILayout={},f.core,f.require$$0,f.components,f.react,f.reactRouterDom))})(this,(function(f,b,w,m,K,L){"use strict";var j={exports:{}},v={};var M;function ee(){if(M)return v;M=1;var r=Symbol.for("react.transitional.element"),l=Symbol.for("react.fragment");function t(s,n,o){var p=null;if(o!==void 0&&(p=""+o),n.key!==void 0&&(p=""+n.key),"key"in n){o={};for(var g in n)g!=="key"&&(o[g]=n[g])}else o=n;return n=o.ref,{$$typeof:r,type:s,key:p,ref:n!==void 0?n:null,props:o}}return v.Fragment=l,v.jsx=t,v.jsxs=t,v}var E={};var I;function re(){return I||(I=1,process.env.NODE_ENV!=="production"&&(function(){function r(e){if(e==null)return null;if(typeof e=="function")return e.$$typeof===he?null:e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case P:return"Fragment";case de:return"Profiler";case ue:return"StrictMode";case ge:return"Suspense";case be:return"SuspenseList";case xe:return"Activity"}if(typeof e=="object")switch(typeof e.tag=="number"&&console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),e.$$typeof){case ie:return"Portal";case me:return e.displayName||"Context";case fe:return(e._context.displayName||"Context")+".Consumer";case pe:var a=e.render;return e=e.displayName,e||(e=a.displayName||a.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ye:return a=e.displayName||null,a!==null?a:r(e.type)||"Memo";case k:a=e._payload,e=e._init;try{return r(e(a))}catch{}}return null}function l(e){return""+e}function t(e){try{l(e);var a=!1}catch{a=!0}if(a){a=console;var i=a.error,u=typeof Symbol=="function"&&Symbol.toStringTag&&e[Symbol.toStringTag]||e.constructor.name||"Object";return i.call(a,"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",u),l(e)}}function s(e){if(e===P)return"<>";if(typeof e=="object"&&e!==null&&e.$$typeof===k)return"<...>";try{var a=r(e);return a?"<"+a+">":"<...>"}catch{return"<...>"}}function n(){var e=C.A;return e===null?null:e.getOwner()}function o(){return Error("react-stack-top-frame")}function p(e){if(z.call(e,"key")){var a=Object.getOwnPropertyDescriptor(e,"key").get;if(a&&a.isReactWarning)return!1}return e.key!==void 0}function g(e,a){function i(){H||(H=!0,console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",a))}i.isReactWarning=!0,Object.defineProperty(e,"key",{get:i,configurable:!0})}function x(){var e=r(this.type);return B[e]||(B[e]=!0,console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.")),e=this.props.ref,e!==void 0?e:null}function R(e,a,i,u,T,N){var d=i.ref;return e={$$typeof:V,type:e,key:a,props:i,_owner:u},(d!==void 0?d:null)!==null?Object.defineProperty(e,"ref",{enumerable:!1,get:x}):Object.defineProperty(e,"ref",{enumerable:!1,value:null}),e._store={},Object.defineProperty(e._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:0}),Object.defineProperty(e,"_debugInfo",{configurable:!1,enumerable:!1,writable:!0,value:null}),Object.defineProperty(e,"_debugStack",{configurable:!1,enumerable:!1,writable:!0,value:T}),Object.defineProperty(e,"_debugTask",{configurable:!1,enumerable:!1,writable:!0,value:N}),Object.freeze&&(Object.freeze(e.props),Object.freeze(e)),e}function y(e,a,i,u,T,N){var d=a.children;if(d!==void 0)if(u)if(ve(d)){for(u=0;u<d.length;u++)W(d[u]);Object.freeze&&Object.freeze(d)}else console.error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");else W(d);if(z.call(a,"key")){d=r(e);var h=Object.keys(a).filter(function(Ee){return Ee!=="key"});u=0<h.length?"{key: someKey, "+h.join(": ..., ")+": ...}":"{key: someKey}",Q[d+u]||(h=0<h.length?"{"+h.join(": ..., ")+": ...}":"{}",console.error(`A props object containing a "key" prop is being spread into JSX:
2
2
  let props = %s;
3
3
  <%s {...props} />
4
4
  React keys must be passed directly to JSX without using spread:
5
5
  let props = %s;
6
- <%s key={someKey} {...props} />`,u,f,v,f),Z[f+u]=!0)}if(f=null,i!==void 0&&(a(i),f=""+i),p(t)&&(a(t.key),f=""+t.key),"key"in t){i={};for(var C in t)C!=="key"&&(i[C]=t[C])}else i=t;return f&&b(i,typeof e=="function"?e.displayName||e.name||"Unknown":e),g(e,f,i,l(),T,O)}function W(e){G(e)?e._store&&(e._store.validated=1):typeof e=="object"&&e!==null&&e.$$typeof===k&&(e._payload.status==="fulfilled"?G(e._payload.value)&&e._payload.value._store&&(e._payload.value._store.validated=1):e._store&&(e._store.validated=1))}function G(e){return typeof e=="object"&&e!==null&&e.$$typeof===J}var S=w,J=Symbol.for("react.transitional.element"),oe=Symbol.for("react.portal"),P=Symbol.for("react.fragment"),se=Symbol.for("react.strict_mode"),le=Symbol.for("react.profiler"),ce=Symbol.for("react.consumer"),ie=Symbol.for("react.context"),ue=Symbol.for("react.forward_ref"),fe=Symbol.for("react.suspense"),de=Symbol.for("react.suspense_list"),me=Symbol.for("react.memo"),k=Symbol.for("react.lazy"),pe=Symbol.for("react.activity"),be=Symbol.for("react.client.reference"),A=S.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,V=Object.prototype.hasOwnProperty,ye=Array.isArray,N=console.createTask?console.createTask:function(){return null};S={react_stack_bottom_frame:function(e){return e()}};var z,H={},B=S.react_stack_bottom_frame.bind(S,n)(),X=N(s(n)),Z={};E.Fragment=P,E.jsx=function(e,t,i){var u=1e4>A.recentlyCreatedOwnerStacks++;return h(e,t,i,!1,u?Error("react-stack-top-frame"):B,u?N(s(e)):X)},E.jsxs=function(e,t,i){var u=1e4>A.recentlyCreatedOwnerStacks++;return h(e,t,i,!0,u?Error("react-stack-top-frame"):B,u?N(s(e)):X)}})()),E}var Y;function re(){return Y||(Y=1,process.env.NODE_ENV==="production"?_.exports=K():_.exports=ee()),_.exports}var o=re();function j({title:r,description:c,action:a,className:s,children:l,...n}){return o.jsx("div",{className:m.cn("flex flex-col gap-4 pb-4 md:pb-8",s),...n,children:o.jsxs("div",{className:"flex items-center justify-between gap-4",children:[o.jsxs("div",{className:"flex flex-col gap-1",children:[o.jsx("h1",{className:"text-2xl font-bold tracking-tight md:text-3xl",children:r}),c&&o.jsx("p",{className:"text-sm text-muted-foreground",children:c})]}),(a||l)&&o.jsxs("div",{className:"flex items-center gap-2",children:[a,l]})]})})}function $(r){const c=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(r);if(!c)return null;const a=parseInt(c[1],16)/255,s=parseInt(c[2],16)/255,l=parseInt(c[3],16)/255,n=Math.max(a,s,l),p=Math.min(a,s,l);let b=0,R=0;const g=(n+p)/2;if(n!==p){const h=n-p;switch(R=g>.5?h/(2-n-p):h/(n+p),n){case a:b=((s-l)/h+(s<l?6:0))/6;break;case s:b=((l-a)/h+2)/6;break;case l:b=((a-s)/h+4)/6;break}}return`${Math.round(b*360)} ${Math.round(R*100)}% ${Math.round(g*100)}%`}function q(r,c){w.useEffect(()=>{const a=document.documentElement;if(r?.primaryColor){const s=$(r.primaryColor);s&&(a.style.setProperty("--brand-primary",r.primaryColor),a.style.setProperty("--brand-primary-hsl",s))}else a.style.removeProperty("--brand-primary"),a.style.removeProperty("--brand-primary-hsl");if(r?.accentColor){const s=$(r.accentColor);s&&(a.style.setProperty("--brand-accent",r.accentColor),a.style.setProperty("--brand-accent-hsl",s))}else a.style.removeProperty("--brand-accent"),a.style.removeProperty("--brand-accent-hsl");if(r?.favicon){const s=document.querySelector("#favicon")||document.querySelector('link[rel="icon"]');s&&(s.href=r.favicon)}return c&&(document.title=c),()=>{a.style.removeProperty("--brand-primary"),a.style.removeProperty("--brand-primary-hsl"),a.style.removeProperty("--brand-accent"),a.style.removeProperty("--brand-accent-hsl")}},[r?.primaryColor,r?.accentColor,r?.favicon,c])}function F({sidebar:r,navbar:c,children:a,className:s,defaultOpen:l=!0,branding:n}){return q(n,n?.title),o.jsxs(m.SidebarProvider,{defaultOpen:l,children:[r,o.jsxs(m.SidebarInset,{children:[o.jsxs("header",{className:"flex h-14 sm:h-16 shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4",children:[o.jsx(m.SidebarTrigger,{className:"-ml-1"}),o.jsx("div",{className:"w-px h-4 bg-border mx-1 sm:mx-2"}),c]}),o.jsx("main",{className:m.cn("flex-1 overflow-auto p-3 sm:p-4 md:p-6",s),children:a})]})]})}function D({className:r,children:c,...a}){return o.jsx("div",{className:m.cn("rounded-xl border bg-card text-card-foreground shadow",r),...a,children:o.jsx("div",{className:"p-6",children:c})})}const te=r=>r?Array.isArray(r)?r:[r]:[];function ae({schema:r,className:c,style:a,id:s,...l}){const n=te(r.children);return o.jsxs("div",{id:s||r.id,className:m.cn("flex flex-col h-full space-y-4",c),style:a,children:[o.jsx(j,{title:r.title,description:r.description}),o.jsx("div",{className:"flex-1 overflow-auto",children:n.map((p,b)=>o.jsx(Q.SchemaRenderer,{schema:p,...l},p?.id||b))})]})}function ne({items:r,title:c="Application",className:a,collapsible:s="icon"}){const l=I.useLocation();return o.jsx(m.Sidebar,{className:a,collapsible:s,children:o.jsx(m.SidebarContent,{children:o.jsxs(m.SidebarGroup,{children:[o.jsx(m.SidebarGroupLabel,{children:c}),o.jsx(m.SidebarGroupContent,{children:o.jsx(m.SidebarMenu,{children:r.map(n=>o.jsx(m.SidebarMenuItem,{children:o.jsx(m.SidebarMenuButton,{asChild:!0,isActive:l.pathname===n.href,children:o.jsxs(I.NavLink,{to:n.href,children:[n.icon&&o.jsx(n.icon,{}),o.jsx("span",{children:n.title})]})})},n.href))})})]})})})}function U(){y.ComponentRegistry.register("page-header",j,{namespace:"layout",label:"Page Header",category:"Layout",inputs:[{name:"title",type:"string"},{name:"description",type:"string"}]}),y.ComponentRegistry.register("page:header",j,{namespace:"layout"}),y.ComponentRegistry.register("page:card",D,{namespace:"layout",label:"Page Card",category:"Layout",isContainer:!0}),y.ComponentRegistry.register("app-shell",F,{namespace:"layout",label:"App Shell",category:"Layout"})}try{U()}catch{}d.AppShell=F,d.Page=ae,d.PageCard=D,d.PageHeader=j,d.SidebarNav=ne,d.registerLayout=U,d.useAppShellBranding=q,Object.defineProperty(d,Symbol.toStringTag,{value:"Module"})}));
6
+ <%s key={someKey} {...props} />`,u,d,h,d),Q[d+u]=!0)}if(d=null,i!==void 0&&(t(i),d=""+i),p(a)&&(t(a.key),d=""+a.key),"key"in a){i={};for(var O in a)O!=="key"&&(i[O]=a[O])}else i=a;return d&&g(i,typeof e=="function"?e.displayName||e.name||"Unknown":e),R(e,d,i,n(),T,N)}function W(e){J(e)?e._store&&(e._store.validated=1):typeof e=="object"&&e!==null&&e.$$typeof===k&&(e._payload.status==="fulfilled"?J(e._payload.value)&&e._payload.value._store&&(e._payload.value._store.validated=1):e._store&&(e._store.validated=1))}function J(e){return typeof e=="object"&&e!==null&&e.$$typeof===V}var S=w,V=Symbol.for("react.transitional.element"),ie=Symbol.for("react.portal"),P=Symbol.for("react.fragment"),ue=Symbol.for("react.strict_mode"),de=Symbol.for("react.profiler"),fe=Symbol.for("react.consumer"),me=Symbol.for("react.context"),pe=Symbol.for("react.forward_ref"),ge=Symbol.for("react.suspense"),be=Symbol.for("react.suspense_list"),ye=Symbol.for("react.memo"),k=Symbol.for("react.lazy"),xe=Symbol.for("react.activity"),he=Symbol.for("react.client.reference"),C=S.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,z=Object.prototype.hasOwnProperty,ve=Array.isArray,A=console.createTask?console.createTask:function(){return null};S={react_stack_bottom_frame:function(e){return e()}};var H,B={},X=S.react_stack_bottom_frame.bind(S,o)(),Z=A(s(o)),Q={};E.Fragment=P,E.jsx=function(e,a,i){var u=1e4>C.recentlyCreatedOwnerStacks++;return y(e,a,i,!1,u?Error("react-stack-top-frame"):X,u?A(s(e)):Z)},E.jsxs=function(e,a,i){var u=1e4>C.recentlyCreatedOwnerStacks++;return y(e,a,i,!0,u?Error("react-stack-top-frame"):X,u?A(s(e)):Z)}})()),E}var Y;function te(){return Y||(Y=1,process.env.NODE_ENV==="production"?j.exports=ee():j.exports=re()),j.exports}var c=te();function _({title:r,description:l,action:t,className:s,children:n,...o}){return c.jsx("div",{className:m.cn("flex flex-col gap-4 pb-4 md:pb-8",s),...o,children:c.jsxs("div",{className:"flex items-center justify-between gap-4",children:[c.jsxs("div",{className:"flex flex-col gap-1",children:[c.jsx("h1",{className:"text-2xl font-bold tracking-tight md:text-3xl",children:r}),l&&c.jsx("p",{className:"text-sm text-muted-foreground",children:l})]}),(t||n)&&c.jsxs("div",{className:"flex items-center gap-2",children:[t,n]})]})})}function $(r){const l=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(r);if(!l)return null;const t=parseInt(l[1],16)/255,s=parseInt(l[2],16)/255,n=parseInt(l[3],16)/255,o=Math.max(t,s,n),p=Math.min(t,s,n);let g=0,x=0;const R=(o+p)/2;if(o!==p){const y=o-p;switch(x=R>.5?y/(2-o-p):y/(o+p),o){case t:g=((s-n)/y+(s<n?6:0))/6;break;case s:g=((n-t)/y+2)/6;break;case n:g=((t-s)/y+4)/6;break}}return`${Math.round(g*360)} ${Math.round(x*100)}% ${Math.round(R*100)}%`}function q(r,l){w.useEffect(()=>{const t=document.documentElement;if(r?.primaryColor){const s=$(r.primaryColor);s&&(t.style.setProperty("--brand-primary",r.primaryColor),t.style.setProperty("--brand-primary-hsl",s))}else t.style.removeProperty("--brand-primary"),t.style.removeProperty("--brand-primary-hsl");if(r?.accentColor){const s=$(r.accentColor);s&&(t.style.setProperty("--brand-accent",r.accentColor),t.style.setProperty("--brand-accent-hsl",s))}else t.style.removeProperty("--brand-accent"),t.style.removeProperty("--brand-accent-hsl");if(r?.favicon){const s=document.querySelector("#favicon")||document.querySelector('link[rel="icon"]');s&&(s.href=r.favicon)}return l&&(document.title=l),()=>{t.style.removeProperty("--brand-primary"),t.style.removeProperty("--brand-primary-hsl"),t.style.removeProperty("--brand-accent"),t.style.removeProperty("--brand-accent-hsl")}},[r?.primaryColor,r?.accentColor,r?.favicon,l])}function F({sidebar:r,navbar:l,children:t,className:s,defaultOpen:n=!0,branding:o}){return q(o,o?.title),c.jsxs(m.SidebarProvider,{defaultOpen:n,children:[r,c.jsxs(m.SidebarInset,{children:[c.jsxs("header",{className:"flex h-14 sm:h-16 shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4",children:[c.jsx(m.SidebarTrigger,{className:"-ml-1"}),c.jsx("div",{className:"w-px h-4 bg-border mx-1 sm:mx-2"}),l]}),c.jsx("main",{className:m.cn("flex-1 overflow-auto p-3 sm:p-4 md:p-6",s),children:t})]})]})}function D({className:r,children:l,...t}){return c.jsx("div",{className:m.cn("rounded-xl border bg-card text-card-foreground shadow",r),...t,children:c.jsx("div",{className:"p-6",children:l})})}const ae={xs:{1:"grid-cols-1",2:"grid-cols-2",3:"grid-cols-3",4:"grid-cols-4",6:"grid-cols-6",12:"grid-cols-12"},sm:{1:"sm:grid-cols-1",2:"sm:grid-cols-2",3:"sm:grid-cols-3",4:"sm:grid-cols-4",6:"sm:grid-cols-6",12:"sm:grid-cols-12"},md:{1:"md:grid-cols-1",2:"md:grid-cols-2",3:"md:grid-cols-3",4:"md:grid-cols-4",6:"md:grid-cols-6",12:"md:grid-cols-12"},lg:{1:"lg:grid-cols-1",2:"lg:grid-cols-2",3:"lg:grid-cols-3",4:"lg:grid-cols-4",6:"lg:grid-cols-6",12:"lg:grid-cols-12"},xl:{1:"xl:grid-cols-1",2:"xl:grid-cols-2",3:"xl:grid-cols-3",4:"xl:grid-cols-4",6:"xl:grid-cols-6",12:"xl:grid-cols-12"},"2xl":{1:"2xl:grid-cols-1",2:"2xl:grid-cols-2",3:"2xl:grid-cols-3",4:"2xl:grid-cols-4",6:"2xl:grid-cols-6",12:"2xl:grid-cols-12"}};function se(r){if(!r)return"grid-cols-1";const l=[];for(const[t,s]of Object.entries(r)){const n=ae[t];if(n&&s){const p=Object.keys(n).map(Number).reduce((g,x)=>Math.abs(x-s)<Math.abs(g-s)?x:g);l.push(n[p])}}return l.join(" ")||"grid-cols-1"}const oe={0:"gap-0",1:"gap-1",2:"gap-2",3:"gap-3",4:"gap-4",5:"gap-5",6:"gap-6",8:"gap-8"},G=({columns:r,gap:l=4,className:t,children:s})=>{const n=se(r),o=typeof l=="number"?oe[l]||`gap-${l}`:"";return c.jsx("div",{className:m.cn("grid",n,o,t),children:s})},ne=r=>r?Array.isArray(r)?r:[r]:[];function le({schema:r,className:l,style:t,id:s,...n}){const o=ne(r.children);return c.jsxs("div",{id:s||r.id,className:m.cn("flex flex-col h-full space-y-4",l),style:t,children:[c.jsx(_,{title:r.title,description:r.description}),c.jsx("div",{className:"flex-1 overflow-auto",children:o.map((p,g)=>c.jsx(K.SchemaRenderer,{schema:p,...n},p?.id||g))})]})}function ce({items:r,title:l="Application",className:t,collapsible:s="icon"}){const n=L.useLocation();return c.jsx(m.Sidebar,{className:t,collapsible:s,children:c.jsx(m.SidebarContent,{children:c.jsxs(m.SidebarGroup,{children:[c.jsx(m.SidebarGroupLabel,{children:l}),c.jsx(m.SidebarGroupContent,{children:c.jsx(m.SidebarMenu,{children:r.map(o=>c.jsx(m.SidebarMenuItem,{children:c.jsx(m.SidebarMenuButton,{asChild:!0,isActive:n.pathname===o.href,children:c.jsxs(L.NavLink,{to:o.href,children:[o.icon&&c.jsx(o.icon,{}),c.jsx("span",{children:o.title})]})})},o.href))})})]})})})}function U(){b.ComponentRegistry.register("page-header",_,{namespace:"layout",label:"Page Header",category:"Layout",inputs:[{name:"title",type:"string"},{name:"description",type:"string"}]}),b.ComponentRegistry.register("page:header",_,{namespace:"layout"}),b.ComponentRegistry.register("page:card",D,{namespace:"layout",label:"Page Card",category:"Layout",isContainer:!0}),b.ComponentRegistry.register("app-shell",F,{namespace:"layout",label:"App Shell",category:"Layout"}),b.ComponentRegistry.register("responsive-grid",G,{namespace:"layout",label:"Responsive Grid",category:"Layout",isContainer:!0,inputs:[{name:"columns",type:"object"},{name:"gap",type:"number"}]})}try{U()}catch{}f.AppShell=F,f.Page=le,f.PageCard=D,f.PageHeader=_,f.ResponsiveGrid=G,f.SidebarNav=ce,f.registerLayout=U,f.useAppShellBranding=q,Object.defineProperty(f,Symbol.toStringTag,{value:"Module"})}));
@@ -0,0 +1,51 @@
1
+ import { default as React } from 'react';
2
+ /**
3
+ * Spec-aligned breakpoint column map (mirrors @objectstack/spec BreakpointColumnMapSchema).
4
+ * Maps breakpoint names to grid column counts (1-12).
5
+ */
6
+ export interface BreakpointColumnMap {
7
+ xs?: number;
8
+ sm?: number;
9
+ md?: number;
10
+ lg?: number;
11
+ xl?: number;
12
+ '2xl'?: number;
13
+ }
14
+ /**
15
+ * Spec-aligned breakpoint order map (mirrors @objectstack/spec BreakpointOrderMapSchema).
16
+ * Maps breakpoint names to display order numbers.
17
+ */
18
+ export interface BreakpointOrderMap {
19
+ xs?: number;
20
+ sm?: number;
21
+ md?: number;
22
+ lg?: number;
23
+ xl?: number;
24
+ '2xl'?: number;
25
+ }
26
+ export interface ResponsiveGridProps {
27
+ /** Grid column map per breakpoint */
28
+ columns?: BreakpointColumnMap;
29
+ /** Gap between grid items */
30
+ gap?: number | string;
31
+ /** Additional class names */
32
+ className?: string;
33
+ /** Children */
34
+ children: React.ReactNode;
35
+ }
36
+ /**
37
+ * ResponsiveGrid — A layout component that consumes @objectstack/spec
38
+ * BreakpointColumnMapSchema for responsive grid layouts.
39
+ *
40
+ * Uses pure Tailwind CSS classes for responsive behavior (no JS resize listeners).
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * <ResponsiveGrid columns={{ xs: 1, sm: 2, lg: 3 }} gap={4}>
45
+ * <Card>Item 1</Card>
46
+ * <Card>Item 2</Card>
47
+ * <Card>Item 3</Card>
48
+ * </ResponsiveGrid>
49
+ * ```
50
+ */
51
+ export declare const ResponsiveGrid: React.FC<ResponsiveGridProps>;
@@ -7,4 +7,5 @@ export * from './AppShell';
7
7
  export * from './Page';
8
8
  export * from './PageCard';
9
9
  export * from './SidebarNav';
10
+ export * from './ResponsiveGrid';
10
11
  export declare function registerLayout(): void;
@@ -0,0 +1,24 @@
1
+ import { StoryObj } from '@storybook/react';
2
+ /**
3
+ * AppShell responsive layout stories.
4
+ * Demonstrates the shell + responsive grid working together.
5
+ *
6
+ * Part of Q1 2026 roadmap §1.3 — Responsive layout stories in Storybook.
7
+ */
8
+ declare const meta: {
9
+ title: string;
10
+ component: any;
11
+ parameters: {
12
+ layout: string;
13
+ docs: {
14
+ description: {
15
+ component: string;
16
+ };
17
+ };
18
+ };
19
+ tags: string[];
20
+ };
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+ export declare const ResponsiveDashboard: Story;
24
+ export declare const MinimalShell: Story;
@@ -0,0 +1,29 @@
1
+ import { StoryObj } from '@storybook/react';
2
+ /**
3
+ * ResponsiveGrid stories demonstrating spec-aligned responsive layouts.
4
+ * Uses BreakpointColumnMapSchema to configure columns per breakpoint.
5
+ *
6
+ * Part of Q1 2026 roadmap §1.3 — Responsive layout stories in Storybook.
7
+ */
8
+ declare const meta: {
9
+ title: string;
10
+ component: any;
11
+ parameters: {
12
+ layout: string;
13
+ docs: {
14
+ description: {
15
+ component: string;
16
+ };
17
+ };
18
+ };
19
+ tags: string[];
20
+ };
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+ export declare const Default: Story;
24
+ export declare const SingleColumn: Story;
25
+ export declare const TwoColumns: Story;
26
+ export declare const FourColumnGrid: Story;
27
+ export declare const DashboardLayout: Story;
28
+ export declare const CompactGap: Story;
29
+ export declare const WideGap: Story;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/layout",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "main": "dist/index.umd.cjs",
@@ -14,15 +14,15 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "clsx": "^2.1.0",
17
+ "clsx": "^2.1.1",
18
18
  "lucide-react": "^0.563.0",
19
- "react": "^19.2.4",
20
- "react-dom": "^19.2.4",
21
- "tailwind-merge": "^2.2.1",
22
- "@object-ui/components": "2.0.0",
23
- "@object-ui/core": "2.0.0",
24
- "@object-ui/react": "2.0.0",
25
- "@object-ui/types": "2.0.0"
19
+ "react": "19.2.4",
20
+ "react-dom": "19.2.4",
21
+ "tailwind-merge": "^2.6.1",
22
+ "@object-ui/components": "3.0.0",
23
+ "@object-ui/core": "3.0.0",
24
+ "@object-ui/react": "3.0.0",
25
+ "@object-ui/types": "3.0.0"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "react": "^18.0.0 || ^19.0.0",
@@ -30,8 +30,8 @@
30
30
  "react-router-dom": "^6.0.0 || ^7.0.0"
31
31
  },
32
32
  "devDependencies": {
33
+ "@vitejs/plugin-react": "^5.1.4",
33
34
  "react-router-dom": "^7.13.0",
34
- "@vitejs/plugin-react": "^5.1.3",
35
35
  "vite": "^7.3.1",
36
36
  "vite-plugin-dts": "^4.5.4"
37
37
  },
@@ -0,0 +1,118 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import React from 'react';
10
+ import { cn } from '@object-ui/components';
11
+
12
+ /**
13
+ * Spec-aligned breakpoint column map (mirrors @objectstack/spec BreakpointColumnMapSchema).
14
+ * Maps breakpoint names to grid column counts (1-12).
15
+ */
16
+ export interface BreakpointColumnMap {
17
+ xs?: number;
18
+ sm?: number;
19
+ md?: number;
20
+ lg?: number;
21
+ xl?: number;
22
+ '2xl'?: number;
23
+ }
24
+
25
+ /**
26
+ * Spec-aligned breakpoint order map (mirrors @objectstack/spec BreakpointOrderMapSchema).
27
+ * Maps breakpoint names to display order numbers.
28
+ */
29
+ export interface BreakpointOrderMap {
30
+ xs?: number;
31
+ sm?: number;
32
+ md?: number;
33
+ lg?: number;
34
+ xl?: number;
35
+ '2xl'?: number;
36
+ }
37
+
38
+ export interface ResponsiveGridProps {
39
+ /** Grid column map per breakpoint */
40
+ columns?: BreakpointColumnMap;
41
+ /** Gap between grid items */
42
+ gap?: number | string;
43
+ /** Additional class names */
44
+ className?: string;
45
+ /** Children */
46
+ children: React.ReactNode;
47
+ }
48
+
49
+ /**
50
+ * Tailwind class mapping for grid columns at each breakpoint.
51
+ * Uses standard Tailwind grid-cols utilities for CSS-only responsiveness.
52
+ */
53
+ const COLS_CLASSES: Record<string, Record<number, string>> = {
54
+ xs: { 1: 'grid-cols-1', 2: 'grid-cols-2', 3: 'grid-cols-3', 4: 'grid-cols-4', 6: 'grid-cols-6', 12: 'grid-cols-12' },
55
+ sm: { 1: 'sm:grid-cols-1', 2: 'sm:grid-cols-2', 3: 'sm:grid-cols-3', 4: 'sm:grid-cols-4', 6: 'sm:grid-cols-6', 12: 'sm:grid-cols-12' },
56
+ md: { 1: 'md:grid-cols-1', 2: 'md:grid-cols-2', 3: 'md:grid-cols-3', 4: 'md:grid-cols-4', 6: 'md:grid-cols-6', 12: 'md:grid-cols-12' },
57
+ lg: { 1: 'lg:grid-cols-1', 2: 'lg:grid-cols-2', 3: 'lg:grid-cols-3', 4: 'lg:grid-cols-4', 6: 'lg:grid-cols-6', 12: 'lg:grid-cols-12' },
58
+ xl: { 1: 'xl:grid-cols-1', 2: 'xl:grid-cols-2', 3: 'xl:grid-cols-3', 4: 'xl:grid-cols-4', 6: 'xl:grid-cols-6', 12: 'xl:grid-cols-12' },
59
+ '2xl': { 1: '2xl:grid-cols-1', 2: '2xl:grid-cols-2', 3: '2xl:grid-cols-3', 4: '2xl:grid-cols-4', 6: '2xl:grid-cols-6', 12: '2xl:grid-cols-12' },
60
+ };
61
+
62
+ /**
63
+ * Resolve a BreakpointColumnMap into Tailwind CSS grid classes.
64
+ */
65
+ function resolveColumnClasses(columns?: BreakpointColumnMap): string {
66
+ if (!columns) return 'grid-cols-1';
67
+
68
+ const classes: string[] = [];
69
+ for (const [bp, cols] of Object.entries(columns)) {
70
+ const bpClasses = COLS_CLASSES[bp];
71
+ if (bpClasses && cols) {
72
+ // Use closest supported column count
73
+ const supported = Object.keys(bpClasses).map(Number);
74
+ const closest = supported.reduce((prev, curr) =>
75
+ Math.abs(curr - cols) < Math.abs(prev - cols) ? curr : prev
76
+ );
77
+ classes.push(bpClasses[closest]);
78
+ }
79
+ }
80
+
81
+ return classes.join(' ') || 'grid-cols-1';
82
+ }
83
+
84
+ const GAP_CLASSES: Record<number, string> = {
85
+ 0: 'gap-0', 1: 'gap-1', 2: 'gap-2', 3: 'gap-3', 4: 'gap-4',
86
+ 5: 'gap-5', 6: 'gap-6', 8: 'gap-8',
87
+ };
88
+
89
+ /**
90
+ * ResponsiveGrid — A layout component that consumes @objectstack/spec
91
+ * BreakpointColumnMapSchema for responsive grid layouts.
92
+ *
93
+ * Uses pure Tailwind CSS classes for responsive behavior (no JS resize listeners).
94
+ *
95
+ * @example
96
+ * ```tsx
97
+ * <ResponsiveGrid columns={{ xs: 1, sm: 2, lg: 3 }} gap={4}>
98
+ * <Card>Item 1</Card>
99
+ * <Card>Item 2</Card>
100
+ * <Card>Item 3</Card>
101
+ * </ResponsiveGrid>
102
+ * ```
103
+ */
104
+ export const ResponsiveGrid: React.FC<ResponsiveGridProps> = ({
105
+ columns,
106
+ gap = 4,
107
+ className,
108
+ children,
109
+ }) => {
110
+ const colClasses = resolveColumnClasses(columns);
111
+ const gapClass = typeof gap === 'number' ? (GAP_CLASSES[gap] || `gap-${gap}`) : '';
112
+
113
+ return (
114
+ <div className={cn('grid', colClasses, gapClass, className)}>
115
+ {children}
116
+ </div>
117
+ );
118
+ };
package/src/index.ts CHANGED
@@ -9,12 +9,14 @@ import { AppShell } from './AppShell';
9
9
  import { Page } from './Page';
10
10
  import { PageCard } from './PageCard';
11
11
  import { SidebarNav } from './SidebarNav';
12
+ import { ResponsiveGrid } from './ResponsiveGrid';
12
13
 
13
14
  export * from './PageHeader';
14
15
  export * from './AppShell';
15
16
  export * from './Page';
16
17
  export * from './PageCard';
17
18
  export * from './SidebarNav';
19
+ export * from './ResponsiveGrid';
18
20
 
19
21
  export function registerLayout() {
20
22
  ComponentRegistry.register('page-header', PageHeader, {
@@ -44,6 +46,17 @@ export function registerLayout() {
44
46
  category: 'Layout',
45
47
  });
46
48
 
49
+ ComponentRegistry.register('responsive-grid', ResponsiveGrid, {
50
+ namespace: 'layout',
51
+ label: 'Responsive Grid',
52
+ category: 'Layout',
53
+ isContainer: true,
54
+ inputs: [
55
+ { name: 'columns', type: 'object' },
56
+ { name: 'gap', type: 'number' },
57
+ ],
58
+ });
59
+
47
60
  // NOTE: 'page' registration is handled by @object-ui/components PageRenderer.
48
61
  // That renderer supports page types (record/home/app/utility), named regions,
49
62
  // and PageVariablesProvider. Do NOT re-register 'page' here to avoid conflicts.
@@ -0,0 +1,110 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import React from 'react';
3
+ import { AppShell, PageHeader, ResponsiveGrid } from '@object-ui/layout';
4
+ import { Card, CardContent, CardHeader, CardTitle, Button } from '@object-ui/components';
5
+
6
+ /**
7
+ * AppShell responsive layout stories.
8
+ * Demonstrates the shell + responsive grid working together.
9
+ *
10
+ * Part of Q1 2026 roadmap §1.3 — Responsive layout stories in Storybook.
11
+ */
12
+ const meta = {
13
+ title: 'Layout/AppShell',
14
+ component: AppShell,
15
+ parameters: {
16
+ layout: 'fullscreen',
17
+ docs: {
18
+ description: {
19
+ component:
20
+ 'The application shell provides a sidebar + main content area. ' +
21
+ 'Combine with ResponsiveGrid for fully responsive page layouts.',
22
+ },
23
+ },
24
+ },
25
+ tags: ['autodocs'],
26
+ } satisfies Meta<typeof AppShell>;
27
+
28
+ export default meta;
29
+ type Story = StoryObj<typeof meta>;
30
+
31
+ function SampleSidebar() {
32
+ return (
33
+ <nav className="p-4 space-y-2" aria-label="Main navigation">
34
+ {['Dashboard', 'Contacts', 'Tasks', 'Reports', 'Settings'].map((item) => (
35
+ <div
36
+ key={item}
37
+ className="px-3 py-2 rounded-md text-sm hover:bg-accent cursor-pointer"
38
+ >
39
+ {item}
40
+ </div>
41
+ ))}
42
+ </nav>
43
+ );
44
+ }
45
+
46
+ function MetricCard({ title, value }: { title: string; value: string }) {
47
+ return (
48
+ <Card>
49
+ <CardHeader className="pb-2">
50
+ <CardTitle className="text-sm font-medium text-muted-foreground">{title}</CardTitle>
51
+ </CardHeader>
52
+ <CardContent>
53
+ <div className="text-2xl font-bold">{value}</div>
54
+ </CardContent>
55
+ </Card>
56
+ );
57
+ }
58
+
59
+ export const ResponsiveDashboard: Story = {
60
+ name: 'Responsive Dashboard',
61
+ args: {
62
+ sidebar: <SampleSidebar />,
63
+ children: (
64
+ <div className="p-6 space-y-6">
65
+ <PageHeader
66
+ title="Dashboard"
67
+ description="Key metrics overview"
68
+ action={<Button size="sm">Export</Button>}
69
+ />
70
+ <ResponsiveGrid columns={{ xs: 1, sm: 2, xl: 4 }} gap={4}>
71
+ <MetricCard title="Revenue" value="$45,231" />
72
+ <MetricCard title="Users" value="2,350" />
73
+ <MetricCard title="Orders" value="1,247" />
74
+ <MetricCard title="Growth" value="+12.5%" />
75
+ </ResponsiveGrid>
76
+ <ResponsiveGrid columns={{ xs: 1, lg: 2 }} gap={4}>
77
+ <Card>
78
+ <CardHeader><CardTitle>Recent Activity</CardTitle></CardHeader>
79
+ <CardContent><div className="h-48 bg-muted rounded" /></CardContent>
80
+ </Card>
81
+ <Card>
82
+ <CardHeader><CardTitle>Analytics</CardTitle></CardHeader>
83
+ <CardContent><div className="h-48 bg-muted rounded" /></CardContent>
84
+ </Card>
85
+ </ResponsiveGrid>
86
+ </div>
87
+ ),
88
+ },
89
+ };
90
+
91
+ export const MinimalShell: Story = {
92
+ name: 'Minimal (No Sidebar)',
93
+ args: {
94
+ children: (
95
+ <div className="p-6">
96
+ <PageHeader title="Settings" description="Manage your preferences" />
97
+ <ResponsiveGrid columns={{ xs: 1, md: 2 }} gap={4}>
98
+ <Card>
99
+ <CardHeader><CardTitle>Profile</CardTitle></CardHeader>
100
+ <CardContent><div className="h-32 bg-muted rounded" /></CardContent>
101
+ </Card>
102
+ <Card>
103
+ <CardHeader><CardTitle>Security</CardTitle></CardHeader>
104
+ <CardContent><div className="h-32 bg-muted rounded" /></CardContent>
105
+ </Card>
106
+ </ResponsiveGrid>
107
+ </div>
108
+ ),
109
+ },
110
+ };
@@ -0,0 +1,110 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import React from 'react';
3
+ import { ResponsiveGrid } from '@object-ui/layout';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@object-ui/components';
5
+
6
+ /**
7
+ * ResponsiveGrid stories demonstrating spec-aligned responsive layouts.
8
+ * Uses BreakpointColumnMapSchema to configure columns per breakpoint.
9
+ *
10
+ * Part of Q1 2026 roadmap §1.3 — Responsive layout stories in Storybook.
11
+ */
12
+ const meta = {
13
+ title: 'Layout/ResponsiveGrid',
14
+ component: ResponsiveGrid,
15
+ parameters: {
16
+ layout: 'padded',
17
+ docs: {
18
+ description: {
19
+ component:
20
+ 'A responsive grid layout that consumes @objectstack/spec BreakpointColumnMapSchema. ' +
21
+ 'Columns adjust automatically at each breakpoint using pure Tailwind CSS.',
22
+ },
23
+ },
24
+ },
25
+ tags: ['autodocs'],
26
+ } satisfies Meta<typeof ResponsiveGrid>;
27
+
28
+ export default meta;
29
+ type Story = StoryObj<typeof meta>;
30
+
31
+ /** Placeholder card for demos */
32
+ function DemoCard({ label }: { label: string }) {
33
+ return (
34
+ <Card>
35
+ <CardHeader className="p-4">
36
+ <CardTitle className="text-sm">{label}</CardTitle>
37
+ </CardHeader>
38
+ <CardContent className="p-4 pt-0">
39
+ <div className="h-16 rounded bg-muted" />
40
+ </CardContent>
41
+ </Card>
42
+ );
43
+ }
44
+
45
+ const items = Array.from({ length: 6 }, (_, i) => (
46
+ <DemoCard key={i} label={`Card ${i + 1}`} />
47
+ ));
48
+
49
+ // --- Stories ---
50
+
51
+ export const Default: Story = {
52
+ args: {
53
+ columns: { xs: 1, sm: 2, lg: 3 },
54
+ gap: 4,
55
+ children: items,
56
+ },
57
+ };
58
+
59
+ export const SingleColumn: Story = {
60
+ args: {
61
+ columns: { xs: 1 },
62
+ gap: 4,
63
+ children: items.slice(0, 3),
64
+ },
65
+ };
66
+
67
+ export const TwoColumns: Story = {
68
+ args: {
69
+ columns: { xs: 1, sm: 2 },
70
+ gap: 4,
71
+ children: items.slice(0, 4),
72
+ },
73
+ };
74
+
75
+ export const FourColumnGrid: Story = {
76
+ args: {
77
+ columns: { xs: 1, sm: 2, md: 3, lg: 4 },
78
+ gap: 4,
79
+ children: items,
80
+ },
81
+ };
82
+
83
+ export const DashboardLayout: Story = {
84
+ name: 'Dashboard Layout (1 → 2 → 4)',
85
+ args: {
86
+ columns: { xs: 1, sm: 2, xl: 4 },
87
+ gap: 6,
88
+ children: Array.from({ length: 8 }, (_, i) => (
89
+ <DemoCard key={i} label={`Metric ${i + 1}`} />
90
+ )),
91
+ },
92
+ };
93
+
94
+ export const CompactGap: Story = {
95
+ name: 'Compact Gap (gap-2)',
96
+ args: {
97
+ columns: { xs: 2, md: 3 },
98
+ gap: 2,
99
+ children: items,
100
+ },
101
+ };
102
+
103
+ export const WideGap: Story = {
104
+ name: 'Wide Gap (gap-8)',
105
+ args: {
106
+ columns: { xs: 1, md: 2 },
107
+ gap: 8,
108
+ children: items.slice(0, 4),
109
+ },
110
+ };