@object-ui/layout 0.5.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.
- package/.turbo/turbo-build.log +15 -6
- package/CHANGELOG.md +34 -0
- package/dist/index.js +305 -211
- package/dist/index.umd.cjs +2 -2
- package/dist/layout/src/AppShell.d.ts +24 -1
- package/dist/layout/src/ResponsiveGrid.d.ts +51 -0
- package/dist/layout/src/index.d.ts +1 -0
- package/dist/layout/src/stories/AppShell.stories.d.ts +24 -0
- package/dist/layout/src/stories/ResponsiveGrid.stories.d.ts +29 -0
- package/package.json +11 -10
- package/src/AppShell.tsx +113 -4
- package/src/ResponsiveGrid.tsx +118 -0
- package/src/index.ts +20 -7
- package/src/stories/AppShell.stories.tsx +110 -0
- package/src/stories/ResponsiveGrid.stories.tsx +110 -0
package/dist/index.umd.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
(function(
|
|
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} />`,
|
|
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"})}));
|
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Branding configuration for the AppShell.
|
|
4
|
+
* Applies CSS custom properties to the document root for theme customization.
|
|
5
|
+
*/
|
|
6
|
+
export interface AppShellBranding {
|
|
7
|
+
/** Primary brand color (hex, e.g. "#3B82F6") */
|
|
8
|
+
primaryColor?: string;
|
|
9
|
+
/** Accent brand color (hex, e.g. "#10B981") */
|
|
10
|
+
accentColor?: string;
|
|
11
|
+
/** Favicon URL — replaces the <link rel="icon"> href */
|
|
12
|
+
favicon?: string;
|
|
13
|
+
/** Logo URL — passed to sidebar/navbar via context */
|
|
14
|
+
logo?: string;
|
|
15
|
+
/** Page title suffix (sets document.title) */
|
|
16
|
+
title?: string;
|
|
17
|
+
}
|
|
2
18
|
export interface AppShellProps {
|
|
3
19
|
sidebar?: React.ReactNode;
|
|
4
20
|
navbar?: React.ReactNode;
|
|
5
21
|
children: React.ReactNode;
|
|
6
22
|
className?: string;
|
|
7
23
|
defaultOpen?: boolean;
|
|
24
|
+
/** App branding — applies CSS custom properties for theming */
|
|
25
|
+
branding?: AppShellBranding;
|
|
8
26
|
}
|
|
9
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Apply branding CSS custom properties to the document root.
|
|
29
|
+
* This is extracted as a standalone hook so it can be re-used independently.
|
|
30
|
+
*/
|
|
31
|
+
export declare function useAppShellBranding(branding?: AppShellBranding, title?: string): void;
|
|
32
|
+
export declare function AppShell({ sidebar, navbar, children, className, defaultOpen, branding, }: AppShellProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -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>;
|
|
@@ -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,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/layout",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
5
6
|
"main": "dist/index.umd.cjs",
|
|
6
7
|
"module": "dist/index.js",
|
|
7
8
|
"types": "dist/index.d.ts",
|
|
@@ -13,15 +14,15 @@
|
|
|
13
14
|
}
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
|
-
"clsx": "^2.1.
|
|
17
|
+
"clsx": "^2.1.1",
|
|
17
18
|
"lucide-react": "^0.563.0",
|
|
18
|
-
"react": "
|
|
19
|
-
"react-dom": "
|
|
20
|
-
"tailwind-merge": "^2.
|
|
21
|
-
"@object-ui/components": "0.
|
|
22
|
-
"@object-ui/core": "0.
|
|
23
|
-
"@object-ui/react": "0.
|
|
24
|
-
"@object-ui/types": "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"
|
|
25
26
|
},
|
|
26
27
|
"peerDependencies": {
|
|
27
28
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -29,8 +30,8 @@
|
|
|
29
30
|
"react-router-dom": "^6.0.0 || ^7.0.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
33
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
32
34
|
"react-router-dom": "^7.13.0",
|
|
33
|
-
"@vitejs/plugin-react": "^5.1.3",
|
|
34
35
|
"vite": "^7.3.1",
|
|
35
36
|
"vite-plugin-dts": "^4.5.4"
|
|
36
37
|
},
|
package/src/AppShell.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
SidebarProvider,
|
|
4
4
|
SidebarTrigger,
|
|
@@ -7,12 +7,117 @@ import {
|
|
|
7
7
|
} from '@object-ui/components';
|
|
8
8
|
import { cn } from '@object-ui/components';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Branding configuration for the AppShell.
|
|
12
|
+
* Applies CSS custom properties to the document root for theme customization.
|
|
13
|
+
*/
|
|
14
|
+
export interface AppShellBranding {
|
|
15
|
+
/** Primary brand color (hex, e.g. "#3B82F6") */
|
|
16
|
+
primaryColor?: string;
|
|
17
|
+
/** Accent brand color (hex, e.g. "#10B981") */
|
|
18
|
+
accentColor?: string;
|
|
19
|
+
/** Favicon URL — replaces the <link rel="icon"> href */
|
|
20
|
+
favicon?: string;
|
|
21
|
+
/** Logo URL — passed to sidebar/navbar via context */
|
|
22
|
+
logo?: string;
|
|
23
|
+
/** Page title suffix (sets document.title) */
|
|
24
|
+
title?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
export interface AppShellProps {
|
|
11
28
|
sidebar?: React.ReactNode;
|
|
12
29
|
navbar?: React.ReactNode; // Top navbar content
|
|
13
30
|
children: React.ReactNode;
|
|
14
31
|
className?: string;
|
|
15
32
|
defaultOpen?: boolean;
|
|
33
|
+
/** App branding — applies CSS custom properties for theming */
|
|
34
|
+
branding?: AppShellBranding;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Convert a hex color (#RRGGBB) to HSL string "H S% L%"
|
|
39
|
+
* for use in Tailwind CSS custom properties.
|
|
40
|
+
*/
|
|
41
|
+
function hexToHSL(hex: string): string | null {
|
|
42
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
43
|
+
if (!result) return null;
|
|
44
|
+
|
|
45
|
+
const r = parseInt(result[1], 16) / 255;
|
|
46
|
+
const g = parseInt(result[2], 16) / 255;
|
|
47
|
+
const b = parseInt(result[3], 16) / 255;
|
|
48
|
+
|
|
49
|
+
const max = Math.max(r, g, b);
|
|
50
|
+
const min = Math.min(r, g, b);
|
|
51
|
+
let h = 0;
|
|
52
|
+
let s = 0;
|
|
53
|
+
const l = (max + min) / 2;
|
|
54
|
+
|
|
55
|
+
if (max !== min) {
|
|
56
|
+
const d = max - min;
|
|
57
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
58
|
+
switch (max) {
|
|
59
|
+
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
|
60
|
+
case g: h = ((b - r) / d + 2) / 6; break;
|
|
61
|
+
case b: h = ((r - g) / d + 4) / 6; break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Apply branding CSS custom properties to the document root.
|
|
70
|
+
* This is extracted as a standalone hook so it can be re-used independently.
|
|
71
|
+
*/
|
|
72
|
+
export function useAppShellBranding(branding?: AppShellBranding, title?: string) {
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const root = document.documentElement;
|
|
75
|
+
|
|
76
|
+
// Primary color
|
|
77
|
+
if (branding?.primaryColor) {
|
|
78
|
+
const hsl = hexToHSL(branding.primaryColor);
|
|
79
|
+
if (hsl) {
|
|
80
|
+
root.style.setProperty('--brand-primary', branding.primaryColor);
|
|
81
|
+
root.style.setProperty('--brand-primary-hsl', hsl);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
root.style.removeProperty('--brand-primary');
|
|
85
|
+
root.style.removeProperty('--brand-primary-hsl');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Accent color
|
|
89
|
+
if (branding?.accentColor) {
|
|
90
|
+
const hsl = hexToHSL(branding.accentColor);
|
|
91
|
+
if (hsl) {
|
|
92
|
+
root.style.setProperty('--brand-accent', branding.accentColor);
|
|
93
|
+
root.style.setProperty('--brand-accent-hsl', hsl);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
root.style.removeProperty('--brand-accent');
|
|
97
|
+
root.style.removeProperty('--brand-accent-hsl');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Favicon
|
|
101
|
+
if (branding?.favicon) {
|
|
102
|
+
const link = document.querySelector<HTMLLinkElement>('#favicon')
|
|
103
|
+
|| document.querySelector<HTMLLinkElement>('link[rel="icon"]');
|
|
104
|
+
if (link) {
|
|
105
|
+
link.href = branding.favicon;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Page title
|
|
110
|
+
if (title) {
|
|
111
|
+
document.title = title;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return () => {
|
|
115
|
+
root.style.removeProperty('--brand-primary');
|
|
116
|
+
root.style.removeProperty('--brand-primary-hsl');
|
|
117
|
+
root.style.removeProperty('--brand-accent');
|
|
118
|
+
root.style.removeProperty('--brand-accent-hsl');
|
|
119
|
+
};
|
|
120
|
+
}, [branding?.primaryColor, branding?.accentColor, branding?.favicon, title]);
|
|
16
121
|
}
|
|
17
122
|
|
|
18
123
|
export function AppShell({
|
|
@@ -21,17 +126,21 @@ export function AppShell({
|
|
|
21
126
|
children,
|
|
22
127
|
className,
|
|
23
128
|
defaultOpen = true,
|
|
129
|
+
branding,
|
|
24
130
|
}: AppShellProps) {
|
|
131
|
+
// Apply branding CSS custom properties
|
|
132
|
+
useAppShellBranding(branding, branding?.title);
|
|
133
|
+
|
|
25
134
|
return (
|
|
26
135
|
<SidebarProvider defaultOpen={defaultOpen}>
|
|
27
136
|
{sidebar}
|
|
28
137
|
<SidebarInset>
|
|
29
|
-
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background px-4">
|
|
138
|
+
<header className="flex h-14 sm:h-16 shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4">
|
|
30
139
|
<SidebarTrigger className="-ml-1" />
|
|
31
|
-
<div className="w-px h-4 bg-border mx-2" />
|
|
140
|
+
<div className="w-px h-4 bg-border mx-1 sm:mx-2" />
|
|
32
141
|
{navbar}
|
|
33
142
|
</header>
|
|
34
|
-
<main className={cn("flex-1 overflow-auto p-4 md:p-6", className)}>
|
|
143
|
+
<main className={cn("flex-1 overflow-auto p-3 sm:p-4 md:p-6", className)}>
|
|
35
144
|
{children}
|
|
36
145
|
</main>
|
|
37
146
|
</SidebarInset>
|
|
@@ -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,15 +9,18 @@ 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, {
|
|
23
|
+
namespace: 'layout',
|
|
21
24
|
label: 'Page Header',
|
|
22
25
|
category: 'Layout',
|
|
23
26
|
inputs: [
|
|
@@ -27,26 +30,36 @@ export function registerLayout() {
|
|
|
27
30
|
});
|
|
28
31
|
|
|
29
32
|
// Alias for protocol compliance
|
|
30
|
-
ComponentRegistry.register('page:header', PageHeader);
|
|
33
|
+
ComponentRegistry.register('page:header', PageHeader, { namespace: 'layout' });
|
|
31
34
|
|
|
32
35
|
// Page Card
|
|
33
36
|
ComponentRegistry.register('page:card', PageCard, {
|
|
37
|
+
namespace: 'layout',
|
|
34
38
|
label: 'Page Card',
|
|
35
39
|
category: 'Layout',
|
|
36
40
|
isContainer: true
|
|
37
41
|
});
|
|
38
42
|
|
|
39
43
|
ComponentRegistry.register('app-shell', AppShell, {
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
namespace: 'layout',
|
|
45
|
+
label: 'App Shell',
|
|
46
|
+
category: 'Layout',
|
|
42
47
|
});
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
label: '
|
|
49
|
+
ComponentRegistry.register('responsive-grid', ResponsiveGrid, {
|
|
50
|
+
namespace: 'layout',
|
|
51
|
+
label: 'Responsive Grid',
|
|
47
52
|
category: 'Layout',
|
|
48
|
-
isContainer: true
|
|
53
|
+
isContainer: true,
|
|
54
|
+
inputs: [
|
|
55
|
+
{ name: 'columns', type: 'object' },
|
|
56
|
+
{ name: 'gap', type: 'number' },
|
|
57
|
+
],
|
|
49
58
|
});
|
|
59
|
+
|
|
60
|
+
// NOTE: 'page' registration is handled by @object-ui/components PageRenderer.
|
|
61
|
+
// That renderer supports page types (record/home/app/utility), named regions,
|
|
62
|
+
// and PageVariablesProvider. Do NOT re-register 'page' here to avoid conflicts.
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
// Keep backward compatibility for now if called directly
|