@dalgoridim/headless-cms 0.1.0 → 0.2.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/ARCHITECTURE.md +126 -0
- package/README.md +238 -48
- package/REDESIGN.md +250 -0
- package/dist/adapters/firestore/index.cjs +24 -3
- package/dist/adapters/firestore/index.cjs.map +1 -1
- package/dist/adapters/firestore/index.js +24 -3
- package/dist/adapters/firestore/index.js.map +1 -1
- package/dist/adapters/postgres/index.cjs +37 -11
- package/dist/adapters/postgres/index.cjs.map +1 -1
- package/dist/adapters/postgres/index.js +37 -11
- package/dist/adapters/postgres/index.js.map +1 -1
- package/dist/client/index.cjs +94 -543
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +89 -26
- package/dist/client/index.d.ts +89 -26
- package/dist/client/index.js +96 -547
- package/dist/client/index.js.map +1 -1
- package/dist/index.cjs +16 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -16
- package/dist/index.d.ts +81 -16
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +63 -9
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +35 -5
- package/dist/server/index.d.ts +35 -5
- package/dist/server/index.js +61 -8
- package/dist/server/index.js.map +1 -1
- package/package.json +5 -10
package/dist/client/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/client/PageProvider.tsx","../../src/client/auth.tsx","../../src/client/ContentEditSpan.tsx","../../src/client/utils.ts","../../src/client/EditableImage.tsx","../../src/client/MarkdownEditor.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useState,\n useCallback,\n type ReactNode,\n} from \"react\";\nimport { toast } from \"sonner\";\nimport type {\n ClientStorageAdapter,\n NestedSections,\n PendingImage,\n Section,\n} from \"../types\";\n\nexport type { PendingImage } from \"../types\";\n\n/** Notification sink. Defaults to `sonner` toasts; override to integrate your own. */\nexport interface Notifier {\n success: (message: string) => void;\n error: (message: string) => void;\n}\n\nconst defaultNotifier: Notifier = {\n success: (m) => toast.success(m),\n error: (m) => toast.error(m),\n};\n\ninterface PageContextType {\n sections: NestedSections;\n hasUnsavedChanges: boolean;\n saving: boolean;\n pendingImages: PendingImage[];\n setSection: (collection: string, key: string, section: Section) => void;\n editField: (\n collection: string,\n sectionKey: string,\n fieldKey: string,\n value: unknown,\n ) => void;\n setPendingImage: (image: PendingImage) => void;\n saveSection: (collection: string, sectionKey: string) => Promise<void>;\n saveAll: () => Promise<void>;\n}\n\nconst PageContext = createContext<PageContextType | undefined>(undefined);\n\nconst dirtyKey = (collection: string, sectionKey: string) =>\n `${collection}:${sectionKey}`;\n\nexport interface PageProviderProps {\n children: ReactNode;\n /** Server-rendered sections to hydrate from. */\n initialSections?: NestedSections;\n /**\n * Base path of the CMS API route mounted via `createCmsHandlers`. Saves\n * `PATCH` to `${apiBasePath}/{collection}/{id}`. Defaults to `/api/admin`.\n */\n apiBasePath?: string;\n /** Client storage adapter used to upload pending (non-external) images on save. */\n storage?: ClientStorageAdapter;\n /** Notification sink. Defaults to `sonner` toasts. */\n notify?: Notifier;\n}\n\nexport const PageProvider = ({\n children,\n initialSections = {},\n apiBasePath = \"/api/admin\",\n storage,\n notify = defaultNotifier,\n}: PageProviderProps) => {\n const [saving, setSaving] = useState(false);\n const [sections, setSections] = useState<NestedSections>(initialSections);\n const [pendingImages, setPendingImages] = useState<PendingImage[]>([]);\n const [dirtySections, setDirtySections] = useState<Set<string>>(new Set());\n\n const hasUnsavedChanges = dirtySections.size > 0;\n\n const resolveImageUrl = useCallback(\n async (img: PendingImage): Promise<string> => {\n if (img.isExternal) return img.localUrl;\n if (!storage) {\n throw new Error(\n \"PageProvider received a file upload but no `storage` adapter was provided.\",\n );\n }\n const { url } = await storage.upload(img.file!);\n return url;\n },\n [storage],\n );\n\n const setSection = useCallback(\n (collection: string, key: string, section: Section) => {\n setSections((prev) => ({\n ...prev,\n [collection]: { ...prev[collection], [key]: section },\n }));\n },\n [],\n );\n\n const editField = useCallback(\n (\n collection: string,\n sectionKey: string,\n fieldKey: string,\n value: unknown,\n ) => {\n setSections((prev) => {\n const currentSection = prev[collection]?.[sectionKey];\n\n if (!currentSection) {\n console.error(`Section not found: ${collection}/${sectionKey}`);\n return prev;\n }\n\n const keys = fieldKey.split(\".\");\n const updated: Section = { ...currentSection };\n\n if (keys.length === 1) {\n updated[fieldKey] = value;\n } else {\n let current: Record<string, unknown> = updated;\n for (let i = 0; i < keys.length - 1; i++) {\n if (!current[keys[i]]) current[keys[i]] = {};\n current[keys[i]] = { ...(current[keys[i]] as object) };\n current = current[keys[i]] as Record<string, unknown>;\n }\n current[keys[keys.length - 1]] = value;\n }\n\n return {\n ...prev,\n [collection]: { ...prev[collection], [sectionKey]: updated },\n };\n });\n\n setDirtySections((prev) => {\n const next = new Set(prev);\n next.add(dirtyKey(collection, sectionKey));\n return next;\n });\n },\n [],\n );\n\n const setPendingImage = useCallback((image: PendingImage) => {\n setPendingImages((prev) => [\n ...prev.filter(\n (img) =>\n !(\n img.collection === image.collection &&\n img.sectionKey === image.sectionKey &&\n img.fieldKey === image.fieldKey\n ),\n ),\n image,\n ]);\n\n setDirtySections((prev) => {\n const next = new Set(prev);\n next.add(dirtyKey(image.collection, image.sectionKey));\n return next;\n });\n }, []);\n\n const persist = useCallback(\n async (section: Section) => {\n const response = await fetch(\n `${apiBasePath}/${section.collection}/${section.id}`,\n {\n method: \"PATCH\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(section),\n },\n );\n if (!response.ok) throw new Error(\"Failed to save section\");\n },\n [apiBasePath],\n );\n\n const saveSection = useCallback(\n async (collection: string, sectionKey: string) => {\n if (saving) return;\n setSaving(true);\n\n try {\n const section = sections[collection]?.[sectionKey];\n if (!section?.id || !section?.collection) {\n console.error(`Invalid section: ${collection}/${sectionKey}`);\n setSaving(false);\n return;\n }\n\n const images = pendingImages.filter(\n (img) =>\n img.collection === collection && img.sectionKey === sectionKey,\n );\n\n let updatedSection: Section = { ...section };\n\n for (const img of images) {\n const url = await resolveImageUrl(img);\n const keys = img.fieldKey.split(\".\");\n if (keys.length === 1) {\n updatedSection = { ...updatedSection, [img.fieldKey]: url };\n } else {\n let current: Record<string, unknown> = updatedSection;\n for (let i = 0; i < keys.length - 1; i++) {\n if (!current[keys[i]]) current[keys[i]] = {};\n current = current[keys[i]] as Record<string, unknown>;\n }\n current[keys[keys.length - 1]] = url;\n }\n }\n\n await persist(updatedSection);\n\n setSections((prev) => ({\n ...prev,\n [collection]: { ...prev[collection], [sectionKey]: updatedSection },\n }));\n\n setPendingImages((prev) =>\n prev.filter(\n (img) =>\n !(img.collection === collection && img.sectionKey === sectionKey),\n ),\n );\n\n setDirtySections((prev) => {\n const next = new Set(prev);\n next.delete(dirtyKey(collection, sectionKey));\n return next;\n });\n\n notify.success(\"Changes saved successfully!\");\n } catch (error) {\n console.error(\"Save failed:\", error);\n notify.error(\"Failed to save changes\");\n } finally {\n setSaving(false);\n }\n },\n [sections, pendingImages, saving, resolveImageUrl, persist, notify],\n );\n\n const saveAll = useCallback(async () => {\n if (saving || dirtySections.size === 0) return;\n setSaving(true);\n\n try {\n const updatedSections: NestedSections = { ...sections };\n\n for (const img of pendingImages) {\n const url = await resolveImageUrl(img);\n\n if (!updatedSections[img.collection])\n updatedSections[img.collection] = {};\n\n if (!updatedSections[img.collection][img.sectionKey]) {\n updatedSections[img.collection][img.sectionKey] = {\n id: img.docId,\n collection: img.collection,\n };\n }\n\n const keys = img.fieldKey.split(\".\");\n let current: Record<string, unknown> =\n updatedSections[img.collection][img.sectionKey];\n\n if (keys.length === 1) {\n updatedSections[img.collection][img.sectionKey] = {\n ...(current as Section),\n [img.fieldKey]: url,\n };\n } else {\n for (let i = 0; i < keys.length - 1; i++) {\n if (!current[keys[i]]) current[keys[i]] = {};\n current = current[keys[i]] as Record<string, unknown>;\n }\n current[keys[keys.length - 1]] = url;\n }\n }\n\n for (const entry of dirtySections) {\n const [collection, key] = entry.split(\":\");\n const section = updatedSections[collection]?.[key];\n\n if (!section?.id || !section?.collection) {\n console.error(`Invalid section for save: ${entry}`);\n continue;\n }\n\n await persist(section);\n }\n\n setSections(updatedSections);\n setPendingImages([]);\n setDirtySections(new Set());\n notify.success(\"All changes saved successfully!\");\n } catch (error) {\n console.error(\"Save all failed:\", error);\n notify.error(\"Failed to save changes\");\n } finally {\n setSaving(false);\n }\n }, [sections, pendingImages, dirtySections, saving, resolveImageUrl, persist, notify]);\n\n return (\n <PageContext.Provider\n value={{\n sections,\n hasUnsavedChanges,\n pendingImages,\n saving,\n setSection,\n editField,\n setPendingImage,\n saveSection,\n saveAll,\n }}\n >\n {children}\n </PageContext.Provider>\n );\n};\n\nexport const usePageContext = () => {\n const context = useContext(PageContext);\n if (!context)\n throw new Error(\"usePageContext must be used within a PageProvider\");\n return context;\n};\n","\"use client\";\n\nimport { createContext, useContext, type ReactNode } from \"react\";\nimport type { CmsAuthState } from \"../types\";\n\n/**\n * The single source of client-side auth state for the engine. Every built-in\n * auth provider (and any custom one) feeds this context; the edit primitives\n * read it via {@link useCmsAuth}.\n *\n * Cross-entry note: provider packages (e.g. `…/auth/firebase/client`) import\n * this context through the package's public `…/client` specifier so they share\n * the *same* context instance as the consumer's primitives at runtime.\n */\nexport const CmsAuthContext = createContext<CmsAuthState | undefined>(undefined);\n\nexport function useCmsAuth(): CmsAuthState {\n const ctx = useContext(CmsAuthContext);\n if (!ctx) {\n throw new Error(\n \"useCmsAuth must be used within a CmsAuthProvider (or a built-in auth provider such as FirebaseAuthProvider).\",\n );\n }\n return ctx;\n}\n\n/**\n * Controlled provider for consumers wiring their own auth. Pass the resolved\n * {@link CmsAuthState}; the built-in providers wrap this for you.\n */\nexport function CmsAuthProvider({\n value,\n children,\n}: {\n value: CmsAuthState;\n children: ReactNode;\n}) {\n return (\n <CmsAuthContext.Provider value={value}>{children}</CmsAuthContext.Provider>\n );\n}\n","\"use client\";\n\nimport React, { useRef, useEffect, useState, useCallback } from \"react\";\nimport { usePageContext } from \"./PageProvider\";\nimport { useCmsAuth } from \"./auth\";\nimport { cn } from \"./utils\";\n\ntype AsTag = \"span\" | \"h1\" | \"h2\" | \"h3\" | \"p\" | \"div\";\n\ninterface ContentSpanProps {\n collection?: string;\n sectionKey: string;\n fieldKey: string;\n className?: string;\n children: React.ReactNode;\n as?: AsTag;\n}\n\ntype CustomText = {\n text: string;\n bold?: boolean;\n italic?: boolean;\n strike?: boolean;\n primary?: boolean;\n underline?: boolean;\n break?: boolean;\n link?: string;\n};\n\nconst PATTERNS = [\n { regex: /^\\*\\*(.+?)\\*\\*/, mark: \"bold\" },\n { regex: /^\\*(.+?)\\*/, mark: \"italic\" },\n { regex: /^~~br~~/, mark: \"break\" },\n { regex: /^~~(.+?)~~/, mark: \"strike\" },\n { regex: /^\\^\\^(.+?)\\^\\^/, mark: \"primary\" },\n { regex: /^__(.+?)__/, mark: \"underline\" },\n {\n regex: /^\\[(.+?)\\]\\((https?:\\/\\/[^\\s)]+)\\)/,\n mark: \"link\",\n isLink: true,\n },\n] as const;\n\nfunction parseSpecialString(input: string): CustomText[] {\n const out: CustomText[] = [];\n let text = input;\n\n while (text.length) {\n let matched = false;\n\n for (const p of PATTERNS) {\n const m = p.regex.exec(text);\n if (!m) continue;\n\n matched = true;\n\n if (p.mark === \"break\") {\n out.push({ text: \"\\n\", break: true });\n } else if (\"isLink\" in p) {\n out.push({ text: m[1], link: m[2] });\n } else {\n const inner = parseSpecialString(m[1]);\n inner.forEach(\n (n) => ((n as unknown as Record<string, boolean>)[p.mark] = true),\n );\n out.push(...inner);\n }\n\n text = text.slice(m[0].length);\n break;\n }\n\n if (!matched) {\n out.push({ text: text[0] });\n text = text.slice(1);\n }\n }\n\n return out;\n}\n\nfunction RenderStatic({\n raw,\n as: Component = \"span\",\n className,\n}: {\n raw: string;\n as?: AsTag;\n className?: string;\n}) {\n const nodes = parseSpecialString(raw);\n\n const content = (\n <>\n {nodes.map((l, i) => {\n if (l.break) return <br key={i} />;\n let el: React.ReactNode = l.text;\n\n if (l.bold) el = <strong>{el}</strong>;\n if (l.italic) el = <em>{el}</em>;\n if (l.strike) el = <s>{el}</s>;\n\n if (l.primary || l.underline) {\n el = (\n <span\n style={{\n color: l.primary ? \"var(--color-primary)\" : undefined,\n textDecoration: l.underline ? \"underline\" : undefined,\n }}\n >\n {el}\n </span>\n );\n }\n\n if (l.link) {\n el = (\n <a\n href={l.link}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline hover:underline\"\n >\n {el}\n </a>\n );\n }\n\n return <span key={i}>{el}</span>;\n })}\n </>\n );\n\n return <Component className={className}>{content}</Component>;\n}\n\nfunction getNestedValue(obj: unknown, path: string): unknown {\n const keys = path.split(\".\");\n let current: unknown = obj;\n\n for (const key of keys) {\n if (current == null || (current as Record<string, unknown>)[key] === undefined)\n return undefined;\n current = (current as Record<string, unknown>)[key];\n }\n\n return current;\n}\n\nexport default function ContentEditSpan({\n collection = \"portfolio\",\n sectionKey,\n fieldKey,\n className,\n children,\n as = \"span\",\n}: ContentSpanProps) {\n const { sections, editField } = usePageContext();\n const { isEditing } = useCmsAuth();\n\n const section = sections[collection]?.[sectionKey];\n const raw =\n (getNestedValue(section, fieldKey) as string) ??\n (typeof children === \"string\" ? children : \"\");\n\n if (!isEditing) {\n return <RenderStatic raw={raw} as={as} className={className} />;\n }\n\n return (\n <EditableContentSpan\n collection={collection}\n sectionKey={sectionKey}\n fieldKey={fieldKey}\n className={className}\n raw={raw}\n editField={editField}\n as={as}\n />\n );\n}\n\nfunction EditableContentSpan({\n collection = \"portfolio\",\n sectionKey,\n fieldKey,\n className,\n raw,\n editField,\n as: Component = \"span\",\n}: {\n collection?: string;\n sectionKey: string;\n fieldKey: string;\n className?: string;\n raw: string;\n editField: (\n collection: string,\n sectionKey: string,\n fieldKey: string,\n value: string,\n ) => void;\n as?: AsTag;\n}) {\n const { isEditing } = useCmsAuth();\n const [isFocused, setIsFocused] = useState(false);\n const [editValue, setEditValue] = useState(raw);\n const contentRef = useRef<HTMLElement>(null);\n const rawRef = useRef(raw);\n\n useEffect(() => {\n if (!isFocused && rawRef.current !== raw) {\n rawRef.current = raw;\n }\n }, [raw, isFocused]);\n\n useEffect(() => {\n if (!isFocused && rawRef.current !== editValue) {\n setEditValue(rawRef.current);\n }\n }, [isFocused, editValue]);\n\n const handleInput = useCallback(() => {\n if (contentRef.current) {\n setEditValue(contentRef.current.textContent || \"\");\n }\n }, []);\n\n const handleBlur = useCallback(() => {\n setIsFocused(false);\n if (editValue !== raw) {\n editField(collection!, sectionKey, fieldKey, editValue);\n }\n }, [editValue, raw, collection, sectionKey, fieldKey, editField]);\n\n const handleFocus = useCallback(() => {\n setIsFocused(true);\n if (contentRef.current) {\n contentRef.current.textContent = editValue;\n setTimeout(() => {\n if (contentRef.current) {\n const range = document.createRange();\n const sel = window.getSelection();\n range.selectNodeContents(contentRef.current);\n range.collapse(false);\n sel?.removeAllRanges();\n sel?.addRange(range);\n }\n }, 0);\n }\n }, [editValue]);\n\n return (\n <Component\n key={isFocused ? \"editing\" : \"static\"}\n ref={\n contentRef as React.RefObject<\n HTMLElement &\n HTMLSpanElement &\n HTMLHeadingElement &\n HTMLParagraphElement &\n HTMLDivElement\n >\n }\n className={cn(\n className,\n \"outline-none transition-all duration-200\",\n \"whitespace-pre-wrap break-words overflow-wrap-anywhere\",\n isFocused &&\n \"ring-2 ring-primary/50 ring-offset-2 ring-offset-neutral-900 rounded-sm px-2\",\n !isFocused &&\n isEditing &&\n \"hover:ring-1 hover:ring-primary/30 hover:ring-offset-1 hover:ring-offset-neutral-900 hover:rounded-sm hover:px-2 cursor-text\",\n )}\n contentEditable={isEditing}\n suppressContentEditableWarning\n onInput={handleInput}\n onBlur={handleBlur}\n onFocus={handleFocus}\n >\n {!isFocused && <RenderStatic raw={editValue} as=\"span\" />}\n </Component>\n );\n}\n","import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","\"use client\";\n\nimport React, { useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { CameraIcon, Link2Icon, XIcon, CheckIcon } from \"lucide-react\";\nimport { useCmsAuth } from \"./auth\";\nimport { usePageContext } from \"./PageProvider\";\nimport { cn } from \"./utils\";\n\ninterface EditableImageProps {\n sectionKey: string;\n fieldKey: string;\n src: string;\n collection: string;\n docId: string;\n className?: string;\n}\n\nexport default function EditableImage({\n sectionKey,\n fieldKey,\n src,\n collection,\n docId,\n className,\n}: EditableImageProps) {\n const { isEditing } = useCmsAuth();\n const { editField, setPendingImage, pendingImages, saving } =\n usePageContext();\n\n const [preview, setPreview] = useState(src);\n const [hasError, setHasError] = useState(false);\n const [showUrlModal, setShowUrlModal] = useState(false);\n const [urlInput, setUrlInput] = useState(\"\");\n const [urlPreview, setUrlPreview] = useState(\"\");\n\n const inputRef = useRef<HTMLInputElement>(null);\n\n const pendingImage = pendingImages.find(\n (img) => img.sectionKey === sectionKey && img.fieldKey === fieldKey,\n );\n\n const imgSrc = pendingImage?.localUrl || preview;\n\n const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n if (saving) return;\n const file = e.target.files?.[0];\n if (!file) return;\n\n const localUrl = URL.createObjectURL(file);\n setPreview(localUrl);\n setHasError(false);\n\n editField(collection, sectionKey, fieldKey, localUrl);\n\n setPendingImage({\n file,\n localUrl,\n sectionKey,\n fieldKey,\n collection,\n docId,\n isExternal: false,\n });\n };\n\n const handleUrlConfirm = () => {\n if (!urlPreview) return;\n\n setPreview(urlPreview);\n setHasError(false);\n\n editField(collection, sectionKey, fieldKey, urlPreview);\n\n setPendingImage({\n file: null,\n localUrl: urlPreview,\n sectionKey,\n fieldKey,\n collection,\n docId,\n isExternal: true,\n });\n\n setShowUrlModal(false);\n setUrlInput(\"\");\n setUrlPreview(\"\");\n };\n\n const handleUrlChange = (value: string) => {\n setUrlInput(value);\n try {\n const url = new URL(value);\n if (url.protocol === \"http:\" || url.protocol === \"https:\") {\n setUrlPreview(value);\n } else {\n setUrlPreview(\"\");\n }\n } catch {\n setUrlPreview(\"\");\n }\n };\n\n const imageNode =\n hasError || !imgSrc ? (\n <div className=\"flex items-center justify-center w-full h-full p-4\">\n <div className=\"text-center space-y-4\">\n <div className=\"w-32 h-32 mx-auto rounded-2xl bg-neutral-800/50 flex items-center justify-center\">\n <svg\n className=\"w-16 h-16 text-neutral-600\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z\"\n />\n </svg>\n </div>\n <p className=\"text-neutral-500 text-lg\">No image available</p>\n </div>\n </div>\n ) : (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={imgSrc}\n alt=\"\"\n className=\"w-full h-full object-cover\"\n onError={() => setHasError(true)}\n />\n );\n\n if (!isEditing) {\n return <div className={className}>{imageNode}</div>;\n }\n\n return (\n <>\n <div className={cn(\"relative group\", className)}>\n {imageNode}\n\n <div className=\"absolute inset-0 flex items-center justify-center bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity\">\n {saving ? (\n <div className=\"w-8 h-8 border-4 border-white border-t-transparent rounded-full animate-spin\" />\n ) : (\n <div className=\"flex gap-12\">\n <CameraIcon\n className=\"w-12 h-12 text-white cursor-pointer hover:text-primary\"\n onClick={(e) => {\n e.stopPropagation();\n inputRef.current?.click();\n }}\n />\n <Link2Icon\n className=\"w-12 h-12 text-white cursor-pointer hover:text-primary\"\n onClick={(e) => {\n e.stopPropagation();\n setShowUrlModal(true);\n }}\n />\n </div>\n )}\n </div>\n\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/*\"\n disabled={saving}\n onChange={handleFileChange}\n className=\"absolute inset-0 opacity-0 pointer-events-none\"\n />\n </div>\n\n {showUrlModal &&\n createPortal(\n <div className=\"fixed inset-0 bg-black/70 flex items-center justify-center z-9999 p-4\">\n <div className=\"bg-neutral-900 rounded-xl p-6 max-w-sm w-full space-y-4\">\n <h3 className=\"text-lg font-bold\">Add Image URL</h3>\n\n <input\n type=\"text\"\n value={urlInput}\n onChange={(e) => handleUrlChange(e.target.value)}\n placeholder=\"https://example.com/image.png\"\n className=\"w-full px-3 py-2 rounded bg-neutral-800 border border-neutral-700\"\n />\n\n {urlPreview ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={urlPreview}\n alt=\"\"\n className=\"w-full h-40 object-contain rounded\"\n onError={() => setHasError(true)}\n />\n ) : (\n <div className=\"h-40 flex items-center justify-center text-neutral-500\">\n Invalid URL\n </div>\n )}\n\n <div className=\"flex justify-end gap-2\">\n <button\n onClick={() => setShowUrlModal(false)}\n className=\"px-3 py-1 bg-neutral-700 rounded text-sm\"\n >\n <XIcon className=\"w-4 h-4 inline\" /> Cancel\n </button>\n <button\n onClick={handleUrlConfirm}\n disabled={!urlPreview}\n className=\"px-3 py-1 bg-primary rounded text-sm disabled:opacity-50\"\n >\n <CheckIcon className=\"w-4 h-4 inline\" /> Confirm\n </button>\n </div>\n </div>\n </div>,\n document.body,\n )}\n </>\n );\n}\n","\"use client\";\n\nimport React, { useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport ReactMarkdown from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\nimport {\n EyeIcon,\n EditIcon,\n SaveIcon,\n XIcon,\n TypeIcon,\n BoldIcon,\n ItalicIcon,\n ListIcon,\n LinkIcon,\n CodeIcon,\n ImageIcon,\n Heading1Icon,\n Heading2Icon,\n} from \"lucide-react\";\nimport { toast } from \"sonner\";\nimport { cn } from \"./utils\";\n\ninterface MarkdownEditorProps {\n initialValue: string;\n onSave: (content: string) => void;\n trigger?: React.ReactNode;\n title?: string;\n}\n\nexport function MarkdownEditor({\n initialValue,\n onSave,\n trigger,\n title = \"Edit Content\",\n}: MarkdownEditorProps) {\n const [open, setOpen] = useState(false);\n const [content, setContent] = useState(initialValue);\n const [isPreview, setIsPreview] = useState(false);\n\n const handleOpen = () => {\n setContent(initialValue);\n setOpen(true);\n };\n\n const handleSave = () => {\n onSave(content);\n setOpen(false);\n };\n\n const handleCancel = () => {\n setContent(initialValue);\n setOpen(false);\n };\n\n const insertMarkdown = (\n before: string,\n after: string = \"\",\n placeholder: string = \"text\",\n ) => {\n const textarea = document.querySelector(\n \"textarea[data-markdown-editor]\",\n ) as HTMLTextAreaElement | null;\n if (!textarea) return;\n\n const start = textarea.selectionStart;\n const end = textarea.selectionEnd;\n const selectedText = content.substring(start, end) || placeholder;\n const newText =\n content.substring(0, start) +\n before +\n selectedText +\n after +\n content.substring(end);\n\n setContent(newText);\n\n setTimeout(() => {\n textarea.focus();\n const newCursorPos = start + before.length + selectedText.length;\n textarea.setSelectionRange(newCursorPos, newCursorPos);\n }, 0);\n };\n\n return (\n <>\n {trigger ? (\n <div onClick={handleOpen}>{trigger}</div>\n ) : (\n <button\n type=\"button\"\n onClick={handleOpen}\n className=\"inline-flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg border border-neutral-700 hover:bg-neutral-800 transition-colors\"\n >\n <EditIcon className=\"w-4 h-4\" />\n Edit Content\n </button>\n )}\n\n {open &&\n createPortal(\n <div className=\"fixed inset-0 z-[10001] flex items-center justify-center bg-black/70 p-4\">\n <div className=\"md:max-w-6xl w-full h-[90vh] flex flex-col bg-neutral-950 border border-neutral-800 rounded-xl overflow-hidden\">\n <div className=\"px-6 pt-6 pb-4 border-b border-neutral-800\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"text-2xl font-bold flex items-center gap-3\">\n <div className=\"p-2 bg-primary/10 rounded-lg\">\n <TypeIcon className=\"w-5 h-5 text-primary\" />\n </div>\n {title}\n </h2>\n <button\n type=\"button\"\n onClick={() => setIsPreview(!isPreview)}\n className={cn(\n \"flex items-center gap-2 px-4 py-2 rounded-lg transition-all font-medium\",\n isPreview\n ? \"bg-primary text-white\"\n : \"bg-neutral-800 hover:bg-neutral-700 text-neutral-300\",\n )}\n >\n {isPreview ? (\n <>\n <EditIcon className=\"w-4 h-4\" />\n Edit\n </>\n ) : (\n <>\n <EyeIcon className=\"w-4 h-4\" />\n Preview\n </>\n )}\n </button>\n </div>\n </div>\n\n <div className=\"flex-1 overflow-hidden flex flex-col\">\n {!isPreview && (\n <div className=\"flex items-center gap-1 px-4 py-3 border-b border-neutral-800 bg-neutral-900/50 overflow-x-auto\">\n <ToolbarButton\n icon={<Heading1Icon className=\"w-4 h-4\" />}\n label=\"Heading 1\"\n onClick={() => insertMarkdown(\"# \", \"\", \"Heading\")}\n />\n <ToolbarButton\n icon={<Heading2Icon className=\"w-4 h-4\" />}\n label=\"Heading 2\"\n onClick={() => insertMarkdown(\"## \", \"\", \"Heading\")}\n />\n <div className=\"w-px h-6 bg-neutral-700 mx-1\" />\n <ToolbarButton\n icon={<BoldIcon className=\"w-4 h-4\" />}\n label=\"Bold\"\n onClick={() => insertMarkdown(\"**\", \"**\", \"bold text\")}\n />\n <ToolbarButton\n icon={<ItalicIcon className=\"w-4 h-4\" />}\n label=\"Italic\"\n onClick={() => insertMarkdown(\"*\", \"*\", \"italic text\")}\n />\n <div className=\"w-px h-6 bg-neutral-700 mx-1\" />\n <ToolbarButton\n icon={<LinkIcon className=\"w-4 h-4\" />}\n label=\"Link\"\n onClick={() => insertMarkdown(\"[\", \"](url)\", \"link text\")}\n />\n <ToolbarButton\n icon={<ImageIcon className=\"w-4 h-4\" />}\n label=\"Image\"\n onClick={() => insertMarkdown(\"\", \"alt text\")}\n />\n <div className=\"w-px h-6 bg-neutral-700 mx-1\" />\n <ToolbarButton\n icon={<ListIcon className=\"w-4 h-4\" />}\n label=\"List\"\n onClick={() => insertMarkdown(\"- \", \"\", \"list item\")}\n />\n <ToolbarButton\n icon={<CodeIcon className=\"w-4 h-4\" />}\n label=\"Code\"\n onClick={() => insertMarkdown(\"```\\n\", \"\\n```\", \"code\")}\n />\n </div>\n )}\n\n <div className=\"flex-1 overflow-auto p-6\">\n {isPreview ? (\n <div className=\"prose prose-invert prose-lg max-w-none\">\n <ReactMarkdown remarkPlugins={[remarkGfm]}>\n {content}\n </ReactMarkdown>\n </div>\n ) : (\n <div className=\"h-full flex flex-col\">\n <textarea\n data-markdown-editor\n value={content}\n onChange={(e) => setContent(e.target.value)}\n className=\"h-full w-full resize-none rounded-md font-mono text-sm bg-neutral-900/50 border border-neutral-800 p-3 outline-none focus:border-primary\"\n placeholder={\"# Project Title\\n\\n## Overview\\nWrite your description here...\"}\n />\n\n <div className=\"mt-4 p-4 bg-neutral-900/50 border border-neutral-800 rounded-lg\">\n <h4 className=\"text-sm font-semibold mb-3 text-neutral-300 flex items-center gap-2\">\n <CodeIcon className=\"w-4 h-4 text-primary\" />\n Markdown Guide\n </h4>\n <div className=\"grid grid-cols-2 md:grid-cols-3 gap-3 text-xs\">\n <GuideItem code=\"# Heading 1\" desc=\"Main heading\" />\n <GuideItem code=\"## Heading 2\" desc=\"Sub heading\" />\n <GuideItem code=\"**bold**\" desc=\"Bold text\" />\n <GuideItem code=\"*italic*\" desc=\"Italic text\" />\n <GuideItem code=\"[link](url)\" desc=\"Hyperlink\" />\n <GuideItem code=\"- list item\" desc=\"Bullet list\" />\n </div>\n </div>\n </div>\n )}\n </div>\n </div>\n\n <div className=\"px-6 py-4 border-t border-neutral-800 bg-neutral-900/30\">\n <div className=\"flex items-center justify-between w-full\">\n <p className=\"text-sm text-neutral-500\">\n {content.length} characters\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={handleCancel}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm rounded-md border border-neutral-700 hover:bg-neutral-800 transition-colors\"\n >\n <XIcon className=\"w-4 h-4\" />\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={handleSave}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm rounded-md bg-primary text-white hover:opacity-90 transition-opacity\"\n >\n <SaveIcon className=\"w-4 h-4\" />\n Save Content\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>,\n document.body,\n )}\n </>\n );\n}\n\nfunction ToolbarButton({\n icon,\n label,\n onClick,\n}: {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n title={label}\n className=\"p-2 rounded-lg hover:bg-neutral-800 text-neutral-400 hover:text-white transition-colors\"\n >\n {icon}\n </button>\n );\n}\n\nfunction GuideItem({ code, desc }: { code: string; desc: string }) {\n return (\n <div className=\"flex flex-col gap-1\">\n <code className=\"text-primary bg-primary/10 px-2 py-1 rounded text-xs font-mono\">\n {code}\n </code>\n <span className=\"text-neutral-500\">{desc}</span>\n </div>\n );\n}\n\ninterface ProjectContentEditorProps {\n content: string;\n onSave: (content: string) => Promise<void>;\n}\n\nexport function ProjectContentEditor({\n content,\n onSave,\n}: ProjectContentEditorProps) {\n const [saving, setSaving] = useState(false);\n\n const handleSave = async (newContent: string) => {\n setSaving(true);\n try {\n await onSave(newContent);\n toast.success(\"Content saved successfully!\");\n } catch (error) {\n console.error(\"Failed to save content:\", error);\n toast.error(\"Failed to save content\");\n } finally {\n setSaving(false);\n }\n };\n\n return (\n <MarkdownEditor\n initialValue={content}\n onSave={handleSave}\n title=\"Edit Project Content\"\n trigger={\n <button\n className=\"px-4 py-2 bg-neutral-800 rounded-lg hover:bg-neutral-700 transition-colors flex items-center gap-2 border border-neutral-700 hover:border-primary/50\"\n disabled={saving}\n >\n <EditIcon className=\"w-4 h-4\" />\n {saving ? \"Saving...\" : \"Edit Full Content\"}\n </button>\n }\n />\n );\n}\n"],"mappings":"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","names":["createContext","useContext","jsx","useState","useCallback","jsx","useState","useCallback","useRef","useState","Fragment","jsx","useState","useRef","useState","createPortal","XIcon","toast","Fragment","jsx","jsxs","useState","createPortal","XIcon","toast"]}
|
|
1
|
+
{"version":3,"sources":["../../src/client/PageProvider.tsx","../../src/client/auth.tsx","../../src/client/ContentEditSpan.tsx","../../src/client/EditableImage.tsx","../../src/client/MarkdownEditor.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useState,\n useCallback,\n type ReactNode,\n} from \"react\";\nimport type {\n ClientStorageAdapter,\n NestedSections,\n PendingImage,\n Section,\n} from \"../types\";\n\nexport type { PendingImage } from \"../types\";\n\n/**\n * Notification sink. The package ships a dependency-free console default so it\n * imposes no toast library; pass your own (e.g. a `sonner`-backed sink) via the\n * `notify` prop to surface UI toasts.\n */\nexport interface Notifier {\n success: (message: string) => void;\n error: (message: string) => void;\n}\n\nconst defaultNotifier: Notifier = {\n success: (m) => console.info(`[cms] ${m}`),\n error: (m) => console.error(`[cms] ${m}`),\n};\n\ninterface PageContextType {\n sections: NestedSections;\n hasUnsavedChanges: boolean;\n saving: boolean;\n pendingImages: PendingImage[];\n setSection: (collection: string, key: string, section: Section) => void;\n editField: (\n collection: string,\n sectionKey: string,\n fieldKey: string,\n value: unknown,\n ) => void;\n setPendingImage: (image: PendingImage) => void;\n saveSection: (collection: string, sectionKey: string) => Promise<void>;\n saveAll: () => Promise<void>;\n}\n\nconst PageContext = createContext<PageContextType | undefined>(undefined);\n\nconst dirtyKey = (collection: string, sectionKey: string) =>\n `${collection}:${sectionKey}`;\n\nexport interface PageProviderProps {\n children: ReactNode;\n /** Server-rendered sections to hydrate from. */\n initialSections?: NestedSections;\n /**\n * Base path of the CMS API route mounted via `createCmsHandlers`. Saves\n * `PATCH` to `${apiBasePath}/{collection}/{id}`. Defaults to `/api/admin`.\n */\n apiBasePath?: string;\n /** Client storage adapter used to upload pending (non-external) images on save. */\n storage?: ClientStorageAdapter;\n /** Notification sink. Defaults to `sonner` toasts. */\n notify?: Notifier;\n}\n\nexport const PageProvider = ({\n children,\n initialSections = {},\n apiBasePath = \"/api/admin\",\n storage,\n notify = defaultNotifier,\n}: PageProviderProps) => {\n const [saving, setSaving] = useState(false);\n const [sections, setSections] = useState<NestedSections>(initialSections);\n const [pendingImages, setPendingImages] = useState<PendingImage[]>([]);\n const [dirtySections, setDirtySections] = useState<Set<string>>(new Set());\n\n const hasUnsavedChanges = dirtySections.size > 0;\n\n const resolveImageUrl = useCallback(\n async (img: PendingImage): Promise<string> => {\n if (img.isExternal) return img.localUrl;\n if (!storage) {\n throw new Error(\n \"PageProvider received a file upload but no `storage` adapter was provided.\",\n );\n }\n const { url } = await storage.upload(img.file!);\n return url;\n },\n [storage],\n );\n\n const setSection = useCallback(\n (collection: string, key: string, section: Section) => {\n setSections((prev) => ({\n ...prev,\n [collection]: { ...prev[collection], [key]: section },\n }));\n },\n [],\n );\n\n const editField = useCallback(\n (\n collection: string,\n sectionKey: string,\n fieldKey: string,\n value: unknown,\n ) => {\n setSections((prev) => {\n const currentSection = prev[collection]?.[sectionKey];\n\n if (!currentSection) {\n console.error(`Section not found: ${collection}/${sectionKey}`);\n return prev;\n }\n\n const keys = fieldKey.split(\".\");\n const updated: Section = { ...currentSection };\n\n if (keys.length === 1) {\n updated[fieldKey] = value;\n } else {\n let current: Record<string, unknown> = updated;\n for (let i = 0; i < keys.length - 1; i++) {\n if (!current[keys[i]]) current[keys[i]] = {};\n current[keys[i]] = { ...(current[keys[i]] as object) };\n current = current[keys[i]] as Record<string, unknown>;\n }\n current[keys[keys.length - 1]] = value;\n }\n\n return {\n ...prev,\n [collection]: { ...prev[collection], [sectionKey]: updated },\n };\n });\n\n setDirtySections((prev) => {\n const next = new Set(prev);\n next.add(dirtyKey(collection, sectionKey));\n return next;\n });\n },\n [],\n );\n\n const setPendingImage = useCallback((image: PendingImage) => {\n setPendingImages((prev) => [\n ...prev.filter(\n (img) =>\n !(\n img.collection === image.collection &&\n img.sectionKey === image.sectionKey &&\n img.fieldKey === image.fieldKey\n ),\n ),\n image,\n ]);\n\n setDirtySections((prev) => {\n const next = new Set(prev);\n next.add(dirtyKey(image.collection, image.sectionKey));\n return next;\n });\n }, []);\n\n const persist = useCallback(\n async (section: Section) => {\n const response = await fetch(\n `${apiBasePath}/${section.collection}/${section.id}`,\n {\n method: \"PATCH\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(section),\n },\n );\n if (!response.ok) throw new Error(\"Failed to save section\");\n },\n [apiBasePath],\n );\n\n const saveSection = useCallback(\n async (collection: string, sectionKey: string) => {\n if (saving) return;\n setSaving(true);\n\n try {\n const section = sections[collection]?.[sectionKey];\n if (!section?.id || !section?.collection) {\n console.error(`Invalid section: ${collection}/${sectionKey}`);\n setSaving(false);\n return;\n }\n\n const images = pendingImages.filter(\n (img) =>\n img.collection === collection && img.sectionKey === sectionKey,\n );\n\n let updatedSection: Section = { ...section };\n\n for (const img of images) {\n const url = await resolveImageUrl(img);\n const keys = img.fieldKey.split(\".\");\n if (keys.length === 1) {\n updatedSection = { ...updatedSection, [img.fieldKey]: url };\n } else {\n let current: Record<string, unknown> = updatedSection;\n for (let i = 0; i < keys.length - 1; i++) {\n if (!current[keys[i]]) current[keys[i]] = {};\n current = current[keys[i]] as Record<string, unknown>;\n }\n current[keys[keys.length - 1]] = url;\n }\n }\n\n await persist(updatedSection);\n\n setSections((prev) => ({\n ...prev,\n [collection]: { ...prev[collection], [sectionKey]: updatedSection },\n }));\n\n setPendingImages((prev) =>\n prev.filter(\n (img) =>\n !(img.collection === collection && img.sectionKey === sectionKey),\n ),\n );\n\n setDirtySections((prev) => {\n const next = new Set(prev);\n next.delete(dirtyKey(collection, sectionKey));\n return next;\n });\n\n notify.success(\"Changes saved successfully!\");\n } catch (error) {\n console.error(\"Save failed:\", error);\n notify.error(\"Failed to save changes\");\n } finally {\n setSaving(false);\n }\n },\n [sections, pendingImages, saving, resolveImageUrl, persist, notify],\n );\n\n const saveAll = useCallback(async () => {\n if (saving || dirtySections.size === 0) return;\n setSaving(true);\n\n try {\n const updatedSections: NestedSections = { ...sections };\n\n for (const img of pendingImages) {\n const url = await resolveImageUrl(img);\n\n if (!updatedSections[img.collection])\n updatedSections[img.collection] = {};\n\n if (!updatedSections[img.collection][img.sectionKey]) {\n updatedSections[img.collection][img.sectionKey] = {\n id: img.docId,\n collection: img.collection,\n };\n }\n\n const keys = img.fieldKey.split(\".\");\n let current: Record<string, unknown> =\n updatedSections[img.collection][img.sectionKey];\n\n if (keys.length === 1) {\n updatedSections[img.collection][img.sectionKey] = {\n ...(current as Section),\n [img.fieldKey]: url,\n };\n } else {\n for (let i = 0; i < keys.length - 1; i++) {\n if (!current[keys[i]]) current[keys[i]] = {};\n current = current[keys[i]] as Record<string, unknown>;\n }\n current[keys[keys.length - 1]] = url;\n }\n }\n\n for (const entry of dirtySections) {\n const [collection, key] = entry.split(\":\");\n const section = updatedSections[collection]?.[key];\n\n if (!section?.id || !section?.collection) {\n console.error(`Invalid section for save: ${entry}`);\n continue;\n }\n\n await persist(section);\n }\n\n setSections(updatedSections);\n setPendingImages([]);\n setDirtySections(new Set());\n notify.success(\"All changes saved successfully!\");\n } catch (error) {\n console.error(\"Save all failed:\", error);\n notify.error(\"Failed to save changes\");\n } finally {\n setSaving(false);\n }\n }, [sections, pendingImages, dirtySections, saving, resolveImageUrl, persist, notify]);\n\n return (\n <PageContext.Provider\n value={{\n sections,\n hasUnsavedChanges,\n pendingImages,\n saving,\n setSection,\n editField,\n setPendingImage,\n saveSection,\n saveAll,\n }}\n >\n {children}\n </PageContext.Provider>\n );\n};\n\nexport const usePageContext = () => {\n const context = useContext(PageContext);\n if (!context)\n throw new Error(\"usePageContext must be used within a PageProvider\");\n return context;\n};\n","\"use client\";\n\nimport { createContext, useContext, type ReactNode } from \"react\";\nimport type { CmsAuthState } from \"../types\";\n\n/**\n * The single source of client-side auth state for the engine. Every built-in\n * auth provider (and any custom one) feeds this context; the edit primitives\n * read it via {@link useCmsAuth}.\n *\n * Cross-entry note: provider packages (e.g. `…/auth/firebase/client`) import\n * this context through the package's public `…/client` specifier so they share\n * the *same* context instance as the consumer's primitives at runtime.\n */\nexport const CmsAuthContext = createContext<CmsAuthState | undefined>(undefined);\n\nexport function useCmsAuth(): CmsAuthState {\n const ctx = useContext(CmsAuthContext);\n if (!ctx) {\n throw new Error(\n \"useCmsAuth must be used within a CmsAuthProvider (or a built-in auth provider such as FirebaseAuthProvider).\",\n );\n }\n return ctx;\n}\n\n/**\n * Controlled provider for consumers wiring their own auth. Pass the resolved\n * {@link CmsAuthState}; the built-in providers wrap this for you.\n */\nexport function CmsAuthProvider({\n value,\n children,\n}: {\n value: CmsAuthState;\n children: ReactNode;\n}) {\n return (\n <CmsAuthContext.Provider value={value}>{children}</CmsAuthContext.Provider>\n );\n}\n","\"use client\";\n\nimport React, { useRef, useEffect, useState, useCallback } from \"react\";\nimport { usePageContext } from \"./PageProvider\";\nimport { useCmsAuth } from \"./auth\";\n\n/**\n * Inline-editable text primitive. Headless: it wires `contentEditable`, reads\n * and persists the field, and exposes edit state via `data-*` attributes —\n * styling and rich-text rendering are entirely yours.\n *\n * - Style it with `className` and the `data-cms-editing` / `data-cms-focused`\n * attributes (no built-in look, no Tailwind, no design tokens).\n * - Supply `renderValue` to turn the stored raw string into rich nodes (e.g. a\n * markdown parser). Defaults to plain text.\n */\ninterface ContentEditSpanProps {\n collection: string;\n sectionKey: string;\n fieldKey: string;\n className?: string;\n children?: React.ReactNode;\n /** Element/tag to render. Defaults to `span`. */\n as?: React.ElementType;\n /** Render the stored raw string into nodes. Defaults to plain text. */\n renderValue?: (raw: string) => React.ReactNode;\n}\n\nconst defaultRenderValue = (raw: string): React.ReactNode => raw;\n\nfunction getNestedValue(obj: unknown, path: string): unknown {\n const keys = path.split(\".\");\n let current: unknown = obj;\n\n for (const key of keys) {\n if (current == null || (current as Record<string, unknown>)[key] === undefined)\n return undefined;\n current = (current as Record<string, unknown>)[key];\n }\n\n return current;\n}\n\nexport default function ContentEditSpan({\n collection,\n sectionKey,\n fieldKey,\n className,\n children,\n as = \"span\",\n renderValue = defaultRenderValue,\n}: ContentEditSpanProps) {\n const { sections, editField } = usePageContext();\n const { isEditing } = useCmsAuth();\n\n const section = sections[collection]?.[sectionKey];\n const raw =\n (getNestedValue(section, fieldKey) as string) ??\n (typeof children === \"string\" ? children : \"\");\n\n const Component = as;\n\n if (!isEditing) {\n return <Component className={className}>{renderValue(raw)}</Component>;\n }\n\n return (\n <EditableContentSpan\n collection={collection}\n sectionKey={sectionKey}\n fieldKey={fieldKey}\n className={className}\n raw={raw}\n editField={editField}\n as={as}\n renderValue={renderValue}\n />\n );\n}\n\nfunction EditableContentSpan({\n collection,\n sectionKey,\n fieldKey,\n className,\n raw,\n editField,\n as: Component = \"span\",\n renderValue,\n}: {\n collection: string;\n sectionKey: string;\n fieldKey: string;\n className?: string;\n raw: string;\n editField: (\n collection: string,\n sectionKey: string,\n fieldKey: string,\n value: string,\n ) => void;\n as?: React.ElementType;\n renderValue: (raw: string) => React.ReactNode;\n}) {\n const { isEditing } = useCmsAuth();\n const [isFocused, setIsFocused] = useState(false);\n const [editValue, setEditValue] = useState(raw);\n const contentRef = useRef<HTMLElement>(null);\n const rawRef = useRef(raw);\n\n useEffect(() => {\n if (!isFocused && rawRef.current !== raw) {\n rawRef.current = raw;\n }\n }, [raw, isFocused]);\n\n useEffect(() => {\n if (!isFocused && rawRef.current !== editValue) {\n setEditValue(rawRef.current);\n }\n }, [isFocused, editValue]);\n\n const handleInput = useCallback(() => {\n if (contentRef.current) {\n setEditValue(contentRef.current.textContent || \"\");\n }\n }, []);\n\n const handleBlur = useCallback(() => {\n setIsFocused(false);\n if (editValue !== raw) {\n editField(collection, sectionKey, fieldKey, editValue);\n }\n }, [editValue, raw, collection, sectionKey, fieldKey, editField]);\n\n const handleFocus = useCallback(() => {\n setIsFocused(true);\n if (contentRef.current) {\n contentRef.current.textContent = editValue;\n setTimeout(() => {\n if (contentRef.current) {\n const range = document.createRange();\n const sel = window.getSelection();\n range.selectNodeContents(contentRef.current);\n range.collapse(false);\n sel?.removeAllRanges();\n sel?.addRange(range);\n }\n }, 0);\n }\n }, [editValue]);\n\n return (\n <Component\n key={isFocused ? \"editing\" : \"static\"}\n ref={contentRef}\n className={className}\n data-cms-editable=\"\"\n data-cms-editing={isEditing ? \"\" : undefined}\n data-cms-focused={isFocused ? \"\" : undefined}\n contentEditable={isEditing}\n suppressContentEditableWarning\n onInput={handleInput}\n onBlur={handleBlur}\n onFocus={handleFocus}\n >\n {!isFocused && renderValue(editValue)}\n </Component>\n );\n}\n","\"use client\";\n\nimport React, { useRef, useState } from \"react\";\nimport { useCmsAuth } from \"./auth\";\nimport { usePageContext } from \"./PageProvider\";\n\n/** State + actions handed to the {@link EditableImage} render-prop. */\nexport interface EditableImageRenderState {\n /** URL to display (pending upload preview, external URL, or the saved src). */\n src: string;\n isEditing: boolean;\n saving: boolean;\n /** True after the current `src` failed to load. */\n hasError: boolean;\n /** Open the native file picker (a hidden input is managed for you). */\n openFilePicker: () => void;\n /**\n * Queue an external image URL. Returns `false` if it isn't a valid http(s)\n * URL (nothing is changed in that case).\n */\n setExternalUrl: (url: string) => boolean;\n /** Convenience props for an `<img>`: `{ src, onError }`. */\n imgProps: { src: string; onError: () => void };\n}\n\ninterface EditableImageProps {\n sectionKey: string;\n fieldKey: string;\n src: string;\n collection: string;\n docId: string;\n className?: string;\n /**\n * Render the image and any editing chrome (overlay, buttons, URL modal). The\n * package ships no visual look — bring your own. When omitted, a bare\n * unstyled `<img>` is rendered.\n */\n children?: (state: EditableImageRenderState) => React.ReactNode;\n}\n\nexport default function EditableImage({\n sectionKey,\n fieldKey,\n src,\n collection,\n docId,\n className,\n children,\n}: EditableImageProps) {\n const { isEditing } = useCmsAuth();\n const { editField, setPendingImage, pendingImages, saving } =\n usePageContext();\n\n const [preview, setPreview] = useState(src);\n const [hasError, setHasError] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const pendingImage = pendingImages.find(\n (img) => img.sectionKey === sectionKey && img.fieldKey === fieldKey,\n );\n const imgSrc = pendingImage?.localUrl || preview;\n\n const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n if (saving) return;\n const file = e.target.files?.[0];\n if (!file) return;\n\n const localUrl = URL.createObjectURL(file);\n setPreview(localUrl);\n setHasError(false);\n\n editField(collection, sectionKey, fieldKey, localUrl);\n setPendingImage({\n file,\n localUrl,\n sectionKey,\n fieldKey,\n collection,\n docId,\n isExternal: false,\n });\n };\n\n const openFilePicker = () => {\n if (saving) return;\n inputRef.current?.click();\n };\n\n const setExternalUrl = (value: string): boolean => {\n let valid = false;\n try {\n const url = new URL(value);\n valid = url.protocol === \"http:\" || url.protocol === \"https:\";\n } catch {\n valid = false;\n }\n if (!valid) return false;\n\n setPreview(value);\n setHasError(false);\n editField(collection, sectionKey, fieldKey, value);\n setPendingImage({\n file: null,\n localUrl: value,\n sectionKey,\n fieldKey,\n collection,\n docId,\n isExternal: true,\n });\n return true;\n };\n\n const state: EditableImageRenderState = {\n src: imgSrc,\n isEditing,\n saving,\n hasError,\n openFilePicker,\n setExternalUrl,\n imgProps: { src: imgSrc, onError: () => setHasError(true) },\n };\n\n return (\n <div className={className}>\n {/* Internal hidden file input — driven via openFilePicker(). */}\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/*\"\n disabled={saving}\n onChange={handleFileChange}\n style={{ display: \"none\" }}\n />\n {children ? (\n children(state)\n ) : (\n // eslint-disable-next-line @next/next/no-img-element\n <img {...state.imgProps} alt=\"\" />\n )}\n </div>\n );\n}\n","\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\n\nexport interface UseMarkdownEditorOptions {\n initialValue: string;\n onSave: (content: string) => void | Promise<void>;\n}\n\nexport interface MarkdownEditorApi {\n /** Current editor text. */\n value: string;\n setValue: (next: string) => void;\n /** Attach to your `<textarea>` so `insert` can target the selection. */\n textareaRef: React.RefObject<HTMLTextAreaElement | null>;\n /**\n * Wrap the current selection (or insert a placeholder) with `before`/`after`\n * markers — e.g. `insert(\"**\", \"**\", \"bold text\")`. Restores focus and caret.\n */\n insert: (before: string, after?: string, placeholder?: string) => void;\n /** Reset back to `initialValue` (or a provided value). */\n reset: (to?: string) => void;\n /** Persist the current value via the provided `onSave`. */\n save: () => void | Promise<void>;\n charCount: number;\n}\n\n/**\n * Headless markdown-editing logic: value state, a selection-aware `insert`\n * command, and save/reset. The package ships **no** modal, toolbar, icons, or\n * preview renderer — compose those yourself (e.g. with `react-markdown`). This\n * keeps the package free of any UI library or visual opinion.\n *\n * ```tsx\n * const md = useMarkdownEditor({ initialValue, onSave });\n * <textarea ref={md.textareaRef} value={md.value}\n * onChange={(e) => md.setValue(e.target.value)} />\n * <button onClick={() => md.insert(\"**\", \"**\", \"bold\")}>Bold</button>\n * ```\n */\nexport function useMarkdownEditor({\n initialValue,\n onSave,\n}: UseMarkdownEditorOptions): MarkdownEditorApi {\n const [value, setValue] = useState(initialValue);\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const insert = useCallback(\n (before: string, after = \"\", placeholder = \"text\") => {\n const textarea = textareaRef.current;\n const start = textarea?.selectionStart ?? value.length;\n const end = textarea?.selectionEnd ?? value.length;\n const selected = value.substring(start, end) || placeholder;\n const next =\n value.substring(0, start) +\n before +\n selected +\n after +\n value.substring(end);\n\n setValue(next);\n\n setTimeout(() => {\n if (!textarea) return;\n textarea.focus();\n const caret = start + before.length + selected.length;\n textarea.setSelectionRange(caret, caret);\n }, 0);\n },\n [value],\n );\n\n const reset = useCallback(\n (to: string = initialValue) => setValue(to),\n [initialValue],\n );\n\n const save = useCallback(() => onSave(value), [onSave, value]);\n\n return {\n value,\n setValue,\n textareaRef,\n insert,\n reset,\n save,\n charCount: value.length,\n };\n}\n"],"mappings":"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","names":["createContext","useContext","jsx","useState","useCallback","jsx","useState","useCallback","useRef","useState","jsx","useState","useRef","useCallback","useRef","useState"]}
|
package/dist/index.cjs
CHANGED
|
@@ -3,6 +3,10 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
6
10
|
var __copyProps = (to, from, except, desc) => {
|
|
7
11
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
8
12
|
for (let key of __getOwnPropNames(from))
|
|
@@ -15,5 +19,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
15
19
|
|
|
16
20
|
// src/index.ts
|
|
17
21
|
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
isFilterGroup: () => isFilterGroup
|
|
24
|
+
});
|
|
18
25
|
module.exports = __toCommonJS(src_exports);
|
|
26
|
+
|
|
27
|
+
// src/types.ts
|
|
28
|
+
function isFilterGroup(c) {
|
|
29
|
+
return typeof c === "object" && c !== null && "or" in c;
|
|
30
|
+
}
|
|
31
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
32
|
+
0 && (module.exports = {
|
|
33
|
+
isFilterGroup
|
|
34
|
+
});
|
|
19
35
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Root entry — shared types only. No runtime; safe to import anywhere.\nexport * from \"./types\";\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/types.ts"],"sourcesContent":["// Root entry — shared types only. No runtime; safe to import anywhere.\nexport * from \"./types\";\n","/**\n * Shared, runtime-free types. Safe to import in any environment (client or\n * server) — this module pulls in zero runtime dependencies.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/**\n * Engine addressing fields. The engine needs to know an entity's `id` and which\n * `collection` it lives in to route saves; everything else is the consumer's own\n * shape. Kept separate from the user's content type `T` so we never force these\n * onto domain models — use {@link Editable}<YourType> when you want both.\n */\nexport interface EntityAddress {\n id: string;\n collection: string;\n}\n\n/**\n * The user's content type `T` decorated with the engine's addressing fields.\n * `T` is completely unconstrained — bring any shape; the engine only adds `id`\n * and `collection`.\n */\nexport type Editable<T = Record<string, any>> = T & EntityAddress;\n\n/**\n * A single editable record. Schemaless by design. Equivalent to\n * `Editable<Record<string, any>>`; kept as a named alias for the common case and\n * for backwards compatibility.\n */\nexport type Section = Editable;\n\nexport type SectionMap = Record<string, Section>;\n\n/**\n * The shape the engine holds in memory:\n * `{ [collection]: { [sectionKey]: Section } }`.\n */\nexport type NestedSections = {\n [collection: string]: {\n [sectionKey: string]: Section;\n };\n};\n\n/**\n * An image edit queued in the provider. The actual upload is deferred until\n * save. `file` is null when the user pasted an external URL (`isExternal`).\n */\nexport interface PendingImage {\n file: File | null;\n localUrl: string;\n sectionKey: string;\n fieldKey: string;\n collection: string;\n docId: string;\n isExternal?: boolean;\n}\n\n/**\n * Neutral query language. Keeps any backend's native query type (Firestore's\n * `Query`, SQL, etc.) from leaking through the engine.\n *\n * Op support is backend-dependent — see each adapter. `contains` is a\n * case-insensitive substring match; `in`/`nin` take an array value.\n */\nexport type QueryFilterOp =\n | \"eq\"\n | \"ne\"\n | \"lt\"\n | \"lte\"\n | \"gt\"\n | \"gte\"\n | \"in\"\n | \"nin\"\n | \"contains\";\n\n/** A single field condition. */\nexport type QueryFilter = { field: string; op: QueryFilterOp; value: unknown };\n\n/**\n * A disjunction: the inner filters are combined with OR. Sits alongside plain\n * filters in `Query.filters`, which are combined with AND at the top level.\n */\nexport type QueryFilterGroup = { or: QueryFilter[] };\n\n/** Either a bare condition (AND-ed) or an OR group. */\nexport type QueryCondition = QueryFilter | QueryFilterGroup;\n\nexport type Query = {\n /** Top-level conditions combined with AND. Use `{ or: [...] }` for disjunction. */\n filters?: QueryCondition[];\n orderBy?: { field: string; direction: \"asc\" | \"desc\" }[];\n limit?: number;\n /** Skip this many rows (offset pagination). */\n offset?: number;\n /**\n * Fields holding references to other documents to inline-resolve after the\n * primary fetch. Resolved by {@link resolveRelations}, never by the adapter.\n */\n populate?: string[];\n};\n\n/** Narrow a {@link QueryCondition} to an OR group. */\nexport function isFilterGroup(c: QueryCondition): c is QueryFilterGroup {\n return typeof c === \"object\" && c !== null && \"or\" in c;\n}\n\n/**\n * A reference to another document. Either self-describing (`{ collection, id }`)\n * or a bare id string resolved via a {@link RelationConfig}.\n */\nexport type Ref = { collection: string; id: string };\n\n/**\n * Maps a reference field name → the collection it points to. Needed only when\n * refs are stored as bare id strings (self-describing `Ref` objects don't need\n * it). Used by {@link resolveRelations}.\n */\nexport type RelationConfig = Record<string, { collection: string }>;\n\n/**\n * Persistence contract. Every backend (Firestore, Postgres, …) implements this;\n * the engine and the route factory only ever speak to this interface.\n */\nexport interface DataAdapter {\n fetchCollection<T = Record<string, any>>(\n collection: string,\n q?: Query,\n ): Promise<(T & { id: string })[]>;\n fetchById<T = Record<string, any>>(\n collection: string,\n id: string,\n ): Promise<(T & { id: string }) | null>;\n create<T = Record<string, any>>(\n collection: string,\n data: T,\n ): Promise<T & { id: string }>;\n createWithId<T = Record<string, any>>(\n collection: string,\n id: string,\n data: T,\n ): Promise<T & { id: string }>;\n update<T = Record<string, any>>(\n collection: string,\n id: string,\n data: Partial<T>,\n ): Promise<void>;\n upsert<T = Record<string, any>>(\n collection: string,\n id: string,\n data: Partial<T>,\n ): Promise<void>;\n delete(collection: string, id: string): Promise<void>;\n}\n\n/**\n * The identity resolved from an incoming request by an {@link AuthAdapter}.\n * Minimal by design: only `isAdmin` is meaningful to the default gate. Carry any\n * additional claims (roles, scopes, tenant, …) as extra keys and authorize on\n * them with a custom `authorize` predicate. `userId`/`email` are conventional\n * but optional.\n */\nexport interface AuthIdentity {\n isAdmin: boolean;\n userId?: string;\n email?: string;\n [claim: string]: unknown;\n}\n\n/**\n * Decides whether a resolved identity may perform admin actions. Defaults to\n * `identity.isAdmin === true`; override to gate on roles/scopes/etc.\n */\nexport type AuthorizeFn = (\n identity: AuthIdentity,\n req: Request,\n) => boolean | Promise<boolean>;\n\n/**\n * Server-side auth contract. Gates every admin API route. Return `null` to\n * reject outright; otherwise the gate's `authorize` predicate decides.\n */\nexport interface AuthAdapter {\n verifyRequest(req: Request): Promise<AuthIdentity | null>;\n}\n\n/**\n * Client half of storage: performs the browser-side upload and returns the\n * final URL. Has zero server dependencies, so it is safe to import in client\n * components. Built from e.g. `@dalgoridim/headless-cms/storage/cloudinary`.\n */\nexport interface ClientStorageAdapter {\n upload(file: File): Promise<{ url: string }>;\n}\n\n/**\n * Server half of storage: issues a presign / signature (or, for local storage,\n * writes the file). Mounted by the route factory at `${apiBasePath}/sign`.\n * Pulls in server-only SDKs, so it lives in a `/server` subpath. Built from e.g.\n * `@dalgoridim/headless-cms/storage/cloudinary/server`.\n */\nexport interface ServerStorageAdapter {\n sign(req: Request): Promise<unknown>;\n}\n\n/** Full two-sided contract (rarely needed; client and server halves are split). */\nexport interface StorageAdapter extends ClientStorageAdapter {\n sign?(req: Request): Promise<unknown>;\n}\n\n/**\n * Minimal client-side auth state the edit primitives depend on. Any auth\n * implementation (Firebase, NextAuth, custom) provides this via a context;\n * `@dalgoridim/headless-cms/auth/firebase/client` ships the default.\n */\nexport interface CmsAuthState {\n isAdmin: boolean;\n isEditing: boolean;\n toggleEdit: () => void;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACuGO,SAAS,cAAc,GAA0C;AACtE,SAAO,OAAO,MAAM,YAAY,MAAM,QAAQ,QAAQ;AACxD;","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -3,14 +3,27 @@
|
|
|
3
3
|
* server) — this module pulls in zero runtime dependencies.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
6
|
+
* Engine addressing fields. The engine needs to know an entity's `id` and which
|
|
7
|
+
* `collection` it lives in to route saves; everything else is the consumer's own
|
|
8
|
+
* shape. Kept separate from the user's content type `T` so we never force these
|
|
9
|
+
* onto domain models — use {@link Editable}<YourType> when you want both.
|
|
8
10
|
*/
|
|
9
|
-
interface
|
|
11
|
+
interface EntityAddress {
|
|
10
12
|
id: string;
|
|
11
13
|
collection: string;
|
|
12
|
-
[key: string]: any;
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* The user's content type `T` decorated with the engine's addressing fields.
|
|
17
|
+
* `T` is completely unconstrained — bring any shape; the engine only adds `id`
|
|
18
|
+
* and `collection`.
|
|
19
|
+
*/
|
|
20
|
+
type Editable<T = Record<string, any>> = T & EntityAddress;
|
|
21
|
+
/**
|
|
22
|
+
* A single editable record. Schemaless by design. Equivalent to
|
|
23
|
+
* `Editable<Record<string, any>>`; kept as a named alias for the common case and
|
|
24
|
+
* for backwards compatibility.
|
|
25
|
+
*/
|
|
26
|
+
type Section = Editable;
|
|
14
27
|
type SectionMap = Record<string, Section>;
|
|
15
28
|
/**
|
|
16
29
|
* The shape the engine holds in memory:
|
|
@@ -37,20 +50,60 @@ interface PendingImage {
|
|
|
37
50
|
/**
|
|
38
51
|
* Neutral query language. Keeps any backend's native query type (Firestore's
|
|
39
52
|
* `Query`, SQL, etc.) from leaking through the engine.
|
|
53
|
+
*
|
|
54
|
+
* Op support is backend-dependent — see each adapter. `contains` is a
|
|
55
|
+
* case-insensitive substring match; `in`/`nin` take an array value.
|
|
56
|
+
*/
|
|
57
|
+
type QueryFilterOp = "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "nin" | "contains";
|
|
58
|
+
/** A single field condition. */
|
|
59
|
+
type QueryFilter = {
|
|
60
|
+
field: string;
|
|
61
|
+
op: QueryFilterOp;
|
|
62
|
+
value: unknown;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* A disjunction: the inner filters are combined with OR. Sits alongside plain
|
|
66
|
+
* filters in `Query.filters`, which are combined with AND at the top level.
|
|
40
67
|
*/
|
|
41
|
-
type
|
|
68
|
+
type QueryFilterGroup = {
|
|
69
|
+
or: QueryFilter[];
|
|
70
|
+
};
|
|
71
|
+
/** Either a bare condition (AND-ed) or an OR group. */
|
|
72
|
+
type QueryCondition = QueryFilter | QueryFilterGroup;
|
|
42
73
|
type Query = {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
op: QueryFilterOp;
|
|
46
|
-
value: unknown;
|
|
47
|
-
}[];
|
|
74
|
+
/** Top-level conditions combined with AND. Use `{ or: [...] }` for disjunction. */
|
|
75
|
+
filters?: QueryCondition[];
|
|
48
76
|
orderBy?: {
|
|
49
77
|
field: string;
|
|
50
78
|
direction: "asc" | "desc";
|
|
51
79
|
}[];
|
|
52
80
|
limit?: number;
|
|
81
|
+
/** Skip this many rows (offset pagination). */
|
|
82
|
+
offset?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Fields holding references to other documents to inline-resolve after the
|
|
85
|
+
* primary fetch. Resolved by {@link resolveRelations}, never by the adapter.
|
|
86
|
+
*/
|
|
87
|
+
populate?: string[];
|
|
88
|
+
};
|
|
89
|
+
/** Narrow a {@link QueryCondition} to an OR group. */
|
|
90
|
+
declare function isFilterGroup(c: QueryCondition): c is QueryFilterGroup;
|
|
91
|
+
/**
|
|
92
|
+
* A reference to another document. Either self-describing (`{ collection, id }`)
|
|
93
|
+
* or a bare id string resolved via a {@link RelationConfig}.
|
|
94
|
+
*/
|
|
95
|
+
type Ref = {
|
|
96
|
+
collection: string;
|
|
97
|
+
id: string;
|
|
53
98
|
};
|
|
99
|
+
/**
|
|
100
|
+
* Maps a reference field name → the collection it points to. Needed only when
|
|
101
|
+
* refs are stored as bare id strings (self-describing `Ref` objects don't need
|
|
102
|
+
* it). Used by {@link resolveRelations}.
|
|
103
|
+
*/
|
|
104
|
+
type RelationConfig = Record<string, {
|
|
105
|
+
collection: string;
|
|
106
|
+
}>;
|
|
54
107
|
/**
|
|
55
108
|
* Persistence contract. Every backend (Firestore, Postgres, …) implements this;
|
|
56
109
|
* the engine and the route factory only ever speak to this interface.
|
|
@@ -72,15 +125,27 @@ interface DataAdapter {
|
|
|
72
125
|
upsert<T = Record<string, any>>(collection: string, id: string, data: Partial<T>): Promise<void>;
|
|
73
126
|
delete(collection: string, id: string): Promise<void>;
|
|
74
127
|
}
|
|
75
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* The identity resolved from an incoming request by an {@link AuthAdapter}.
|
|
130
|
+
* Minimal by design: only `isAdmin` is meaningful to the default gate. Carry any
|
|
131
|
+
* additional claims (roles, scopes, tenant, …) as extra keys and authorize on
|
|
132
|
+
* them with a custom `authorize` predicate. `userId`/`email` are conventional
|
|
133
|
+
* but optional.
|
|
134
|
+
*/
|
|
76
135
|
interface AuthIdentity {
|
|
77
|
-
userId: string;
|
|
78
|
-
email?: string;
|
|
79
136
|
isAdmin: boolean;
|
|
137
|
+
userId?: string;
|
|
138
|
+
email?: string;
|
|
139
|
+
[claim: string]: unknown;
|
|
80
140
|
}
|
|
81
141
|
/**
|
|
82
|
-
*
|
|
83
|
-
* `isAdmin
|
|
142
|
+
* Decides whether a resolved identity may perform admin actions. Defaults to
|
|
143
|
+
* `identity.isAdmin === true`; override to gate on roles/scopes/etc.
|
|
144
|
+
*/
|
|
145
|
+
type AuthorizeFn = (identity: AuthIdentity, req: Request) => boolean | Promise<boolean>;
|
|
146
|
+
/**
|
|
147
|
+
* Server-side auth contract. Gates every admin API route. Return `null` to
|
|
148
|
+
* reject outright; otherwise the gate's `authorize` predicate decides.
|
|
84
149
|
*/
|
|
85
150
|
interface AuthAdapter {
|
|
86
151
|
verifyRequest(req: Request): Promise<AuthIdentity | null>;
|
|
@@ -119,4 +184,4 @@ interface CmsAuthState {
|
|
|
119
184
|
toggleEdit: () => void;
|
|
120
185
|
}
|
|
121
186
|
|
|
122
|
-
export type
|
|
187
|
+
export { type AuthAdapter, type AuthIdentity, type AuthorizeFn, type ClientStorageAdapter, type CmsAuthState, type DataAdapter, type Editable, type EntityAddress, type NestedSections, type PendingImage, type Query, type QueryCondition, type QueryFilter, type QueryFilterGroup, type QueryFilterOp, type Ref, type RelationConfig, type Section, type SectionMap, type ServerStorageAdapter, type StorageAdapter, isFilterGroup };
|
package/dist/index.d.ts
CHANGED
|
@@ -3,14 +3,27 @@
|
|
|
3
3
|
* server) — this module pulls in zero runtime dependencies.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
6
|
+
* Engine addressing fields. The engine needs to know an entity's `id` and which
|
|
7
|
+
* `collection` it lives in to route saves; everything else is the consumer's own
|
|
8
|
+
* shape. Kept separate from the user's content type `T` so we never force these
|
|
9
|
+
* onto domain models — use {@link Editable}<YourType> when you want both.
|
|
8
10
|
*/
|
|
9
|
-
interface
|
|
11
|
+
interface EntityAddress {
|
|
10
12
|
id: string;
|
|
11
13
|
collection: string;
|
|
12
|
-
[key: string]: any;
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* The user's content type `T` decorated with the engine's addressing fields.
|
|
17
|
+
* `T` is completely unconstrained — bring any shape; the engine only adds `id`
|
|
18
|
+
* and `collection`.
|
|
19
|
+
*/
|
|
20
|
+
type Editable<T = Record<string, any>> = T & EntityAddress;
|
|
21
|
+
/**
|
|
22
|
+
* A single editable record. Schemaless by design. Equivalent to
|
|
23
|
+
* `Editable<Record<string, any>>`; kept as a named alias for the common case and
|
|
24
|
+
* for backwards compatibility.
|
|
25
|
+
*/
|
|
26
|
+
type Section = Editable;
|
|
14
27
|
type SectionMap = Record<string, Section>;
|
|
15
28
|
/**
|
|
16
29
|
* The shape the engine holds in memory:
|
|
@@ -37,20 +50,60 @@ interface PendingImage {
|
|
|
37
50
|
/**
|
|
38
51
|
* Neutral query language. Keeps any backend's native query type (Firestore's
|
|
39
52
|
* `Query`, SQL, etc.) from leaking through the engine.
|
|
53
|
+
*
|
|
54
|
+
* Op support is backend-dependent — see each adapter. `contains` is a
|
|
55
|
+
* case-insensitive substring match; `in`/`nin` take an array value.
|
|
56
|
+
*/
|
|
57
|
+
type QueryFilterOp = "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "nin" | "contains";
|
|
58
|
+
/** A single field condition. */
|
|
59
|
+
type QueryFilter = {
|
|
60
|
+
field: string;
|
|
61
|
+
op: QueryFilterOp;
|
|
62
|
+
value: unknown;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* A disjunction: the inner filters are combined with OR. Sits alongside plain
|
|
66
|
+
* filters in `Query.filters`, which are combined with AND at the top level.
|
|
40
67
|
*/
|
|
41
|
-
type
|
|
68
|
+
type QueryFilterGroup = {
|
|
69
|
+
or: QueryFilter[];
|
|
70
|
+
};
|
|
71
|
+
/** Either a bare condition (AND-ed) or an OR group. */
|
|
72
|
+
type QueryCondition = QueryFilter | QueryFilterGroup;
|
|
42
73
|
type Query = {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
op: QueryFilterOp;
|
|
46
|
-
value: unknown;
|
|
47
|
-
}[];
|
|
74
|
+
/** Top-level conditions combined with AND. Use `{ or: [...] }` for disjunction. */
|
|
75
|
+
filters?: QueryCondition[];
|
|
48
76
|
orderBy?: {
|
|
49
77
|
field: string;
|
|
50
78
|
direction: "asc" | "desc";
|
|
51
79
|
}[];
|
|
52
80
|
limit?: number;
|
|
81
|
+
/** Skip this many rows (offset pagination). */
|
|
82
|
+
offset?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Fields holding references to other documents to inline-resolve after the
|
|
85
|
+
* primary fetch. Resolved by {@link resolveRelations}, never by the adapter.
|
|
86
|
+
*/
|
|
87
|
+
populate?: string[];
|
|
88
|
+
};
|
|
89
|
+
/** Narrow a {@link QueryCondition} to an OR group. */
|
|
90
|
+
declare function isFilterGroup(c: QueryCondition): c is QueryFilterGroup;
|
|
91
|
+
/**
|
|
92
|
+
* A reference to another document. Either self-describing (`{ collection, id }`)
|
|
93
|
+
* or a bare id string resolved via a {@link RelationConfig}.
|
|
94
|
+
*/
|
|
95
|
+
type Ref = {
|
|
96
|
+
collection: string;
|
|
97
|
+
id: string;
|
|
53
98
|
};
|
|
99
|
+
/**
|
|
100
|
+
* Maps a reference field name → the collection it points to. Needed only when
|
|
101
|
+
* refs are stored as bare id strings (self-describing `Ref` objects don't need
|
|
102
|
+
* it). Used by {@link resolveRelations}.
|
|
103
|
+
*/
|
|
104
|
+
type RelationConfig = Record<string, {
|
|
105
|
+
collection: string;
|
|
106
|
+
}>;
|
|
54
107
|
/**
|
|
55
108
|
* Persistence contract. Every backend (Firestore, Postgres, …) implements this;
|
|
56
109
|
* the engine and the route factory only ever speak to this interface.
|
|
@@ -72,15 +125,27 @@ interface DataAdapter {
|
|
|
72
125
|
upsert<T = Record<string, any>>(collection: string, id: string, data: Partial<T>): Promise<void>;
|
|
73
126
|
delete(collection: string, id: string): Promise<void>;
|
|
74
127
|
}
|
|
75
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* The identity resolved from an incoming request by an {@link AuthAdapter}.
|
|
130
|
+
* Minimal by design: only `isAdmin` is meaningful to the default gate. Carry any
|
|
131
|
+
* additional claims (roles, scopes, tenant, …) as extra keys and authorize on
|
|
132
|
+
* them with a custom `authorize` predicate. `userId`/`email` are conventional
|
|
133
|
+
* but optional.
|
|
134
|
+
*/
|
|
76
135
|
interface AuthIdentity {
|
|
77
|
-
userId: string;
|
|
78
|
-
email?: string;
|
|
79
136
|
isAdmin: boolean;
|
|
137
|
+
userId?: string;
|
|
138
|
+
email?: string;
|
|
139
|
+
[claim: string]: unknown;
|
|
80
140
|
}
|
|
81
141
|
/**
|
|
82
|
-
*
|
|
83
|
-
* `isAdmin
|
|
142
|
+
* Decides whether a resolved identity may perform admin actions. Defaults to
|
|
143
|
+
* `identity.isAdmin === true`; override to gate on roles/scopes/etc.
|
|
144
|
+
*/
|
|
145
|
+
type AuthorizeFn = (identity: AuthIdentity, req: Request) => boolean | Promise<boolean>;
|
|
146
|
+
/**
|
|
147
|
+
* Server-side auth contract. Gates every admin API route. Return `null` to
|
|
148
|
+
* reject outright; otherwise the gate's `authorize` predicate decides.
|
|
84
149
|
*/
|
|
85
150
|
interface AuthAdapter {
|
|
86
151
|
verifyRequest(req: Request): Promise<AuthIdentity | null>;
|
|
@@ -119,4 +184,4 @@ interface CmsAuthState {
|
|
|
119
184
|
toggleEdit: () => void;
|
|
120
185
|
}
|
|
121
186
|
|
|
122
|
-
export type
|
|
187
|
+
export { type AuthAdapter, type AuthIdentity, type AuthorizeFn, type ClientStorageAdapter, type CmsAuthState, type DataAdapter, type Editable, type EntityAddress, type NestedSections, type PendingImage, type Query, type QueryCondition, type QueryFilter, type QueryFilterGroup, type QueryFilterOp, type Ref, type RelationConfig, type Section, type SectionMap, type ServerStorageAdapter, type StorageAdapter, isFilterGroup };
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["/**\n * Shared, runtime-free types. Safe to import in any environment (client or\n * server) — this module pulls in zero runtime dependencies.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/**\n * Engine addressing fields. The engine needs to know an entity's `id` and which\n * `collection` it lives in to route saves; everything else is the consumer's own\n * shape. Kept separate from the user's content type `T` so we never force these\n * onto domain models — use {@link Editable}<YourType> when you want both.\n */\nexport interface EntityAddress {\n id: string;\n collection: string;\n}\n\n/**\n * The user's content type `T` decorated with the engine's addressing fields.\n * `T` is completely unconstrained — bring any shape; the engine only adds `id`\n * and `collection`.\n */\nexport type Editable<T = Record<string, any>> = T & EntityAddress;\n\n/**\n * A single editable record. Schemaless by design. Equivalent to\n * `Editable<Record<string, any>>`; kept as a named alias for the common case and\n * for backwards compatibility.\n */\nexport type Section = Editable;\n\nexport type SectionMap = Record<string, Section>;\n\n/**\n * The shape the engine holds in memory:\n * `{ [collection]: { [sectionKey]: Section } }`.\n */\nexport type NestedSections = {\n [collection: string]: {\n [sectionKey: string]: Section;\n };\n};\n\n/**\n * An image edit queued in the provider. The actual upload is deferred until\n * save. `file` is null when the user pasted an external URL (`isExternal`).\n */\nexport interface PendingImage {\n file: File | null;\n localUrl: string;\n sectionKey: string;\n fieldKey: string;\n collection: string;\n docId: string;\n isExternal?: boolean;\n}\n\n/**\n * Neutral query language. Keeps any backend's native query type (Firestore's\n * `Query`, SQL, etc.) from leaking through the engine.\n *\n * Op support is backend-dependent — see each adapter. `contains` is a\n * case-insensitive substring match; `in`/`nin` take an array value.\n */\nexport type QueryFilterOp =\n | \"eq\"\n | \"ne\"\n | \"lt\"\n | \"lte\"\n | \"gt\"\n | \"gte\"\n | \"in\"\n | \"nin\"\n | \"contains\";\n\n/** A single field condition. */\nexport type QueryFilter = { field: string; op: QueryFilterOp; value: unknown };\n\n/**\n * A disjunction: the inner filters are combined with OR. Sits alongside plain\n * filters in `Query.filters`, which are combined with AND at the top level.\n */\nexport type QueryFilterGroup = { or: QueryFilter[] };\n\n/** Either a bare condition (AND-ed) or an OR group. */\nexport type QueryCondition = QueryFilter | QueryFilterGroup;\n\nexport type Query = {\n /** Top-level conditions combined with AND. Use `{ or: [...] }` for disjunction. */\n filters?: QueryCondition[];\n orderBy?: { field: string; direction: \"asc\" | \"desc\" }[];\n limit?: number;\n /** Skip this many rows (offset pagination). */\n offset?: number;\n /**\n * Fields holding references to other documents to inline-resolve after the\n * primary fetch. Resolved by {@link resolveRelations}, never by the adapter.\n */\n populate?: string[];\n};\n\n/** Narrow a {@link QueryCondition} to an OR group. */\nexport function isFilterGroup(c: QueryCondition): c is QueryFilterGroup {\n return typeof c === \"object\" && c !== null && \"or\" in c;\n}\n\n/**\n * A reference to another document. Either self-describing (`{ collection, id }`)\n * or a bare id string resolved via a {@link RelationConfig}.\n */\nexport type Ref = { collection: string; id: string };\n\n/**\n * Maps a reference field name → the collection it points to. Needed only when\n * refs are stored as bare id strings (self-describing `Ref` objects don't need\n * it). Used by {@link resolveRelations}.\n */\nexport type RelationConfig = Record<string, { collection: string }>;\n\n/**\n * Persistence contract. Every backend (Firestore, Postgres, …) implements this;\n * the engine and the route factory only ever speak to this interface.\n */\nexport interface DataAdapter {\n fetchCollection<T = Record<string, any>>(\n collection: string,\n q?: Query,\n ): Promise<(T & { id: string })[]>;\n fetchById<T = Record<string, any>>(\n collection: string,\n id: string,\n ): Promise<(T & { id: string }) | null>;\n create<T = Record<string, any>>(\n collection: string,\n data: T,\n ): Promise<T & { id: string }>;\n createWithId<T = Record<string, any>>(\n collection: string,\n id: string,\n data: T,\n ): Promise<T & { id: string }>;\n update<T = Record<string, any>>(\n collection: string,\n id: string,\n data: Partial<T>,\n ): Promise<void>;\n upsert<T = Record<string, any>>(\n collection: string,\n id: string,\n data: Partial<T>,\n ): Promise<void>;\n delete(collection: string, id: string): Promise<void>;\n}\n\n/**\n * The identity resolved from an incoming request by an {@link AuthAdapter}.\n * Minimal by design: only `isAdmin` is meaningful to the default gate. Carry any\n * additional claims (roles, scopes, tenant, …) as extra keys and authorize on\n * them with a custom `authorize` predicate. `userId`/`email` are conventional\n * but optional.\n */\nexport interface AuthIdentity {\n isAdmin: boolean;\n userId?: string;\n email?: string;\n [claim: string]: unknown;\n}\n\n/**\n * Decides whether a resolved identity may perform admin actions. Defaults to\n * `identity.isAdmin === true`; override to gate on roles/scopes/etc.\n */\nexport type AuthorizeFn = (\n identity: AuthIdentity,\n req: Request,\n) => boolean | Promise<boolean>;\n\n/**\n * Server-side auth contract. Gates every admin API route. Return `null` to\n * reject outright; otherwise the gate's `authorize` predicate decides.\n */\nexport interface AuthAdapter {\n verifyRequest(req: Request): Promise<AuthIdentity | null>;\n}\n\n/**\n * Client half of storage: performs the browser-side upload and returns the\n * final URL. Has zero server dependencies, so it is safe to import in client\n * components. Built from e.g. `@dalgoridim/headless-cms/storage/cloudinary`.\n */\nexport interface ClientStorageAdapter {\n upload(file: File): Promise<{ url: string }>;\n}\n\n/**\n * Server half of storage: issues a presign / signature (or, for local storage,\n * writes the file). Mounted by the route factory at `${apiBasePath}/sign`.\n * Pulls in server-only SDKs, so it lives in a `/server` subpath. Built from e.g.\n * `@dalgoridim/headless-cms/storage/cloudinary/server`.\n */\nexport interface ServerStorageAdapter {\n sign(req: Request): Promise<unknown>;\n}\n\n/** Full two-sided contract (rarely needed; client and server halves are split). */\nexport interface StorageAdapter extends ClientStorageAdapter {\n sign?(req: Request): Promise<unknown>;\n}\n\n/**\n * Minimal client-side auth state the edit primitives depend on. Any auth\n * implementation (Firebase, NextAuth, custom) provides this via a context;\n * `@dalgoridim/headless-cms/auth/firebase/client` ships the default.\n */\nexport interface CmsAuthState {\n isAdmin: boolean;\n isEditing: boolean;\n toggleEdit: () => void;\n}\n"],"mappings":";AAuGO,SAAS,cAAc,GAA0C;AACtE,SAAO,OAAO,MAAM,YAAY,MAAM,QAAQ,QAAQ;AACxD;","names":[]}
|