@alfakit/react 0.1.12 → 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/CHANGELOG.md +12 -0
- package/dist/components/cms/blocks.d.ts.map +1 -1
- package/dist/components/cms/blocks.js +2 -2
- package/dist/components/cms/blocks.js.map +1 -1
- package/dist/components/cms/head.d.ts +6 -4
- package/dist/components/cms/head.d.ts.map +1 -1
- package/dist/components/cms/head.js +10 -4
- package/dist/components/cms/head.js.map +1 -1
- package/dist/components/cms/page.d.ts.map +1 -1
- package/dist/components/cms/page.js +16 -3
- package/dist/components/cms/page.js.map +1 -1
- package/dist/components/fields/file-upload.js +4 -4
- package/dist/components/fields/file-upload.js.map +1 -1
- package/dist/components/fields/rich-editor.js +4 -2
- package/dist/components/fields/rich-editor.js.map +1 -1
- package/dist/components/fields/slider.js +1 -1
- package/dist/components/fields/slider.js.map +1 -1
- package/dist/components/helpdesk/chat-widget.js +1 -1
- package/dist/components/layout/section.js +1 -1
- package/dist/components/layout/tabs.js +1 -1
- package/dist/components/panel/app-sidebar.d.ts.map +1 -1
- package/dist/components/panel/app-sidebar.js +27 -5
- package/dist/components/panel/app-sidebar.js.map +1 -1
- package/dist/components/panel/global-search.js +2 -2
- package/dist/components/panel/global-search.js.map +1 -1
- package/dist/components/panel/shortcut-keys.js +1 -1
- package/dist/components/panel/shortcut-keys.js.map +1 -1
- package/dist/components/table/list-tabs.js +1 -1
- package/dist/components/table/list-tabs.js.map +1 -1
- package/dist/components/table/table-views.js +1 -1
- package/dist/components/table/table-views.js.map +1 -1
- package/dist/components/versions/record-versions.js +1 -1
- package/dist/context.d.ts +8 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +27 -2
- package/dist/context.js.map +1 -1
- package/dist/create-alfakit.d.ts.map +1 -1
- package/dist/create-alfakit.js +6 -3
- package/dist/create-alfakit.js.map +1 -1
- package/dist/eject-manifest.json +2 -2
- package/dist/index.js +1 -1
- package/dist/layouts/app-layout.d.ts.map +1 -1
- package/dist/layouts/app-layout.js +13 -3
- package/dist/layouts/app-layout.js.map +1 -1
- package/dist/lib/discovery.d.ts +4 -0
- package/dist/lib/discovery.d.ts.map +1 -1
- package/dist/lib/discovery.js +17 -7
- package/dist/lib/discovery.js.map +1 -1
- package/dist/lib/icons.d.ts +2 -1
- package/dist/lib/icons.d.ts.map +1 -1
- package/dist/lib/icons.js +55 -19
- package/dist/lib/icons.js.map +1 -1
- package/dist/pages/page.d.ts +1 -1
- package/dist/pages/page.d.ts.map +1 -1
- package/dist/pages/page.js +47 -10
- package/dist/pages/page.js.map +1 -1
- package/dist/standalone/alfakit-component.js +2 -1
- package/dist/standalone/alfakit-component.js.map +1 -1
- package/dist/standalone/alfakit-toaster.js +2 -1
- package/dist/standalone/alfakit-toaster.js.map +1 -1
- package/dist/standalone/runtime.js +1 -11
- package/dist/standalone/runtime.js.map +1 -1
- package/dist/standalone/toaster-claims.js +16 -0
- package/dist/standalone/toaster-claims.js.map +1 -0
- package/dist/starter-kit/widgets-loaded.js +63 -6
- package/dist/starter-kit/widgets-loaded.js.map +1 -1
- package/dist/starter-kit/widgets-triggers.js +1 -1
- package/dist/starter-kit/widgets-triggers.js.map +1 -1
- package/dist/themes.css +92 -0
- package/package.json +2 -2
- package/src/components/cms/blocks.tsx +7 -1
- package/src/components/cms/head.tsx +19 -4
- package/src/components/cms/page.tsx +17 -2
- package/src/components/fields/file-upload.tsx +5 -4
- package/src/components/fields/rich-editor.tsx +7 -2
- package/src/components/fields/slider.tsx +3 -1
- package/src/components/panel/app-sidebar.tsx +28 -7
- package/src/components/panel/global-search.tsx +4 -1
- package/src/components/panel/shortcut-keys.tsx +1 -1
- package/src/components/table/list-tabs.tsx +3 -1
- package/src/components/table/table-views.tsx +3 -1
- package/src/context.tsx +46 -1
- package/src/create-alfakit.tsx +13 -2
- package/src/layouts/app-layout.tsx +16 -3
- package/src/lib/discovery.tsx +23 -8
- package/src/lib/icons.tsx +90 -24
- package/src/pages/page.tsx +85 -27
- package/src/standalone/runtime.ts +2 -14
- package/src/standalone/toaster-claims.ts +16 -0
- package/src/starter-kit/widgets-loaded.tsx +84 -1
- package/src/starter-kit/widgets-triggers.tsx +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @alfakit/react
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Panel layout stays mounted between pages (no sidebar flicker) and icons render instantly once loaded; CMS Site design and block styles; saving waits for uploads in progress; slider accessible name; starter-kit SSR hydration fixed; contrast of shortcut hints and inactive tabs; Kanban refused drops; friendly file type labels.
|
|
8
|
+
|
|
9
|
+
## 0.1.13
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Starter kit header components bring a toaster to the app's own pages, so push and PWA messages show there; AlfaKit pages keep a single toaster.
|
|
14
|
+
|
|
3
15
|
## 0.1.12
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blocks.d.ts","names":["FormBlock"],"sources":["../../../src/components/cms/blocks.tsx"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"blocks.d.ts","names":["FormBlock"],"sources":["../../../src/components/cms/blocks.tsx"],"mappings":";;;;;iBAiCiB,cAAc,QAAQ;EACrC,OAAO,eAAe;EACtB,MAAM;;KAGH,eAAe,gBAAgB,oBAAoB,cACtD,cAAc,iBAAiB;wBAgBjB,YAAY,QAAQ,cAAc,oCAAiB,IAAA;wBA+CnD,gBAAgB,QAAQ,cAAc,wCAAqB,IAAA;wBAa3D,aAAa,QAAQ,cAAc,qCAAkB,IAAA;wBAuBrD,eAAe,QAAQ,cAAc,uCAAoB,IAAA;wBAuBzD,oBAAoB,QAAQ,cAAc,4CAAyB,IAAA;wBA4CnE,gBAAgB,QAAQ,cAAc,wCAAqB,IAAA;wBA8C3D,WAAW,OAAO,QAAQ,cAAc,mCAAgB,IAAA;wBAyBxD,oBAAoB,QAAQ,cAAc,4CAAyB,IAAA;wBAqCnE,aAAa,QAAQ,cAAc,qCAAkB,IAAA;wBA6CrD,eAAe,QAAQ,cAAc,uCAAoB,IAAA;iBAuBzDA,cAAY,OAAO,QAAQ,cAAc,oCAAiB,IAAA;;wBA6B1D,eAAe,QAAQ,cAAc,uCAAoB,IAAA;;qBAS5D,yBAAyB,WAAW,mBAAmB,eAAe;;;;;;;;;;wBAwBnE,YACd,QACA;EAEA,QAAQ;EACR,aAAa,eAAe;oBAC7B,IAAA"}
|
|
@@ -3,7 +3,7 @@ import { cn } from "../../lib/utils.js";
|
|
|
3
3
|
import { Icon } from "../../lib/icons.js";
|
|
4
4
|
import { useCms } from "./context.js";
|
|
5
5
|
import { CmsBlockHeading, CmsImage, CmsLinkButton } from "./parts.js";
|
|
6
|
-
import { CMS_FORM_KEY, cmsAspectClass, cmsBlockKey, cmsBlocks, cmsColumnsClass, cmsGridClass, cmsWidthClass } from "@alfakit/core";
|
|
6
|
+
import { CMS_FORM_KEY, cmsAspectClass, cmsBlockAttributes, cmsBlockKey, cmsBlocks, cmsColumnsClass, cmsGridClass, cmsWidthClass } from "@alfakit/core";
|
|
7
7
|
import { createContext, createElement, useContext } from "react";
|
|
8
8
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
import { ChevronDownIcon, QuoteIcon } from "lucide-react";
|
|
@@ -383,7 +383,7 @@ function CmsBlocks({ blocks, components }) {
|
|
|
383
383
|
const Component = merged[block.type] ?? cms.components[block.type] ?? registry?.resolve(cmsBlockKey(block.type)) ?? CMS_BLOCK_COMPONENTS[block.type];
|
|
384
384
|
if (!Component) return null;
|
|
385
385
|
return /* @__PURE__ */ jsx("div", {
|
|
386
|
-
|
|
386
|
+
...cmsBlockAttributes(block),
|
|
387
387
|
"data-slot": "cms-block",
|
|
388
388
|
"data-cms-block": block.type,
|
|
389
389
|
children: /* @__PURE__ */ jsx(Component, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blocks.js","names":["FormBlock"],"sources":["../../../src/components/cms/blocks.tsx"],"sourcesContent":["import {\n CMS_FORM_KEY,\n cmsAspectClass,\n cmsBlockKey,\n cmsBlocks,\n cmsColumnsClass,\n cmsGridClass,\n cmsWidthClass,\n type CmsBlockSchema,\n type CmsBuiltInBlocks,\n type CmsCallToActionBlockData,\n type CmsColumnsBlockData,\n type CmsEmbedBlockData,\n type CmsFaqBlockData,\n type CmsFeaturesBlockData,\n type CmsFormBlockData,\n type CmsGalleryBlockData,\n type CmsHeroBlockData,\n type CmsImageBlockData,\n type CmsRichTextBlockData,\n type CmsSectionBlockData,\n type CmsTestimonialsBlockData,\n} from '@alfakit/core'\nimport { ChevronDownIcon, QuoteIcon } from 'lucide-react'\nimport { createContext, createElement, useContext, type ComponentType } from 'react'\nimport { useOptionalRegistry, type AnyComponent } from '../../context'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { useCms } from './context'\nimport { CmsBlockHeading, CmsImage, CmsLinkButton } from './parts'\n\n/** Props of every block renderer, built-in or registered as `cms:block:{type}`. */\nexport interface CmsBlockProps<TData = Record<string, unknown>> {\n block: CmsBlockSchema<TData>\n data: TData\n}\n\ntype BlockComponent<K extends keyof CmsBuiltInBlocks> = ComponentType<\n CmsBlockProps<CmsBuiltInBlocks[K]>\n>\n\nconst SECTION = 'px-4 py-12 sm:px-6 sm:py-16 lg:px-8'\n\n/** Block renderers given to an outer `<CmsBlocks components>`, for the blocks inside columns. */\nconst ComponentsContext = createContext<Record<string, AnyComponent>>({})\n\n/** Blocks inside a columns block: the column already has the page padding. */\nconst NestedContext = createContext(false)\n\n/** The outer padding of a block, none when it is nested in a column. */\nfunction useSection(extra?: string): string {\n return useContext(NestedContext) ? 'py-3' : cn(SECTION, extra)\n}\n\nexport function HeroBlock({ data }: CmsBlockProps<CmsHeroBlockData>) {\n const sectionClass = useSection('sm:py-20 lg:py-24')\n const centered = data.alignment === 'center' && !data.image\n\n return (\n <section data-slot=\"cms-hero\" className={sectionClass}>\n <div\n className={cn(\n 'mx-auto grid max-w-6xl items-center gap-10',\n data.image && 'lg:grid-cols-2',\n centered && 'text-center',\n )}\n >\n <div className={cn('flex flex-col gap-5', centered && 'items-center')}>\n {data.eyebrow && (\n <p className=\"text-primary text-sm font-semibold tracking-wide uppercase\">\n {data.eyebrow}\n </p>\n )}\n <h1 className=\"text-4xl font-bold tracking-tight text-balance sm:text-5xl\">\n {data.heading}\n </h1>\n {data.subheading && (\n <p className=\"text-muted-foreground max-w-2xl text-lg text-pretty sm:text-xl\">\n {data.subheading}\n </p>\n )}\n {(data.primaryAction || data.secondaryAction) && (\n <div className={cn('flex flex-wrap gap-3 pt-2', centered && 'justify-center')}>\n <CmsLinkButton link={data.primaryAction} />\n <CmsLinkButton link={data.secondaryAction} variant=\"outline\" />\n </div>\n )}\n </div>\n {data.image && (\n <CmsImage\n image={data.image}\n eager\n className=\"w-full rounded-2xl object-cover shadow-sm\"\n sizes=\"(min-width: 1024px) 50vw, 100vw\"\n />\n )}\n </div>\n </section>\n )\n}\n\nexport function RichTextBlock({ data }: CmsBlockProps<CmsRichTextBlockData>) {\n const sectionClass = useSection('py-8 sm:py-10')\n return (\n <section data-slot=\"cms-rich-text\" className={sectionClass}>\n <div\n className={cn('alfakit-prose', cmsWidthClass(data.width))}\n // Sanitised on the server (`Html::sanitize()`); never raw editor input.\n dangerouslySetInnerHTML={{ __html: data.html }}\n />\n </section>\n )\n}\n\nexport function ImageBlock({ data }: CmsBlockProps<CmsImageBlockData>) {\n const nested = useContext(NestedContext)\n const sectionClass = useSection('py-8 sm:py-10')\n return (\n <section\n data-slot=\"cms-image-block\"\n className={data.width === 'full' && !nested ? 'py-8' : sectionClass}\n >\n <figure className={cn(cmsWidthClass(data.width), 'flex flex-col gap-3')}>\n <CmsImage\n image={data.image}\n className={cn('w-full object-cover', data.width !== 'full' && 'rounded-xl')}\n />\n {data.caption && (\n <figcaption className=\"text-muted-foreground text-center text-sm\">\n {data.caption}\n </figcaption>\n )}\n </figure>\n </section>\n )\n}\n\nexport function GalleryBlock({ data }: CmsBlockProps<CmsGalleryBlockData>) {\n const sectionClass = useSection()\n return (\n <section data-slot=\"cms-gallery\" className={sectionClass}>\n <div className=\"mx-auto max-w-6xl\">\n <CmsBlockHeading heading={data.heading} />\n <ul className={cmsGridClass(data.columns)} role=\"list\">\n {data.images.map((image, index) => (\n <li key={`${image.url}-${index}`}>\n <figure className=\"flex flex-col gap-2\">\n <CmsImage image={image} className=\"aspect-[4/3] w-full rounded-xl object-cover\" />\n {image.caption && (\n <figcaption className=\"text-muted-foreground text-sm\">{image.caption}</figcaption>\n )}\n </figure>\n </li>\n ))}\n </ul>\n </div>\n </section>\n )\n}\n\nexport function CallToActionBlock({ data }: CmsBlockProps<CmsCallToActionBlockData>) {\n const sectionClass = useSection()\n const primary = data.tone === 'primary'\n\n return (\n <section data-slot=\"cms-call-to-action\" className={sectionClass}>\n <div\n className={cn(\n 'mx-auto flex max-w-5xl flex-col items-center gap-5 rounded-2xl px-6 py-12 text-center sm:px-12',\n primary && 'bg-primary text-primary-foreground',\n data.tone === 'muted' && 'bg-muted',\n data.tone === 'default' && 'border',\n )}\n >\n <h2 className=\"text-3xl font-semibold tracking-tight text-balance\">{data.heading}</h2>\n {data.body && (\n <p\n className={cn(\n 'max-w-2xl text-lg text-pretty',\n primary ? 'text-primary-foreground/85' : 'text-muted-foreground',\n )}\n >\n {data.body}\n </p>\n )}\n {(data.primaryAction || data.secondaryAction) && (\n <div className=\"flex flex-wrap justify-center gap-3 pt-2\">\n <CmsLinkButton link={data.primaryAction} variant={primary ? 'secondary' : 'default'} />\n <CmsLinkButton\n link={data.secondaryAction}\n variant=\"outline\"\n className={\n primary\n ? 'text-primary-foreground border-primary-foreground/40 hover:bg-primary-foreground/10 bg-transparent'\n : undefined\n }\n />\n </div>\n )}\n </div>\n </section>\n )\n}\n\nexport function FeaturesBlock({ data }: CmsBlockProps<CmsFeaturesBlockData>) {\n const sectionClass = useSection()\n return (\n <section data-slot=\"cms-features\" className={sectionClass}>\n <div className=\"mx-auto max-w-6xl\">\n <CmsBlockHeading heading={data.heading} intro={data.intro} />\n <ul className={cmsGridClass(data.columns)} role=\"list\">\n {data.items.map((item, index) => {\n const content = (\n <>\n {item.icon && (\n <span className=\"bg-primary/10 text-primary inline-flex size-10 items-center justify-center rounded-lg\">\n <Icon name={item.icon} className=\"size-5\" aria-hidden=\"true\" />\n </span>\n )}\n <h3 className=\"text-lg font-semibold\">{item.title}</h3>\n {item.description && (\n <p className=\"text-muted-foreground text-pretty\">{item.description}</p>\n )}\n </>\n )\n\n return (\n <li\n key={`${item.title}-${index}`}\n className=\"bg-card flex flex-col gap-3 rounded-xl border p-6\"\n >\n {item.url ? (\n <a\n href={item.url}\n className=\"focus-visible:ring-ring/50 flex flex-col gap-3 rounded-md outline-none focus-visible:ring-[3px]\"\n >\n {content}\n </a>\n ) : (\n content\n )}\n </li>\n )\n })}\n </ul>\n </div>\n </section>\n )\n}\n\nexport function FaqBlock({ block, data }: CmsBlockProps<CmsFaqBlockData>) {\n const sectionClass = useSection()\n return (\n <section data-slot=\"cms-faq\" className={sectionClass}>\n <div className=\"mx-auto max-w-3xl\">\n <CmsBlockHeading heading={data.heading} />\n <div className=\"divide-y rounded-xl border\">\n {data.items.map((item, index) => (\n <details key={`${block.id}-${index}`} className=\"group px-5\" data-slot=\"cms-faq-item\">\n <summary className=\"focus-visible:ring-ring/50 flex cursor-pointer list-none items-center justify-between gap-4 rounded-md py-4 text-start font-medium outline-none focus-visible:ring-[3px] [&::-webkit-details-marker]:hidden\">\n {item.question}\n <ChevronDownIcon\n className=\"text-muted-foreground size-4 shrink-0 transition-transform group-open:rotate-180\"\n aria-hidden=\"true\"\n />\n </summary>\n <p className=\"text-muted-foreground pb-5 whitespace-pre-line\">{item.answer}</p>\n </details>\n ))}\n </div>\n </div>\n </section>\n )\n}\n\nexport function TestimonialsBlock({ data }: CmsBlockProps<CmsTestimonialsBlockData>) {\n const sectionClass = useSection()\n return (\n <section data-slot=\"cms-testimonials\" className={sectionClass}>\n <div className=\"mx-auto max-w-6xl\">\n <CmsBlockHeading heading={data.heading} />\n <ul className={cmsGridClass(Math.min(3, data.items.length))} role=\"list\">\n {data.items.map((item, index) => (\n <li key={`${item.author}-${index}`}>\n <figure className=\"bg-card flex h-full flex-col gap-5 rounded-xl border p-6\">\n <QuoteIcon className=\"text-primary/60 size-6 rtl:-scale-x-100\" aria-hidden=\"true\" />\n <blockquote className=\"flex-1 text-pretty\">\n <p>{item.quote}</p>\n </blockquote>\n <figcaption className=\"flex items-center gap-3\">\n {item.avatar && (\n <CmsImage\n image={{ ...item.avatar, alt: '' }}\n className=\"size-10 rounded-full object-cover\"\n />\n )}\n <span className=\"flex flex-col\">\n <span className=\"font-medium\">{item.author}</span>\n {item.role && (\n <span className=\"text-muted-foreground text-sm\">{item.role}</span>\n )}\n </span>\n </figcaption>\n </figure>\n </li>\n ))}\n </ul>\n </div>\n </section>\n )\n}\n\nexport function EmbedBlock({ data }: CmsBlockProps<CmsEmbedBlockData>) {\n const sectionClass = useSection('py-8 sm:py-10')\n const { embed } = data\n\n return (\n <section data-slot=\"cms-embed\" className={sectionClass}>\n <figure className=\"mx-auto flex max-w-4xl flex-col gap-3\">\n <div\n className={cn('bg-muted overflow-hidden rounded-xl', cmsAspectClass(data.aspectRatio))}\n >\n {embed.provider === 'video' ? (\n <video\n src={embed.src}\n controls\n preload=\"metadata\"\n className=\"size-full\"\n title={embed.title}\n />\n ) : (\n <iframe\n src={embed.src}\n title={embed.title}\n loading=\"lazy\"\n className=\"size-full border-0\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen\"\n allowFullScreen\n referrerPolicy=\"strict-origin-when-cross-origin\"\n sandbox={\n embed.provider === 'iframe'\n ? 'allow-scripts allow-same-origin allow-popups allow-forms'\n : undefined\n }\n />\n )}\n </div>\n {data.caption && (\n <figcaption className=\"text-muted-foreground text-center text-sm\">\n {data.caption}\n </figcaption>\n )}\n </figure>\n </section>\n )\n}\n\nexport function ColumnsBlock({ data }: CmsBlockProps<CmsColumnsBlockData>) {\n const sectionClass = useSection('py-8 sm:py-10')\n return (\n <section data-slot=\"cms-columns\" className={sectionClass}>\n <div\n className={cn(\n 'mx-auto max-w-6xl',\n cmsColumnsClass(data.columns.length),\n data.align === 'center' && 'items-center',\n )}\n >\n {data.columns.map((column, index) => (\n <div key={index} className=\"min-w-0\" data-slot=\"cms-column\">\n <NestedContext.Provider value={true}>\n <CmsBlocks blocks={column.blocks} />\n </NestedContext.Provider>\n </div>\n ))}\n </div>\n </section>\n )\n}\n\nexport function FormBlock({ block, data }: CmsBlockProps<CmsFormBlockData>) {\n const sectionClass = useSection()\n const registry = useOptionalRegistry()\n const { components, preview, t } = useCms()\n const Form = components[CMS_FORM_KEY] ?? registry?.resolve(CMS_FORM_KEY)\n\n if (!Form) {\n // Nothing renders the form: visitors see nothing, editors see why in previews.\n return preview ? (\n <p\n className=\"text-muted-foreground mx-auto max-w-3xl px-4 py-6 text-center text-sm\"\n role=\"status\"\n >\n {t('cms.form_unavailable', {}, \"This form isn't available.\")}\n </p>\n ) : null\n }\n\n return (\n <section data-slot=\"cms-form\" className={sectionClass}>\n <div className=\"mx-auto max-w-2xl\">\n <CmsBlockHeading heading={data.heading} intro={data.description} />\n {createElement(Form, { form: data.payload, formKey: data.form, block })}\n </div>\n </section>\n )\n}\n\n/** A reusable section: its blocks, at full page width. */\nexport function SectionBlock({ data }: CmsBlockProps<CmsSectionBlockData>) {\n return (\n <div data-slot=\"cms-section\" data-section={data.name}>\n <CmsBlocks blocks={data.blocks} />\n </div>\n )\n}\n\n/** The built-in block renderers, by type. */\nexport const CMS_BLOCK_COMPONENTS: { [K in keyof CmsBuiltInBlocks]: BlockComponent<K> } = {\n hero: HeroBlock,\n 'rich-text': RichTextBlock,\n image: ImageBlock,\n gallery: GalleryBlock,\n 'call-to-action': CallToActionBlock,\n features: FeaturesBlock,\n faq: FaqBlock,\n testimonials: TestimonialsBlock,\n embed: EmbedBlock,\n columns: ColumnsBlock,\n form: FormBlock,\n section: SectionBlock,\n}\n\n/**\n * Renders rendered CMS blocks (`page.blocks`, `Cms::blocks()`, the JSON API). Each type\n * resolves, in order, to `components[type]`, the registry's `cms:block:{type}` and the\n * built-in renderer; unknown types render nothing.\n *\n * ```tsx\n * <CmsBlocks blocks={cms.page.blocks} components={{ pricing: PricingBlock }} />\n * ```\n */\nexport function CmsBlocks({\n blocks,\n components,\n}: {\n blocks: CmsBlockSchema[] | null | undefined\n components?: Record<string, AnyComponent>\n}) {\n const registry = useOptionalRegistry()\n const cms = useCms()\n const outer = useContext(ComponentsContext)\n const merged = components ? { ...outer, ...components } : outer\n\n return (\n <ComponentsContext.Provider value={merged}>\n {cmsBlocks(blocks).map((block) => {\n const Component: AnyComponent | undefined =\n merged[block.type] ??\n cms.components[block.type] ??\n registry?.resolve(cmsBlockKey(block.type)) ??\n (CMS_BLOCK_COMPONENTS as Record<string, AnyComponent>)[block.type]\n\n if (!Component) return null\n\n return (\n <div key={block.id} id={block.id} data-slot=\"cms-block\" data-cms-block={block.type}>\n <Component block={block} data={block.data} />\n </div>\n )\n })}\n </ComponentsContext.Provider>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"blocks.js","names":["FormBlock"],"sources":["../../../src/components/cms/blocks.tsx"],"sourcesContent":["import {\n CMS_FORM_KEY,\n cmsAspectClass,\n cmsBlockAttributes,\n cmsBlockKey,\n cmsBlocks,\n cmsColumnsClass,\n cmsGridClass,\n cmsWidthClass,\n type CmsBlockSchema,\n type CmsBuiltInBlocks,\n type CmsCallToActionBlockData,\n type CmsColumnsBlockData,\n type CmsEmbedBlockData,\n type CmsFaqBlockData,\n type CmsFeaturesBlockData,\n type CmsFormBlockData,\n type CmsGalleryBlockData,\n type CmsHeroBlockData,\n type CmsImageBlockData,\n type CmsRichTextBlockData,\n type CmsSectionBlockData,\n type CmsTestimonialsBlockData,\n} from '@alfakit/core'\nimport { ChevronDownIcon, QuoteIcon } from 'lucide-react'\nimport { createContext, createElement, useContext, type ComponentType } from 'react'\nimport { useOptionalRegistry, type AnyComponent } from '../../context'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { useCms } from './context'\nimport { CmsBlockHeading, CmsImage, CmsLinkButton } from './parts'\n\n/** Props of every block renderer, built-in or registered as `cms:block:{type}`. */\nexport interface CmsBlockProps<TData = Record<string, unknown>> {\n block: CmsBlockSchema<TData>\n data: TData\n}\n\ntype BlockComponent<K extends keyof CmsBuiltInBlocks> = ComponentType<\n CmsBlockProps<CmsBuiltInBlocks[K]>\n>\n\nconst SECTION = 'px-4 py-12 sm:px-6 sm:py-16 lg:px-8'\n\n/** Block renderers given to an outer `<CmsBlocks components>`, for the blocks inside columns. */\nconst ComponentsContext = createContext<Record<string, AnyComponent>>({})\n\n/** Blocks inside a columns block: the column already has the page padding. */\nconst NestedContext = createContext(false)\n\n/** The outer padding of a block, none when it is nested in a column. */\nfunction useSection(extra?: string): string {\n return useContext(NestedContext) ? 'py-3' : cn(SECTION, extra)\n}\n\nexport function HeroBlock({ data }: CmsBlockProps<CmsHeroBlockData>) {\n const sectionClass = useSection('sm:py-20 lg:py-24')\n const centered = data.alignment === 'center' && !data.image\n\n return (\n <section data-slot=\"cms-hero\" className={sectionClass}>\n <div\n className={cn(\n 'mx-auto grid max-w-6xl items-center gap-10',\n data.image && 'lg:grid-cols-2',\n centered && 'text-center',\n )}\n >\n <div className={cn('flex flex-col gap-5', centered && 'items-center')}>\n {data.eyebrow && (\n <p className=\"text-primary text-sm font-semibold tracking-wide uppercase\">\n {data.eyebrow}\n </p>\n )}\n <h1 className=\"text-4xl font-bold tracking-tight text-balance sm:text-5xl\">\n {data.heading}\n </h1>\n {data.subheading && (\n <p className=\"text-muted-foreground max-w-2xl text-lg text-pretty sm:text-xl\">\n {data.subheading}\n </p>\n )}\n {(data.primaryAction || data.secondaryAction) && (\n <div className={cn('flex flex-wrap gap-3 pt-2', centered && 'justify-center')}>\n <CmsLinkButton link={data.primaryAction} />\n <CmsLinkButton link={data.secondaryAction} variant=\"outline\" />\n </div>\n )}\n </div>\n {data.image && (\n <CmsImage\n image={data.image}\n eager\n className=\"w-full rounded-2xl object-cover shadow-sm\"\n sizes=\"(min-width: 1024px) 50vw, 100vw\"\n />\n )}\n </div>\n </section>\n )\n}\n\nexport function RichTextBlock({ data }: CmsBlockProps<CmsRichTextBlockData>) {\n const sectionClass = useSection('py-8 sm:py-10')\n return (\n <section data-slot=\"cms-rich-text\" className={sectionClass}>\n <div\n className={cn('alfakit-prose', cmsWidthClass(data.width))}\n // Sanitised on the server (`Html::sanitize()`); never raw editor input.\n dangerouslySetInnerHTML={{ __html: data.html }}\n />\n </section>\n )\n}\n\nexport function ImageBlock({ data }: CmsBlockProps<CmsImageBlockData>) {\n const nested = useContext(NestedContext)\n const sectionClass = useSection('py-8 sm:py-10')\n return (\n <section\n data-slot=\"cms-image-block\"\n className={data.width === 'full' && !nested ? 'py-8' : sectionClass}\n >\n <figure className={cn(cmsWidthClass(data.width), 'flex flex-col gap-3')}>\n <CmsImage\n image={data.image}\n className={cn('w-full object-cover', data.width !== 'full' && 'rounded-xl')}\n />\n {data.caption && (\n <figcaption className=\"text-muted-foreground text-center text-sm\">\n {data.caption}\n </figcaption>\n )}\n </figure>\n </section>\n )\n}\n\nexport function GalleryBlock({ data }: CmsBlockProps<CmsGalleryBlockData>) {\n const sectionClass = useSection()\n return (\n <section data-slot=\"cms-gallery\" className={sectionClass}>\n <div className=\"mx-auto max-w-6xl\">\n <CmsBlockHeading heading={data.heading} />\n <ul className={cmsGridClass(data.columns)} role=\"list\">\n {data.images.map((image, index) => (\n <li key={`${image.url}-${index}`}>\n <figure className=\"flex flex-col gap-2\">\n <CmsImage image={image} className=\"aspect-[4/3] w-full rounded-xl object-cover\" />\n {image.caption && (\n <figcaption className=\"text-muted-foreground text-sm\">{image.caption}</figcaption>\n )}\n </figure>\n </li>\n ))}\n </ul>\n </div>\n </section>\n )\n}\n\nexport function CallToActionBlock({ data }: CmsBlockProps<CmsCallToActionBlockData>) {\n const sectionClass = useSection()\n const primary = data.tone === 'primary'\n\n return (\n <section data-slot=\"cms-call-to-action\" className={sectionClass}>\n <div\n className={cn(\n 'mx-auto flex max-w-5xl flex-col items-center gap-5 rounded-2xl px-6 py-12 text-center sm:px-12',\n primary && 'bg-primary text-primary-foreground',\n data.tone === 'muted' && 'bg-muted',\n data.tone === 'default' && 'border',\n )}\n >\n <h2 className=\"text-3xl font-semibold tracking-tight text-balance\">{data.heading}</h2>\n {data.body && (\n <p\n className={cn(\n 'max-w-2xl text-lg text-pretty',\n primary ? 'text-primary-foreground/85' : 'text-muted-foreground',\n )}\n >\n {data.body}\n </p>\n )}\n {(data.primaryAction || data.secondaryAction) && (\n <div className=\"flex flex-wrap justify-center gap-3 pt-2\">\n <CmsLinkButton link={data.primaryAction} variant={primary ? 'secondary' : 'default'} />\n <CmsLinkButton\n link={data.secondaryAction}\n variant=\"outline\"\n className={\n primary\n ? 'text-primary-foreground border-primary-foreground/40 hover:bg-primary-foreground/10 bg-transparent'\n : undefined\n }\n />\n </div>\n )}\n </div>\n </section>\n )\n}\n\nexport function FeaturesBlock({ data }: CmsBlockProps<CmsFeaturesBlockData>) {\n const sectionClass = useSection()\n return (\n <section data-slot=\"cms-features\" className={sectionClass}>\n <div className=\"mx-auto max-w-6xl\">\n <CmsBlockHeading heading={data.heading} intro={data.intro} />\n <ul className={cmsGridClass(data.columns)} role=\"list\">\n {data.items.map((item, index) => {\n const content = (\n <>\n {item.icon && (\n <span className=\"bg-primary/10 text-primary inline-flex size-10 items-center justify-center rounded-lg\">\n <Icon name={item.icon} className=\"size-5\" aria-hidden=\"true\" />\n </span>\n )}\n <h3 className=\"text-lg font-semibold\">{item.title}</h3>\n {item.description && (\n <p className=\"text-muted-foreground text-pretty\">{item.description}</p>\n )}\n </>\n )\n\n return (\n <li\n key={`${item.title}-${index}`}\n className=\"bg-card flex flex-col gap-3 rounded-xl border p-6\"\n >\n {item.url ? (\n <a\n href={item.url}\n className=\"focus-visible:ring-ring/50 flex flex-col gap-3 rounded-md outline-none focus-visible:ring-[3px]\"\n >\n {content}\n </a>\n ) : (\n content\n )}\n </li>\n )\n })}\n </ul>\n </div>\n </section>\n )\n}\n\nexport function FaqBlock({ block, data }: CmsBlockProps<CmsFaqBlockData>) {\n const sectionClass = useSection()\n return (\n <section data-slot=\"cms-faq\" className={sectionClass}>\n <div className=\"mx-auto max-w-3xl\">\n <CmsBlockHeading heading={data.heading} />\n <div className=\"divide-y rounded-xl border\">\n {data.items.map((item, index) => (\n <details key={`${block.id}-${index}`} className=\"group px-5\" data-slot=\"cms-faq-item\">\n <summary className=\"focus-visible:ring-ring/50 flex cursor-pointer list-none items-center justify-between gap-4 rounded-md py-4 text-start font-medium outline-none focus-visible:ring-[3px] [&::-webkit-details-marker]:hidden\">\n {item.question}\n <ChevronDownIcon\n className=\"text-muted-foreground size-4 shrink-0 transition-transform group-open:rotate-180\"\n aria-hidden=\"true\"\n />\n </summary>\n <p className=\"text-muted-foreground pb-5 whitespace-pre-line\">{item.answer}</p>\n </details>\n ))}\n </div>\n </div>\n </section>\n )\n}\n\nexport function TestimonialsBlock({ data }: CmsBlockProps<CmsTestimonialsBlockData>) {\n const sectionClass = useSection()\n return (\n <section data-slot=\"cms-testimonials\" className={sectionClass}>\n <div className=\"mx-auto max-w-6xl\">\n <CmsBlockHeading heading={data.heading} />\n <ul className={cmsGridClass(Math.min(3, data.items.length))} role=\"list\">\n {data.items.map((item, index) => (\n <li key={`${item.author}-${index}`}>\n <figure className=\"bg-card flex h-full flex-col gap-5 rounded-xl border p-6\">\n <QuoteIcon className=\"text-primary/60 size-6 rtl:-scale-x-100\" aria-hidden=\"true\" />\n <blockquote className=\"flex-1 text-pretty\">\n <p>{item.quote}</p>\n </blockquote>\n <figcaption className=\"flex items-center gap-3\">\n {item.avatar && (\n <CmsImage\n image={{ ...item.avatar, alt: '' }}\n className=\"size-10 rounded-full object-cover\"\n />\n )}\n <span className=\"flex flex-col\">\n <span className=\"font-medium\">{item.author}</span>\n {item.role && (\n <span className=\"text-muted-foreground text-sm\">{item.role}</span>\n )}\n </span>\n </figcaption>\n </figure>\n </li>\n ))}\n </ul>\n </div>\n </section>\n )\n}\n\nexport function EmbedBlock({ data }: CmsBlockProps<CmsEmbedBlockData>) {\n const sectionClass = useSection('py-8 sm:py-10')\n const { embed } = data\n\n return (\n <section data-slot=\"cms-embed\" className={sectionClass}>\n <figure className=\"mx-auto flex max-w-4xl flex-col gap-3\">\n <div\n className={cn('bg-muted overflow-hidden rounded-xl', cmsAspectClass(data.aspectRatio))}\n >\n {embed.provider === 'video' ? (\n <video\n src={embed.src}\n controls\n preload=\"metadata\"\n className=\"size-full\"\n title={embed.title}\n />\n ) : (\n <iframe\n src={embed.src}\n title={embed.title}\n loading=\"lazy\"\n className=\"size-full border-0\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen\"\n allowFullScreen\n referrerPolicy=\"strict-origin-when-cross-origin\"\n sandbox={\n embed.provider === 'iframe'\n ? 'allow-scripts allow-same-origin allow-popups allow-forms'\n : undefined\n }\n />\n )}\n </div>\n {data.caption && (\n <figcaption className=\"text-muted-foreground text-center text-sm\">\n {data.caption}\n </figcaption>\n )}\n </figure>\n </section>\n )\n}\n\nexport function ColumnsBlock({ data }: CmsBlockProps<CmsColumnsBlockData>) {\n const sectionClass = useSection('py-8 sm:py-10')\n return (\n <section data-slot=\"cms-columns\" className={sectionClass}>\n <div\n className={cn(\n 'mx-auto max-w-6xl',\n cmsColumnsClass(data.columns.length),\n data.align === 'center' && 'items-center',\n )}\n >\n {data.columns.map((column, index) => (\n <div key={index} className=\"min-w-0\" data-slot=\"cms-column\">\n <NestedContext.Provider value={true}>\n <CmsBlocks blocks={column.blocks} />\n </NestedContext.Provider>\n </div>\n ))}\n </div>\n </section>\n )\n}\n\nexport function FormBlock({ block, data }: CmsBlockProps<CmsFormBlockData>) {\n const sectionClass = useSection()\n const registry = useOptionalRegistry()\n const { components, preview, t } = useCms()\n const Form = components[CMS_FORM_KEY] ?? registry?.resolve(CMS_FORM_KEY)\n\n if (!Form) {\n // Nothing renders the form: visitors see nothing, editors see why in previews.\n return preview ? (\n <p\n className=\"text-muted-foreground mx-auto max-w-3xl px-4 py-6 text-center text-sm\"\n role=\"status\"\n >\n {t('cms.form_unavailable', {}, \"This form isn't available.\")}\n </p>\n ) : null\n }\n\n return (\n <section data-slot=\"cms-form\" className={sectionClass}>\n <div className=\"mx-auto max-w-2xl\">\n <CmsBlockHeading heading={data.heading} intro={data.description} />\n {createElement(Form, { form: data.payload, formKey: data.form, block })}\n </div>\n </section>\n )\n}\n\n/** A reusable section: its blocks, at full page width. */\nexport function SectionBlock({ data }: CmsBlockProps<CmsSectionBlockData>) {\n return (\n <div data-slot=\"cms-section\" data-section={data.name}>\n <CmsBlocks blocks={data.blocks} />\n </div>\n )\n}\n\n/** The built-in block renderers, by type. */\nexport const CMS_BLOCK_COMPONENTS: { [K in keyof CmsBuiltInBlocks]: BlockComponent<K> } = {\n hero: HeroBlock,\n 'rich-text': RichTextBlock,\n image: ImageBlock,\n gallery: GalleryBlock,\n 'call-to-action': CallToActionBlock,\n features: FeaturesBlock,\n faq: FaqBlock,\n testimonials: TestimonialsBlock,\n embed: EmbedBlock,\n columns: ColumnsBlock,\n form: FormBlock,\n section: SectionBlock,\n}\n\n/**\n * Renders rendered CMS blocks (`page.blocks`, `Cms::blocks()`, the JSON API). Each type\n * resolves, in order, to `components[type]`, the registry's `cms:block:{type}` and the\n * built-in renderer; unknown types render nothing.\n *\n * ```tsx\n * <CmsBlocks blocks={cms.page.blocks} components={{ pricing: PricingBlock }} />\n * ```\n */\nexport function CmsBlocks({\n blocks,\n components,\n}: {\n blocks: CmsBlockSchema[] | null | undefined\n components?: Record<string, AnyComponent>\n}) {\n const registry = useOptionalRegistry()\n const cms = useCms()\n const outer = useContext(ComponentsContext)\n const merged = components ? { ...outer, ...components } : outer\n\n return (\n <ComponentsContext.Provider value={merged}>\n {cmsBlocks(blocks).map((block) => {\n const Component: AnyComponent | undefined =\n merged[block.type] ??\n cms.components[block.type] ??\n registry?.resolve(cmsBlockKey(block.type)) ??\n (CMS_BLOCK_COMPONENTS as Record<string, AnyComponent>)[block.type]\n\n if (!Component) return null\n\n return (\n <div\n key={block.id}\n {...cmsBlockAttributes(block)}\n data-slot=\"cms-block\"\n data-cms-block={block.type}\n >\n <Component block={block} data={block.data} />\n </div>\n )\n })}\n </ComponentsContext.Provider>\n )\n}\n"],"mappings":"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"}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { CmsSeoSchema } from "@alfakit/core";
|
|
1
|
+
import { CmsSeoSchema, CmsSiteDesign } from "@alfakit/core";
|
|
2
2
|
//#region src/components/cms/head.d.ts
|
|
3
3
|
/**
|
|
4
|
-
* The page's `<title>`, meta, canonical, `hreflang` and JSON-LD tags,
|
|
5
|
-
* `<Head>` (server-rendered with SSR,
|
|
4
|
+
* The page's `<title>`, meta, canonical, `hreflang` and JSON-LD tags, plus the site
|
|
5
|
+
* design's favicon and fonts, through Inertia's `<Head>` (server-rendered with SSR,
|
|
6
|
+
* updated on every visit).
|
|
6
7
|
*/
|
|
7
|
-
export declare function CmsHead({ seo }: {
|
|
8
|
+
export declare function CmsHead({ seo, design }: {
|
|
8
9
|
seo: CmsSeoSchema | null | undefined;
|
|
10
|
+
design?: CmsSiteDesign | null;
|
|
9
11
|
}): import("react").JSX.Element;
|
|
10
12
|
//#endregion
|
|
11
13
|
//# sourceMappingURL=head.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"head.d.ts","names":[],"sources":["../../../src/components/cms/head.tsx"],"mappings":"
|
|
1
|
+
{"version":3,"file":"head.d.ts","names":[],"sources":["../../../src/components/cms/head.tsx"],"mappings":";;;;;;;wBAQgB,UACd,KACA;EAEA,KAAK;EACL,SAAS;oBACV,IAAA"}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { cmsHeadTags } from "@alfakit/core";
|
|
1
|
+
import { cmsDesignLinks, cmsHeadTags } from "@alfakit/core";
|
|
2
2
|
import { Head } from "@inertiajs/react";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
//#region src/components/cms/head.tsx
|
|
5
5
|
/**
|
|
6
|
-
* The page's `<title>`, meta, canonical, `hreflang` and JSON-LD tags,
|
|
7
|
-
* `<Head>` (server-rendered with SSR,
|
|
6
|
+
* The page's `<title>`, meta, canonical, `hreflang` and JSON-LD tags, plus the site
|
|
7
|
+
* design's favicon and fonts, through Inertia's `<Head>` (server-rendered with SSR,
|
|
8
|
+
* updated on every visit).
|
|
8
9
|
*/
|
|
9
|
-
function CmsHead({ seo }) {
|
|
10
|
+
function CmsHead({ seo, design }) {
|
|
10
11
|
const tags = cmsHeadTags(seo);
|
|
11
12
|
return /* @__PURE__ */ jsxs(Head, {
|
|
12
13
|
title: tags.title,
|
|
@@ -26,6 +27,11 @@ function CmsHead({ seo }) {
|
|
|
26
27
|
href: link.href,
|
|
27
28
|
hrefLang: link.hreflang
|
|
28
29
|
}, `${link.rel}:${link.hreflang ?? ""}:${link.href}`)),
|
|
30
|
+
cmsDesignLinks(design).map((link) => /* @__PURE__ */ jsx("link", {
|
|
31
|
+
"head-key": `cms:design:${link.rel}`,
|
|
32
|
+
rel: link.rel,
|
|
33
|
+
href: link.href
|
|
34
|
+
}, `design:${link.rel}`)),
|
|
29
35
|
tags.jsonLd && /* @__PURE__ */ jsx("script", {
|
|
30
36
|
"head-key": "cms:jsonld",
|
|
31
37
|
type: "application/ld+json",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"head.js","names":[],"sources":["../../../src/components/cms/head.tsx"],"sourcesContent":["import { cmsHeadTags, type CmsSeoSchema } from '@alfakit/core'\nimport { Head } from '@inertiajs/react'\n\n/**\n * The page's `<title>`, meta, canonical, `hreflang` and JSON-LD tags, through Inertia's
|
|
1
|
+
{"version":3,"file":"head.js","names":[],"sources":["../../../src/components/cms/head.tsx"],"sourcesContent":["import { cmsDesignLinks, cmsHeadTags, type CmsSeoSchema, type CmsSiteDesign } from '@alfakit/core'\nimport { Head } from '@inertiajs/react'\n\n/**\n * The page's `<title>`, meta, canonical, `hreflang` and JSON-LD tags, plus the site\n * design's favicon and fonts, through Inertia's `<Head>` (server-rendered with SSR,\n * updated on every visit).\n */\nexport function CmsHead({\n seo,\n design,\n}: {\n seo: CmsSeoSchema | null | undefined\n design?: CmsSiteDesign | null\n}) {\n const tags = cmsHeadTags(seo)\n\n return (\n <Head title={tags.title}>\n {tags.meta.map((tag) =>\n tag.property ? (\n <meta\n key={`p:${tag.property}`}\n head-key={`cms:${tag.property}`}\n property={tag.property}\n content={tag.content}\n />\n ) : (\n <meta\n key={`n:${tag.name}`}\n head-key={`cms:${tag.name}`}\n name={tag.name}\n content={tag.content}\n />\n ),\n )}\n {tags.links.map((link) => (\n <link\n key={`${link.rel}:${link.hreflang ?? ''}:${link.href}`}\n head-key={`cms:${link.rel}:${link.hreflang ?? ''}`}\n rel={link.rel}\n href={link.href}\n hrefLang={link.hreflang}\n />\n ))}\n {cmsDesignLinks(design).map((link) => (\n <link\n key={`design:${link.rel}`}\n head-key={`cms:design:${link.rel}`}\n rel={link.rel}\n href={link.href}\n />\n ))}\n {/* Encoded on the server with `<` escaped, so it can't close the element. */}\n {tags.jsonLd && (\n <script head-key=\"cms:jsonld\" type=\"application/ld+json\">\n {tags.jsonLd}\n </script>\n )}\n </Head>\n )\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,QAAQ,EACtB,KACA,UAIC;CACD,MAAM,OAAO,YAAY,GAAG;CAE5B,OACE,qBAAC,MAAD;EAAM,OAAO,KAAK;EAAlB,UAAA;GACG,KAAK,KAAK,KAAK,QACd,IAAI,WACF,oBAAC,QAAD;IAEE,YAAU,OAAO,IAAI;IACrB,UAAU,IAAI;IACd,SAAS,IAAI;GACd,GAJM,KAAK,IAAI,UAIf,IAED,oBAAC,QAAD;IAEE,YAAU,OAAO,IAAI;IACrB,MAAM,IAAI;IACV,SAAS,IAAI;GACd,GAJM,KAAK,IAAI,MAIf,CAEL;GACC,KAAK,MAAM,KAAK,SACf,oBAAC,QAAD;IAEE,YAAU,OAAO,KAAK,IAAI,GAAG,KAAK,YAAY;IAC9C,KAAK,KAAK;IACV,MAAM,KAAK;IACX,UAAU,KAAK;GAChB,GALM,GAAG,KAAK,IAAI,GAAG,KAAK,YAAY,GAAG,GAAG,KAAK,MAKjD,CACF;GACA,eAAe,MAAM,CAAC,CAAC,KAAK,SAC3B,oBAAC,QAAD;IAEE,YAAU,cAAc,KAAK;IAC7B,KAAK,KAAK;IACV,MAAM,KAAK;GACZ,GAJM,UAAU,KAAK,KAIrB,CACF;GAEA,KAAK,UACJ,oBAAC,UAAD;IAAQ,YAAS;IAAa,MAAK;IAChC,UAAA,KAAK;GACA,CAAA;EAEN;;AAEV"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page.d.ts","names":[],"sources":["../../../src/components/cms/page.tsx"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"page.d.ts","names":[],"sources":["../../../src/components/cms/page.tsx"],"mappings":";;;;wBAkCgB,YAAY;EAAS,KAAK;oBAAY,IAAA;;wBAiCtC,YAAY;EAAS,KAAK;oBAAY,IAAA;;;;;;;;;;;;wBAqItC,UACd,KAAK,SACL,cACC,QAAQ;EAAkB,aAAa,eAAe;oBAAe,IAAA"}
|
|
@@ -6,7 +6,7 @@ import { CmsBlocks } from "./blocks.js";
|
|
|
6
6
|
import { CmsHead } from "./head.js";
|
|
7
7
|
import { CmsLanguageSwitcher, CmsMenuList, CmsNav } from "./menu.js";
|
|
8
8
|
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "../ui/breadcrumb.js";
|
|
9
|
-
import { CMS_FOOTER_KEY, CMS_HEADER_KEY, cmsPayload } from "@alfakit/core";
|
|
9
|
+
import { CMS_FOOTER_KEY, CMS_HEADER_KEY, cmsDesignCss, cmsPayload } from "@alfakit/core";
|
|
10
10
|
import { usePage } from "@inertiajs/react";
|
|
11
11
|
import { Fragment, createElement, useEffect } from "react";
|
|
12
12
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -24,7 +24,12 @@ function CmsHeader({ cms }) {
|
|
|
24
24
|
children: [/* @__PURE__ */ jsx("a", {
|
|
25
25
|
href: cms.site.url,
|
|
26
26
|
className: "focus-visible:ring-ring/50 rounded-md text-lg font-semibold tracking-tight outline-none focus-visible:ring-[3px]",
|
|
27
|
-
children: cms.site.
|
|
27
|
+
children: cms.site.design?.logo ? /* @__PURE__ */ jsx("img", {
|
|
28
|
+
src: cms.site.design.logo.url,
|
|
29
|
+
alt: cms.site.design.logo.alt,
|
|
30
|
+
"data-slot": "cms-logo",
|
|
31
|
+
className: "h-9 w-auto max-w-48 object-contain"
|
|
32
|
+
}) : cms.site.name
|
|
28
33
|
}), /* @__PURE__ */ jsx("div", {
|
|
29
34
|
className: "flex items-center gap-2",
|
|
30
35
|
children: /* @__PURE__ */ jsx(CmsNav, {
|
|
@@ -106,6 +111,7 @@ function CmsPageLayout({ cms, children }) {
|
|
|
106
111
|
const { t } = useCms();
|
|
107
112
|
const header = registry?.resolve(CMS_HEADER_KEY) ?? CmsHeader;
|
|
108
113
|
const footer = registry?.resolve(CMS_FOOTER_KEY) ?? CmsFooter;
|
|
114
|
+
const designCss = cmsDesignCss(cms.site.design);
|
|
109
115
|
return /* @__PURE__ */ jsxs("div", {
|
|
110
116
|
dir: cms.site.direction,
|
|
111
117
|
lang: cms.site.tag,
|
|
@@ -118,6 +124,10 @@ function CmsPageLayout({ cms, children }) {
|
|
|
118
124
|
className: "bg-background focus-visible:ring-ring/50 sr-only z-50 rounded-md px-4 py-2 focus:not-sr-only focus:absolute focus:start-4 focus:top-4 focus-visible:ring-[3px]",
|
|
119
125
|
children: t("cms.skip_to_content", {}, "Skip to content")
|
|
120
126
|
}),
|
|
127
|
+
designCss && /* @__PURE__ */ jsx("style", {
|
|
128
|
+
"data-slot": "cms-design",
|
|
129
|
+
dangerouslySetInnerHTML: { __html: designCss }
|
|
130
|
+
}),
|
|
121
131
|
cms.preview && /* @__PURE__ */ jsx(PreviewBanner, {}),
|
|
122
132
|
createElement(header, { cms }),
|
|
123
133
|
/* @__PURE__ */ jsxs("main", {
|
|
@@ -164,7 +174,10 @@ function CmsPage({ cms: cmsProp, components }) {
|
|
|
164
174
|
return /* @__PURE__ */ jsxs(CmsProvider, {
|
|
165
175
|
payload: cms,
|
|
166
176
|
components,
|
|
167
|
-
children: [/* @__PURE__ */ jsx(CmsHead, {
|
|
177
|
+
children: [/* @__PURE__ */ jsx(CmsHead, {
|
|
178
|
+
seo: cms.page.seo,
|
|
179
|
+
design: cms.site.design
|
|
180
|
+
}), /* @__PURE__ */ jsx(CmsPageLayout, { cms })]
|
|
168
181
|
});
|
|
169
182
|
}
|
|
170
183
|
function usePageProps() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page.js","names":[],"sources":["../../../src/components/cms/page.tsx"],"sourcesContent":["import {\n CMS_FOOTER_KEY,\n CMS_HEADER_KEY,\n cmsPayload,\n type CmsPageProps,\n type CmsPayload,\n} from '@alfakit/core'\nimport { usePage } from '@inertiajs/react'\nimport { EyeIcon } from 'lucide-react'\nimport { Fragment, createElement, useEffect, type ReactNode } from 'react'\nimport { useOptionalRegistry, type AnyComponent } from '../../context'\nimport { lazyComponent } from '../../lib/lazy'\nimport { cn } from '../../lib/utils'\nimport {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n BreadcrumbSeparator,\n} from '../ui/breadcrumb'\nimport { CmsBlocks } from './blocks'\nimport { CmsProvider, useCms } from './context'\nimport { CmsHead } from './head'\nimport { CmsLanguageSwitcher, CmsMenuList, CmsNav } from './menu'\n\n/** The admin bar loads only for signed-in editors (the server sends `cms.admin` to them alone). */\nconst CmsAdminBar = lazyComponent<{ admin: NonNullable<CmsPayload['admin']> }>(\n () => import('./admin-bar').then((module) => module.CmsAdminBar),\n () => null,\n)\n\n/** The site header: the site name, the `main` menu and the language links. */\nexport function CmsHeader({ cms }: { cms: CmsPayload }) {\n return (\n <header\n data-slot=\"cms-header\"\n className=\"bg-background/90 supports-[backdrop-filter]:bg-background/70 sticky top-0 z-30 border-b backdrop-blur\"\n >\n <div className=\"relative mx-auto flex h-16 max-w-6xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8\">\n <a\n href={cms.site.url}\n className=\"focus-visible:ring-ring/50 rounded-md text-lg font-semibold tracking-tight outline-none focus-visible:ring-[3px]\"\n >\n {cms.site.name}\n </a>\n <div className=\"flex items-center gap-2\">\n <CmsNav menu={cms.menus.main}>\n <CmsLanguageSwitcher site={cms.site} className=\"hidden sm:block\" />\n </CmsNav>\n </div>\n </div>\n </header>\n )\n}\n\n/** The site footer: the `footer` menu and the site name. */\nexport function CmsFooter({ cms }: { cms: CmsPayload }) {\n const { t } = useCms()\n const footer = cms.menus.footer\n\n return (\n <footer data-slot=\"cms-footer\" className=\"border-t\">\n <div className=\"mx-auto flex max-w-6xl flex-col gap-6 px-4 py-10 sm:flex-row sm:items-start sm:justify-between sm:px-6 lg:px-8\">\n <p className=\"font-semibold\">{cms.site.name}</p>\n {footer && footer.items.length > 0 && (\n <nav aria-label={t('cms.footer_navigation', {}, 'Footer navigation')}>\n <CmsMenuList\n items={footer.items}\n className=\"flex flex-col gap-1 sm:flex-row sm:flex-wrap sm:gap-x-4\"\n />\n </nav>\n )}\n <CmsLanguageSwitcher site={cms.site} className=\"sm:hidden\" />\n </div>\n </footer>\n )\n}\n\nfunction CmsBreadcrumbs({ cms }: { cms: CmsPayload }) {\n const { t } = useCms()\n const crumbs = cms.page.breadcrumbs\n\n if (crumbs.length < 2) return null\n\n return (\n <Breadcrumb\n aria-label={t('cms.breadcrumbs', {}, 'Breadcrumbs')}\n className=\"mx-auto max-w-6xl px-4 pt-6 sm:px-6 lg:px-8\"\n >\n <BreadcrumbList>\n {crumbs.map((crumb, index) => (\n <Fragment key={crumb.url}>\n {index > 0 && <BreadcrumbSeparator />}\n <BreadcrumbItem>\n {index === crumbs.length - 1 ? (\n <BreadcrumbPage>{crumb.label}</BreadcrumbPage>\n ) : (\n <BreadcrumbLink href={crumb.url}>{crumb.label}</BreadcrumbLink>\n )}\n </BreadcrumbItem>\n </Fragment>\n ))}\n </BreadcrumbList>\n </Breadcrumb>\n )\n}\n\nfunction PreviewBanner() {\n const { t } = useCms()\n\n return (\n <div\n role=\"status\"\n data-slot=\"cms-preview-banner\"\n className=\"flex items-center justify-center gap-2 bg-amber-100 px-4 py-2 text-sm text-amber-950 dark:bg-amber-950 dark:text-amber-100\"\n >\n <EyeIcon className=\"size-4 shrink-0\" aria-hidden=\"true\" />\n <span>\n <strong className=\"font-semibold\">{t('cms.preview', {}, 'Preview')}</strong> ·{' '}\n {t('cms.preview_notice', {}, \"This is a preview. Visitors don't see unpublished changes.\")}\n </span>\n </div>\n )\n}\n\n/** The page body inside `CmsPage`: header, breadcrumbs, blocks and footer. */\nfunction CmsPageLayout({ cms, children }: { cms: CmsPayload; children?: ReactNode }) {\n const registry = useOptionalRegistry()\n const { t } = useCms()\n const header: AnyComponent = registry?.resolve(CMS_HEADER_KEY) ?? CmsHeader\n const footer: AnyComponent = registry?.resolve(CMS_FOOTER_KEY) ?? CmsFooter\n\n return (\n <div\n dir={cms.site.direction}\n lang={cms.site.tag}\n data-slot=\"cms-page\"\n data-layout={cms.page.layout ?? undefined}\n className=\"bg-background text-foreground flex min-h-svh flex-col\"\n >\n <a\n href=\"#cms-content\"\n className=\"bg-background focus-visible:ring-ring/50 sr-only z-50 rounded-md px-4 py-2 focus:not-sr-only focus:absolute focus:start-4 focus:top-4 focus-visible:ring-[3px]\"\n >\n {t('cms.skip_to_content', {}, 'Skip to content')}\n </a>\n {cms.preview && <PreviewBanner />}\n {createElement(header, { cms })}\n <main\n id=\"cms-content\"\n tabIndex={-1}\n className={cn('flex-1 outline-none')}\n data-slot=\"cms-content\"\n >\n <CmsBreadcrumbs cms={cms} />\n {children ??\n (cms.page.blocks.length > 0 ? (\n <CmsBlocks blocks={cms.page.blocks} />\n ) : (\n <p className=\"text-muted-foreground mx-auto max-w-3xl px-4 py-24 text-center\">\n {t('cms.empty_page', {}, 'This page has no content yet.')}\n </p>\n ))}\n </main>\n {createElement(footer, { cms })}\n {cms.poweredBy && (\n <div data-slot=\"cms-powered-by\" dangerouslySetInnerHTML={{ __html: cms.poweredBy }} />\n )}\n {cms.admin && <CmsAdminBar admin={cms.admin} />}\n </div>\n )\n}\n\n/**\n * The `Alfakit/CmsPage` Inertia page (public CMS pages and previews). Register it in your\n * public site's Inertia app:\n *\n * ```tsx\n * createAlfakit({ pages: { 'Alfakit/CmsPage': CmsPage } })\n * ```\n *\n * Replace the header or footer with `cms:header` / `cms:footer` registry components, a\n * block with `cms:block:{type}`, or build your own page from `CmsBlocks` and `CmsHead`.\n */\nexport function CmsPage({\n cms: cmsProp,\n components,\n}: Partial<CmsPageProps> & { components?: Record<string, AnyComponent> }) {\n const page = usePageProps()\n const cms = cmsProp ?? cmsPayload(page)\n\n const tag = cms?.site.tag\n const direction = cms?.site.direction\n\n useEffect(() => {\n if (!tag || !direction) return\n\n document.documentElement.lang = tag\n document.documentElement.dir = direction\n }, [tag, direction])\n\n if (!cms) return null\n\n return (\n <CmsProvider payload={cms} components={components}>\n <CmsHead seo={cms.page.seo} />\n <CmsPageLayout cms={cms} />\n </CmsProvider>\n )\n}\n\nfunction usePageProps(): unknown {\n try {\n return usePage().props\n } catch {\n return null\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AA2BA,MAAM,cAAc,oBACZ,OAAO,iBAAc,CAAC,MAAM,WAAW,OAAO,WAAW,SACzD,IACR;;AAGA,SAAgB,UAAU,EAAE,OAA4B;CACtD,OACE,oBAAC,UAAD;EACE,aAAU;EACV,WAAU;EAEV,UAAA,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,oBAAC,KAAD;IACE,MAAM,IAAI,KAAK;IACf,WAAU;IAET,UAAA,IAAI,KAAK;GACT,CAAA,GACH,oBAAC,OAAD;IAAK,WAAU;IACb,UAAA,oBAAC,QAAD;KAAQ,MAAM,IAAI,MAAM;KACtB,UAAA,oBAAC,qBAAD;MAAqB,MAAM,IAAI;MAAM,WAAU;KAAmB,CAAA;IAC5D,CAAA;GACL,CAAA,CACF;;CACC,CAAA;AAEZ;;AAGA,SAAgB,UAAU,EAAE,OAA4B;CACtD,MAAM,EAAE,MAAM,OAAO;CACrB,MAAM,SAAS,IAAI,MAAM;CAEzB,OACE,oBAAC,UAAD;EAAQ,aAAU;EAAa,WAAU;EACvC,UAAA,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA;IACE,oBAAC,KAAD;KAAG,WAAU;KAAiB,UAAA,IAAI,KAAK;IAAQ,CAAA;IAC9C,UAAU,OAAO,MAAM,SAAS,KAC/B,oBAAC,OAAD;KAAK,cAAY,EAAE,yBAAyB,CAAC,GAAG,mBAAmB;KACjE,UAAA,oBAAC,aAAD;MACE,OAAO,OAAO;MACd,WAAU;KACX,CAAA;IACE,CAAA;IAEP,oBAAC,qBAAD;KAAqB,MAAM,IAAI;KAAM,WAAU;IAAa,CAAA;GACzD;;CACC,CAAA;AAEZ;AAEA,SAAS,eAAe,EAAE,OAA4B;CACpD,MAAM,EAAE,MAAM,OAAO;CACrB,MAAM,SAAS,IAAI,KAAK;CAExB,IAAI,OAAO,SAAS,GAAG,OAAO;CAE9B,OACE,oBAAC,YAAD;EACE,cAAY,EAAE,mBAAmB,CAAC,GAAG,aAAa;EAClD,WAAU;EAEV,UAAA,oBAAC,gBAAD,EAAA,UACG,OAAO,KAAK,OAAO,UAClB,qBAAC,UAAD,EAAA,UAAA,CACG,QAAQ,KAAK,oBAAC,qBAAD,CAAsB,CAAA,GACpC,oBAAC,gBAAD,EAAA,UACG,UAAU,OAAO,SAAS,IACzB,oBAAC,gBAAD,EAAA,UAAiB,MAAM,MAAsB,CAAA,IAE7C,oBAAC,gBAAD;GAAgB,MAAM,MAAM;GAAM,UAAA,MAAM;EAAsB,CAAA,EAElD,CAAA,CACR,EAAA,GATK,MAAM,GASX,CACX,EACa,CAAA;CACN,CAAA;AAEhB;AAEA,SAAS,gBAAgB;CACvB,MAAM,EAAE,MAAM,OAAO;CAErB,OACE,qBAAC,OAAD;EACE,MAAK;EACL,aAAU;EACV,WAAU;EAHZ,UAAA,CAKE,oBAAC,SAAD;GAAS,WAAU;GAAkB,eAAY;EAAQ,CAAA,GACzD,qBAAC,QAAD,EAAA,UAAA;GACE,oBAAC,UAAD;IAAQ,WAAU;IAAiB,UAAA,EAAE,eAAe,CAAC,GAAG,SAAS;GAAU,CAAA;GAAC;GAAG;GAC9E,EAAE,sBAAsB,CAAC,GAAG,4DAA4D;EACrF,EAAA,CAAA,CACH;;AAET;;AAGA,SAAS,cAAc,EAAE,KAAK,YAAuD;CACnF,MAAM,WAAW,oBAAoB;CACrC,MAAM,EAAE,MAAM,OAAO;CACrB,MAAM,SAAuB,UAAU,QAAQ,cAAc,KAAK;CAClE,MAAM,SAAuB,UAAU,QAAQ,cAAc,KAAK;CAElE,OACE,qBAAC,OAAD;EACE,KAAK,IAAI,KAAK;EACd,MAAM,IAAI,KAAK;EACf,aAAU;EACV,eAAa,IAAI,KAAK,UAAU,KAAA;EAChC,WAAU;EALZ,UAAA;GAOE,oBAAC,KAAD;IACE,MAAK;IACL,WAAU;IAET,UAAA,EAAE,uBAAuB,CAAC,GAAG,iBAAiB;GAC9C,CAAA;GACF,IAAI,WAAW,oBAAC,eAAD,CAAgB,CAAA;GAC/B,cAAc,QAAQ,EAAE,IAAI,CAAC;GAC9B,qBAAC,QAAD;IACE,IAAG;IACH,UAAU;IACV,WAAW,GAAG,qBAAqB;IACnC,aAAU;IAJZ,UAAA,CAME,oBAAC,gBAAD,EAAqB,IAAM,CAAA,GAC1B,aACE,IAAI,KAAK,OAAO,SAAS,IACxB,oBAAC,WAAD,EAAW,QAAQ,IAAI,KAAK,OAAS,CAAA,IAErC,oBAAC,KAAD;KAAG,WAAU;KACV,UAAA,EAAE,kBAAkB,CAAC,GAAG,+BAA+B;IACvD,CAAA,EAEH;;GACL,cAAc,QAAQ,EAAE,IAAI,CAAC;GAC7B,IAAI,aACH,oBAAC,OAAD;IAAK,aAAU;IAAiB,yBAAyB,EAAE,QAAQ,IAAI,UAAU;GAAI,CAAA;GAEtF,IAAI,SAAS,oBAAC,aAAD,EAAa,OAAO,IAAI,MAAQ,CAAA;EAC3C;;AAET;;;;;;;;;;;;AAaA,SAAgB,QAAQ,EACtB,KAAK,SACL,cACwE;CACxE,MAAM,OAAO,aAAa;CAC1B,MAAM,MAAM,WAAW,WAAW,IAAI;CAEtC,MAAM,MAAM,KAAK,KAAK;CACtB,MAAM,YAAY,KAAK,KAAK;CAE5B,gBAAgB;EACd,IAAI,CAAC,OAAO,CAAC,WAAW;EAExB,SAAS,gBAAgB,OAAO;EAChC,SAAS,gBAAgB,MAAM;CACjC,GAAG,CAAC,KAAK,SAAS,CAAC;CAEnB,IAAI,CAAC,KAAK,OAAO;CAEjB,OACE,qBAAC,aAAD;EAAa,SAAS;EAAiB;EAAvC,UAAA,CACE,oBAAC,SAAD,EAAS,KAAK,IAAI,KAAK,IAAM,CAAA,GAC7B,oBAAC,eAAD,EAAoB,IAAM,CAAA,CACf;;AAEjB;AAEA,SAAS,eAAwB;CAC/B,IAAI;EACF,OAAO,QAAQ,CAAC,CAAC;CACnB,QAAQ;EACN,OAAO;CACT;AACF"}
|
|
1
|
+
{"version":3,"file":"page.js","names":[],"sources":["../../../src/components/cms/page.tsx"],"sourcesContent":["import {\n CMS_FOOTER_KEY,\n CMS_HEADER_KEY,\n cmsDesignCss,\n cmsPayload,\n type CmsPageProps,\n type CmsPayload,\n} from '@alfakit/core'\nimport { usePage } from '@inertiajs/react'\nimport { EyeIcon } from 'lucide-react'\nimport { Fragment, createElement, useEffect, type ReactNode } from 'react'\nimport { useOptionalRegistry, type AnyComponent } from '../../context'\nimport { lazyComponent } from '../../lib/lazy'\nimport { cn } from '../../lib/utils'\nimport {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n BreadcrumbSeparator,\n} from '../ui/breadcrumb'\nimport { CmsBlocks } from './blocks'\nimport { CmsProvider, useCms } from './context'\nimport { CmsHead } from './head'\nimport { CmsLanguageSwitcher, CmsMenuList, CmsNav } from './menu'\n\n/** The admin bar loads only for signed-in editors (the server sends `cms.admin` to them alone). */\nconst CmsAdminBar = lazyComponent<{ admin: NonNullable<CmsPayload['admin']> }>(\n () => import('./admin-bar').then((module) => module.CmsAdminBar),\n () => null,\n)\n\n/** The site header: the site name, the `main` menu and the language links. */\nexport function CmsHeader({ cms }: { cms: CmsPayload }) {\n return (\n <header\n data-slot=\"cms-header\"\n className=\"bg-background/90 supports-[backdrop-filter]:bg-background/70 sticky top-0 z-30 border-b backdrop-blur\"\n >\n <div className=\"relative mx-auto flex h-16 max-w-6xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8\">\n <a\n href={cms.site.url}\n className=\"focus-visible:ring-ring/50 rounded-md text-lg font-semibold tracking-tight outline-none focus-visible:ring-[3px]\"\n >\n {cms.site.design?.logo ? (\n <img\n src={cms.site.design.logo.url}\n alt={cms.site.design.logo.alt}\n data-slot=\"cms-logo\"\n className=\"h-9 w-auto max-w-48 object-contain\"\n />\n ) : (\n cms.site.name\n )}\n </a>\n <div className=\"flex items-center gap-2\">\n <CmsNav menu={cms.menus.main}>\n <CmsLanguageSwitcher site={cms.site} className=\"hidden sm:block\" />\n </CmsNav>\n </div>\n </div>\n </header>\n )\n}\n\n/** The site footer: the `footer` menu and the site name. */\nexport function CmsFooter({ cms }: { cms: CmsPayload }) {\n const { t } = useCms()\n const footer = cms.menus.footer\n\n return (\n <footer data-slot=\"cms-footer\" className=\"border-t\">\n <div className=\"mx-auto flex max-w-6xl flex-col gap-6 px-4 py-10 sm:flex-row sm:items-start sm:justify-between sm:px-6 lg:px-8\">\n <p className=\"font-semibold\">{cms.site.name}</p>\n {footer && footer.items.length > 0 && (\n <nav aria-label={t('cms.footer_navigation', {}, 'Footer navigation')}>\n <CmsMenuList\n items={footer.items}\n className=\"flex flex-col gap-1 sm:flex-row sm:flex-wrap sm:gap-x-4\"\n />\n </nav>\n )}\n <CmsLanguageSwitcher site={cms.site} className=\"sm:hidden\" />\n </div>\n </footer>\n )\n}\n\nfunction CmsBreadcrumbs({ cms }: { cms: CmsPayload }) {\n const { t } = useCms()\n const crumbs = cms.page.breadcrumbs\n\n if (crumbs.length < 2) return null\n\n return (\n <Breadcrumb\n aria-label={t('cms.breadcrumbs', {}, 'Breadcrumbs')}\n className=\"mx-auto max-w-6xl px-4 pt-6 sm:px-6 lg:px-8\"\n >\n <BreadcrumbList>\n {crumbs.map((crumb, index) => (\n <Fragment key={crumb.url}>\n {index > 0 && <BreadcrumbSeparator />}\n <BreadcrumbItem>\n {index === crumbs.length - 1 ? (\n <BreadcrumbPage>{crumb.label}</BreadcrumbPage>\n ) : (\n <BreadcrumbLink href={crumb.url}>{crumb.label}</BreadcrumbLink>\n )}\n </BreadcrumbItem>\n </Fragment>\n ))}\n </BreadcrumbList>\n </Breadcrumb>\n )\n}\n\nfunction PreviewBanner() {\n const { t } = useCms()\n\n return (\n <div\n role=\"status\"\n data-slot=\"cms-preview-banner\"\n className=\"flex items-center justify-center gap-2 bg-amber-100 px-4 py-2 text-sm text-amber-950 dark:bg-amber-950 dark:text-amber-100\"\n >\n <EyeIcon className=\"size-4 shrink-0\" aria-hidden=\"true\" />\n <span>\n <strong className=\"font-semibold\">{t('cms.preview', {}, 'Preview')}</strong> ·{' '}\n {t('cms.preview_notice', {}, \"This is a preview. Visitors don't see unpublished changes.\")}\n </span>\n </div>\n )\n}\n\n/** The page body inside `CmsPage`: header, breadcrumbs, blocks and footer. */\nfunction CmsPageLayout({ cms, children }: { cms: CmsPayload; children?: ReactNode }) {\n const registry = useOptionalRegistry()\n const { t } = useCms()\n const header: AnyComponent = registry?.resolve(CMS_HEADER_KEY) ?? CmsHeader\n const footer: AnyComponent = registry?.resolve(CMS_FOOTER_KEY) ?? CmsFooter\n const designCss = cmsDesignCss(cms.site.design)\n\n return (\n <div\n dir={cms.site.direction}\n lang={cms.site.tag}\n data-slot=\"cms-page\"\n data-layout={cms.page.layout ?? undefined}\n className=\"bg-background text-foreground flex min-h-svh flex-col\"\n >\n <a\n href=\"#cms-content\"\n className=\"bg-background focus-visible:ring-ring/50 sr-only z-50 rounded-md px-4 py-2 focus:not-sr-only focus:absolute focus:start-4 focus:top-4 focus-visible:ring-[3px]\"\n >\n {t('cms.skip_to_content', {}, 'Skip to content')}\n </a>\n {/* Site design: built on the server from checked values, never contains `<`. */}\n {designCss && (\n <style data-slot=\"cms-design\" dangerouslySetInnerHTML={{ __html: designCss }} />\n )}\n {cms.preview && <PreviewBanner />}\n {createElement(header, { cms })}\n <main\n id=\"cms-content\"\n tabIndex={-1}\n className={cn('flex-1 outline-none')}\n data-slot=\"cms-content\"\n >\n <CmsBreadcrumbs cms={cms} />\n {children ??\n (cms.page.blocks.length > 0 ? (\n <CmsBlocks blocks={cms.page.blocks} />\n ) : (\n <p className=\"text-muted-foreground mx-auto max-w-3xl px-4 py-24 text-center\">\n {t('cms.empty_page', {}, 'This page has no content yet.')}\n </p>\n ))}\n </main>\n {createElement(footer, { cms })}\n {cms.poweredBy && (\n <div data-slot=\"cms-powered-by\" dangerouslySetInnerHTML={{ __html: cms.poweredBy }} />\n )}\n {cms.admin && <CmsAdminBar admin={cms.admin} />}\n </div>\n )\n}\n\n/**\n * The `Alfakit/CmsPage` Inertia page (public CMS pages and previews). Register it in your\n * public site's Inertia app:\n *\n * ```tsx\n * createAlfakit({ pages: { 'Alfakit/CmsPage': CmsPage } })\n * ```\n *\n * Replace the header or footer with `cms:header` / `cms:footer` registry components, a\n * block with `cms:block:{type}`, or build your own page from `CmsBlocks` and `CmsHead`.\n */\nexport function CmsPage({\n cms: cmsProp,\n components,\n}: Partial<CmsPageProps> & { components?: Record<string, AnyComponent> }) {\n const page = usePageProps()\n const cms = cmsProp ?? cmsPayload(page)\n\n const tag = cms?.site.tag\n const direction = cms?.site.direction\n\n useEffect(() => {\n if (!tag || !direction) return\n\n document.documentElement.lang = tag\n document.documentElement.dir = direction\n }, [tag, direction])\n\n if (!cms) return null\n\n return (\n <CmsProvider payload={cms} components={components}>\n <CmsHead seo={cms.page.seo} design={cms.site.design} />\n <CmsPageLayout cms={cms} />\n </CmsProvider>\n )\n}\n\nfunction usePageProps(): unknown {\n try {\n return usePage().props\n } catch {\n return null\n }\n}\n"],"mappings":"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"}
|
|
@@ -64,7 +64,7 @@ function FileUploadField({ component }) {
|
|
|
64
64
|
if (!file) continue;
|
|
65
65
|
const local = pendingFile(file, position);
|
|
66
66
|
setUploading((state) => [...state, local]);
|
|
67
|
-
queued.push(limit(() => send(endpoint, file, local)));
|
|
67
|
+
queued.push(store.trackUpload(limit(() => send(endpoint, file, local))));
|
|
68
68
|
}
|
|
69
69
|
await Promise.all(queued);
|
|
70
70
|
};
|
|
@@ -134,7 +134,7 @@ function FileUploadField({ component }) {
|
|
|
134
134
|
if (!file) return;
|
|
135
135
|
const local = pendingFile(file, `edit:${existing.path}`);
|
|
136
136
|
setUploading((state) => [...state, local]);
|
|
137
|
-
await send(endpoint, file, local, existing.path);
|
|
137
|
+
await store.trackUpload(send(endpoint, file, local, existing.path));
|
|
138
138
|
};
|
|
139
139
|
const send = async (endpoint, file, local, replacing) => {
|
|
140
140
|
try {
|
|
@@ -351,10 +351,10 @@ function FileUploadField({ component }) {
|
|
|
351
351
|
children: [
|
|
352
352
|
/* @__PURE__ */ jsx(UploadCloudIcon, { className: "size-5" }),
|
|
353
353
|
!circle && /* @__PURE__ */ jsx("span", { children: component.placeholder ?? t("form.drop_files", {}, "Drop files here or click to browse") }),
|
|
354
|
-
!circle && (component.
|
|
354
|
+
!circle && (component.acceptedFileTypesLabel || component.maxSize || component.minFiles) && /* @__PURE__ */ jsx("span", {
|
|
355
355
|
className: "text-muted-foreground text-xs",
|
|
356
356
|
children: [
|
|
357
|
-
component.
|
|
357
|
+
component.acceptedFileTypesLabel,
|
|
358
358
|
component.maxSize ? `≤ ${fileManagerSize(component.maxSize * 1024, formatter)}` : null,
|
|
359
359
|
component.minFiles ? choice("form.min_files", component.minFiles, {}, "At least :count file|At least :count files") : null
|
|
360
360
|
].filter(Boolean).join(" · ")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-upload.js","names":[],"sources":["../../../src/components/fields/file-upload.tsx"],"sourcesContent":["import type { FileUploadSchema, UploadedFile } from '@alfakit/core'\nimport {\n canEditImage,\n concurrencyLimiter,\n cssAspectRatio,\n editableImageName,\n fetchEditableImage,\n fieldMemory,\n fileManagerSize,\n filesFromClipboard,\n fileUploadAccept,\n fileUploadEntries,\n fileUploadPaths,\n insertUploadedFile,\n moveRepeaterItem,\n panelUploadOptions,\n requestJson,\n transformImage,\n uploadedFileLinks,\n uploadTemporaryFile,\n} from '@alfakit/core'\nimport {\n ChevronDownIcon,\n ChevronUpIcon,\n DownloadIcon,\n ExternalLinkIcon,\n FileIcon,\n PencilIcon,\n UploadCloudIcon,\n XIcon,\n} from 'lucide-react'\nimport { useRef, useState, type ClipboardEvent, type DragEvent } from 'react'\nimport { useChoice, useFormatter, usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { cn } from '../../lib/utils'\nimport { useField, useFormStore } from '../form/form-context'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { Button } from '../ui/button'\nimport { Spinner } from '../ui/spinner'\nimport type { ImageEditRequest } from './image-editor-dialog'\nimport { ImageEditorDialog } from './image-editor-dialog-lazy'\n\ninterface LocalFile extends UploadedFile {\n progress?: number\n error?: string\n preview?: string\n}\n\nexport function FileUploadField({ component }: { component: FileUploadSchema }) {\n const t = useT()\n const formatter = useFormatter()\n const choice = useChoice()\n const panel = usePanel()\n const { value, setValue, error, id, disabled, readOnly, aria } = useField<\n string | string[] | null\n >(component)\n // Read-only keeps open/download links but hides upload and remove controls.\n const locked = disabled || readOnly\n const store = useFormStore()\n // In-flight uploads and the metadata of files uploaded in this session (the server schema\n // only knows about saved files) live in the form's field memory, not in component state:\n // a virtualised Repeater item unmounts when scrolled out of view and finds them again,\n // local image previews included, when it mounts back.\n const uploadingStore = fieldMemory<LocalFile[]>(\n store,\n `${component.statePath}:uploading`,\n () => [],\n )\n const uploadedStore = fieldMemory<FileUploadSchema['files']>(\n store,\n `${component.statePath}:uploaded`,\n () => [],\n )\n const uploading = useStore(uploadingStore)\n const uploaded = useStore(uploadedStore)\n const setUploading = uploadingStore.set\n const setUploaded = uploadedStore.set\n const [dragging, setDragging] = useState(false)\n // Previews whose image failed to load (missing or unreachable file) show the file icon.\n const [brokenPreviews, setBrokenPreviews] = useState<ReadonlySet<string>>(() => new Set())\n const inputRef = useRef<HTMLInputElement>(null)\n const [dragPath, setDragPath] = useState<string | null>(null)\n // The image open in the editor (`imageEditor()`), one at a time.\n const [editing, setEditing] = useState<ImageEditRequest | null>(null)\n const editImage = (file: File) =>\n new Promise<File | null>((resolve) =>\n setEditing({\n file,\n resolve: (edited) => {\n setEditing(null)\n resolve(edited)\n },\n }),\n )\n // Object URLs made for local image previews; released when the file is removed.\n const objectUrls = fieldMemory<Set<string>>(\n store,\n `${component.statePath}:previews`,\n () => new Set(),\n ).get()\n\n const releasePreview = (url: string | undefined) => {\n if (url && objectUrls.delete(url)) URL.revokeObjectURL(url)\n }\n\n const paths = fileUploadPaths(value)\n const files: LocalFile[] = fileUploadEntries(paths, component.files, uploaded)\n const maxFiles = component.multiple ? (component.maxFiles ?? Infinity) : 1\n const canAddMore = files.length + uploading.length < maxFiles\n\n const commit = (next: string[]) => setValue(component.multiple ? next : (next[0] ?? null))\n\n const upload = async (list: FileList | File[]) => {\n const endpoint = panel.endpoints.upload\n\n if (!endpoint) {\n return\n }\n\n const accepted = Array.from(list).slice(\n 0,\n Math.max(0, maxFiles - files.length - uploading.length),\n )\n\n // `maxParallelUploads()`: files are prepared (edited, resized) one by one and then\n // queued, so at most that many upload at once.\n const limit = concurrencyLimiter(component.maxParallelUploads ?? 1)\n const queued: Promise<void>[] = []\n\n for (const [position, picked] of accepted.entries()) {\n let original = picked\n const edit = Boolean(component.imageEditor && canEditImage(picked))\n\n if (edit) {\n const edited = await editImage(picked)\n\n // Cancelled: the file is not uploaded.\n if (!edited) continue\n\n original = edited\n }\n\n const file = await prepare(original, edit)\n\n if (!file) continue\n\n const local = pendingFile(file, position)\n setUploading((state) => [...state, local])\n\n queued.push(limit(() => send(endpoint, file, local)))\n }\n\n await Promise.all(queued)\n }\n\n /** Resize and orient a picked (or edited) file, and check its size. Null when rejected. */\n const prepare = async (original: File, edited: boolean): Promise<File | null> => {\n const file =\n component.imageCropAspectRatio ||\n component.imageResizeTargetWidth ||\n component.imageResizeTargetHeight ||\n component.orientImagesFromExif\n ? await transformImage(original, {\n // The editor already cropped the image.\n aspectRatio: edited ? null : component.imageCropAspectRatio,\n mode: component.imageResizeMode,\n width: component.imageResizeTargetWidth,\n height: component.imageResizeTargetHeight,\n orient: component.orientImagesFromExif,\n })\n : original\n\n if (component.minSize && file.size < component.minSize * 1024) {\n setUploading((state) => [\n ...state,\n {\n path: file.name,\n name: file.name,\n size: file.size,\n error: t('form.file_too_small', {}, 'This file is too small.'),\n },\n ])\n\n return null\n }\n\n if (component.maxSize && file.size > component.maxSize * 1024) {\n setUploading((state) => [\n ...state,\n {\n path: file.name,\n name: file.name,\n size: file.size,\n error: t('form.file_too_large', {}, 'This file is too large.'),\n },\n ])\n\n return null\n }\n\n return file\n }\n\n /** The in-progress entry shown while `file` uploads. */\n const pendingFile = (file: File, position: number | string): LocalFile => {\n const local: LocalFile = {\n // Unique within the batch and across re-picks of a changed file.\n path: `pending:${position}:${file.lastModified}:${file.size}:${file.name}`,\n name: file.name,\n size: file.size,\n progress: 0,\n preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined,\n }\n if (local.preview) objectUrls.add(local.preview)\n\n return local\n }\n\n /**\n * The \"Edit\" button on an uploaded image: open it in the editor and upload the result\n * in its place (the old file goes like any removed one: a temporary upload is deleted\n * now, a stored file when the form is saved with `deleteRemovedFiles()`).\n */\n const editExisting = async (existing: LocalFile) => {\n const endpoint = panel.endpoints.upload\n const url = existing.url ?? existing.preview\n const name = editableImageName(existing)\n\n if (!endpoint || !url || !name) return\n\n const source = await fetchEditableImage(url, name)\n\n if (!source) {\n setUploading((state) => [\n ...state,\n {\n path: `failed:${existing.path}`,\n name: existing.name,\n error: t('image_editor.load_failed', {}, 'The image could not be opened.'),\n },\n ])\n\n return\n }\n\n const edited = await editImage(source)\n\n // Cancelled, or the editor couldn't decode it: keep the file as it is.\n if (!edited || edited === source) return\n\n const file = await prepare(edited, true)\n\n if (!file) return\n\n const local = pendingFile(file, `edit:${existing.path}`)\n setUploading((state) => [...state, local])\n await send(endpoint, file, local, existing.path)\n }\n\n const send = async (endpoint: string, file: File, local: LocalFile, replacing?: string) => {\n try {\n // Large files go in resumable chunks; progress covers the whole file.\n const response = await uploadTemporaryFile(endpoint, file, {\n ...panelUploadOptions(panel, component.uploadPolicy),\n onProgress: (progress) =>\n setUploading((state) =>\n state.map((item) => (item.path === local.path ? { ...item, progress } : item)),\n ),\n })\n\n setUploaded((state) => [\n ...state,\n {\n path: response.key,\n name: response.name,\n size: response.size,\n temporary: true,\n url: local.preview,\n },\n ])\n setUploading((state) => state.filter((item) => item.path !== local.path))\n // Read the store, not `value`: it's stale after the first file of a batch.\n const current = fileUploadPaths(\n store.getValue<string | string[] | null>(component.statePath, null),\n )\n\n if (replacing !== undefined && current.includes(replacing)) {\n // An edited image takes the old one's place.\n commit(current.map((path) => (path === replacing ? response.key : path)))\n discard(replacing)\n } else {\n commit(insertUploadedFile(current, response.key, component, maxFiles))\n }\n } catch (uploadError) {\n const message =\n (uploadError as { errors?: Record<string, string[]> }).errors?.file?.[0] ??\n (uploadError as Error).message\n setUploading((state) =>\n state.map((item) =>\n item.path === local.path ? { ...item, error: message, progress: undefined } : item,\n ),\n )\n }\n }\n\n const remove = (path: string) => {\n commit(paths.filter((item) => item !== path))\n discard(path)\n }\n\n /** Release a file that left the field: its preview, and a temporary upload's file. */\n const discard = (path: string) => {\n releasePreview(uploadedStore.get().find((file) => file.path === path)?.url)\n\n if (path.startsWith('tmp:') && panel.endpoints.uploadDestroy) {\n void requestJson(panel.endpoints.uploadDestroy, {\n method: 'DELETE',\n body: { key: path },\n }).catch(() => undefined)\n }\n }\n\n const onDrop = (event: DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n setDragging(false)\n\n if (!locked && event.dataTransfer.files.length > 0) {\n void upload(event.dataTransfer.files)\n }\n }\n\n // `pasteable()`: images and files pasted while the drop zone has focus.\n const onPaste = (event: ClipboardEvent<HTMLDivElement>) => {\n if (locked || component.pasteable === false) return\n\n const pasted = filesFromClipboard(event.clipboardData)\n\n if (pasted.length > 0) {\n event.preventDefault()\n void upload(pasted)\n }\n }\n\n const move = (from: number, to: number) => commit(moveRepeaterItem(paths, from, to) as string[])\n const reorderable = Boolean(component.reorderable && component.multiple && !locked)\n\n // `previewable(false)` shows every file as a name and icon, without the image.\n const isImage = (file: LocalFile) =>\n component.previewable !== false &&\n (component.image || /\\.(png|jpe?g|gif|webp|svg|avif)$/i.test(file.name))\n const circle = component.panelLayout === 'circle' || component.avatar\n const integrated = component.panelLayout === 'integrated' && !circle\n const removeAtStart = component.positions?.removeButton === 'start'\n const progressAtStart = component.positions?.progressIndicator === 'start'\n const uploadAlign = component.positions?.uploadButton ?? 'center'\n\n // `imageEditor()`: uploaded JPEG, PNG and WebP images can be edited again.\n const canEditExisting = (file: LocalFile, index: number) =>\n Boolean(component.imageEditor) &&\n !locked &&\n index < files.length &&\n !file.missing &&\n Boolean(file.url ?? file.preview) &&\n editableImageName(file) !== null\n\n const renderRemove = (file: LocalFile) =>\n component.deletable &&\n !locked && (\n <Button\n type=\"button\"\n variant={circle ? 'secondary' : 'ghost'}\n size=\"icon-xs\"\n className={cn(circle && 'rounded-full shadow')}\n aria-label={t('file_upload.remove', { name: file.name }, 'Remove :name')}\n onClick={() => {\n if (file.error || file.progress !== undefined) {\n releasePreview(file.preview)\n setUploading((state) => state.filter((item) => item.path !== file.path))\n } else {\n remove(file.path)\n }\n }}\n >\n <XIcon />\n </Button>\n )\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div\n data-slot=\"file-upload\"\n data-layout={component.panelLayout}\n className={cn('flex flex-col gap-3', integrated && 'rounded-md border p-2')}\n >\n {(files.length > 0 || uploading.length > 0) && (\n <ul\n className={cn(\n 'grid gap-2',\n component.panelLayout === 'grid' && 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4',\n circle && 'justify-items-center',\n )}\n >\n {[...files, ...uploading].map((file, index) => (\n <li\n key={file.path}\n draggable={reorderable && index < files.length}\n onDragStart={() => setDragPath(file.path)}\n onDragOver={(event) => reorderable && event.preventDefault()}\n onDrop={(event) => {\n if (!reorderable || dragPath === null) return\n event.preventDefault()\n event.stopPropagation()\n const from = paths.indexOf(dragPath)\n if (from !== -1 && index < files.length) move(from, index)\n setDragPath(null)\n }}\n onDragEnd={() => setDragPath(null)}\n className={cn(\n dragPath === file.path && 'opacity-50',\n 'bg-muted/30 relative flex min-w-0 items-center gap-3 overflow-hidden rounded-md border p-2 text-sm',\n circle && 'size-28 justify-center rounded-full border-0 p-0',\n file.error && 'border-destructive',\n )}\n >\n {!circle && removeAtStart && renderRemove(file)}\n {!circle && progressAtStart && file.progress !== undefined && !file.error && (\n <Spinner />\n )}\n {isImage(file) && (file.url || file.preview) && !brokenPreviews.has(file.path) ? (\n <img\n src={file.url ?? file.preview}\n alt=\"\"\n onError={() => setBrokenPreviews((current) => new Set(current).add(file.path))}\n className={cn(\n 'size-12 shrink-0 rounded object-cover',\n circle && 'size-28 rounded-full',\n )}\n style={\n !circle && component.imagePreviewHeight\n ? {\n height: cssLength(component.imagePreviewHeight),\n width: 'auto',\n maxWidth: '50%',\n }\n : undefined\n }\n />\n ) : (\n !circle && <FileIcon className=\"text-muted-foreground size-6 shrink-0\" />\n )}\n {!circle && (\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-medium\">{file.name}</p>\n <p\n className={cn(\n 'text-muted-foreground text-xs',\n file.missing && 'text-destructive',\n )}\n >\n {file.error ??\n (file.missing\n ? t('form.file_missing', {}, 'This file no longer exists.')\n : file.progress !== undefined && !file.error\n ? (component.uploadingMessage ?? t('form.uploading', {}, 'Uploading…'))\n : file.size\n ? fileManagerSize(file.size, formatter)\n : file.temporary\n ? t('form.uploading', {}, 'Pending')\n : '')}\n </p>\n </div>\n )}\n <div\n className={cn('flex items-center gap-1', circle && 'absolute -end-1 -bottom-1')}\n >\n {(circle || !progressAtStart) && file.progress !== undefined && !file.error && (\n <Spinner />\n )}\n {reorderable && index < files.length && (\n <>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={t('file_upload.move_up', { name: file.name }, 'Move :name up')}\n disabled={index === 0}\n onClick={() => move(index, index - 1)}\n >\n <ChevronUpIcon />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={t(\n 'file_upload.move_down',\n { name: file.name },\n 'Move :name down',\n )}\n disabled={index === files.length - 1}\n onClick={() => move(index, index + 1)}\n >\n <ChevronDownIcon />\n </Button>\n </>\n )}\n {canEditExisting(file, index) && (\n <Button\n type=\"button\"\n variant={circle ? 'secondary' : 'ghost'}\n size=\"icon-xs\"\n className={cn(circle && 'rounded-full shadow')}\n aria-label={t('file_upload.edit', { name: file.name }, 'Edit :name')}\n title={t('image_editor.title', {}, 'Edit image')}\n data-action=\"edit-image\"\n onClick={() => void editExisting(file)}\n >\n <PencilIcon />\n </Button>\n )}\n {component.openable && uploadedFileLinks(file).open && (\n <Button asChild type=\"button\" variant=\"ghost\" size=\"icon-xs\">\n <a\n href={uploadedFileLinks(file).open}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label={`${t('file_upload.open', { name: file.name }, 'Open :name')} ${t('opens_in_new_tab', {}, '(opens in a new tab)')}`}\n >\n <ExternalLinkIcon />\n </a>\n </Button>\n )}\n {component.downloadable && uploadedFileLinks(file).download && (\n <Button asChild type=\"button\" variant=\"ghost\" size=\"icon-xs\">\n <a\n href={uploadedFileLinks(file).download}\n download\n aria-label={t(\n 'file_upload.download',\n { name: file.name },\n 'Download :name',\n )}\n >\n <DownloadIcon />\n </a>\n </Button>\n )}\n {(circle || !removeAtStart) && renderRemove(file)}\n </div>\n </li>\n ))}\n </ul>\n )}\n {canAddMore && !locked && (\n <div\n role=\"button\"\n tabIndex={0}\n onClick={() => inputRef.current?.click()}\n aria-describedby={aria['aria-describedby']}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n inputRef.current?.click()\n }\n }}\n onDragOver={(event) => {\n event.preventDefault()\n setDragging(true)\n }}\n onDragLeave={() => setDragging(false)}\n onDrop={onDrop}\n onPaste={onPaste}\n style={\n !circle && component.panelAspectRatio\n ? { aspectRatio: cssAspectRatio(component.panelAspectRatio) }\n : undefined\n }\n data-align={uploadAlign}\n className={cn(\n 'text-muted-foreground hover:border-ring flex cursor-pointer flex-col justify-center gap-1 rounded-md border border-dashed px-4 py-6 text-sm transition-colors',\n uploadAlign === 'start'\n ? 'items-start text-start'\n : uploadAlign === 'end'\n ? 'items-end text-end'\n : 'items-center text-center',\n integrated && 'border-transparent',\n dragging && 'border-ring bg-accent',\n circle && 'size-28 items-center rounded-full py-0',\n )}\n >\n <UploadCloudIcon className=\"size-5\" />\n {!circle && (\n <span>\n {component.placeholder ??\n t('form.drop_files', {}, 'Drop files here or click to browse')}\n </span>\n )}\n {!circle &&\n (component.acceptedFileTypes || component.maxSize || component.minFiles) && (\n <span className=\"text-muted-foreground text-xs\">\n {[\n component.acceptedFileTypes?.join(', '),\n component.maxSize\n ? `≤ ${fileManagerSize(component.maxSize * 1024, formatter)}`\n : null,\n component.minFiles\n ? choice(\n 'form.min_files',\n component.minFiles,\n {},\n 'At least :count file|At least :count files',\n )\n : null,\n ]\n .filter(Boolean)\n .join(' · ')}\n </span>\n )}\n </div>\n )}\n <input\n ref={inputRef}\n id={id}\n type=\"file\"\n className=\"sr-only\"\n tabIndex={-1}\n {...aria}\n multiple={component.multiple}\n accept={fileUploadAccept(component)}\n disabled={locked}\n onChange={(event) => {\n // Copy the list: clearing the input empties it while the editor is open.\n const picked = event.target.files ? Array.from(event.target.files) : []\n event.target.value = ''\n void upload(picked)\n }}\n />\n {component.imageEditor && (\n <ImageEditorDialog request={editing} options={component.imageEditor} />\n )}\n </div>\n </FieldWrapper>\n )\n}\n\n/** `imagePreviewHeight()`: numbers are pixels, strings are CSS lengths (`'250px'`, `'10rem'`). */\nfunction cssLength(value: number | string): string {\n return typeof value === 'number' || /^\\d+(\\.\\d+)?$/.test(value) ? `${value}px` : value\n}\n"],"mappings":";;;;;;;;;;;;;AAgDA,SAAgB,gBAAgB,EAAE,aAA8C;CAC9E,MAAM,IAAI,KAAK;CACf,MAAM,YAAY,aAAa;CAC/B,MAAM,SAAS,UAAU;CACzB,MAAM,QAAQ,SAAS;CACvB,MAAM,EAAE,OAAO,UAAU,OAAO,IAAI,UAAU,UAAU,SAAS,SAE/D,SAAS;CAEX,MAAM,SAAS,YAAY;CAC3B,MAAM,QAAQ,aAAa;CAK3B,MAAM,iBAAiB,YACrB,OACA,GAAG,UAAU,UAAU,mBACjB,CAAC,CACT;CACA,MAAM,gBAAgB,YACpB,OACA,GAAG,UAAU,UAAU,kBACjB,CAAC,CACT;CACA,MAAM,YAAY,SAAS,cAAc;CACzC,MAAM,WAAW,SAAS,aAAa;CACvC,MAAM,eAAe,eAAe;CACpC,MAAM,cAAc,cAAc;CAClC,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAE9C,MAAM,CAAC,gBAAgB,qBAAqB,+BAAoC,IAAI,IAAI,CAAC;CACzF,MAAM,WAAW,OAAyB,IAAI;CAC9C,MAAM,CAAC,UAAU,eAAe,SAAwB,IAAI;CAE5D,MAAM,CAAC,SAAS,cAAc,SAAkC,IAAI;CACpE,MAAM,aAAa,SACjB,IAAI,SAAsB,YACxB,WAAW;EACT;EACA,UAAU,WAAW;GACnB,WAAW,IAAI;GACf,QAAQ,MAAM;EAChB;CACF,CAAC,CACH;CAEF,MAAM,aAAa,YACjB,OACA,GAAG,UAAU,UAAU,kCACjB,IAAI,IAAI,CAChB,CAAC,CAAC,IAAI;CAEN,MAAM,kBAAkB,QAA4B;EAClD,IAAI,OAAO,WAAW,OAAO,GAAG,GAAG,IAAI,gBAAgB,GAAG;CAC5D;CAEA,MAAM,QAAQ,gBAAgB,KAAK;CACnC,MAAM,QAAqB,kBAAkB,OAAO,UAAU,OAAO,QAAQ;CAC7E,MAAM,WAAW,UAAU,WAAY,UAAU,YAAY,WAAY;CACzE,MAAM,aAAa,MAAM,SAAS,UAAU,SAAS;CAErD,MAAM,UAAU,SAAmB,SAAS,UAAU,WAAW,OAAQ,KAAK,MAAM,IAAK;CAEzF,MAAM,SAAS,OAAO,SAA4B;EAChD,MAAM,WAAW,MAAM,UAAU;EAEjC,IAAI,CAAC,UACH;EAGF,MAAM,WAAW,MAAM,KAAK,IAAI,CAAC,CAAC,MAChC,GACA,KAAK,IAAI,GAAG,WAAW,MAAM,SAAS,UAAU,MAAM,CACxD;EAIA,MAAM,QAAQ,mBAAmB,UAAU,sBAAsB,CAAC;EAClE,MAAM,SAA0B,CAAC;EAEjC,KAAK,MAAM,CAAC,UAAU,WAAW,SAAS,QAAQ,GAAG;GACnD,IAAI,WAAW;GACf,MAAM,OAAO,QAAQ,UAAU,eAAe,aAAa,MAAM,CAAC;GAElE,IAAI,MAAM;IACR,MAAM,SAAS,MAAM,UAAU,MAAM;IAGrC,IAAI,CAAC,QAAQ;IAEb,WAAW;GACb;GAEA,MAAM,OAAO,MAAM,QAAQ,UAAU,IAAI;GAEzC,IAAI,CAAC,MAAM;GAEX,MAAM,QAAQ,YAAY,MAAM,QAAQ;GACxC,cAAc,UAAU,CAAC,GAAG,OAAO,KAAK,CAAC;GAEzC,OAAO,KAAK,YAAY,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC;EACtD;EAEA,MAAM,QAAQ,IAAI,MAAM;CAC1B;;CAGA,MAAM,UAAU,OAAO,UAAgB,WAA0C;EAC/E,MAAM,OACJ,UAAU,wBACV,UAAU,0BACV,UAAU,2BACV,UAAU,uBACN,MAAM,eAAe,UAAU;GAE7B,aAAa,SAAS,OAAO,UAAU;GACvC,MAAM,UAAU;GAChB,OAAO,UAAU;GACjB,QAAQ,UAAU;GAClB,QAAQ,UAAU;EACpB,CAAC,IACD;EAEN,IAAI,UAAU,WAAW,KAAK,OAAO,UAAU,UAAU,MAAM;GAC7D,cAAc,UAAU,CACtB,GAAG,OACH;IACE,MAAM,KAAK;IACX,MAAM,KAAK;IACX,MAAM,KAAK;IACX,OAAO,EAAE,uBAAuB,CAAC,GAAG,yBAAyB;GAC/D,CACF,CAAC;GAED,OAAO;EACT;EAEA,IAAI,UAAU,WAAW,KAAK,OAAO,UAAU,UAAU,MAAM;GAC7D,cAAc,UAAU,CACtB,GAAG,OACH;IACE,MAAM,KAAK;IACX,MAAM,KAAK;IACX,MAAM,KAAK;IACX,OAAO,EAAE,uBAAuB,CAAC,GAAG,yBAAyB;GAC/D,CACF,CAAC;GAED,OAAO;EACT;EAEA,OAAO;CACT;;CAGA,MAAM,eAAe,MAAY,aAAyC;EACxE,MAAM,QAAmB;GAEvB,MAAM,WAAW,SAAS,GAAG,KAAK,aAAa,GAAG,KAAK,KAAK,GAAG,KAAK;GACpE,MAAM,KAAK;GACX,MAAM,KAAK;GACX,UAAU;GACV,SAAS,KAAK,KAAK,WAAW,QAAQ,IAAI,IAAI,gBAAgB,IAAI,IAAI,KAAA;EACxE;EACA,IAAI,MAAM,SAAS,WAAW,IAAI,MAAM,OAAO;EAE/C,OAAO;CACT;;;;;;CAOA,MAAM,eAAe,OAAO,aAAwB;EAClD,MAAM,WAAW,MAAM,UAAU;EACjC,MAAM,MAAM,SAAS,OAAO,SAAS;EACrC,MAAM,OAAO,kBAAkB,QAAQ;EAEvC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM;EAEhC,MAAM,SAAS,MAAM,mBAAmB,KAAK,IAAI;EAEjD,IAAI,CAAC,QAAQ;GACX,cAAc,UAAU,CACtB,GAAG,OACH;IACE,MAAM,UAAU,SAAS;IACzB,MAAM,SAAS;IACf,OAAO,EAAE,4BAA4B,CAAC,GAAG,gCAAgC;GAC3E,CACF,CAAC;GAED;EACF;EAEA,MAAM,SAAS,MAAM,UAAU,MAAM;EAGrC,IAAI,CAAC,UAAU,WAAW,QAAQ;EAElC,MAAM,OAAO,MAAM,QAAQ,QAAQ,IAAI;EAEvC,IAAI,CAAC,MAAM;EAEX,MAAM,QAAQ,YAAY,MAAM,QAAQ,SAAS,MAAM;EACvD,cAAc,UAAU,CAAC,GAAG,OAAO,KAAK,CAAC;EACzC,MAAM,KAAK,UAAU,MAAM,OAAO,SAAS,IAAI;CACjD;CAEA,MAAM,OAAO,OAAO,UAAkB,MAAY,OAAkB,cAAuB;EACzF,IAAI;GAEF,MAAM,WAAW,MAAM,oBAAoB,UAAU,MAAM;IACzD,GAAG,mBAAmB,OAAO,UAAU,YAAY;IACnD,aAAa,aACX,cAAc,UACZ,MAAM,KAAK,SAAU,KAAK,SAAS,MAAM,OAAO;KAAE,GAAG;KAAM;IAAS,IAAI,IAAK,CAC/E;GACJ,CAAC;GAED,aAAa,UAAU,CACrB,GAAG,OACH;IACE,MAAM,SAAS;IACf,MAAM,SAAS;IACf,MAAM,SAAS;IACf,WAAW;IACX,KAAK,MAAM;GACb,CACF,CAAC;GACD,cAAc,UAAU,MAAM,QAAQ,SAAS,KAAK,SAAS,MAAM,IAAI,CAAC;GAExE,MAAM,UAAU,gBACd,MAAM,SAAmC,UAAU,WAAW,IAAI,CACpE;GAEA,IAAI,cAAc,KAAA,KAAa,QAAQ,SAAS,SAAS,GAAG;IAE1D,OAAO,QAAQ,KAAK,SAAU,SAAS,YAAY,SAAS,MAAM,IAAK,CAAC;IACxE,QAAQ,SAAS;GACnB,OACE,OAAO,mBAAmB,SAAS,SAAS,KAAK,WAAW,QAAQ,CAAC;EAEzE,SAAS,aAAa;GACpB,MAAM,UACH,YAAsD,QAAQ,OAAO,MACrE,YAAsB;GACzB,cAAc,UACZ,MAAM,KAAK,SACT,KAAK,SAAS,MAAM,OAAO;IAAE,GAAG;IAAM,OAAO;IAAS,UAAU,KAAA;GAAU,IAAI,IAChF,CACF;EACF;CACF;CAEA,MAAM,UAAU,SAAiB;EAC/B,OAAO,MAAM,QAAQ,SAAS,SAAS,IAAI,CAAC;EAC5C,QAAQ,IAAI;CACd;;CAGA,MAAM,WAAW,SAAiB;EAChC,eAAe,cAAc,IAAI,CAAC,CAAC,MAAM,SAAS,KAAK,SAAS,IAAI,CAAC,EAAE,GAAG;EAE1E,IAAI,KAAK,WAAW,MAAM,KAAK,MAAM,UAAU,eAC7C,YAAiB,MAAM,UAAU,eAAe;GAC9C,QAAQ;GACR,MAAM,EAAE,KAAK,KAAK;EACpB,CAAC,CAAC,CAAC,YAAY,KAAA,CAAS;CAE5B;CAEA,MAAM,UAAU,UAAqC;EACnD,MAAM,eAAe;EACrB,YAAY,KAAK;EAEjB,IAAI,CAAC,UAAU,MAAM,aAAa,MAAM,SAAS,GAC/C,OAAY,MAAM,aAAa,KAAK;CAExC;CAGA,MAAM,WAAW,UAA0C;EACzD,IAAI,UAAU,UAAU,cAAc,OAAO;EAE7C,MAAM,SAAS,mBAAmB,MAAM,aAAa;EAErD,IAAI,OAAO,SAAS,GAAG;GACrB,MAAM,eAAe;GACrB,OAAY,MAAM;EACpB;CACF;CAEA,MAAM,QAAQ,MAAc,OAAe,OAAO,iBAAiB,OAAO,MAAM,EAAE,CAAa;CAC/F,MAAM,cAAc,QAAQ,UAAU,eAAe,UAAU,YAAY,CAAC,MAAM;CAGlF,MAAM,WAAW,SACf,UAAU,gBAAgB,UACzB,UAAU,SAAS,oCAAoC,KAAK,KAAK,IAAI;CACxE,MAAM,SAAS,UAAU,gBAAgB,YAAY,UAAU;CAC/D,MAAM,aAAa,UAAU,gBAAgB,gBAAgB,CAAC;CAC9D,MAAM,gBAAgB,UAAU,WAAW,iBAAiB;CAC5D,MAAM,kBAAkB,UAAU,WAAW,sBAAsB;CACnE,MAAM,cAAc,UAAU,WAAW,gBAAgB;CAGzD,MAAM,mBAAmB,MAAiB,UACxC,QAAQ,UAAU,WAAW,KAC7B,CAAC,UACD,QAAQ,MAAM,UACd,CAAC,KAAK,WACN,QAAQ,KAAK,OAAO,KAAK,OAAO,KAChC,kBAAkB,IAAI,MAAM;CAE9B,MAAM,gBAAgB,SACpB,UAAU,aACV,CAAC,UACC,oBAAC,QAAD;EACE,MAAK;EACL,SAAS,SAAS,cAAc;EAChC,MAAK;EACL,WAAW,GAAG,UAAU,qBAAqB;EAC7C,cAAY,EAAE,sBAAsB,EAAE,MAAM,KAAK,KAAK,GAAG,cAAc;EACvE,eAAe;GACb,IAAI,KAAK,SAAS,KAAK,aAAa,KAAA,GAAW;IAC7C,eAAe,KAAK,OAAO;IAC3B,cAAc,UAAU,MAAM,QAAQ,SAAS,KAAK,SAAS,KAAK,IAAI,CAAC;GACzE,OACE,OAAO,KAAK,IAAI;EAEpB;EAEA,UAAA,oBAAC,OAAD,CAAQ,CAAA;CACF,CAAA;CAGZ,OACE,oBAAC,cAAD;EAAc,OAAO;EAAW,SAAS;EAAW;EAClD,UAAA,qBAAC,OAAD;GACE,aAAU;GACV,eAAa,UAAU;GACvB,WAAW,GAAG,uBAAuB,cAAc,uBAAuB;GAH5E,UAAA;KAKI,MAAM,SAAS,KAAK,UAAU,SAAS,MACvC,oBAAC,MAAD;KACE,WAAW,GACT,cACA,UAAU,gBAAgB,UAAU,6CACpC,UAAU,sBACZ;KAEC,UAAA,CAAC,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC,KAAK,MAAM,UACnC,qBAAC,MAAD;MAEE,WAAW,eAAe,QAAQ,MAAM;MACxC,mBAAmB,YAAY,KAAK,IAAI;MACxC,aAAa,UAAU,eAAe,MAAM,eAAe;MAC3D,SAAS,UAAU;OACjB,IAAI,CAAC,eAAe,aAAa,MAAM;OACvC,MAAM,eAAe;OACrB,MAAM,gBAAgB;OACtB,MAAM,OAAO,MAAM,QAAQ,QAAQ;OACnC,IAAI,SAAS,MAAM,QAAQ,MAAM,QAAQ,KAAK,MAAM,KAAK;OACzD,YAAY,IAAI;MAClB;MACA,iBAAiB,YAAY,IAAI;MACjC,WAAW,GACT,aAAa,KAAK,QAAQ,cAC1B,sGACA,UAAU,oDACV,KAAK,SAAS,oBAChB;MAnBF,UAAA;OAqBG,CAAC,UAAU,iBAAiB,aAAa,IAAI;OAC7C,CAAC,UAAU,mBAAmB,KAAK,aAAa,KAAA,KAAa,CAAC,KAAK,SAClE,oBAAC,SAAD,CAAU,CAAA;OAEX,QAAQ,IAAI,MAAM,KAAK,OAAO,KAAK,YAAY,CAAC,eAAe,IAAI,KAAK,IAAI,IAC3E,oBAAC,OAAD;QACE,KAAK,KAAK,OAAO,KAAK;QACtB,KAAI;QACJ,eAAe,mBAAmB,YAAY,IAAI,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC;QAC7E,WAAW,GACT,yCACA,UAAU,sBACZ;QACA,OACE,CAAC,UAAU,UAAU,qBACjB;SACE,QAAQ,UAAU,UAAU,kBAAkB;SAC9C,OAAO;SACP,UAAU;QACZ,IACA,KAAA;OAEP,CAAA,IAED,CAAC,UAAU,oBAAC,UAAD,EAAU,WAAU,wCAAyC,CAAA;OAEzE,CAAC,UACA,qBAAC,OAAD;QAAK,WAAU;QAAf,UAAA,CACE,oBAAC,KAAD;SAAG,WAAU;SAAwB,UAAA,KAAK;QAAQ,CAAA,GAClD,oBAAC,KAAD;SACE,WAAW,GACT,iCACA,KAAK,WAAW,kBAClB;SAEC,UAAA,KAAK,UACH,KAAK,UACF,EAAE,qBAAqB,CAAC,GAAG,6BAA6B,IACxD,KAAK,aAAa,KAAA,KAAa,CAAC,KAAK,QAClC,UAAU,oBAAoB,EAAE,kBAAkB,CAAC,GAAG,YAAY,IACnE,KAAK,OACH,gBAAgB,KAAK,MAAM,SAAS,IACpC,KAAK,YACH,EAAE,kBAAkB,CAAC,GAAG,SAAS,IACjC;QACX,CAAA,CACA;;OAEP,qBAAC,OAAD;QACE,WAAW,GAAG,2BAA2B,UAAU,2BAA2B;QADhF,UAAA;UAGI,UAAU,CAAC,oBAAoB,KAAK,aAAa,KAAA,KAAa,CAAC,KAAK,SACpE,oBAAC,SAAD,CAAU,CAAA;SAEX,eAAe,QAAQ,MAAM,UAC5B,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,QAAD;UACE,MAAK;UACL,SAAQ;UACR,MAAK;UACL,cAAY,EAAE,uBAAuB,EAAE,MAAM,KAAK,KAAK,GAAG,eAAe;UACzE,UAAU,UAAU;UACpB,eAAe,KAAK,OAAO,QAAQ,CAAC;UAEpC,UAAA,oBAAC,eAAD,CAAgB,CAAA;SACV,CAAA,GACR,oBAAC,QAAD;UACE,MAAK;UACL,SAAQ;UACR,MAAK;UACL,cAAY,EACV,yBACA,EAAE,MAAM,KAAK,KAAK,GAClB,iBACF;UACA,UAAU,UAAU,MAAM,SAAS;UACnC,eAAe,KAAK,OAAO,QAAQ,CAAC;UAEpC,UAAA,oBAAC,iBAAD,CAAkB,CAAA;SACZ,CAAA,CACR,EAAA,CAAA;SAEH,gBAAgB,MAAM,KAAK,KAC1B,oBAAC,QAAD;UACE,MAAK;UACL,SAAS,SAAS,cAAc;UAChC,MAAK;UACL,WAAW,GAAG,UAAU,qBAAqB;UAC7C,cAAY,EAAE,oBAAoB,EAAE,MAAM,KAAK,KAAK,GAAG,YAAY;UACnE,OAAO,EAAE,sBAAsB,CAAC,GAAG,YAAY;UAC/C,eAAY;UACZ,eAAe,KAAK,aAAa,IAAI;UAErC,UAAA,oBAAC,YAAD,CAAa,CAAA;SACP,CAAA;SAET,UAAU,YAAY,kBAAkB,IAAI,CAAC,CAAC,QAC7C,oBAAC,QAAD;UAAQ,SAAA;UAAQ,MAAK;UAAS,SAAQ;UAAQ,MAAK;UACjD,UAAA,oBAAC,KAAD;WACE,MAAM,kBAAkB,IAAI,CAAC,CAAC;WAC9B,QAAO;WACP,KAAI;WACJ,cAAY,GAAG,EAAE,oBAAoB,EAAE,MAAM,KAAK,KAAK,GAAG,YAAY,EAAE,GAAG,EAAE,oBAAoB,CAAC,GAAG,sBAAsB;WAE3H,UAAA,oBAAC,kBAAD,CAAmB,CAAA;UAClB,CAAA;SACG,CAAA;SAET,UAAU,gBAAgB,kBAAkB,IAAI,CAAC,CAAC,YACjD,oBAAC,QAAD;UAAQ,SAAA;UAAQ,MAAK;UAAS,SAAQ;UAAQ,MAAK;UACjD,UAAA,oBAAC,KAAD;WACE,MAAM,kBAAkB,IAAI,CAAC,CAAC;WAC9B,UAAA;WACA,cAAY,EACV,wBACA,EAAE,MAAM,KAAK,KAAK,GAClB,gBACF;WAEA,UAAA,oBAAC,cAAD,CAAe,CAAA;UACd,CAAA;SACG,CAAA;UAER,UAAU,CAAC,kBAAkB,aAAa,IAAI;QAC7C;;MACH;KAjJG,GAAA,KAAK,IAiJR,CACL;IACC,CAAA;IAEL,cAAc,CAAC,UACd,qBAAC,OAAD;KACE,MAAK;KACL,UAAU;KACV,eAAe,SAAS,SAAS,MAAM;KACvC,oBAAkB,KAAK;KACvB,YAAY,UAAU;MACpB,IAAI,MAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK;OAC9C,MAAM,eAAe;OACrB,SAAS,SAAS,MAAM;MAC1B;KACF;KACA,aAAa,UAAU;MACrB,MAAM,eAAe;MACrB,YAAY,IAAI;KAClB;KACA,mBAAmB,YAAY,KAAK;KAC5B;KACC;KACT,OACE,CAAC,UAAU,UAAU,mBACjB,EAAE,aAAa,eAAe,UAAU,gBAAgB,EAAE,IAC1D,KAAA;KAEN,cAAY;KACZ,WAAW,GACT,iKACA,gBAAgB,UACZ,2BACA,gBAAgB,QACd,uBACA,4BACN,cAAc,sBACd,YAAY,yBACZ,UAAU,wCACZ;KAlCF,UAAA;MAoCE,oBAAC,iBAAD,EAAiB,WAAU,SAAU,CAAA;MACpC,CAAC,UACA,oBAAC,QAAD,EAAA,UACG,UAAU,eACT,EAAE,mBAAmB,CAAC,GAAG,oCAAoC,EAC3D,CAAA;MAEP,CAAC,WACC,UAAU,qBAAqB,UAAU,WAAW,UAAU,aAC7D,oBAAC,QAAD;OAAM,WAAU;OACb,UAAA;QACC,UAAU,mBAAmB,KAAK,IAAI;QACtC,UAAU,UACN,KAAK,gBAAgB,UAAU,UAAU,MAAM,SAAS,MACxD;QACJ,UAAU,WACN,OACE,kBACA,UAAU,UACV,CAAC,GACD,4CACF,IACA;OACN,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,KAAK;MACT,CAAA;KAEP;;IAEP,oBAAC,SAAD;KACE,KAAK;KACD;KACJ,MAAK;KACL,WAAU;KACV,UAAU;KACV,GAAI;KACJ,UAAU,UAAU;KACpB,QAAQ,iBAAiB,SAAS;KAClC,UAAU;KACV,WAAW,UAAU;MAEnB,MAAM,SAAS,MAAM,OAAO,QAAQ,MAAM,KAAK,MAAM,OAAO,KAAK,IAAI,CAAC;MACtE,MAAM,OAAO,QAAQ;MACrB,OAAY,MAAM;KACpB;IACD,CAAA;IACA,UAAU,eACT,oBAAC,mBAAD;KAAmB,SAAS;KAAS,SAAS,UAAU;IAAc,CAAA;GAErE;;CACO,CAAA;AAElB;;AAGA,SAAS,UAAU,OAAgC;CACjD,OAAO,OAAO,UAAU,YAAY,gBAAgB,KAAK,KAAK,IAAI,GAAG,MAAM,MAAM;AACnF"}
|
|
1
|
+
{"version":3,"file":"file-upload.js","names":[],"sources":["../../../src/components/fields/file-upload.tsx"],"sourcesContent":["import type { FileUploadSchema, UploadedFile } from '@alfakit/core'\nimport {\n canEditImage,\n concurrencyLimiter,\n cssAspectRatio,\n editableImageName,\n fetchEditableImage,\n fieldMemory,\n fileManagerSize,\n filesFromClipboard,\n fileUploadAccept,\n fileUploadEntries,\n fileUploadPaths,\n insertUploadedFile,\n moveRepeaterItem,\n panelUploadOptions,\n requestJson,\n transformImage,\n uploadedFileLinks,\n uploadTemporaryFile,\n} from '@alfakit/core'\nimport {\n ChevronDownIcon,\n ChevronUpIcon,\n DownloadIcon,\n ExternalLinkIcon,\n FileIcon,\n PencilIcon,\n UploadCloudIcon,\n XIcon,\n} from 'lucide-react'\nimport { useRef, useState, type ClipboardEvent, type DragEvent } from 'react'\nimport { useChoice, useFormatter, usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { cn } from '../../lib/utils'\nimport { useField, useFormStore } from '../form/form-context'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { Button } from '../ui/button'\nimport { Spinner } from '../ui/spinner'\nimport type { ImageEditRequest } from './image-editor-dialog'\nimport { ImageEditorDialog } from './image-editor-dialog-lazy'\n\ninterface LocalFile extends UploadedFile {\n progress?: number\n error?: string\n preview?: string\n}\n\nexport function FileUploadField({ component }: { component: FileUploadSchema }) {\n const t = useT()\n const formatter = useFormatter()\n const choice = useChoice()\n const panel = usePanel()\n const { value, setValue, error, id, disabled, readOnly, aria } = useField<\n string | string[] | null\n >(component)\n // Read-only keeps open/download links but hides upload and remove controls.\n const locked = disabled || readOnly\n const store = useFormStore()\n // In-flight uploads and the metadata of files uploaded in this session (the server schema\n // only knows about saved files) live in the form's field memory, not in component state:\n // a virtualised Repeater item unmounts when scrolled out of view and finds them again,\n // local image previews included, when it mounts back.\n const uploadingStore = fieldMemory<LocalFile[]>(\n store,\n `${component.statePath}:uploading`,\n () => [],\n )\n const uploadedStore = fieldMemory<FileUploadSchema['files']>(\n store,\n `${component.statePath}:uploaded`,\n () => [],\n )\n const uploading = useStore(uploadingStore)\n const uploaded = useStore(uploadedStore)\n const setUploading = uploadingStore.set\n const setUploaded = uploadedStore.set\n const [dragging, setDragging] = useState(false)\n // Previews whose image failed to load (missing or unreachable file) show the file icon.\n const [brokenPreviews, setBrokenPreviews] = useState<ReadonlySet<string>>(() => new Set())\n const inputRef = useRef<HTMLInputElement>(null)\n const [dragPath, setDragPath] = useState<string | null>(null)\n // The image open in the editor (`imageEditor()`), one at a time.\n const [editing, setEditing] = useState<ImageEditRequest | null>(null)\n const editImage = (file: File) =>\n new Promise<File | null>((resolve) =>\n setEditing({\n file,\n resolve: (edited) => {\n setEditing(null)\n resolve(edited)\n },\n }),\n )\n // Object URLs made for local image previews; released when the file is removed.\n const objectUrls = fieldMemory<Set<string>>(\n store,\n `${component.statePath}:previews`,\n () => new Set(),\n ).get()\n\n const releasePreview = (url: string | undefined) => {\n if (url && objectUrls.delete(url)) URL.revokeObjectURL(url)\n }\n\n const paths = fileUploadPaths(value)\n const files: LocalFile[] = fileUploadEntries(paths, component.files, uploaded)\n const maxFiles = component.multiple ? (component.maxFiles ?? Infinity) : 1\n const canAddMore = files.length + uploading.length < maxFiles\n\n const commit = (next: string[]) => setValue(component.multiple ? next : (next[0] ?? null))\n\n const upload = async (list: FileList | File[]) => {\n const endpoint = panel.endpoints.upload\n\n if (!endpoint) {\n return\n }\n\n const accepted = Array.from(list).slice(\n 0,\n Math.max(0, maxFiles - files.length - uploading.length),\n )\n\n // `maxParallelUploads()`: files are prepared (edited, resized) one by one and then\n // queued, so at most that many upload at once.\n const limit = concurrencyLimiter(component.maxParallelUploads ?? 1)\n const queued: Promise<void>[] = []\n\n for (const [position, picked] of accepted.entries()) {\n let original = picked\n const edit = Boolean(component.imageEditor && canEditImage(picked))\n\n if (edit) {\n const edited = await editImage(picked)\n\n // Cancelled: the file is not uploaded.\n if (!edited) continue\n\n original = edited\n }\n\n const file = await prepare(original, edit)\n\n if (!file) continue\n\n const local = pendingFile(file, position)\n setUploading((state) => [...state, local])\n\n // Submits wait for queued and running uploads, so saving now doesn't drop the file.\n queued.push(store.trackUpload(limit(() => send(endpoint, file, local))))\n }\n\n await Promise.all(queued)\n }\n\n /** Resize and orient a picked (or edited) file, and check its size. Null when rejected. */\n const prepare = async (original: File, edited: boolean): Promise<File | null> => {\n const file =\n component.imageCropAspectRatio ||\n component.imageResizeTargetWidth ||\n component.imageResizeTargetHeight ||\n component.orientImagesFromExif\n ? await transformImage(original, {\n // The editor already cropped the image.\n aspectRatio: edited ? null : component.imageCropAspectRatio,\n mode: component.imageResizeMode,\n width: component.imageResizeTargetWidth,\n height: component.imageResizeTargetHeight,\n orient: component.orientImagesFromExif,\n })\n : original\n\n if (component.minSize && file.size < component.minSize * 1024) {\n setUploading((state) => [\n ...state,\n {\n path: file.name,\n name: file.name,\n size: file.size,\n error: t('form.file_too_small', {}, 'This file is too small.'),\n },\n ])\n\n return null\n }\n\n if (component.maxSize && file.size > component.maxSize * 1024) {\n setUploading((state) => [\n ...state,\n {\n path: file.name,\n name: file.name,\n size: file.size,\n error: t('form.file_too_large', {}, 'This file is too large.'),\n },\n ])\n\n return null\n }\n\n return file\n }\n\n /** The in-progress entry shown while `file` uploads. */\n const pendingFile = (file: File, position: number | string): LocalFile => {\n const local: LocalFile = {\n // Unique within the batch and across re-picks of a changed file.\n path: `pending:${position}:${file.lastModified}:${file.size}:${file.name}`,\n name: file.name,\n size: file.size,\n progress: 0,\n preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined,\n }\n if (local.preview) objectUrls.add(local.preview)\n\n return local\n }\n\n /**\n * The \"Edit\" button on an uploaded image: open it in the editor and upload the result\n * in its place (the old file goes like any removed one: a temporary upload is deleted\n * now, a stored file when the form is saved with `deleteRemovedFiles()`).\n */\n const editExisting = async (existing: LocalFile) => {\n const endpoint = panel.endpoints.upload\n const url = existing.url ?? existing.preview\n const name = editableImageName(existing)\n\n if (!endpoint || !url || !name) return\n\n const source = await fetchEditableImage(url, name)\n\n if (!source) {\n setUploading((state) => [\n ...state,\n {\n path: `failed:${existing.path}`,\n name: existing.name,\n error: t('image_editor.load_failed', {}, 'The image could not be opened.'),\n },\n ])\n\n return\n }\n\n const edited = await editImage(source)\n\n // Cancelled, or the editor couldn't decode it: keep the file as it is.\n if (!edited || edited === source) return\n\n const file = await prepare(edited, true)\n\n if (!file) return\n\n const local = pendingFile(file, `edit:${existing.path}`)\n setUploading((state) => [...state, local])\n await store.trackUpload(send(endpoint, file, local, existing.path))\n }\n\n const send = async (endpoint: string, file: File, local: LocalFile, replacing?: string) => {\n try {\n // Large files go in resumable chunks; progress covers the whole file.\n const response = await uploadTemporaryFile(endpoint, file, {\n ...panelUploadOptions(panel, component.uploadPolicy),\n onProgress: (progress) =>\n setUploading((state) =>\n state.map((item) => (item.path === local.path ? { ...item, progress } : item)),\n ),\n })\n\n setUploaded((state) => [\n ...state,\n {\n path: response.key,\n name: response.name,\n size: response.size,\n temporary: true,\n url: local.preview,\n },\n ])\n setUploading((state) => state.filter((item) => item.path !== local.path))\n // Read the store, not `value`: it's stale after the first file of a batch.\n const current = fileUploadPaths(\n store.getValue<string | string[] | null>(component.statePath, null),\n )\n\n if (replacing !== undefined && current.includes(replacing)) {\n // An edited image takes the old one's place.\n commit(current.map((path) => (path === replacing ? response.key : path)))\n discard(replacing)\n } else {\n commit(insertUploadedFile(current, response.key, component, maxFiles))\n }\n } catch (uploadError) {\n const message =\n (uploadError as { errors?: Record<string, string[]> }).errors?.file?.[0] ??\n (uploadError as Error).message\n setUploading((state) =>\n state.map((item) =>\n item.path === local.path ? { ...item, error: message, progress: undefined } : item,\n ),\n )\n }\n }\n\n const remove = (path: string) => {\n commit(paths.filter((item) => item !== path))\n discard(path)\n }\n\n /** Release a file that left the field: its preview, and a temporary upload's file. */\n const discard = (path: string) => {\n releasePreview(uploadedStore.get().find((file) => file.path === path)?.url)\n\n if (path.startsWith('tmp:') && panel.endpoints.uploadDestroy) {\n void requestJson(panel.endpoints.uploadDestroy, {\n method: 'DELETE',\n body: { key: path },\n }).catch(() => undefined)\n }\n }\n\n const onDrop = (event: DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n setDragging(false)\n\n if (!locked && event.dataTransfer.files.length > 0) {\n void upload(event.dataTransfer.files)\n }\n }\n\n // `pasteable()`: images and files pasted while the drop zone has focus.\n const onPaste = (event: ClipboardEvent<HTMLDivElement>) => {\n if (locked || component.pasteable === false) return\n\n const pasted = filesFromClipboard(event.clipboardData)\n\n if (pasted.length > 0) {\n event.preventDefault()\n void upload(pasted)\n }\n }\n\n const move = (from: number, to: number) => commit(moveRepeaterItem(paths, from, to) as string[])\n const reorderable = Boolean(component.reorderable && component.multiple && !locked)\n\n // `previewable(false)` shows every file as a name and icon, without the image.\n const isImage = (file: LocalFile) =>\n component.previewable !== false &&\n (component.image || /\\.(png|jpe?g|gif|webp|svg|avif)$/i.test(file.name))\n const circle = component.panelLayout === 'circle' || component.avatar\n const integrated = component.panelLayout === 'integrated' && !circle\n const removeAtStart = component.positions?.removeButton === 'start'\n const progressAtStart = component.positions?.progressIndicator === 'start'\n const uploadAlign = component.positions?.uploadButton ?? 'center'\n\n // `imageEditor()`: uploaded JPEG, PNG and WebP images can be edited again.\n const canEditExisting = (file: LocalFile, index: number) =>\n Boolean(component.imageEditor) &&\n !locked &&\n index < files.length &&\n !file.missing &&\n Boolean(file.url ?? file.preview) &&\n editableImageName(file) !== null\n\n const renderRemove = (file: LocalFile) =>\n component.deletable &&\n !locked && (\n <Button\n type=\"button\"\n variant={circle ? 'secondary' : 'ghost'}\n size=\"icon-xs\"\n className={cn(circle && 'rounded-full shadow')}\n aria-label={t('file_upload.remove', { name: file.name }, 'Remove :name')}\n onClick={() => {\n if (file.error || file.progress !== undefined) {\n releasePreview(file.preview)\n setUploading((state) => state.filter((item) => item.path !== file.path))\n } else {\n remove(file.path)\n }\n }}\n >\n <XIcon />\n </Button>\n )\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div\n data-slot=\"file-upload\"\n data-layout={component.panelLayout}\n className={cn('flex flex-col gap-3', integrated && 'rounded-md border p-2')}\n >\n {(files.length > 0 || uploading.length > 0) && (\n <ul\n className={cn(\n 'grid gap-2',\n component.panelLayout === 'grid' && 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4',\n circle && 'justify-items-center',\n )}\n >\n {[...files, ...uploading].map((file, index) => (\n <li\n key={file.path}\n draggable={reorderable && index < files.length}\n onDragStart={() => setDragPath(file.path)}\n onDragOver={(event) => reorderable && event.preventDefault()}\n onDrop={(event) => {\n if (!reorderable || dragPath === null) return\n event.preventDefault()\n event.stopPropagation()\n const from = paths.indexOf(dragPath)\n if (from !== -1 && index < files.length) move(from, index)\n setDragPath(null)\n }}\n onDragEnd={() => setDragPath(null)}\n className={cn(\n dragPath === file.path && 'opacity-50',\n 'bg-muted/30 relative flex min-w-0 items-center gap-3 overflow-hidden rounded-md border p-2 text-sm',\n circle && 'size-28 justify-center rounded-full border-0 p-0',\n file.error && 'border-destructive',\n )}\n >\n {!circle && removeAtStart && renderRemove(file)}\n {!circle && progressAtStart && file.progress !== undefined && !file.error && (\n <Spinner />\n )}\n {isImage(file) && (file.url || file.preview) && !brokenPreviews.has(file.path) ? (\n <img\n src={file.url ?? file.preview}\n alt=\"\"\n onError={() => setBrokenPreviews((current) => new Set(current).add(file.path))}\n className={cn(\n 'size-12 shrink-0 rounded object-cover',\n circle && 'size-28 rounded-full',\n )}\n style={\n !circle && component.imagePreviewHeight\n ? {\n height: cssLength(component.imagePreviewHeight),\n width: 'auto',\n maxWidth: '50%',\n }\n : undefined\n }\n />\n ) : (\n !circle && <FileIcon className=\"text-muted-foreground size-6 shrink-0\" />\n )}\n {!circle && (\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-medium\">{file.name}</p>\n <p\n className={cn(\n 'text-muted-foreground text-xs',\n file.missing && 'text-destructive',\n )}\n >\n {file.error ??\n (file.missing\n ? t('form.file_missing', {}, 'This file no longer exists.')\n : file.progress !== undefined && !file.error\n ? (component.uploadingMessage ?? t('form.uploading', {}, 'Uploading…'))\n : file.size\n ? fileManagerSize(file.size, formatter)\n : file.temporary\n ? t('form.uploading', {}, 'Pending')\n : '')}\n </p>\n </div>\n )}\n <div\n className={cn('flex items-center gap-1', circle && 'absolute -end-1 -bottom-1')}\n >\n {(circle || !progressAtStart) && file.progress !== undefined && !file.error && (\n <Spinner />\n )}\n {reorderable && index < files.length && (\n <>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={t('file_upload.move_up', { name: file.name }, 'Move :name up')}\n disabled={index === 0}\n onClick={() => move(index, index - 1)}\n >\n <ChevronUpIcon />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={t(\n 'file_upload.move_down',\n { name: file.name },\n 'Move :name down',\n )}\n disabled={index === files.length - 1}\n onClick={() => move(index, index + 1)}\n >\n <ChevronDownIcon />\n </Button>\n </>\n )}\n {canEditExisting(file, index) && (\n <Button\n type=\"button\"\n variant={circle ? 'secondary' : 'ghost'}\n size=\"icon-xs\"\n className={cn(circle && 'rounded-full shadow')}\n aria-label={t('file_upload.edit', { name: file.name }, 'Edit :name')}\n title={t('image_editor.title', {}, 'Edit image')}\n data-action=\"edit-image\"\n onClick={() => void editExisting(file)}\n >\n <PencilIcon />\n </Button>\n )}\n {component.openable && uploadedFileLinks(file).open && (\n <Button asChild type=\"button\" variant=\"ghost\" size=\"icon-xs\">\n <a\n href={uploadedFileLinks(file).open}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label={`${t('file_upload.open', { name: file.name }, 'Open :name')} ${t('opens_in_new_tab', {}, '(opens in a new tab)')}`}\n >\n <ExternalLinkIcon />\n </a>\n </Button>\n )}\n {component.downloadable && uploadedFileLinks(file).download && (\n <Button asChild type=\"button\" variant=\"ghost\" size=\"icon-xs\">\n <a\n href={uploadedFileLinks(file).download}\n download\n aria-label={t(\n 'file_upload.download',\n { name: file.name },\n 'Download :name',\n )}\n >\n <DownloadIcon />\n </a>\n </Button>\n )}\n {(circle || !removeAtStart) && renderRemove(file)}\n </div>\n </li>\n ))}\n </ul>\n )}\n {canAddMore && !locked && (\n <div\n role=\"button\"\n tabIndex={0}\n onClick={() => inputRef.current?.click()}\n aria-describedby={aria['aria-describedby']}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n inputRef.current?.click()\n }\n }}\n onDragOver={(event) => {\n event.preventDefault()\n setDragging(true)\n }}\n onDragLeave={() => setDragging(false)}\n onDrop={onDrop}\n onPaste={onPaste}\n style={\n !circle && component.panelAspectRatio\n ? { aspectRatio: cssAspectRatio(component.panelAspectRatio) }\n : undefined\n }\n data-align={uploadAlign}\n className={cn(\n 'text-muted-foreground hover:border-ring flex cursor-pointer flex-col justify-center gap-1 rounded-md border border-dashed px-4 py-6 text-sm transition-colors',\n uploadAlign === 'start'\n ? 'items-start text-start'\n : uploadAlign === 'end'\n ? 'items-end text-end'\n : 'items-center text-center',\n integrated && 'border-transparent',\n dragging && 'border-ring bg-accent',\n circle && 'size-28 items-center rounded-full py-0',\n )}\n >\n <UploadCloudIcon className=\"size-5\" />\n {!circle && (\n <span>\n {component.placeholder ??\n t('form.drop_files', {}, 'Drop files here or click to browse')}\n </span>\n )}\n {!circle &&\n (component.acceptedFileTypesLabel || component.maxSize || component.minFiles) && (\n <span className=\"text-muted-foreground text-xs\">\n {[\n component.acceptedFileTypesLabel,\n component.maxSize\n ? `≤ ${fileManagerSize(component.maxSize * 1024, formatter)}`\n : null,\n component.minFiles\n ? choice(\n 'form.min_files',\n component.minFiles,\n {},\n 'At least :count file|At least :count files',\n )\n : null,\n ]\n .filter(Boolean)\n .join(' · ')}\n </span>\n )}\n </div>\n )}\n <input\n ref={inputRef}\n id={id}\n type=\"file\"\n className=\"sr-only\"\n tabIndex={-1}\n {...aria}\n multiple={component.multiple}\n accept={fileUploadAccept(component)}\n disabled={locked}\n onChange={(event) => {\n // Copy the list: clearing the input empties it while the editor is open.\n const picked = event.target.files ? Array.from(event.target.files) : []\n event.target.value = ''\n void upload(picked)\n }}\n />\n {component.imageEditor && (\n <ImageEditorDialog request={editing} options={component.imageEditor} />\n )}\n </div>\n </FieldWrapper>\n )\n}\n\n/** `imagePreviewHeight()`: numbers are pixels, strings are CSS lengths (`'250px'`, `'10rem'`). */\nfunction cssLength(value: number | string): string {\n return typeof value === 'number' || /^\\d+(\\.\\d+)?$/.test(value) ? `${value}px` : value\n}\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { useActionRunner, usePanel, useT } from "../../context.js";
|
|
|
2
2
|
import { cn } from "../../lib/utils.js";
|
|
3
3
|
import { Icon } from "../../lib/icons.js";
|
|
4
4
|
import { Button } from "../ui/button.js";
|
|
5
|
-
import { useField } from "../form/form-context.js";
|
|
5
|
+
import { useField, useOptionalFormStore } from "../form/form-context.js";
|
|
6
6
|
import { Skeleton } from "../ui/skeleton.js";
|
|
7
7
|
import { FieldWrapper } from "../schema/field-wrapper.js";
|
|
8
8
|
import { Separator } from "../ui/separator.js";
|
|
@@ -150,6 +150,7 @@ function RichEditorLoading({ component }) {
|
|
|
150
150
|
function RichEditorInput({ component, plugins }) {
|
|
151
151
|
const t = useT();
|
|
152
152
|
const panel = usePanel();
|
|
153
|
+
const formStore = useOptionalFormStore();
|
|
153
154
|
const runner = useActionRunner();
|
|
154
155
|
const { value, setValue, blur, error, id, disabled, readOnly, aria } = useField(component);
|
|
155
156
|
const fileInput = useRef(null);
|
|
@@ -298,7 +299,8 @@ function RichEditorInput({ component, plugins }) {
|
|
|
298
299
|
return () => window.removeEventListener("scroll", onScroll, { capture: true });
|
|
299
300
|
}, [hasFloatingToolbars]);
|
|
300
301
|
const pickImage = () => fileInput.current?.click();
|
|
301
|
-
const uploadImage =
|
|
302
|
+
const uploadImage = (file) => formStore ? formStore.trackUpload(sendImage(file)) : sendImage(file);
|
|
303
|
+
const sendImage = async (file) => {
|
|
302
304
|
if (!panel.endpoints.upload || !editor) return;
|
|
303
305
|
const problem = fileAttachmentError(file, component.fileAttachments);
|
|
304
306
|
if (problem) {
|