@babajaga3/react-bricks 0.1.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/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # react-layout-bricks
2
+
3
+ A tiny (~1 kB gzipped) factory for building **composable, Tailwind-friendly React layout component systems**. Define your own named layout slots once, then snap them together like bricks anywhere in your codebase.
4
+
5
+ ```tsx
6
+ const MobileLayout = createLayout({
7
+ Main: { as: 'main', className: 'flex flex-col min-h-screen' },
8
+ Header: { as: 'header', className: 'sticky top-0 z-50 h-14 border-b' },
9
+ Content: { className: 'flex-1 overflow-y-auto px-4 py-6' },
10
+ Footer: { as: 'footer', className: 'h-16 border-t flex items-center px-4' },
11
+ });
12
+
13
+ function Page() {
14
+ return (
15
+ <MobileLayout.Main>
16
+ <MobileLayout.Header>My App</MobileLayout.Header>
17
+ <MobileLayout.Content>Hello world</MobileLayout.Content>
18
+ <MobileLayout.Footer>© 2025</MobileLayout.Footer>
19
+ </MobileLayout.Main>
20
+ );
21
+ }
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Features
27
+
28
+ - **Zero config** — works with plain CSS classes, Tailwind, or any utility framework
29
+ - **Tailwind-aware** — uses `tailwind-merge` to resolve class conflicts when installed
30
+ - **Fully typed** — TypeScript generics infer slot names from your config; invalid slot access is a compile error
31
+ - **Polymorphic** — every slot accepts an `as` prop to swap the rendered element
32
+ - **Composable** — extend layouts, merge layouts, override per-instance
33
+ - **Tree-shakeable** — dual ESM + CJS output, `"sideEffects": false`
34
+ - **React 17+** compatible, framework agnostic (Next.js, Vite, Remix…)
35
+
36
+ ---
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ npm install react-layout-bricks
42
+ # or
43
+ pnpm add react-layout-bricks
44
+ ```
45
+
46
+ ### Optional peer dependencies (recommended)
47
+
48
+ For Tailwind class-conflict resolution and conditional class support:
49
+
50
+ ```bash
51
+ npm install tailwind-merge clsx
52
+ ```
53
+
54
+ The package works without them — it falls back to plain space-joined class concatenation.
55
+
56
+ ---
57
+
58
+ ## API
59
+
60
+ ### `createLayout(config, name?)`
61
+
62
+ Creates a namespaced object of slot components from a config.
63
+
64
+ ```ts
65
+ function createLayout<T extends LayoutConfig>(
66
+ config: T,
67
+ name?: string, // shown in React DevTools as "name.SlotKey"
68
+ ): Layout<T>
69
+ ```
70
+
71
+ **Config shape:**
72
+
73
+ | Property | Type | Default | Description |
74
+ |---------------|-----------------------|----------|------------------------------------------------------|
75
+ | `as` | `React.ElementType` | `'div'` | The HTML element or component this slot renders as |
76
+ | `className` | `string` | `''` | Default classes applied to every instance |
77
+ | `displayName` | `string` | inferred | Label in React DevTools |
78
+
79
+ **Slot component props:**
80
+
81
+ Every generated slot accepts:
82
+
83
+ | Prop | Type | Description |
84
+ |-------------|---------------------|--------------------------------------------------------------------------|
85
+ | `as` | `React.ElementType` | Override the rendered element/component for this single instance |
86
+ | `className` | `string` | Extra classes merged on top of defaults (via `tailwind-merge` if present)|
87
+ | `children` | `React.ReactNode` | Slot content |
88
+ | `...rest` | element props | All other props forwarded to the underlying element |
89
+
90
+ ---
91
+
92
+ ### `extendLayout(base, extension, name?)`
93
+
94
+ Creates a new layout by extending an existing one. Slots in `extension` override matching slots in `base`; new keys are added.
95
+
96
+ ```tsx
97
+ const DesktopLayout = extendLayout(
98
+ MobileLayout,
99
+ {
100
+ // Override
101
+ Content: { className: 'flex-1 px-8 max-w-5xl mx-auto' },
102
+ // Add new
103
+ Sidebar: { as: 'aside', className: 'w-64 border-r hidden lg:block' },
104
+ },
105
+ 'DesktopLayout',
106
+ );
107
+
108
+ // DesktopLayout.Header ← from MobileLayout (unchanged)
109
+ // DesktopLayout.Content ← overridden
110
+ // DesktopLayout.Sidebar ← new
111
+ ```
112
+
113
+ ---
114
+
115
+ ### `mergeLayouts(a, b)`
116
+
117
+ Merges two already-built layout objects into one. Slots in `b` win when keys collide.
118
+
119
+ ```tsx
120
+ const CardLayout = createLayout({ Root: { … }, Body: { … } });
121
+ const AppLayout = mergeLayouts(MobileLayout, CardLayout);
122
+
123
+ // AppLayout.Main / .Header / .Content / .Footer / .Root / .Body
124
+ ```
125
+
126
+ ---
127
+
128
+ ### `cn(...classes)`
129
+
130
+ The internal class merger is exported in case you want to use it in your own components.
131
+
132
+ ```tsx
133
+ import { cn } from 'react-layout-bricks';
134
+
135
+ <div className={cn('px-4', isActive && 'bg-blue-500', props.className)} />
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Patterns
141
+
142
+ ### One layout file per breakpoint / product area
143
+
144
+ ```ts
145
+ // layouts/mobile.ts
146
+ export const MobileLayout = createLayout({ … });
147
+
148
+ // layouts/desktop.ts
149
+ export const DesktopLayout = extendLayout(MobileLayout, { … });
150
+
151
+ // layouts/dashboard.ts
152
+ export const DashboardLayout = createLayout({ … });
153
+ ```
154
+
155
+ ### Per-page className overrides
156
+
157
+ Default classes live in the layout definition. Instance overrides are merged at render time — Tailwind conflicts are resolved automatically.
158
+
159
+ ```tsx
160
+ // Default: px-4
161
+ <MobileLayout.Content className="px-8">…</MobileLayout.Content>
162
+ // Rendered: px-8 (tailwind-merge resolves the conflict)
163
+ ```
164
+
165
+ ### Polymorphic slot rendering
166
+
167
+ ```tsx
168
+ // Render Content as <article> for semantic HTML
169
+ <MobileLayout.Content as="article" className="prose">
170
+ <h2>…</h2>
171
+ </MobileLayout.Content>
172
+
173
+ // Render Content as a third-party motion component
174
+ import { motion } from 'motion/react';
175
+ <MobileLayout.Content as={motion.div} animate={{ opacity: 1 }}>
176
+
177
+ </MobileLayout.Content>
178
+ ```
179
+
180
+ ---
181
+
182
+ ## TypeScript
183
+
184
+ Slot names are inferred from your config — accessing a slot that doesn't exist is a compile-time error.
185
+
186
+ ```ts
187
+ const Layout = createLayout({ Main: { … }, Header: { … } });
188
+
189
+ <Layout.Main /> // ✅
190
+ <Layout.Footer /> // ❌ Property 'Footer' does not exist on type 'Layout<…>'
191
+ ```
192
+
193
+ You can also export the layout type for use in other files:
194
+
195
+ ```ts
196
+ import type { Layout } from 'react-layout-bricks';
197
+ import type { myLayoutConfig } from './layouts/mobile';
198
+
199
+ type MobileLayoutType = Layout<typeof myLayoutConfig>;
200
+ ```
201
+
202
+ ---
203
+
204
+ ## License
205
+
206
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+
5
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
+
7
+ var React__default = /*#__PURE__*/_interopDefault(React);
8
+
9
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
10
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
11
+ }) : x)(function(x) {
12
+ if (typeof require !== "undefined") return require.apply(this, arguments);
13
+ throw Error('Dynamic require of "' + x + '" is not supported');
14
+ });
15
+
16
+ // src/utils/cn.ts
17
+ var _twMerge = null;
18
+ var _clsx = null;
19
+ try {
20
+ _twMerge = __require("tailwind-merge").twMerge;
21
+ } catch {
22
+ }
23
+ try {
24
+ _clsx = __require("clsx").clsx;
25
+ } catch {
26
+ }
27
+ function cn(...inputs) {
28
+ const joined = _clsx ? _clsx(...inputs) : inputs.filter(Boolean).join(" ");
29
+ return _twMerge ? _twMerge(joined) : joined;
30
+ }
31
+
32
+ // src/create-layout.tsx
33
+ function createSlotComponent(defaultElement, defaultClassName, displayName) {
34
+ function Slot({
35
+ as,
36
+ className,
37
+ children,
38
+ ...rest
39
+ }) {
40
+ const Element = as ?? defaultElement;
41
+ return React__default.default.createElement(
42
+ Element,
43
+ { className: cn(defaultClassName, className), ...rest },
44
+ children
45
+ );
46
+ }
47
+ Slot.displayName = displayName;
48
+ return Slot;
49
+ }
50
+ function createLayout(config, name) {
51
+ const layout = {};
52
+ for (const key in config) {
53
+ if (!Object.prototype.hasOwnProperty.call(config, key)) continue;
54
+ const slotConfig = config[key];
55
+ layout[key] = createSlotComponent(
56
+ slotConfig?.as ?? "div",
57
+ slotConfig?.className ?? "",
58
+ slotConfig?.displayName ?? (name ? `${name}.${key}` : key)
59
+ );
60
+ }
61
+ return layout;
62
+ }
63
+
64
+ // src/extend-layout.ts
65
+ function extendLayout(base, extension, name) {
66
+ const extendedParts = createLayout(extension, name);
67
+ return { ...base, ...extendedParts };
68
+ }
69
+ function mergeLayouts(a, b) {
70
+ return { ...a, ...b };
71
+ }
72
+
73
+ exports.cn = cn;
74
+ exports.createLayout = createLayout;
75
+ exports.extendLayout = extendLayout;
76
+ exports.mergeLayouts = mergeLayouts;
77
+ //# sourceMappingURL=index.cjs.map
78
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/cn.ts","../src/create-layout.tsx","../src/extend-layout.ts"],"names":["React"],"mappings":";;;;;;;;;;;;;;;;AAiBA,IAAI,QAAA,GAAwD,IAAA;AAC5D,IAAI,KAAA,GAAsD,IAAA;AAE1D,IAAI;AAEF,EAAA,QAAA,GAAY,SAAA,CAAQ,gBAAgB,CAAA,CAA8C,OAAA;AACpF,CAAA,CAAA,MAAQ;AAER;AAEA,IAAI;AAEF,EAAA,KAAA,GAAS,SAAA,CAAQ,MAAM,CAAA,CAA+C,IAAA;AACxE,CAAA,CAAA,MAAQ;AAER;AAiBO,SAAS,MAAM,MAAA,EAA8B;AAClD,EAAA,MAAM,MAAA,GAAS,KAAA,GACX,KAAA,CAAM,GAAG,MAAM,CAAA,GACf,MAAA,CAAO,MAAA,CAAO,OAAO,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAEnC,EAAA,OAAO,QAAA,GAAW,QAAA,CAAS,MAAM,CAAA,GAAI,MAAA;AACvC;;;AC1CA,SAAS,mBAAA,CACP,cAAA,EACA,gBAAA,EACA,WAAA,EACe;AAQf,EAAA,SAAS,IAAA,CAA0C;AAAA,IACjD,EAAA;AAAA,IACA,SAAA;AAAA,IACA,QAAA;AAAA,IACA,GAAG;AAAA,GACL,EAA4C;AAC1C,IAAA,MAAM,UAAW,EAAA,IAAM,cAAA;AAEvB,IAAA,OAAOA,sBAAA,CAAM,aAAA;AAAA,MACX,OAAA;AAAA,MACA,EAAE,SAAA,EAAW,EAAA,CAAG,kBAAkB,SAAS,CAAA,EAAG,GAAG,IAAA,EAAK;AAAA,MACtD;AAAA,KACF;AAAA,EACF;AAEA,EAAA,IAAA,CAAK,WAAA,GAAc,WAAA;AAEnB,EAAA,OAAO,IAAA;AACT;AAqCO,SAAS,YAAA,CACd,QACA,IAAA,EACW;AACX,EAAA,MAAM,SAAS,EAAC;AAEhB,EAAA,KAAA,MAAW,OAAO,MAAA,EAAQ;AACxB,IAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,eAAe,IAAA,CAAK,MAAA,EAAQ,GAAG,CAAA,EAAG;AAExD,IAAA,MAAM,UAAA,GAAa,OAAO,GAAG,CAAA;AAE7B,IAAA,MAAA,CAAO,GAAG,CAAA,GAAI,mBAAA;AAAA,MACZ,YAAY,EAAA,IAAM,KAAA;AAAA,MAClB,YAAY,SAAA,IAAa,EAAA;AAAA,MACzB,YAAY,WAAA,KAAgB,IAAA,GAAO,GAAG,IAAI,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA,GAAK,GAAA;AAAA,KACxD;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;;;ACzDO,SAAS,YAAA,CAId,IAAA,EACA,SAAA,EACA,IAAA,EAC6B;AAE7B,EAAA,MAAM,aAAA,GAAgB,YAAA,CAAa,SAAA,EAAW,IAAI,CAAA;AAGlD,EAAA,OAAO,EAAE,GAAG,IAAA,EAAM,GAAG,aAAA,EAAc;AACrC;AAwBO,SAAS,YAAA,CAId,GACA,CAAA,EACO;AACP,EAAA,OAAO,EAAE,GAAG,CAAA,EAAG,GAAG,CAAA,EAAE;AACtB","file":"index.cjs","sourcesContent":["/**\n * Lightweight className merger.\n *\n * Tries to use `tailwind-merge` + `clsx` when available (peer deps).\n * Falls back to simple space-joined concatenation if they aren't installed.\n *\n * Because this runs at module initialisation time we wrap the dynamic\n * requires in try/catch so the package stays usable even without the\n * optional peers.\n */\n\ntype ClassValue = string | undefined | null | false;\n\n// --------------------------------------------------------------------------\n// Attempt to load peer deps at runtime\n// --------------------------------------------------------------------------\n\nlet _twMerge: (((...classes: string[]) => string) | null) = null;\nlet _clsx: ((...values: ClassValue[]) => string) | null = null;\n\ntry {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n _twMerge = (require('tailwind-merge') as { twMerge: (...c: string[]) => string }).twMerge;\n} catch {\n // tailwind-merge not installed — no Tailwind conflict resolution\n}\n\ntry {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n _clsx = (require('clsx') as { clsx: (...v: ClassValue[]) => string }).clsx;\n} catch {\n // clsx not installed — use simple join\n}\n\n// --------------------------------------------------------------------------\n// Exported merger\n// --------------------------------------------------------------------------\n\n/**\n * Merges class values in order.\n *\n * - When `clsx` is available: handles arrays, conditionals, objects, etc.\n * - When `tailwind-merge` is available: resolves Tailwind class conflicts\n * (e.g. `p-4` + `p-6` → `p-6`).\n * - Without either peer dep: falls back to a plain space-separated join.\n *\n * @example\n * cn('flex flex-col', isActive && 'bg-blue-500', props.className)\n */\nexport function cn(...inputs: ClassValue[]): string {\n const joined = _clsx\n ? _clsx(...inputs)\n : inputs.filter(Boolean).join(' ');\n\n return _twMerge ? _twMerge(joined) : joined;\n}\n","import React from 'react';\nimport type {\n Layout,\n LayoutConfig,\n SlotComponent,\n SlotProps,\n} from './types';\nimport { cn } from './utils/cn';\n\n// ---------------------------------------------------------------------------\n// Internal slot factory\n// ---------------------------------------------------------------------------\n\nfunction createSlotComponent(\n defaultElement: React.ElementType,\n defaultClassName: string,\n displayName: string,\n): SlotComponent {\n /**\n * Polymorphic slot component.\n *\n * - Renders `as` (if provided) or the default element from config.\n * - Merges the config's default className with the instance's className.\n * - Forwards all other props straight to the rendered element.\n */\n function Slot<C extends React.ElementType = 'div'>({\n as,\n className,\n children,\n ...rest\n }: SlotProps<C>): React.ReactElement | null {\n const Element = (as ?? defaultElement) as React.ElementType;\n\n return React.createElement(\n Element,\n { className: cn(defaultClassName, className), ...rest },\n children,\n );\n }\n\n Slot.displayName = displayName;\n\n return Slot as SlotComponent;\n}\n\n// ---------------------------------------------------------------------------\n// createLayout\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a namespaced set of layout slot components from a config object.\n *\n * Each key in `config` becomes a component on the returned object.\n * Components are fully polymorphic (support the `as` prop), merge classNames\n * with `tailwind-merge` when available, and forward all HTML/component props.\n *\n * @param config Slot definitions — keys are component names, values are slot options.\n * @param name Optional name used in React DevTools (`Name.SlotKey`).\n *\n * @example\n * ```tsx\n * const MobileLayout = createLayout({\n * Main: { as: 'main', className: 'flex flex-col min-h-screen bg-white' },\n * Header: { as: 'header', className: 'sticky top-0 z-50 h-16 border-b' },\n * Content: { className: 'flex-1 overflow-y-auto px-4 py-6' },\n * Footer: { as: 'footer', className: 'h-16 border-t flex items-center px-4' },\n * }, 'MobileLayout');\n *\n * // Usage:\n * function Page() {\n * return (\n * <MobileLayout.Main>\n * <MobileLayout.Header className=\"bg-brand\">...</MobileLayout.Header>\n * <MobileLayout.Content>...</MobileLayout.Content>\n * <MobileLayout.Footer>...</MobileLayout.Footer>\n * </MobileLayout.Main>\n * );\n * }\n * ```\n */\nexport function createLayout<T extends LayoutConfig>(\n config: T,\n name?: string,\n): Layout<T> {\n const layout = {} as Layout<T>;\n\n for (const key in config) {\n if (!Object.prototype.hasOwnProperty.call(config, key)) continue;\n\n const slotConfig = config[key];\n\n layout[key] = createSlotComponent(\n slotConfig?.as ?? 'div',\n slotConfig?.className ?? '',\n slotConfig?.displayName ?? (name ? `${name}.${key}` : key),\n );\n }\n\n return layout;\n}\n","import { createLayout } from './create-layout';\nimport type {\n LayoutConfig,\n Layout,\n ExtendedLayout,\n SlotComponent,\n} from './types';\n\n// ---------------------------------------------------------------------------\n// extendLayout\n// ---------------------------------------------------------------------------\n\n/**\n * Extends an existing layout with additional or overriding slot configs.\n *\n * - Slots present in `extension` **replace** slots in `base` entirely.\n * - Slots only in `base` are kept as-is.\n * - New slots in `extension` are added to the result.\n *\n * @example\n * ```tsx\n * const BaseLayout = createLayout({\n * Main: { as: 'main', className: 'flex flex-col min-h-screen' },\n * Content: { className: 'flex-1 px-4' },\n * });\n *\n * const TabletLayout = extendLayout(BaseLayout, {\n * // Override Content with wider padding\n * Content: { className: 'flex-1 px-8 max-w-3xl mx-auto' },\n * // Add a new slot that didn't exist before\n * Sidebar: { className: 'w-64 border-l hidden md:block' },\n * });\n *\n * // TabletLayout.Main ← from base\n * // TabletLayout.Content ← overridden\n * // TabletLayout.Sidebar ← new\n * ```\n *\n * @param base The original layout object (from `createLayout`).\n * @param extension Config for new or overriding slots.\n * @param name Optional DevTools label for the extended layout.\n */\nexport function extendLayout<\n TBase extends LayoutConfig,\n TExt extends LayoutConfig,\n>(\n base: Layout<TBase>,\n extension: TExt,\n name?: string,\n): ExtendedLayout<TBase, TExt> {\n // Build the new components from the extension config\n const extendedParts = createLayout(extension, name);\n\n // Merge: base slots first, then extension slots override\n return { ...base, ...extendedParts } as ExtendedLayout<TBase, TExt>;\n}\n\n// ---------------------------------------------------------------------------\n// mergeLayouts\n// ---------------------------------------------------------------------------\n\n/**\n * Merges two layout objects together into one.\n *\n * Unlike `extendLayout` this operates on **already-built layout objects**,\n * so it merges components directly. Slots in `b` win over slots in `a`\n * when keys collide.\n *\n * Useful for combining unrelated layout namespaces into a single object.\n *\n * @example\n * ```tsx\n * const BaseLayout = createLayout({ Main: { … }, Header: { … } });\n * const WidgetLayout = createLayout({ Card: { … }, Badge: { … } });\n *\n * const PageLayout = mergeLayouts(BaseLayout, WidgetLayout);\n * // PageLayout.Main / .Header / .Card / .Badge all available\n * ```\n */\nexport function mergeLayouts<\n A extends Record<string, SlotComponent>,\n B extends Record<string, SlotComponent>,\n>(\n a: A,\n b: B,\n): A & B {\n return { ...a, ...b };\n}\n"]}
@@ -0,0 +1,186 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * Extracts props from a given element type, excluding the ones we own.
5
+ */
6
+ type PropsOf<C extends React.ElementType> = React.ComponentPropsWithoutRef<C>;
7
+ type OwnProps = 'as' | 'className' | 'children';
8
+ /**
9
+ * Props for a single layout slot component.
10
+ * Supports the `as` prop for full polymorphism — any HTML element or React component.
11
+ */
12
+ type SlotProps<C extends React.ElementType = 'div'> = {
13
+ /** Override the rendered element/component for this slot */
14
+ as?: C;
15
+ /** Extra classes merged on top of the slot's default className */
16
+ className?: string;
17
+ children?: React.ReactNode;
18
+ } & Omit<PropsOf<C>, OwnProps>;
19
+ /**
20
+ * Configuration for a single slot inside a layout.
21
+ */
22
+ type SlotConfig = {
23
+ /**
24
+ * The HTML element or component this slot renders by default.
25
+ * @default 'div'
26
+ */
27
+ as?: React.ElementType;
28
+ /**
29
+ * Default Tailwind / CSS classes applied to every instance.
30
+ * User-supplied `className` is merged on top via tailwind-merge.
31
+ */
32
+ className?: string;
33
+ /**
34
+ * Label shown in React DevTools.
35
+ * Defaults to `LayoutName.SlotKey` when using `createLayout`.
36
+ */
37
+ displayName?: string;
38
+ };
39
+ /**
40
+ * The config object passed to `createLayout`.
41
+ * Keys become the component names on the returned layout object.
42
+ *
43
+ * @example
44
+ * const config = {
45
+ * Main: { as: 'main', className: 'flex flex-col min-h-screen' },
46
+ * Header: { as: 'header', className: 'sticky top-0 z-50 h-16' },
47
+ * Content: { className: 'flex-1 overflow-auto px-4' },
48
+ * } satisfies LayoutConfig;
49
+ */
50
+ type LayoutConfig = Record<string, SlotConfig>;
51
+ /**
52
+ * A single slot component produced by `createLayout`.
53
+ * Fully polymorphic — the `as` prop changes which element/component is rendered
54
+ * and also narrows the available HTML/component props accordingly.
55
+ */
56
+ interface SlotComponent {
57
+ <C extends React.ElementType = 'div'>(props: SlotProps<C>): React.ReactElement | null;
58
+ displayName?: string;
59
+ }
60
+ /**
61
+ * The object returned by `createLayout<T>`.
62
+ * Each key in the config becomes a `SlotComponent`.
63
+ */
64
+ type Layout<T extends LayoutConfig> = {
65
+ [K in keyof T]: SlotComponent;
66
+ };
67
+ /**
68
+ * Config passed to `extendLayout` — every key is optional so callers only
69
+ * need to list the slots they want to add or override.
70
+ */
71
+ type ExtendConfig<T extends LayoutConfig> = Partial<T> & LayoutConfig;
72
+ /**
73
+ * The merged layout type produced by `extendLayout`.
74
+ * Preserves both the base keys and the extension keys.
75
+ */
76
+ type ExtendedLayout<TBase extends LayoutConfig, TExt extends LayoutConfig> = Layout<Omit<TBase, keyof TExt> & TExt>;
77
+
78
+ /**
79
+ * Creates a namespaced set of layout slot components from a config object.
80
+ *
81
+ * Each key in `config` becomes a component on the returned object.
82
+ * Components are fully polymorphic (support the `as` prop), merge classNames
83
+ * with `tailwind-merge` when available, and forward all HTML/component props.
84
+ *
85
+ * @param config Slot definitions — keys are component names, values are slot options.
86
+ * @param name Optional name used in React DevTools (`Name.SlotKey`).
87
+ *
88
+ * @example
89
+ * ```tsx
90
+ * const MobileLayout = createLayout({
91
+ * Main: { as: 'main', className: 'flex flex-col min-h-screen bg-white' },
92
+ * Header: { as: 'header', className: 'sticky top-0 z-50 h-16 border-b' },
93
+ * Content: { className: 'flex-1 overflow-y-auto px-4 py-6' },
94
+ * Footer: { as: 'footer', className: 'h-16 border-t flex items-center px-4' },
95
+ * }, 'MobileLayout');
96
+ *
97
+ * // Usage:
98
+ * function Page() {
99
+ * return (
100
+ * <MobileLayout.Main>
101
+ * <MobileLayout.Header className="bg-brand">...</MobileLayout.Header>
102
+ * <MobileLayout.Content>...</MobileLayout.Content>
103
+ * <MobileLayout.Footer>...</MobileLayout.Footer>
104
+ * </MobileLayout.Main>
105
+ * );
106
+ * }
107
+ * ```
108
+ */
109
+ declare function createLayout<T extends LayoutConfig>(config: T, name?: string): Layout<T>;
110
+
111
+ /**
112
+ * Extends an existing layout with additional or overriding slot configs.
113
+ *
114
+ * - Slots present in `extension` **replace** slots in `base` entirely.
115
+ * - Slots only in `base` are kept as-is.
116
+ * - New slots in `extension` are added to the result.
117
+ *
118
+ * @example
119
+ * ```tsx
120
+ * const BaseLayout = createLayout({
121
+ * Main: { as: 'main', className: 'flex flex-col min-h-screen' },
122
+ * Content: { className: 'flex-1 px-4' },
123
+ * });
124
+ *
125
+ * const TabletLayout = extendLayout(BaseLayout, {
126
+ * // Override Content with wider padding
127
+ * Content: { className: 'flex-1 px-8 max-w-3xl mx-auto' },
128
+ * // Add a new slot that didn't exist before
129
+ * Sidebar: { className: 'w-64 border-l hidden md:block' },
130
+ * });
131
+ *
132
+ * // TabletLayout.Main ← from base
133
+ * // TabletLayout.Content ← overridden
134
+ * // TabletLayout.Sidebar ← new
135
+ * ```
136
+ *
137
+ * @param base The original layout object (from `createLayout`).
138
+ * @param extension Config for new or overriding slots.
139
+ * @param name Optional DevTools label for the extended layout.
140
+ */
141
+ declare function extendLayout<TBase extends LayoutConfig, TExt extends LayoutConfig>(base: Layout<TBase>, extension: TExt, name?: string): ExtendedLayout<TBase, TExt>;
142
+ /**
143
+ * Merges two layout objects together into one.
144
+ *
145
+ * Unlike `extendLayout` this operates on **already-built layout objects**,
146
+ * so it merges components directly. Slots in `b` win over slots in `a`
147
+ * when keys collide.
148
+ *
149
+ * Useful for combining unrelated layout namespaces into a single object.
150
+ *
151
+ * @example
152
+ * ```tsx
153
+ * const BaseLayout = createLayout({ Main: { … }, Header: { … } });
154
+ * const WidgetLayout = createLayout({ Card: { … }, Badge: { … } });
155
+ *
156
+ * const PageLayout = mergeLayouts(BaseLayout, WidgetLayout);
157
+ * // PageLayout.Main / .Header / .Card / .Badge all available
158
+ * ```
159
+ */
160
+ declare function mergeLayouts<A extends Record<string, SlotComponent>, B extends Record<string, SlotComponent>>(a: A, b: B): A & B;
161
+
162
+ /**
163
+ * Lightweight className merger.
164
+ *
165
+ * Tries to use `tailwind-merge` + `clsx` when available (peer deps).
166
+ * Falls back to simple space-joined concatenation if they aren't installed.
167
+ *
168
+ * Because this runs at module initialisation time we wrap the dynamic
169
+ * requires in try/catch so the package stays usable even without the
170
+ * optional peers.
171
+ */
172
+ type ClassValue = string | undefined | null | false;
173
+ /**
174
+ * Merges class values in order.
175
+ *
176
+ * - When `clsx` is available: handles arrays, conditionals, objects, etc.
177
+ * - When `tailwind-merge` is available: resolves Tailwind class conflicts
178
+ * (e.g. `p-4` + `p-6` → `p-6`).
179
+ * - Without either peer dep: falls back to a plain space-separated join.
180
+ *
181
+ * @example
182
+ * cn('flex flex-col', isActive && 'bg-blue-500', props.className)
183
+ */
184
+ declare function cn(...inputs: ClassValue[]): string;
185
+
186
+ export { type ExtendConfig, type ExtendedLayout, type Layout, type LayoutConfig, type SlotComponent, type SlotConfig, type SlotProps, cn, createLayout, extendLayout, mergeLayouts };
@@ -0,0 +1,186 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * Extracts props from a given element type, excluding the ones we own.
5
+ */
6
+ type PropsOf<C extends React.ElementType> = React.ComponentPropsWithoutRef<C>;
7
+ type OwnProps = 'as' | 'className' | 'children';
8
+ /**
9
+ * Props for a single layout slot component.
10
+ * Supports the `as` prop for full polymorphism — any HTML element or React component.
11
+ */
12
+ type SlotProps<C extends React.ElementType = 'div'> = {
13
+ /** Override the rendered element/component for this slot */
14
+ as?: C;
15
+ /** Extra classes merged on top of the slot's default className */
16
+ className?: string;
17
+ children?: React.ReactNode;
18
+ } & Omit<PropsOf<C>, OwnProps>;
19
+ /**
20
+ * Configuration for a single slot inside a layout.
21
+ */
22
+ type SlotConfig = {
23
+ /**
24
+ * The HTML element or component this slot renders by default.
25
+ * @default 'div'
26
+ */
27
+ as?: React.ElementType;
28
+ /**
29
+ * Default Tailwind / CSS classes applied to every instance.
30
+ * User-supplied `className` is merged on top via tailwind-merge.
31
+ */
32
+ className?: string;
33
+ /**
34
+ * Label shown in React DevTools.
35
+ * Defaults to `LayoutName.SlotKey` when using `createLayout`.
36
+ */
37
+ displayName?: string;
38
+ };
39
+ /**
40
+ * The config object passed to `createLayout`.
41
+ * Keys become the component names on the returned layout object.
42
+ *
43
+ * @example
44
+ * const config = {
45
+ * Main: { as: 'main', className: 'flex flex-col min-h-screen' },
46
+ * Header: { as: 'header', className: 'sticky top-0 z-50 h-16' },
47
+ * Content: { className: 'flex-1 overflow-auto px-4' },
48
+ * } satisfies LayoutConfig;
49
+ */
50
+ type LayoutConfig = Record<string, SlotConfig>;
51
+ /**
52
+ * A single slot component produced by `createLayout`.
53
+ * Fully polymorphic — the `as` prop changes which element/component is rendered
54
+ * and also narrows the available HTML/component props accordingly.
55
+ */
56
+ interface SlotComponent {
57
+ <C extends React.ElementType = 'div'>(props: SlotProps<C>): React.ReactElement | null;
58
+ displayName?: string;
59
+ }
60
+ /**
61
+ * The object returned by `createLayout<T>`.
62
+ * Each key in the config becomes a `SlotComponent`.
63
+ */
64
+ type Layout<T extends LayoutConfig> = {
65
+ [K in keyof T]: SlotComponent;
66
+ };
67
+ /**
68
+ * Config passed to `extendLayout` — every key is optional so callers only
69
+ * need to list the slots they want to add or override.
70
+ */
71
+ type ExtendConfig<T extends LayoutConfig> = Partial<T> & LayoutConfig;
72
+ /**
73
+ * The merged layout type produced by `extendLayout`.
74
+ * Preserves both the base keys and the extension keys.
75
+ */
76
+ type ExtendedLayout<TBase extends LayoutConfig, TExt extends LayoutConfig> = Layout<Omit<TBase, keyof TExt> & TExt>;
77
+
78
+ /**
79
+ * Creates a namespaced set of layout slot components from a config object.
80
+ *
81
+ * Each key in `config` becomes a component on the returned object.
82
+ * Components are fully polymorphic (support the `as` prop), merge classNames
83
+ * with `tailwind-merge` when available, and forward all HTML/component props.
84
+ *
85
+ * @param config Slot definitions — keys are component names, values are slot options.
86
+ * @param name Optional name used in React DevTools (`Name.SlotKey`).
87
+ *
88
+ * @example
89
+ * ```tsx
90
+ * const MobileLayout = createLayout({
91
+ * Main: { as: 'main', className: 'flex flex-col min-h-screen bg-white' },
92
+ * Header: { as: 'header', className: 'sticky top-0 z-50 h-16 border-b' },
93
+ * Content: { className: 'flex-1 overflow-y-auto px-4 py-6' },
94
+ * Footer: { as: 'footer', className: 'h-16 border-t flex items-center px-4' },
95
+ * }, 'MobileLayout');
96
+ *
97
+ * // Usage:
98
+ * function Page() {
99
+ * return (
100
+ * <MobileLayout.Main>
101
+ * <MobileLayout.Header className="bg-brand">...</MobileLayout.Header>
102
+ * <MobileLayout.Content>...</MobileLayout.Content>
103
+ * <MobileLayout.Footer>...</MobileLayout.Footer>
104
+ * </MobileLayout.Main>
105
+ * );
106
+ * }
107
+ * ```
108
+ */
109
+ declare function createLayout<T extends LayoutConfig>(config: T, name?: string): Layout<T>;
110
+
111
+ /**
112
+ * Extends an existing layout with additional or overriding slot configs.
113
+ *
114
+ * - Slots present in `extension` **replace** slots in `base` entirely.
115
+ * - Slots only in `base` are kept as-is.
116
+ * - New slots in `extension` are added to the result.
117
+ *
118
+ * @example
119
+ * ```tsx
120
+ * const BaseLayout = createLayout({
121
+ * Main: { as: 'main', className: 'flex flex-col min-h-screen' },
122
+ * Content: { className: 'flex-1 px-4' },
123
+ * });
124
+ *
125
+ * const TabletLayout = extendLayout(BaseLayout, {
126
+ * // Override Content with wider padding
127
+ * Content: { className: 'flex-1 px-8 max-w-3xl mx-auto' },
128
+ * // Add a new slot that didn't exist before
129
+ * Sidebar: { className: 'w-64 border-l hidden md:block' },
130
+ * });
131
+ *
132
+ * // TabletLayout.Main ← from base
133
+ * // TabletLayout.Content ← overridden
134
+ * // TabletLayout.Sidebar ← new
135
+ * ```
136
+ *
137
+ * @param base The original layout object (from `createLayout`).
138
+ * @param extension Config for new or overriding slots.
139
+ * @param name Optional DevTools label for the extended layout.
140
+ */
141
+ declare function extendLayout<TBase extends LayoutConfig, TExt extends LayoutConfig>(base: Layout<TBase>, extension: TExt, name?: string): ExtendedLayout<TBase, TExt>;
142
+ /**
143
+ * Merges two layout objects together into one.
144
+ *
145
+ * Unlike `extendLayout` this operates on **already-built layout objects**,
146
+ * so it merges components directly. Slots in `b` win over slots in `a`
147
+ * when keys collide.
148
+ *
149
+ * Useful for combining unrelated layout namespaces into a single object.
150
+ *
151
+ * @example
152
+ * ```tsx
153
+ * const BaseLayout = createLayout({ Main: { … }, Header: { … } });
154
+ * const WidgetLayout = createLayout({ Card: { … }, Badge: { … } });
155
+ *
156
+ * const PageLayout = mergeLayouts(BaseLayout, WidgetLayout);
157
+ * // PageLayout.Main / .Header / .Card / .Badge all available
158
+ * ```
159
+ */
160
+ declare function mergeLayouts<A extends Record<string, SlotComponent>, B extends Record<string, SlotComponent>>(a: A, b: B): A & B;
161
+
162
+ /**
163
+ * Lightweight className merger.
164
+ *
165
+ * Tries to use `tailwind-merge` + `clsx` when available (peer deps).
166
+ * Falls back to simple space-joined concatenation if they aren't installed.
167
+ *
168
+ * Because this runs at module initialisation time we wrap the dynamic
169
+ * requires in try/catch so the package stays usable even without the
170
+ * optional peers.
171
+ */
172
+ type ClassValue = string | undefined | null | false;
173
+ /**
174
+ * Merges class values in order.
175
+ *
176
+ * - When `clsx` is available: handles arrays, conditionals, objects, etc.
177
+ * - When `tailwind-merge` is available: resolves Tailwind class conflicts
178
+ * (e.g. `p-4` + `p-6` → `p-6`).
179
+ * - Without either peer dep: falls back to a plain space-separated join.
180
+ *
181
+ * @example
182
+ * cn('flex flex-col', isActive && 'bg-blue-500', props.className)
183
+ */
184
+ declare function cn(...inputs: ClassValue[]): string;
185
+
186
+ export { type ExtendConfig, type ExtendedLayout, type Layout, type LayoutConfig, type SlotComponent, type SlotConfig, type SlotProps, cn, createLayout, extendLayout, mergeLayouts };
package/dist/index.js ADDED
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+
3
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
4
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
5
+ }) : x)(function(x) {
6
+ if (typeof require !== "undefined") return require.apply(this, arguments);
7
+ throw Error('Dynamic require of "' + x + '" is not supported');
8
+ });
9
+
10
+ // src/utils/cn.ts
11
+ var _twMerge = null;
12
+ var _clsx = null;
13
+ try {
14
+ _twMerge = __require("tailwind-merge").twMerge;
15
+ } catch {
16
+ }
17
+ try {
18
+ _clsx = __require("clsx").clsx;
19
+ } catch {
20
+ }
21
+ function cn(...inputs) {
22
+ const joined = _clsx ? _clsx(...inputs) : inputs.filter(Boolean).join(" ");
23
+ return _twMerge ? _twMerge(joined) : joined;
24
+ }
25
+
26
+ // src/create-layout.tsx
27
+ function createSlotComponent(defaultElement, defaultClassName, displayName) {
28
+ function Slot({
29
+ as,
30
+ className,
31
+ children,
32
+ ...rest
33
+ }) {
34
+ const Element = as ?? defaultElement;
35
+ return React.createElement(
36
+ Element,
37
+ { className: cn(defaultClassName, className), ...rest },
38
+ children
39
+ );
40
+ }
41
+ Slot.displayName = displayName;
42
+ return Slot;
43
+ }
44
+ function createLayout(config, name) {
45
+ const layout = {};
46
+ for (const key in config) {
47
+ if (!Object.prototype.hasOwnProperty.call(config, key)) continue;
48
+ const slotConfig = config[key];
49
+ layout[key] = createSlotComponent(
50
+ slotConfig?.as ?? "div",
51
+ slotConfig?.className ?? "",
52
+ slotConfig?.displayName ?? (name ? `${name}.${key}` : key)
53
+ );
54
+ }
55
+ return layout;
56
+ }
57
+
58
+ // src/extend-layout.ts
59
+ function extendLayout(base, extension, name) {
60
+ const extendedParts = createLayout(extension, name);
61
+ return { ...base, ...extendedParts };
62
+ }
63
+ function mergeLayouts(a, b) {
64
+ return { ...a, ...b };
65
+ }
66
+
67
+ export { cn, createLayout, extendLayout, mergeLayouts };
68
+ //# sourceMappingURL=index.js.map
69
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/cn.ts","../src/create-layout.tsx","../src/extend-layout.ts"],"names":[],"mappings":";;;;;;;;;;AAiBA,IAAI,QAAA,GAAwD,IAAA;AAC5D,IAAI,KAAA,GAAsD,IAAA;AAE1D,IAAI;AAEF,EAAA,QAAA,GAAY,SAAA,CAAQ,gBAAgB,CAAA,CAA8C,OAAA;AACpF,CAAA,CAAA,MAAQ;AAER;AAEA,IAAI;AAEF,EAAA,KAAA,GAAS,SAAA,CAAQ,MAAM,CAAA,CAA+C,IAAA;AACxE,CAAA,CAAA,MAAQ;AAER;AAiBO,SAAS,MAAM,MAAA,EAA8B;AAClD,EAAA,MAAM,MAAA,GAAS,KAAA,GACX,KAAA,CAAM,GAAG,MAAM,CAAA,GACf,MAAA,CAAO,MAAA,CAAO,OAAO,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAEnC,EAAA,OAAO,QAAA,GAAW,QAAA,CAAS,MAAM,CAAA,GAAI,MAAA;AACvC;;;AC1CA,SAAS,mBAAA,CACP,cAAA,EACA,gBAAA,EACA,WAAA,EACe;AAQf,EAAA,SAAS,IAAA,CAA0C;AAAA,IACjD,EAAA;AAAA,IACA,SAAA;AAAA,IACA,QAAA;AAAA,IACA,GAAG;AAAA,GACL,EAA4C;AAC1C,IAAA,MAAM,UAAW,EAAA,IAAM,cAAA;AAEvB,IAAA,OAAO,KAAA,CAAM,aAAA;AAAA,MACX,OAAA;AAAA,MACA,EAAE,SAAA,EAAW,EAAA,CAAG,kBAAkB,SAAS,CAAA,EAAG,GAAG,IAAA,EAAK;AAAA,MACtD;AAAA,KACF;AAAA,EACF;AAEA,EAAA,IAAA,CAAK,WAAA,GAAc,WAAA;AAEnB,EAAA,OAAO,IAAA;AACT;AAqCO,SAAS,YAAA,CACd,QACA,IAAA,EACW;AACX,EAAA,MAAM,SAAS,EAAC;AAEhB,EAAA,KAAA,MAAW,OAAO,MAAA,EAAQ;AACxB,IAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,eAAe,IAAA,CAAK,MAAA,EAAQ,GAAG,CAAA,EAAG;AAExD,IAAA,MAAM,UAAA,GAAa,OAAO,GAAG,CAAA;AAE7B,IAAA,MAAA,CAAO,GAAG,CAAA,GAAI,mBAAA;AAAA,MACZ,YAAY,EAAA,IAAM,KAAA;AAAA,MAClB,YAAY,SAAA,IAAa,EAAA;AAAA,MACzB,YAAY,WAAA,KAAgB,IAAA,GAAO,GAAG,IAAI,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA,GAAK,GAAA;AAAA,KACxD;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;;;ACzDO,SAAS,YAAA,CAId,IAAA,EACA,SAAA,EACA,IAAA,EAC6B;AAE7B,EAAA,MAAM,aAAA,GAAgB,YAAA,CAAa,SAAA,EAAW,IAAI,CAAA;AAGlD,EAAA,OAAO,EAAE,GAAG,IAAA,EAAM,GAAG,aAAA,EAAc;AACrC;AAwBO,SAAS,YAAA,CAId,GACA,CAAA,EACO;AACP,EAAA,OAAO,EAAE,GAAG,CAAA,EAAG,GAAG,CAAA,EAAE;AACtB","file":"index.js","sourcesContent":["/**\n * Lightweight className merger.\n *\n * Tries to use `tailwind-merge` + `clsx` when available (peer deps).\n * Falls back to simple space-joined concatenation if they aren't installed.\n *\n * Because this runs at module initialisation time we wrap the dynamic\n * requires in try/catch so the package stays usable even without the\n * optional peers.\n */\n\ntype ClassValue = string | undefined | null | false;\n\n// --------------------------------------------------------------------------\n// Attempt to load peer deps at runtime\n// --------------------------------------------------------------------------\n\nlet _twMerge: (((...classes: string[]) => string) | null) = null;\nlet _clsx: ((...values: ClassValue[]) => string) | null = null;\n\ntry {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n _twMerge = (require('tailwind-merge') as { twMerge: (...c: string[]) => string }).twMerge;\n} catch {\n // tailwind-merge not installed — no Tailwind conflict resolution\n}\n\ntry {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n _clsx = (require('clsx') as { clsx: (...v: ClassValue[]) => string }).clsx;\n} catch {\n // clsx not installed — use simple join\n}\n\n// --------------------------------------------------------------------------\n// Exported merger\n// --------------------------------------------------------------------------\n\n/**\n * Merges class values in order.\n *\n * - When `clsx` is available: handles arrays, conditionals, objects, etc.\n * - When `tailwind-merge` is available: resolves Tailwind class conflicts\n * (e.g. `p-4` + `p-6` → `p-6`).\n * - Without either peer dep: falls back to a plain space-separated join.\n *\n * @example\n * cn('flex flex-col', isActive && 'bg-blue-500', props.className)\n */\nexport function cn(...inputs: ClassValue[]): string {\n const joined = _clsx\n ? _clsx(...inputs)\n : inputs.filter(Boolean).join(' ');\n\n return _twMerge ? _twMerge(joined) : joined;\n}\n","import React from 'react';\nimport type {\n Layout,\n LayoutConfig,\n SlotComponent,\n SlotProps,\n} from './types';\nimport { cn } from './utils/cn';\n\n// ---------------------------------------------------------------------------\n// Internal slot factory\n// ---------------------------------------------------------------------------\n\nfunction createSlotComponent(\n defaultElement: React.ElementType,\n defaultClassName: string,\n displayName: string,\n): SlotComponent {\n /**\n * Polymorphic slot component.\n *\n * - Renders `as` (if provided) or the default element from config.\n * - Merges the config's default className with the instance's className.\n * - Forwards all other props straight to the rendered element.\n */\n function Slot<C extends React.ElementType = 'div'>({\n as,\n className,\n children,\n ...rest\n }: SlotProps<C>): React.ReactElement | null {\n const Element = (as ?? defaultElement) as React.ElementType;\n\n return React.createElement(\n Element,\n { className: cn(defaultClassName, className), ...rest },\n children,\n );\n }\n\n Slot.displayName = displayName;\n\n return Slot as SlotComponent;\n}\n\n// ---------------------------------------------------------------------------\n// createLayout\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a namespaced set of layout slot components from a config object.\n *\n * Each key in `config` becomes a component on the returned object.\n * Components are fully polymorphic (support the `as` prop), merge classNames\n * with `tailwind-merge` when available, and forward all HTML/component props.\n *\n * @param config Slot definitions — keys are component names, values are slot options.\n * @param name Optional name used in React DevTools (`Name.SlotKey`).\n *\n * @example\n * ```tsx\n * const MobileLayout = createLayout({\n * Main: { as: 'main', className: 'flex flex-col min-h-screen bg-white' },\n * Header: { as: 'header', className: 'sticky top-0 z-50 h-16 border-b' },\n * Content: { className: 'flex-1 overflow-y-auto px-4 py-6' },\n * Footer: { as: 'footer', className: 'h-16 border-t flex items-center px-4' },\n * }, 'MobileLayout');\n *\n * // Usage:\n * function Page() {\n * return (\n * <MobileLayout.Main>\n * <MobileLayout.Header className=\"bg-brand\">...</MobileLayout.Header>\n * <MobileLayout.Content>...</MobileLayout.Content>\n * <MobileLayout.Footer>...</MobileLayout.Footer>\n * </MobileLayout.Main>\n * );\n * }\n * ```\n */\nexport function createLayout<T extends LayoutConfig>(\n config: T,\n name?: string,\n): Layout<T> {\n const layout = {} as Layout<T>;\n\n for (const key in config) {\n if (!Object.prototype.hasOwnProperty.call(config, key)) continue;\n\n const slotConfig = config[key];\n\n layout[key] = createSlotComponent(\n slotConfig?.as ?? 'div',\n slotConfig?.className ?? '',\n slotConfig?.displayName ?? (name ? `${name}.${key}` : key),\n );\n }\n\n return layout;\n}\n","import { createLayout } from './create-layout';\nimport type {\n LayoutConfig,\n Layout,\n ExtendedLayout,\n SlotComponent,\n} from './types';\n\n// ---------------------------------------------------------------------------\n// extendLayout\n// ---------------------------------------------------------------------------\n\n/**\n * Extends an existing layout with additional or overriding slot configs.\n *\n * - Slots present in `extension` **replace** slots in `base` entirely.\n * - Slots only in `base` are kept as-is.\n * - New slots in `extension` are added to the result.\n *\n * @example\n * ```tsx\n * const BaseLayout = createLayout({\n * Main: { as: 'main', className: 'flex flex-col min-h-screen' },\n * Content: { className: 'flex-1 px-4' },\n * });\n *\n * const TabletLayout = extendLayout(BaseLayout, {\n * // Override Content with wider padding\n * Content: { className: 'flex-1 px-8 max-w-3xl mx-auto' },\n * // Add a new slot that didn't exist before\n * Sidebar: { className: 'w-64 border-l hidden md:block' },\n * });\n *\n * // TabletLayout.Main ← from base\n * // TabletLayout.Content ← overridden\n * // TabletLayout.Sidebar ← new\n * ```\n *\n * @param base The original layout object (from `createLayout`).\n * @param extension Config for new or overriding slots.\n * @param name Optional DevTools label for the extended layout.\n */\nexport function extendLayout<\n TBase extends LayoutConfig,\n TExt extends LayoutConfig,\n>(\n base: Layout<TBase>,\n extension: TExt,\n name?: string,\n): ExtendedLayout<TBase, TExt> {\n // Build the new components from the extension config\n const extendedParts = createLayout(extension, name);\n\n // Merge: base slots first, then extension slots override\n return { ...base, ...extendedParts } as ExtendedLayout<TBase, TExt>;\n}\n\n// ---------------------------------------------------------------------------\n// mergeLayouts\n// ---------------------------------------------------------------------------\n\n/**\n * Merges two layout objects together into one.\n *\n * Unlike `extendLayout` this operates on **already-built layout objects**,\n * so it merges components directly. Slots in `b` win over slots in `a`\n * when keys collide.\n *\n * Useful for combining unrelated layout namespaces into a single object.\n *\n * @example\n * ```tsx\n * const BaseLayout = createLayout({ Main: { … }, Header: { … } });\n * const WidgetLayout = createLayout({ Card: { … }, Badge: { … } });\n *\n * const PageLayout = mergeLayouts(BaseLayout, WidgetLayout);\n * // PageLayout.Main / .Header / .Card / .Badge all available\n * ```\n */\nexport function mergeLayouts<\n A extends Record<string, SlotComponent>,\n B extends Record<string, SlotComponent>,\n>(\n a: A,\n b: B,\n): A & B {\n return { ...a, ...b };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@babajaga3/react-bricks",
3
+ "version": "0.1.0",
4
+ "description": "A tiny factory for building composable, Tailwind-friendly React layout component systems.",
5
+ "author": "",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "react",
9
+ "layout",
10
+ "tailwind",
11
+ "components",
12
+ "composable",
13
+ "typescript"
14
+ ],
15
+ "sideEffects": false,
16
+ "type": "module",
17
+ "main": "./dist/index.cjs",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "import": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "require": {
27
+ "types": "./dist/index.d.cts",
28
+ "default": "./dist/index.cjs"
29
+ }
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "dev": "tsup --watch",
38
+ "typecheck": "tsc --noEmit",
39
+ "lint": "eslint src --ext .ts,.tsx",
40
+ "prepublishOnly": "npm run build && npm run typecheck"
41
+ },
42
+ "peerDependencies": {
43
+ "react": ">=17.0.0",
44
+ "react-dom": ">=17.0.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "tailwind-merge": {
48
+ "optional": true
49
+ },
50
+ "clsx": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^25.5.2",
56
+ "@types/react": "^18.3.0",
57
+ "@types/react-dom": "^18.3.0",
58
+ "clsx": "^2.1.1",
59
+ "tailwind-merge": "^2.5.4",
60
+ "tsup": "^8.3.0",
61
+ "typescript": "^5.6.0"
62
+ },
63
+ "optionalDependencies": {
64
+ "clsx": "^2.1.1",
65
+ "tailwind-merge": "^2.5.4"
66
+ },
67
+ "packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be"
68
+ }