@tulip-systems/create-app 0.12.3 → 0.12.4

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.
Files changed (57) hide show
  1. package/dist/templates/basic/package.json +5 -5
  2. package/dist/templates/code/package.json +16 -14
  3. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-black-italic.otf +0 -0
  4. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-black.otf +0 -0
  5. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold-italic.otf +0 -0
  6. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold.otf +0 -0
  7. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light-italic.otf +0 -0
  8. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light.otf +0 -0
  9. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-light-italic.otf +0 -0
  10. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-light.otf +0 -0
  11. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.otf +0 -0
  12. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium.otf +0 -0
  13. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular-italic.otf +0 -0
  14. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular.otf +0 -0
  15. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin-italic.otf +0 -0
  16. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin.otf +0 -0
  17. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin-italic.otf +0 -0
  18. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin.otf +0 -0
  19. package/dist/templates/code/public/images/preview/hero-home.webp +0 -0
  20. package/dist/templates/code/src/app/admin/quotations/(list)/layout.tsx +24 -0
  21. package/dist/templates/code/src/app/admin/quotations/(list)/page.tsx +23 -0
  22. package/dist/templates/code/src/app/admin/quotations/[id]/(dashboard)/layout.tsx +40 -0
  23. package/dist/templates/code/src/app/admin/quotations/[id]/(dashboard)/page.tsx +56 -0
  24. package/dist/templates/code/src/app/admin/quotations/[id]/preview/page.tsx +17 -0
  25. package/dist/templates/code/src/app/admin/quotations/_blocks/commands.tsx +24 -0
  26. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/config.ts +14 -0
  27. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/create.tsx +152 -0
  28. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/inline.tsx +90 -0
  29. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/preview.tsx +90 -0
  30. package/dist/templates/code/src/app/admin/quotations/_blocks/registry.ts +13 -0
  31. package/dist/templates/code/src/app/admin/quotations/_blocks/render.tsx +190 -0
  32. package/dist/templates/code/src/app/admin/quotations/_blocks/styles.ts +24 -0
  33. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/config.ts +13 -0
  34. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/create.tsx +175 -0
  35. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/inline.tsx +93 -0
  36. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/preview.tsx +25 -0
  37. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/config.ts +18 -0
  38. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/create.tsx +177 -0
  39. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/inline.tsx +96 -0
  40. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/preview.tsx +32 -0
  41. package/dist/templates/code/src/app/admin/quotations/_components/create-command.tsx +97 -0
  42. package/dist/templates/code/src/app/admin/quotations/_components/tables.tsx +68 -0
  43. package/dist/templates/code/src/app/admin/quotations/_components/toolbars.tsx +18 -0
  44. package/dist/templates/code/src/app/admin/quotations/_config/columns-data.tsx +35 -0
  45. package/dist/templates/code/src/app/admin/quotations/_config/commands.tsx +71 -0
  46. package/dist/templates/code/src/app/admin/quotations/_config/filters.ts +22 -0
  47. package/dist/templates/code/src/app/admin/quotations/_config/sorting.ts +23 -0
  48. package/dist/templates/code/src/app/admin/quotations/_lib/router.ts +260 -0
  49. package/dist/templates/code/src/app/admin/quotations/_lib/schema.ts +42 -0
  50. package/dist/templates/code/src/app/admin/quotations/_lib/validators.ts +20 -0
  51. package/dist/templates/code/src/lib/config/paths.tsx +8 -0
  52. package/dist/templates/code/src/lib/utils/fonts.ts +102 -0
  53. package/dist/templates/code/src/server/auth/permissions.ts +4 -0
  54. package/dist/templates/code/src/server/db/schema.ts +1 -0
  55. package/dist/templates/code/src/server/router/router.ts +10 -0
  56. package/dist/templates/spark/package.json +13 -13
  57. package/package.json +1 -1
@@ -25,11 +25,11 @@
25
25
  "@orpc/tanstack-query": "^1.14.15",
26
26
  "@tailwindcss/typography": "^0.5.20",
27
27
  "@tanstack/react-query": "^5.101.4",
