@guard-rail/core 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.
Files changed (111) hide show
  1. package/README.md +339 -0
  2. package/dist/adapters/supabase/auth.d.ts +9 -0
  3. package/dist/adapters/supabase/auth.js +57 -0
  4. package/dist/adapters/supabase/auth.js.map +1 -0
  5. package/dist/adapters/supabase/client.d.ts +28 -0
  6. package/dist/adapters/supabase/client.js +44 -0
  7. package/dist/adapters/supabase/client.js.map +1 -0
  8. package/dist/adapters/supabase/confirm.d.ts +19 -0
  9. package/dist/adapters/supabase/confirm.js +27 -0
  10. package/dist/adapters/supabase/confirm.js.map +1 -0
  11. package/dist/adapters/supabase/db.d.ts +9 -0
  12. package/dist/adapters/supabase/db.js +49 -0
  13. package/dist/adapters/supabase/db.js.map +1 -0
  14. package/dist/adapters/supabase/index.d.ts +25 -0
  15. package/dist/adapters/supabase/index.js +29 -0
  16. package/dist/adapters/supabase/index.js.map +1 -0
  17. package/dist/adapters/supabase/members.d.ts +9 -0
  18. package/dist/adapters/supabase/members.js +73 -0
  19. package/dist/adapters/supabase/members.js.map +1 -0
  20. package/dist/adapters/supabase/repeaters.d.ts +9 -0
  21. package/dist/adapters/supabase/repeaters.js +73 -0
  22. package/dist/adapters/supabase/repeaters.js.map +1 -0
  23. package/dist/adapters/supabase/storage.d.ts +18 -0
  24. package/dist/adapters/supabase/storage.js +93 -0
  25. package/dist/adapters/supabase/storage.js.map +1 -0
  26. package/dist/core/adapter-types.d.ts +212 -0
  27. package/dist/core/adapter-types.js +1 -0
  28. package/dist/core/adapter-types.js.map +1 -0
  29. package/dist/core/bailout.d.ts +9 -0
  30. package/dist/core/bailout.js +14 -0
  31. package/dist/core/bailout.js.map +1 -0
  32. package/dist/core/config.d.ts +27 -0
  33. package/dist/core/config.js +46 -0
  34. package/dist/core/config.js.map +1 -0
  35. package/dist/core/defs.d.ts +22 -0
  36. package/dist/core/defs.js +16 -0
  37. package/dist/core/defs.js.map +1 -0
  38. package/dist/core/types.d.ts +136 -0
  39. package/dist/core/types.js +1 -0
  40. package/dist/core/types.js.map +1 -0
  41. package/dist/index.d.ts +13 -0
  42. package/dist/index.js +43 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/react/Admin.d.ts +8 -0
  45. package/dist/react/Admin.js +116 -0
  46. package/dist/react/Admin.js.map +1 -0
  47. package/dist/react/GRail/client.d.ts +11 -0
  48. package/dist/react/GRail/client.js +242 -0
  49. package/dist/react/GRail/client.js.map +1 -0
  50. package/dist/react/GRail/index.d.ts +12 -0
  51. package/dist/react/GRail/index.js +19 -0
  52. package/dist/react/GRail/index.js.map +1 -0
  53. package/dist/react/GRail/render.d.ts +23 -0
  54. package/dist/react/GRail/render.js +67 -0
  55. package/dist/react/GRail/render.js.map +1 -0
  56. package/dist/react/Icons.d.ts +13 -0
  57. package/dist/react/Icons.js +68 -0
  58. package/dist/react/Icons.js.map +1 -0
  59. package/dist/react/MediaPicker.d.ts +14 -0
  60. package/dist/react/MediaPicker.js +172 -0
  61. package/dist/react/MediaPicker.js.map +1 -0
  62. package/dist/react/Provider.d.ts +11 -0
  63. package/dist/react/Provider.js +26 -0
  64. package/dist/react/Provider.js.map +1 -0
  65. package/dist/react/Repeater/client.d.ts +16 -0
  66. package/dist/react/Repeater/client.js +148 -0
  67. package/dist/react/Repeater/client.js.map +1 -0
  68. package/dist/react/Repeater/index.d.ts +35 -0
  69. package/dist/react/Repeater/index.js +22 -0
  70. package/dist/react/Repeater/index.js.map +1 -0
  71. package/dist/react/Syncer.d.ts +17 -0
  72. package/dist/react/Syncer.js +23 -0
  73. package/dist/react/Syncer.js.map +1 -0
  74. package/dist/react/Team.d.ts +7 -0
  75. package/dist/react/Team.js +190 -0
  76. package/dist/react/Team.js.map +1 -0
  77. package/dist/react/resizeImage.d.ts +25 -0
  78. package/dist/react/resizeImage.js +52 -0
  79. package/dist/react/resizeImage.js.map +1 -0
  80. package/dist/react/styles.d.ts +421 -0
  81. package/dist/react/styles.js +402 -0
  82. package/dist/react/styles.js.map +1 -0
  83. package/dist/react/useAnchorRect.d.ts +17 -0
  84. package/dist/react/useAnchorRect.js +32 -0
  85. package/dist/react/useAnchorRect.js.map +1 -0
  86. package/dist/server/actions.d.ts +41 -0
  87. package/dist/server/actions.js +214 -0
  88. package/dist/server/actions.js.map +1 -0
  89. package/dist/server/data.d.ts +10 -0
  90. package/dist/server/data.js +24 -0
  91. package/dist/server/data.js.map +1 -0
  92. package/dist/server/fieldData.d.ts +26 -0
  93. package/dist/server/fieldData.js +9 -0
  94. package/dist/server/fieldData.js.map +1 -0
  95. package/dist/server/loader.d.ts +6 -0
  96. package/dist/server/loader.js +53 -0
  97. package/dist/server/loader.js.map +1 -0
  98. package/dist/server/repeaterActions.d.ts +20 -0
  99. package/dist/server/repeaterActions.js +72 -0
  100. package/dist/server/repeaterActions.js.map +1 -0
  101. package/dist/server/repeaterData.d.ts +18 -0
  102. package/dist/server/repeaterData.js +27 -0
  103. package/dist/server/repeaterData.js.map +1 -0
  104. package/dist/server/session.d.ts +14 -0
  105. package/dist/server/session.js +37 -0
  106. package/dist/server/session.js.map +1 -0
  107. package/dist/server/wrapper.d.ts +12 -0
  108. package/dist/server/wrapper.js +42 -0
  109. package/dist/server/wrapper.js.map +1 -0
  110. package/package.json +58 -0
  111. package/supabase/migrations/20260911000000_guardrail_init.sql +288 -0
