Skip to content

Commit 46b84d9

Browse files
committed
WEB-111 Add responsive styling system with breakpoint selection
1 parent d9cccdc commit 46b84d9

25 files changed

Lines changed: 1081 additions & 585 deletions

prisma/seed.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import "dotenv/config";
2-
import { Prisma, PrismaClient } from "../src/generated/prisma/client";
2+
import { PrismaClient } from "../src/generated/prisma/client";
33
import { PrismaPg } from "@prisma/adapter-pg";
44
import fs from "fs";
55
import path from "path";
6+
import type { Prisma } from "../src/generated/prisma/client";
67

78
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
89
const prisma = new PrismaClient({ adapter });

src/app/editor/[id]/[slug]/EditorPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { notFound } from "next/navigation";
55
import { Client } from "./client";
66
import { getDocumentById } from "../../../../lib/documents/queries";
77
import { getEditorSlug, getEditorUrl } from "../../../../lib/editor-url";
8+
import { createEmptyPuckData } from "../../../../lib/puck/utils";
89

910
export default async function EditorPage({
1011
documentId,
@@ -63,5 +64,5 @@ function resolveVersion(
6364
return { data: versions[0].content as Data, versionId: versions[0].id };
6465
}
6566

66-
return { data: { content: [], root: {} } as Data };
67+
return { data: createEmptyPuckData() };
6768
}

src/app/editor/[id]/[slug]/client.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,12 @@ export function Client({
4545
}: {
4646
documentId: number;
4747
documentName: string;
48-
data: Partial<Data>;
48+
data: Data;
4949
versionId?: number;
5050
publishedVersionId?: number;
5151
versions: Version[];
5252
isArchived: boolean;
5353
}) {
54-
const [currentData, setCurrentData] = useState<Data>(data as Data);
5554
const [versions, setVersions] = useState(initialVersions);
5655
const [versionId, setVersionId] = useState(initialVersionId);
5756
const [publishedVersionId, setPublishedVersionId] = useState(initialPublishedVersionId);
@@ -67,7 +66,7 @@ export function Client({
6766
>
6867
<Puck
6968
config={config}
70-
data={currentData}
69+
data={data}
7170
ui={{plugin: {current: "version-plugin"}}}
7271
plugins={[VersionPlugin, blocksPlugin(), outlinePlugin()]}
7372
permissions={isArchived
@@ -78,9 +77,6 @@ export function Client({
7877
actionBar: ActionBarOverride,
7978
headerActions: SaveButton
8079
}}
81-
onChange={(data) => {
82-
setCurrentData(data);
83-
}}
8480
/>
8581
</DocumentContext.Provider>
8682
);

src/app/styles.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
@import "tw-animate-css";
33
@import "shadcn/tailwind.css";
44

5+
@source inline("{md:,lg:,}p-{0,1,2,4,6,8,12}");
6+
@source inline("{md:,lg:,}gap-{0,1,2,4,6,8,12}");
7+
@source inline("{md:,lg:,}grid-cols-{1,2,3,4,5,6}");
8+
@source inline("{md:,lg:,}grid-rows-{none,1,2,3,4,5,6}");
9+
@source inline("{md:,lg:,}auto-rows-{auto,[0]}");
10+
@source inline("{md:,lg:,}overflow-{visible,hidden}");
11+
512
@custom-variant dark (&:is(.dark *));
613

714
@theme {

src/components/puck/button.tsx

Lines changed: 27 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
import type { ComponentConfig } from "@puckeditor/core";
22
import { Button } from "@/components/ui/button";
3-
import { selectFrom } from "@/lib/puck/tokens";
3+
import { defineProps, field } from "@/components/puck/define-props";
4+
import { defineToken, type TokenValue } from "@/lib/puck/tokens";
45

5-
const variantOptions = [
6-
"default",
7-
"secondary",
8-
"outline",
9-
"ghost",
10-
"destructive",
11-
"link",
12-
] as const;
13-
type Variant = (typeof variantOptions)[number];
6+
const variant = defineToken({
7+
default: "Default",
8+
secondary: "Secondary",
9+
outline: "Outline",
10+
ghost: "Ghost",
11+
destructive: "Destructive",
12+
link: "Link",
13+
});
14+
type Variant = TokenValue<typeof variant>;
1415

15-
const sizeOptions = ["sm", "default", "lg"] as const;
16-
type Size = (typeof sizeOptions)[number];
16+
const size = defineToken({
17+
sm: "Small",
18+
default: "Default",
19+
lg: "Large",
20+
}, "default");
21+
type Size = TokenValue<typeof size>;
1722

1823
type PuckButtonProps = {
1924
label: string;
@@ -22,31 +27,19 @@ type PuckButtonProps = {
2227
size: Size;
2328
};
2429

30+
const props = defineProps({
31+
label: field.raw({ type: "text", label: "Label" } as const, "Click me"),
32+
href: field.raw({ type: "text", label: "Link URL" } as const, ""),
33+
variant: field.select(variant, { label: "Variant" }),
34+
size: field.select(size, { label: "Size" }),
35+
});
36+
2537
export const PuckButton: ComponentConfig<PuckButtonProps> = {
2638
label: "Button",
27-
fields: {
28-
label: { type: "text", label: "Label" },
29-
href: { type: "text", label: "Link URL" },
30-
variant: selectFrom(variantOptions, "Variant"),
31-
size: {
32-
type: "select",
33-
label: "Size",
34-
options: [
35-
{ label: "Small", value: "sm" },
36-
{ label: "Default", value: "default" },
37-
{ label: "Large", value: "lg" },
38-
],
39-
},
40-
},
41-
defaultProps: {
42-
label: "Click me",
43-
href: "",
44-
variant: "default",
45-
size: "default",
46-
},
47-
render: ({ label, href, variant, size }) => {
39+
...props,
40+
render: ({ label, href, variant: v, size: s }) => {
4841
const button = (
49-
<Button variant={variant} size={size}>
42+
<Button variant={v} size={s}>
5043
{label}
5144
</Button>
5245
);

src/components/puck/columns.tsx

Lines changed: 39 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,59 @@
11
import type { ComponentConfig } from "@puckeditor/core";
2-
import { cva } from "class-variance-authority";
3-
import { cn } from "@/lib/utils";
4-
import { fields, gapVariants, type Spacing } from "@/lib/puck/tokens";
2+
import { defineProps, responsive, field } from "@/components/puck/define-props";
3+
import { columnCount, spacing, type ColumnCount, type Spacing } from "@/lib/puck/tokens";
4+
import type { ResponsiveValue } from "@/lib/puck/responsive";
5+
import { getGridClassName, getMaxCols } from "@/components/puck/layout";
56

6-
const columnValues = ["2", "3", "4", "5", "6"] as const;
7-
type ColumnCount = (typeof columnValues)[number];
8-
9-
const columnsVariants = cva("grid", {
10-
variants: {
11-
columns: {
12-
"2": "grid-cols-1 md:grid-cols-2",
13-
"3": "grid-cols-1 md:grid-cols-3",
14-
"4": "grid-cols-1 md:grid-cols-2 lg:grid-cols-4",
15-
"5": "grid-cols-1 md:grid-cols-3 lg:grid-cols-5",
16-
"6": "grid-cols-1 md:grid-cols-3 lg:grid-cols-6",
17-
},
18-
gap: gapVariants,
19-
},
20-
defaultVariants: {
21-
columns: "2",
22-
gap: "md",
23-
},
24-
});
7+
type SlotKey = `column${ColumnCount}`;
258

269
type ColumnsProps = {
27-
column1: any;
28-
column2: any;
29-
column3: any;
30-
column4: any;
31-
column5: any;
32-
column6: any;
33-
columns: ColumnCount;
34-
gap: Spacing;
10+
[K in SlotKey]: any;
11+
} & {
12+
columns: ResponsiveValue<ColumnCount>;
13+
gap: ResponsiveValue<Spacing>;
3514
};
3615

37-
const slotField = { type: "slot" as const };
16+
const slotField = { type: "slot" } as const;
17+
const columnSlotKeys = columnCount.options.map(
18+
({ value }) => `column${value}` as SlotKey,
19+
);
20+
21+
const columnSlotFields = Object.fromEntries(
22+
columnSlotKeys.map((key) => [key, slotField]),
23+
) as Record<SlotKey, typeof slotField>;
24+
25+
const props = defineProps({
26+
columns: responsive.token(columnCount, { label: "Columns", default: { base: "1", md: "2" } }),
27+
gap: responsive.token(spacing, { label: "Gap", default: "md" }),
28+
});
3829

3930
export const Columns: ComponentConfig<ColumnsProps> = {
4031
label: "Columns",
41-
fields: {
42-
column1: slotField,
43-
column2: slotField,
44-
column3: slotField,
45-
column4: slotField,
46-
column5: slotField,
47-
column6: slotField,
48-
columns: {
49-
type: "select",
50-
label: "Columns",
51-
options: columnValues.map((v) => ({ label: v, value: v })),
52-
},
53-
gap: fields.gap(),
54-
},
55-
defaultProps: {
56-
columns: "2",
57-
gap: "md",
58-
} as ColumnsProps,
32+
fields: { ...columnSlotFields, ...props.fields },
33+
defaultProps: props.defaultProps as ColumnsProps,
34+
// Hide column slot fields that exceed the selected column count,
35+
// so the editor sidebar only shows slots that are actually rendered.
5936
resolveFields: (data) => {
6037
const f = { ...Columns.fields! };
61-
const colCount = parseInt(data.props.columns ?? "2", 10);
38+
const columns = data.props.columns ?? { base: "1", md: "2" };
39+
const maxCols = getMaxCols(columns);
6240

63-
f.column1 = { ...f.column1, visible: colCount >= 1 };
64-
f.column2 = { ...f.column2, visible: colCount >= 2 };
65-
f.column3 = { ...f.column3, visible: colCount >= 3 };
66-
f.column4 = { ...f.column4, visible: colCount >= 4 };
67-
f.column5 = { ...f.column5, visible: colCount >= 5 };
68-
f.column6 = { ...f.column6, visible: colCount >= 6 };
41+
for (const [index, key] of columnSlotKeys.entries()) {
42+
f[key] = { ...f[key], visible: index < maxCols };
43+
}
6944

7045
return f;
7146
},
72-
render: ({
73-
column1: Column1,
74-
column2: Column2,
75-
column3: Column3,
76-
column4: Column4,
77-
column5: Column5,
78-
column6: Column6,
79-
columns,
80-
gap,
81-
}) => {
82-
const colCount = parseInt(columns ?? "2", 10);
83-
const columnSlots = [Column1, Column2, Column3, Column4, Column5, Column6];
47+
render: ({ columns, gap, ...slots }) => {
48+
const maxCols = getMaxCols(columns);
49+
const slotMap = slots as Record<SlotKey, any>;
8450

8551
return (
86-
<div className={cn(columnsVariants({ columns, gap }))}>
87-
{columnSlots
88-
.slice(0, colCount)
89-
.map((Col, i) => Col && <Col key={i} />)}
52+
<div className={getGridClassName({ columns, rows: { base: "auto" }, gap })}>
53+
{columnSlotKeys.slice(0, maxCols).map((key) => {
54+
const Col = slotMap[key];
55+
return Col && <Col key={key} />;
56+
})}
9057
</div>
9158
);
9259
},

0 commit comments

Comments
 (0)