28
- "@tulip-systems/app": "0.12.3",
29
- "@tulip-systems/auth": "0.12.3",
30
- "@tulip-systems/commands": "0.12.3",
31
- "@tulip-systems/core": "0.12.3",
32
- "@tulip-systems/ui": "0.12.3",
28
+ "@tulip-systems/app": "0.12.4",
29
+ "@tulip-systems/auth": "0.12.4",
30
+ "@tulip-systems/commands": "0.12.4",
31
+ "@tulip-systems/core": "0.12.4",
32
+ "@tulip-systems/ui": "0.12.4",
33
33
  "@vercel/functions": "^3.9.0",
34
34
  "better-auth": "^1.6.26",
35
35
  "class-variance-authority": "^0.7.1",
@@ -30,6 +30,7 @@
30
30
  "@dnd-kit/helpers": "^0.5.0",
31
31
  "@dnd-kit/modifiers": "^9.0.0",
32
32
  "@dnd-kit/react": "^0.5.0",
33
+ "@dnd-kit/sortable": "^10.0.0",
33
34
  "@hookform/resolvers": "^5.7.1",
34
35
  "@js-temporal/polyfill": "^0.5.1",
35
36
  "@modelcontextprotocol/server": "^2.0.0",
@@ -66,20 +67,21 @@
66
67
  "@tiptap/extension-text-style": "^3.29.2",
67
68
  "@tiptap/extension-underline": "^3.29.2",
68
69
  "@tiptap/extensions": "^3.29.2",
69
- "@tulip-systems/ai": "0.12.3",
70
- "@tulip-systems/app": "0.12.3",
71
- "@tulip-systems/auth": "0.12.3",
72
- "@tulip-systems/commands": "0.12.3",
73
- "@tulip-systems/core": "0.12.3",
74
- "@tulip-systems/data-tables": "0.12.3",
75
- "@tulip-systems/drive": "0.12.3",
76
- "@tulip-systems/drive-google": "0.12.3",
77
- "@tulip-systems/editor": "0.12.3",
78
- "@tulip-systems/editor-storage": "0.12.3",
79
- "@tulip-systems/inline": "0.12.3",
80
- "@tulip-systems/query": "0.12.3",
81
- "@tulip-systems/storage": "0.12.3",
82
- "@tulip-systems/ui": "0.12.3",
70
+ "@tulip-systems/ai": "0.12.4",
71
+ "@tulip-systems/app": "0.12.4",
72
+ "@tulip-systems/auth": "0.12.4",
73
+ "@tulip-systems/blocks": "0.12.4",
74
+ "@tulip-systems/commands": "0.12.4",
75
+ "@tulip-systems/core": "0.12.4",
76
+ "@tulip-systems/data-tables": "0.12.4",
77
+ "@tulip-systems/drive": "0.12.4",
78
+ "@tulip-systems/drive-google": "0.12.4",
79
+ "@tulip-systems/editor": "0.12.4",
80
+ "@tulip-systems/editor-storage": "0.12.4",
81
+ "@tulip-systems/inline": "0.12.4",
82
+ "@tulip-systems/query": "0.12.4",
83
+ "@tulip-systems/storage": "0.12.4",
84
+ "@tulip-systems/ui": "0.12.4",
83
85
  "@uidotdev/usehooks": "^2.4.1",
84
86
  "@vercel/functions": "^3.9.0",
85
87
  "@vercel/otel": "^2.1.3",
