@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 +206 -0
- package/dist/index.cjs +78 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +186 -0
- package/dist/index.d.ts +186 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|