@thebes/cadmea 1.0.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/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/RichTextEditor-BPilh7Pw.js +36 -0
- package/dist/RichTextEditor-BPilh7Pw.js.map +1 -0
- package/dist/RichTextEditor-DcLqdFY7.js +15 -0
- package/dist/RichTextEditor-DcLqdFY7.js.map +1 -0
- package/dist/index/index.d.ts +147 -0
- package/dist/index/index.js +740 -0
- package/dist/index/index.js.map +1 -0
- package/dist/index/server.js +508 -0
- package/dist/index/server.js.map +1 -0
- package/dist/tanstack-start/index.d.ts +180 -0
- package/dist/tanstack-start/index.js +897 -0
- package/dist/tanstack-start/index.js.map +1 -0
- package/dist/tanstack-start/server.js +730 -0
- package/dist/tanstack-start/server.js.map +1 -0
- package/package.json +138 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","names":["CollectionConfig","FieldConfig","createEffect","createSignal","For","lazy","Show","Suspense","CollectionCapabilities","RichTextEditor","then","mod","default","editableFields","config","Object","entries","fields","filter","key","RelationshipOption","id","label","DraftActions","onSaveDraft","values","Record","Promise","onPublish","onPreview","saving","publishing","previewing","canPublish","canPreview","saveDraftLabel","publishLabel","previewLabel","CollectionEditProps","initialValues","onSubmit","submitLabel","error","onUploadFile","file","File","url","relationshipOptions","Partial","onDirtyChange","dirty","draftActions","capabilities","RenderContext","CollectionEdit","props","initialSnapshot","JSON","stringify","setValues","setField","value","prev","editablePayload","fromEntries","type","handleSubmit","event","SubmitEvent","preventDefault","ctx","versioned","versions","drafts","_$ssr","_tmpl$6","_$escape","_$createComponent","when","children","_tmpl$","each","field","_tmpl$8","_$ssrAttribute","required","_tmpl$7","renderInput","fallback","canUpdate","_tmpl$9","_tmpl$2","_tmpl$3","_tmpl$4","_tmpl$5","_tmpl$0","_tmpl$1","options","option","_tmpl$10","_tmpl$11","_tmpl$12","formatDateValue","_tmpl$13","renderUploadInput","renderRelationshipInput","renderArrayInput","content","onChange","doc","uploading","setUploading","uploadError","setUploadError","handleFileChange","e","Event","currentTarget","HTMLInputElement","files","undefined","err","Error","message","_tmpl$16","_tmpl$14","_tmpl$15","hasMany","relationTo","_tmpl$17","String","items","Array","isArray","updateItem","index","itemKey","itemValue","next","slice","addItem","removeItem","_","i","fieldsForItem","item","base","discriminator","variantValue","variantFields","variants","_tmpl$18","_tmpl$19","itemField","inputId","v","date","Date","Number","isNaN","getTime","toLocaleString","CollectionConfig","FieldConfig","createSignal","For","Show","listableFields","config","excluded","Set","Object","entries","fields","filter","key","field","has","type","formatCellValue","value","undefined","Date","toLocaleDateString","String","rowId","row","Record","id","CollectionListProps","rows","onRowClick","page","pageSize","totalCount","onPageChange","sortField","sortDirection","onSortChange","direction","selectable","selectedIds","ReadonlySet","onSelectionChange","CollectionList","props","columns","selectMode","setSelectMode","toggleSelected","next","delete","add","handleRowActivate","_$ssr","_tmpl$7","_$escape","_$createComponent","when","children","_tmpl$","_tmpl$2","_$ssrAttribute","each","_tmpl$8","length","fallback","_tmpl$9","slug","_tmpl$4","_tmpl$3","_tmpl$0","_tmpl$10","_tmpl$1","_tmpl$11","_tmpl$5","_tmpl$13","_tmpl$12","_tmpl$14","_tmpl$6","createEffect","createSignal","For","JSX","onCleanup","Show","SearchPaletteResult","collection","id","label","SearchPaletteProps","onSearch","query","Promise","onSelect","result","DEBOUNCE_MS","capitalize","value","length","toUpperCase","slice","SearchPalette","props","Element","open","setOpen","setQuery","results","setResults","activeIndex","setActiveIndex","inputRef","HTMLInputElement","triggeredBy","HTMLElement","debounceTimer","ReturnType","setTimeout","latestQueryToken","close","focus","runSearch","token","trim","then","found","onInput","clearTimeout","selectResult","onGlobalKeyDown","event","KeyboardEvent","metaKey","ctrlKey","key","toLowerCase","preventDefault","document","activeElement","addEventListener","removeEventListener","onDialogKeyDown","i","Math","min","max","_$createComponent","when","children","_$ssr","_tmpl$2","_$ssrAttribute","_$escape","fallback","_tmpl$3","_tmpl$","each","index","_tmpl$4"],"sources":["../../src/CollectionEdit.tsx","../../src/CollectionList.tsx","../../src/SearchPalette.tsx"],"sourcesContent":["import type { CollectionConfig, FieldConfig } from \"@thebes/cadmus/cms\";\nimport {\n createEffect,\n createSignal,\n For,\n lazy,\n Show,\n Suspense,\n} from \"solid-js\";\nimport type { CollectionCapabilities } from \"./capabilities.js\";\n\n// Dynamic import, not a static one — @tiptap/core + @tiptap/starter-kit\n// are a large dependency (pushed a consuming route's bundle from ~9KB to\n// ~800KB when statically imported, even for collections with zero\n// richText fields). Lazy-loading means only forms that actually render a\n// richText field pull this chunk in at runtime.\nconst RichTextEditor = lazy(() =>\n import(\"./RichTextEditor.js\").then((mod) => ({\n default: mod.RichTextEditor,\n })),\n);\n\n// Fields the generic form can actually render today. `id` is never\n// user-editable. `date` fields (e.g. createdAt) are server-defaulted and\n// shown read-only rather than editable in this step.\nfunction editableFields(config: CollectionConfig): [string, FieldConfig][] {\n return Object.entries(config.fields).filter(([key]) => key !== \"id\");\n}\n\nexport interface RelationshipOption {\n id: number;\n label: string;\n}\n\n/**\n * Replaces the generic \"Save\" button with \"Save draft\"/\"Publish\" when the\n * collection has `versions: { drafts: true }` — a separate privilege from\n * a plain update, matching `access.publish` in `@thebes/cadmus/cms`.\n * `onPublish` takes no values: publishing acts on whatever was last saved\n * as a draft (the consuming route tracks which version that is), not on\n * the live form state.\n */\nexport interface DraftActions {\n onSaveDraft: (values: Record<string, unknown>) => void | Promise<void>;\n onPublish?: () => void | Promise<void>;\n /**\n * Opens a live preview of the last saved draft (issue #28) — like\n * `onPublish`, acts on whatever was last saved as a draft, not the live\n * form state. Omit to not render the Preview button at all.\n */\n onPreview?: () => void | Promise<void>;\n saving?: boolean;\n publishing?: boolean;\n previewing?: boolean;\n /** Disables Publish — e.g. until a draft has been saved at least once. */\n canPublish?: boolean;\n /** Disables Preview — same gating as canPublish, a draft must exist first. */\n canPreview?: boolean;\n saveDraftLabel?: string;\n publishLabel?: string;\n previewLabel?: string;\n}\n\nexport interface CollectionEditProps {\n config: CollectionConfig;\n initialValues?: Record<string, unknown>;\n onSubmit: (values: Record<string, unknown>) => void | Promise<void>;\n submitLabel?: string;\n error?: string;\n /** Disables the Save button and shows a spinner in its place. */\n saving?: boolean;\n /**\n * Resolves an `upload` field's selected file to a stored URL. Required\n * if the collection has any `upload` fields — `CollectionEdit` never\n * talks to storage directly (stays agnostic of R2/cadmus/storage), so\n * the consuming route wires this to a server function that calls an\n * `ImageService`'s `upload()`.\n */\n onUploadFile?: (file: File) => Promise<{ url: string }>;\n /**\n * Options for `relationship` fields (hasMany:false only — see\n * RelationshipFieldConfig's `hasMany` caveat), keyed by the field's\n * `relationTo` collection slug. `CollectionEdit` can't query another\n * collection itself, so the consuming route fetches the related rows\n * and passes them in.\n */\n relationshipOptions?: Partial<Record<string, RelationshipOption[]>>;\n /**\n * Fired whenever the dirty (unsaved-changes) state changes — wire this\n * to a router-level navigation guard (e.g. `useBlocker` in the\n * consuming route) since `CollectionEdit` has no router access itself.\n */\n onDirtyChange?: (dirty: boolean) => void;\n /** Only rendered when `config.versions?.drafts` is also true. */\n draftActions?: DraftActions;\n /**\n * Hides the Save button when `canUpdate` is `false` — see issue #26's\n * RBAC-aware admin UI. Undefined (the default — most collections don't\n * wire this up) reads as \"allowed\", same as `@thebes/cadmus/cms`'s own\n * \"no access fn = allowed\" default.\n */\n capabilities?: CollectionCapabilities;\n}\n\ninterface RenderContext {\n onUploadFile?: (file: File) => Promise<{ url: string }>;\n relationshipOptions?: Partial<Record<string, RelationshipOption[]>>;\n}\n\nexport function CollectionEdit(props: CollectionEditProps) {\n const initialSnapshot = JSON.stringify(props.initialValues ?? {});\n const [values, setValues] = createSignal<Record<string, unknown>>(\n props.initialValues ?? {},\n );\n\n // Reported via `onDirtyChange` rather than tracked by the consuming\n // route itself — only this component sees every field edit as it\n // happens. A plain JSON.stringify comparison is enough: form values are\n // already plain JSON-shaped data (TipTap docs, array-field items), so\n // there's no Date/Map/Set edge case to special-case here.\n createEffect(() => {\n props.onDirtyChange?.(JSON.stringify(values()) !== initialSnapshot);\n });\n\n function setField(key: string, value: unknown) {\n setValues((prev) => ({ ...prev, [key]: value }));\n }\n\n // date fields are read-only — never include them in a submitted/draft payload\n function editablePayload(): Record<string, unknown> {\n return Object.fromEntries(\n Object.entries(values()).filter(\n ([key]) => props.config.fields[key]?.type !== \"date\",\n ),\n );\n }\n\n function handleSubmit(event: SubmitEvent) {\n event.preventDefault();\n void props.onSubmit(editablePayload());\n }\n\n const ctx: RenderContext = {\n onUploadFile: props.onUploadFile,\n relationshipOptions: props.relationshipOptions,\n };\n\n const versioned = () => props.config.versions?.drafts && props.draftActions;\n\n return (\n <form class=\"flex flex-col gap-4\" onSubmit={handleSubmit}>\n <Show when={props.error}>\n {/* role=\"alert\" so assistive tech announces submit failures the\n moment they appear, not only if the user happens to navigate to\n them. */}\n <p class=\"text-sm text-error\" role=\"alert\">\n {props.error}\n </p>\n </Show>\n <For each={editableFields(props.config)}>\n {([key, field]) => (\n <div class=\"form-control\">\n {/* The \" *\" stays inside the label's accessible name (it reads\n as \"required\" alongside each input's `required` attribute);\n the span only colors it, it does not change the text. */}\n <label class=\"label\" for={key}>\n {key}\n <Show when={field.required}>\n <span class=\"text-error\">{\" *\"}</span>\n </Show>\n </label>\n {renderInput(key, field, values()[key], setField, ctx)}\n </div>\n )}\n </For>\n {/* Bottom-anchored, full-width action bar — not a top toolbar, per\n issue #25's mobile-first note. */}\n <div class=\"bg-base-100 sticky bottom-0 flex gap-2 border-t py-3\">\n <Show\n when={versioned()}\n fallback={\n <Show when={props.capabilities?.canUpdate !== false}>\n <button\n type=\"submit\"\n class=\"btn btn-primary flex-1\"\n disabled={props.saving}\n >\n <Show\n when={props.saving}\n fallback={props.submitLabel ?? \"Save\"}\n >\n <span class=\"loading loading-spinner loading-sm\" />\n </Show>\n </button>\n </Show>\n }\n >\n <button\n type=\"button\"\n class=\"btn flex-1\"\n disabled={props.draftActions?.saving}\n onClick={() =>\n void props.draftActions?.onSaveDraft(editablePayload())\n }\n >\n <Show\n when={props.draftActions?.saving}\n fallback={props.draftActions?.saveDraftLabel ?? \"Save draft\"}\n >\n <span class=\"loading loading-spinner loading-sm\" />\n </Show>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary flex-1\"\n disabled={\n !props.draftActions?.canPublish || props.draftActions?.publishing\n }\n onClick={() => void props.draftActions?.onPublish?.()}\n >\n <Show\n when={props.draftActions?.publishing}\n fallback={props.draftActions?.publishLabel ?? \"Publish\"}\n >\n <span class=\"loading loading-spinner loading-sm\" />\n </Show>\n </button>\n <Show when={props.draftActions?.onPreview}>\n <button\n type=\"button\"\n class=\"btn btn-outline flex-1\"\n disabled={\n !props.draftActions?.canPreview ||\n props.draftActions?.previewing\n }\n onClick={() => void props.draftActions?.onPreview?.()}\n >\n <Show\n when={props.draftActions?.previewing}\n fallback={props.draftActions?.previewLabel ?? \"Preview\"}\n >\n <span class=\"loading loading-spinner loading-sm\" />\n </Show>\n </button>\n </Show>\n </Show>\n </div>\n </form>\n );\n}\n\nfunction renderInput(\n key: string,\n field: FieldConfig,\n value: unknown,\n setField: (key: string, value: unknown) => void,\n ctx: RenderContext,\n) {\n switch (field.type) {\n case \"text\":\n return (\n <input\n id={key}\n class=\"input\"\n type=\"text\"\n value={(value as string) ?? \"\"}\n required={field.required}\n onInput={(e) => setField(key, e.currentTarget.value)}\n />\n );\n case \"select\":\n return (\n <select\n id={key}\n class=\"select\"\n value={(value as string) ?? \"\"}\n required={field.required}\n onChange={(e) => setField(key, e.currentTarget.value)}\n >\n <For each={field.options}>\n {(option) => <option value={option}>{option}</option>}\n </For>\n </select>\n );\n case \"number\":\n return (\n <input\n id={key}\n class=\"input\"\n type=\"number\"\n value={(value as number) ?? \"\"}\n required={field.required}\n onInput={(e) => setField(key, e.currentTarget.valueAsNumber)}\n />\n );\n case \"date\":\n return (\n <input\n id={key}\n class=\"input\"\n type=\"text\"\n readOnly\n value={formatDateValue(value)}\n />\n );\n case \"checkbox\":\n return (\n <input\n id={key}\n class=\"checkbox\"\n type=\"checkbox\"\n checked={(value as boolean) ?? false}\n onChange={(e) => setField(key, e.currentTarget.checked)}\n />\n );\n case \"upload\":\n return renderUploadInput(key, field, value, setField, ctx);\n case \"relationship\":\n return renderRelationshipInput(key, field, value, setField, ctx);\n case \"array\":\n return renderArrayInput(key, field, value, setField, ctx);\n case \"richText\":\n return (\n <Suspense\n fallback={<span class=\"loading loading-spinner loading-sm\" />}\n >\n <RichTextEditor\n id={key}\n content={value as object | undefined}\n onChange={(doc) => setField(key, doc)}\n />\n </Suspense>\n );\n default:\n return null;\n }\n}\n\nfunction renderUploadInput(\n key: string,\n field: FieldConfig & { type: \"upload\" },\n value: unknown,\n setField: (key: string, value: unknown) => void,\n ctx: RenderContext,\n) {\n const [uploading, setUploading] = createSignal(false);\n const [uploadError, setUploadError] = createSignal<string>();\n\n async function handleFileChange(\n e: Event & { currentTarget: HTMLInputElement },\n ) {\n const file = e.currentTarget.files?.[0];\n if (!file) return;\n if (!ctx.onUploadFile) {\n setUploadError(\"No upload handler configured for this form.\");\n return;\n }\n setUploading(true);\n setUploadError(undefined);\n try {\n const { url } = await ctx.onUploadFile(file);\n setField(key, url);\n } catch (err) {\n setUploadError(err instanceof Error ? err.message : \"Upload failed\");\n } finally {\n setUploading(false);\n }\n }\n\n return (\n <div class=\"flex flex-col gap-2\">\n <Show when={value}>\n <p class=\"text-sm opacity-70 break-all\">{value as string}</p>\n </Show>\n <input\n id={key}\n class=\"file-input\"\n type=\"file\"\n required={field.required && !value}\n disabled={uploading()}\n onChange={handleFileChange}\n />\n <Show when={uploading()}>\n <span class=\"loading loading-spinner loading-sm\" />\n </Show>\n <Show when={uploadError()}>\n <p class=\"text-sm text-error\">{uploadError()}</p>\n </Show>\n </div>\n );\n}\n\nfunction renderRelationshipInput(\n key: string,\n field: FieldConfig & { type: \"relationship\" },\n value: unknown,\n setField: (key: string, value: unknown) => void,\n ctx: RenderContext,\n) {\n // hasMany:true relationships are join-table-backed (no plain FK column\n // to bind a single <select> to) — not supported by this generic form\n // yet. See RelationshipFieldConfig's `hasMany` doc.\n if (field.hasMany) return null;\n\n const options = ctx.relationshipOptions?.[field.relationTo] ?? [];\n\n return (\n <select\n id={key}\n class=\"select\"\n value={value != null ? String(value) : \"\"}\n required={field.required}\n onChange={(e) =>\n setField(\n key,\n e.currentTarget.value === \"\" ? null : Number(e.currentTarget.value),\n )\n }\n >\n <option value=\"\">—</option>\n <For each={options}>\n {(option) => <option value={option.id}>{option.label}</option>}\n </For>\n </select>\n );\n}\n\nfunction renderArrayInput(\n key: string,\n field: FieldConfig & { type: \"array\" },\n value: unknown,\n setField: (key: string, value: unknown) => void,\n ctx: RenderContext,\n) {\n const items = () =>\n Array.isArray(value) ? (value as Record<string, unknown>[]) : [];\n\n function updateItem(index: number, itemKey: string, itemValue: unknown) {\n const next = items().slice();\n next[index] = { ...next[index], [itemKey]: itemValue };\n setField(key, next);\n }\n\n function addItem() {\n setField(key, [...items(), {}]);\n }\n\n function removeItem(index: number) {\n setField(\n key,\n items().filter((_, i) => i !== index),\n );\n }\n\n function fieldsForItem(\n item: Record<string, unknown>,\n ): [string, FieldConfig][] {\n const base = Object.entries(field.fields);\n const discriminator = field.discriminator;\n if (!discriminator) return base;\n\n const variantValue = item[discriminator.key];\n const variantFields =\n typeof variantValue === \"string\"\n ? discriminator.variants[variantValue]\n : undefined;\n return variantFields ? [...base, ...Object.entries(variantFields)] : base;\n }\n\n return (\n <div class=\"flex flex-col gap-3\">\n <For each={items()}>\n {(item, index) => (\n <div class=\"card bg-base-200 flex flex-col gap-2 p-3\">\n <For each={fieldsForItem(item)}>\n {([itemKey, itemField]) => {\n const inputId = `${key}.${index()}.${itemKey}`;\n return (\n <div class=\"form-control\">\n <label class=\"label\" for={inputId}>\n {itemKey}\n <Show when={itemField.required}>\n <span class=\"text-error\">{\" *\"}</span>\n </Show>\n </label>\n {renderInput(\n inputId,\n itemField,\n item[itemKey],\n (_, v) => updateItem(index(), itemKey, v),\n ctx,\n )}\n </div>\n );\n }}\n </For>\n <button\n type=\"button\"\n class=\"btn btn-error btn-outline btn-sm self-start\"\n onClick={() => removeItem(index())}\n >\n Remove\n </button>\n </div>\n )}\n </For>\n <button\n type=\"button\"\n class=\"btn btn-outline btn-sm self-start\"\n onClick={addItem}\n >\n Add {key}\n </button>\n </div>\n );\n}\n\nfunction formatDateValue(value: unknown): string {\n if (!value) return \"—\";\n const date = value instanceof Date ? value : new Date(value as string);\n return Number.isNaN(date.getTime()) ? \"—\" : date.toLocaleString();\n}\n","import type { CollectionConfig, FieldConfig } from \"@thebes/cadmus/cms\";\nimport { createSignal, For, Show } from \"solid-js\";\n\n// Field types that can be rendered as a plain table cell today.\n// `id` is intentionally excluded — it's never a useful list column.\n// `richText`/`array` are structured content, not a sensible table cell;\n// `relationship` has no resolved label available here (CollectionList\n// only receives raw row data, not the related collection's rows) so it'd\n// show a bare numeric id — excluded until that's worth solving.\nfunction listableFields(config: CollectionConfig): [string, FieldConfig][] {\n const excluded = new Set([\"richText\", \"array\", \"relationship\"]);\n return Object.entries(config.fields).filter(\n ([key, field]) => key !== \"id\" && !excluded.has(field.type),\n );\n}\n\nfunction formatCellValue(value: unknown): string {\n if (value === null || value === undefined) return \"—\";\n if (value instanceof Date) return value.toLocaleDateString();\n return String(value);\n}\n\nfunction rowId(row: Record<string, unknown>): number | undefined {\n return typeof row.id === \"number\" ? row.id : undefined;\n}\n\nexport interface CollectionListProps {\n config: CollectionConfig;\n rows: Record<string, unknown>[];\n onRowClick?: (row: Record<string, unknown>) => void;\n\n /**\n * 1-based current page. Omit (along with `pageSize`) to render without\n * the pagination bar entirely — list views with no `find()` paging\n * wired up yet still render correctly.\n */\n page?: number;\n pageSize?: number;\n /** Total row count across all pages — see `LocalApi.count()`. Enables\n * disabling \"Next\" exactly at the last page; omit to fall back to a\n * `rows.length < pageSize` heuristic. */\n totalCount?: number;\n onPageChange?: (page: number) => void;\n\n /** Field key currently sorted on. Omit to hide the sort control. */\n sortField?: string;\n sortDirection?: \"asc\" | \"desc\";\n onSortChange?: (field: string, direction: \"asc\" | \"desc\") => void;\n\n /** Shows the \"Select\" bulk-select mode toggle. */\n selectable?: boolean;\n selectedIds?: ReadonlySet<number>;\n onSelectionChange?: (selectedIds: Set<number>) => void;\n}\n\nexport function CollectionList(props: CollectionListProps) {\n const columns = () => listableFields(props.config);\n const [selectMode, setSelectMode] = createSignal(false);\n\n function toggleSelected(id: number) {\n const next = new Set(props.selectedIds ?? []);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n props.onSelectionChange?.(next);\n }\n\n function handleRowActivate(row: Record<string, unknown>) {\n if (selectMode()) {\n const id = rowId(row);\n if (id !== undefined) toggleSelected(id);\n return;\n }\n props.onRowClick?.(row);\n }\n\n return (\n <div class=\"flex flex-col gap-3\">\n <div class=\"flex flex-wrap items-center justify-between gap-2\">\n <Show when={props.selectable}>\n <button\n type=\"button\"\n class=\"btn btn-outline btn-sm\"\n onClick={() => setSelectMode((v) => !v)}\n >\n {selectMode() ? \"Done\" : \"Select\"}\n </button>\n </Show>\n {/* Dropdown picker, not clickable column headers — sort works the\n same on touch and desktop, see issue #25's mobile-first note. */}\n <Show when={props.onSortChange}>\n <div class=\"join\">\n <select\n aria-label=\"Sort by\"\n class=\"select select-sm join-item\"\n value={props.sortField ?? \"\"}\n onChange={(e) =>\n props.onSortChange?.(\n e.currentTarget.value,\n props.sortDirection ?? \"asc\",\n )\n }\n >\n <For each={columns()}>\n {([key]) => <option value={key}>{key}</option>}\n </For>\n </select>\n <select\n aria-label=\"Sort direction\"\n class=\"select select-sm join-item\"\n value={props.sortDirection ?? \"asc\"}\n onChange={(e) =>\n props.onSortChange?.(\n props.sortField ?? columns()[0]?.[0] ?? \"\",\n e.currentTarget.value as \"asc\" | \"desc\",\n )\n }\n >\n <option value=\"asc\">Ascending</option>\n <option value=\"desc\">Descending</option>\n </select>\n </div>\n </Show>\n </div>\n\n <Show\n when={props.rows.length > 0}\n fallback={<p class=\"text-sm opacity-70\">No {props.config.slug} yet.</p>}\n >\n {/* Table on desktop — hidden below md per the mobile-first card\n layout below, not the other way around. */}\n <table class=\"table hidden md:table\">\n <thead>\n <tr>\n <Show when={selectMode()}>\n <th />\n </Show>\n <For each={columns()}>{([key]) => <th>{key}</th>}</For>\n </tr>\n </thead>\n <tbody>\n <For each={props.rows}>\n {(row) => (\n <tr\n class={\n props.onRowClick || selectMode()\n ? \"cursor-pointer hover\"\n : undefined\n }\n onClick={() => handleRowActivate(row)}\n >\n <Show when={selectMode()}>\n <td>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm\"\n onClick={(e) => e.stopPropagation()}\n checked={\n rowId(row) !== undefined &&\n (props.selectedIds?.has(rowId(row) as number) ??\n false)\n }\n onChange={() => {\n const id = rowId(row);\n if (id !== undefined) toggleSelected(id);\n }}\n />\n </td>\n </Show>\n <For each={columns()}>\n {([key]) => <td>{formatCellValue(row[key])}</td>}\n </For>\n </tr>\n )}\n </For>\n </tbody>\n </table>\n\n {/* Stacked card list on mobile/tablet — tap-to-select via an\n always-visible checkbox in select mode, never hover-revealed. */}\n <div class=\"flex flex-col gap-2 md:hidden\">\n <For each={props.rows}>\n {(row) => (\n // biome-ignore lint/a11y/useSemanticElements: a native <button> can't contain interactive content (the select-mode checkbox below); role=\"button\" + tabIndex/onKeyDown is the standard fallback.\n <div\n class=\"card bg-base-200 cursor-pointer p-3\"\n role=\"button\"\n tabIndex={0}\n onClick={() => handleRowActivate(row)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n handleRowActivate(row);\n }\n }}\n >\n <div class=\"flex items-start gap-3\">\n <Show when={selectMode()}>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm mt-1\"\n onClick={(e) => e.stopPropagation()}\n checked={\n rowId(row) !== undefined &&\n (props.selectedIds?.has(rowId(row) as number) ?? false)\n }\n onChange={() => {\n const id = rowId(row);\n if (id !== undefined) toggleSelected(id);\n }}\n />\n </Show>\n <div class=\"flex flex-1 flex-col gap-1\">\n <For each={columns()}>\n {([key]) => (\n <div class=\"flex justify-between gap-2 text-sm\">\n <span class=\"opacity-60\">{key}</span>\n <span class=\"text-right\">\n {formatCellValue(row[key])}\n </span>\n </div>\n )}\n </For>\n </div>\n </div>\n </div>\n )}\n </For>\n </div>\n </Show>\n\n {/* Bottom-anchored prev/next bar — no page numbers, per issue #25's\n mobile-first note. Renders only when pagination is wired up. */}\n <Show when={props.page !== undefined && props.pageSize !== undefined}>\n <div class=\"bg-base-100 sticky bottom-0 flex items-center justify-between gap-2 border-t py-2\">\n <button\n type=\"button\"\n class=\"btn btn-sm\"\n disabled={(props.page ?? 1) <= 1}\n onClick={() => props.onPageChange?.((props.page ?? 1) - 1)}\n >\n Prev\n </button>\n <span class=\"text-sm opacity-70\">Page {props.page}</span>\n <button\n type=\"button\"\n class=\"btn btn-sm\"\n disabled={\n props.totalCount !== undefined\n ? (props.page ?? 1) * (props.pageSize ?? 0) >= props.totalCount\n : props.rows.length < (props.pageSize ?? 0)\n }\n onClick={() => props.onPageChange?.((props.page ?? 1) + 1)}\n >\n Next\n </button>\n </div>\n </Show>\n </div>\n );\n}\n","import {\n createEffect,\n createSignal,\n For,\n type JSX,\n onCleanup,\n Show,\n} from \"solid-js\";\n\nexport interface SearchPaletteResult {\n collection: string;\n id: number;\n label: string;\n}\n\nexport interface SearchPaletteProps {\n /** Runs a query against `LocalApi.search()` for every searchable collection — see `@thebes/cadmus/cms`'s `getCollectionsMeta`. */\n onSearch: (query: string) => Promise<SearchPaletteResult[]>;\n /** Navigates to the chosen result; the palette closes itself afterward. */\n onSelect: (result: SearchPaletteResult) => void;\n}\n\nconst DEBOUNCE_MS = 200;\n\nfunction capitalize(value: string): string {\n return value.length === 0 ? value : value[0].toUpperCase() + value.slice(1);\n}\n\n/**\n * Self-contained Cmd+K (Ctrl+K on non-Mac) search palette — issue #29.\n * Owns its own open/closed state and keyboard listener; the host app only\n * supplies `onSearch` (wired to a server function that fans out across\n * every collection with `search` configured) and `onSelect` (navigation).\n * Mirrors PanelNav's focus-trap-on-open / restore-focus-on-close pattern\n * rather than introducing a second one.\n */\nexport function SearchPalette(props: SearchPaletteProps): JSX.Element {\n const [open, setOpen] = createSignal(false);\n const [query, setQuery] = createSignal(\"\");\n const [results, setResults] = createSignal<SearchPaletteResult[]>([]);\n const [activeIndex, setActiveIndex] = createSignal(0);\n let inputRef: HTMLInputElement | undefined;\n let triggeredBy: HTMLElement | null = null;\n let debounceTimer: ReturnType<typeof setTimeout> | undefined;\n // Guards a result fetch that resolves after a newer one was already\n // kicked off (or after the palette closed) from clobbering fresher\n // results with stale ones.\n let latestQueryToken = 0;\n\n function close() {\n setOpen(false);\n setQuery(\"\");\n setResults([]);\n triggeredBy?.focus();\n }\n\n function runSearch(value: string) {\n const token = ++latestQueryToken;\n if (value.trim().length < 2) {\n setResults([]);\n return;\n }\n props.onSearch(value).then((found) => {\n if (token !== latestQueryToken) return;\n setResults(found);\n setActiveIndex(0);\n });\n }\n\n function onInput(value: string) {\n setQuery(value);\n clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => runSearch(value), DEBOUNCE_MS);\n }\n\n function selectResult(result: SearchPaletteResult | undefined) {\n if (!result) return;\n props.onSelect(result);\n close();\n }\n\n // Global Cmd+K / Ctrl+K listener — registered once, independent of\n // `open` (unlike PanelNav's Escape/Tab handling, which only needs to\n // exist while open).\n createEffect(() => {\n function onGlobalKeyDown(event: KeyboardEvent) {\n if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === \"k\") {\n event.preventDefault();\n triggeredBy = document.activeElement as HTMLElement | null;\n setOpen(true);\n }\n }\n document.addEventListener(\"keydown\", onGlobalKeyDown);\n onCleanup(() => document.removeEventListener(\"keydown\", onGlobalKeyDown));\n });\n\n createEffect(() => {\n if (open()) inputRef?.focus();\n });\n\n function onDialogKeyDown(event: KeyboardEvent) {\n if (event.key === \"Escape\") {\n close();\n return;\n }\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n setActiveIndex((i) => Math.min(i + 1, results().length - 1));\n return;\n }\n if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((i) => Math.max(i - 1, 0));\n return;\n }\n if (event.key === \"Enter\") {\n event.preventDefault();\n selectResult(results()[activeIndex()]);\n }\n }\n\n return (\n <Show when={open()}>\n <div\n aria-hidden=\"true\"\n class=\"fixed inset-0 z-50 flex items-start justify-center bg-[var(--color-backdrop)] px-4 pt-[15vh]\"\n onClick={close}\n >\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Search\"\n class=\"w-full max-w-lg overflow-hidden rounded-2xl border border-[var(--line)] bg-[var(--surface-strong)] shadow-2xl\"\n onClick={(event) => event.stopPropagation()}\n onKeyDown={onDialogKeyDown}\n >\n <div class=\"flex items-center gap-2 border-b border-[var(--line)] px-4 py-3\">\n <i\n class=\"ph ph-magnifying-glass text-lg text-[var(--sea-ink-soft)]\"\n aria-hidden=\"true\"\n />\n <input\n ref={inputRef}\n type=\"text\"\n value={query()}\n onInput={(event) => onInput(event.currentTarget.value)}\n placeholder=\"Search…\"\n aria-label=\"Search\"\n class=\"flex-1 bg-transparent text-base text-[var(--sea-ink)] outline-none placeholder:text-[var(--sea-ink-soft)]\"\n />\n <kbd class=\"rounded border border-[var(--chip-line)] bg-[var(--chip-bg)] px-1.5 py-0.5 text-xs text-[var(--sea-ink-soft)]\">\n Esc\n </kbd>\n </div>\n\n <Show\n when={results().length > 0}\n fallback={\n <p class=\"px-4 py-6 text-center text-sm text-[var(--sea-ink-soft)]\">\n {query().trim().length < 2\n ? \"Keep typing to search…\"\n : \"No results\"}\n </p>\n }\n >\n <ul class=\"max-h-80 overflow-y-auto py-2\">\n <For each={results()}>\n {(result, index) => (\n <li>\n <button\n type=\"button\"\n onClick={() => selectResult(result)}\n onMouseEnter={() => setActiveIndex(index())}\n class=\"flex w-full items-center justify-between gap-3 px-4 py-2 text-left text-sm text-[var(--sea-ink)] hover:bg-[var(--link-bg-hover)]\"\n classList={{\n \"bg-[var(--link-bg-hover)]\": activeIndex() === index(),\n }}\n >\n <span class=\"truncate\">{result.label}</span>\n <span class=\"shrink-0 text-xs text-[var(--sea-ink-soft)]\">\n {capitalize(result.collection)}\n </span>\n </button>\n </li>\n )}\n </For>\n </ul>\n </Show>\n </div>\n </div>\n </Show>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { CollectionConfig } from "@thebes/cadmus/cms";
|
|
2
|
+
|
|
3
|
+
//#region src/tanstack-start/create.d.ts
|
|
4
|
+
interface CollectionCreatePageOptions<TCreated extends Record<string, unknown>> {
|
|
5
|
+
collection: CollectionConfig;
|
|
6
|
+
/** Page heading — e.g. "New page". Defaults to `New ${collection.slug}`. */
|
|
7
|
+
label?: string;
|
|
8
|
+
submitLabel?: string;
|
|
9
|
+
createFn: (values: Record<string, unknown>) => Promise<TCreated>;
|
|
10
|
+
/** Query key to invalidate after a successful create — e.g. `['pages']`. */
|
|
11
|
+
invalidateQueryKey: readonly unknown[];
|
|
12
|
+
/** Called after a successful create+cache-invalidation — wire this to navigate to the new row's edit page. */
|
|
13
|
+
onCreated?: (created: TCreated) => void;
|
|
14
|
+
/** Forwarded to CollectionEdit — resolves an `upload` field's selected file to a stored URL. */
|
|
15
|
+
onUploadFile?: (file: File) => Promise<{
|
|
16
|
+
url: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Builds a create-page component for a collection. See
|
|
21
|
+
* `createCollectionListPage`'s doc comment for the same rationale on
|
|
22
|
+
* keeping navigation in the route file rather than this package.
|
|
23
|
+
*/
|
|
24
|
+
declare function createCollectionCreatePage<TCreated extends Record<string, unknown>>(options: CollectionCreatePageOptions<TCreated>): () => import("solid-js").JSX.Element;
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/capabilities.d.ts
|
|
27
|
+
/**
|
|
28
|
+
* Drives action gating in `CollectionEdit` and the `tanstack-start` list/
|
|
29
|
+
* edit page factories — hide/disable an action a context can't perform
|
|
30
|
+
* rather than let it fail server-side after a click. A field left
|
|
31
|
+
* `undefined` reads as "allowed", mirroring `@thebes/cadmus/cms`'s own
|
|
32
|
+
* "no access fn configured = allowed" default, so collections that don't
|
|
33
|
+
* wire this up at all keep today's unrestricted behavior.
|
|
34
|
+
*/
|
|
35
|
+
interface CollectionCapabilities {
|
|
36
|
+
canCreate?: boolean;
|
|
37
|
+
canUpdate?: boolean;
|
|
38
|
+
canDelete?: boolean;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/tanstack-start/edit.d.ts
|
|
42
|
+
interface CollectionEditDraftOptions {
|
|
43
|
+
/** Saves the live form values as a new draft version, returning its id. */
|
|
44
|
+
saveDraftFn: (values: Record<string, unknown>) => Promise<{
|
|
45
|
+
id: number;
|
|
46
|
+
}>;
|
|
47
|
+
/** Publishes a saved draft by id — the most recent one from `saveDraftFn`. */
|
|
48
|
+
publishFn: (versionId: number) => Promise<unknown>;
|
|
49
|
+
/**
|
|
50
|
+
* Resolves a saved draft by id to a live preview URL (issue #28) — opens
|
|
51
|
+
* in a new tab on success. Omit to not render the Preview button.
|
|
52
|
+
*/
|
|
53
|
+
previewFn?: (versionId: number) => Promise<{
|
|
54
|
+
url: string;
|
|
55
|
+
}>;
|
|
56
|
+
saveDraftLabel?: string;
|
|
57
|
+
publishLabel?: string;
|
|
58
|
+
previewLabel?: string;
|
|
59
|
+
}
|
|
60
|
+
interface CollectionEditPageOptions {
|
|
61
|
+
collection: CollectionConfig;
|
|
62
|
+
/** Page heading — e.g. "Edit page". Defaults to `Edit ${collection.slug}`. */
|
|
63
|
+
label?: string;
|
|
64
|
+
submitLabel?: string;
|
|
65
|
+
deleteLabel?: string;
|
|
66
|
+
/**
|
|
67
|
+
* A function, not a plain array — re-evaluated on every reactive read
|
|
68
|
+
* inside `createQuery`'s tracking scope, so it stays correct when
|
|
69
|
+
* TanStack Router reuses this component across a param change (e.g.
|
|
70
|
+
* navigating between two `$pageId` values on the same route doesn't
|
|
71
|
+
* always remount). A plain array captured once at creation time would
|
|
72
|
+
* go stale the moment the id changes underneath it.
|
|
73
|
+
*/
|
|
74
|
+
queryKey: () => readonly unknown[];
|
|
75
|
+
queryFn: () => Promise<Record<string, unknown> | null | undefined>;
|
|
76
|
+
updateFn: (values: Record<string, unknown>) => Promise<unknown>;
|
|
77
|
+
deleteFn: () => Promise<unknown>;
|
|
78
|
+
/** Query key to invalidate after a successful update or delete — e.g. `['pages']`. */
|
|
79
|
+
invalidateQueryKey: readonly unknown[];
|
|
80
|
+
/** Called after a successful delete+cache-invalidation — wire this to navigate back to the list page. */
|
|
81
|
+
onDeleted?: () => void;
|
|
82
|
+
/** Forwarded to CollectionEdit — resolves an `upload` field's selected file to a stored URL. */
|
|
83
|
+
onUploadFile?: (file: File) => Promise<{
|
|
84
|
+
url: string;
|
|
85
|
+
}>;
|
|
86
|
+
/**
|
|
87
|
+
* Renders "Save draft"/"Publish" instead of the generic Save button —
|
|
88
|
+
* only meaningful when `collection.versions?.drafts` is also true (see
|
|
89
|
+
* `CollectionEdit`'s `draftActions` doc).
|
|
90
|
+
*/
|
|
91
|
+
draftActions?: CollectionEditDraftOptions;
|
|
92
|
+
/**
|
|
93
|
+
* A function, not a plain value — same reactivity rationale as
|
|
94
|
+
* `queryKey` above (re-evaluated on every tracking read, so it stays
|
|
95
|
+
* correct as the underlying capabilities query resolves/refetches).
|
|
96
|
+
* Hides the Delete button when `canDelete` is `false`; forwarded to
|
|
97
|
+
* `CollectionEdit` to gate Save via `canUpdate`. See issue #26.
|
|
98
|
+
*/
|
|
99
|
+
capabilities?: () => CollectionCapabilities | undefined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Builds an edit-page component for a collection — fetch, update, and
|
|
103
|
+
* delete, all wired together, plus a router-level unsaved-changes guard
|
|
104
|
+
* (`useBlocker`) driven by `CollectionEdit`'s `onDirtyChange`. See
|
|
105
|
+
* `createCollectionListPage`'s doc comment for the rationale on keeping
|
|
106
|
+
* navigation in the route file.
|
|
107
|
+
*/
|
|
108
|
+
declare function createCollectionEditPage(options: CollectionEditPageOptions): () => import("solid-js").JSX.Element;
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/tanstack-start/list.d.ts
|
|
111
|
+
interface CollectionListQueryParams {
|
|
112
|
+
page: number;
|
|
113
|
+
pageSize: number;
|
|
114
|
+
sortField?: string;
|
|
115
|
+
sortDirection?: "asc" | "desc";
|
|
116
|
+
}
|
|
117
|
+
interface CollectionListQueryResult<TRow> {
|
|
118
|
+
rows: TRow[];
|
|
119
|
+
/** Total row count across all pages — see `LocalApi.count()`. Drives
|
|
120
|
+
* `CollectionList`'s "Next" disabled state. */
|
|
121
|
+
total: number;
|
|
122
|
+
}
|
|
123
|
+
interface CollectionListPageOptions<TRow extends Record<string, unknown>> {
|
|
124
|
+
collection: CollectionConfig;
|
|
125
|
+
/** Page heading — e.g. "Pages". Defaults to the collection slug. */
|
|
126
|
+
label?: string;
|
|
127
|
+
queryKey: readonly unknown[];
|
|
128
|
+
/**
|
|
129
|
+
* Receives the current page/sort state — re-run whenever any of them
|
|
130
|
+
* change, since pagination/sorting happen server-side via `LocalApi`'s
|
|
131
|
+
* `find({ limit, offset, orderBy })` + `count()`, not by slicing an
|
|
132
|
+
* already-fetched array client-side.
|
|
133
|
+
*/
|
|
134
|
+
queryFn: (params: CollectionListQueryParams) => Promise<CollectionListQueryResult<TRow>>;
|
|
135
|
+
/** Rows per page. Defaults to 20. */
|
|
136
|
+
pageSize?: number;
|
|
137
|
+
/** Link href for the "New …" button. Omit to hide the button entirely. */
|
|
138
|
+
newHref?: string;
|
|
139
|
+
/** Label for the "New …" button — e.g. "New page". */
|
|
140
|
+
newLabel?: string;
|
|
141
|
+
/** Called when a row is clicked — wire this to your router's navigate(). */
|
|
142
|
+
onRowClick?: (row: TRow) => void;
|
|
143
|
+
/**
|
|
144
|
+
* A function, not a plain value — re-evaluated on every reactive read,
|
|
145
|
+
* so it stays correct as the underlying capabilities query resolves/
|
|
146
|
+
* refetches. Hides the "New …" button when `canCreate` is `false`. See
|
|
147
|
+
* issue #26's RBAC-aware admin UI.
|
|
148
|
+
*/
|
|
149
|
+
capabilities?: () => CollectionCapabilities | undefined;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Builds a list-view page component for a collection — paginated/sortable
|
|
153
|
+
* query, loading state, and the generic table/card list, wired together.
|
|
154
|
+
* The returned component is meant to be passed directly as a route's
|
|
155
|
+
* `component`:
|
|
156
|
+
*
|
|
157
|
+
* ```tsx
|
|
158
|
+
* export const Route = createFileRoute('/admin/pages/')({
|
|
159
|
+
* component: createCollectionListPage({
|
|
160
|
+
* collection: pagesCollection,
|
|
161
|
+
* label: 'Pages',
|
|
162
|
+
* queryKey: ['pages'],
|
|
163
|
+
* queryFn: (params) => getPages({ data: params }),
|
|
164
|
+
* newHref: '/admin/pages/new',
|
|
165
|
+
* newLabel: 'New page',
|
|
166
|
+
* onRowClick: (row) => navigate({ to: '/admin/pages/$pageId', params: { pageId: String(row.id) } }),
|
|
167
|
+
* }),
|
|
168
|
+
* })
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* Navigation stays in the route file (via `onRowClick`/`newHref` as plain
|
|
172
|
+
* strings) rather than this package calling `useNavigate()` itself —
|
|
173
|
+
* TanStack Router's route-typing is generated per-app, so a generic
|
|
174
|
+
* package can't produce a correctly-typed `navigate()` call for routes
|
|
175
|
+
* it doesn't know about.
|
|
176
|
+
*/
|
|
177
|
+
declare function createCollectionListPage<TRow extends Record<string, unknown>>(options: CollectionListPageOptions<TRow>): () => import("solid-js").JSX.Element;
|
|
178
|
+
//#endregion
|
|
179
|
+
export { type CollectionCreatePageOptions, type CollectionEditPageOptions, type CollectionListPageOptions, createCollectionCreatePage, createCollectionEditPage, createCollectionListPage };
|
|
180
|
+
//# sourceMappingURL=index.d.ts.map
|