@@ -0,0 +1,172 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import React, { useCallback, useEffect, useRef, useState } from "react";
4
+ import { createAssetUpload, listAssets } from "../server/actions";
5
+ import { CloseIcon, SpinnerIcon } from "./Icons";
6
+ import { S } from "./styles";
7
+ import { prepareImage } from "./resizeImage";
8
+ function measure(url) {
9
+ return new Promise((resolve) => {
10
+ const img = new Image();
11
+ img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });
12
+ img.onerror = () => resolve({});
13
+ img.src = url;
14
+ });
15
+ }
16
+ function MediaPicker({
17
+ current,
18
+ onSelect,
19
+ onClose
20
+ }) {
21
+ const [assets, setAssets] = useState(null);
22
+ const [uploading, setUploading] = useState(false);
23
+ const [dragging, setDragging] = useState(false);
24
+ const [error, setError] = useState(null);
25
+ const inputRef = useRef(null);
26
+ useEffect(() => {
27
+ void listAssets().then(setAssets);
28
+ }, []);
29
+ useEffect(() => {
30
+ const onKey = (e) => {
31
+ if (e.key !== "Escape") return;
32
+ e.stopPropagation();
33
+ onClose();
34
+ };
35
+ window.addEventListener("keydown", onKey, true);
36
+ return () => window.removeEventListener("keydown", onKey, true);
37
+ }, [onClose]);
38
+ const choose = useCallback(
39
+ (src, size) => {
40
+ onSelect({ src, ...size });
41
+ onClose();
42
+ },
43
+ [onSelect, onClose]
44
+ );
45
+ const upload = async (file) => {
46
+ if (uploading) return;
47
+ setError(null);
48
+ setUploading(true);
49
+ try {
50
+ const prepared = await prepareImage(file);
51
+ const payload = prepared.file;
52
+ const res = await createAssetUpload({
53
+ name: payload.name,
54
+ contentType: payload.type,
55
+ size: payload.size
56
+ });
57
+ if (!res.success) {
58
+ setError(res.error);
59
+ return;
60
+ }
61
+ const { target } = res;
62
+ const put = await fetch(target.url, {
63
+ method: target.method,
64
+ headers: target.headers,
65
+ body: payload
66
+ });
67
+ if (!put.ok) {
68
+ setError(`Upload failed (${put.status}).`);
69
+ return;
70
+ }
71
+ setAssets((prev) => [target.asset, ...prev ?? []]);
72
+ choose(target.asset.url, { width: prepared.width, height: prepared.height });
73
+ } catch {
74
+ setError("Upload failed. Check your connection and try again.");
75
+ } finally {
76
+ setUploading(false);
77
+ }
78
+ };
79
+ const onFiles = (files) => {
80
+ const file = files?.[0];
81
+ if (file) void upload(file);
82
+ };
83
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
84
+ /* @__PURE__ */ jsx("div", { "data-guardrail": true, style: S.pickerScrim, onClick: onClose }),
85
+ /* @__PURE__ */ jsxs(
86
+ "div",
87
+ {
88
+ "data-guardrail": true,
89
+ role: "dialog",
90
+ "aria-label": "Choose an image",
91
+ style: S.picker,
92
+ onClick: (e) => e.stopPropagation(),
93
+ children: [
94
+ /* @__PURE__ */ jsxs("header", { style: S.drawerHead, children: [
95
+ /* @__PURE__ */ jsx("h2", { style: S.drawerTitle, children: "Choose an image" }),
96
+ /* @__PURE__ */ jsx(
97
+ "button",
98
+ {
99
+ style: { ...S.iconBtn, color: "#6b7280" },
100
+ onClick: onClose,
101
+ "aria-label": "Close",
102
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
103
+ }
104
+ )
105
+ ] }),
106
+ /* @__PURE__ */ jsxs("div", { style: S.drawerBody, children: [
107
+ /* @__PURE__ */ jsxs(
108
+ "div",
109
+ {
110
+ role: "button",
111
+ tabIndex: 0,
112
+ style: { ...S.dropzone, ...dragging ? S.dropzoneActive : {} },
113
+ onClick: () => !uploading && inputRef.current?.click(),
114
+ onKeyDown: (e) => {
115
+ if (e.key === "Enter" || e.key === " ") inputRef.current?.click();
116
+ },
117
+ onDragOver: (e) => {
118
+ e.preventDefault();
119
+ setDragging(true);
120
+ },
121
+ onDragLeave: () => setDragging(false),
122
+ onDrop: (e) => {
123
+ e.preventDefault();
124
+ setDragging(false);
125
+ onFiles(e.dataTransfer.files);
126
+ },
127
+ children: [
128
+ uploading ? /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", gap: 8, alignItems: "center" }, children: [
129
+ /* @__PURE__ */ jsx(SpinnerIcon, {}),
130
+ " Uploading\u2026"
131
+ ] }) : "Drop an image here, or click to upload",
132
+ /* @__PURE__ */ jsx(
133
+ "input",
134
+ {
135
+ ref: inputRef,
136
+ type: "file",
137
+ accept: "image/png,image/jpeg,image/webp,image/gif,image/avif",
138
+ hidden: true,
139
+ onChange: (e) => {
140
+ onFiles(e.target.files);
141
+ e.target.value = "";
142
+ }
143
+ }
144
+ )
145
+ ]
146
+ }
147
+ ),
148
+ error && /* @__PURE__ */ jsx("div", { style: { ...S.error, marginBottom: 12 }, children: error }),
149
+ assets === null ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, color: "#6b7280" }, children: "Loading\u2026" }) : assets.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, color: "#6b7280" }, children: "No images uploaded yet." }) : /* @__PURE__ */ jsx("div", { style: S.pickerGrid, children: assets.map((asset) => /* @__PURE__ */ jsx(
150
+ "button",
151
+ {
152
+ type: "button",
153
+ title: asset.name,
154
+ style: {
155
+ ...S.pickerTile,
156
+ ...asset.url === current ? S.pickerTileActive : {}
157
+ },
158
+ onClick: async () => choose(asset.url, await measure(asset.url)),
159
+ children: /* @__PURE__ */ jsx("img", { src: asset.url, alt: "", loading: "lazy", style: S.pickerThumb })
160
+ },
161
+ asset.id
162
+ )) })
163
+ ] })
164
+ ]
165
+ }
166
+ )
167
+ ] });
168
+ }
169
+ export {
170
+ MediaPicker
171
+ };
172
+ //# sourceMappingURL=MediaPicker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/MediaPicker.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createAssetUpload, listAssets } from \"../server/actions\";\nimport { CloseIcon, SpinnerIcon } from \"./Icons\";\nimport { S } from \"./styles\";\nimport { prepareImage } from \"./resizeImage\";\nimport type { GRAsset } from \"../core/types\";\n\nexport interface PickedImage {\n src: string;\n width?: number;\n height?: number;\n}\n\ntype Size = { width?: number; height?: number };\n\n/**\n * Natural size is read in the browser, so no dimensions need storing on the\n * storage side and every adapter gets them for free.\n */\nfunction measure(url: string): Promise<Size> {\n return new Promise((resolve) => {\n const img = new Image();\n img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });\n img.onerror = () => resolve({});\n img.src = url;\n });\n}\n\nexport function MediaPicker({\n current,\n onSelect,\n onClose,\n}: {\n current?: string;\n onSelect: (image: PickedImage) => void;\n onClose: () => void;\n}) {\n const [assets, setAssets] = useState<GRAsset[] | null>(null);\n const [uploading, setUploading] = useState(false);\n const [dragging, setDragging] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n void listAssets().then(setAssets);\n }, []);\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n e.stopPropagation();\n onClose();\n };\n window.addEventListener(\"keydown\", onKey, true);\n return () => window.removeEventListener(\"keydown\", onKey, true);\n }, [onClose]);\n\n const choose = useCallback(\n (src: string, size: Size) => {\n onSelect({ src, ...size });\n onClose();\n },\n [onSelect, onClose],\n );\n\n const upload = async (file: File) => {\n if (uploading) return;\n setError(null);\n setUploading(true);\n try {\n // Scaled first, so the size check and the upload both see the file\n // that will actually be stored — an oversized phone photo that shrinks\n // under the cap is accepted.\n const prepared = await prepareImage(file);\n const payload = prepared.file;\n\n const res = await createAssetUpload({\n name: payload.name,\n contentType: payload.type,\n size: payload.size,\n });\n if (!res.success) {\n setError(res.error);\n return;\n }\n const { target } = res;\n\n // Straight to storage — the bytes never touch the Next.js server.\n const put = await fetch(target.url, {\n method: target.method,\n headers: target.headers,\n body: payload,\n });\n if (!put.ok) {\n setError(`Upload failed (${put.status}).`);\n return;\n }\n\n setAssets((prev) => [target.asset, ...(prev ?? [])]);\n choose(target.asset.url, { width: prepared.width, height: prepared.height });\n } catch {\n setError(\"Upload failed. Check your connection and try again.\");\n } finally {\n setUploading(false);\n }\n };\n\n const onFiles = (files: FileList | null | undefined) => {\n const file = files?.[0];\n if (file) void upload(file);\n };\n\n return (\n <>\n <div data-guardrail style={S.pickerScrim} onClick={onClose} />\n <div\n data-guardrail\n role=\"dialog\"\n aria-label=\"Choose an image\"\n style={S.picker}\n onClick={(e) => e.stopPropagation()}\n >\n <header style={S.drawerHead}>\n <h2 style={S.drawerTitle}>Choose an image</h2>\n <button\n style={{ ...S.iconBtn, color: \"#6b7280\" }}\n onClick={onClose}\n aria-label=\"Close\"\n >\n <CloseIcon />\n </button>\n </header>\n\n <div style={S.drawerBody}>\n <div\n role=\"button\"\n tabIndex={0}\n style={{ ...S.dropzone, ...(dragging ? S.dropzoneActive : {}) }}\n onClick={() => !uploading && inputRef.current?.click()}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") inputRef.current?.click();\n }}\n onDragOver={(e) => {\n e.preventDefault();\n setDragging(true);\n }}\n onDragLeave={() => setDragging(false)}\n onDrop={(e) => {\n e.preventDefault();\n setDragging(false);\n onFiles(e.dataTransfer.files);\n }}\n >\n {uploading ? (\n <span style={{ display: \"inline-flex\", gap: 8, alignItems: \"center\" }}>\n <SpinnerIcon /> Uploading…\n </span>\n ) : (\n \"Drop an image here, or click to upload\"\n )}\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/png,image/jpeg,image/webp,image/gif,image/avif\"\n hidden\n onChange={(e) => {\n onFiles(e.target.files);\n e.target.value = \"\";\n }}\n />\n </div>\n\n {error && <div style={{ ...S.error, marginBottom: 12 }}>{error}</div>}\n\n {assets === null ? (\n <p style={{ fontSize: 13, color: \"#6b7280\" }}>Loading…</p>\n ) : assets.length === 0 ? (\n <p style={{ fontSize: 13, color: \"#6b7280\" }}>No images uploaded yet.</p>\n ) : (\n <div style={S.pickerGrid}>\n {assets.map((asset) => (\n <button\n key={asset.id}\n type=\"button\"\n title={asset.name}\n style={{\n ...S.pickerTile,\n ...(asset.url === current ? S.pickerTileActive : {}),\n }}\n onClick={async () => choose(asset.url, await measure(asset.url))}\n >\n {/* Plain img: the admin UI must not depend on next/image's\n remotePatterns being configured. */}\n <img src={asset.url} alt=\"\" loading=\"lazy\" style={S.pickerThumb} />\n </button>\n ))}\n </div>\n )}\n </div>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,11 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React from 'react';
3
+ import { GRState } from '../core/types.js';
4
+
5
+ declare function GRailProvider({ children, state, }: {
6
+ children: React.ReactNode;
7
+ state: GRState;
8
+ }): react_jsx_runtime.JSX.Element;
9
+ declare function useGRail(): GRState;
10
+
11
+ export { GRailProvider, useGRail };
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import React, { createContext, useContext, Suspense } from "react";
4
+ import { EditSyncer } from "./Syncer";
5
+ const GRContext = createContext(void 0);
6
+ function GRailProvider({
7
+ children,
8
+ state
9
+ }) {
10
+ return /* @__PURE__ */ jsxs(GRContext.Provider, { value: state, children: [
11
+ /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(EditSyncer, {}) }),
12
+ children
13
+ ] });
14
+ }
15
+ function useGRail() {
16
+ const context = useContext(GRContext);
17
+ if (!context) {
18
+ throw new Error("useGRail must be used inside withGuardrail()");
19
+ }
20
+ return context;
21
+ }
22
+ export {
23
+ GRailProvider,
24
+ useGRail
25
+ };
26
+ //# sourceMappingURL=Provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/Provider.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { createContext, useContext, Suspense } from \"react\";\nimport type { GRState } from \"../core/types\";\nimport { EditSyncer } from \"./Syncer\";\n\nconst GRContext = createContext<GRState | undefined>(undefined);\n\nexport function GRailProvider({\n children,\n state,\n}: {\n children: React.ReactNode;\n state: GRState;\n}) {\n // Deliberately not held in useState. The server is the source of truth here,\n // and useState ignores prop changes after mount — so after signing in,\n // router.refresh() would deliver a fresh state that the provider discarded,\n // leaving the login bar on screen until a full page reload.\n return (\n <GRContext.Provider value={state}>\n {/* EditSyncer reads searchParams, which suspends during static render. */}\n <Suspense fallback={null}>\n <EditSyncer />\n </Suspense>\n {children}\n </GRContext.Provider>\n );\n}\n\nexport function useGRail(): GRState {\n const context = useContext(GRContext);\n if (!context) {\n throw new Error(\"useGRail must be used inside withGuardrail()\");\n }\n return context;\n}\n"],"mappings":";AAoBI,SAGI,KAHJ;AAlBJ,OAAO,SAAS,eAAe,YAAY,gBAAgB;AAE3D,SAAS,kBAAkB;AAE3B,MAAM,YAAY,cAAmC,MAAS;AAEvD,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AACF,GAGG;AAKD,SACE,qBAAC,UAAU,UAAV,EAAmB,OAAO,OAEzB;AAAA,wBAAC,YAAS,UAAU,MAClB,8BAAC,cAAW,GACd;AAAA,IACC;AAAA,KACH;AAEJ;AAEO,SAAS,WAAoB;AAClC,QAAM,UAAU,WAAW,SAAS;AACpC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,8CAA8C;AAAA,EAChE;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,16 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React from 'react';
3
+
4
+ interface RenderedItem {
5
+ id: string;
6
+ /** Already rendered server-side — this component only wraps it in chrome. */
7
+ node: React.ReactNode;
8
+ }
9
+ declare function RepeaterClient({ repeaterId, routed, items, children, }: {
10
+ repeaterId: string;
11
+ routed?: boolean;
12
+ items: RenderedItem[];
13
+ children?: React.ReactNode;
14
+ }): react_jsx_runtime.JSX.Element;
15
+
16
+ export { RepeaterClient };
@@ -0,0 +1,148 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import React, { useState, useTransition } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from "../../server/repeaterActions";
6
+ import {
7
+ ArrowDownIcon,
8
+ ArrowUpIcon,
9
+ PlusIcon,
10
+ SpinnerIcon,
11
+ TrashIcon
12
+ } from "../Icons";
13
+ import { S } from "../styles";
14
+ function RepeaterClient({
15
+ repeaterId,
16
+ routed,
17
+ items,
18
+ children
19
+ }) {
20
+ const router = useRouter();
21
+ const [error, setError] = useState(null);
22
+ const [adding, setAdding] = useState(false);
23
+ const [slug, setSlug] = useState("");
24
+ const [pending, startTransition] = useTransition();
25
+ const run = (fn) => startTransition(async () => {
26
+ setError(null);
27
+ const res = await fn();
28
+ if (!res.success) setError(res.error ?? "Something went wrong.");
29
+ router.refresh();
30
+ });
31
+ const move = (id, dir) => {
32
+ const ids = items.map((i) => i.id);
33
+ const index = ids.indexOf(id);
34
+ const target = index + dir;
35
+ if (target < 0 || target >= ids.length) return;
36
+ const a = ids[index];
37
+ const b = ids[target];
38
+ ids[index] = b;
39
+ ids[target] = a;
40
+ run(() => reorderRepeaterItems(repeaterId, ids));
41
+ };
42
+ const submitAdd = () => {
43
+ if (routed && !slug.trim()) return;
44
+ run(async () => {
45
+ const res = await addRepeaterItem(
46
+ repeaterId,
47
+ routed ? { slug: slug.trim() } : void 0
48
+ );
49
+ if (res.success) {
50
+ setAdding(false);
51
+ setSlug("");
52
+ }
53
+ return res;
54
+ });
55
+ };
56
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
57
+ items.length === 0 && children,
58
+ items.map(({ id, node }, index) => /* @__PURE__ */ jsxs("div", { style: S.repeaterItem, children: [
59
+ /* @__PURE__ */ jsxs("div", { style: S.repeaterToolbar, children: [
60
+ /* @__PURE__ */ jsx(
61
+ "button",
62
+ {
63
+ style: S.iconBtn,
64
+ disabled: pending || index === 0,
65
+ onClick: () => move(id, -1),
66
+ "aria-label": "Move up",
67
+ title: "Move up",
68
+ children: /* @__PURE__ */ jsx(ArrowUpIcon, {})
69
+ }
70
+ ),
71
+ /* @__PURE__ */ jsx(
72
+ "button",
73
+ {
74
+ style: S.iconBtn,
75
+ disabled: pending || index === items.length - 1,
76
+ onClick: () => move(id, 1),
77
+ "aria-label": "Move down",
78
+ title: "Move down",
79
+ children: /* @__PURE__ */ jsx(ArrowDownIcon, {})
80
+ }
81
+ ),
82
+ /* @__PURE__ */ jsx(
83
+ "button",
84
+ {
85
+ style: { ...S.iconBtn, color: "#f87171" },
86
+ disabled: pending,
87
+ onClick: () => {
88
+ if (confirm("Remove this item and its content?")) {
89
+ run(() => removeRepeaterItem(repeaterId, id));
90
+ }
91
+ },
92
+ "aria-label": "Remove",
93
+ title: "Remove",
94
+ children: /* @__PURE__ */ jsx(TrashIcon, {})
95
+ }
96
+ )
97
+ ] }),
98
+ node
99
+ ] }, id)),
100
+ error && /* @__PURE__ */ jsx("div", { style: { ...S.error, marginTop: 8 }, children: error }),
101
+ /* @__PURE__ */ jsx("div", { style: S.repeaterAddRow, children: adding && routed ? /* @__PURE__ */ jsxs(Fragment, { children: [
102
+ /* @__PURE__ */ jsx(
103
+ "input",
104
+ {
105
+ style: S.repeaterSlugInput,
106
+ placeholder: "url-slug",
107
+ value: slug,
108
+ autoFocus: true,
109
+ onChange: (e) => setSlug(e.target.value),
110
+ onKeyDown: (e) => {
111
+ if (e.key === "Enter") submitAdd();
112
+ if (e.key === "Escape") setAdding(false);
113
+ }
114
+ }
115
+ ),
116
+ /* @__PURE__ */ jsx("button", { style: S.primaryBtn, disabled: pending, onClick: submitAdd, children: pending ? /* @__PURE__ */ jsx(SpinnerIcon, {}) : "Add" }),
117
+ /* @__PURE__ */ jsx(
118
+ "button",
119
+ {
120
+ style: S.cancelBtn,
121
+ disabled: pending,
122
+ onClick: () => {
123
+ setAdding(false);
124
+ setSlug("");
125
+ },
126
+ children: "Cancel"
127
+ }
128
+ )
129
+ ] }) : /* @__PURE__ */ jsxs(
130
+ "button",
131
+ {
132
+ type: "button",
133
+ style: S.repeaterAddBtn,
134
+ disabled: pending,
135
+ onClick: () => routed ? setAdding(true) : submitAdd(),
136
+ children: [
137
+ /* @__PURE__ */ jsx(PlusIcon, {}),
138
+ " Add ",
139
+ routed ? "item" : "card"
140
+ ]
141
+ }
142
+ ) })
143
+ ] });
144
+ }
145
+ export {
146
+ RepeaterClient
147
+ };
148
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/react/Repeater/client.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useState, useTransition } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from \"../../server/repeaterActions\";\nimport {\n ArrowDownIcon,\n ArrowUpIcon,\n PlusIcon,\n SpinnerIcon,\n TrashIcon,\n} from \"../Icons\";\nimport { S } from \"../styles\";\n\ninterface RenderedItem {\n id: string;\n /** Already rendered server-side — this component only wraps it in chrome. */\n node: React.ReactNode;\n}\n\nexport function RepeaterClient({\n repeaterId,\n routed,\n items,\n children,\n}: {\n repeaterId: string;\n routed?: boolean;\n items: RenderedItem[];\n children?: React.ReactNode;\n}) {\n const router = useRouter();\n const [error, setError] = useState<string | null>(null);\n const [adding, setAdding] = useState(false);\n const [slug, setSlug] = useState(\"\");\n const [pending, startTransition] = useTransition();\n\n /**\n * There is deliberately no local copy of `items`. Each item's node can\n * contain <GRail> fields, which only exist as a resolved server render —\n * they cannot be reproduced by holding data in client state and re-invoking\n * a component function in the browser. router.refresh() re-runs <GRepeater>\n * on the server and delivers freshly rendered nodes as new props, the same\n * pattern used for the login flow in Admin.tsx.\n */\n const run = (fn: () => Promise<{ success: boolean; error?: string }>) =>\n startTransition(async () => {\n setError(null);\n const res = await fn();\n if (!res.success) setError(res.error ?? \"Something went wrong.\");\n router.refresh();\n });\n\n const move = (id: string, dir: -1 | 1) => {\n const ids = items.map((i) => i.id);\n const index = ids.indexOf(id);\n const target = index + dir;\n if (target < 0 || target >= ids.length) return;\n const a = ids[index]!;\n const b = ids[target]!;\n ids[index] = b;\n ids[target] = a;\n run(() => reorderRepeaterItems(repeaterId, ids));\n };\n\n const submitAdd = () => {\n if (routed && !slug.trim()) return;\n run(async () => {\n const res = await addRepeaterItem(\n repeaterId,\n routed ? { slug: slug.trim() } : undefined,\n );\n if (res.success) {\n setAdding(false);\n setSlug(\"\");\n }\n return res;\n });\n };\n\n return (\n <>\n {items.length === 0 && children}\n\n {items.map(({ id, node }, index) => (\n <div key={id} style={S.repeaterItem}>\n <div style={S.repeaterToolbar}>\n <button\n style={S.iconBtn}\n disabled={pending || index === 0}\n onClick={() => move(id, -1)}\n aria-label=\"Move up\"\n title=\"Move up\"\n >\n <ArrowUpIcon />\n </button>\n <button\n style={S.iconBtn}\n disabled={pending || index === items.length - 1}\n onClick={() => move(id, 1)}\n aria-label=\"Move down\"\n title=\"Move down\"\n >\n <ArrowDownIcon />\n </button>\n <button\n style={{ ...S.iconBtn, color: \"#f87171\" }}\n disabled={pending}\n onClick={() => {\n if (confirm(\"Remove this item and its content?\")) {\n run(() => removeRepeaterItem(repeaterId, id));\n }\n }}\n aria-label=\"Remove\"\n title=\"Remove\"\n >\n <TrashIcon />\n </button>\n </div>\n {node}\n </div>\n ))}\n\n {error && <div style={{ ...S.error, marginTop: 8 }}>{error}</div>}\n\n <div style={S.repeaterAddRow}>\n {adding && routed ? (\n <>\n <input\n style={S.repeaterSlugInput}\n placeholder=\"url-slug\"\n value={slug}\n autoFocus\n onChange={(e) => setSlug(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") submitAdd();\n if (e.key === \"Escape\") setAdding(false);\n }}\n />\n <button style={S.primaryBtn} disabled={pending} onClick={submitAdd}>\n {pending ? <SpinnerIcon /> : \"Add\"}\n </button>\n <button\n style={S.cancelBtn}\n disabled={pending}\n onClick={() => {\n setAdding(false);\n setSlug(\"\");\n }}\n >\n Cancel\n </button>\n </>\n ) : (\n <button\n type=\"button\"\n style={S.repeaterAddBtn}\n disabled={pending}\n onClick={() => (routed ? setAdding(true) : submitAdd())}\n >\n <PlusIcon /> Add {routed ? \"item\" : \"card\"}\n </button>\n )}\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,35 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React from 'react';
3
+ import { GRRepeaterItem } from '../../core/types.js';
4
+
5
+ interface RepeaterItemProps {
6
+ item: GRRepeaterItem;
7
+ }
8
+ interface GRepeaterProps {
9
+ /** The repeater's own key. Item fields are namespaced under it: `${id}.${item.id}.*`. */
10
+ id: string;
11
+ /** The developer's own component — the one place the item's shape is defined. */
12
+ as: React.ComponentType<RepeaterItemProps>;
13
+ /**
14
+ * Marks this repeater as routed: each item gets its own URL (a product, a
15
+ * post), so adding one prompts for a slug. Omit for a plain row of items on
16
+ * one page (service cards) — those need no slug at all.
17
+ */
18
+ routed?: boolean;
19
+ /** Shown in place of the list when it has no items yet. */
20
+ children?: React.ReactNode;
21
+ }
22
+ /**
23
+ * Renders N instances of one developer-authored template component, with the
24
+ * marketer controlling how many and in what order — not what shape they take.
25
+ * That's the whole primitive: no schema, no field-type registry, no relations.
26
+ * An item's own fields are just <GRail> fields living under the item's id,
27
+ * identical in kind to every other field on the page.
28
+ *
29
+ * Server Component by default, same split as <GRail>: anonymous visitors get
30
+ * the template rendered N times with no admin JS; the add/remove/reorder
31
+ * chrome only loads for someone who can actually write.
32
+ */
33
+ declare function GRepeater({ id, as: Item, routed, children }: GRepeaterProps): Promise<react_jsx_runtime.JSX.Element>;
34
+
35
+ export { GRepeater, type GRepeaterProps, type RepeaterItemProps };
@@ -0,0 +1,22 @@
1
+ import { Fragment, jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { listRepeaterItems } from "../../server/repeaterData";
4
+ import { getCachedViewer } from "../../server/session";
5
+ import { RepeaterClient } from "./client";
6
+ async function GRepeater({ id, as: Item, routed, children }) {
7
+ const items = await listRepeaterItems(id);
8
+ const viewer = await getCachedViewer();
9
+ if (viewer?.role) {
10
+ const rendered = items.map((item) => ({
11
+ id: item.id,
12
+ node: /* @__PURE__ */ jsx(Item, { item }, item.id)
13
+ }));
14
+ return /* @__PURE__ */ jsx(RepeaterClient, { repeaterId: id, routed, items: rendered, children });
15
+ }
16
+ if (items.length === 0) return /* @__PURE__ */ jsx(Fragment, { children: children ?? null });
17
+ return /* @__PURE__ */ jsx(Fragment, { children: items.map((item) => /* @__PURE__ */ jsx(Item, { item }, item.id)) });
18
+ }
19
+ export {
20
+ GRepeater
21
+ };
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/react/Repeater/index.tsx"],"sourcesContent":["import React from \"react\";\nimport { listRepeaterItems } from \"../../server/repeaterData\";\nimport { getCachedViewer } from \"../../server/session\";\nimport { RepeaterClient } from \"./client\";\nimport type { GRRepeaterItem } from \"../../core/types\";\n\nexport interface RepeaterItemProps {\n item: GRRepeaterItem;\n}\n\nexport interface GRepeaterProps {\n /** The repeater's own key. Item fields are namespaced under it: `${id}.${item.id}.*`. */\n id: string;\n /** The developer's own component — the one place the item's shape is defined. */\n as: React.ComponentType<RepeaterItemProps>;\n /**\n * Marks this repeater as routed: each item gets its own URL (a product, a\n * post), so adding one prompts for a slug. Omit for a plain row of items on\n * one page (service cards) — those need no slug at all.\n */\n routed?: boolean;\n /** Shown in place of the list when it has no items yet. */\n children?: React.ReactNode;\n}\n\n/**\n * Renders N instances of one developer-authored template component, with the\n * marketer controlling how many and in what order — not what shape they take.\n * That's the whole primitive: no schema, no field-type registry, no relations.\n * An item's own fields are just <GRail> fields living under the item's id,\n * identical in kind to every other field on the page.\n *\n * Server Component by default, same split as <GRail>: anonymous visitors get\n * the template rendered N times with no admin JS; the add/remove/reorder\n * chrome only loads for someone who can actually write.\n */\nexport async function GRepeater({ id, as: Item, routed, children }: GRepeaterProps) {\n const items = await listRepeaterItems(id);\n const viewer = await getCachedViewer();\n\n if (viewer?.role) {\n // Rendered here, on the server, and handed down as already-resolved\n // elements — never as the Item component itself. Item almost certainly\n // renders <GRail>, an async Server Component; passing the function across\n // the client boundary would let RepeaterClient try to invoke it in the\n // browser, which cannot execute a Server Component at all. An element\n // (the *result* of calling it) is plain serializable RSC content and\n // composes into a Client Component exactly like `children` does.\n const rendered = items.map((item) => ({\n id: item.id,\n node: <Item key={item.id} item={item} />,\n }));\n\n return (\n <RepeaterClient repeaterId={id} routed={routed} items={rendered}>\n {children}\n </RepeaterClient>\n );\n }\n\n if (items.length === 0) return <>{children ?? null}</>;\n return (\n <>\n {items.map((item) => (\n <Item key={item.id} item={item} />\n ))}\n </>\n );\n}\n"],"mappings":"AAkDY,SAUqB,UAVrB;AAlDZ,OAAO,WAAW;AAClB,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AAChC,SAAS,sBAAsB;AAiC/B,eAAsB,UAAU,EAAE,IAAI,IAAI,MAAM,QAAQ,SAAS,GAAmB;AAClF,QAAM,QAAQ,MAAM,kBAAkB,EAAE;AACxC,QAAM,SAAS,MAAM,gBAAgB;AAErC,MAAI,QAAQ,MAAM;AAQhB,UAAM,WAAW,MAAM,IAAI,CAAC,UAAU;AAAA,MACpC,IAAI,KAAK;AAAA,MACT,MAAM,oBAAC,QAAmB,QAAT,KAAK,EAAgB;AAAA,IACxC,EAAE;AAEF,WACE,oBAAC,kBAAe,YAAY,IAAI,QAAgB,OAAO,UACpD,UACH;AAAA,EAEJ;AAEA,MAAI,MAAM,WAAW,EAAG,QAAO,gCAAG,sBAAY,MAAK;AACnD,SACE,gCACG,gBAAM,IAAI,CAAC,SACV,oBAAC,QAAmB,QAAT,KAAK,EAAgB,CACjC,GACH;AAEJ;","names":[]}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Turns edit mode on from the URL. Off is deliberately not symmetrical with
3
+ * on: the cookie is the actual state (withGuardrail reads it fresh from
4
+ * cookies() on every navigation, which is why edit mode survives clicking
5
+ * through the site at all), and ?edit=true is a one-time switch, not a value
6
+ * that has to keep appearing on every URL to hold that state.
7
+ *
8
+ * A page that simply doesn't mention `edit` — which is every normal link on
9
+ * the site, including a repeater's card linking to its own route — must
10
+ * leave the cookie alone. Treating "no param" as "turn it off" was the actual
11
+ * bug: it silently ended the edit session on the very first click to a page
12
+ * that didn't happen to carry the query string. Only an explicit
13
+ * ?edit=false turns it off from the URL; Log out is the normal way to end it.
14
+ */
15
+ declare function EditSyncer(): null;
16
+
17
+ export { EditSyncer };
@@ -0,0 +1,23 @@
1
+ "use client";
2
+ import { useEffect } from "react";
3
+ import { useSearchParams } from "next/navigation";
4
+ import { GR_COOKIES } from "../core/defs";
5
+ function EditSyncer() {
6
+ const searchParams = useSearchParams();
7
+ useEffect(() => {
8
+ const edit = searchParams.get("edit");
9
+ const hasCookie = document.cookie.includes(`${GR_COOKIES.EDIT_MODE}=true`);
10
+ if (edit === "true" && !hasCookie) {
11
+ document.cookie = `${GR_COOKIES.EDIT_MODE}=true; path=/; SameSite=Lax`;
12
+ window.location.reload();
13
+ } else if (edit === "false" && hasCookie) {
14
+ document.cookie = `${GR_COOKIES.EDIT_MODE}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
15
+ window.location.reload();
16
+ }
17
+ }, [searchParams]);
18
+ return null;
19
+ }
20
+ export {
21
+ EditSyncer
22
+ };
23
+ //# sourceMappingURL=Syncer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/Syncer.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { GR_COOKIES } from \"../core/defs\";\n\n/**\n * Turns edit mode on from the URL. Off is deliberately not symmetrical with\n * on: the cookie is the actual state (withGuardrail reads it fresh from\n * cookies() on every navigation, which is why edit mode survives clicking\n * through the site at all), and ?edit=true is a one-time switch, not a value\n * that has to keep appearing on every URL to hold that state.\n *\n * A page that simply doesn't mention `edit` — which is every normal link on\n * the site, including a repeater's card linking to its own route — must\n * leave the cookie alone. Treating \"no param\" as \"turn it off\" was the actual\n * bug: it silently ended the edit session on the very first click to a page\n * that didn't happen to carry the query string. Only an explicit\n * ?edit=false turns it off from the URL; Log out is the normal way to end it.\n */\nexport function EditSyncer() {\n const searchParams = useSearchParams();\n\n useEffect(() => {\n const edit = searchParams.get(\"edit\");\n const hasCookie = document.cookie.includes(`${GR_COOKIES.EDIT_MODE}=true`);\n\n if (edit === \"true\" && !hasCookie) {\n document.cookie = `${GR_COOKIES.EDIT_MODE}=true; path=/; SameSite=Lax`;\n window.location.reload();\n } else if (edit === \"false\" && hasCookie) {\n document.cookie = `${GR_COOKIES.EDIT_MODE}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n window.location.reload();\n }\n }, [searchParams]);\n\n return null;\n}\n"],"mappings":";AAEA,SAAS,iBAAiB;AAC1B,SAAS,uBAAuB;AAChC,SAAS,kBAAkB;AAgBpB,SAAS,aAAa;AAC3B,QAAM,eAAe,gBAAgB;AAErC,YAAU,MAAM;AACd,UAAM,OAAO,aAAa,IAAI,MAAM;AACpC,UAAM,YAAY,SAAS,OAAO,SAAS,GAAG,WAAW,SAAS,OAAO;AAEzE,QAAI,SAAS,UAAU,CAAC,WAAW;AACjC,eAAS,SAAS,GAAG,WAAW,SAAS;AACzC,aAAO,SAAS,OAAO;AAAA,IACzB,WAAW,SAAS,WAAW,WAAW;AACxC,eAAS,SAAS,GAAG,WAAW,SAAS;AACzC,aAAO,SAAS,OAAO;AAAA,IACzB;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AAEjB,SAAO;AACT;","names":[]}
@@ -0,0 +1,7 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ declare function TeamDrawer({ onClose }: {
4
+ onClose: () => void;
5
+ }): react_jsx_runtime.JSX.Element;
6
+
7
+ export { TeamDrawer };