@alfakit/react 0.1.8 → 0.1.10
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 +14 -0
- package/dist/components/actions/action-modals.js +1 -1
- package/dist/components/actions/action-modals.js.map +1 -1
- package/dist/components/dev-tools/dev-tools-inspector.js +1 -1
- package/dist/components/dev-tools/dev-tools-inspector.js.map +1 -1
- package/dist/components/fields/checkbox-list.js +2 -2
- package/dist/components/fields/checkbox-list.js.map +1 -1
- package/dist/components/fields/rich-editor-extensions.js +14 -4
- package/dist/components/fields/rich-editor-extensions.js.map +1 -1
- package/dist/components/file-manager/file-manager.js +2 -0
- package/dist/components/file-manager/file-manager.js.map +1 -1
- package/dist/components/form/form-block.d.ts.map +1 -1
- package/dist/components/form/form-block.js +6 -3
- package/dist/components/form/form-block.js.map +1 -1
- package/dist/components/layout/elements.d.ts.map +1 -1
- package/dist/components/layout/elements.js +16 -5
- package/dist/components/layout/elements.js.map +1 -1
- package/dist/components/layout/mixed-content.d.ts +6 -4
- package/dist/components/layout/mixed-content.d.ts.map +1 -1
- package/dist/components/layout/mixed-content.js +6 -4
- package/dist/components/layout/mixed-content.js.map +1 -1
- package/dist/components/layout/section.js +4 -2
- package/dist/components/layout/section.js.map +1 -1
- package/dist/components/notifications/notifications-menu.d.ts.map +1 -1
- package/dist/components/notifications/notifications-menu.js +45 -17
- package/dist/components/notifications/notifications-menu.js.map +1 -1
- package/dist/components/panel/app-sidebar.js +3 -3
- package/dist/components/panel/app-sidebar.js.map +1 -1
- package/dist/components/panel/bottom-nav.js +2 -2
- package/dist/components/panel/bottom-nav.js.map +1 -1
- package/dist/components/panel/keyboard-shortcuts.js +1 -1
- package/dist/components/panel/keyboard-shortcuts.js.map +1 -1
- package/dist/components/panel/shortcuts-overlay.js +13 -10
- package/dist/components/panel/shortcuts-overlay.js.map +1 -1
- package/dist/components/table/column-manager.js +4 -4
- package/dist/components/table/column-manager.js.map +1 -1
- package/dist/components/table/spreadsheet.js +8 -1
- package/dist/components/table/spreadsheet.js.map +1 -1
- package/dist/components/translation-manager/translation-manager.js +1 -1
- package/dist/components/translation-manager/translation-manager.js.map +1 -1
- package/dist/components/ui/tooltip.d.ts.map +1 -1
- package/dist/components/ui/tooltip.js +1 -1
- package/dist/components/ui/tooltip.js.map +1 -1
- package/dist/components/widgets/stats-overview.js +3 -3
- package/dist/components/widgets/stats-overview.js.map +1 -1
- package/dist/eject-manifest.json +2 -2
- package/dist/layouts/top-nav-layout.js +3 -3
- package/dist/layouts/top-nav-layout.js.map +1 -1
- package/dist/pages/page.d.ts.map +1 -1
- package/dist/pages/page.js +4 -7
- package/dist/pages/page.js.map +1 -1
- package/dist/standalone/alfakit-component.d.ts +0 -10
- package/dist/standalone/alfakit-component.d.ts.map +1 -1
- package/dist/standalone/alfakit-component.js +20 -9
- package/dist/standalone/alfakit-component.js.map +1 -1
- package/dist/standalone/runtime.js +2 -1
- package/dist/standalone/runtime.js.map +1 -1
- package/dist/standalone.css +12 -7
- package/dist/starter-kit/navigation.d.ts +6 -1
- package/dist/starter-kit/navigation.d.ts.map +1 -1
- package/dist/starter-kit/navigation.js +3 -2
- package/dist/starter-kit/navigation.js.map +1 -1
- package/dist/starter-kit/starter-kit.d.ts.map +1 -1
- package/dist/starter-kit/starter-kit.js +7 -2
- package/dist/starter-kit/starter-kit.js.map +1 -1
- package/dist/styles.css +2 -1
- package/dist/themes.css +4 -0
- package/package.json +2 -2
- package/src/components/actions/action-modals.tsx +2 -0
- package/src/components/dev-tools/dev-tools-inspector.tsx +1 -1
- package/src/components/fields/checkbox-list.tsx +2 -2
- package/src/components/fields/rich-editor-extensions.tsx +24 -2
- package/src/components/file-manager/file-manager.tsx +3 -1
- package/src/components/form/form-block.tsx +15 -2
- package/src/components/layout/elements.tsx +27 -4
- package/src/components/layout/mixed-content.tsx +8 -4
- package/src/components/layout/section.tsx +2 -2
- package/src/components/notifications/notifications-menu.tsx +42 -1
- package/src/components/panel/app-sidebar.tsx +3 -3
- package/src/components/panel/bottom-nav.tsx +2 -2
- package/src/components/panel/keyboard-shortcuts.tsx +1 -1
- package/src/components/panel/shortcuts-overlay.tsx +29 -24
- package/src/components/table/column-manager.tsx +6 -4
- package/src/components/table/spreadsheet.tsx +18 -7
- package/src/components/translation-manager/translation-manager.tsx +1 -1
- package/src/components/ui/tooltip.tsx +10 -1
- package/src/components/widgets/stats-overview.tsx +6 -3
- package/src/layouts/top-nav-layout.tsx +3 -3
- package/src/pages/page.tsx +7 -3
- package/src/standalone/alfakit-component.tsx +18 -3
- package/src/standalone/runtime.ts +3 -0
- package/src/starter-kit/navigation.ts +12 -1
- package/src/starter-kit/starter-kit.tsx +10 -1
- package/src/styles.css +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @alfakit/react
|
|
2
2
|
|
|
3
|
+
## 0.1.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Back after a save shows fresh data; `useAlfakitNavigation().userMenuItems` for starter kits; push toggle in `<AlfakitNotifications />`; rich editor keeps focus after a toolbar group pick; Callout/EmptyState/section footer actions run with the form's state; column manager Reset fix; `<AlfakitComponent>` deferred loading fix.
|
|
8
|
+
|
|
9
|
+
## 0.1.9
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Row action modals work on every list tab; starter kits: the debug panel, "G then D" to the app dashboard and badge contrast; two standalone forms on a page get their own field ids; an `<AlfakitComponent>` in a panel page posts to its own endpoint; tooltips keep the button's `data-slot`; navigation items keyed by URL; modal headers leave room for the close button.
|
|
14
|
+
- Updated dependencies
|
|
15
|
+
- @alfakit/core@0.1.9
|
|
16
|
+
|
|
3
17
|
## 0.1.8
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
|
@@ -345,7 +345,7 @@ function ActionModal({ modal, hidden = false }) {
|
|
|
345
345
|
onEscapeKeyDown: (event) => !schema.closeByEscaping && event.preventDefault(),
|
|
346
346
|
children: [
|
|
347
347
|
/* @__PURE__ */ jsx(DialogHeader, {
|
|
348
|
-
className: cn(schema.stickyHeader && "bg-background sticky -top-6 z-10 -mx-6 -mt-6 rounded-t-lg border-b px-6 pt-6 pb-4"),
|
|
348
|
+
className: cn(schema.stickyHeader && "bg-background sticky -top-6 z-10 -mx-6 -mt-6 rounded-t-lg border-b px-6 pt-6 pb-4", schema.closeButton !== false && (schema.stickyHeader ? "pe-12" : "pe-6")),
|
|
349
349
|
children: header
|
|
350
350
|
}),
|
|
351
351
|
(schema.content || form || schema.contentFooter || schema.passwordConfirmation) && body,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-modals.js","names":[],"sources":["../../../src/components/actions/action-modals.tsx"],"sourcesContent":["import type { FormState, OpenModal } from '@alfakit/core'\nimport {\n actionFormComponent,\n actionFormParams,\n actionPath,\n confirmModalClose,\n formIdPrefix,\n FormStore,\n guardModalUnload,\n isModalHiddenByChild,\n isModalSubmitVisible,\n modalFooterAlignment,\n nestedActionContext,\n PASSWORD_CONFIRMATION_INPUT,\n scheduleAutofocus,\n slideOverSide,\n wizardProgressStore,\n} from '@alfakit/core'\nimport { useEffect, useId, useMemo, useRef, useState } from 'react'\nimport { useActionRunner, useDirection, usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { extraProps } from '../../lib/attributes'\nimport {\n colorClass,\n iconBackgroundColorClasses,\n outlineButtonColorClasses,\n solidButtonColorClasses,\n} from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { FormProvider } from '../form/form-context'\nimport { showToast } from '../notifications/flash-toasts'\nimport { Slot } from '../panel/slot'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { Button } from '../ui/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../ui/dialog'\nimport { Input } from '../ui/input'\nimport { Label } from '../ui/label'\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetTitle,\n} from '../ui/sheet'\nimport { Spinner } from '../ui/spinner'\nimport { ActionList } from './action-list'\n\nconst WIDTHS: Record<string, string> = {\n '3xs': 'sm:max-w-3xs',\n '2xs': 'sm:max-w-2xs',\n xs: 'sm:max-w-xs',\n sm: 'sm:max-w-sm',\n md: 'sm:max-w-md',\n lg: 'sm:max-w-lg',\n xl: 'sm:max-w-xl',\n '2xl': 'sm:max-w-2xl',\n '3xl': 'sm:max-w-3xl',\n '4xl': 'sm:max-w-4xl',\n '5xl': 'sm:max-w-5xl',\n '6xl': 'sm:max-w-6xl',\n '7xl': 'sm:max-w-7xl',\n none: 'sm:max-w-none',\n min: 'sm:max-w-min',\n max: 'sm:max-w-max',\n fit: 'sm:max-w-fit',\n prose: 'sm:max-w-prose',\n container: 'sm:container',\n 'screen-sm': 'sm:max-w-(--breakpoint-sm)',\n 'screen-md': 'sm:max-w-(--breakpoint-md)',\n 'screen-lg': 'sm:max-w-(--breakpoint-lg)',\n 'screen-xl': 'sm:max-w-(--breakpoint-xl)',\n 'screen-2xl': 'sm:max-w-(--breakpoint-2xl)',\n full: 'sm:max-w-[calc(100%-2rem)]',\n screen: 'sm:max-w-[calc(100%-2rem)] h-[calc(100%-2rem)]',\n}\n\n// Module-level so the snapshot stays stable (a new object per read re-renders forever).\nconst EMPTY_PROGRESS = { step: 0, count: 0 }\nconst NO_PROGRESS = { get: () => EMPTY_PROGRESS, subscribe: () => () => {} }\nconst NO_FORM = { get: () => null, subscribe: () => () => {} }\n\n/** Renders every open action modal from the runner store. Mount once per app. */\nexport function ActionModals() {\n const runner = useActionRunner()\n const { modals } = useStore(runner.store)\n\n // Notifications from JSON responses (a modal `mountUsing()` cancelled, field actions).\n useEffect(() => {\n runner.setNotificationHandler((notifications) => notifications.forEach((n) => showToast(n)))\n\n return () => runner.setNotificationHandler(undefined)\n }, [runner])\n\n return (\n <>\n {modals.map((modal, index) => (\n <ActionModal\n key={`${actionPath(modal.action, modal.context)}-${modal.context.record ?? ''}`}\n modal={modal}\n hidden={isModalHiddenByChild(modals, index)}\n />\n ))}\n </>\n )\n}\n\nconst FOOTER_JUSTIFY: Record<string, string> = {\n start: 'sm:justify-start',\n center: 'sm:justify-center',\n end: 'sm:justify-end',\n between: 'sm:justify-between',\n}\n\n/**\n * Put PHP attributes (`extraModalOverlayAttributes()`) on an element the renderer doesn't\n * create itself (the dialog's backdrop). `class` is added to the element's classes.\n */\nfunction applyAttributes(\n element: Element | null | undefined,\n attributes: Record<string, unknown> | undefined,\n) {\n if (!element || !attributes) return\n\n for (const [name, value] of Object.entries(attributes)) {\n if (value === null || value === undefined || value === false) continue\n\n if (name === 'class' || name === 'className') {\n element.classList.add(...String(value).split(/\\s+/).filter(Boolean))\n } else {\n element.setAttribute(name, value === true ? '' : String(value))\n }\n }\n}\n\nfunction ActionModal({ modal, hidden = false }: { modal: OpenModal; hidden?: boolean }) {\n const runner = useActionRunner()\n const panel = usePanel()\n const direction = useDirection()\n const t = useT()\n const { action, context, modal: schema } = modal\n const submittedRef = useRef(false)\n\n const form = useMemo(() => {\n if (!schema.form) {\n return null\n }\n\n return new FormStore(schema.form, {\n token: context.token,\n component: actionFormComponent(action, context),\n // Own field ids: the modal opens over a form whose fields may share names.\n idPrefix: formIdPrefix(actionFormComponent(action, context)),\n record: context.record ?? null,\n refreshUrl: panel.endpoints.schemaRefresh,\n optionsUrl: panel.endpoints.schemaOptions,\n validateUrl: panel.endpoints.schemaValidate,\n // A halted wizard step shows what its hook sent.\n onNotifications: (notifications) => notifications.forEach((n) => showToast(n)),\n params: actionFormParams(context),\n })\n }, [schema.form, context, action, panel.endpoints])\n\n useEffect(() => {\n form?.setErrors(modal.errors)\n }, [form, modal.errors])\n\n useEffect(() => () => form?.destroy(), [form])\n\n // \"Create & create another\": back to the `autofocus()` field of the fresh form.\n const refocus = modal.refocus\n\n useEffect(() => {\n if (!refocus || !form) return\n\n return scheduleAutofocus(\n () => document.getElementById(`action-form-${action.name}`),\n form.state.components,\n )\n }, [refocus, form, action.name])\n\n // Nested actions read this form (`$mountedActions`, `getRawData()`) and write into it\n // (`fillData()`).\n useEffect(\n () => (form ? runner.registerModalForm(action, context, form) : undefined),\n [runner, form, action, context],\n )\n\n // `extraModalOverlayAttributes()`: the backdrop is the content's previous sibling.\n // A callback ref: the portal renders the content after the first commit.\n const [content, setContent] = useState<HTMLDivElement | null>(null)\n\n useEffect(() => {\n if (hidden) return\n\n const overlay = content?.previousElementSibling\n\n if (overlay?.getAttribute('data-slot')?.endsWith('-overlay')) {\n applyAttributes(overlay, schema.overlayAttributes)\n }\n }, [schema.overlayAttributes, hidden, content])\n\n // Live updates replace the form's components (fields shown, options changed).\n const formState = useStore<FormState | null>(form?.store ?? NO_FORM)\n\n // A wizard form reports its step, so the submit button waits for the last one.\n const progressStore = useMemo(() => (form ? wizardProgressStore(form) : null), [form])\n const progress = useStore(progressStore ?? NO_PROGRESS)\n const submitVisible = isModalSubmitVisible(schema, progressStore ? progress : null)\n\n const [password, setPassword] = useState('')\n const passwordId = useId()\n const passwordError = modal.errors[PASSWORD_CONFIRMATION_INPUT]?.[0]\n\n const submit = (createAnother = false) => {\n submittedRef.current = true\n\n // A click that blurred a live field started a refresh: send the state it returns.\n if (form) {\n form.whenSettled(() => run(createAnother))\n } else {\n run(createAnother)\n }\n }\n\n const run = (createAnother: boolean) => {\n const values = form?.state.values ?? {}\n const options = {\n ...(schema.passwordConfirmation ? { password } : {}),\n ...(createAnother ? { createAnother: true } : {}),\n }\n\n if (Object.keys(options).length > 0) {\n runner.execute(action, context, values, options)\n } else {\n runner.execute(action, context, values)\n }\n }\n\n const footerContext = useMemo(() => nestedActionContext(context, action), [context, action])\n\n // Dismissing the modal (not submitting it): `cancelParentActionsOnClose()` applies.\n const close = () => runner.cancelModal(action.name)\n const unsavedMessage = t('unsaved_changes_close', {}, 'You have unsaved changes. Close anyway?')\n // Cancel, Escape, clicking away and the close button ask first when the form changed.\n const requestClose = () => {\n if (confirmModalClose(schema, form, unsavedMessage)) close()\n }\n\n useEffect(() => guardModalUnload(schema, form, unsavedMessage), [schema, form, unsavedMessage])\n const title = schema.heading ?? action.label ?? t('actions', {}, 'Actions')\n\n const header = (\n <div\n className={cn(\n 'flex gap-4',\n schema.alignment === 'center' && 'flex-col items-center text-center',\n )}\n >\n {schema.icon && (\n <div\n className={cn(\n 'flex size-10 shrink-0 items-center justify-center rounded-full',\n colorClass(iconBackgroundColorClasses, schema.iconColor, 'primary'),\n )}\n >\n <Icon name={schema.icon} className=\"size-5\" />\n </div>\n )}\n <div className=\"flex flex-col gap-1.5\">\n {/* Every dialog needs an accessible name: without a heading it stays visually hidden. */}\n {schema.slideOver ? (\n <SheetTitle className={cn(!schema.heading && 'sr-only')}>{title}</SheetTitle>\n ) : (\n <DialogTitle className={cn(!schema.heading && 'sr-only')}>{title}</DialogTitle>\n )}\n {schema.description ? (\n schema.slideOver ? (\n <SheetDescription dangerouslySetInnerHTML={{ __html: schema.description }} />\n ) : (\n <DialogDescription dangerouslySetInnerHTML={{ __html: schema.description }} />\n )\n ) : schema.slideOver ? (\n <SheetDescription className=\"sr-only\">{title}</SheetDescription>\n ) : (\n <DialogDescription className=\"sr-only\">{title}</DialogDescription>\n )}\n </div>\n </div>\n )\n\n const body = (\n <div className=\"flex flex-col gap-6\">\n {schema.content && <SchemaRenderer components={schema.content} />}\n {form && <Slot name=\"modal.schema.before\" data={{ action: action.name }} />}\n {form && (\n <FormProvider store={form}>\n <form\n id={`action-form-${action.name}`}\n onSubmit={(event) => {\n event.preventDefault()\n submit()\n }}\n >\n <SchemaRenderer\n components={formState?.components ?? form.state.components}\n columns={formState?.columns ?? form.state.columns}\n />\n </form>\n </FormProvider>\n )}\n {form && <Slot name=\"modal.schema.after\" data={{ action: action.name }} />}\n {schema.contentFooter && <SchemaRenderer components={schema.contentFooter} />}\n {schema.passwordConfirmation && (\n <div data-slot=\"password-confirmation\" className=\"flex flex-col gap-2\">\n <Label htmlFor={passwordId}>{t('current_password', {}, 'Current password')}</Label>\n <Input\n id={passwordId}\n type=\"password\"\n autoComplete=\"current-password\"\n value={password}\n aria-invalid={passwordError ? true : undefined}\n aria-describedby={passwordError ? `${passwordId}-error` : `${passwordId}-hint`}\n onChange={(event) => setPassword(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n submit()\n }\n }}\n />\n {passwordError ? (\n <p id={`${passwordId}-error`} className=\"text-destructive text-sm\">\n {passwordError}\n </p>\n ) : (\n <p id={`${passwordId}-hint`} className=\"text-muted-foreground text-sm\">\n {t('confirm_password', {}, 'Confirm your password to continue')}\n </p>\n )}\n </div>\n )}\n </div>\n )\n\n const footerAlignment = modalFooterAlignment(schema)\n const footer = (schema.submitLabel ||\n schema.cancelLabel ||\n schema.extraFooterActions?.length) && (\n <div\n className={cn(\n 'flex w-full flex-col-reverse gap-2 sm:flex-row sm:items-center',\n FOOTER_JUSTIFY[footerAlignment],\n )}\n >\n {schema.extraFooterActions && (\n <ActionList\n actions={schema.extraFooterActions}\n context={footerContext}\n className={footerAlignment === 'end' ? 'sm:me-auto' : undefined}\n />\n )}\n {schema.cancelLabel && (\n <Button\n type=\"button\"\n variant=\"outline\"\n className={\n schema.cancelColor\n ? colorClass(outlineButtonColorClasses, schema.cancelColor)\n : undefined\n }\n onClick={requestClose}\n disabled={modal.processing}\n >\n {schema.cancelLabel}\n </Button>\n )}\n {schema.createAnotherLabel && schema.submitLabel && submitVisible && (\n <Button\n type=\"button\"\n variant=\"outline\"\n data-slot=\"create-another\"\n className={\n schema.createAnotherColor\n ? colorClass(outlineButtonColorClasses, schema.createAnotherColor)\n : undefined\n }\n onClick={() => submit(true)}\n disabled={modal.processing}\n >\n {schema.createAnotherIcon && <Icon name={schema.createAnotherIcon} />}\n {schema.createAnotherLabel}\n </Button>\n )}\n {schema.submitLabel && submitVisible && (\n <Button\n type=\"button\"\n className={colorClass(solidButtonColorClasses, schema.submitColor, 'primary')}\n onClick={() => submit()}\n disabled={modal.processing}\n autoFocus={schema.autofocus && !form}\n >\n {modal.processing ? <Spinner /> : schema.submitIcon && <Icon name={schema.submitIcon} />}\n {schema.submitLabel}\n </Button>\n )}\n </div>\n )\n\n const onOpenChange = (open: boolean) => {\n if (!open && !modal.processing) {\n requestClose()\n }\n }\n\n // `modalClickThrough()`: a non-modal dialog has no backdrop and leaves the page usable.\n const isModal = !schema.clickThrough\n\n if (schema.slideOver) {\n return (\n <Sheet open={!hidden} modal={isModal} onOpenChange={onOpenChange}>\n <SheetContent\n ref={setContent}\n side={slideOverSide(schema.slideOverPosition, direction)}\n {...extraProps(\n schema.windowAttributes,\n cn('flex w-full flex-col gap-0 p-0', WIDTHS[schema.width] ?? 'sm:max-w-md'),\n )}\n showCloseButton={schema.closeButton}\n onInteractOutside={(event) => !schema.closeByClickingAway && event.preventDefault()}\n onEscapeKeyDown={(event) => !schema.closeByEscaping && event.preventDefault()}\n >\n <SheetHeader className=\"border-b\">{header}</SheetHeader>\n <div className=\"flex-1 overflow-y-auto p-4\">{body}</div>\n {footer && (\n <SheetFooter\n className={cn('border-t', schema.stickyFooter && 'bg-background sticky bottom-0')}\n >\n {footer}\n </SheetFooter>\n )}\n </SheetContent>\n </Sheet>\n )\n }\n\n return (\n <Dialog open={!hidden} modal={isModal} onOpenChange={onOpenChange}>\n <DialogContent\n ref={setContent}\n {...extraProps(\n schema.windowAttributes,\n cn('max-h-[calc(100vh-2rem)] overflow-y-auto', WIDTHS[schema.width] ?? 'sm:max-w-lg'),\n )}\n showCloseButton={schema.closeButton}\n onInteractOutside={(event) => !schema.closeByClickingAway && event.preventDefault()}\n onEscapeKeyDown={(event) => !schema.closeByEscaping && event.preventDefault()}\n >\n <DialogHeader\n className={cn(\n // Pinned to the top of the scrolling dialog, edge to edge.\n schema.stickyHeader &&\n 'bg-background sticky -top-6 z-10 -mx-6 -mt-6 rounded-t-lg border-b px-6 pt-6 pb-4',\n )}\n >\n {header}\n </DialogHeader>\n {(schema.content || form || schema.contentFooter || schema.passwordConfirmation) && body}\n {footer && (\n <DialogFooter\n className={cn(\n // Pinned to the bottom of the scrolling dialog, edge to edge.\n schema.stickyFooter &&\n 'bg-background sticky bottom-0 -mx-6 -mb-6 rounded-b-lg border-t px-6 py-4',\n )}\n >\n {footer}\n </DialogFooter>\n )}\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"action-modals.js","names":[],"sources":["../../../src/components/actions/action-modals.tsx"],"sourcesContent":["import type { FormState, OpenModal } from '@alfakit/core'\nimport {\n actionFormComponent,\n actionFormParams,\n actionPath,\n confirmModalClose,\n formIdPrefix,\n FormStore,\n guardModalUnload,\n isModalHiddenByChild,\n isModalSubmitVisible,\n modalFooterAlignment,\n nestedActionContext,\n PASSWORD_CONFIRMATION_INPUT,\n scheduleAutofocus,\n slideOverSide,\n wizardProgressStore,\n} from '@alfakit/core'\nimport { useEffect, useId, useMemo, useRef, useState } from 'react'\nimport { useActionRunner, useDirection, usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { extraProps } from '../../lib/attributes'\nimport {\n colorClass,\n iconBackgroundColorClasses,\n outlineButtonColorClasses,\n solidButtonColorClasses,\n} from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { FormProvider } from '../form/form-context'\nimport { showToast } from '../notifications/flash-toasts'\nimport { Slot } from '../panel/slot'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { Button } from '../ui/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../ui/dialog'\nimport { Input } from '../ui/input'\nimport { Label } from '../ui/label'\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetTitle,\n} from '../ui/sheet'\nimport { Spinner } from '../ui/spinner'\nimport { ActionList } from './action-list'\n\nconst WIDTHS: Record<string, string> = {\n '3xs': 'sm:max-w-3xs',\n '2xs': 'sm:max-w-2xs',\n xs: 'sm:max-w-xs',\n sm: 'sm:max-w-sm',\n md: 'sm:max-w-md',\n lg: 'sm:max-w-lg',\n xl: 'sm:max-w-xl',\n '2xl': 'sm:max-w-2xl',\n '3xl': 'sm:max-w-3xl',\n '4xl': 'sm:max-w-4xl',\n '5xl': 'sm:max-w-5xl',\n '6xl': 'sm:max-w-6xl',\n '7xl': 'sm:max-w-7xl',\n none: 'sm:max-w-none',\n min: 'sm:max-w-min',\n max: 'sm:max-w-max',\n fit: 'sm:max-w-fit',\n prose: 'sm:max-w-prose',\n container: 'sm:container',\n 'screen-sm': 'sm:max-w-(--breakpoint-sm)',\n 'screen-md': 'sm:max-w-(--breakpoint-md)',\n 'screen-lg': 'sm:max-w-(--breakpoint-lg)',\n 'screen-xl': 'sm:max-w-(--breakpoint-xl)',\n 'screen-2xl': 'sm:max-w-(--breakpoint-2xl)',\n full: 'sm:max-w-[calc(100%-2rem)]',\n screen: 'sm:max-w-[calc(100%-2rem)] h-[calc(100%-2rem)]',\n}\n\n// Module-level so the snapshot stays stable (a new object per read re-renders forever).\nconst EMPTY_PROGRESS = { step: 0, count: 0 }\nconst NO_PROGRESS = { get: () => EMPTY_PROGRESS, subscribe: () => () => {} }\nconst NO_FORM = { get: () => null, subscribe: () => () => {} }\n\n/** Renders every open action modal from the runner store. Mount once per app. */\nexport function ActionModals() {\n const runner = useActionRunner()\n const { modals } = useStore(runner.store)\n\n // Notifications from JSON responses (a modal `mountUsing()` cancelled, field actions).\n useEffect(() => {\n runner.setNotificationHandler((notifications) => notifications.forEach((n) => showToast(n)))\n\n return () => runner.setNotificationHandler(undefined)\n }, [runner])\n\n return (\n <>\n {modals.map((modal, index) => (\n <ActionModal\n key={`${actionPath(modal.action, modal.context)}-${modal.context.record ?? ''}`}\n modal={modal}\n hidden={isModalHiddenByChild(modals, index)}\n />\n ))}\n </>\n )\n}\n\nconst FOOTER_JUSTIFY: Record<string, string> = {\n start: 'sm:justify-start',\n center: 'sm:justify-center',\n end: 'sm:justify-end',\n between: 'sm:justify-between',\n}\n\n/**\n * Put PHP attributes (`extraModalOverlayAttributes()`) on an element the renderer doesn't\n * create itself (the dialog's backdrop). `class` is added to the element's classes.\n */\nfunction applyAttributes(\n element: Element | null | undefined,\n attributes: Record<string, unknown> | undefined,\n) {\n if (!element || !attributes) return\n\n for (const [name, value] of Object.entries(attributes)) {\n if (value === null || value === undefined || value === false) continue\n\n if (name === 'class' || name === 'className') {\n element.classList.add(...String(value).split(/\\s+/).filter(Boolean))\n } else {\n element.setAttribute(name, value === true ? '' : String(value))\n }\n }\n}\n\nfunction ActionModal({ modal, hidden = false }: { modal: OpenModal; hidden?: boolean }) {\n const runner = useActionRunner()\n const panel = usePanel()\n const direction = useDirection()\n const t = useT()\n const { action, context, modal: schema } = modal\n const submittedRef = useRef(false)\n\n const form = useMemo(() => {\n if (!schema.form) {\n return null\n }\n\n return new FormStore(schema.form, {\n token: context.token,\n component: actionFormComponent(action, context),\n // Own field ids: the modal opens over a form whose fields may share names.\n idPrefix: formIdPrefix(actionFormComponent(action, context)),\n record: context.record ?? null,\n refreshUrl: panel.endpoints.schemaRefresh,\n optionsUrl: panel.endpoints.schemaOptions,\n validateUrl: panel.endpoints.schemaValidate,\n // A halted wizard step shows what its hook sent.\n onNotifications: (notifications) => notifications.forEach((n) => showToast(n)),\n params: actionFormParams(context),\n })\n }, [schema.form, context, action, panel.endpoints])\n\n useEffect(() => {\n form?.setErrors(modal.errors)\n }, [form, modal.errors])\n\n useEffect(() => () => form?.destroy(), [form])\n\n // \"Create & create another\": back to the `autofocus()` field of the fresh form.\n const refocus = modal.refocus\n\n useEffect(() => {\n if (!refocus || !form) return\n\n return scheduleAutofocus(\n () => document.getElementById(`action-form-${action.name}`),\n form.state.components,\n )\n }, [refocus, form, action.name])\n\n // Nested actions read this form (`$mountedActions`, `getRawData()`) and write into it\n // (`fillData()`).\n useEffect(\n () => (form ? runner.registerModalForm(action, context, form) : undefined),\n [runner, form, action, context],\n )\n\n // `extraModalOverlayAttributes()`: the backdrop is the content's previous sibling.\n // A callback ref: the portal renders the content after the first commit.\n const [content, setContent] = useState<HTMLDivElement | null>(null)\n\n useEffect(() => {\n if (hidden) return\n\n const overlay = content?.previousElementSibling\n\n if (overlay?.getAttribute('data-slot')?.endsWith('-overlay')) {\n applyAttributes(overlay, schema.overlayAttributes)\n }\n }, [schema.overlayAttributes, hidden, content])\n\n // Live updates replace the form's components (fields shown, options changed).\n const formState = useStore<FormState | null>(form?.store ?? NO_FORM)\n\n // A wizard form reports its step, so the submit button waits for the last one.\n const progressStore = useMemo(() => (form ? wizardProgressStore(form) : null), [form])\n const progress = useStore(progressStore ?? NO_PROGRESS)\n const submitVisible = isModalSubmitVisible(schema, progressStore ? progress : null)\n\n const [password, setPassword] = useState('')\n const passwordId = useId()\n const passwordError = modal.errors[PASSWORD_CONFIRMATION_INPUT]?.[0]\n\n const submit = (createAnother = false) => {\n submittedRef.current = true\n\n // A click that blurred a live field started a refresh: send the state it returns.\n if (form) {\n form.whenSettled(() => run(createAnother))\n } else {\n run(createAnother)\n }\n }\n\n const run = (createAnother: boolean) => {\n const values = form?.state.values ?? {}\n const options = {\n ...(schema.passwordConfirmation ? { password } : {}),\n ...(createAnother ? { createAnother: true } : {}),\n }\n\n if (Object.keys(options).length > 0) {\n runner.execute(action, context, values, options)\n } else {\n runner.execute(action, context, values)\n }\n }\n\n const footerContext = useMemo(() => nestedActionContext(context, action), [context, action])\n\n // Dismissing the modal (not submitting it): `cancelParentActionsOnClose()` applies.\n const close = () => runner.cancelModal(action.name)\n const unsavedMessage = t('unsaved_changes_close', {}, 'You have unsaved changes. Close anyway?')\n // Cancel, Escape, clicking away and the close button ask first when the form changed.\n const requestClose = () => {\n if (confirmModalClose(schema, form, unsavedMessage)) close()\n }\n\n useEffect(() => guardModalUnload(schema, form, unsavedMessage), [schema, form, unsavedMessage])\n const title = schema.heading ?? action.label ?? t('actions', {}, 'Actions')\n\n const header = (\n <div\n className={cn(\n 'flex gap-4',\n schema.alignment === 'center' && 'flex-col items-center text-center',\n )}\n >\n {schema.icon && (\n <div\n className={cn(\n 'flex size-10 shrink-0 items-center justify-center rounded-full',\n colorClass(iconBackgroundColorClasses, schema.iconColor, 'primary'),\n )}\n >\n <Icon name={schema.icon} className=\"size-5\" />\n </div>\n )}\n <div className=\"flex flex-col gap-1.5\">\n {/* Every dialog needs an accessible name: without a heading it stays visually hidden. */}\n {schema.slideOver ? (\n <SheetTitle className={cn(!schema.heading && 'sr-only')}>{title}</SheetTitle>\n ) : (\n <DialogTitle className={cn(!schema.heading && 'sr-only')}>{title}</DialogTitle>\n )}\n {schema.description ? (\n schema.slideOver ? (\n <SheetDescription dangerouslySetInnerHTML={{ __html: schema.description }} />\n ) : (\n <DialogDescription dangerouslySetInnerHTML={{ __html: schema.description }} />\n )\n ) : schema.slideOver ? (\n <SheetDescription className=\"sr-only\">{title}</SheetDescription>\n ) : (\n <DialogDescription className=\"sr-only\">{title}</DialogDescription>\n )}\n </div>\n </div>\n )\n\n const body = (\n <div className=\"flex flex-col gap-6\">\n {schema.content && <SchemaRenderer components={schema.content} />}\n {form && <Slot name=\"modal.schema.before\" data={{ action: action.name }} />}\n {form && (\n <FormProvider store={form}>\n <form\n id={`action-form-${action.name}`}\n onSubmit={(event) => {\n event.preventDefault()\n submit()\n }}\n >\n <SchemaRenderer\n components={formState?.components ?? form.state.components}\n columns={formState?.columns ?? form.state.columns}\n />\n </form>\n </FormProvider>\n )}\n {form && <Slot name=\"modal.schema.after\" data={{ action: action.name }} />}\n {schema.contentFooter && <SchemaRenderer components={schema.contentFooter} />}\n {schema.passwordConfirmation && (\n <div data-slot=\"password-confirmation\" className=\"flex flex-col gap-2\">\n <Label htmlFor={passwordId}>{t('current_password', {}, 'Current password')}</Label>\n <Input\n id={passwordId}\n type=\"password\"\n autoComplete=\"current-password\"\n value={password}\n aria-invalid={passwordError ? true : undefined}\n aria-describedby={passwordError ? `${passwordId}-error` : `${passwordId}-hint`}\n onChange={(event) => setPassword(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n submit()\n }\n }}\n />\n {passwordError ? (\n <p id={`${passwordId}-error`} className=\"text-destructive text-sm\">\n {passwordError}\n </p>\n ) : (\n <p id={`${passwordId}-hint`} className=\"text-muted-foreground text-sm\">\n {t('confirm_password', {}, 'Confirm your password to continue')}\n </p>\n )}\n </div>\n )}\n </div>\n )\n\n const footerAlignment = modalFooterAlignment(schema)\n const footer = (schema.submitLabel ||\n schema.cancelLabel ||\n schema.extraFooterActions?.length) && (\n <div\n className={cn(\n 'flex w-full flex-col-reverse gap-2 sm:flex-row sm:items-center',\n FOOTER_JUSTIFY[footerAlignment],\n )}\n >\n {schema.extraFooterActions && (\n <ActionList\n actions={schema.extraFooterActions}\n context={footerContext}\n className={footerAlignment === 'end' ? 'sm:me-auto' : undefined}\n />\n )}\n {schema.cancelLabel && (\n <Button\n type=\"button\"\n variant=\"outline\"\n className={\n schema.cancelColor\n ? colorClass(outlineButtonColorClasses, schema.cancelColor)\n : undefined\n }\n onClick={requestClose}\n disabled={modal.processing}\n >\n {schema.cancelLabel}\n </Button>\n )}\n {schema.createAnotherLabel && schema.submitLabel && submitVisible && (\n <Button\n type=\"button\"\n variant=\"outline\"\n data-slot=\"create-another\"\n className={\n schema.createAnotherColor\n ? colorClass(outlineButtonColorClasses, schema.createAnotherColor)\n : undefined\n }\n onClick={() => submit(true)}\n disabled={modal.processing}\n >\n {schema.createAnotherIcon && <Icon name={schema.createAnotherIcon} />}\n {schema.createAnotherLabel}\n </Button>\n )}\n {schema.submitLabel && submitVisible && (\n <Button\n type=\"button\"\n className={colorClass(solidButtonColorClasses, schema.submitColor, 'primary')}\n onClick={() => submit()}\n disabled={modal.processing}\n autoFocus={schema.autofocus && !form}\n >\n {modal.processing ? <Spinner /> : schema.submitIcon && <Icon name={schema.submitIcon} />}\n {schema.submitLabel}\n </Button>\n )}\n </div>\n )\n\n const onOpenChange = (open: boolean) => {\n if (!open && !modal.processing) {\n requestClose()\n }\n }\n\n // `modalClickThrough()`: a non-modal dialog has no backdrop and leaves the page usable.\n const isModal = !schema.clickThrough\n\n if (schema.slideOver) {\n return (\n <Sheet open={!hidden} modal={isModal} onOpenChange={onOpenChange}>\n <SheetContent\n ref={setContent}\n side={slideOverSide(schema.slideOverPosition, direction)}\n {...extraProps(\n schema.windowAttributes,\n cn('flex w-full flex-col gap-0 p-0', WIDTHS[schema.width] ?? 'sm:max-w-md'),\n )}\n showCloseButton={schema.closeButton}\n onInteractOutside={(event) => !schema.closeByClickingAway && event.preventDefault()}\n onEscapeKeyDown={(event) => !schema.closeByEscaping && event.preventDefault()}\n >\n <SheetHeader className=\"border-b\">{header}</SheetHeader>\n <div className=\"flex-1 overflow-y-auto p-4\">{body}</div>\n {footer && (\n <SheetFooter\n className={cn('border-t', schema.stickyFooter && 'bg-background sticky bottom-0')}\n >\n {footer}\n </SheetFooter>\n )}\n </SheetContent>\n </Sheet>\n )\n }\n\n return (\n <Dialog open={!hidden} modal={isModal} onOpenChange={onOpenChange}>\n <DialogContent\n ref={setContent}\n {...extraProps(\n schema.windowAttributes,\n cn('max-h-[calc(100vh-2rem)] overflow-y-auto', WIDTHS[schema.width] ?? 'sm:max-w-lg'),\n )}\n showCloseButton={schema.closeButton}\n onInteractOutside={(event) => !schema.closeByClickingAway && event.preventDefault()}\n onEscapeKeyDown={(event) => !schema.closeByEscaping && event.preventDefault()}\n >\n <DialogHeader\n className={cn(\n // Pinned to the top of the scrolling dialog, edge to edge.\n schema.stickyHeader &&\n 'bg-background sticky -top-6 z-10 -mx-6 -mt-6 rounded-t-lg border-b px-6 pt-6 pb-4',\n // Room for the close button (X): a long heading would run under it.\n schema.closeButton !== false && (schema.stickyHeader ? 'pe-12' : 'pe-6'),\n )}\n >\n {header}\n </DialogHeader>\n {(schema.content || form || schema.contentFooter || schema.passwordConfirmation) && body}\n {footer && (\n <DialogFooter\n className={cn(\n // Pinned to the bottom of the scrolling dialog, edge to edge.\n schema.stickyFooter &&\n 'bg-background sticky bottom-0 -mx-6 -mb-6 rounded-b-lg border-t px-6 py-4',\n )}\n >\n {footer}\n </DialogFooter>\n )}\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":"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"}
|
|
@@ -48,7 +48,7 @@ function DevToolsInspector() {
|
|
|
48
48
|
size: "icon",
|
|
49
49
|
"aria-label": t("dev_tools.open", {}, "Open schema inspector"),
|
|
50
50
|
"data-slot": "dev-tools-toggle",
|
|
51
|
-
className: "fixed end-4 bottom-
|
|
51
|
+
className: "fixed end-4 bottom-16 z-40 rounded-full shadow-md [[data-slot=starter-kit-root]_&]:end-auto [[data-slot=starter-kit-root]_&]:right-4",
|
|
52
52
|
children: /* @__PURE__ */ jsx(BracesIcon, {})
|
|
53
53
|
})
|
|
54
54
|
}), /* @__PURE__ */ jsx(SheetContent, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-tools-inspector.js","names":[],"sources":["../../../src/components/dev-tools/dev-tools-inspector.tsx"],"sourcesContent":["import {\n byteLength,\n formatBytes,\n inspectableProps,\n jsonEntries,\n jsonScalar,\n jsonSize,\n latestServerTiming,\n prettyJson,\n searchJson,\n type JsonEntry,\n type JsonSearchResult,\n} from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { BracesIcon, CheckIcon, ChevronRightIcon, CopyIcon, ExternalLinkIcon } from 'lucide-react'\nimport { useEffect, useMemo, useState } from 'react'\nimport { useAlfakitProps, useDirection, useFormatter, useT } from '../../context'\nimport { cn } from '../../lib/utils'\nimport { Button } from '../ui/button'\nimport { Input } from '../ui/input'\nimport { ScrollArea } from '../ui/scroll-area'\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n} from '../ui/sheet'\n\n/**\n * The schema inspector: a floating developer button that opens a drawer with the page's\n * props as a searchable JSON tree (with a copy button), what the context token identifies,\n * the server time reported in `Server-Timing`, the payload size and editor links for the\n * PHP classes behind the page. Rendered only for pages that carry `alfakitDevTools`.\n */\nexport function DevToolsInspector() {\n const props = useAlfakitProps()\n const t = useT()\n const formatter = useFormatter()\n const direction = useDirection()\n const tools = props.alfakitDevTools\n const data = useMemo(() => inspectableProps(props as unknown as Record<string, unknown>), [props])\n const payload = useMemo(() => byteLength(props), [props])\n const [query, setQuery] = useState('')\n const [copied, setCopied] = useState(false)\n // The inspector renders in the browser only (lazy chunk), so the Performance API is there.\n const [serverTime, setServerTime] = useState<number | null>(() => latestServerTiming())\n const search = useMemo(() => searchJson(data, query), [data, query])\n\n // Re-read the timing after every visit: the latest response carries the newest header.\n useEffect(() => router.on('finish', () => setServerTime(latestServerTiming())), [])\n\n useEffect(() => {\n if (!copied) return\n\n const timer = setTimeout(() => setCopied(false), 2000)\n\n return () => clearTimeout(timer)\n }, [copied])\n\n if (!tools) {\n return null\n }\n\n const copy = () => {\n void navigator.clipboard?.writeText(prettyJson(data)).then(() => setCopied(true))\n }\n\n const time = (value: number | null) =>\n value === null ? t('dev_tools.never', {}, 'Never') : formatter.dateTime(value * 1000)\n const parameters = Object.entries(tools.context.parameters)\n\n return (\n <Sheet>\n <SheetTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon\"\n aria-label={t('dev_tools.open', {}, 'Open schema inspector')}\n data-slot=\"dev-tools-toggle\"\n className=\"fixed end-4 bottom-4 z-40 rounded-full shadow-md\"\n >\n <BracesIcon />\n </Button>\n </SheetTrigger>\n <SheetContent\n side={direction === 'rtl' ? 'left' : 'right'}\n className=\"flex w-full flex-col gap-0 p-0 sm:max-w-xl\"\n >\n <div data-slot=\"dev-tools\" className=\"flex min-h-0 flex-1 flex-col\">\n <SheetHeader className=\"border-b\">\n <SheetTitle className=\"flex items-center gap-2\">\n <BracesIcon className=\"size-4\" />\n {t('dev_tools.title', {}, 'Schema inspector')}\n </SheetTitle>\n <SheetDescription className=\"truncate font-mono text-xs\" title={tools.context.page}>\n {tools.context.page}\n </SheetDescription>\n </SheetHeader>\n <ScrollArea className=\"min-h-0 flex-1\">\n <div className=\"space-y-5 p-4 text-sm\">\n <dl className=\"grid grid-cols-2 gap-3\">\n <Stat\n label={t('dev_tools.server_time', {}, 'Server time')}\n value={\n serverTime === null\n ? t('dev_tools.not_measured', {}, 'Not measured')\n : t(\n 'dev_tools.milliseconds',\n { value: formatter.number(serverTime) },\n ':value ms',\n )\n }\n />\n <Stat\n label={t('dev_tools.payload', {}, 'Payload size')}\n value={formatBytes(payload)}\n />\n </dl>\n\n <section data-slot=\"dev-tools-context\">\n <h3 className=\"mb-2 text-sm font-medium\">\n {t('dev_tools.context', {}, 'Context token')}\n </h3>\n <dl className=\"grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-xs\">\n <Row label={t('dev_tools.panel', {}, 'Panel')} value={tools.context.panel} />\n <Row label={t('dev_tools.page', {}, 'Page')} value={tools.context.page} />\n {tools.context.component && (\n <Row\n label={t('dev_tools.component', {}, 'Component')}\n value={tools.context.component}\n />\n )}\n {tools.tenant !== null && (\n <Row label={t('dev_tools.tenant', {}, 'Tenant')} value={tools.tenant} />\n )}\n {parameters.map(([key, value]) => (\n <Row key={key} label={key} value={jsonScalar(value)} />\n ))}\n <Row\n label={t('dev_tools.bound_to_user', {}, 'Bound to the user')}\n value={\n tools.context.boundToUser\n ? t('dev_tools.yes', {}, 'Yes')\n : t('dev_tools.no', {}, 'No')\n }\n plain\n />\n <Row\n label={t('dev_tools.issued_at', {}, 'Issued')}\n value={time(tools.context.issuedAt)}\n plain\n />\n <Row\n label={t('dev_tools.expires_at', {}, 'Expires')}\n value={time(tools.context.expiresAt)}\n plain\n />\n </dl>\n </section>\n\n <section data-slot=\"dev-tools-classes\">\n <h3 className=\"mb-2 text-sm font-medium\">\n {t('dev_tools.classes', {}, 'Classes')}\n </h3>\n <ul className=\"divide-y rounded-md border text-xs\">\n {tools.classes.map((item) => (\n <li key={item.role} className=\"flex items-center gap-2 px-3 py-1.5\">\n <span className=\"text-muted-foreground w-16 shrink-0\">\n {t(`dev_tools.roles.${item.role}`, {}, item.role)}\n </span>\n <span\n className=\"min-w-0 flex-1 truncate font-mono\"\n title={item.file ? `${item.file}:${item.line ?? 1}` : item.class}\n >\n {item.class}\n </span>\n {item.url && (\n <a\n href={item.url}\n className=\"text-primary inline-flex shrink-0 items-center gap-1 hover:underline\"\n aria-label={t(\n 'dev_tools.open_in_editor',\n { class: item.class, editor: tools.editor ?? '' },\n 'Open :class in :editor',\n )}\n >\n <ExternalLinkIcon className=\"size-3.5\" />\n </a>\n )}\n </li>\n ))}\n </ul>\n {tools.editor === null && (\n <p className=\"text-muted-foreground mt-2 text-xs\">\n {t(\n 'dev_tools.no_editor',\n {},\n 'Set APP_EDITOR (config app.editor) to open these classes in your editor.',\n )}\n </p>\n )}\n </section>\n\n <section data-slot=\"dev-tools-schema\">\n <div className=\"mb-2 flex items-center gap-2\">\n <h3 className=\"me-auto text-sm font-medium\">\n {t('dev_tools.schema', {}, 'Schema')}\n </h3>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={copy}>\n {copied ? <CheckIcon /> : <CopyIcon />}\n {copied\n ? t('dev_tools.copied', {}, 'Copied')\n : t('dev_tools.copy', {}, 'Copy JSON')}\n </Button>\n </div>\n <Input\n type=\"search\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n placeholder={t('dev_tools.search', {}, 'Search keys and values')}\n aria-label={t('dev_tools.search', {}, 'Search keys and values')}\n className=\"mb-2 h-8\"\n />\n {query.trim() !== '' && search.matches.size === 0 ? (\n <p className=\"text-muted-foreground text-sm\">\n {t(\n 'dev_tools.no_matches',\n { query: query.trim() },\n 'Nothing matches “:query”.',\n )}\n </p>\n ) : (\n <ul\n dir=\"ltr\"\n role=\"tree\"\n aria-label={t('dev_tools.schema', {}, 'Schema')}\n className=\"bg-muted/40 overflow-x-auto rounded-md p-2 text-start font-mono text-xs leading-relaxed\"\n >\n {jsonEntries(data).map((entry) => (\n <JsonNode\n key={entry.path}\n entry={entry}\n depth={0}\n search={search}\n query={query}\n />\n ))}\n </ul>\n )}\n </section>\n </div>\n </ScrollArea>\n </div>\n </SheetContent>\n </Sheet>\n )\n}\n\nfunction JsonNode({\n entry,\n depth,\n search,\n query,\n}: {\n entry: JsonEntry\n depth: number\n search: JsonSearchResult\n query: string\n}) {\n const t = useT()\n const formatter = useFormatter()\n const searching = query.trim() !== ''\n const [open, setOpen] = useState(depth < 1)\n\n if (searching && !search.visible.has(entry.path)) {\n return null\n }\n\n const container = entry.kind === 'object' || entry.kind === 'array'\n const expanded = searching || open\n const matched = search.matches.has(entry.path)\n const size = jsonSize(entry.value)\n const key = (\n <span className={cn('text-foreground', matched && 'bg-primary/15 rounded-sm')}>\n {entry.key}\n </span>\n )\n\n if (!container) {\n return (\n <li\n role=\"treeitem\"\n aria-selected={false}\n className=\"py-px\"\n style={{ paddingInlineStart: `${depth * 12 + 16}px` }}\n >\n {key}\n <span className=\"text-muted-foreground\">: </span>\n <span\n className={cn(\n entry.kind === 'string' && 'text-emerald-700 dark:text-emerald-400',\n entry.kind === 'number' && 'text-sky-700 dark:text-sky-400',\n entry.kind === 'boolean' && 'text-violet-700 dark:text-violet-400',\n entry.kind === 'null' && 'text-muted-foreground',\n 'break-all',\n )}\n >\n {jsonScalar(entry.value)}\n </span>\n </li>\n )\n }\n\n return (\n <li role=\"treeitem\" aria-selected={false} aria-expanded={expanded}>\n <button\n type=\"button\"\n className=\"hover:bg-muted flex w-full items-center gap-1 rounded py-px text-start\"\n style={{ paddingInlineStart: `${depth * 12}px` }}\n onClick={() => setOpen(!open)}\n aria-label={\n expanded\n ? t('dev_tools.collapse', { key: entry.key }, 'Collapse :key')\n : t('dev_tools.expand', { key: entry.key }, 'Expand :key')\n }\n >\n {/* The tree is always left to right (it is code), so the chevron does not flip. */}\n <ChevronRightIcon\n className={cn('size-3 shrink-0 transition-transform', expanded && 'rotate-90')}\n />\n {key}\n <span className=\"text-muted-foreground\">\n {entry.kind === 'array' ? `[${formatter.number(size)}]` : `{${formatter.number(size)}}`}\n </span>\n </button>\n {expanded && size > 0 && (\n <ul role=\"group\">\n {jsonEntries(entry.value, entry.path).map((child) => (\n <JsonNode\n key={child.path}\n entry={child}\n depth={depth + 1}\n search={search}\n query={query}\n />\n ))}\n </ul>\n )}\n </li>\n )\n}\n\nfunction Stat({ label, value }: { label: string; value: string }) {\n return (\n <div className=\"rounded-md border p-3\">\n <dt className=\"text-muted-foreground text-xs\">{label}</dt>\n <dd className=\"font-mono text-sm font-semibold tabular-nums\">{value}</dd>\n </div>\n )\n}\n\nfunction Row({ label, value, plain = false }: { label: string; value: string; plain?: boolean }) {\n return (\n <>\n <dt className=\"text-muted-foreground\">{label}</dt>\n <dd className={cn('min-w-0 truncate', !plain && 'font-mono')} title={value}>\n {value}\n </dd>\n </>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoCA,SAAgB,oBAAoB;CAClC,MAAM,QAAQ,gBAAgB;CAC9B,MAAM,IAAI,KAAK;CACf,MAAM,YAAY,aAAa;CAC/B,MAAM,YAAY,aAAa;CAC/B,MAAM,QAAQ,MAAM;CACpB,MAAM,OAAO,cAAc,iBAAiB,KAA2C,GAAG,CAAC,KAAK,CAAC;CACjG,MAAM,UAAU,cAAc,WAAW,KAAK,GAAG,CAAC,KAAK,CAAC;CACxD,MAAM,CAAC,OAAO,YAAY,SAAS,EAAE;CACrC,MAAM,CAAC,QAAQ,aAAa,SAAS,KAAK;CAE1C,MAAM,CAAC,YAAY,iBAAiB,eAA8B,mBAAmB,CAAC;CACtF,MAAM,SAAS,cAAc,WAAW,MAAM,KAAK,GAAG,CAAC,MAAM,KAAK,CAAC;CAGnE,gBAAgB,OAAO,GAAG,gBAAgB,cAAc,mBAAmB,CAAC,CAAC,GAAG,CAAC,CAAC;CAElF,gBAAgB;EACd,IAAI,CAAC,QAAQ;EAEb,MAAM,QAAQ,iBAAiB,UAAU,KAAK,GAAG,GAAI;EAErD,aAAa,aAAa,KAAK;CACjC,GAAG,CAAC,MAAM,CAAC;CAEX,IAAI,CAAC,OACH,OAAO;CAGT,MAAM,aAAa;EACjB,UAAe,WAAW,UAAU,WAAW,IAAI,CAAC,CAAC,CAAC,WAAW,UAAU,IAAI,CAAC;CAClF;CAEA,MAAM,QAAQ,UACZ,UAAU,OAAO,EAAE,mBAAmB,CAAC,GAAG,OAAO,IAAI,UAAU,SAAS,QAAQ,GAAI;CACtF,MAAM,aAAa,OAAO,QAAQ,MAAM,QAAQ,UAAU;CAE1D,OACE,qBAAC,OAAD,EAAA,UAAA,CACE,oBAAC,cAAD;EAAc,SAAA;EACZ,UAAA,oBAAC,QAAD;GACE,MAAK;GACL,SAAQ;GACR,MAAK;GACL,cAAY,EAAE,kBAAkB,CAAC,GAAG,uBAAuB;GAC3D,aAAU;GACV,WAAU;GAEV,UAAA,oBAAC,YAAD,CAAa,CAAA;EACP,CAAA;CACI,CAAA,GACd,oBAAC,cAAD;EACE,MAAM,cAAc,QAAQ,SAAS;EACrC,WAAU;EAEV,UAAA,qBAAC,OAAD;GAAK,aAAU;GAAY,WAAU;GAArC,UAAA,CACE,qBAAC,aAAD;IAAa,WAAU;IAAvB,UAAA,CACE,qBAAC,YAAD;KAAY,WAAU;KAAtB,UAAA,CACE,oBAAC,YAAD,EAAY,WAAU,SAAU,CAAA,GAC/B,EAAE,mBAAmB,CAAC,GAAG,kBAAkB,CAClC;IACZ,CAAA,GAAA,oBAAC,kBAAD;KAAkB,WAAU;KAA6B,OAAO,MAAM,QAAQ;KAC3E,UAAA,MAAM,QAAQ;IACC,CAAA,CACP;GACb,CAAA,GAAA,oBAAC,YAAD;IAAY,WAAU;IACpB,UAAA,qBAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MACE,qBAAC,MAAD;OAAI,WAAU;OAAd,UAAA,CACE,oBAAC,MAAD;QACE,OAAO,EAAE,yBAAyB,CAAC,GAAG,aAAa;QACnD,OACE,eAAe,OACX,EAAE,0BAA0B,CAAC,GAAG,cAAc,IAC9C,EACE,0BACA,EAAE,OAAO,UAAU,OAAO,UAAU,EAAE,GACtC,WACF;OAEP,CAAA,GACD,oBAAC,MAAD;QACE,OAAO,EAAE,qBAAqB,CAAC,GAAG,cAAc;QAChD,OAAO,YAAY,OAAO;OAC3B,CAAA,CACC;;MAEJ,qBAAC,WAAD;OAAS,aAAU;OAAnB,UAAA,CACE,oBAAC,MAAD;QAAI,WAAU;QACX,UAAA,EAAE,qBAAqB,CAAC,GAAG,eAAe;OACzC,CAAA,GACJ,qBAAC,MAAD;QAAI,WAAU;QAAd,UAAA;SACE,oBAAC,KAAD;UAAK,OAAO,EAAE,mBAAmB,CAAC,GAAG,OAAO;UAAG,OAAO,MAAM,QAAQ;SAAQ,CAAA;SAC5E,oBAAC,KAAD;UAAK,OAAO,EAAE,kBAAkB,CAAC,GAAG,MAAM;UAAG,OAAO,MAAM,QAAQ;SAAO,CAAA;SACxE,MAAM,QAAQ,aACb,oBAAC,KAAD;UACE,OAAO,EAAE,uBAAuB,CAAC,GAAG,WAAW;UAC/C,OAAO,MAAM,QAAQ;SACtB,CAAA;SAEF,MAAM,WAAW,QAChB,oBAAC,KAAD;UAAK,OAAO,EAAE,oBAAoB,CAAC,GAAG,QAAQ;UAAG,OAAO,MAAM;SAAS,CAAA;SAExE,WAAW,KAAK,CAAC,KAAK,WACrB,oBAAC,KAAD;UAAe,OAAO;UAAK,OAAO,WAAW,KAAK;SAAI,GAA5C,GAA4C,CACvD;SACD,oBAAC,KAAD;UACE,OAAO,EAAE,2BAA2B,CAAC,GAAG,mBAAmB;UAC3D,OACE,MAAM,QAAQ,cACV,EAAE,iBAAiB,CAAC,GAAG,KAAK,IAC5B,EAAE,gBAAgB,CAAC,GAAG,IAAI;UAEhC,OAAA;SACD,CAAA;SACD,oBAAC,KAAD;UACE,OAAO,EAAE,uBAAuB,CAAC,GAAG,QAAQ;UAC5C,OAAO,KAAK,MAAM,QAAQ,QAAQ;UAClC,OAAA;SACD,CAAA;SACD,oBAAC,KAAD;UACE,OAAO,EAAE,wBAAwB,CAAC,GAAG,SAAS;UAC9C,OAAO,KAAK,MAAM,QAAQ,SAAS;UACnC,OAAA;SACD,CAAA;QACC;OACG,CAAA,CAAA;;MAET,qBAAC,WAAD;OAAS,aAAU;OAAnB,UAAA;QACE,oBAAC,MAAD;SAAI,WAAU;SACX,UAAA,EAAE,qBAAqB,CAAC,GAAG,SAAS;QACnC,CAAA;QACJ,oBAAC,MAAD;SAAI,WAAU;SACX,UAAA,MAAM,QAAQ,KAAK,SAClB,qBAAC,MAAD;UAAoB,WAAU;UAA9B,UAAA;WACE,oBAAC,QAAD;YAAM,WAAU;YACb,UAAA,EAAE,mBAAmB,KAAK,QAAQ,CAAC,GAAG,KAAK,IAAI;WAC5C,CAAA;WACN,oBAAC,QAAD;YACE,WAAU;YACV,OAAO,KAAK,OAAO,GAAG,KAAK,KAAK,GAAG,KAAK,QAAQ,MAAM,KAAK;YAE1D,UAAA,KAAK;WACF,CAAA;WACL,KAAK,OACJ,oBAAC,KAAD;YACE,MAAM,KAAK;YACX,WAAU;YACV,cAAY,EACV,4BACA;aAAE,OAAO,KAAK;aAAO,QAAQ,MAAM,UAAU;YAAG,GAChD,wBACF;YAEA,UAAA,oBAAC,kBAAD,EAAkB,WAAU,WAAY,CAAA;WACvC,CAAA;UAEH;SAvBK,GAAA,KAAK,IAuBV,CACL;QACC,CAAA;QACH,MAAM,WAAW,QAChB,oBAAC,KAAD;SAAG,WAAU;SACV,UAAA,EACC,uBACA,CAAC,GACD,0EACF;QACC,CAAA;OAEE;;MAET,qBAAC,WAAD;OAAS,aAAU;OAAnB,UAAA;QACE,qBAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CACE,oBAAC,MAAD;UAAI,WAAU;UACX,UAAA,EAAE,oBAAoB,CAAC,GAAG,QAAQ;SACjC,CAAA,GACJ,qBAAC,QAAD;UAAQ,MAAK;UAAS,SAAQ;UAAQ,MAAK;UAAK,SAAS;UAAzD,UAAA,CACG,SAAS,oBAAC,WAAD,CAAY,CAAA,IAAI,oBAAC,UAAD,CAAW,CAAA,GACpC,SACG,EAAE,oBAAoB,CAAC,GAAG,QAAQ,IAClC,EAAE,kBAAkB,CAAC,GAAG,WAAW,CACjC;SACL,CAAA,CAAA;;QACL,oBAAC,OAAD;SACE,MAAK;SACL,OAAO;SACP,WAAW,UAAU,SAAS,MAAM,OAAO,KAAK;SAChD,aAAa,EAAE,oBAAoB,CAAC,GAAG,wBAAwB;SAC/D,cAAY,EAAE,oBAAoB,CAAC,GAAG,wBAAwB;SAC9D,WAAU;QACX,CAAA;QACA,MAAM,KAAK,MAAM,MAAM,OAAO,QAAQ,SAAS,IAC9C,oBAAC,KAAD;SAAG,WAAU;SACV,UAAA,EACC,wBACA,EAAE,OAAO,MAAM,KAAK,EAAE,GACtB,2BACF;QACC,CAAA,IAEH,oBAAC,MAAD;SACE,KAAI;SACJ,MAAK;SACL,cAAY,EAAE,oBAAoB,CAAC,GAAG,QAAQ;SAC9C,WAAU;SAET,UAAA,YAAY,IAAI,CAAC,CAAC,KAAK,UACtB,oBAAC,UAAD;UAES;UACP,OAAO;UACC;UACD;SACR,GALM,MAAM,IAKZ,CACF;QACC,CAAA;OAEC;;KACN;;GACK,CAAA,CACT;;CACO,CAAA,CACT,EAAA,CAAA;AAEX;AAEA,SAAS,SAAS,EAChB,OACA,OACA,QACA,SAMC;CACD,MAAM,IAAI,KAAK;CACf,MAAM,YAAY,aAAa;CAC/B,MAAM,YAAY,MAAM,KAAK,MAAM;CACnC,MAAM,CAAC,MAAM,WAAW,SAAS,QAAQ,CAAC;CAE1C,IAAI,aAAa,CAAC,OAAO,QAAQ,IAAI,MAAM,IAAI,GAC7C,OAAO;CAGT,MAAM,YAAY,MAAM,SAAS,YAAY,MAAM,SAAS;CAC5D,MAAM,WAAW,aAAa;CAC9B,MAAM,UAAU,OAAO,QAAQ,IAAI,MAAM,IAAI;CAC7C,MAAM,OAAO,SAAS,MAAM,KAAK;CACjC,MAAM,MACJ,oBAAC,QAAD;EAAM,WAAW,GAAG,mBAAmB,WAAW,0BAA0B;EACzE,UAAA,MAAM;CACH,CAAA;CAGR,IAAI,CAAC,WACH,OACE,qBAAC,MAAD;EACE,MAAK;EACL,iBAAe;EACf,WAAU;EACV,OAAO,EAAE,oBAAoB,GAAG,QAAQ,KAAK,GAAG,IAAI;EAJtD,UAAA;GAMG;GACD,oBAAC,QAAD;IAAM,WAAU;IAAwB,UAAA;GAAQ,CAAA;GAChD,oBAAC,QAAD;IACE,WAAW,GACT,MAAM,SAAS,YAAY,0CAC3B,MAAM,SAAS,YAAY,kCAC3B,MAAM,SAAS,aAAa,wCAC5B,MAAM,SAAS,UAAU,yBACzB,WACF;IAEC,UAAA,WAAW,MAAM,KAAK;GACnB,CAAA;EACJ;;CAIR,OACE,qBAAC,MAAD;EAAI,MAAK;EAAW,iBAAe;EAAO,iBAAe;EAAzD,UAAA,CACE,qBAAC,UAAD;GACE,MAAK;GACL,WAAU;GACV,OAAO,EAAE,oBAAoB,GAAG,QAAQ,GAAG,IAAI;GAC/C,eAAe,QAAQ,CAAC,IAAI;GAC5B,cACE,WACI,EAAE,sBAAsB,EAAE,KAAK,MAAM,IAAI,GAAG,eAAe,IAC3D,EAAE,oBAAoB,EAAE,KAAK,MAAM,IAAI,GAAG,aAAa;GAR/D,UAAA;IAYE,oBAAC,kBAAD,EACE,WAAW,GAAG,wCAAwC,YAAY,WAAW,EAC9E,CAAA;IACA;IACD,oBAAC,QAAD;KAAM,WAAU;KACb,UAAA,MAAM,SAAS,UAAU,IAAI,UAAU,OAAO,IAAI,EAAE,KAAK,IAAI,UAAU,OAAO,IAAI,EAAE;IACjF,CAAA;GACA;EACP,CAAA,GAAA,YAAY,OAAO,KAClB,oBAAC,MAAD;GAAI,MAAK;GACN,UAAA,YAAY,MAAM,OAAO,MAAM,IAAI,CAAC,CAAC,KAAK,UACzC,oBAAC,UAAD;IAEE,OAAO;IACP,OAAO,QAAQ;IACP;IACD;GACR,GALM,MAAM,IAKZ,CACF;EACC,CAAA,CAEJ;;AAER;AAEA,SAAS,KAAK,EAAE,OAAO,SAA2C;CAChE,OACE,qBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,oBAAC,MAAD;GAAI,WAAU;GAAiC,UAAA;EAAU,CAAA,GACzD,oBAAC,MAAD;GAAI,WAAU;GAAgD,UAAA;EAAU,CAAA,CACrE;;AAET;AAEA,SAAS,IAAI,EAAE,OAAO,OAAO,QAAQ,SAA4D;CAC/F,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,MAAD;EAAI,WAAU;EAAyB,UAAA;CAAU,CAAA,GACjD,oBAAC,MAAD;EAAI,WAAW,GAAG,oBAAoB,CAAC,SAAS,WAAW;EAAG,OAAO;EAClE,UAAA;CACC,CAAA,CACJ,EAAA,CAAA;AAEN"}
|
|
1
|
+
{"version":3,"file":"dev-tools-inspector.js","names":[],"sources":["../../../src/components/dev-tools/dev-tools-inspector.tsx"],"sourcesContent":["import {\n byteLength,\n formatBytes,\n inspectableProps,\n jsonEntries,\n jsonScalar,\n jsonSize,\n latestServerTiming,\n prettyJson,\n searchJson,\n type JsonEntry,\n type JsonSearchResult,\n} from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { BracesIcon, CheckIcon, ChevronRightIcon, CopyIcon, ExternalLinkIcon } from 'lucide-react'\nimport { useEffect, useMemo, useState } from 'react'\nimport { useAlfakitProps, useDirection, useFormatter, useT } from '../../context'\nimport { cn } from '../../lib/utils'\nimport { Button } from '../ui/button'\nimport { Input } from '../ui/input'\nimport { ScrollArea } from '../ui/scroll-area'\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n} from '../ui/sheet'\n\n/**\n * The schema inspector: a floating developer button that opens a drawer with the page's\n * props as a searchable JSON tree (with a copy button), what the context token identifies,\n * the server time reported in `Server-Timing`, the payload size and editor links for the\n * PHP classes behind the page. Rendered only for pages that carry `alfakitDevTools`.\n */\nexport function DevToolsInspector() {\n const props = useAlfakitProps()\n const t = useT()\n const formatter = useFormatter()\n const direction = useDirection()\n const tools = props.alfakitDevTools\n const data = useMemo(() => inspectableProps(props as unknown as Record<string, unknown>), [props])\n const payload = useMemo(() => byteLength(props), [props])\n const [query, setQuery] = useState('')\n const [copied, setCopied] = useState(false)\n // The inspector renders in the browser only (lazy chunk), so the Performance API is there.\n const [serverTime, setServerTime] = useState<number | null>(() => latestServerTiming())\n const search = useMemo(() => searchJson(data, query), [data, query])\n\n // Re-read the timing after every visit: the latest response carries the newest header.\n useEffect(() => router.on('finish', () => setServerTime(latestServerTiming())), [])\n\n useEffect(() => {\n if (!copied) return\n\n const timer = setTimeout(() => setCopied(false), 2000)\n\n return () => clearTimeout(timer)\n }, [copied])\n\n if (!tools) {\n return null\n }\n\n const copy = () => {\n void navigator.clipboard?.writeText(prettyJson(data)).then(() => setCopied(true))\n }\n\n const time = (value: number | null) =>\n value === null ? t('dev_tools.never', {}, 'Never') : formatter.dateTime(value * 1000)\n const parameters = Object.entries(tools.context.parameters)\n\n return (\n <Sheet>\n <SheetTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon\"\n aria-label={t('dev_tools.open', {}, 'Open schema inspector')}\n data-slot=\"dev-tools-toggle\"\n className=\"fixed end-4 bottom-16 z-40 rounded-full shadow-md [[data-slot=starter-kit-root]_&]:end-auto [[data-slot=starter-kit-root]_&]:right-4\"\n >\n <BracesIcon />\n </Button>\n </SheetTrigger>\n <SheetContent\n side={direction === 'rtl' ? 'left' : 'right'}\n className=\"flex w-full flex-col gap-0 p-0 sm:max-w-xl\"\n >\n <div data-slot=\"dev-tools\" className=\"flex min-h-0 flex-1 flex-col\">\n <SheetHeader className=\"border-b\">\n <SheetTitle className=\"flex items-center gap-2\">\n <BracesIcon className=\"size-4\" />\n {t('dev_tools.title', {}, 'Schema inspector')}\n </SheetTitle>\n <SheetDescription className=\"truncate font-mono text-xs\" title={tools.context.page}>\n {tools.context.page}\n </SheetDescription>\n </SheetHeader>\n <ScrollArea className=\"min-h-0 flex-1\">\n <div className=\"space-y-5 p-4 text-sm\">\n <dl className=\"grid grid-cols-2 gap-3\">\n <Stat\n label={t('dev_tools.server_time', {}, 'Server time')}\n value={\n serverTime === null\n ? t('dev_tools.not_measured', {}, 'Not measured')\n : t(\n 'dev_tools.milliseconds',\n { value: formatter.number(serverTime) },\n ':value ms',\n )\n }\n />\n <Stat\n label={t('dev_tools.payload', {}, 'Payload size')}\n value={formatBytes(payload)}\n />\n </dl>\n\n <section data-slot=\"dev-tools-context\">\n <h3 className=\"mb-2 text-sm font-medium\">\n {t('dev_tools.context', {}, 'Context token')}\n </h3>\n <dl className=\"grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-xs\">\n <Row label={t('dev_tools.panel', {}, 'Panel')} value={tools.context.panel} />\n <Row label={t('dev_tools.page', {}, 'Page')} value={tools.context.page} />\n {tools.context.component && (\n <Row\n label={t('dev_tools.component', {}, 'Component')}\n value={tools.context.component}\n />\n )}\n {tools.tenant !== null && (\n <Row label={t('dev_tools.tenant', {}, 'Tenant')} value={tools.tenant} />\n )}\n {parameters.map(([key, value]) => (\n <Row key={key} label={key} value={jsonScalar(value)} />\n ))}\n <Row\n label={t('dev_tools.bound_to_user', {}, 'Bound to the user')}\n value={\n tools.context.boundToUser\n ? t('dev_tools.yes', {}, 'Yes')\n : t('dev_tools.no', {}, 'No')\n }\n plain\n />\n <Row\n label={t('dev_tools.issued_at', {}, 'Issued')}\n value={time(tools.context.issuedAt)}\n plain\n />\n <Row\n label={t('dev_tools.expires_at', {}, 'Expires')}\n value={time(tools.context.expiresAt)}\n plain\n />\n </dl>\n </section>\n\n <section data-slot=\"dev-tools-classes\">\n <h3 className=\"mb-2 text-sm font-medium\">\n {t('dev_tools.classes', {}, 'Classes')}\n </h3>\n <ul className=\"divide-y rounded-md border text-xs\">\n {tools.classes.map((item) => (\n <li key={item.role} className=\"flex items-center gap-2 px-3 py-1.5\">\n <span className=\"text-muted-foreground w-16 shrink-0\">\n {t(`dev_tools.roles.${item.role}`, {}, item.role)}\n </span>\n <span\n className=\"min-w-0 flex-1 truncate font-mono\"\n title={item.file ? `${item.file}:${item.line ?? 1}` : item.class}\n >\n {item.class}\n </span>\n {item.url && (\n <a\n href={item.url}\n className=\"text-primary inline-flex shrink-0 items-center gap-1 hover:underline\"\n aria-label={t(\n 'dev_tools.open_in_editor',\n { class: item.class, editor: tools.editor ?? '' },\n 'Open :class in :editor',\n )}\n >\n <ExternalLinkIcon className=\"size-3.5\" />\n </a>\n )}\n </li>\n ))}\n </ul>\n {tools.editor === null && (\n <p className=\"text-muted-foreground mt-2 text-xs\">\n {t(\n 'dev_tools.no_editor',\n {},\n 'Set APP_EDITOR (config app.editor) to open these classes in your editor.',\n )}\n </p>\n )}\n </section>\n\n <section data-slot=\"dev-tools-schema\">\n <div className=\"mb-2 flex items-center gap-2\">\n <h3 className=\"me-auto text-sm font-medium\">\n {t('dev_tools.schema', {}, 'Schema')}\n </h3>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={copy}>\n {copied ? <CheckIcon /> : <CopyIcon />}\n {copied\n ? t('dev_tools.copied', {}, 'Copied')\n : t('dev_tools.copy', {}, 'Copy JSON')}\n </Button>\n </div>\n <Input\n type=\"search\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n placeholder={t('dev_tools.search', {}, 'Search keys and values')}\n aria-label={t('dev_tools.search', {}, 'Search keys and values')}\n className=\"mb-2 h-8\"\n />\n {query.trim() !== '' && search.matches.size === 0 ? (\n <p className=\"text-muted-foreground text-sm\">\n {t(\n 'dev_tools.no_matches',\n { query: query.trim() },\n 'Nothing matches “:query”.',\n )}\n </p>\n ) : (\n <ul\n dir=\"ltr\"\n role=\"tree\"\n aria-label={t('dev_tools.schema', {}, 'Schema')}\n className=\"bg-muted/40 overflow-x-auto rounded-md p-2 text-start font-mono text-xs leading-relaxed\"\n >\n {jsonEntries(data).map((entry) => (\n <JsonNode\n key={entry.path}\n entry={entry}\n depth={0}\n search={search}\n query={query}\n />\n ))}\n </ul>\n )}\n </section>\n </div>\n </ScrollArea>\n </div>\n </SheetContent>\n </Sheet>\n )\n}\n\nfunction JsonNode({\n entry,\n depth,\n search,\n query,\n}: {\n entry: JsonEntry\n depth: number\n search: JsonSearchResult\n query: string\n}) {\n const t = useT()\n const formatter = useFormatter()\n const searching = query.trim() !== ''\n const [open, setOpen] = useState(depth < 1)\n\n if (searching && !search.visible.has(entry.path)) {\n return null\n }\n\n const container = entry.kind === 'object' || entry.kind === 'array'\n const expanded = searching || open\n const matched = search.matches.has(entry.path)\n const size = jsonSize(entry.value)\n const key = (\n <span className={cn('text-foreground', matched && 'bg-primary/15 rounded-sm')}>\n {entry.key}\n </span>\n )\n\n if (!container) {\n return (\n <li\n role=\"treeitem\"\n aria-selected={false}\n className=\"py-px\"\n style={{ paddingInlineStart: `${depth * 12 + 16}px` }}\n >\n {key}\n <span className=\"text-muted-foreground\">: </span>\n <span\n className={cn(\n entry.kind === 'string' && 'text-emerald-700 dark:text-emerald-400',\n entry.kind === 'number' && 'text-sky-700 dark:text-sky-400',\n entry.kind === 'boolean' && 'text-violet-700 dark:text-violet-400',\n entry.kind === 'null' && 'text-muted-foreground',\n 'break-all',\n )}\n >\n {jsonScalar(entry.value)}\n </span>\n </li>\n )\n }\n\n return (\n <li role=\"treeitem\" aria-selected={false} aria-expanded={expanded}>\n <button\n type=\"button\"\n className=\"hover:bg-muted flex w-full items-center gap-1 rounded py-px text-start\"\n style={{ paddingInlineStart: `${depth * 12}px` }}\n onClick={() => setOpen(!open)}\n aria-label={\n expanded\n ? t('dev_tools.collapse', { key: entry.key }, 'Collapse :key')\n : t('dev_tools.expand', { key: entry.key }, 'Expand :key')\n }\n >\n {/* The tree is always left to right (it is code), so the chevron does not flip. */}\n <ChevronRightIcon\n className={cn('size-3 shrink-0 transition-transform', expanded && 'rotate-90')}\n />\n {key}\n <span className=\"text-muted-foreground\">\n {entry.kind === 'array' ? `[${formatter.number(size)}]` : `{${formatter.number(size)}}`}\n </span>\n </button>\n {expanded && size > 0 && (\n <ul role=\"group\">\n {jsonEntries(entry.value, entry.path).map((child) => (\n <JsonNode\n key={child.path}\n entry={child}\n depth={depth + 1}\n search={search}\n query={query}\n />\n ))}\n </ul>\n )}\n </li>\n )\n}\n\nfunction Stat({ label, value }: { label: string; value: string }) {\n return (\n <div className=\"rounded-md border p-3\">\n <dt className=\"text-muted-foreground text-xs\">{label}</dt>\n <dd className=\"font-mono text-sm font-semibold tabular-nums\">{value}</dd>\n </div>\n )\n}\n\nfunction Row({ label, value, plain = false }: { label: string; value: string; plain?: boolean }) {\n return (\n <>\n <dt className=\"text-muted-foreground\">{label}</dt>\n <dd className={cn('min-w-0 truncate', !plain && 'font-mono')} title={value}>\n {value}\n </dd>\n </>\n )\n}\n"],"mappings":"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"}
|
|
@@ -52,7 +52,7 @@ function CheckboxListField({ component }) {
|
|
|
52
52
|
className: "flex flex-col gap-2",
|
|
53
53
|
children: [
|
|
54
54
|
(component.searchable || component.bulkToggleable && !readOnly) && /* @__PURE__ */ jsxs("div", {
|
|
55
|
-
className: "flex items-center gap-2",
|
|
55
|
+
className: "flex min-w-0 flex-wrap items-center gap-2",
|
|
56
56
|
children: [component.searchable && /* @__PURE__ */ jsx(Input, {
|
|
57
57
|
value: search,
|
|
58
58
|
onChange: (event) => setSearch(event.target.value),
|
|
@@ -60,7 +60,7 @@ function CheckboxListField({ component }) {
|
|
|
60
60
|
"aria-label": component.searchPrompt ?? t("search", {}, "Search"),
|
|
61
61
|
className: "h-8"
|
|
62
62
|
}), component.bulkToggleable && !readOnly && /* @__PURE__ */ jsxs("div", {
|
|
63
|
-
className: "flex gap-1",
|
|
63
|
+
className: "flex flex-wrap gap-1",
|
|
64
64
|
children: [!selectAll?.hidden && /* @__PURE__ */ jsxs(Button, {
|
|
65
65
|
type: "button",
|
|
66
66
|
variant: "link",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkbox-list.js","names":[],"sources":["../../../src/components/fields/checkbox-list.tsx"],"sourcesContent":["import type { CheckboxListSchema } from '@alfakit/core'\nimport { fieldIds, gridColumnsClasses, htmlLabelToText, isOptionGroup } from '@alfakit/core'\nimport { useEffect, useMemo, useState } from 'react'\nimport { useT } from '../../context'\nimport { textColorClasses } from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { useField } from '../form/form-context'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { Button } from '../ui/button'\nimport { Checkbox } from '../ui/checkbox'\nimport { Input } from '../ui/input'\nimport { Label } from '../ui/label'\n\n/**\n * `gridDirection('column')`: options flow down each column (CSS columns). Classes are\n * spelled out so Tailwind generates them.\n */\nconst COLUMN_FLOW_CLASSES: Record<number, string> = {\n 2: 'md:columns-2',\n 3: 'md:columns-3',\n 4: 'md:columns-4',\n 5: 'md:columns-5',\n 6: 'md:columns-6',\n}\n\nexport function CheckboxListField({ component }: { component: CheckboxListSchema }) {\n const t = useT()\n const { value, setValue, error, id, disabled, readOnly, aria } = useField<Array<\n string | number\n > | null>(component)\n const [search, setSearch] = useState('')\n // `searchDebounce()`: the list is filtered by `query`, which trails the input.\n const [debounced, setDebounced] = useState('')\n const query = component.searchDebounce ? debounced : search\n const locked = disabled || readOnly\n\n const selected = useMemo(() => (value ?? []).map(String), [value])\n const options = useMemo(\n () =>\n component.options.flatMap((option) => (isOptionGroup(option) ? option.options : [option])),\n [component.options],\n )\n const visible = options.filter((option) =>\n (component.allowHtml ? htmlLabelToText(option.label) : option.label)\n .toLowerCase()\n .includes(query.toLowerCase()),\n )\n\n useEffect(() => {\n if (!component.searchDebounce) return\n\n const timer = setTimeout(() => setDebounced(search), component.searchDebounce)\n\n return () => clearTimeout(timer)\n }, [search, component.searchDebounce])\n const columnFlow = component.gridDirection === 'column' && (component.gridColumns ?? 1) > 1\n const selectAll = component.selectAllAction\n const deselectAll = component.deselectAllAction\n\n const toggle = (key: string, checked: boolean) => {\n setValue(checked ? [...selected, key] : selected.filter((item) => item !== key))\n }\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div className=\"flex flex-col gap-2\">\n {(component.searchable || (component.bulkToggleable && !readOnly)) && (\n <div className=\"flex items-center gap-2\">\n {component.searchable && (\n <Input\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n placeholder={component.searchPrompt ?? t('search', {}, 'Search')}\n aria-label={component.searchPrompt ?? t('search', {}, 'Search')}\n className=\"h-8\"\n />\n )}\n {component.bulkToggleable && !readOnly && (\n <div className=\"flex gap-1\">\n {!selectAll?.hidden && (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n data-slot=\"select-all\"\n disabled={disabled}\n onClick={() =>\n setValue(\n options.filter((option) => !option.disabled).map((option) => option.value),\n )\n }\n >\n {selectAll?.icon && <Icon name={selectAll.icon} />}\n {selectAll?.label ?? t('table.select_all', {}, 'Select all')}\n </Button>\n )}\n {!deselectAll?.hidden && (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n data-slot=\"deselect-all\"\n disabled={disabled}\n onClick={() => setValue([])}\n >\n {deselectAll?.icon && <Icon name={deselectAll.icon} />}\n {deselectAll?.label ?? t('table.deselect_all', {}, 'Deselect all')}\n </Button>\n )}\n </div>\n )}\n </div>\n )}\n <div\n data-grid-direction={component.gridDirection}\n className={cn(\n component.inline\n ? 'flex flex-wrap gap-4'\n : columnFlow\n ? cn(\n 'gap-x-3 *:mb-3 *:break-inside-avoid',\n COLUMN_FLOW_CLASSES[component.gridColumns ?? 1],\n )\n : 'grid gap-3',\n !component.inline &&\n !columnFlow &&\n gridColumnsClasses(\n component.gridColumns ? { default: 1, md: component.gridColumns } : { default: 1 },\n ),\n )}\n role=\"group\"\n aria-labelledby={component.label ? fieldIds(id).label : undefined}\n aria-describedby={aria['aria-describedby']}\n >\n {visible.map((option) => {\n const optionId = `${id}-${option.value}`\n\n return (\n <div key={option.value} className=\"flex items-start gap-2\">\n <Checkbox\n id={optionId}\n checked={selected.includes(option.value)}\n disabled={locked || option.disabled}\n onCheckedChange={(checked) => toggle(option.value, checked === true)}\n aria-invalid={aria['aria-invalid']}\n className=\"mt-0.5\"\n />\n <div className=\"grid gap-0.5\">\n <Label\n htmlFor={optionId}\n className={cn('font-normal', option.color && textColorClasses[option.color])}\n >\n {option.icon && <Icon name={option.icon} className=\"size-4\" />}\n {component.allowHtml ? (\n <span dangerouslySetInnerHTML={{ __html: option.label }} />\n ) : (\n option.label\n )}\n </Label>\n {option.description && (\n <p className=\"text-muted-foreground text-xs\">{option.description}</p>\n )}\n </div>\n </div>\n )\n })}\n </div>\n {component.searchable && query !== '' && visible.length === 0 && (\n <p\n data-slot=\"checkbox-list-empty\"\n role=\"status\"\n className=\"text-muted-foreground text-sm\"\n >\n {component.noSearchResultsMessage ?? t('no_results', {}, 'No results.')}\n </p>\n )}\n </div>\n </FieldWrapper>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"checkbox-list.js","names":[],"sources":["../../../src/components/fields/checkbox-list.tsx"],"sourcesContent":["import type { CheckboxListSchema } from '@alfakit/core'\nimport { fieldIds, gridColumnsClasses, htmlLabelToText, isOptionGroup } from '@alfakit/core'\nimport { useEffect, useMemo, useState } from 'react'\nimport { useT } from '../../context'\nimport { textColorClasses } from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { useField } from '../form/form-context'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { Button } from '../ui/button'\nimport { Checkbox } from '../ui/checkbox'\nimport { Input } from '../ui/input'\nimport { Label } from '../ui/label'\n\n/**\n * `gridDirection('column')`: options flow down each column (CSS columns). Classes are\n * spelled out so Tailwind generates them.\n */\nconst COLUMN_FLOW_CLASSES: Record<number, string> = {\n 2: 'md:columns-2',\n 3: 'md:columns-3',\n 4: 'md:columns-4',\n 5: 'md:columns-5',\n 6: 'md:columns-6',\n}\n\nexport function CheckboxListField({ component }: { component: CheckboxListSchema }) {\n const t = useT()\n const { value, setValue, error, id, disabled, readOnly, aria } = useField<Array<\n string | number\n > | null>(component)\n const [search, setSearch] = useState('')\n // `searchDebounce()`: the list is filtered by `query`, which trails the input.\n const [debounced, setDebounced] = useState('')\n const query = component.searchDebounce ? debounced : search\n const locked = disabled || readOnly\n\n const selected = useMemo(() => (value ?? []).map(String), [value])\n const options = useMemo(\n () =>\n component.options.flatMap((option) => (isOptionGroup(option) ? option.options : [option])),\n [component.options],\n )\n const visible = options.filter((option) =>\n (component.allowHtml ? htmlLabelToText(option.label) : option.label)\n .toLowerCase()\n .includes(query.toLowerCase()),\n )\n\n useEffect(() => {\n if (!component.searchDebounce) return\n\n const timer = setTimeout(() => setDebounced(search), component.searchDebounce)\n\n return () => clearTimeout(timer)\n }, [search, component.searchDebounce])\n const columnFlow = component.gridDirection === 'column' && (component.gridColumns ?? 1) > 1\n const selectAll = component.selectAllAction\n const deselectAll = component.deselectAllAction\n\n const toggle = (key: string, checked: boolean) => {\n setValue(checked ? [...selected, key] : selected.filter((item) => item !== key))\n }\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div className=\"flex flex-col gap-2\">\n {(component.searchable || (component.bulkToggleable && !readOnly)) && (\n <div className=\"flex min-w-0 flex-wrap items-center gap-2\">\n {component.searchable && (\n <Input\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n placeholder={component.searchPrompt ?? t('search', {}, 'Search')}\n aria-label={component.searchPrompt ?? t('search', {}, 'Search')}\n className=\"h-8\"\n />\n )}\n {component.bulkToggleable && !readOnly && (\n <div className=\"flex flex-wrap gap-1\">\n {!selectAll?.hidden && (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n data-slot=\"select-all\"\n disabled={disabled}\n onClick={() =>\n setValue(\n options.filter((option) => !option.disabled).map((option) => option.value),\n )\n }\n >\n {selectAll?.icon && <Icon name={selectAll.icon} />}\n {selectAll?.label ?? t('table.select_all', {}, 'Select all')}\n </Button>\n )}\n {!deselectAll?.hidden && (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n data-slot=\"deselect-all\"\n disabled={disabled}\n onClick={() => setValue([])}\n >\n {deselectAll?.icon && <Icon name={deselectAll.icon} />}\n {deselectAll?.label ?? t('table.deselect_all', {}, 'Deselect all')}\n </Button>\n )}\n </div>\n )}\n </div>\n )}\n <div\n data-grid-direction={component.gridDirection}\n className={cn(\n component.inline\n ? 'flex flex-wrap gap-4'\n : columnFlow\n ? cn(\n 'gap-x-3 *:mb-3 *:break-inside-avoid',\n COLUMN_FLOW_CLASSES[component.gridColumns ?? 1],\n )\n : 'grid gap-3',\n !component.inline &&\n !columnFlow &&\n gridColumnsClasses(\n component.gridColumns ? { default: 1, md: component.gridColumns } : { default: 1 },\n ),\n )}\n role=\"group\"\n aria-labelledby={component.label ? fieldIds(id).label : undefined}\n aria-describedby={aria['aria-describedby']}\n >\n {visible.map((option) => {\n const optionId = `${id}-${option.value}`\n\n return (\n <div key={option.value} className=\"flex items-start gap-2\">\n <Checkbox\n id={optionId}\n checked={selected.includes(option.value)}\n disabled={locked || option.disabled}\n onCheckedChange={(checked) => toggle(option.value, checked === true)}\n aria-invalid={aria['aria-invalid']}\n className=\"mt-0.5\"\n />\n <div className=\"grid gap-0.5\">\n <Label\n htmlFor={optionId}\n className={cn('font-normal', option.color && textColorClasses[option.color])}\n >\n {option.icon && <Icon name={option.icon} className=\"size-4\" />}\n {component.allowHtml ? (\n <span dangerouslySetInnerHTML={{ __html: option.label }} />\n ) : (\n option.label\n )}\n </Label>\n {option.description && (\n <p className=\"text-muted-foreground text-xs\">{option.description}</p>\n )}\n </div>\n </div>\n )\n })}\n </div>\n {component.searchable && query !== '' && visible.length === 0 && (\n <p\n data-slot=\"checkbox-list-empty\"\n role=\"status\"\n className=\"text-muted-foreground text-sm\"\n >\n {component.noSearchResultsMessage ?? t('no_results', {}, 'No results.')}\n </p>\n )}\n </div>\n </FieldWrapper>\n )\n}\n"],"mappings":"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"}
|
|
@@ -7,7 +7,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
|
|
|
7
7
|
import { useStore } from "../../hooks/use-store.js";
|
|
8
8
|
import { useOptionalFormStore } from "../form/form-context.js";
|
|
9
9
|
import { Input } from "../ui/input.js";
|
|
10
|
-
import { EditorSuggestions, RichEditorRemote, commandLabel, customBlockNodeConfig, fieldActionContext, groupCustomBlocks, groupTriggerButton, isCustomBlockResult, mentionNodeConfig, mergeTagNodeConfig, mergeTagText } from "@alfakit/core";
|
|
10
|
+
import { EditorSuggestions, RichEditorRemote, commandLabel, customBlockNodeConfig, fieldActionContext, fixedContainingBlockOffset, groupCustomBlocks, groupTriggerButton, isCustomBlockResult, mentionNodeConfig, mergeTagNodeConfig, mergeTagText } from "@alfakit/core";
|
|
11
11
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
12
12
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
13
|
import { BlocksIcon, BracesIcon, ChevronDownIcon, XIcon } from "lucide-react";
|
|
@@ -219,6 +219,7 @@ function RichEditorSidePanel({ panel, component, editable, sticky, onInsertBlock
|
|
|
219
219
|
/** A `ToolbarButtonGroup`: a dropdown of buttons (icons, or labels with `textualButtons()`). */
|
|
220
220
|
function ToolbarGroupButton({ group, editor, editable, commands, icons, translate, onRun }) {
|
|
221
221
|
const buttons = group.buttons.filter((name) => commands[name]);
|
|
222
|
+
const picked = useRef(false);
|
|
222
223
|
if (buttons.length === 0) return null;
|
|
223
224
|
const trigger = groupTriggerButton(buttons, editor, commands);
|
|
224
225
|
const active = buttons.some((name) => commands[name]?.active?.(editor));
|
|
@@ -239,6 +240,12 @@ function ToolbarGroupButton({ group, editor, editable, commands, icons, translat
|
|
|
239
240
|
align: "start",
|
|
240
241
|
"aria-label": group.label,
|
|
241
242
|
className: cn(!group.textual && "flex min-w-0 flex-row gap-0.5"),
|
|
243
|
+
onCloseAutoFocus: (event) => {
|
|
244
|
+
if (!picked.current) return;
|
|
245
|
+
picked.current = false;
|
|
246
|
+
event.preventDefault();
|
|
247
|
+
editor.commands.focus();
|
|
248
|
+
},
|
|
242
249
|
children: buttons.map((name) => {
|
|
243
250
|
const label = commandLabel(name, commands, translate);
|
|
244
251
|
const pressed = commands[name]?.active?.(editor);
|
|
@@ -246,7 +253,10 @@ function ToolbarGroupButton({ group, editor, editable, commands, icons, translat
|
|
|
246
253
|
"aria-label": group.textual ? void 0 : label,
|
|
247
254
|
title: label,
|
|
248
255
|
className: cn(pressed && "bg-accent text-accent-foreground"),
|
|
249
|
-
onSelect: () =>
|
|
256
|
+
onSelect: () => {
|
|
257
|
+
picked.current = true;
|
|
258
|
+
onRun(name);
|
|
259
|
+
},
|
|
250
260
|
children: [icons(name), group.textual && /* @__PURE__ */ jsx("span", { children: label })]
|
|
251
261
|
}, name);
|
|
252
262
|
})
|
|
@@ -268,8 +278,8 @@ function SuggestionPopup({ suggestions, editor, id }) {
|
|
|
268
278
|
"data-slot": "rich-editor-suggestions",
|
|
269
279
|
className: "bg-popover text-popover-foreground fixed z-50 min-w-48 overflow-hidden rounded-md border p-1 shadow-md",
|
|
270
280
|
style: state.coords ? {
|
|
271
|
-
left: state.coords.left,
|
|
272
|
-
top: state.coords.bottom + 4
|
|
281
|
+
left: state.coords.left - fixedContainingBlockOffset(editor.view.dom).x,
|
|
282
|
+
top: state.coords.bottom + 4 - fixedContainingBlockOffset(editor.view.dom).y
|
|
273
283
|
} : void 0,
|
|
274
284
|
children: state.loading && state.items.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
275
285
|
className: "text-muted-foreground px-2 py-1.5 text-sm",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rich-editor-extensions.js","names":[],"sources":["../../../src/components/fields/rich-editor-extensions.tsx"],"sourcesContent":["import type {\n ActionContext,\n RichEditorPanel,\n RichEditorSchema,\n RichEditorToolbarGroupSchema,\n SuggestionState,\n ToolbarCommand,\n} from '@alfakit/core'\nimport {\n commandLabel,\n customBlockNodeConfig,\n EditorSuggestions,\n fieldActionContext,\n groupCustomBlocks,\n groupTriggerButton,\n isCustomBlockResult,\n mentionNodeConfig,\n mergeTagNodeConfig,\n mergeTagText,\n RichEditorRemote,\n} from '@alfakit/core'\nimport { Node, type Editor, type Extensions } from '@tiptap/react'\nimport { BlocksIcon, BracesIcon, ChevronDownIcon, XIcon } from 'lucide-react'\nimport {\n useEffect,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n type MutableRefObject,\n type ReactNode,\n} from 'react'\nimport { useActionRunner, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { usePageActionContext } from '../actions/action-context'\nimport { useOptionalFormStore } from '../form/form-context'\nimport { Button } from '../ui/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '../ui/dropdown-menu'\nimport { Input } from '../ui/input'\n\ntype NodeConfig = Parameters<typeof Node.create>[0]\n\nexport interface RichEditorExtensions {\n extensions: Extensions\n suggestions: EditorSuggestions\n /** Insert a custom block (opening its modal first when it has a form). */\n insertBlock: (id: string) => Promise<void>\n context: ActionContext\n}\n\n/**\n * Mentions, merge tags and custom blocks for a RichEditor. `editorRef` points at the live\n * editor, which the custom block node views and suggestions act on.\n */\nexport function useRichEditorExtensions(\n component: RichEditorSchema,\n editorRef: MutableRefObject<Editor | null>,\n): RichEditorExtensions {\n const t = useT()\n const form = useOptionalFormStore()\n const page = usePageActionContext()\n const runner = useActionRunner()\n const context = useMemo(\n () => fieldActionContext({ token: page.token, form, key: component.key }),\n [page.token, form, component.key],\n )\n const contextRef = useRef(context)\n const componentRef = useRef(component)\n\n const [remote] = useState(() => new RichEditorRemote(form, component.statePath))\n const [suggestions] = useState(\n () =>\n new EditorSuggestions({\n mentions: component.mentions ?? [],\n mergeTags: component.mergeTags ?? [],\n search: (trigger, query) => remote.searchMentions(trigger, query),\n }),\n )\n\n useEffect(() => () => suggestions.destroy(), [suggestions])\n\n const runBlock = async (id: string, config?: Record<string, unknown>) => {\n const block = componentRef.current.customBlocks?.find((candidate) => candidate.id === id)\n\n if (!block) return null\n\n const result = await runner.runField(block.action, {\n ...contextRef.current,\n arguments: config ? { config, editing: true } : {},\n })\n\n if (!isCustomBlockResult(result)) return null\n\n if (result.preview !== undefined) {\n remote.primePreview(result.id, result.config, result.preview, result.prose === true)\n }\n\n return result\n }\n\n const runBlockRef = useRef(runBlock)\n\n useEffect(() => {\n contextRef.current = context\n componentRef.current = component\n runBlockRef.current = runBlock\n })\n\n // The editor is created once; the node views read the latest props through refs when\n // they run (on edit, on preview), never during render.\n // eslint-disable-next-line react-hooks/refs\n const [extensions] = useState<Extensions>(() => [\n Node.create(mentionNodeConfig() as NodeConfig),\n Node.create(mergeTagNodeConfig() as NodeConfig),\n Node.create(\n customBlockNodeConfig({\n get blocks() {\n return componentRef.current.customBlocks ?? []\n },\n loadPreview: (id, config) => remote.loadPreview(id, config),\n onEdit: ({ id, config, pos }) => {\n void runBlockRef.current(id, config).then((result) => {\n const editor = editorRef.current\n\n if (!result || !editor || editor.isDestroyed) return\n\n editor.view.dispatch(\n editor.state.tr.setNodeMarkup(pos, undefined, { id, config: result.config }),\n )\n })\n },\n labels: {\n edit: t('editor.edit_block', {}, 'Edit block'),\n remove: t('editor.remove_block', {}, 'Remove block'),\n },\n minimal: Boolean(component.minimalCustomBlockControls),\n }) as NodeConfig,\n ),\n ])\n\n const insertBlock = async (id: string) => {\n const result = await runBlock(id)\n const editor = editorRef.current\n\n if (!result || !editor || editor.isDestroyed) return\n\n editor\n .chain()\n .focus()\n .insertContent({ type: 'customBlock', attrs: { id: result.id, config: result.config } })\n .run()\n }\n\n return { extensions, suggestions, insertBlock, context }\n}\n\n/** The toolbar button that opens the custom blocks or merge tags panel. */\nexport function ExtensionToolbarButton({\n name,\n component,\n editable,\n open,\n onToggle,\n}: {\n name: RichEditorPanel\n component: RichEditorSchema\n editable: boolean\n open: boolean\n onToggle: () => void\n}) {\n const t = useT()\n const count =\n name === 'customBlocks'\n ? (component.customBlocks ?? []).length\n : (component.mergeTags ?? []).length\n\n if (count === 0) return null\n\n const label =\n name === 'customBlocks'\n ? t('editor.custom_blocks', {}, 'Insert block')\n : t('editor.merge_tags', {}, 'Insert merge tag')\n\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={label}\n title={label}\n aria-pressed={open}\n aria-expanded={open}\n disabled={!editable}\n className={cn(open && 'bg-accent text-accent-foreground')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={onToggle}\n >\n {name === 'customBlocks' ? <BlocksIcon /> : <BracesIcon />}\n </Button>\n )\n}\n\n/**\n * The custom blocks or merge tags panel: beside the content on wide editors, above it on\n * narrow ones. Blocks are grouped, optionally searchable and shown as a grid.\n */\nexport function RichEditorSidePanel({\n panel,\n component,\n editable,\n sticky,\n onInsertBlock,\n onInsertMergeTag,\n onClose,\n}: {\n panel: RichEditorPanel\n component: RichEditorSchema\n editable: boolean\n /** `top` of a sticky panel, or null. */\n sticky: string | null\n onInsertBlock: (id: string) => void\n onInsertMergeTag: (id: string) => void\n onClose: () => void\n}) {\n const t = useT()\n const [search, setSearch] = useState('')\n const heading =\n panel === 'customBlocks'\n ? t('editor.custom_blocks_heading', {}, 'Custom blocks')\n : t('editor.merge_tags_heading', {}, 'Merge tags')\n const groups =\n panel === 'customBlocks' ? groupCustomBlocks(component.customBlocks ?? [], search) : []\n const grid = Boolean(component.customBlocksGrid)\n const itemClass =\n 'hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring/50 flex min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50'\n\n return (\n <aside\n aria-label={heading}\n data-slot=\"rich-editor-panel\"\n data-panel={panel}\n className=\"bg-muted/20 shrink-0 border-b @2xl:w-64 @2xl:border-s @2xl:border-b-0\"\n >\n <div\n className={cn('flex flex-col gap-2 p-2', sticky !== null && '@2xl:sticky')}\n // The offset is a runtime value: no Tailwind class can hold it.\n style={sticky !== null ? { top: sticky } : undefined}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-muted-foreground text-xs font-medium\">{heading}</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={t('editor.close_panel', {}, 'Close panel')}\n title={t('editor.close_panel', {}, 'Close panel')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={onClose}\n >\n <XIcon />\n </Button>\n </div>\n {panel === 'customBlocks' && component.searchableCustomBlocks && (\n <Input\n type=\"search\"\n value={search}\n aria-label={t('editor.search_blocks', {}, 'Search blocks')}\n placeholder={t('editor.search_blocks', {}, 'Search blocks')}\n className=\"h-8\"\n onChange={(event) => setSearch(event.target.value)}\n />\n )}\n {panel === 'customBlocks' ? (\n groups.length === 0 ? (\n <p className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n {t('no_results', {}, 'No results.')}\n </p>\n ) : (\n groups.map(({ group, blocks }) => (\n <div key={group ?? ''} role=\"group\" aria-label={group ?? heading}>\n {group && (\n <div className=\"text-muted-foreground px-2 pt-1 pb-0.5 text-xs font-medium\">\n {group}\n </div>\n )}\n <div className={cn(grid ? 'grid grid-cols-2 gap-1' : 'flex flex-col gap-0.5')}>\n {blocks.map((block) => (\n <button\n key={block.id}\n type=\"button\"\n disabled={!editable}\n className={cn(\n itemClass,\n grid && 'flex-col justify-center border py-3 text-center',\n )}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => onInsertBlock(block.id)}\n >\n {block.icon ? (\n <Icon name={block.icon} className=\"size-4 shrink-0\" />\n ) : (\n <BlocksIcon className=\"size-4 shrink-0\" />\n )}\n <span className=\"truncate\">{block.label}</span>\n </button>\n ))}\n </div>\n </div>\n ))\n )\n ) : (\n <div className=\"flex flex-col gap-0.5\">\n {(component.mergeTags ?? []).map((tag) => (\n <button\n key={tag.id}\n type=\"button\"\n disabled={!editable}\n className={itemClass}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => onInsertMergeTag(tag.id)}\n >\n <span className=\"truncate\">{tag.label}</span>\n <span className=\"text-muted-foreground ms-auto font-mono text-xs\" dir=\"ltr\">\n {mergeTagText({ id: tag.id })}\n </span>\n </button>\n ))}\n </div>\n )}\n </div>\n </aside>\n )\n}\n\n/** A `ToolbarButtonGroup`: a dropdown of buttons (icons, or labels with `textualButtons()`). */\nexport function ToolbarGroupButton({\n group,\n editor,\n editable,\n commands,\n icons,\n translate,\n onRun,\n}: {\n group: RichEditorToolbarGroupSchema\n editor: Editor\n editable: boolean\n commands: Record<string, ToolbarCommand>\n icons: (name: string) => ReactNode\n translate: (key: string, fallback: string) => string\n onRun: (name: string) => void\n}) {\n const buttons = group.buttons.filter((name) => commands[name])\n\n if (buttons.length === 0) return null\n\n const trigger = groupTriggerButton(buttons, editor, commands)\n const active = buttons.some((name) => commands[name]?.active?.(editor))\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={group.label}\n title={group.label}\n disabled={!editable}\n className={cn('h-8 gap-0.5 px-1.5', active && 'bg-accent text-accent-foreground')}\n onMouseDown={(event) => event.preventDefault()}\n >\n {group.icon ? <Icon name={group.icon} /> : trigger ? icons(trigger) : group.label}\n <ChevronDownIcon className=\"size-3 opacity-60\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n align=\"start\"\n aria-label={group.label}\n className={cn(!group.textual && 'flex min-w-0 flex-row gap-0.5')}\n >\n {buttons.map((name) => {\n const label = commandLabel(name, commands, translate)\n const pressed = commands[name]?.active?.(editor)\n\n return (\n <DropdownMenuItem\n key={name}\n aria-label={group.textual ? undefined : label}\n title={label}\n className={cn(pressed && 'bg-accent text-accent-foreground')}\n onSelect={() => onRun(name)}\n >\n {icons(name)}\n {group.textual && <span>{label}</span>}\n </DropdownMenuItem>\n )\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/** Inline style of a sticky toolbar. */\nexport function stickyStyle(top: string | null): CSSProperties | undefined {\n return top === null ? undefined : { top }\n}\n\n/** The mention / merge tag listbox under the trigger. */\nexport function SuggestionPopup({\n suggestions,\n editor,\n id,\n}: {\n suggestions: EditorSuggestions\n editor: Editor | null\n id: string\n}) {\n const t = useT()\n const state: SuggestionState = useStore(suggestions.store)\n\n if (!state.open || !editor) return null\n\n return (\n <div\n id={id}\n role=\"listbox\"\n aria-label={t('editor.suggestions', {}, 'Suggestions')}\n data-slot=\"rich-editor-suggestions\"\n className=\"bg-popover text-popover-foreground fixed z-50 min-w-48 overflow-hidden rounded-md border p-1 shadow-md\"\n // Runtime coordinates cannot be Tailwind classes.\n style={state.coords ? { left: state.coords.left, top: state.coords.bottom + 4 } : undefined}\n >\n {state.loading && state.items.length === 0 ? (\n <div className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n {t('loading', {}, 'Loading…')}\n </div>\n ) : state.items.length === 0 ? (\n <div className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n {t('no_results', {}, 'No results.')}\n </div>\n ) : (\n state.items.map((item, index) => (\n <div\n key={item.id}\n id={`${id}-${index}`}\n role=\"option\"\n aria-selected={index === state.index}\n className={cn(\n 'cursor-pointer rounded-sm px-2 py-1.5 text-sm',\n index === state.index && 'bg-accent text-accent-foreground',\n )}\n onMouseDown={(event) => event.preventDefault()}\n onMouseEnter={() => suggestions.highlight(index)}\n onClick={() => suggestions.select(index, editor)}\n >\n {state.kind === 'mention' ? `${state.trigger}${item.label}` : item.label}\n </div>\n ))\n )}\n </div>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rich-editor-extensions.js","names":[],"sources":["../../../src/components/fields/rich-editor-extensions.tsx"],"sourcesContent":["import type {\n ActionContext,\n RichEditorPanel,\n RichEditorSchema,\n RichEditorToolbarGroupSchema,\n SuggestionState,\n ToolbarCommand,\n} from '@alfakit/core'\nimport {\n commandLabel,\n customBlockNodeConfig,\n EditorSuggestions,\n fieldActionContext,\n fixedContainingBlockOffset,\n groupCustomBlocks,\n groupTriggerButton,\n isCustomBlockResult,\n mentionNodeConfig,\n mergeTagNodeConfig,\n mergeTagText,\n RichEditorRemote,\n} from '@alfakit/core'\nimport { Node, type Editor, type Extensions } from '@tiptap/react'\nimport { BlocksIcon, BracesIcon, ChevronDownIcon, XIcon } from 'lucide-react'\nimport {\n useEffect,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n type MutableRefObject,\n type ReactNode,\n} from 'react'\nimport { useActionRunner, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { usePageActionContext } from '../actions/action-context'\nimport { useOptionalFormStore } from '../form/form-context'\nimport { Button } from '../ui/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '../ui/dropdown-menu'\nimport { Input } from '../ui/input'\n\ntype NodeConfig = Parameters<typeof Node.create>[0]\n\nexport interface RichEditorExtensions {\n extensions: Extensions\n suggestions: EditorSuggestions\n /** Insert a custom block (opening its modal first when it has a form). */\n insertBlock: (id: string) => Promise<void>\n context: ActionContext\n}\n\n/**\n * Mentions, merge tags and custom blocks for a RichEditor. `editorRef` points at the live\n * editor, which the custom block node views and suggestions act on.\n */\nexport function useRichEditorExtensions(\n component: RichEditorSchema,\n editorRef: MutableRefObject<Editor | null>,\n): RichEditorExtensions {\n const t = useT()\n const form = useOptionalFormStore()\n const page = usePageActionContext()\n const runner = useActionRunner()\n const context = useMemo(\n () => fieldActionContext({ token: page.token, form, key: component.key }),\n [page.token, form, component.key],\n )\n const contextRef = useRef(context)\n const componentRef = useRef(component)\n\n const [remote] = useState(() => new RichEditorRemote(form, component.statePath))\n const [suggestions] = useState(\n () =>\n new EditorSuggestions({\n mentions: component.mentions ?? [],\n mergeTags: component.mergeTags ?? [],\n search: (trigger, query) => remote.searchMentions(trigger, query),\n }),\n )\n\n useEffect(() => () => suggestions.destroy(), [suggestions])\n\n const runBlock = async (id: string, config?: Record<string, unknown>) => {\n const block = componentRef.current.customBlocks?.find((candidate) => candidate.id === id)\n\n if (!block) return null\n\n const result = await runner.runField(block.action, {\n ...contextRef.current,\n arguments: config ? { config, editing: true } : {},\n })\n\n if (!isCustomBlockResult(result)) return null\n\n if (result.preview !== undefined) {\n remote.primePreview(result.id, result.config, result.preview, result.prose === true)\n }\n\n return result\n }\n\n const runBlockRef = useRef(runBlock)\n\n useEffect(() => {\n contextRef.current = context\n componentRef.current = component\n runBlockRef.current = runBlock\n })\n\n // The editor is created once; the node views read the latest props through refs when\n // they run (on edit, on preview), never during render.\n // eslint-disable-next-line react-hooks/refs\n const [extensions] = useState<Extensions>(() => [\n Node.create(mentionNodeConfig() as NodeConfig),\n Node.create(mergeTagNodeConfig() as NodeConfig),\n Node.create(\n customBlockNodeConfig({\n get blocks() {\n return componentRef.current.customBlocks ?? []\n },\n loadPreview: (id, config) => remote.loadPreview(id, config),\n onEdit: ({ id, config, pos }) => {\n void runBlockRef.current(id, config).then((result) => {\n const editor = editorRef.current\n\n if (!result || !editor || editor.isDestroyed) return\n\n editor.view.dispatch(\n editor.state.tr.setNodeMarkup(pos, undefined, { id, config: result.config }),\n )\n })\n },\n labels: {\n edit: t('editor.edit_block', {}, 'Edit block'),\n remove: t('editor.remove_block', {}, 'Remove block'),\n },\n minimal: Boolean(component.minimalCustomBlockControls),\n }) as NodeConfig,\n ),\n ])\n\n const insertBlock = async (id: string) => {\n const result = await runBlock(id)\n const editor = editorRef.current\n\n if (!result || !editor || editor.isDestroyed) return\n\n editor\n .chain()\n .focus()\n .insertContent({ type: 'customBlock', attrs: { id: result.id, config: result.config } })\n .run()\n }\n\n return { extensions, suggestions, insertBlock, context }\n}\n\n/** The toolbar button that opens the custom blocks or merge tags panel. */\nexport function ExtensionToolbarButton({\n name,\n component,\n editable,\n open,\n onToggle,\n}: {\n name: RichEditorPanel\n component: RichEditorSchema\n editable: boolean\n open: boolean\n onToggle: () => void\n}) {\n const t = useT()\n const count =\n name === 'customBlocks'\n ? (component.customBlocks ?? []).length\n : (component.mergeTags ?? []).length\n\n if (count === 0) return null\n\n const label =\n name === 'customBlocks'\n ? t('editor.custom_blocks', {}, 'Insert block')\n : t('editor.merge_tags', {}, 'Insert merge tag')\n\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={label}\n title={label}\n aria-pressed={open}\n aria-expanded={open}\n disabled={!editable}\n className={cn(open && 'bg-accent text-accent-foreground')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={onToggle}\n >\n {name === 'customBlocks' ? <BlocksIcon /> : <BracesIcon />}\n </Button>\n )\n}\n\n/**\n * The custom blocks or merge tags panel: beside the content on wide editors, above it on\n * narrow ones. Blocks are grouped, optionally searchable and shown as a grid.\n */\nexport function RichEditorSidePanel({\n panel,\n component,\n editable,\n sticky,\n onInsertBlock,\n onInsertMergeTag,\n onClose,\n}: {\n panel: RichEditorPanel\n component: RichEditorSchema\n editable: boolean\n /** `top` of a sticky panel, or null. */\n sticky: string | null\n onInsertBlock: (id: string) => void\n onInsertMergeTag: (id: string) => void\n onClose: () => void\n}) {\n const t = useT()\n const [search, setSearch] = useState('')\n const heading =\n panel === 'customBlocks'\n ? t('editor.custom_blocks_heading', {}, 'Custom blocks')\n : t('editor.merge_tags_heading', {}, 'Merge tags')\n const groups =\n panel === 'customBlocks' ? groupCustomBlocks(component.customBlocks ?? [], search) : []\n const grid = Boolean(component.customBlocksGrid)\n const itemClass =\n 'hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring/50 flex min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50'\n\n return (\n <aside\n aria-label={heading}\n data-slot=\"rich-editor-panel\"\n data-panel={panel}\n className=\"bg-muted/20 shrink-0 border-b @2xl:w-64 @2xl:border-s @2xl:border-b-0\"\n >\n <div\n className={cn('flex flex-col gap-2 p-2', sticky !== null && '@2xl:sticky')}\n // The offset is a runtime value: no Tailwind class can hold it.\n style={sticky !== null ? { top: sticky } : undefined}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-muted-foreground text-xs font-medium\">{heading}</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={t('editor.close_panel', {}, 'Close panel')}\n title={t('editor.close_panel', {}, 'Close panel')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={onClose}\n >\n <XIcon />\n </Button>\n </div>\n {panel === 'customBlocks' && component.searchableCustomBlocks && (\n <Input\n type=\"search\"\n value={search}\n aria-label={t('editor.search_blocks', {}, 'Search blocks')}\n placeholder={t('editor.search_blocks', {}, 'Search blocks')}\n className=\"h-8\"\n onChange={(event) => setSearch(event.target.value)}\n />\n )}\n {panel === 'customBlocks' ? (\n groups.length === 0 ? (\n <p className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n {t('no_results', {}, 'No results.')}\n </p>\n ) : (\n groups.map(({ group, blocks }) => (\n <div key={group ?? ''} role=\"group\" aria-label={group ?? heading}>\n {group && (\n <div className=\"text-muted-foreground px-2 pt-1 pb-0.5 text-xs font-medium\">\n {group}\n </div>\n )}\n <div className={cn(grid ? 'grid grid-cols-2 gap-1' : 'flex flex-col gap-0.5')}>\n {blocks.map((block) => (\n <button\n key={block.id}\n type=\"button\"\n disabled={!editable}\n className={cn(\n itemClass,\n grid && 'flex-col justify-center border py-3 text-center',\n )}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => onInsertBlock(block.id)}\n >\n {block.icon ? (\n <Icon name={block.icon} className=\"size-4 shrink-0\" />\n ) : (\n <BlocksIcon className=\"size-4 shrink-0\" />\n )}\n <span className=\"truncate\">{block.label}</span>\n </button>\n ))}\n </div>\n </div>\n ))\n )\n ) : (\n <div className=\"flex flex-col gap-0.5\">\n {(component.mergeTags ?? []).map((tag) => (\n <button\n key={tag.id}\n type=\"button\"\n disabled={!editable}\n className={itemClass}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => onInsertMergeTag(tag.id)}\n >\n <span className=\"truncate\">{tag.label}</span>\n <span className=\"text-muted-foreground ms-auto font-mono text-xs\" dir=\"ltr\">\n {mergeTagText({ id: tag.id })}\n </span>\n </button>\n ))}\n </div>\n )}\n </div>\n </aside>\n )\n}\n\n/** A `ToolbarButtonGroup`: a dropdown of buttons (icons, or labels with `textualButtons()`). */\nexport function ToolbarGroupButton({\n group,\n editor,\n editable,\n commands,\n icons,\n translate,\n onRun,\n}: {\n group: RichEditorToolbarGroupSchema\n editor: Editor\n editable: boolean\n commands: Record<string, ToolbarCommand>\n icons: (name: string) => ReactNode\n translate: (key: string, fallback: string) => string\n onRun: (name: string) => void\n}) {\n const buttons = group.buttons.filter((name) => commands[name])\n // After a pick, typing goes on in the editor (the command focused it); the menu would\n // otherwise return focus to its trigger. Closing without a pick (Escape) still does.\n const picked = useRef(false)\n\n if (buttons.length === 0) return null\n\n const trigger = groupTriggerButton(buttons, editor, commands)\n const active = buttons.some((name) => commands[name]?.active?.(editor))\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={group.label}\n title={group.label}\n disabled={!editable}\n className={cn('h-8 gap-0.5 px-1.5', active && 'bg-accent text-accent-foreground')}\n onMouseDown={(event) => event.preventDefault()}\n >\n {group.icon ? <Icon name={group.icon} /> : trigger ? icons(trigger) : group.label}\n <ChevronDownIcon className=\"size-3 opacity-60\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n align=\"start\"\n aria-label={group.label}\n className={cn(!group.textual && 'flex min-w-0 flex-row gap-0.5')}\n onCloseAutoFocus={(event) => {\n if (!picked.current) return\n\n picked.current = false\n event.preventDefault()\n editor.commands.focus()\n }}\n >\n {buttons.map((name) => {\n const label = commandLabel(name, commands, translate)\n const pressed = commands[name]?.active?.(editor)\n\n return (\n <DropdownMenuItem\n key={name}\n aria-label={group.textual ? undefined : label}\n title={label}\n className={cn(pressed && 'bg-accent text-accent-foreground')}\n onSelect={() => {\n picked.current = true\n onRun(name)\n }}\n >\n {icons(name)}\n {group.textual && <span>{label}</span>}\n </DropdownMenuItem>\n )\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/** Inline style of a sticky toolbar. */\nexport function stickyStyle(top: string | null): CSSProperties | undefined {\n return top === null ? undefined : { top }\n}\n\n/** The mention / merge tag listbox under the trigger. */\nexport function SuggestionPopup({\n suggestions,\n editor,\n id,\n}: {\n suggestions: EditorSuggestions\n editor: Editor | null\n id: string\n}) {\n const t = useT()\n const state: SuggestionState = useStore(suggestions.store)\n\n if (!state.open || !editor) return null\n\n return (\n <div\n id={id}\n role=\"listbox\"\n aria-label={t('editor.suggestions', {}, 'Suggestions')}\n data-slot=\"rich-editor-suggestions\"\n className=\"bg-popover text-popover-foreground fixed z-50 min-w-48 overflow-hidden rounded-md border p-1 shadow-md\"\n // Runtime coordinates cannot be Tailwind classes.\n // Inside a modal the dialog's transform is the containing block of this fixed list.\n style={\n state.coords\n ? {\n left: state.coords.left - fixedContainingBlockOffset(editor.view.dom).x,\n top: state.coords.bottom + 4 - fixedContainingBlockOffset(editor.view.dom).y,\n }\n : undefined\n }\n >\n {state.loading && state.items.length === 0 ? (\n <div className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n {t('loading', {}, 'Loading…')}\n </div>\n ) : state.items.length === 0 ? (\n <div className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n {t('no_results', {}, 'No results.')}\n </div>\n ) : (\n state.items.map((item, index) => (\n <div\n key={item.id}\n id={`${id}-${index}`}\n role=\"option\"\n aria-selected={index === state.index}\n className={cn(\n 'cursor-pointer rounded-sm px-2 py-1.5 text-sm',\n index === state.index && 'bg-accent text-accent-foreground',\n )}\n onMouseDown={(event) => event.preventDefault()}\n onMouseEnter={() => suggestions.highlight(index)}\n onClick={() => suggestions.select(index, editor)}\n >\n {state.kind === 'mention' ? `${state.trigger}${item.label}` : item.label}\n </div>\n ))\n )}\n </div>\n )\n}\n"],"mappings":"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"}
|
|
@@ -1023,6 +1023,7 @@ function Dialogs({ controller, tt, choice, formatter }) {
|
|
|
1023
1023
|
onOpenChange: (open) => !open && close(),
|
|
1024
1024
|
children: /* @__PURE__ */ jsx(DialogContent, {
|
|
1025
1025
|
"data-slot": "file-manager-dialog",
|
|
1026
|
+
"aria-describedby": void 0,
|
|
1026
1027
|
children: /* @__PURE__ */ jsxs("form", {
|
|
1027
1028
|
className: "flex flex-col gap-4",
|
|
1028
1029
|
onSubmit: (event) => {
|
|
@@ -1286,6 +1287,7 @@ function Preview({ controller, tt, direction }) {
|
|
|
1286
1287
|
children: /* @__PURE__ */ jsxs(DialogContent, {
|
|
1287
1288
|
className: "flex max-h-[90vh] flex-col sm:max-w-4xl",
|
|
1288
1289
|
"data-slot": "file-manager-preview",
|
|
1290
|
+
"aria-describedby": void 0,
|
|
1289
1291
|
onKeyDown,
|
|
1290
1292
|
children: [
|
|
1291
1293
|
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, {
|