@babajaga3/react-bricks 0.1.0 → 0.1.1
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 +11 -5
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +21 -3
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# react-
|
|
1
|
+
# react-bricks
|
|
2
2
|
|
|
3
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
4
|
|
|
@@ -38,9 +38,9 @@ function Page() {
|
|
|
38
38
|
## Installation
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
npm install react-
|
|
41
|
+
npm install @babajaga3/react-bricks
|
|
42
42
|
# or
|
|
43
|
-
pnpm add react-
|
|
43
|
+
pnpm add @babajaga3/react-bricks
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
### Optional peer dependencies (recommended)
|
|
@@ -130,7 +130,7 @@ const AppLayout = mergeLayouts(MobileLayout, CardLayout);
|
|
|
130
130
|
The internal class merger is exported in case you want to use it in your own components.
|
|
131
131
|
|
|
132
132
|
```tsx
|
|
133
|
-
import { cn } from 'react-
|
|
133
|
+
import { cn } from '@babajaga3/react-bricks';
|
|
134
134
|
|
|
135
135
|
<div className={cn('px-4', isActive && 'bg-blue-500', props.className)} />
|
|
136
136
|
```
|
|
@@ -193,7 +193,7 @@ const Layout = createLayout({ Main: { … }, Header: { … } });
|
|
|
193
193
|
You can also export the layout type for use in other files:
|
|
194
194
|
|
|
195
195
|
```ts
|
|
196
|
-
import type { Layout } from 'react-
|
|
196
|
+
import type { Layout } from '@babajaga3/react-bricks';
|
|
197
197
|
import type { myLayoutConfig } from './layouts/mobile';
|
|
198
198
|
|
|
199
199
|
type MobileLayoutType = Layout<typeof myLayoutConfig>;
|
|
@@ -201,6 +201,12 @@ type MobileLayoutType = Layout<typeof myLayoutConfig>;
|
|
|
201
201
|
|
|
202
202
|
---
|
|
203
203
|
|
|
204
|
+
## Acknowledgement
|
|
205
|
+
|
|
206
|
+
The majority of the initial codebase has been generated with Claude Sonnet 4.6 - everything from commit [0b67bb3](https://github.com/babajaga3/react-bricks/commit/0b67bb3b3cc3355d929fa389737b28f88512c253). I had this idea in my mind and it was a fast way to prototype it. Do what you will with that information.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
204
210
|
## License
|
|
205
211
|
|
|
206
212
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -50,7 +50,7 @@ function createSlotComponent(defaultElement, defaultClassName, displayName) {
|
|
|
50
50
|
function createLayout(config, name) {
|
|
51
51
|
const layout = {};
|
|
52
52
|
for (const key in config) {
|
|
53
|
-
if (!Object.
|
|
53
|
+
if (!Object.hasOwn(config, key)) continue;
|
|
54
54
|
const slotConfig = config[key];
|
|
55
55
|
layout[key] = createSlotComponent(
|
|
56
56
|
slotConfig?.as ?? "div",
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/cn.ts","../src/create-layout.tsx","../src/extend-layout.ts"],"names":["React"],"mappings":";;;;;;;;;;;;;;;;AAiBA,IAAI,QAAA,
|
|
1
|
+
{"version":3,"sources":["../src/utils/cn.ts","../src/create-layout.tsx","../src/extend-layout.ts"],"names":["React"],"mappings":";;;;;;;;;;;;;;;;AAiBA,IAAI,QAAA,GAAsD,IAAA;AAC1D,IAAI,KAAA,GAAsD,IAAA;AAE1D,IAAI;AAEH,EAAA,QAAA,GACC,SAAA,CAAQ,gBAAgB,CAAA,CACvB,OAAA;AACH,CAAA,CAAA,MAAQ;AAER;AAEA,IAAI;AAEH,EAAA,KAAA,GAAS,SAAA,CAAQ,MAAM,CAAA,CAA+C,IAAA;AACvE,CAAA,CAAA,MAAQ;AAER;AAiBO,SAAS,MAAM,MAAA,EAA8B;AACnD,EAAA,MAAM,MAAA,GAAS,KAAA,GAAQ,KAAA,CAAM,GAAG,MAAM,CAAA,GAAI,MAAA,CAAO,MAAA,CAAO,OAAO,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAEzE,EAAA,OAAO,QAAA,GAAW,QAAA,CAAS,MAAM,CAAA,GAAI,MAAA;AACtC;;;AC/CA,SAAS,mBAAA,CACR,cAAA,EACA,gBAAA,EACA,WAAA,EACgB;AAQhB,EAAA,SAAS,IAAA,CAA0C;AAAA,IAClD,EAAA;AAAA,IACA,SAAA;AAAA,IACA,QAAA;AAAA,IACA,GAAG;AAAA,GACJ,EAA4C;AAC3C,IAAA,MAAM,UAAW,EAAA,IAAM,cAAA;AAEvB,IAAA,OAAOA,sBAAA,CAAM,aAAA;AAAA,MACZ,OAAA;AAAA,MACA,EAAE,SAAA,EAAW,EAAA,CAAG,kBAAkB,SAAS,CAAA,EAAG,GAAG,IAAA,EAAK;AAAA,MACtD;AAAA,KACD;AAAA,EACD;AAEA,EAAA,IAAA,CAAK,WAAA,GAAc,WAAA;AAEnB,EAAA,OAAO,IAAA;AACR;AAqCO,SAAS,YAAA,CACf,QACA,IAAA,EACY;AACZ,EAAA,MAAM,SAAS,EAAC;AAEhB,EAAA,KAAA,MAAW,OAAO,MAAA,EAAQ;AACzB,IAAA,IAAI,CAAC,MAAA,CAAO,MAAA,CAAO,MAAA,EAAQ,GAAG,CAAA,EAAG;AAEjC,IAAA,MAAM,UAAA,GAAa,OAAO,GAAG,CAAA;AAE7B,IAAA,MAAA,CAAO,GAAG,CAAA,GAAI,mBAAA;AAAA,MACb,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,KACvD;AAAA,EACD;AAEA,EAAA,OAAO,MAAA;AACR;;;ACpDO,SAAS,YAAA,CAIf,IAAA,EACA,SAAA,EACA,IAAA,EAC8B;AAE9B,EAAA,MAAM,aAAA,GAAgB,YAAA,CAAa,SAAA,EAAW,IAAI,CAAA;AAGlD,EAAA,OAAO,EAAE,GAAG,IAAA,EAAM,GAAG,aAAA,EAAc;AACpC;AAwBO,SAAS,YAAA,CAGd,GAAM,CAAA,EAAa;AACpB,EAAA,OAAO,EAAE,GAAG,CAAA,EAAG,GAAG,CAAA,EAAE;AACrB","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\t// eslint-disable-next-line @typescript-eslint/no-var-requires\n\t_twMerge = (\n\t\trequire(\"tailwind-merge\") as { twMerge: (...c: string[]) => string }\n\t).twMerge;\n} catch {\n\t// tailwind-merge not installed — no Tailwind conflict resolution\n}\n\ntry {\n\t// eslint-disable-next-line @typescript-eslint/no-var-requires\n\t_clsx = (require(\"clsx\") as { clsx: (...v: ClassValue[]) => string }).clsx;\n} catch {\n\t// 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\tconst joined = _clsx ? _clsx(...inputs) : inputs.filter(Boolean).join(\" \");\n\n\treturn _twMerge ? _twMerge(joined) : joined;\n}\n","import React from \"react\";\nimport type { Layout, LayoutConfig, SlotComponent, SlotProps } from \"./types\";\nimport { cn } from \"./utils/cn\";\n\n// ---------------------------------------------------------------------------\n// Internal slot factory\n// ---------------------------------------------------------------------------\n\nfunction createSlotComponent(\n\tdefaultElement: React.ElementType,\n\tdefaultClassName: string,\n\tdisplayName: string,\n): SlotComponent {\n\t/**\n\t * Polymorphic slot component.\n\t *\n\t * - Renders `as` (if provided) or the default element from config.\n\t * - Merges the config's default className with the instance's className.\n\t * - Forwards all other props straight to the rendered element.\n\t */\n\tfunction Slot<C extends React.ElementType = \"div\">({\n\t\tas,\n\t\tclassName,\n\t\tchildren,\n\t\t...rest\n\t}: SlotProps<C>): React.ReactElement | null {\n\t\tconst Element = (as ?? defaultElement) as React.ElementType;\n\n\t\treturn React.createElement(\n\t\t\tElement,\n\t\t\t{ className: cn(defaultClassName, className), ...rest },\n\t\t\tchildren,\n\t\t);\n\t}\n\n\tSlot.displayName = displayName;\n\n\treturn 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\tconfig: T,\n\tname?: string,\n): Layout<T> {\n\tconst layout = {} as Layout<T>;\n\n\tfor (const key in config) {\n\t\tif (!Object.hasOwn(config, key)) continue;\n\n\t\tconst slotConfig = config[key];\n\n\t\tlayout[key] = createSlotComponent(\n\t\t\tslotConfig?.as ?? \"div\",\n\t\t\tslotConfig?.className ?? \"\",\n\t\t\tslotConfig?.displayName ?? (name ? `${name}.${key}` : key),\n\t\t);\n\t}\n\n\treturn layout;\n}\n","import { createLayout } from \"./create-layout\";\nimport type {\n\tLayoutConfig,\n\tLayout,\n\tExtendedLayout,\n\tSlotComponent,\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\tTBase extends LayoutConfig,\n\tTExt extends LayoutConfig,\n>(\n\tbase: Layout<TBase>,\n\textension: TExt,\n\tname?: string,\n): ExtendedLayout<TBase, TExt> {\n\t// Build the new components from the extension config\n\tconst extendedParts = createLayout(extension, name);\n\n\t// Merge: base slots first, then extension slots override\n\treturn { ...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\tA extends Record<string, SlotComponent>,\n\tB extends Record<string, SlotComponent>,\n>(a: A, b: B): A & B {\n\treturn { ...a, ...b };\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -4,12 +4,12 @@ import React from 'react';
|
|
|
4
4
|
* Extracts props from a given element type, excluding the ones we own.
|
|
5
5
|
*/
|
|
6
6
|
type PropsOf<C extends React.ElementType> = React.ComponentPropsWithoutRef<C>;
|
|
7
|
-
type OwnProps =
|
|
7
|
+
type OwnProps = "as" | "className" | "children";
|
|
8
8
|
/**
|
|
9
9
|
* Props for a single layout slot component.
|
|
10
10
|
* Supports the `as` prop for full polymorphism — any HTML element or React component.
|
|
11
11
|
*/
|
|
12
|
-
type SlotProps<C extends React.ElementType =
|
|
12
|
+
type SlotProps<C extends React.ElementType = "div"> = {
|
|
13
13
|
/** Override the rendered element/component for this slot */
|
|
14
14
|
as?: C;
|
|
15
15
|
/** Extra classes merged on top of the slot's default className */
|
|
@@ -54,7 +54,7 @@ type LayoutConfig = Record<string, SlotConfig>;
|
|
|
54
54
|
* and also narrows the available HTML/component props accordingly.
|
|
55
55
|
*/
|
|
56
56
|
interface SlotComponent {
|
|
57
|
-
<C extends React.ElementType =
|
|
57
|
+
<C extends React.ElementType = "div">(props: SlotProps<C>): React.ReactElement | null;
|
|
58
58
|
displayName?: string;
|
|
59
59
|
}
|
|
60
60
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -4,12 +4,12 @@ import React from 'react';
|
|
|
4
4
|
* Extracts props from a given element type, excluding the ones we own.
|
|
5
5
|
*/
|
|
6
6
|
type PropsOf<C extends React.ElementType> = React.ComponentPropsWithoutRef<C>;
|
|
7
|
-
type OwnProps =
|
|
7
|
+
type OwnProps = "as" | "className" | "children";
|
|
8
8
|
/**
|
|
9
9
|
* Props for a single layout slot component.
|
|
10
10
|
* Supports the `as` prop for full polymorphism — any HTML element or React component.
|
|
11
11
|
*/
|
|
12
|
-
type SlotProps<C extends React.ElementType =
|
|
12
|
+
type SlotProps<C extends React.ElementType = "div"> = {
|
|
13
13
|
/** Override the rendered element/component for this slot */
|
|
14
14
|
as?: C;
|
|
15
15
|
/** Extra classes merged on top of the slot's default className */
|
|
@@ -54,7 +54,7 @@ type LayoutConfig = Record<string, SlotConfig>;
|
|
|
54
54
|
* and also narrows the available HTML/component props accordingly.
|
|
55
55
|
*/
|
|
56
56
|
interface SlotComponent {
|
|
57
|
-
<C extends React.ElementType =
|
|
57
|
+
<C extends React.ElementType = "div">(props: SlotProps<C>): React.ReactElement | null;
|
|
58
58
|
displayName?: string;
|
|
59
59
|
}
|
|
60
60
|
/**
|
package/dist/index.js
CHANGED
|
@@ -44,7 +44,7 @@ function createSlotComponent(defaultElement, defaultClassName, displayName) {
|
|
|
44
44
|
function createLayout(config, name) {
|
|
45
45
|
const layout = {};
|
|
46
46
|
for (const key in config) {
|
|
47
|
-
if (!Object.
|
|
47
|
+
if (!Object.hasOwn(config, key)) continue;
|
|
48
48
|
const slotConfig = config[key];
|
|
49
49
|
layout[key] = createSlotComponent(
|
|
50
50
|
slotConfig?.as ?? "div",
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/cn.ts","../src/create-layout.tsx","../src/extend-layout.ts"],"names":[],"mappings":";;;;;;;;;;AAiBA,IAAI,QAAA,
|
|
1
|
+
{"version":3,"sources":["../src/utils/cn.ts","../src/create-layout.tsx","../src/extend-layout.ts"],"names":[],"mappings":";;;;;;;;;;AAiBA,IAAI,QAAA,GAAsD,IAAA;AAC1D,IAAI,KAAA,GAAsD,IAAA;AAE1D,IAAI;AAEH,EAAA,QAAA,GACC,SAAA,CAAQ,gBAAgB,CAAA,CACvB,OAAA;AACH,CAAA,CAAA,MAAQ;AAER;AAEA,IAAI;AAEH,EAAA,KAAA,GAAS,SAAA,CAAQ,MAAM,CAAA,CAA+C,IAAA;AACvE,CAAA,CAAA,MAAQ;AAER;AAiBO,SAAS,MAAM,MAAA,EAA8B;AACnD,EAAA,MAAM,MAAA,GAAS,KAAA,GAAQ,KAAA,CAAM,GAAG,MAAM,CAAA,GAAI,MAAA,CAAO,MAAA,CAAO,OAAO,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAEzE,EAAA,OAAO,QAAA,GAAW,QAAA,CAAS,MAAM,CAAA,GAAI,MAAA;AACtC;;;AC/CA,SAAS,mBAAA,CACR,cAAA,EACA,gBAAA,EACA,WAAA,EACgB;AAQhB,EAAA,SAAS,IAAA,CAA0C;AAAA,IAClD,EAAA;AAAA,IACA,SAAA;AAAA,IACA,QAAA;AAAA,IACA,GAAG;AAAA,GACJ,EAA4C;AAC3C,IAAA,MAAM,UAAW,EAAA,IAAM,cAAA;AAEvB,IAAA,OAAO,KAAA,CAAM,aAAA;AAAA,MACZ,OAAA;AAAA,MACA,EAAE,SAAA,EAAW,EAAA,CAAG,kBAAkB,SAAS,CAAA,EAAG,GAAG,IAAA,EAAK;AAAA,MACtD;AAAA,KACD;AAAA,EACD;AAEA,EAAA,IAAA,CAAK,WAAA,GAAc,WAAA;AAEnB,EAAA,OAAO,IAAA;AACR;AAqCO,SAAS,YAAA,CACf,QACA,IAAA,EACY;AACZ,EAAA,MAAM,SAAS,EAAC;AAEhB,EAAA,KAAA,MAAW,OAAO,MAAA,EAAQ;AACzB,IAAA,IAAI,CAAC,MAAA,CAAO,MAAA,CAAO,MAAA,EAAQ,GAAG,CAAA,EAAG;AAEjC,IAAA,MAAM,UAAA,GAAa,OAAO,GAAG,CAAA;AAE7B,IAAA,MAAA,CAAO,GAAG,CAAA,GAAI,mBAAA;AAAA,MACb,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,KACvD;AAAA,EACD;AAEA,EAAA,OAAO,MAAA;AACR;;;ACpDO,SAAS,YAAA,CAIf,IAAA,EACA,SAAA,EACA,IAAA,EAC8B;AAE9B,EAAA,MAAM,aAAA,GAAgB,YAAA,CAAa,SAAA,EAAW,IAAI,CAAA;AAGlD,EAAA,OAAO,EAAE,GAAG,IAAA,EAAM,GAAG,aAAA,EAAc;AACpC;AAwBO,SAAS,YAAA,CAGd,GAAM,CAAA,EAAa;AACpB,EAAA,OAAO,EAAE,GAAG,CAAA,EAAG,GAAG,CAAA,EAAE;AACrB","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\t// eslint-disable-next-line @typescript-eslint/no-var-requires\n\t_twMerge = (\n\t\trequire(\"tailwind-merge\") as { twMerge: (...c: string[]) => string }\n\t).twMerge;\n} catch {\n\t// tailwind-merge not installed — no Tailwind conflict resolution\n}\n\ntry {\n\t// eslint-disable-next-line @typescript-eslint/no-var-requires\n\t_clsx = (require(\"clsx\") as { clsx: (...v: ClassValue[]) => string }).clsx;\n} catch {\n\t// 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\tconst joined = _clsx ? _clsx(...inputs) : inputs.filter(Boolean).join(\" \");\n\n\treturn _twMerge ? _twMerge(joined) : joined;\n}\n","import React from \"react\";\nimport type { Layout, LayoutConfig, SlotComponent, SlotProps } from \"./types\";\nimport { cn } from \"./utils/cn\";\n\n// ---------------------------------------------------------------------------\n// Internal slot factory\n// ---------------------------------------------------------------------------\n\nfunction createSlotComponent(\n\tdefaultElement: React.ElementType,\n\tdefaultClassName: string,\n\tdisplayName: string,\n): SlotComponent {\n\t/**\n\t * Polymorphic slot component.\n\t *\n\t * - Renders `as` (if provided) or the default element from config.\n\t * - Merges the config's default className with the instance's className.\n\t * - Forwards all other props straight to the rendered element.\n\t */\n\tfunction Slot<C extends React.ElementType = \"div\">({\n\t\tas,\n\t\tclassName,\n\t\tchildren,\n\t\t...rest\n\t}: SlotProps<C>): React.ReactElement | null {\n\t\tconst Element = (as ?? defaultElement) as React.ElementType;\n\n\t\treturn React.createElement(\n\t\t\tElement,\n\t\t\t{ className: cn(defaultClassName, className), ...rest },\n\t\t\tchildren,\n\t\t);\n\t}\n\n\tSlot.displayName = displayName;\n\n\treturn 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\tconfig: T,\n\tname?: string,\n): Layout<T> {\n\tconst layout = {} as Layout<T>;\n\n\tfor (const key in config) {\n\t\tif (!Object.hasOwn(config, key)) continue;\n\n\t\tconst slotConfig = config[key];\n\n\t\tlayout[key] = createSlotComponent(\n\t\t\tslotConfig?.as ?? \"div\",\n\t\t\tslotConfig?.className ?? \"\",\n\t\t\tslotConfig?.displayName ?? (name ? `${name}.${key}` : key),\n\t\t);\n\t}\n\n\treturn layout;\n}\n","import { createLayout } from \"./create-layout\";\nimport type {\n\tLayoutConfig,\n\tLayout,\n\tExtendedLayout,\n\tSlotComponent,\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\tTBase extends LayoutConfig,\n\tTExt extends LayoutConfig,\n>(\n\tbase: Layout<TBase>,\n\textension: TExt,\n\tname?: string,\n): ExtendedLayout<TBase, TExt> {\n\t// Build the new components from the extension config\n\tconst extendedParts = createLayout(extension, name);\n\n\t// Merge: base slots first, then extension slots override\n\treturn { ...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\tA extends Record<string, SlotComponent>,\n\tB extends Record<string, SlotComponent>,\n>(a: A, b: B): A & B {\n\treturn { ...a, ...b };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@babajaga3/react-bricks",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "A tiny factory for building composable, Tailwind-friendly React layout component systems.",
|
|
5
|
-
"author":
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Toma Bourov",
|
|
7
|
+
"url": "https://tomabourov.com",
|
|
8
|
+
"email": "me@tomabourov.com"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/babajaga3/react-bricks#readme",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/babajaga3/react-bricks.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/babajaga3/react-bricks/issues",
|
|
17
|
+
"email": "me@tomabourov.com"
|
|
18
|
+
},
|
|
6
19
|
"license": "MIT",
|
|
7
20
|
"keywords": [
|
|
8
21
|
"react",
|
|
@@ -37,7 +50,11 @@
|
|
|
37
50
|
"dev": "tsup --watch",
|
|
38
51
|
"typecheck": "tsc --noEmit",
|
|
39
52
|
"lint": "eslint src --ext .ts,.tsx",
|
|
40
|
-
"prepublishOnly": "
|
|
53
|
+
"prepublishOnly": "pnpm run build && pnpm run typecheck",
|
|
54
|
+
"preinstall": "npx only-allow pnpm"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=18.0.0"
|
|
41
58
|
},
|
|
42
59
|
"peerDependencies": {
|
|
43
60
|
"react": ">=17.0.0",
|
|
@@ -52,6 +69,7 @@
|
|
|
52
69
|
}
|
|
53
70
|
},
|
|
54
71
|
"devDependencies": {
|
|
72
|
+
"@biomejs/biome": "2.4.10",
|
|
55
73
|
"@types/node": "^25.5.2",
|
|
56
74
|
"@types/react": "^18.3.0",
|
|
57
75
|
"@types/react-dom": "^18.3.0",
|