@damarkuncoro/layout-engine 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 +45 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +3 -0
- package/dist/core/render.d.ts +5 -0
- package/dist/core/render.js +41 -0
- package/dist/core/responsiveSystem.d.ts +11 -0
- package/dist/core/responsiveSystem.js +41 -0
- package/dist/core/spacingSystem.d.ts +2 -0
- package/dist/core/spacingSystem.js +17 -0
- package/dist/core/styleResolver.d.ts +5 -0
- package/dist/core/styleResolver.js +10 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/patterns/DashboardLayout.d.ts +8 -0
- package/dist/patterns/DashboardLayout.js +11 -0
- package/dist/primitives/Box.d.ts +15 -0
- package/dist/primitives/Box.js +23 -0
- package/dist/primitives/Flex.d.ts +20 -0
- package/dist/primitives/Flex.js +27 -0
- package/dist/primitives/Grid.d.ts +15 -0
- package/dist/primitives/Grid.js +27 -0
- package/dist/primitives/Stack.d.ts +13 -0
- package/dist/primitives/Stack.js +21 -0
- package/dist/primitives/index.d.ts +4 -0
- package/dist/primitives/index.js +4 -0
- package/dist/structures/SidebarLayout.d.ts +13 -0
- package/dist/structures/SidebarLayout.js +16 -0
- package/dist/system/contracts.d.ts +17 -0
- package/dist/system/contracts.js +1 -0
- package/dist/system/index.d.ts +2 -0
- package/dist/system/index.js +2 -0
- package/dist/system/types.d.ts +38 -0
- package/dist/system/types.js +1 -0
- package/dist/tests/basic.test.d.ts +1 -0
- package/dist/tests/basic.test.js +38 -0
- package/dist/tests/structures.test.d.ts +1 -0
- package/dist/tests/structures.test.js +25 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# layout-engine
|
|
2
|
+
|
|
3
|
+
Headless layout engine berfokus pada primitives (Box, Flex, Stack, Grid) dan structures (SidebarLayout) dengan sistem responsive sederhana.
|
|
4
|
+
|
|
5
|
+
## Instalasi
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @damarkuncoro/layout-engine
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Fitur
|
|
12
|
+
- Headless nodes → mudah dirender ke HTML string atau adaptor UI.
|
|
13
|
+
- Primitives: Box, Flex, Stack, Grid.
|
|
14
|
+
- Structures: SidebarLayout.
|
|
15
|
+
- Sistem responsive berbasis breakpoints (sm, md, lg, xl, 2xl).
|
|
16
|
+
|
|
17
|
+
## Penggunaan Dasar
|
|
18
|
+
|
|
19
|
+
Render headless ke HTML:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import { Box, Flex, Stack, Grid, SidebarLayout, renderToString } from "layout-engine"
|
|
23
|
+
|
|
24
|
+
const page = SidebarLayout({
|
|
25
|
+
sidebar: Box({ children: "Sidebar" }),
|
|
26
|
+
children: Stack({ gap: 16, children: [Box({ children: "Card 1" }), Box({ children: "Card 2" })] }),
|
|
27
|
+
sidebarWidth: { base: 120, md: 200, xl: 280 },
|
|
28
|
+
viewportWidth: 1024
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
console.log(renderToString(page))
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Responsive utility:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import { resolveResponsive } from "layout-engine"
|
|
38
|
+
const gap = resolveResponsive({ base: 8, md: 16, xl: 24 }, 768) // 16
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## TypeScript
|
|
42
|
+
- ESM-only, deklarasi tipe tersedia pada `dist/index.d.ts`.
|
|
43
|
+
|
|
44
|
+
## Lisensi
|
|
45
|
+
MIT
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const styleToString = (style) => {
|
|
2
|
+
if (!style)
|
|
3
|
+
return "";
|
|
4
|
+
const entries = Object.entries(style)
|
|
5
|
+
.filter(([_, v]) => v !== undefined && v !== null && v !== "")
|
|
6
|
+
.map(([k, v]) => {
|
|
7
|
+
const prop = k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
8
|
+
return `${prop}:${String(v)}`;
|
|
9
|
+
});
|
|
10
|
+
return entries.join(";");
|
|
11
|
+
};
|
|
12
|
+
const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
13
|
+
const renderChild = (child) => {
|
|
14
|
+
if (child === null || child === undefined || child === false)
|
|
15
|
+
return "";
|
|
16
|
+
if (Array.isArray(child))
|
|
17
|
+
return child.map(renderChild).join("");
|
|
18
|
+
if (typeof child === "object" && child.type && child.props) {
|
|
19
|
+
return renderToString(child);
|
|
20
|
+
}
|
|
21
|
+
return escapeHtml(String(child));
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Merender HeadlessNode menjadi string HTML dengan inline style.
|
|
25
|
+
*/
|
|
26
|
+
export const renderToString = (node) => {
|
|
27
|
+
const { type, props } = node;
|
|
28
|
+
const { children, style, className, ...rest } = props || {};
|
|
29
|
+
const styleStr = styleToString(style);
|
|
30
|
+
const attr = (className ? ` class="${className}"` : "") +
|
|
31
|
+
(styleStr ? ` style="${styleStr}"` : "") +
|
|
32
|
+
Object.entries(rest)
|
|
33
|
+
.map(([k, v]) => {
|
|
34
|
+
if (v === undefined || v === null || typeof v === "object")
|
|
35
|
+
return "";
|
|
36
|
+
return ` ${k}="${String(v)}"`;
|
|
37
|
+
})
|
|
38
|
+
.join("");
|
|
39
|
+
const inner = renderChild(children);
|
|
40
|
+
return `<${type}${attr}>${inner}</${type}>`;
|
|
41
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type BreakpointKey = "sm" | "md" | "lg" | "xl" | "2xl";
|
|
2
|
+
export declare const breakpoints: Record<BreakpointKey, number>;
|
|
3
|
+
export type ResponsiveValue<T> = T | {
|
|
4
|
+
base?: T;
|
|
5
|
+
sm?: T;
|
|
6
|
+
md?: T;
|
|
7
|
+
lg?: T;
|
|
8
|
+
xl?: T;
|
|
9
|
+
"2xl"?: T;
|
|
10
|
+
} | T[];
|
|
11
|
+
export declare const resolveResponsive: <T>(value: ResponsiveValue<T> | undefined, width: number) => T | undefined;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const breakpoints = {
|
|
2
|
+
sm: 640,
|
|
3
|
+
md: 768,
|
|
4
|
+
lg: 1024,
|
|
5
|
+
xl: 1280,
|
|
6
|
+
"2xl": 1536
|
|
7
|
+
};
|
|
8
|
+
export const resolveResponsive = (value, width) => {
|
|
9
|
+
if (value === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (!Array.isArray(value) && typeof value !== "object")
|
|
12
|
+
return value;
|
|
13
|
+
const order = [
|
|
14
|
+
{ min: 0 },
|
|
15
|
+
{ key: "sm", min: breakpoints.sm },
|
|
16
|
+
{ key: "md", min: breakpoints.md },
|
|
17
|
+
{ key: "lg", min: breakpoints.lg },
|
|
18
|
+
{ key: "xl", min: breakpoints.xl },
|
|
19
|
+
{ key: "2xl", min: breakpoints["2xl"] }
|
|
20
|
+
];
|
|
21
|
+
let resolved = undefined;
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
for (let i = 0; i < order.length; i++) {
|
|
24
|
+
if (width >= order[i].min && i < value.length) {
|
|
25
|
+
resolved = value[i];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return resolved;
|
|
29
|
+
}
|
|
30
|
+
const obj = value;
|
|
31
|
+
if (obj.base !== undefined)
|
|
32
|
+
resolved = obj.base;
|
|
33
|
+
for (const it of order) {
|
|
34
|
+
if (!it.key)
|
|
35
|
+
continue;
|
|
36
|
+
if (width >= it.min && obj[it.key] !== undefined) {
|
|
37
|
+
resolved = obj[it.key];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return resolved;
|
|
41
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const spacingScale = {
|
|
2
|
+
0: 0,
|
|
3
|
+
1: 4,
|
|
4
|
+
2: 8,
|
|
5
|
+
3: 12,
|
|
6
|
+
4: 16,
|
|
7
|
+
5: 24,
|
|
8
|
+
6: 32,
|
|
9
|
+
7: 40,
|
|
10
|
+
8: 48,
|
|
11
|
+
9: 64
|
|
12
|
+
};
|
|
13
|
+
export const space = (i) => {
|
|
14
|
+
const v = spacingScale[i];
|
|
15
|
+
const n = typeof v === "number" ? v : i;
|
|
16
|
+
return `${n}px`;
|
|
17
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mengubah nilai panjang CSS: angka → px, string tetap.
|
|
3
|
+
* Contoh: 16 -> "16px", "2rem" -> "2rem", undefined -> undefined
|
|
4
|
+
*/
|
|
5
|
+
export const normalizeUnit = (value) => {
|
|
6
|
+
if (typeof value === "number") {
|
|
7
|
+
return `${value}px`;
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./core/styleResolver.js";
|
|
2
|
+
export * from "./core/spacingSystem.js";
|
|
3
|
+
export { resolveResponsive, breakpoints, type BreakpointKey } from "./core/responsiveSystem.js";
|
|
4
|
+
export * from "./core/render.js";
|
|
5
|
+
export * from "./system/types.js";
|
|
6
|
+
export * from "./system/contracts.js";
|
|
7
|
+
export { Box, type LayoutProps } from "./primitives/Box.js";
|
|
8
|
+
export { Flex, type FlexProps } from "./primitives/Flex.js";
|
|
9
|
+
export { Stack, type StackProps } from "./primitives/Stack.js";
|
|
10
|
+
export { Grid, type GridProps } from "./primitives/Grid.js";
|
|
11
|
+
export { SidebarLayout, type ResponsiveSidebarLayoutProps } from "./structures/SidebarLayout.js";
|
|
12
|
+
export * from "./patterns/DashboardLayout.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./core/styleResolver.js";
|
|
2
|
+
export * from "./core/spacingSystem.js";
|
|
3
|
+
export { resolveResponsive, breakpoints } from "./core/responsiveSystem.js";
|
|
4
|
+
export * from "./core/render.js";
|
|
5
|
+
export * from "./system/types.js";
|
|
6
|
+
export * from "./system/contracts.js";
|
|
7
|
+
export { Box } from "./primitives/Box.js";
|
|
8
|
+
export { Flex } from "./primitives/Flex.js";
|
|
9
|
+
export { Stack } from "./primitives/Stack.js";
|
|
10
|
+
export { Grid } from "./primitives/Grid.js";
|
|
11
|
+
export { SidebarLayout } from "./structures/SidebarLayout.js";
|
|
12
|
+
export * from "./patterns/DashboardLayout.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Flex } from "../primitives/Flex.js";
|
|
2
|
+
import { SidebarLayout } from "../structures/SidebarLayout.js";
|
|
3
|
+
export function DashboardLayout({ header, sidebar, children }) {
|
|
4
|
+
return Flex({
|
|
5
|
+
direction: "column",
|
|
6
|
+
children: [
|
|
7
|
+
header,
|
|
8
|
+
SidebarLayout({ sidebar, children })
|
|
9
|
+
]
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LayoutProps } from "../system/types.js";
|
|
2
|
+
export { type LayoutProps };
|
|
3
|
+
/**
|
|
4
|
+
* Komponen blok dasar yang memetakan LayoutProps → inline style.
|
|
5
|
+
* Gunakan untuk membangun primitive lainnya.
|
|
6
|
+
*/
|
|
7
|
+
export declare function Box({ children, padding, margin, width, height, display, style, ...rest }: LayoutProps & {
|
|
8
|
+
children?: any;
|
|
9
|
+
}): {
|
|
10
|
+
type: string;
|
|
11
|
+
props: {
|
|
12
|
+
children: any;
|
|
13
|
+
style: Record<string, any>;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { normalizeUnit } from "../core/styleResolver.js";
|
|
2
|
+
/**
|
|
3
|
+
* Komponen blok dasar yang memetakan LayoutProps → inline style.
|
|
4
|
+
* Gunakan untuk membangun primitive lainnya.
|
|
5
|
+
*/
|
|
6
|
+
export function Box({ children, padding, margin, width, height, display, style, ...rest }) {
|
|
7
|
+
const resolved = {
|
|
8
|
+
padding: normalizeUnit(padding),
|
|
9
|
+
margin: normalizeUnit(margin),
|
|
10
|
+
width: normalizeUnit(width),
|
|
11
|
+
height: normalizeUnit(height),
|
|
12
|
+
display,
|
|
13
|
+
...style
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
type: "div",
|
|
17
|
+
props: {
|
|
18
|
+
style: resolved,
|
|
19
|
+
...rest,
|
|
20
|
+
children
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { LayoutProps, CSSLength } from "../system/types.js";
|
|
2
|
+
export interface FlexProps extends LayoutProps {
|
|
3
|
+
justify?: string;
|
|
4
|
+
align?: string;
|
|
5
|
+
gap?: CSSLength;
|
|
6
|
+
direction?: "row" | "row-reverse" | "column" | "column-reverse";
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Kontainer fleksibel berbasis style inline (tanpa dependency React).
|
|
10
|
+
* Menghasilkan struktur objek yang merepresentasikan node 'div' dengan style flex.
|
|
11
|
+
*/
|
|
12
|
+
export declare function Flex({ children, justify, align, gap, direction, padding, margin, width, height, display, style, ...rest }: FlexProps & {
|
|
13
|
+
children?: any;
|
|
14
|
+
}): {
|
|
15
|
+
type: string;
|
|
16
|
+
props: {
|
|
17
|
+
children: any;
|
|
18
|
+
style: Record<string, any>;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { normalizeUnit } from "../core/styleResolver.js";
|
|
2
|
+
/**
|
|
3
|
+
* Kontainer fleksibel berbasis style inline (tanpa dependency React).
|
|
4
|
+
* Menghasilkan struktur objek yang merepresentasikan node 'div' dengan style flex.
|
|
5
|
+
*/
|
|
6
|
+
export function Flex({ children, justify, align, gap, direction = "row", padding, margin, width, height, display, style, ...rest }) {
|
|
7
|
+
const resolved = {
|
|
8
|
+
padding: normalizeUnit(padding),
|
|
9
|
+
margin: normalizeUnit(margin),
|
|
10
|
+
width: normalizeUnit(width),
|
|
11
|
+
height: normalizeUnit(height),
|
|
12
|
+
display: display !== null && display !== void 0 ? display : "flex",
|
|
13
|
+
justifyContent: justify,
|
|
14
|
+
alignItems: align,
|
|
15
|
+
gap: normalizeUnit(gap),
|
|
16
|
+
flexDirection: direction,
|
|
17
|
+
...style
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
type: "div",
|
|
21
|
+
props: {
|
|
22
|
+
style: resolved,
|
|
23
|
+
...rest,
|
|
24
|
+
children
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CSSLength, LayoutProps } from "../system/types.js";
|
|
2
|
+
export interface GridProps extends LayoutProps {
|
|
3
|
+
columns?: number | string;
|
|
4
|
+
rows?: number | string;
|
|
5
|
+
gap?: CSSLength;
|
|
6
|
+
}
|
|
7
|
+
export declare function Grid({ children, columns, rows, gap, padding, margin, width, height, display, style, ...rest }: GridProps & {
|
|
8
|
+
children?: any;
|
|
9
|
+
}): {
|
|
10
|
+
type: string;
|
|
11
|
+
props: {
|
|
12
|
+
children: any;
|
|
13
|
+
style: Record<string, any>;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { normalizeUnit } from "../core/styleResolver.js";
|
|
2
|
+
const toTemplate = (v) => {
|
|
3
|
+
if (typeof v === "number")
|
|
4
|
+
return `repeat(${v}, minmax(0, 1fr))`;
|
|
5
|
+
return v;
|
|
6
|
+
};
|
|
7
|
+
export function Grid({ children, columns, rows, gap, padding, margin, width, height, display, style, ...rest }) {
|
|
8
|
+
const resolved = {
|
|
9
|
+
padding: normalizeUnit(padding),
|
|
10
|
+
margin: normalizeUnit(margin),
|
|
11
|
+
width: normalizeUnit(width),
|
|
12
|
+
height: normalizeUnit(height),
|
|
13
|
+
display: display !== null && display !== void 0 ? display : "grid",
|
|
14
|
+
gridTemplateColumns: toTemplate(columns),
|
|
15
|
+
gridTemplateRows: toTemplate(rows),
|
|
16
|
+
gap: normalizeUnit(gap),
|
|
17
|
+
...style
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
type: "div",
|
|
21
|
+
props: {
|
|
22
|
+
style: resolved,
|
|
23
|
+
...rest,
|
|
24
|
+
children
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CSSLength, LayoutProps } from "../system/types.js";
|
|
2
|
+
export interface StackProps extends LayoutProps {
|
|
3
|
+
gap?: CSSLength;
|
|
4
|
+
}
|
|
5
|
+
export declare function Stack({ children, gap, padding, margin, width, height, display, style, ...rest }: StackProps & {
|
|
6
|
+
children?: any;
|
|
7
|
+
}): {
|
|
8
|
+
type: string;
|
|
9
|
+
props: {
|
|
10
|
+
children: any;
|
|
11
|
+
style: Record<string, any>;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { normalizeUnit } from "../core/styleResolver.js";
|
|
2
|
+
export function Stack({ children, gap = 8, padding, margin, width, height, display, style, ...rest }) {
|
|
3
|
+
const resolved = {
|
|
4
|
+
padding: normalizeUnit(padding),
|
|
5
|
+
margin: normalizeUnit(margin),
|
|
6
|
+
width: normalizeUnit(width),
|
|
7
|
+
height: normalizeUnit(height),
|
|
8
|
+
display: display !== null && display !== void 0 ? display : "flex",
|
|
9
|
+
flexDirection: "column",
|
|
10
|
+
gap: normalizeUnit(gap),
|
|
11
|
+
...style
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
type: "div",
|
|
15
|
+
props: {
|
|
16
|
+
style: resolved,
|
|
17
|
+
...rest,
|
|
18
|
+
children
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SidebarLayoutContract } from "../system/contracts.js";
|
|
2
|
+
import type { CSSLength, ResponsiveValue } from "../system/types.js";
|
|
3
|
+
export interface ResponsiveSidebarLayoutProps extends Omit<SidebarLayoutContract, "sidebarWidth"> {
|
|
4
|
+
sidebarWidth?: ResponsiveValue<CSSLength>;
|
|
5
|
+
viewportWidth?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function SidebarLayout({ sidebar, children, sidebarWidth, viewportWidth }: ResponsiveSidebarLayoutProps): {
|
|
8
|
+
type: string;
|
|
9
|
+
props: {
|
|
10
|
+
children: any;
|
|
11
|
+
style: Record<string, any>;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Flex } from "../primitives/Flex.js";
|
|
2
|
+
import { Box } from "../primitives/Box.js";
|
|
3
|
+
import { resolveResponsive } from "../core/responsiveSystem.js";
|
|
4
|
+
import { normalizeUnit } from "../core/styleResolver.js";
|
|
5
|
+
export function SidebarLayout({ sidebar, children, sidebarWidth = 240, viewportWidth = 1024 }) {
|
|
6
|
+
var _a;
|
|
7
|
+
// Resolve responsive sidebarWidth based on viewport
|
|
8
|
+
const resolvedWidth = normalizeUnit((_a = resolveResponsive(sidebarWidth, viewportWidth)) !== null && _a !== void 0 ? _a : 240);
|
|
9
|
+
return Flex({
|
|
10
|
+
gap: 16,
|
|
11
|
+
children: [
|
|
12
|
+
Box({ width: resolvedWidth, children: sidebar }),
|
|
13
|
+
Box({ style: { flex: 1 }, children })
|
|
14
|
+
]
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CSSLength, HeadlessNode, ResponsiveValue } from "./types.js";
|
|
2
|
+
export type NodeLike = HeadlessNode | string | number | boolean | null | undefined;
|
|
3
|
+
export interface SidebarLayoutContract {
|
|
4
|
+
sidebar: NodeLike;
|
|
5
|
+
children: NodeLike;
|
|
6
|
+
sidebarWidth?: CSSLength | ResponsiveValue<CSSLength>;
|
|
7
|
+
viewportWidth?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface HeaderLayoutContract {
|
|
10
|
+
header: NodeLike;
|
|
11
|
+
children: NodeLike;
|
|
12
|
+
}
|
|
13
|
+
export interface DashboardLayoutContract {
|
|
14
|
+
header: NodeLike;
|
|
15
|
+
sidebar: NodeLike;
|
|
16
|
+
children: NodeLike;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tipe panjang CSS. Angka akan dinormalisasi ke 'px' oleh resolver.
|
|
3
|
+
*/
|
|
4
|
+
export type CSSLength = number | string;
|
|
5
|
+
/**
|
|
6
|
+
* Responsive value that adapts based on viewport width.
|
|
7
|
+
*/
|
|
8
|
+
export type ResponsiveValue<T> = T | {
|
|
9
|
+
base?: T;
|
|
10
|
+
sm?: T;
|
|
11
|
+
md?: T;
|
|
12
|
+
lg?: T;
|
|
13
|
+
xl?: T;
|
|
14
|
+
"2xl"?: T;
|
|
15
|
+
} | T[];
|
|
16
|
+
/**
|
|
17
|
+
* Props dasar untuk layout primitives.
|
|
18
|
+
* Hanya properti yang dipetakan ke style inline.
|
|
19
|
+
*/
|
|
20
|
+
export interface LayoutProps {
|
|
21
|
+
padding?: CSSLength;
|
|
22
|
+
margin?: CSSLength;
|
|
23
|
+
width?: CSSLength;
|
|
24
|
+
height?: CSSLength;
|
|
25
|
+
display?: string;
|
|
26
|
+
style?: Record<string, any>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Representasi node headless untuk renderer.
|
|
30
|
+
*/
|
|
31
|
+
export interface HeadlessNode {
|
|
32
|
+
type: string;
|
|
33
|
+
props: {
|
|
34
|
+
style?: Record<string, any>;
|
|
35
|
+
children?: any;
|
|
36
|
+
[key: string]: any;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const assert = (cond, msg) => {
|
|
2
|
+
if (!cond) {
|
|
3
|
+
throw new Error(`Test failed: ${msg}`);
|
|
4
|
+
}
|
|
5
|
+
};
|
|
6
|
+
const run = async () => {
|
|
7
|
+
const api = await import(new URL("../index.js", import.meta.url).href);
|
|
8
|
+
const { normalizeUnit, space, resolveResponsive, Flex, Box, renderToString } = api;
|
|
9
|
+
// normalizeUnit
|
|
10
|
+
assert(normalizeUnit(16) === "16px", "normalizeUnit number to px");
|
|
11
|
+
assert(normalizeUnit("2rem") === "2rem", "normalizeUnit keeps string");
|
|
12
|
+
// spacing tokens
|
|
13
|
+
assert(space(2) === "8px", "spacing index 2 is 8px");
|
|
14
|
+
// responsive resolver (object form)
|
|
15
|
+
assert(resolveResponsive({ base: 8, md: 16, xl: 24 }, 800) === 16, "resolveResponsive picks md at width 800");
|
|
16
|
+
assert(resolveResponsive({ base: 8, md: 16, xl: 24 }, 1300) === 24, "resolveResponsive picks xl at width 1300");
|
|
17
|
+
// primitives mapping + render
|
|
18
|
+
const node = Flex({
|
|
19
|
+
direction: "row",
|
|
20
|
+
gap: 16,
|
|
21
|
+
children: [
|
|
22
|
+
Box({ width: 240, children: "Sidebar" }),
|
|
23
|
+
Box({ style: { flex: 1 }, children: "Content" })
|
|
24
|
+
]
|
|
25
|
+
});
|
|
26
|
+
const html = renderToString(node);
|
|
27
|
+
assert(html.includes("display:flex"), "Flex renders display flex");
|
|
28
|
+
assert(html.includes("flex-direction:row"), "Flex renders direction");
|
|
29
|
+
assert(html.includes("gap:16px"), "Flex renders gap 16px");
|
|
30
|
+
assert(html.includes("width:240px"), "Box width 240px");
|
|
31
|
+
assert(html.includes("flex:1"), "Box flex:1 in style");
|
|
32
|
+
console.log("All tests passed");
|
|
33
|
+
};
|
|
34
|
+
run().catch((e) => {
|
|
35
|
+
console.error(e);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|
|
38
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const assert = (cond, msg) => {
|
|
2
|
+
if (!cond) {
|
|
3
|
+
throw new Error(`Test failed: ${msg}`);
|
|
4
|
+
}
|
|
5
|
+
};
|
|
6
|
+
const run = async () => {
|
|
7
|
+
const api = await import(new URL("../index.js", import.meta.url).href);
|
|
8
|
+
const { SidebarLayout, DashboardLayout, renderToString, Box } = api;
|
|
9
|
+
const sidebar = Box({ children: "SIDE" });
|
|
10
|
+
const content = Box({ children: "MAIN" });
|
|
11
|
+
const header = Box({ children: "HEAD" });
|
|
12
|
+
const node1 = SidebarLayout({ sidebar, children: content, sidebarWidth: 200 });
|
|
13
|
+
const html1 = renderToString(node1);
|
|
14
|
+
assert(html1.includes("width:200px"), "Sidebar width applied");
|
|
15
|
+
assert(html1.indexOf("SIDE") < html1.indexOf("MAIN"), "Sidebar before content");
|
|
16
|
+
const node2 = DashboardLayout({ header, sidebar, children: content });
|
|
17
|
+
const html2 = renderToString(node2);
|
|
18
|
+
assert(html2.indexOf("HEAD") < html2.indexOf("SIDE"), "Header before sidebar/content");
|
|
19
|
+
console.log("Structures tests passed");
|
|
20
|
+
};
|
|
21
|
+
run().catch((e) => {
|
|
22
|
+
console.error(e);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
25
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@damarkuncoro/layout-engine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Minimal layout engine primitives and resolvers",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"layout",
|
|
16
|
+
"ui",
|
|
17
|
+
"headless",
|
|
18
|
+
"responsive",
|
|
19
|
+
"primitives"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.json",
|
|
32
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
33
|
+
"test": "npm run build && node dist/tests/basic.test.js && node dist/tests/structures.test.js"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"typescript": "^5.4.0"
|
|
38
|
+
}
|
|
39
|
+
}
|