@@ -0,0 +1,24 @@
1
+ import { Header, Topbar, TopbarTitle, TopbarTools } from "@tulip-systems/app/components/client";
2
+ import { InlineCommandMenu, InlineCommandMenuLoading } from "@tulip-systems/commands/client";
3
+ import { type PropsWithChildren, Suspense } from "react";
4
+ import { quotationCreateCommand } from "../_config/commands";
5
+
6
+ export default function QuotationsLayout({ children }: PropsWithChildren) {
7
+ return (
8
+ <>
9
+ <Header>
10
+ <Topbar>
11
+ <TopbarTitle breadcrumbs={[{ label: "Offertes" }]} />
12
+
13
+ <TopbarTools>
14
+ <Suspense fallback={<InlineCommandMenuLoading />}>
15
+ <InlineCommandMenu data={{}} commands={[quotationCreateCommand]} />
16
+ </Suspense>
17
+ </TopbarTools>
18
+ </Topbar>
19
+ </Header>
20
+
21
+ {children}
22
+ </>
23
+ );
24
+ }
@@ -0,0 +1,23 @@
1
+ import { Allowed } from "@tulip-systems/auth/permissions/client";
2
+ import { TableLayout } from "@tulip-systems/data-tables";
3
+ import { DataTable } from "@tulip-systems/data-tables/client";
4
+ import { QuotationPaginationTableProvider } from "../_components/tables";
5
+ import { QuotationTableToolbar } from "../_components/toolbars";
6
+ import { quotationColumns } from "../_config/columns-data";
7
+ import { quotationDeleteCommand } from "../_config/commands";
8
+
9
+ export default function QuotationsPage() {
10
+ return (
11
+ <Allowed permission={{ quotation: ["view"] }}>
12
+ <TableLayout className="p-content">
13
+ <QuotationPaginationTableProvider
14
+ columns={quotationColumns}
15
+ commands={[quotationDeleteCommand]}
16
+ >
17
+ <QuotationTableToolbar />
18
+ <DataTable />
19
+ </QuotationPaginationTableProvider>
20
+ </TableLayout>
21
+ </Allowed>
22
+ );
23
+ }
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { useSuspenseQuery } from "@tanstack/react-query";
4
+ import { Header, Topbar, TopbarTitle, TopbarTools } from "@tulip-systems/app/components/client";
5
+ import { InlineCommandMenu, InlineCommandMenuLoading } from "@tulip-systems/commands/client";
6
+ import { notFound } from "next/navigation";
7
+ import { Suspense, use } from "react";
8
+ import { orpc } from "@/server/router/client";
9
+ import { quotationDeleteCommand, quotationPreviewCommand } from "../../_config/commands";
10
+
11
+ export default function QuotationsLayout(props: LayoutProps<"/admin/quotations/[id]">) {
12
+ const params = use(props.params);
13
+ const id = params.id?.toString();
14
+
15
+ const { data } = useSuspenseQuery(orpc.quotations.getById.queryOptions({ input: { id } }));
16
+ if (!data) notFound();
17
+
18
+ return (
19
+ <>
20
+ <Header>
21
+ <Topbar>
22
+ <TopbarTitle
23
+ breadcrumbs={[{ label: "Offertes", href: "/admin/quotations" }, { label: data.title }]}
24
+ />
25
+
26
+ <TopbarTools>
27
+ <Suspense fallback={<InlineCommandMenuLoading />}>
28
+ <InlineCommandMenu
29
+ data={data}
30
+ commands={[quotationPreviewCommand, quotationDeleteCommand]}
31
+ />
32
+ </Suspense>
33
+ </TopbarTools>
34
+ </Topbar>
35
+ </Header>
36
+
37
+ {props.children}
38
+ </>
39
+ );
40
+ }
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import { useSuspenseQuery } from "@tanstack/react-query";
4
+ import { Allowed } from "@tulip-systems/auth/permissions/client";
5
+ import {
6
+ InlineField,
7
+ InlineFieldError,
8
+ InlineFieldLabel,
9
+ InlineStringInput,
10
+ } from "@tulip-systems/inline/client";
11
+ import { DataStackLayout } from "@tulip-systems/ui/data-stack";
12
+ import { notFound } from "next/navigation";
13
+ import { use } from "react";
14
+ import { z } from "zod";
15
+ import { orpc } from "@/server/router/client";
16
+ import { CreateQuotationBlock, RenderQuotationInlineBlocks } from "../../_blocks/render";
17
+
18
+ const quotationMutation = orpc.quotations.update.mutationOptions({
19
+ meta: { invalidates: [orpc.quotations.key()] },
20
+ });
21
+
22
+ export default function QuotationDetailPage(props: PageProps<"/admin/quotations/[id]">) {
23
+ const params = use(props.params);
24
+ const id = params.id?.toString();
25
+
26
+ const { data } = useSuspenseQuery(orpc.quotations.getById.queryOptions({ input: { id } }));
27
+ if (!data) notFound();
28
+
29
+ return (
30
+ <Allowed permission={{ quotation: ["view"] }}>
31
+ <DataStackLayout className="p-content">
32
+ <InlineField>
33
+ <InlineFieldLabel>Titel</InlineFieldLabel>
34
+ <InlineStringInput
35
+ initialValue={data.title}
36
+ variables={(value) => ({ id, data: { title: value ?? "" } })}
37
+ mutation={quotationMutation}
38
+ strategy={{ mutate: { on: "blur" }, validate: { blur: z.string().min(1) } }}
39
+ />
40
+ <InlineFieldError />
41
+ </InlineField>
42
+
43
+ <InlineField>
44
+ <InlineFieldLabel>Blokken</InlineFieldLabel>
45
+
46
+ <div className="flex flex-col gap-2">
47
+ <RenderQuotationInlineBlocks quotationId={id} />
48
+ <CreateQuotationBlock quotationId={id} />
49
+ </div>
50
+
51
+ <InlineFieldError />
52
+ </InlineField>
53
+ </DataStackLayout>
54
+ </Allowed>
55
+ );
56
+ }
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import { useSuspenseQuery } from "@tanstack/react-query";
4
+ import { notFound } from "next/navigation";
5
+ import { use } from "react";
6
+ import { orpc } from "@/server/router/client";
7
+ import { RenderQuotationPreview } from "../../_blocks/render";
8
+
9
+ export default function QuotationPreviewPage(props: PageProps<"/admin/quotations/[id]/preview">) {
10
+ const params = use(props.params);
11
+ const id = params.id?.toString();
12
+
13
+ const { data } = useSuspenseQuery(orpc.quotations.getById.queryOptions({ input: { id } }));
14
+ if (!data) notFound();
15
+
16
+ return <RenderQuotationPreview quotationId={id} />;
17
+ }
@@ -0,0 +1,24 @@
1
+ "use client";
2
+
3
+ import { commandBuilder } from "@tulip-systems/commands";
4
+ import { DeleteCommand } from "@tulip-systems/commands/client";
5
+ import { toast } from "@tulip-systems/ui/sonner";
6
+ import { orpc } from "@/server/router/client";
7
+ import type { QuotationBlock } from "./registry";
8
+
9
+ type QuotationBlockCommandItem = Pick<QuotationBlock, "id">;
10
+
11
+ export const quotationBlockDeleteCommand = commandBuilder
12
+ .$type<QuotationBlockCommandItem>()
13
+ .permission({ quotation: ["update"] })
14
+ .render(({ data }) => (
15
+ <DeleteCommand
16
+ variables={{ id: data.id }}
17
+ mutation={orpc.quotationBlocks.delete.mutationOptions({
18
+ meta: { invalidates: [orpc.quotationBlocks.key()] },
19
+ onSuccess: () => toast.success("Blok succesvol verwijderd"),
20
+ })}
21
+ />
22
+ ));
23
+
24
+ export const quotationInlineBlockCommands = [quotationBlockDeleteCommand];
@@ -0,0 +1,14 @@
1
+ import type { BlockDefinition } from "@tulip-systems/blocks";
2
+ import { z } from "zod";
3
+
4
+ export const quotationHeroBlock = {
5
+ slug: "hero",
6
+ label: "Hero",
7
+ description: "Homepage hero title, subtitle, and supporting copy.",
8
+ schema: z.object({
9
+ media: z.unknown().optional(),
10
+ titlePart1: z.string().min(1),
11
+ titlePart2: z.string().optional(),
12
+ description: z.string().optional(),
13
+ }),
14
+ } as const satisfies BlockDefinition;
@@ -0,0 +1,152 @@
1
+ "use client";
2
+
3
+ import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
4
+ import type { DefaultError } from "@tanstack/react-query";
5
+ import type { BlockCreateComponentProps } from "@tulip-systems/blocks";
6
+ import {
7
+ CreateBlock,
8
+ CreateBlockCancel,
9
+ CreateBlockContent,
10
+ CreateBlockDescription,
11
+ CreateBlockFields,
12
+ CreateBlockFooter,
13
+ CreateBlockHeader,
14
+ CreateBlockSubmit,
15
+ CreateBlockTitle,
16
+ CreateBlockTrigger,
17
+ CreateBlockTriggerContent,
18
+ CreateBlockTriggerDescription,
19
+ CreateBlockTriggerIcon,
20
+ CreateBlockTriggerTitle,
21
+ } from "@tulip-systems/blocks/client";
22
+ import { Input } from "@tulip-systems/ui/input";
23
+ import {
24
+ Form,
25
+ FormControl,
26
+ FormField,
27
+ FormItem,
28
+ FormLabel,
29
+ FormMessage,
30
+ } from "@tulip-systems/ui/react-hook-form";
31
+ import { Textarea } from "@tulip-systems/ui/textarea";
32
+ import { BlocksIcon } from "lucide-react";
33
+ import { useForm } from "react-hook-form";
34
+ import type { z } from "zod";
35
+ import { quotationHeroBlock } from "./config";
36
+
37
+ export type QuotationHeroCreateValues = z.input<typeof quotationHeroBlock.schema>;
38
+
39
+ type QuotationHeroCreateBlockInput = {
40
+ slug: typeof quotationHeroBlock.slug;
41
+ data: QuotationHeroCreateValues;
42
+ };
43
+
44
+ type QuotationHeroCreateBlockProps<
45
+ TData = unknown,
46
+ TError = DefaultError,
47
+ TVariables = undefined,
48
+ TOnMutateResult = unknown,
49
+ > = BlockCreateComponentProps<
50
+ QuotationHeroCreateBlockInput,
51
+ TData,
52
+ TError,
53
+ TVariables,
54
+ TOnMutateResult
55
+ >;
56
+
57
+ export function QuotationHeroCreateBlock<
58
+ TData = unknown,
59
+ TError = DefaultError,
60
+ TVariables = undefined,
61
+ TOnMutateResult = unknown,
62
+ >({
63
+ variables,
64
+ mutation,
65
+ }: QuotationHeroCreateBlockProps<TData, TError, TVariables, TOnMutateResult>) {
66
+ const form = useForm<QuotationHeroCreateValues>({
67
+ resolver: standardSchemaResolver(quotationHeroBlock.schema),
68
+ });
69
+
70
+ return (
71
+ <CreateBlock>
72
+ <CreateBlockTrigger>
73
+ <CreateBlockTriggerIcon>
74
+ <BlocksIcon />
75
+ </CreateBlockTriggerIcon>
76
+
77
+ <CreateBlockTriggerContent>
78
+ <CreateBlockTriggerTitle>{quotationHeroBlock.label}</CreateBlockTriggerTitle>
79
+ <CreateBlockTriggerDescription>
80
+ {quotationHeroBlock.description}
81
+ </CreateBlockTriggerDescription>
82
+ </CreateBlockTriggerContent>
83
+ </CreateBlockTrigger>
84
+
85
+ <Form {...form}>
86
+ <CreateBlockContent
87
+ className="sm:max-w-xl"
88
+ variables={(data: QuotationHeroCreateValues) =>
89
+ variables({ slug: quotationHeroBlock.slug, data })
90
+ }
91
+ mutation={mutation}
92
+ >
93
+ <CreateBlockHeader>
94
+ <CreateBlockTitle>{quotationHeroBlock.label} toevoegen</CreateBlockTitle>
95
+ <CreateBlockDescription>
96
+ Vul de verplichte velden in om het blok toe te voegen.
97
+ </CreateBlockDescription>
98
+ </CreateBlockHeader>
99
+
100
+ <CreateBlockFields className="grid gap-4 md:grid-cols-2">
101
+ <FormField
102
+ control={form.control}
103
+ name="titlePart1"
104
+ render={({ field }) => (
105
+ <FormItem>
106
+ <FormLabel>Titel deel 1</FormLabel>
107
+ <FormControl>
108
+ <Input {...field} />
109
+ </FormControl>
110
+ <FormMessage />
111
+ </FormItem>
112
+ )}
113
+ />
114
+
115
+ <FormField
116
+ control={form.control}
117
+ name="titlePart2"
118
+ render={({ field }) => (
119
+ <FormItem>
120
+ <FormLabel>Titel deel 2</FormLabel>
121
+ <FormControl>
122
+ <Input {...field} />
123
+ </FormControl>
124
+ <FormMessage />
125
+ </FormItem>
126
+ )}
127
+ />
128
+
129
+ <FormField
130
+ control={form.control}
131
+ name="description"
132
+ render={({ field }) => (
133
+ <FormItem className="md:col-span-2">
134
+ <FormLabel>Beschrijving</FormLabel>
135
+ <FormControl>
136
+ <Textarea {...field} />
137
+ </FormControl>
138
+ <FormMessage />
139
+ </FormItem>
140
+ )}
141
+ />
142
+ </CreateBlockFields>
143
+
144
+ <CreateBlockFooter>
145
+ <CreateBlockCancel>Annuleren</CreateBlockCancel>
146
+ <CreateBlockSubmit>Blok toevoegen</CreateBlockSubmit>
147
+ </CreateBlockFooter>
148
+ </CreateBlockContent>
149
+ </Form>
150
+ </CreateBlock>
151
+ );
152
+ }
@@ -0,0 +1,90 @@
1
+ "use client";
2
+
3
+ import type { BlockComponentProps } from "@tulip-systems/blocks";
4
+ import {
5
+ InlineBlock,
6
+ InlineBlockContent,
7
+ InlineBlockDragHandle,
8
+ InlineBlockHeader,
9
+ InlineBlockSortable,
10
+ InlineBlockTrigger,
11
+ } from "@tulip-systems/blocks/client";
12
+ import { DropdownCommandMenu } from "@tulip-systems/commands/client";
13
+ import {
14
+ InlineField,
15
+ InlineFieldError,
16
+ InlineFieldLabel,
17
+ InlineTextarea,
18
+ } from "@tulip-systems/inline/client";
19
+ import { DataStackLayout, DataStackRow } from "@tulip-systems/ui/data-stack";
20
+ import { GripIcon } from "lucide-react";
21
+ import { z } from "zod";
22
+ import { orpc } from "@/server/router/client";
23
+ import { quotationInlineBlockCommands } from "../commands";
24
+ import { quotationHeroBlock } from "./config";
25
+
26
+ const heroMutation = orpc.quotationBlocks.update.mutationOptions({
27
+ meta: { invalidates: [orpc.quotationBlocks.key()] },
28
+ });
29
+
30
+ export function QuotationHeroInlineBlock({
31
+ block,
32
+ }: BlockComponentProps<typeof quotationHeroBlock>) {
33
+ return (
34
+ <InlineBlockSortable id={block.id}>
35
+ <InlineBlock>
36
+ <InlineBlockHeader>
37
+ <InlineBlockDragHandle>
38
+ <GripIcon className="h-4 w-4" />
39
+ <span className="sr-only">Blok verplaatsen</span>
40
+ </InlineBlockDragHandle>
41
+ <InlineBlockTrigger>{quotationHeroBlock.label}</InlineBlockTrigger>
42
+ <DropdownCommandMenu
43
+ data={{ id: block.id }}
44
+ commands={quotationInlineBlockCommands}
45
+ className="ml-2 size-7"
46
+ />
47
+ </InlineBlockHeader>
48
+
49
+ <InlineBlockContent>
50
+ <DataStackLayout>
51
+ <DataStackRow>
52
+ <InlineField>
53
+ <InlineFieldLabel>Titel deel 1</InlineFieldLabel>
54
+ <InlineTextarea
55
+ initialValue={block.data.titlePart1}
56
+ variables={(value) => ({ id: block.id, data: { titlePart1: value ?? "" } })}
57
+ mutation={heroMutation}
58
+ strategy={{ mutate: { on: "blur" }, validate: { blur: z.string().min(1) } }}
59
+ />
60
+ <InlineFieldError />
61
+ </InlineField>
62
+
63
+ <InlineField>
64
+ <InlineFieldLabel>Titel deel 2</InlineFieldLabel>
65
+ <InlineTextarea
66
+ initialValue={block.data.titlePart2 ?? ""}
67
+ variables={(value) => ({ id: block.id, data: { titlePart2: value ?? "" } })}
68
+ mutation={heroMutation}
69
+ strategy={{ mutate: { on: "blur" }, validate: { blur: z.string() } }}
70
+ />
71
+ <InlineFieldError />
72
+ </InlineField>
73
+ </DataStackRow>
74
+
75
+ <InlineField className="md:col-span-2">
76
+ <InlineFieldLabel>Beschrijving</InlineFieldLabel>
77
+ <InlineTextarea
78
+ initialValue={block.data.description ?? ""}
79
+ variables={(value) => ({ id: block.id, data: { description: value ?? "" } })}
80
+ mutation={heroMutation}
81
+ strategy={{ mutate: { on: "blur" }, validate: { blur: z.string() } }}
82
+ />
83
+ <InlineFieldError />
84
+ </InlineField>
85
+ </DataStackLayout>
86
+ </InlineBlockContent>
87
+ </InlineBlock>
88
+ </InlineBlockSortable>
89
+ );
90
+ }
@@ -0,0 +1,90 @@
1
+ import type { BlockComponentProps } from "@tulip-systems/blocks";
2
+ import { cn } from "@tulip-systems/ui/cn";
3
+ import Image from "next/image";
4
+ import { typographyVariants } from "../styles";
5
+ import type { quotationHeroBlock } from "./config";
6
+
7
+ export async function QuotationHeroPreview({
8
+ block,
9
+ }: BlockComponentProps<typeof quotationHeroBlock>) {
10
+ const { titlePart1, titlePart2, description } = block.data;
11
+
12
+ const fullTitle = [titlePart1, titlePart2].filter(Boolean).join(" ");
13
+
14
+ return (
15
+ <section className="relative flex min-h-svh items-stretch overflow-hidden bg-background px-5 pt-10 pb-10 md:gap-27 md:px-10 lg:items-stretch">
16
+ {fullTitle && <h1 className="sr-only">{fullTitle}</h1>}
17
+
18
+ <div className="absolute inset-0 z-0">
19
+ <Image
20
+ src="/images/preview/hero-home.webp"
21
+ alt="Hero home"
22
+ className="object-cover"
23
+ fill
24
+ sizes="100vw"
25
+ preload
26
+ />
27
+ </div>
28
+
29
+ <div className="relative z-10 flex w-full flex-col gap-2 lg:flex-row lg:justify-between lg:gap-10">
30
+ {/* Right Side — `grow lg:grow-0` on mobile so this column fills the
31
+ inner div's vertical space (inner div itself is stretched by the
32
+ section's `items-stretch`), which lets `justify-between` actually
33
+ push the title-group to the top and the description+buttons to
34
+ the bottom. On desktop the parent is `flex-row`; there we don't
35
+ want horizontal growth so we reset with `lg:grow-0`. */}
36
+ <div className="flex grow flex-col justify-between gap-5 lg:grow-0 lg:gap-0">
37
+ {/* Title group — subTitle + both title parts stack here so that the
38
+ right side div only sees TWO children (title-group and the
39
+ description+buttons block). With 3 children, `justify-between`
40
+ would strand titlePart2 in the middle. */}
41
+ <div className="flex flex-col gap-5 lg:gap-7.5">
42
+ {titlePart1 && (
43
+ <p
44
+ aria-hidden="true"
45
+ className={cn("lg:wrap-anywhere", typographyVariants({ variant: "title-large" }))}
46
+ >
47
+ {titlePart1}
48
+ </p>
49
+ )}
50
+
51
+ {titlePart2 && (
52
+ <p
53
+ aria-hidden="true"
54
+ className={cn(
55
+ "lg:wrap-anywhere text-right lg:hidden",
56
+ typographyVariants({ variant: "title-large" }),
57
+ )}
58
+ >
59
+ {titlePart2}
60
+ </p>
61
+ )}
62
+ </div>
63
+
64
+ <div className="flex flex-col gap-10 pt-5 lg:pt-0">
65
+ {description && (
66
+ <p className={cn("max-w-md", typographyVariants({ variant: "body-small" }))}>
67
+ {description}
68
+ </p>
69
+ )}
70
+ </div>
71
+ </div>
72
+
73
+ {/* Left Side - Desktop */}
74
+ <div className="hidden flex-col gap-27.5 lg:flex lg:items-end lg:self-end">
75
+ {titlePart2 && (
76
+ <p
77
+ aria-hidden="true"
78
+ className={cn(
79
+ "lg:wrap-anywhere text-right",
80
+ typographyVariants({ variant: "title-large" }),
81
+ )}
82
+ >
83
+ {titlePart2}
84
+ </p>
85
+ )}
86
+ </div>
87
+ </div>
88
+ </section>
89
+ );
90
+ }
@@ -0,0 +1,13 @@
1
+ import { createBlockRegistry, type InferBlock, type InferBlockInput } from "@tulip-systems/blocks";
2
+ import { quotationHeroBlock } from "./hero/config";
3
+ import { quotationSummaryBlock } from "./summary/config";
4
+ import { quotationTotalsBlock } from "./totals/config";
5
+
6
+ export const quotationBlocks = createBlockRegistry([
7
+ quotationHeroBlock,
8
+ quotationSummaryBlock,
9
+ quotationTotalsBlock,
10
+ ]);
11
+
12
+ export type QuotationBlock = InferBlock<typeof quotationBlocks>;
13
+ export type QuotationBlockInput = InferBlockInput<typeof quotationBlocks>;