@alfakit/react 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/actions/action-button.js +13 -1
  3. package/dist/components/actions/action-button.js.map +1 -1
  4. package/dist/components/dev-tools/dev-tools-inspector.js +2 -2
  5. package/dist/components/dev-tools/dev-tools-inspector.js.map +1 -1
  6. package/dist/components/entries/other-entries.d.ts.map +1 -1
  7. package/dist/components/entries/other-entries.js +1 -1
  8. package/dist/components/entries/other-entries.js.map +1 -1
  9. package/dist/components/fields/builder.d.ts.map +1 -1
  10. package/dist/components/fields/builder.js +3 -3
  11. package/dist/components/fields/builder.js.map +1 -1
  12. package/dist/components/fields/file-upload.d.ts.map +1 -1
  13. package/dist/components/fields/file-upload.js +2 -5
  14. package/dist/components/fields/file-upload.js.map +1 -1
  15. package/dist/components/fields/money-input.d.ts +5 -0
  16. package/dist/components/fields/money-input.d.ts.map +1 -1
  17. package/dist/components/fields/money-input.js +2 -5
  18. package/dist/components/fields/money-input.js.map +1 -1
  19. package/dist/components/fields/repeater.d.ts.map +1 -1
  20. package/dist/components/fields/repeater.js +3 -3
  21. package/dist/components/fields/repeater.js.map +1 -1
  22. package/dist/components/fields/rich-editor-extensions.js +5 -2
  23. package/dist/components/fields/rich-editor-extensions.js.map +1 -1
  24. package/dist/components/layout/wizard.d.ts.map +1 -1
  25. package/dist/components/layout/wizard.js +4 -1
  26. package/dist/components/layout/wizard.js.map +1 -1
  27. package/dist/components/notifications/flash-toasts.d.ts.map +1 -1
  28. package/dist/components/notifications/flash-toasts.js +11 -1
  29. package/dist/components/notifications/flash-toasts.js.map +1 -1
  30. package/dist/components/notifications/notifications-menu.js +3 -3
  31. package/dist/components/notifications/notifications-menu.js.map +1 -1
  32. package/dist/components/panel/lazy-placeholder.d.ts +1 -0
  33. package/dist/components/panel/lazy-placeholder.d.ts.map +1 -1
  34. package/dist/components/panel/lazy-placeholder.js +38 -4
  35. package/dist/components/panel/lazy-placeholder.js.map +1 -1
  36. package/dist/components/panel/pwa.d.ts.map +1 -1
  37. package/dist/components/panel/pwa.js +8 -7
  38. package/dist/components/panel/pwa.js.map +1 -1
  39. package/dist/components/panel/shortcuts-overlay.js +1 -1
  40. package/dist/components/panel/shortcuts-overlay.js.map +1 -1
  41. package/dist/components/table/cells.js +2 -2
  42. package/dist/components/table/cells.js.map +1 -1
  43. package/dist/components/ui/calendar.d.ts.map +1 -1
  44. package/dist/components/ui/calendar.js +7 -0
  45. package/dist/components/ui/calendar.js.map +1 -1
  46. package/dist/components/widgets/dashboard-customizer.js +2 -2
  47. package/dist/components/widgets/dashboard-customizer.js.map +1 -1
  48. package/dist/components/widgets/widgets-block.js +2 -2
  49. package/dist/components/widgets/widgets-block.js.map +1 -1
  50. package/dist/components.css +19 -0
  51. package/dist/create-alfakit.d.ts.map +1 -1
  52. package/dist/create-alfakit.js +3 -2
  53. package/dist/create-alfakit.js.map +1 -1
  54. package/dist/eject-manifest.json +4 -2
  55. package/dist/index.d.ts +2 -2
  56. package/dist/index.js +2 -2
  57. package/dist/pages/page.js +1 -1
  58. package/dist/pages/page.js.map +1 -1
  59. package/dist/standalone/alfakit-component.js +2 -2
  60. package/dist/standalone/alfakit-component.js.map +1 -1
  61. package/dist/standalone.css +21 -0
  62. package/dist/starter-kit/index.d.ts +2 -2
  63. package/dist/starter-kit/index.js +2 -2
  64. package/dist/starter-kit/starter-kit.d.ts +9 -0
  65. package/dist/starter-kit/starter-kit.d.ts.map +1 -1
  66. package/dist/starter-kit/starter-kit.js +21 -2
  67. package/dist/starter-kit/starter-kit.js.map +1 -1
  68. package/package.json +2 -2
  69. package/src/components/actions/action-button.tsx +15 -1
  70. package/src/components/dev-tools/dev-tools-inspector.tsx +6 -1
  71. package/src/components/entries/other-entries.tsx +2 -0
  72. package/src/components/fields/builder.tsx +7 -2
  73. package/src/components/fields/file-upload.tsx +2 -7
  74. package/src/components/fields/money-input.tsx +3 -14
  75. package/src/components/fields/repeater.tsx +6 -3
  76. package/src/components/fields/rich-editor-extensions.tsx +9 -5
  77. package/src/components/layout/wizard.tsx +5 -1
  78. package/src/components/notifications/flash-toasts.tsx +8 -0
  79. package/src/components/notifications/notifications-menu.tsx +3 -3
  80. package/src/components/panel/lazy-placeholder.tsx +41 -1
  81. package/src/components/panel/pwa.tsx +23 -9
  82. package/src/components/panel/shortcuts-overlay.tsx +1 -1
  83. package/src/components/table/cells.tsx +6 -2
  84. package/src/components/ui/calendar.tsx +8 -0
  85. package/src/components/widgets/dashboard-customizer.tsx +2 -2
  86. package/src/components/widgets/widgets-block.tsx +2 -2
  87. package/src/create-alfakit.tsx +9 -1
  88. package/src/pages/page.tsx +1 -1
  89. package/src/standalone/alfakit-component.tsx +2 -2
  90. package/src/starter-kit/index.ts +1 -0
  91. package/src/starter-kit/starter-kit.tsx +22 -0
@@ -1 +1 @@
1
- {"version":3,"file":"wizard.js","names":[],"sources":["../../../src/components/layout/wizard.tsx"],"sourcesContent":["import type { WizardSchema } from '@alfakit/core'\nimport {\n collectFields,\n completedStepIcon,\n initialWizardStep,\n persistWizardStep,\n reportWizardProgress,\n schemaGapClass,\n wizardRequestedStepIndex,\n wizardStepFieldPaths,\n wizardStepQueryKey,\n wizardStepToReveal,\n} from '@alfakit/core'\nimport { CheckIcon } from 'lucide-react'\nimport { useEffect, useState } from 'react'\nimport { useT } from '../../context'\nimport { useStoreSelector } from '../../hooks/use-store'\nimport { extraProps } from '../../lib/attributes'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { ActionButton } from '../actions/action-button'\nimport { usePageActionContext } from '../actions/action-context'\nimport { useOptionalFormStore } from '../form/form-context'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { Button } from '../ui/button'\nimport { DeferredSchema } from './deferred'\n\n/**\n * \"Next\" validates the current step's fields on the server before moving on; the submit\n * validates the whole form, and the wizard jumps to the first step containing an error\n * when new errors arrive (the user can still move between steps afterwards).\n */\n// Used when a wizard renders outside a form. Module-level so the snapshot stays stable;\n// a new object per read makes useSyncExternalStore re-render forever.\nconst NO_ERRORS: Record<string, string[]> = {}\nconst NO_FORM_STORE = {\n get: () => ({ errors: NO_ERRORS }),\n subscribe: () => () => {},\n}\n\nexport function WizardComponent({ component }: { component: WizardSchema }) {\n const t = useT()\n const steps = component.schema ?? []\n const [current, setCurrent] = useState(() =>\n initialWizardStep(steps.length, component.startStep, component.persistStepInQueryString),\n )\n\n const stepQueryKey = wizardStepQueryKey(component.persistStepInQueryString)\n const actionContext = usePageActionContext()\n\n useEffect(() => {\n if (stepQueryKey) persistWizardStep(current, stepQueryKey)\n }, [current, stepQueryKey])\n const store = useOptionalFormStore()\n\n // An action modal around this wizard shows its submit button on the last step only.\n useEffect(() => {\n reportWizardProgress(store, current, steps.length)\n }, [store, current, steps.length])\n const errors = useStoreSelector(store?.store ?? NO_FORM_STORE, (state) => state.errors)\n\n const stepHasError = (index: number) => {\n const step = steps[index]\n\n return step ? collectFields(step.schema ?? []).some((field) => errors[field.statePath]) : false\n }\n\n const [seenErrors, setSeenErrors] = useState<Record<string, string[]>>({})\n\n if (seenErrors !== errors) {\n setSeenErrors(errors)\n\n const reveal = wizardStepToReveal(steps, seenErrors, errors)\n\n if (reveal !== null) setCurrent(reveal)\n }\n\n // `goToStep()` from the server: move once per request (each carries a new id).\n const [seenStepRequest, setSeenStepRequest] = useState(component.goToStep?.id)\n\n if (seenStepRequest !== component.goToStep?.id) {\n setSeenStepRequest(component.goToStep?.id)\n\n const requested = wizardRequestedStepIndex(\n steps,\n component.goToStep,\n current,\n component.skippable,\n )\n\n if (requested !== null) setCurrent(requested)\n }\n\n const activeIndex = current\n const [validating, setValidating] = useState(false)\n\n const next = async () => {\n const step = steps[activeIndex]\n\n if (store && step) {\n setValidating(true)\n const valid = await store.validateStep(\n step.key,\n step.deferred ? (step.deferredPaths ?? []) : wizardStepFieldPaths(step),\n )\n setValidating(false)\n\n if (!valid) return\n }\n\n setCurrent(activeIndex + 1)\n }\n\n return (\n <div\n data-surface=\"wizard\"\n\n {...extraProps(component.extraAttributes, 'bg-card rounded-xl border shadow-xs')}\n >\n <ol className=\"flex flex-wrap gap-2 border-b p-4\" aria-label={t('steps', {}, 'Steps')}>\n {steps.map((step, index) => {\n const done = index < activeIndex\n const active = index === activeIndex\n const clickable = component.skippable || index <= activeIndex\n\n return (\n <li key={step.key} className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n disabled={!clickable}\n onClick={() => setCurrent(index)}\n className={cn(\n 'flex items-center gap-2 rounded-md px-2 py-1 text-sm transition-colors disabled:cursor-default',\n active ? 'text-foreground font-medium' : 'text-muted-foreground',\n clickable && !active && 'hover:text-foreground',\n )}\n >\n <span\n className={cn(\n 'flex size-6 items-center justify-center rounded-full border text-xs',\n active && 'border-primary bg-primary text-primary-foreground',\n done && 'border-success bg-success text-success-foreground',\n stepHasError(index) && 'border-destructive text-destructive',\n )}\n >\n {done ? (\n completedStepIcon(step, component) ? (\n <Icon name={completedStepIcon(step, component)} className=\"size-3.5\" />\n ) : (\n <Icon\n name=\"alfakit::schemas.wizard.completed-step\"\n fallback={CheckIcon}\n className=\"size-3.5\"\n />\n )\n ) : step.icon ? (\n <Icon name={step.icon} className=\"size-3.5\" />\n ) : (\n index + 1\n )}\n </span>\n <span className=\"flex flex-col items-start leading-tight\">\n {step.label}\n {step.description && (\n <span className=\"text-muted-foreground text-xs font-normal\">\n {step.description}\n </span>\n )}\n </span>\n </button>\n {index < steps.length - 1 && <span className=\"bg-border hidden h-px w-6 sm:block\" />}\n </li>\n )\n })}\n </ol>\n {steps.map((step, index) => (\n <div key={step.key} className={cn('p-6', index !== activeIndex && 'hidden')}>\n <DeferredSchema component={step}>\n {(loaded) => (\n <SchemaRenderer\n components={loaded.schema ?? []}\n columns={loaded.columns}\n attributes={loaded.extraAttributes}\n className={schemaGapClass(loaded) || undefined}\n />\n )}\n </DeferredSchema>\n </div>\n ))}\n <div className=\"flex items-center justify-between gap-2 border-t p-4\">\n {component.previousAction === false ? (\n <span />\n ) : component.previousAction ? (\n <ActionButton\n action={{ ...component.previousAction, disabled: activeIndex === 0 || undefined }}\n context={actionContext}\n onTrigger={() => setCurrent(activeIndex - 1)}\n />\n ) : (\n <Button\n type=\"button\"\n variant=\"outline\"\n disabled={activeIndex === 0}\n onClick={() => setCurrent(activeIndex - 1)}\n >\n {component.previousActionLabel ?? t('form.previous_step', {}, 'Back')}\n </Button>\n )}\n {activeIndex < steps.length - 1 ? (\n component.nextAction === false ? (\n <span />\n ) : component.nextAction ? (\n <ActionButton\n action={{\n ...component.nextAction,\n disabled: component.nextAction.disabled || validating || undefined,\n }}\n context={actionContext}\n onTrigger={() => next()}\n />\n ) : (\n <Button\n type=\"button\"\n disabled={validating}\n aria-busy={validating || undefined}\n onClick={() => void next()}\n >\n {component.nextActionLabel ?? t('form.next_step', {}, 'Next')}\n </Button>\n )\n ) : component.submitAction ? (\n <ActionButton action={component.submitAction} context={actionContext} />\n ) : component.submitActionHtml ? (\n <div\n data-slot=\"wizard-submit\"\n dangerouslySetInnerHTML={{ __html: component.submitActionHtml }}\n />\n ) : (\n <span className=\"text-muted-foreground text-xs\">{component.submitActionLabel}</span>\n )}\n </div>\n </div>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"wizard.js","names":[],"sources":["../../../src/components/layout/wizard.tsx"],"sourcesContent":["import type { WizardSchema } from '@alfakit/core'\nimport {\n collectFields,\n completedStepIcon,\n initialWizardStep,\n persistWizardStep,\n reportWizardProgress,\n schemaGapClass,\n wizardRequestedStepIndex,\n wizardStepFieldPaths,\n wizardStepQueryKey,\n wizardStepToReveal,\n} from '@alfakit/core'\nimport { CheckIcon } from 'lucide-react'\nimport { useEffect, useState } from 'react'\nimport { useT } from '../../context'\nimport { useStoreSelector } from '../../hooks/use-store'\nimport { extraProps } from '../../lib/attributes'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { ActionButton } from '../actions/action-button'\nimport { usePageActionContext } from '../actions/action-context'\nimport { useOptionalFormStore } from '../form/form-context'\nimport { useFieldActionContext } from '../schema/field-actions'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { Button } from '../ui/button'\nimport { DeferredSchema } from './deferred'\n\n/**\n * \"Next\" validates the current step's fields on the server before moving on; the submit\n * validates the whole form, and the wizard jumps to the first step containing an error\n * when new errors arrive (the user can still move between steps afterwards).\n */\n// Used when a wizard renders outside a form. Module-level so the snapshot stays stable;\n// a new object per read makes useSyncExternalStore re-render forever.\nconst NO_ERRORS: Record<string, string[]> = {}\nconst NO_FORM_STORE = {\n get: () => ({ errors: NO_ERRORS }),\n subscribe: () => () => {},\n}\n\nexport function WizardComponent({ component }: { component: WizardSchema }) {\n const t = useT()\n const steps = component.schema ?? []\n const [current, setCurrent] = useState(() =>\n initialWizardStep(steps.length, component.startStep, component.persistStepInQueryString),\n )\n\n const stepQueryKey = wizardStepQueryKey(component.persistStepInQueryString)\n const actionContext = usePageActionContext()\n\n useEffect(() => {\n if (stepQueryKey) persistWizardStep(current, stepQueryKey)\n }, [current, stepQueryKey])\n const store = useOptionalFormStore()\n // Inside a form the submit action runs as a field action, so `$get` / `$set` work (W-S2-09).\n const fieldContext = useFieldActionContext(component)\n const submitContext = store ? fieldContext : actionContext\n\n // An action modal around this wizard shows its submit button on the last step only.\n useEffect(() => {\n reportWizardProgress(store, current, steps.length)\n }, [store, current, steps.length])\n const errors = useStoreSelector(store?.store ?? NO_FORM_STORE, (state) => state.errors)\n\n const stepHasError = (index: number) => {\n const step = steps[index]\n\n return step ? collectFields(step.schema ?? []).some((field) => errors[field.statePath]) : false\n }\n\n const [seenErrors, setSeenErrors] = useState<Record<string, string[]>>({})\n\n if (seenErrors !== errors) {\n setSeenErrors(errors)\n\n const reveal = wizardStepToReveal(steps, seenErrors, errors)\n\n if (reveal !== null) setCurrent(reveal)\n }\n\n // `goToStep()` from the server: move once per request (each carries a new id).\n const [seenStepRequest, setSeenStepRequest] = useState(component.goToStep?.id)\n\n if (seenStepRequest !== component.goToStep?.id) {\n setSeenStepRequest(component.goToStep?.id)\n\n const requested = wizardRequestedStepIndex(\n steps,\n component.goToStep,\n current,\n component.skippable,\n )\n\n if (requested !== null) setCurrent(requested)\n }\n\n const activeIndex = current\n const [validating, setValidating] = useState(false)\n\n const next = async () => {\n const step = steps[activeIndex]\n\n if (store && step) {\n setValidating(true)\n const valid = await store.validateStep(\n step.key,\n step.deferred ? (step.deferredPaths ?? []) : wizardStepFieldPaths(step),\n )\n setValidating(false)\n\n if (!valid) return\n }\n\n setCurrent(activeIndex + 1)\n }\n\n return (\n <div\n data-surface=\"wizard\"\n\n {...extraProps(component.extraAttributes, 'bg-card rounded-xl border shadow-xs')}\n >\n <ol className=\"flex flex-wrap gap-2 border-b p-4\" aria-label={t('steps', {}, 'Steps')}>\n {steps.map((step, index) => {\n const done = index < activeIndex\n const active = index === activeIndex\n const clickable = component.skippable || index <= activeIndex\n\n return (\n <li key={step.key} className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n disabled={!clickable}\n onClick={() => setCurrent(index)}\n className={cn(\n 'flex items-center gap-2 rounded-md px-2 py-1 text-sm transition-colors disabled:cursor-default',\n active ? 'text-foreground font-medium' : 'text-muted-foreground',\n clickable && !active && 'hover:text-foreground',\n )}\n >\n <span\n className={cn(\n 'flex size-6 items-center justify-center rounded-full border text-xs',\n active && 'border-primary bg-primary text-primary-foreground',\n done && 'border-success bg-success text-success-foreground',\n stepHasError(index) && 'border-destructive text-destructive',\n )}\n >\n {done ? (\n completedStepIcon(step, component) ? (\n <Icon name={completedStepIcon(step, component)} className=\"size-3.5\" />\n ) : (\n <Icon\n name=\"alfakit::schemas.wizard.completed-step\"\n fallback={CheckIcon}\n className=\"size-3.5\"\n />\n )\n ) : step.icon ? (\n <Icon name={step.icon} className=\"size-3.5\" />\n ) : (\n index + 1\n )}\n </span>\n <span className=\"flex flex-col items-start leading-tight\">\n {step.label}\n {step.description && (\n <span className=\"text-muted-foreground text-xs font-normal\">\n {step.description}\n </span>\n )}\n </span>\n </button>\n {index < steps.length - 1 && <span className=\"bg-border hidden h-px w-6 sm:block\" />}\n </li>\n )\n })}\n </ol>\n {steps.map((step, index) => (\n <div key={step.key} className={cn('p-6', index !== activeIndex && 'hidden')}>\n <DeferredSchema component={step}>\n {(loaded) => (\n <SchemaRenderer\n components={loaded.schema ?? []}\n columns={loaded.columns}\n attributes={loaded.extraAttributes}\n className={schemaGapClass(loaded) || undefined}\n />\n )}\n </DeferredSchema>\n </div>\n ))}\n <div className=\"flex items-center justify-between gap-2 border-t p-4\">\n {component.previousAction === false ? (\n <span />\n ) : component.previousAction ? (\n <ActionButton\n action={{ ...component.previousAction, disabled: activeIndex === 0 || undefined }}\n context={actionContext}\n onTrigger={() => setCurrent(activeIndex - 1)}\n />\n ) : (\n <Button\n type=\"button\"\n variant=\"outline\"\n disabled={activeIndex === 0}\n onClick={() => setCurrent(activeIndex - 1)}\n >\n {component.previousActionLabel ?? t('form.previous_step', {}, 'Back')}\n </Button>\n )}\n {activeIndex < steps.length - 1 ? (\n component.nextAction === false ? (\n <span />\n ) : component.nextAction ? (\n <ActionButton\n action={{\n ...component.nextAction,\n disabled: component.nextAction.disabled || validating || undefined,\n }}\n context={actionContext}\n onTrigger={() => next()}\n />\n ) : (\n <Button\n type=\"button\"\n disabled={validating}\n aria-busy={validating || undefined}\n onClick={() => void next()}\n >\n {component.nextActionLabel ?? t('form.next_step', {}, 'Next')}\n </Button>\n )\n ) : component.submitAction ? (\n <ActionButton action={component.submitAction} context={submitContext} />\n ) : component.submitActionHtml ? (\n <div\n data-slot=\"wizard-submit\"\n dangerouslySetInnerHTML={{ __html: component.submitActionHtml }}\n />\n ) : (\n <span className=\"text-muted-foreground text-xs\">{component.submitActionLabel}</span>\n )}\n </div>\n </div>\n )\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"flash-toasts.d.ts","names":[],"sources":["../../../src/components/notifications/flash-toasts.tsx"],"mappings":";;;;;;;wBAyBgB;wBA4BA,UACd,cAAc,oBACd,SAAS,MAAM;EAAgB,cAAc;;wBAqE/B,2BACd,QACA,SACA,cACA;EAEA,QAAQ;EACR;EACA;;EAEA;oBACD,IAAA"}
1
+ {"version":3,"file":"flash-toasts.d.ts","names":[],"sources":["../../../src/components/notifications/flash-toasts.tsx"],"mappings":";;;;;;;wBAyBgB;wBAoCA,UACd,cAAc,oBACd,SAAS,MAAM;EAAgB,cAAc;;wBAqE/B,2BACd,QACA,SACA,cACA;EAEA,QAAQ;EACR;EACA;;EAEA;oBACD,IAAA"}
@@ -29,6 +29,16 @@ let toasterId;
29
29
  function setToasterId(id) {
30
30
  toasterId = id;
31
31
  }
32
+ /**
33
+ * Options for a direct `toast.*()` call: the starter kit toaster's id merged in, so PWA and
34
+ * notification messages reach AlfaKit's toaster in a starter kit too (W-S1-03).
35
+ */
36
+ function toastOptions(options) {
37
+ return {
38
+ ...options ?? {},
39
+ toasterId
40
+ };
41
+ }
32
42
  /** `closeNotification(id)` (or a `close-notification` event) dismisses that toast. */
33
43
  function listenForClosedNotifications() {
34
44
  if (closeListening || typeof window === "undefined") return;
@@ -116,6 +126,6 @@ function notificationActionClasses(action) {
116
126
  }
117
127
  }
118
128
  //#endregion
119
- export { FlashToasts, NotificationActionButton, listenForClosedNotifications, setToasterId, showToast };
129
+ export { FlashToasts, NotificationActionButton, listenForClosedNotifications, setToasterId, showToast, toastOptions };
120
130
 
121
131
  //# sourceMappingURL=flash-toasts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"flash-toasts.js","names":[],"sources":["../../../src/components/notifications/flash-toasts.tsx"],"sourcesContent":["import type { NotificationActionSchema, NotificationSchema } from '@alfakit/core'\nimport { notificationTintStyles, onCloseNotification, resolveActionSize } from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { useEffect } from 'react'\nimport { toast } from 'sonner'\nimport { useRegistry } from '../../context'\nimport {\n colorClass,\n cssColor,\n ghostButtonColorClasses,\n outlineButtonColorClasses,\n solidButtonColorClasses,\n textColorClasses,\n} from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { ensureFlashToasts } from '../../standalone/runtime'\nimport { Button } from '../ui/button'\nimport { UndoButton } from './undo-button'\n\n/**\n * Turns flashed Alfakit notifications into sonner toasts. Mount once. Inertia 3 delivers\n * flash data as `page.flash` (and a `flash` event), not as a page prop, so this shares the\n * listener the standalone toaster uses.\n */\nexport function FlashToasts() {\n const registry = useRegistry()\n\n useEffect(() => {\n ensureFlashToasts(registry)\n }, [registry])\n\n return null\n}\n\nlet closeListening = false\n\n// Starter kit mode: toasts go to Alfakit's own toaster (`STARTER_KIT_TOASTER_ID`).\nlet toasterId: string | undefined\n\n/** Send Alfakit's toasts to the `<Toaster id>` with this id (undefined: the id-less one). */\nexport function setToasterId(id: string | undefined): void {\n toasterId = id\n}\n\n/** `closeNotification(id)` (or a `close-notification` event) dismisses that toast. */\nexport function listenForClosedNotifications(): void {\n if (closeListening || typeof window === 'undefined') return\n\n closeListening = true\n onCloseNotification((id) => toast.dismiss(id))\n}\n\nexport function showToast(\n notification: NotificationSchema,\n Custom?: React.ComponentType<{ notification: NotificationSchema }>,\n) {\n listenForClosedNotifications()\n\n const duration =\n notification.duration === 'persistent' ? Infinity : (notification.duration ?? 6000)\n\n if (Custom) {\n toast.custom(() => <Custom notification={notification} />, {\n id: notification.id,\n duration,\n toasterId,\n })\n\n return\n }\n\n const method =\n notification.status === 'success'\n ? toast.success\n : notification.status === 'danger'\n ? toast.error\n : notification.status === 'warning'\n ? toast.warning\n : notification.status === 'info'\n ? toast.info\n : toast\n\n const hasActions = (notification.actions?.length ?? 0) > 0 || Boolean(notification.undo)\n\n method(notification.title ?? '', {\n id: notification.id,\n duration,\n toasterId,\n // `color()`: tints the toast's background.\n style: notification.color\n ? (notificationTintStyles(cssColor(notification.color)).toast as React.CSSProperties)\n : undefined,\n // Action buttons go below the body (as in the Vue and Svelte toasts): sonner's `action`\n // slot sits beside the title and squeezes it into a narrow column.\n description:\n notification.body || hasActions ? (\n <div className=\"flex flex-col gap-1\">\n {notification.body && <span dangerouslySetInnerHTML={{ __html: notification.body }} />}\n {hasActions && (\n <div className=\"mt-1 flex flex-wrap items-center gap-1\">\n {notification.undo && (\n <UndoButton undo={notification.undo} toastId={notification.id} />\n )}\n {(notification.actions ?? []).map((action) => (\n <NotificationActionButton\n key={action.name}\n action={action}\n toastId={notification.id}\n />\n ))}\n </div>\n )}\n </div>\n ) : undefined,\n icon: notification.icon ? (\n <Icon\n name={notification.icon}\n className={cn('size-4', notification.iconColor && textColorClasses[notification.iconColor])}\n />\n ) : undefined,\n })\n}\n\nexport function NotificationActionButton({\n action,\n toastId,\n onMarkAsRead,\n onMarkAsUnread,\n}: {\n action: NotificationActionSchema\n toastId?: string\n onMarkAsRead?: () => void\n /** Database notifications: `markAsUnread()`. */\n onMarkAsUnread?: () => void\n}) {\n const run = () => {\n if (action.dispatch) {\n window.dispatchEvent(new CustomEvent(action.dispatch.event, { detail: action.dispatch.data }))\n }\n\n if (action.markAsRead) onMarkAsRead?.()\n if (action.markAsUnread) onMarkAsUnread?.()\n if (action.close && toastId) toast.dismiss(toastId)\n\n if (action.url) {\n if (action.openUrlInNewTab) {\n window.open(action.url, '_blank', 'noopener')\n } else {\n router.visit(action.url)\n }\n }\n }\n\n return (\n <Button\n type=\"button\"\n size={resolveActionSize(action.size, 'xs')}\n variant={notificationActionVariant(action)}\n className={notificationActionClasses(action)}\n onClick={run}\n >\n {action.icon && <Icon name={action.icon} className=\"size-3.5\" />}\n {action.label}\n </Button>\n )\n}\n\nfunction notificationActionVariant(\n action: NotificationActionSchema,\n): 'default' | 'outline' | 'ghost' | 'link' {\n switch (action.variant) {\n case 'button':\n return 'default'\n case 'outline':\n return 'outline'\n case 'ghost':\n case 'icon-button':\n return 'ghost'\n default:\n return 'link'\n }\n}\n\n/** The action's `color()` applied to its variant (links default to the primary colour). */\nfunction notificationActionClasses(action: NotificationActionSchema): string | undefined {\n if (!action.color) return undefined\n\n switch (action.variant) {\n case 'button':\n return colorClass(solidButtonColorClasses, action.color, 'primary')\n case 'outline':\n return colorClass(outlineButtonColorClasses, action.color, 'gray')\n case 'ghost':\n case 'icon-button':\n return colorClass(ghostButtonColorClasses, action.color, 'gray')\n default:\n return colorClass(textColorClasses, action.color, 'primary')\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"flash-toasts.js","names":[],"sources":["../../../src/components/notifications/flash-toasts.tsx"],"sourcesContent":["import type { NotificationActionSchema, NotificationSchema } from '@alfakit/core'\nimport { notificationTintStyles, onCloseNotification, resolveActionSize } from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { useEffect } from 'react'\nimport { toast } from 'sonner'\nimport { useRegistry } from '../../context'\nimport {\n colorClass,\n cssColor,\n ghostButtonColorClasses,\n outlineButtonColorClasses,\n solidButtonColorClasses,\n textColorClasses,\n} from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { ensureFlashToasts } from '../../standalone/runtime'\nimport { Button } from '../ui/button'\nimport { UndoButton } from './undo-button'\n\n/**\n * Turns flashed Alfakit notifications into sonner toasts. Mount once. Inertia 3 delivers\n * flash data as `page.flash` (and a `flash` event), not as a page prop, so this shares the\n * listener the standalone toaster uses.\n */\nexport function FlashToasts() {\n const registry = useRegistry()\n\n useEffect(() => {\n ensureFlashToasts(registry)\n }, [registry])\n\n return null\n}\n\nlet closeListening = false\n\n// Starter kit mode: toasts go to Alfakit's own toaster (`STARTER_KIT_TOASTER_ID`).\nlet toasterId: string | undefined\n\n/** Send Alfakit's toasts to the `<Toaster id>` with this id (undefined: the id-less one). */\nexport function setToasterId(id: string | undefined): void {\n toasterId = id\n}\n\n/**\n * Options for a direct `toast.*()` call: the starter kit toaster's id merged in, so PWA and\n * notification messages reach AlfaKit's toaster in a starter kit too (W-S1-03).\n */\nexport function toastOptions<T extends object>(options?: T): T & { toasterId?: string } {\n return { ...(options ?? ({} as T)), toasterId }\n}\n\n/** `closeNotification(id)` (or a `close-notification` event) dismisses that toast. */\nexport function listenForClosedNotifications(): void {\n if (closeListening || typeof window === 'undefined') return\n\n closeListening = true\n onCloseNotification((id) => toast.dismiss(id))\n}\n\nexport function showToast(\n notification: NotificationSchema,\n Custom?: React.ComponentType<{ notification: NotificationSchema }>,\n) {\n listenForClosedNotifications()\n\n const duration =\n notification.duration === 'persistent' ? Infinity : (notification.duration ?? 6000)\n\n if (Custom) {\n toast.custom(() => <Custom notification={notification} />, {\n id: notification.id,\n duration,\n toasterId,\n })\n\n return\n }\n\n const method =\n notification.status === 'success'\n ? toast.success\n : notification.status === 'danger'\n ? toast.error\n : notification.status === 'warning'\n ? toast.warning\n : notification.status === 'info'\n ? toast.info\n : toast\n\n const hasActions = (notification.actions?.length ?? 0) > 0 || Boolean(notification.undo)\n\n method(notification.title ?? '', {\n id: notification.id,\n duration,\n toasterId,\n // `color()`: tints the toast's background.\n style: notification.color\n ? (notificationTintStyles(cssColor(notification.color)).toast as React.CSSProperties)\n : undefined,\n // Action buttons go below the body (as in the Vue and Svelte toasts): sonner's `action`\n // slot sits beside the title and squeezes it into a narrow column.\n description:\n notification.body || hasActions ? (\n <div className=\"flex flex-col gap-1\">\n {notification.body && <span dangerouslySetInnerHTML={{ __html: notification.body }} />}\n {hasActions && (\n <div className=\"mt-1 flex flex-wrap items-center gap-1\">\n {notification.undo && (\n <UndoButton undo={notification.undo} toastId={notification.id} />\n )}\n {(notification.actions ?? []).map((action) => (\n <NotificationActionButton\n key={action.name}\n action={action}\n toastId={notification.id}\n />\n ))}\n </div>\n )}\n </div>\n ) : undefined,\n icon: notification.icon ? (\n <Icon\n name={notification.icon}\n className={cn('size-4', notification.iconColor && textColorClasses[notification.iconColor])}\n />\n ) : undefined,\n })\n}\n\nexport function NotificationActionButton({\n action,\n toastId,\n onMarkAsRead,\n onMarkAsUnread,\n}: {\n action: NotificationActionSchema\n toastId?: string\n onMarkAsRead?: () => void\n /** Database notifications: `markAsUnread()`. */\n onMarkAsUnread?: () => void\n}) {\n const run = () => {\n if (action.dispatch) {\n window.dispatchEvent(new CustomEvent(action.dispatch.event, { detail: action.dispatch.data }))\n }\n\n if (action.markAsRead) onMarkAsRead?.()\n if (action.markAsUnread) onMarkAsUnread?.()\n if (action.close && toastId) toast.dismiss(toastId)\n\n if (action.url) {\n if (action.openUrlInNewTab) {\n window.open(action.url, '_blank', 'noopener')\n } else {\n router.visit(action.url)\n }\n }\n }\n\n return (\n <Button\n type=\"button\"\n size={resolveActionSize(action.size, 'xs')}\n variant={notificationActionVariant(action)}\n className={notificationActionClasses(action)}\n onClick={run}\n >\n {action.icon && <Icon name={action.icon} className=\"size-3.5\" />}\n {action.label}\n </Button>\n )\n}\n\nfunction notificationActionVariant(\n action: NotificationActionSchema,\n): 'default' | 'outline' | 'ghost' | 'link' {\n switch (action.variant) {\n case 'button':\n return 'default'\n case 'outline':\n return 'outline'\n case 'ghost':\n case 'icon-button':\n return 'ghost'\n default:\n return 'link'\n }\n}\n\n/** The action's `color()` applied to its variant (links default to the primary colour). */\nfunction notificationActionClasses(action: NotificationActionSchema): string | undefined {\n if (!action.color) return undefined\n\n switch (action.variant) {\n case 'button':\n return colorClass(solidButtonColorClasses, action.color, 'primary')\n case 'outline':\n return colorClass(outlineButtonColorClasses, action.color, 'gray')\n case 'ghost':\n case 'icon-button':\n return colorClass(ghostButtonColorClasses, action.color, 'gray')\n default:\n return colorClass(textColorClasses, action.color, 'primary')\n }\n}\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ import { Button } from "../ui/button.js";
6
6
  import { useStore } from "../../hooks/use-store.js";
7
7
  import { Spinner } from "../ui/spinner.js";
8
8
  import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
9
- import { NotificationActionButton } from "./flash-toasts.js";
9
+ import { NotificationActionButton, toastOptions } from "./flash-toasts.js";
10
10
  import { ScrollArea } from "../ui/scroll-area.js";
11
11
  import { NotificationCenter, notificationTintStyles, onDatabaseNotificationsSent, onOpenDatabaseNotifications, pwaStore, startPwa, togglePushNotifications } from "@alfakit/core";
12
12
  import { useEffect, useMemo, useState } from "react";
@@ -65,8 +65,8 @@ function NotificationsMenu({ variant = "topbar" } = {}) {
65
65
  const togglePush = async () => {
66
66
  const { message } = await togglePushNotifications();
67
67
  const text = t(message.key, {}, message.fallback);
68
- if (message.error) toast.error(text);
69
- else toast.success(text);
68
+ if (message.error) toast.error(text, toastOptions());
69
+ else toast.success(text, toastOptions());
70
70
  };
71
71
  const pushLabel = pwa.push === "subscribed" ? t("pwa.notifications.disable", {}, "Turn off notifications") : t("pwa.notifications.enable", {}, "Turn on notifications");
72
72
  if (!config || !center) return null;
@@ -1 +1 @@
1
- {"version":3,"file":"notifications-menu.js","names":[],"sources":["../../../src/components/notifications/notifications-menu.tsx"],"sourcesContent":["import {\n NotificationCenter,\n notificationTintStyles,\n onDatabaseNotificationsSent,\n onOpenDatabaseNotifications,\n pwaStore,\n startPwa,\n togglePushNotifications,\n} from '@alfakit/core'\nimport { BellIcon, BellOffIcon, BellRingIcon, CheckCheckIcon, Trash2Icon } from 'lucide-react'\nimport { useEffect, useMemo, useState } from 'react'\nimport { toast } from 'sonner'\nimport { useDirection, useFormatter, usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { cssColor, textColorClasses } from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { Button } from '../ui/button'\nimport { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'\nimport { ScrollArea } from '../ui/scroll-area'\nimport { Spinner } from '../ui/spinner'\nimport { NotificationActionButton } from './flash-toasts'\n\nexport interface NotificationsMenuProps {\n /**\n * Where the trigger sits: `topbar` (an icon button, default) or `sidebar` (a full-width\n * row with a label, for layouts that move the bell into the sidebar).\n */\n variant?: 'topbar' | 'sidebar'\n}\n\n/**\n * Bell menu for database notifications. It also opens when something calls\n * `openDatabaseNotifications()` (or an action with `->openDatabaseNotifications()`).\n */\nexport function NotificationsMenu({ variant = 'topbar' }: NotificationsMenuProps = {}) {\n const t = useT()\n const direction = useDirection()\n const format = useFormatter()\n const panel = usePanel()\n const config = panel.databaseNotifications\n const endpoints = panel.endpoints\n\n const center = useMemo(\n () =>\n endpoints.notifications && endpoints.notificationsReadAll && endpoints.notificationsDestroyAll\n ? new NotificationCenter(\n {\n notifications: endpoints.notifications,\n notificationsReadAll: endpoints.notificationsReadAll,\n notificationsDestroyAll: endpoints.notificationsDestroyAll,\n },\n config?.unreadCount ?? 0,\n )\n : null,\n [\n endpoints.notifications,\n endpoints.notificationsReadAll,\n endpoints.notificationsDestroyAll,\n config?.unreadCount,\n ],\n )\n\n const [open, setOpen] = useState(false)\n const state = useStore(\n center?.store ?? {\n get: () => ({ notifications: [], unreadCount: 0, loading: false, hasMore: false, page: 0 }),\n subscribe: () => () => {},\n },\n )\n\n useEffect(() => {\n if (!center) return\n\n center.startPolling(config?.pollingInterval)\n\n return () => center.stopPolling()\n }, [center, config?.pollingInterval])\n\n useEffect(() => {\n if (open) void center?.load()\n }, [open, center])\n\n // `openDatabaseNotifications()`: opened from code or an action.\n useEffect(() => onOpenDatabaseNotifications(() => setOpen(true)), [])\n\n // Realtime notifications announce stored ones, so the unread count updates right away.\n useEffect(\n () => (center ? onDatabaseNotificationsSent(() => void center.load()) : undefined),\n [center],\n )\n\n // `pwa(push: true)` in a starter kit: the app's user menu has no AlfaKit entries, so Web\n // Push is turned on or off from the bell's menu.\n const pwa = useStore(pwaStore)\n const kitPush = Boolean(panel.starterKit && panel.pwa?.push)\n const pushToggle = kitPush && pwa.push !== 'unsupported'\n\n useEffect(() => {\n if (kitPush) startPwa(panel.pwa)\n }, [kitPush, panel.pwa])\n\n const togglePush = async () => {\n const { message } = await togglePushNotifications()\n const text = t(message.key, {}, message.fallback)\n\n if (message.error) toast.error(text)\n else toast.success(text)\n }\n\n const pushLabel =\n pwa.push === 'subscribed'\n ? t('pwa.notifications.disable', {}, 'Turn off notifications')\n : t('pwa.notifications.enable', {}, 'Turn on notifications')\n\n if (!config || !center) {\n return null\n }\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n {variant === 'sidebar' ? (\n // In a collapsed (icon) sidebar only the bell and its badge stay visible.\n <Button\n type=\"button\"\n variant=\"ghost\"\n className=\"relative w-full justify-start gap-2 group-data-[collapsible=icon]:size-8 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-0\"\n data-slot=\"notifications-trigger\"\n data-variant=\"sidebar\"\n >\n <Icon\n name=\"alfakit::panels.topbar.open-database-notifications-button\"\n fallback={BellIcon}\n />\n <span className=\"flex-1 text-start group-data-[collapsible=icon]:sr-only\">\n {t('notifications', {}, 'Notifications')}\n </span>\n {state.unreadCount > 0 && (\n <span className=\"bg-primary text-primary-foreground flex h-5 min-w-5 items-center justify-center rounded-full px-1 text-xs font-medium group-data-[collapsible=icon]:absolute group-data-[collapsible=icon]:-end-1 group-data-[collapsible=icon]:-top-1 group-data-[collapsible=icon]:h-4 group-data-[collapsible=icon]:min-w-4 group-data-[collapsible=icon]:text-[10px]\">\n {state.unreadCount > 99 ? '99+' : state.unreadCount}\n <span className=\"sr-only\"> {t('unread_notifications', {}, 'unread')}</span>\n </span>\n )}\n </Button>\n ) : (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"relative\"\n aria-label={t('notifications', {}, 'Notifications')}\n data-slot=\"notifications-trigger\"\n >\n <Icon\n name=\"alfakit::panels.topbar.open-database-notifications-button\"\n fallback={BellIcon}\n />\n {state.unreadCount > 0 && (\n <span className=\"bg-primary text-primary-foreground absolute -end-0.5 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-medium\">\n {state.unreadCount > 99 ? '99+' : state.unreadCount}\n </span>\n )}\n </Button>\n )}\n </PopoverTrigger>\n <PopoverContent\n align={variant === 'sidebar' ? 'start' : 'end'}\n side={variant === 'sidebar' ? (direction === 'rtl' ? 'left' : 'right') : 'bottom'}\n className=\"w-80 p-0 sm:w-96\"\n >\n <div className=\"flex items-center justify-between border-b px-3 py-2\">\n <span className=\"text-sm font-semibold\">{t('notifications', {}, 'Notifications')}</span>\n <div className=\"flex items-center gap-1\">\n {pushToggle && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n data-slot=\"pwa-push\"\n disabled={pwa.pushBusy || pwa.push === 'denied'}\n title={pushLabel}\n aria-label={pushLabel}\n onClick={() => void togglePush()}\n >\n {pwa.push === 'subscribed' ? <BellOffIcon /> : <BellRingIcon />}\n </Button>\n )}\n {state.unreadCount > 0 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n title={t('mark_all_read', {}, 'Mark all as read')}\n onClick={() => void center.markAllAsRead()}\n >\n <CheckCheckIcon />\n </Button>\n )}\n {state.notifications.length > 0 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n title={t('clear_notifications', {}, 'Clear notifications')}\n onClick={() => void center.clear()}\n >\n <Trash2Icon />\n </Button>\n )}\n </div>\n </div>\n <ScrollArea className=\"max-h-96\">\n {state.loading && state.notifications.length === 0 ? (\n <div className=\"flex justify-center py-8\">\n <Spinner />\n </div>\n ) : state.notifications.length === 0 ? (\n <p className=\"text-muted-foreground px-3 py-8 text-center text-sm\">\n {t('no_notifications', {}, 'You have no notifications.')}\n </p>\n ) : (\n <ul className=\"divide-y\">\n {state.notifications.map((notification) => (\n <li\n key={notification.id}\n className={cn('flex gap-3 px-3 py-3', !notification.readAt && 'bg-primary/5')}\n style={\n notification.color\n ? notificationTintStyles(cssColor(notification.color)).item\n : undefined\n }\n >\n {notification.icon && (\n <Icon\n name={notification.icon}\n className={cn(\n 'mt-0.5 size-5 shrink-0',\n notification.iconColor && textColorClasses[notification.iconColor],\n )}\n />\n )}\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <div className=\"flex items-start justify-between gap-2\">\n <p className=\"text-sm font-medium\">{notification.title}</p>\n {!notification.readAt && (\n <button\n type=\"button\"\n className=\"focus-visible:ring-ring/50 -me-2 -mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full outline-none focus-visible:ring-[3px]\"\n aria-label={t('mark_as_read', {}, 'Mark as read')}\n onClick={() => void center.markAsRead(notification.id)}\n >\n <span className=\"bg-primary size-2 rounded-full\" aria-hidden=\"true\" />\n </button>\n )}\n </div>\n {notification.body && (\n <p\n className=\"text-muted-foreground text-sm\"\n dangerouslySetInnerHTML={{ __html: notification.body }}\n />\n )}\n {notification.date && (\n <time className=\"text-muted-foreground text-xs\">\n {format.relative(notification.date)}\n </time>\n )}\n {notification.actions && (\n <div className=\"flex flex-wrap gap-1\">\n {notification.actions.map((action) => (\n <NotificationActionButton\n key={action.name}\n action={action}\n onMarkAsRead={() => void center.markAsRead(notification.id)}\n onMarkAsUnread={() => void center.markAsUnread(notification.id)}\n />\n ))}\n </div>\n )}\n </div>\n </li>\n ))}\n </ul>\n )}\n {state.hasMore && state.notifications.length > 0 && (\n <div className=\"flex justify-center border-t p-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={state.loading}\n onClick={() => void center.loadMore()}\n >\n {state.loading && <Spinner />}\n {t('load_more', {}, 'Load more')}\n </Button>\n </div>\n )}\n </ScrollArea>\n </PopoverContent>\n </Popover>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"notifications-menu.js","names":[],"sources":["../../../src/components/notifications/notifications-menu.tsx"],"sourcesContent":["import {\n NotificationCenter,\n notificationTintStyles,\n onDatabaseNotificationsSent,\n onOpenDatabaseNotifications,\n pwaStore,\n startPwa,\n togglePushNotifications,\n} from '@alfakit/core'\nimport { BellIcon, BellOffIcon, BellRingIcon, CheckCheckIcon, Trash2Icon } from 'lucide-react'\nimport { useEffect, useMemo, useState } from 'react'\nimport { toast } from 'sonner'\nimport { useDirection, useFormatter, usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { cssColor, textColorClasses } from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { Button } from '../ui/button'\nimport { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'\nimport { ScrollArea } from '../ui/scroll-area'\nimport { Spinner } from '../ui/spinner'\nimport { NotificationActionButton, toastOptions } from './flash-toasts'\n\nexport interface NotificationsMenuProps {\n /**\n * Where the trigger sits: `topbar` (an icon button, default) or `sidebar` (a full-width\n * row with a label, for layouts that move the bell into the sidebar).\n */\n variant?: 'topbar' | 'sidebar'\n}\n\n/**\n * Bell menu for database notifications. It also opens when something calls\n * `openDatabaseNotifications()` (or an action with `->openDatabaseNotifications()`).\n */\nexport function NotificationsMenu({ variant = 'topbar' }: NotificationsMenuProps = {}) {\n const t = useT()\n const direction = useDirection()\n const format = useFormatter()\n const panel = usePanel()\n const config = panel.databaseNotifications\n const endpoints = panel.endpoints\n\n const center = useMemo(\n () =>\n endpoints.notifications && endpoints.notificationsReadAll && endpoints.notificationsDestroyAll\n ? new NotificationCenter(\n {\n notifications: endpoints.notifications,\n notificationsReadAll: endpoints.notificationsReadAll,\n notificationsDestroyAll: endpoints.notificationsDestroyAll,\n },\n config?.unreadCount ?? 0,\n )\n : null,\n [\n endpoints.notifications,\n endpoints.notificationsReadAll,\n endpoints.notificationsDestroyAll,\n config?.unreadCount,\n ],\n )\n\n const [open, setOpen] = useState(false)\n const state = useStore(\n center?.store ?? {\n get: () => ({ notifications: [], unreadCount: 0, loading: false, hasMore: false, page: 0 }),\n subscribe: () => () => {},\n },\n )\n\n useEffect(() => {\n if (!center) return\n\n center.startPolling(config?.pollingInterval)\n\n return () => center.stopPolling()\n }, [center, config?.pollingInterval])\n\n useEffect(() => {\n if (open) void center?.load()\n }, [open, center])\n\n // `openDatabaseNotifications()`: opened from code or an action.\n useEffect(() => onOpenDatabaseNotifications(() => setOpen(true)), [])\n\n // Realtime notifications announce stored ones, so the unread count updates right away.\n useEffect(\n () => (center ? onDatabaseNotificationsSent(() => void center.load()) : undefined),\n [center],\n )\n\n // `pwa(push: true)` in a starter kit: the app's user menu has no AlfaKit entries, so Web\n // Push is turned on or off from the bell's menu.\n const pwa = useStore(pwaStore)\n const kitPush = Boolean(panel.starterKit && panel.pwa?.push)\n const pushToggle = kitPush && pwa.push !== 'unsupported'\n\n useEffect(() => {\n if (kitPush) startPwa(panel.pwa)\n }, [kitPush, panel.pwa])\n\n const togglePush = async () => {\n const { message } = await togglePushNotifications()\n const text = t(message.key, {}, message.fallback)\n\n if (message.error) toast.error(text, toastOptions())\n else toast.success(text, toastOptions())\n }\n\n const pushLabel =\n pwa.push === 'subscribed'\n ? t('pwa.notifications.disable', {}, 'Turn off notifications')\n : t('pwa.notifications.enable', {}, 'Turn on notifications')\n\n if (!config || !center) {\n return null\n }\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n {variant === 'sidebar' ? (\n // In a collapsed (icon) sidebar only the bell and its badge stay visible.\n <Button\n type=\"button\"\n variant=\"ghost\"\n className=\"relative w-full justify-start gap-2 group-data-[collapsible=icon]:size-8 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-0\"\n data-slot=\"notifications-trigger\"\n data-variant=\"sidebar\"\n >\n <Icon\n name=\"alfakit::panels.topbar.open-database-notifications-button\"\n fallback={BellIcon}\n />\n <span className=\"flex-1 text-start group-data-[collapsible=icon]:sr-only\">\n {t('notifications', {}, 'Notifications')}\n </span>\n {state.unreadCount > 0 && (\n <span className=\"bg-primary text-primary-foreground flex h-5 min-w-5 items-center justify-center rounded-full px-1 text-xs font-medium group-data-[collapsible=icon]:absolute group-data-[collapsible=icon]:-end-1 group-data-[collapsible=icon]:-top-1 group-data-[collapsible=icon]:h-4 group-data-[collapsible=icon]:min-w-4 group-data-[collapsible=icon]:text-[10px]\">\n {state.unreadCount > 99 ? '99+' : state.unreadCount}\n <span className=\"sr-only\"> {t('unread_notifications', {}, 'unread')}</span>\n </span>\n )}\n </Button>\n ) : (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"relative\"\n aria-label={t('notifications', {}, 'Notifications')}\n data-slot=\"notifications-trigger\"\n >\n <Icon\n name=\"alfakit::panels.topbar.open-database-notifications-button\"\n fallback={BellIcon}\n />\n {state.unreadCount > 0 && (\n <span className=\"bg-primary text-primary-foreground absolute -end-0.5 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-medium\">\n {state.unreadCount > 99 ? '99+' : state.unreadCount}\n </span>\n )}\n </Button>\n )}\n </PopoverTrigger>\n <PopoverContent\n align={variant === 'sidebar' ? 'start' : 'end'}\n side={variant === 'sidebar' ? (direction === 'rtl' ? 'left' : 'right') : 'bottom'}\n className=\"w-80 p-0 sm:w-96\"\n >\n <div className=\"flex items-center justify-between border-b px-3 py-2\">\n <span className=\"text-sm font-semibold\">{t('notifications', {}, 'Notifications')}</span>\n <div className=\"flex items-center gap-1\">\n {pushToggle && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n data-slot=\"pwa-push\"\n disabled={pwa.pushBusy || pwa.push === 'denied'}\n title={pushLabel}\n aria-label={pushLabel}\n onClick={() => void togglePush()}\n >\n {pwa.push === 'subscribed' ? <BellOffIcon /> : <BellRingIcon />}\n </Button>\n )}\n {state.unreadCount > 0 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n title={t('mark_all_read', {}, 'Mark all as read')}\n onClick={() => void center.markAllAsRead()}\n >\n <CheckCheckIcon />\n </Button>\n )}\n {state.notifications.length > 0 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n title={t('clear_notifications', {}, 'Clear notifications')}\n onClick={() => void center.clear()}\n >\n <Trash2Icon />\n </Button>\n )}\n </div>\n </div>\n <ScrollArea className=\"max-h-96\">\n {state.loading && state.notifications.length === 0 ? (\n <div className=\"flex justify-center py-8\">\n <Spinner />\n </div>\n ) : state.notifications.length === 0 ? (\n <p className=\"text-muted-foreground px-3 py-8 text-center text-sm\">\n {t('no_notifications', {}, 'You have no notifications.')}\n </p>\n ) : (\n <ul className=\"divide-y\">\n {state.notifications.map((notification) => (\n <li\n key={notification.id}\n className={cn('flex gap-3 px-3 py-3', !notification.readAt && 'bg-primary/5')}\n style={\n notification.color\n ? notificationTintStyles(cssColor(notification.color)).item\n : undefined\n }\n >\n {notification.icon && (\n <Icon\n name={notification.icon}\n className={cn(\n 'mt-0.5 size-5 shrink-0',\n notification.iconColor && textColorClasses[notification.iconColor],\n )}\n />\n )}\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <div className=\"flex items-start justify-between gap-2\">\n <p className=\"text-sm font-medium\">{notification.title}</p>\n {!notification.readAt && (\n <button\n type=\"button\"\n className=\"focus-visible:ring-ring/50 -me-2 -mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full outline-none focus-visible:ring-[3px]\"\n aria-label={t('mark_as_read', {}, 'Mark as read')}\n onClick={() => void center.markAsRead(notification.id)}\n >\n <span className=\"bg-primary size-2 rounded-full\" aria-hidden=\"true\" />\n </button>\n )}\n </div>\n {notification.body && (\n <p\n className=\"text-muted-foreground text-sm\"\n dangerouslySetInnerHTML={{ __html: notification.body }}\n />\n )}\n {notification.date && (\n <time className=\"text-muted-foreground text-xs\">\n {format.relative(notification.date)}\n </time>\n )}\n {notification.actions && (\n <div className=\"flex flex-wrap gap-1\">\n {notification.actions.map((action) => (\n <NotificationActionButton\n key={action.name}\n action={action}\n onMarkAsRead={() => void center.markAsRead(notification.id)}\n onMarkAsUnread={() => void center.markAsUnread(notification.id)}\n />\n ))}\n </div>\n )}\n </div>\n </li>\n ))}\n </ul>\n )}\n {state.hasMore && state.notifications.length > 0 && (\n <div className=\"flex justify-center border-t p-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={state.loading}\n onClick={() => void center.loadMore()}\n >\n {state.loading && <Spinner />}\n {t('load_more', {}, 'Load more')}\n </Button>\n </div>\n )}\n </ScrollArea>\n </PopoverContent>\n </Popover>\n )\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import "@alfakit/core";
1
2
  //#region src/components/panel/lazy-placeholder.d.ts
2
3
  /**
3
4
  * Stands in for a lazy widget or relation manager until the `alfakitLazy` deferred prop
@@ -1 +1 @@
1
- {"version":3,"file":"lazy-placeholder.d.ts","names":[],"sources":["../../../src/components/panel/lazy-placeholder.tsx"],"mappings":";;;;;wBAQgB,kBACd,SACA;EAEA;EACA;oBACD,IAAA"}
1
+ {"version":3,"file":"lazy-placeholder.d.ts","names":[],"sources":["../../../src/components/panel/lazy-placeholder.tsx"],"mappings":";;;;;;wBAWgB,kBACd,SACA;EAEA;EACA;oBACD,IAAA"}
@@ -1,7 +1,10 @@
1
- import { useT } from "../../context.js";
1
+ import { useRefreshComponents, useT } from "../../context.js";
2
2
  import { cn } from "../../lib/utils.js";
3
+ import { Button } from "../ui/button.js";
3
4
  import { Skeleton } from "../ui/skeleton.js";
4
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { widgetPollTarget } from "@alfakit/core";
6
+ import { useState } from "react";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
8
  //#region src/components/panel/lazy-placeholder.tsx
6
9
  /**
7
10
  * Stands in for a lazy widget or relation manager until the `alfakitLazy` deferred prop
@@ -21,7 +24,7 @@ function LazyPlaceholder({ variant = "widget", className }) {
21
24
  children: t("loading", {}, "Loading…")
22
25
  }),
23
26
  /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-1/3" }),
24
- variant === "table" ? /* @__PURE__ */ jsxs(Fragment, { children: [
27
+ variant === "table" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
25
28
  /* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full" }),
26
29
  /* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full" }),
27
30
  /* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full" })
@@ -29,7 +32,38 @@ function LazyPlaceholder({ variant = "widget", className }) {
29
32
  ]
30
33
  });
31
34
  }
35
+ /**
36
+ * A lazy widget's cell: the skeleton while its schema loads, or, when the request failed
37
+ * (outside panels: a refused or failed fetch), a message with a retry.
38
+ */
39
+ function LazyWidgetPlaceholder({ widget }) {
40
+ const t = useT();
41
+ const refresh = useRefreshComponents();
42
+ const [retrying, setRetrying] = useState(false);
43
+ const variant = widget.type === "TableWidget" ? "table" : "widget";
44
+ if (!widget.failed) return /* @__PURE__ */ jsx(LazyPlaceholder, { variant });
45
+ const retry = () => {
46
+ if (!refresh) return;
47
+ setRetrying(true);
48
+ Promise.resolve(refresh([widgetPollTarget(widget)])).finally(() => setRetrying(false));
49
+ };
50
+ return /* @__PURE__ */ jsxs("div", {
51
+ "data-slot": "lazy-placeholder",
52
+ "data-variant": variant,
53
+ "data-failed": "",
54
+ role: "alert",
55
+ className: "bg-card text-muted-foreground flex min-h-28 flex-col items-center justify-center gap-3 rounded-xl border border-dashed p-6 text-center text-sm",
56
+ children: [/* @__PURE__ */ jsx("p", { children: t("widget_failed", {}, "This widget could not be loaded.") }), refresh && /* @__PURE__ */ jsx(Button, {
57
+ type: "button",
58
+ variant: "outline",
59
+ size: "sm",
60
+ disabled: retrying,
61
+ onClick: retry,
62
+ children: t("retry", {}, "Retry")
63
+ })]
64
+ });
65
+ }
32
66
  //#endregion
33
- export { LazyPlaceholder };
67
+ export { LazyPlaceholder, LazyWidgetPlaceholder };
34
68
 
35
69
  //# sourceMappingURL=lazy-placeholder.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lazy-placeholder.js","names":[],"sources":["../../../src/components/panel/lazy-placeholder.tsx"],"sourcesContent":["import { useT } from '../../context'\nimport { cn } from '../../lib/utils'\nimport { Skeleton } from '../ui/skeleton'\n\n/**\n * Stands in for a lazy widget or relation manager until the `alfakitLazy` deferred prop\n * brings its schema. `table` mimics a table (relation managers, table widgets).\n */\nexport function LazyPlaceholder({\n variant = 'widget',\n className,\n}: {\n variant?: 'widget' | 'table'\n className?: string\n}) {\n const t = useT()\n\n return (\n <div\n data-slot=\"lazy-placeholder\"\n data-variant={variant}\n role=\"status\"\n aria-busy=\"true\"\n className={cn('bg-card flex flex-col gap-3 rounded-xl border p-6', className)}\n >\n <span className=\"sr-only\">{t('loading', {}, 'Loading…')}</span>\n <Skeleton className=\"h-5 w-1/3\" />\n {variant === 'table' ? (\n <>\n <Skeleton className=\"h-9 w-full\" />\n <Skeleton className=\"h-9 w-full\" />\n <Skeleton className=\"h-9 w-full\" />\n </>\n ) : (\n <Skeleton className=\"h-20 w-full\" />\n )}\n </div>\n )\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,gBAAgB,EAC9B,UAAU,UACV,aAIC;CACD,MAAM,IAAI,KAAK;CAEf,OACE,qBAAC,OAAD;EACE,aAAU;EACV,gBAAc;EACd,MAAK;EACL,aAAU;EACV,WAAW,GAAG,qDAAqD,SAAS;EAL9E,UAAA;GAOE,oBAAC,QAAD;IAAM,WAAU;IAAW,UAAA,EAAE,WAAW,CAAC,GAAG,UAAU;GAAQ,CAAA;GAC9D,oBAAC,UAAD,EAAU,WAAU,YAAa,CAAA;GAChC,YAAY,UACX,qBAAA,UAAA,EAAA,UAAA;IACE,oBAAC,UAAD,EAAU,WAAU,aAAc,CAAA;IAClC,oBAAC,UAAD,EAAU,WAAU,aAAc,CAAA;IAClC,oBAAC,UAAD,EAAU,WAAU,aAAc,CAAA;GAClC,EAAA,CAAA,IAEF,oBAAC,UAAD,EAAU,WAAU,cAAe,CAAA;EAElC;;AAET"}
1
+ {"version":3,"file":"lazy-placeholder.js","names":[],"sources":["../../../src/components/panel/lazy-placeholder.tsx"],"sourcesContent":["import { widgetPollTarget, type LazyWidgetSchema } from '@alfakit/core'\nimport { useState } from 'react'\nimport { useRefreshComponents, useT } from '../../context'\nimport { cn } from '../../lib/utils'\nimport { Button } from '../ui/button'\nimport { Skeleton } from '../ui/skeleton'\n\n/**\n * Stands in for a lazy widget or relation manager until the `alfakitLazy` deferred prop\n * brings its schema. `table` mimics a table (relation managers, table widgets).\n */\nexport function LazyPlaceholder({\n variant = 'widget',\n className,\n}: {\n variant?: 'widget' | 'table'\n className?: string\n}) {\n const t = useT()\n\n return (\n <div\n data-slot=\"lazy-placeholder\"\n data-variant={variant}\n role=\"status\"\n aria-busy=\"true\"\n className={cn('bg-card flex flex-col gap-3 rounded-xl border p-6', className)}\n >\n <span className=\"sr-only\">{t('loading', {}, 'Loading…')}</span>\n <Skeleton className=\"h-5 w-1/3\" />\n {variant === 'table' ? (\n <>\n <Skeleton className=\"h-9 w-full\" />\n <Skeleton className=\"h-9 w-full\" />\n <Skeleton className=\"h-9 w-full\" />\n </>\n ) : (\n <Skeleton className=\"h-20 w-full\" />\n )}\n </div>\n )\n}\n\n/**\n * A lazy widget's cell: the skeleton while its schema loads, or, when the request failed\n * (outside panels: a refused or failed fetch), a message with a retry.\n */\nexport function LazyWidgetPlaceholder({ widget }: { widget: LazyWidgetSchema }) {\n const t = useT()\n const refresh = useRefreshComponents()\n const [retrying, setRetrying] = useState(false)\n const variant = widget.type === 'TableWidget' ? 'table' : 'widget'\n\n if (!widget.failed) return <LazyPlaceholder variant={variant} />\n\n const retry = () => {\n if (!refresh) return\n\n setRetrying(true)\n void Promise.resolve(refresh([widgetPollTarget(widget)])).finally(() => setRetrying(false))\n }\n\n return (\n <div\n data-slot=\"lazy-placeholder\"\n data-variant={variant}\n data-failed=\"\"\n role=\"alert\"\n className=\"bg-card text-muted-foreground flex min-h-28 flex-col items-center justify-center gap-3 rounded-xl border border-dashed p-6 text-center text-sm\"\n >\n <p>{t('widget_failed', {}, 'This widget could not be loaded.')}</p>\n {refresh && (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={retrying} onClick={retry}>\n {t('retry', {}, 'Retry')}\n </Button>\n )}\n </div>\n )\n}\n"],"mappings":";;;;;;;;;;;;AAWA,SAAgB,gBAAgB,EAC9B,UAAU,UACV,aAIC;CACD,MAAM,IAAI,KAAK;CAEf,OACE,qBAAC,OAAD;EACE,aAAU;EACV,gBAAc;EACd,MAAK;EACL,aAAU;EACV,WAAW,GAAG,qDAAqD,SAAS;EAL9E,UAAA;GAOE,oBAAC,QAAD;IAAM,WAAU;IAAW,UAAA,EAAE,WAAW,CAAC,GAAG,UAAU;GAAQ,CAAA;GAC9D,oBAAC,UAAD,EAAU,WAAU,YAAa,CAAA;GAChC,YAAY,UACX,qBAAA,YAAA,EAAA,UAAA;IACE,oBAAC,UAAD,EAAU,WAAU,aAAc,CAAA;IAClC,oBAAC,UAAD,EAAU,WAAU,aAAc,CAAA;IAClC,oBAAC,UAAD,EAAU,WAAU,aAAc,CAAA;GAClC,EAAA,CAAA,IAEF,oBAAC,UAAD,EAAU,WAAU,cAAe,CAAA;EAElC;;AAET;;;;;AAMA,SAAgB,sBAAsB,EAAE,UAAwC;CAC9E,MAAM,IAAI,KAAK;CACf,MAAM,UAAU,qBAAqB;CACrC,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAC9C,MAAM,UAAU,OAAO,SAAS,gBAAgB,UAAU;CAE1D,IAAI,CAAC,OAAO,QAAQ,OAAO,oBAAC,iBAAD,EAA0B,QAAU,CAAA;CAE/D,MAAM,cAAc;EAClB,IAAI,CAAC,SAAS;EAEd,YAAY,IAAI;EAChB,QAAa,QAAQ,QAAQ,CAAC,iBAAiB,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,YAAY,KAAK,CAAC;CAC5F;CAEA,OACE,qBAAC,OAAD;EACE,aAAU;EACV,gBAAc;EACd,eAAY;EACZ,MAAK;EACL,WAAU;EALZ,UAAA,CAOE,oBAAC,KAAD,EAAA,UAAI,EAAE,iBAAiB,CAAC,GAAG,kCAAkC,EAAK,CAAA,GACjE,WACC,oBAAC,QAAD;GAAQ,MAAK;GAAS,SAAQ;GAAU,MAAK;GAAK,UAAU;GAAU,SAAS;GAC5E,UAAA,EAAE,SAAS,CAAC,GAAG,OAAO;EACjB,CAAA,CAEP;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"pwa.d.ts","names":[],"sources":["../../../src/components/panel/pwa.tsx"],"mappings":";;;wBAqBsB,WAAW,GAAG,kBAAkB;;;;;wBAiBtC;;wBAwBA,gCAAY,IAAA"}
1
+ {"version":3,"file":"pwa.d.ts","names":[],"sources":["../../../src/components/panel/pwa.tsx"],"mappings":";;;wBAsBsB,WAAW,GAAG,kBAAkB;;;;;wBAiBtC;;wBA2BA,gCAAY,IAAA"}
@@ -2,6 +2,7 @@ import { usePanel, useT } from "../../context.js";
2
2
  import { Icon } from "../../lib/icons.js";
3
3
  import { DropdownMenuItem } from "../ui/dropdown-menu.js";
4
4
  import { useStore } from "../../hooks/use-store.js";
5
+ import { toastOptions } from "../notifications/flash-toasts.js";
5
6
  import { applyPwaUpdate, canInstallPwa, promptPwaInstall, pwaStore, startPwa, subscribeToPush, unsubscribeFromPush } from "@alfakit/core";
6
7
  import { useEffect } from "react";
7
8
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -11,7 +12,7 @@ import { toast } from "sonner";
11
12
  const UPDATE_TOAST_ID = "alfakit-pwa-update";
12
13
  /** Show the browser's install prompt, or the "Add to Home Screen" hint on iOS Safari. */
13
14
  async function installPwa(t) {
14
- if (await promptPwaInstall() === "ios") toast.info(t("pwa.ios_hint", {}, "To install this app, tap the Share button and then “Add to Home Screen”."), { duration: 1e4 });
15
+ if (await promptPwaInstall() === "ios") toast.info(t("pwa.ios_hint", {}, "To install this app, tap the Share button and then “Add to Home Screen”."), toastOptions({ duration: 1e4 }));
15
16
  }
16
17
  /**
17
18
  * `Panel::pwa()`: registers the service worker and offers "Reload" when a new version
@@ -27,14 +28,14 @@ function PwaManager() {
27
28
  }, [pwa]);
28
29
  useEffect(() => {
29
30
  if (!updateAvailable) return;
30
- toast(t("pwa.update_available", {}, "A new version is available"), {
31
+ toast(t("pwa.update_available", {}, "A new version is available"), toastOptions({
31
32
  id: UPDATE_TOAST_ID,
32
33
  duration: Infinity,
33
34
  action: {
34
35
  label: t("pwa.reload", {}, "Reload"),
35
36
  onClick: () => applyPwaUpdate()
36
37
  }
37
- });
38
+ }));
38
39
  }, [updateAvailable, t]);
39
40
  return null;
40
41
  }
@@ -47,10 +48,10 @@ function PwaMenuItems() {
47
48
  const push = panel.pwa.push && state.push !== "unsupported";
48
49
  const togglePush = async () => {
49
50
  const outcome = state.push === "subscribed" ? await unsubscribeFromPush() : await subscribeToPush();
50
- if (outcome === "subscribed") toast.success(t("pwa.notifications.enabled", {}, "Notifications are on for this device"));
51
- else if (outcome === "unsubscribed") toast.success(t("pwa.notifications.disabled", {}, "Notifications are off for this device"));
52
- else if (outcome === "denied") toast.error(t("pwa.notifications.blocked", {}, "Notifications are blocked in your browser settings"));
53
- else toast.error(t("pwa.notifications.failed", {}, "Couldn’t change notifications. Try again."));
51
+ if (outcome === "subscribed") toast.success(t("pwa.notifications.enabled", {}, "Notifications are on for this device"), toastOptions());
52
+ else if (outcome === "unsubscribed") toast.success(t("pwa.notifications.disabled", {}, "Notifications are off for this device"), toastOptions());
53
+ else if (outcome === "denied") toast.error(t("pwa.notifications.blocked", {}, "Notifications are blocked in your browser settings"), toastOptions());
54
+ else toast.error(t("pwa.notifications.failed", {}, "Couldn’t change notifications. Try again."), toastOptions());
54
55
  };
55
56
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [canInstallPwa(state) && /* @__PURE__ */ jsxs(DropdownMenuItem, {
56
57
  "data-slot": "pwa-install",
@@ -1 +1 @@
1
- {"version":3,"file":"pwa.js","names":[],"sources":["../../../src/components/panel/pwa.tsx"],"sourcesContent":["import {\n applyPwaUpdate,\n canInstallPwa,\n promptPwaInstall,\n pwaStore,\n startPwa,\n subscribeToPush,\n unsubscribeFromPush,\n type Translator,\n} from '@alfakit/core'\nimport { BellIcon, BellOffIcon, DownloadIcon } from 'lucide-react'\nimport { useEffect } from 'react'\nimport { toast } from 'sonner'\nimport { usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { Icon } from '../../lib/icons'\nimport { DropdownMenuItem } from '../ui/dropdown-menu'\n\nconst UPDATE_TOAST_ID = 'alfakit-pwa-update'\n\n/** Show the browser's install prompt, or the \"Add to Home Screen\" hint on iOS Safari. */\nexport async function installPwa(t: Translator['t']): Promise<void> {\n if ((await promptPwaInstall()) === 'ios') {\n toast.info(\n t(\n 'pwa.ios_hint',\n {},\n 'To install this app, tap the Share button and then “Add to Home Screen”.',\n ),\n { duration: 10000 },\n )\n }\n}\n\n/**\n * `Panel::pwa()`: registers the service worker and offers \"Reload\" when a new version\n * waits. Renders nothing; does nothing without `pwa` or during a server render.\n */\nexport function PwaManager() {\n const panel = usePanel()\n const t = useT()\n const pwa = panel?.pwa\n const { updateAvailable } = useStore(pwaStore)\n\n useEffect(() => {\n startPwa(pwa)\n }, [pwa])\n\n useEffect(() => {\n if (!updateAvailable) return\n\n toast(t('pwa.update_available', {}, 'A new version is available'), {\n id: UPDATE_TOAST_ID,\n duration: Infinity,\n action: { label: t('pwa.reload', {}, 'Reload'), onClick: () => applyPwaUpdate() },\n })\n }, [updateAvailable, t])\n\n return null\n}\n\n/** User menu entries: \"Install app\" and turning Web Push on or off for this browser. */\nexport function PwaMenuItems() {\n const panel = usePanel()\n const t = useT()\n const state = useStore(pwaStore)\n\n if (!panel?.pwa) return null\n\n const push = panel.pwa.push && state.push !== 'unsupported'\n\n const togglePush = async () => {\n const outcome =\n state.push === 'subscribed' ? await unsubscribeFromPush() : await subscribeToPush()\n\n if (outcome === 'subscribed') {\n toast.success(t('pwa.notifications.enabled', {}, 'Notifications are on for this device'))\n } else if (outcome === 'unsubscribed') {\n toast.success(t('pwa.notifications.disabled', {}, 'Notifications are off for this device'))\n } else if (outcome === 'denied') {\n toast.error(\n t('pwa.notifications.blocked', {}, 'Notifications are blocked in your browser settings'),\n )\n } else {\n toast.error(t('pwa.notifications.failed', {}, 'Couldn’t change notifications. Try again.'))\n }\n }\n\n return (\n <>\n {canInstallPwa(state) && (\n <DropdownMenuItem data-slot=\"pwa-install\" onSelect={() => void installPwa(t)}>\n <Icon name=\"alfakit::panels.user-menu.install-app-item\" fallback={DownloadIcon} />{' '}\n {t('pwa.install', {}, 'Install app')}\n </DropdownMenuItem>\n )}\n {push && (\n <DropdownMenuItem\n data-slot=\"pwa-push\"\n disabled={state.pushBusy || state.push === 'denied'}\n onSelect={() => void togglePush()}\n >\n {state.push === 'subscribed' ? (\n <>\n <Icon name=\"alfakit::panels.user-menu.push-off-item\" fallback={BellOffIcon} />{' '}\n {t('pwa.notifications.disable', {}, 'Turn off notifications')}\n </>\n ) : (\n <>\n <Icon name=\"alfakit::panels.user-menu.push-on-item\" fallback={BellIcon} />{' '}\n {t('pwa.notifications.enable', {}, 'Turn on notifications')}\n </>\n )}\n </DropdownMenuItem>\n )}\n </>\n )\n}\n"],"mappings":";;;;;;;;;;AAkBA,MAAM,kBAAkB;;AAGxB,eAAsB,WAAW,GAAmC;CAClE,IAAK,MAAM,iBAAiB,MAAO,OACjC,MAAM,KACJ,EACE,gBACA,CAAC,GACD,0EACF,GACA,EAAE,UAAU,IAAM,CACpB;AAEJ;;;;;AAMA,SAAgB,aAAa;CAC3B,MAAM,QAAQ,SAAS;CACvB,MAAM,IAAI,KAAK;CACf,MAAM,MAAM,OAAO;CACnB,MAAM,EAAE,oBAAoB,SAAS,QAAQ;CAE7C,gBAAgB;EACd,SAAS,GAAG;CACd,GAAG,CAAC,GAAG,CAAC;CAER,gBAAgB;EACd,IAAI,CAAC,iBAAiB;EAEtB,MAAM,EAAE,wBAAwB,CAAC,GAAG,4BAA4B,GAAG;GACjE,IAAI;GACJ,UAAU;GACV,QAAQ;IAAE,OAAO,EAAE,cAAc,CAAC,GAAG,QAAQ;IAAG,eAAe,eAAe;GAAE;EAClF,CAAC;CACH,GAAG,CAAC,iBAAiB,CAAC,CAAC;CAEvB,OAAO;AACT;;AAGA,SAAgB,eAAe;CAC7B,MAAM,QAAQ,SAAS;CACvB,MAAM,IAAI,KAAK;CACf,MAAM,QAAQ,SAAS,QAAQ;CAE/B,IAAI,CAAC,OAAO,KAAK,OAAO;CAExB,MAAM,OAAO,MAAM,IAAI,QAAQ,MAAM,SAAS;CAE9C,MAAM,aAAa,YAAY;EAC7B,MAAM,UACJ,MAAM,SAAS,eAAe,MAAM,oBAAoB,IAAI,MAAM,gBAAgB;EAEpF,IAAI,YAAY,cACd,MAAM,QAAQ,EAAE,6BAA6B,CAAC,GAAG,sCAAsC,CAAC;OACnF,IAAI,YAAY,gBACrB,MAAM,QAAQ,EAAE,8BAA8B,CAAC,GAAG,uCAAuC,CAAC;OACrF,IAAI,YAAY,UACrB,MAAM,MACJ,EAAE,6BAA6B,CAAC,GAAG,oDAAoD,CACzF;OAEA,MAAM,MAAM,EAAE,4BAA4B,CAAC,GAAG,2CAA2C,CAAC;CAE9F;CAEA,OACE,qBAAA,YAAA,EAAA,UAAA,CACG,cAAc,KAAK,KAClB,qBAAC,kBAAD;EAAkB,aAAU;EAAc,gBAAgB,KAAK,WAAW,CAAC;EAA3E,UAAA;GACE,oBAAC,MAAD;IAAM,MAAK;IAA6C,UAAU;GAAe,CAAA;GAAE;GAClF,EAAE,eAAe,CAAC,GAAG,aAAa;EACnB;CAEnB,CAAA,GAAA,QACC,oBAAC,kBAAD;EACE,aAAU;EACV,UAAU,MAAM,YAAY,MAAM,SAAS;EAC3C,gBAAgB,KAAK,WAAW;EAE/B,UAAA,MAAM,SAAS,eACd,qBAAA,YAAA,EAAA,UAAA;GACE,oBAAC,MAAD;IAAM,MAAK;IAA0C,UAAU;GAAc,CAAA;GAAE;GAC9E,EAAE,6BAA6B,CAAC,GAAG,wBAAwB;EAC5D,EAAA,CAAA,IAEF,qBAAA,YAAA,EAAA,UAAA;GACE,oBAAC,MAAD;IAAM,MAAK;IAAyC,UAAU;GAAW,CAAA;GAAE;GAC1E,EAAE,4BAA4B,CAAC,GAAG,uBAAuB;EAC1D,EAAA,CAAA;CAEY,CAAA,CAEpB,EAAA,CAAA;AAEN"}
1
+ {"version":3,"file":"pwa.js","names":[],"sources":["../../../src/components/panel/pwa.tsx"],"sourcesContent":["import {\n applyPwaUpdate,\n canInstallPwa,\n promptPwaInstall,\n pwaStore,\n startPwa,\n subscribeToPush,\n unsubscribeFromPush,\n type Translator,\n} from '@alfakit/core'\nimport { BellIcon, BellOffIcon, DownloadIcon } from 'lucide-react'\nimport { useEffect } from 'react'\nimport { toast } from 'sonner'\nimport { usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { Icon } from '../../lib/icons'\nimport { toastOptions } from '../notifications/flash-toasts'\nimport { DropdownMenuItem } from '../ui/dropdown-menu'\n\nconst UPDATE_TOAST_ID = 'alfakit-pwa-update'\n\n/** Show the browser's install prompt, or the \"Add to Home Screen\" hint on iOS Safari. */\nexport async function installPwa(t: Translator['t']): Promise<void> {\n if ((await promptPwaInstall()) === 'ios') {\n toast.info(\n t(\n 'pwa.ios_hint',\n {},\n 'To install this app, tap the Share button and then “Add to Home Screen”.',\n ),\n toastOptions({ duration: 10000 }),\n )\n }\n}\n\n/**\n * `Panel::pwa()`: registers the service worker and offers \"Reload\" when a new version\n * waits. Renders nothing; does nothing without `pwa` or during a server render.\n */\nexport function PwaManager() {\n const panel = usePanel()\n const t = useT()\n const pwa = panel?.pwa\n const { updateAvailable } = useStore(pwaStore)\n\n useEffect(() => {\n startPwa(pwa)\n }, [pwa])\n\n useEffect(() => {\n if (!updateAvailable) return\n\n toast(\n t('pwa.update_available', {}, 'A new version is available'),\n toastOptions({\n id: UPDATE_TOAST_ID,\n duration: Infinity,\n action: { label: t('pwa.reload', {}, 'Reload'), onClick: () => applyPwaUpdate() },\n }),\n )\n }, [updateAvailable, t])\n\n return null\n}\n\n/** User menu entries: \"Install app\" and turning Web Push on or off for this browser. */\nexport function PwaMenuItems() {\n const panel = usePanel()\n const t = useT()\n const state = useStore(pwaStore)\n\n if (!panel?.pwa) return null\n\n const push = panel.pwa.push && state.push !== 'unsupported'\n\n const togglePush = async () => {\n const outcome =\n state.push === 'subscribed' ? await unsubscribeFromPush() : await subscribeToPush()\n\n if (outcome === 'subscribed') {\n toast.success(\n t('pwa.notifications.enabled', {}, 'Notifications are on for this device'),\n toastOptions(),\n )\n } else if (outcome === 'unsubscribed') {\n toast.success(\n t('pwa.notifications.disabled', {}, 'Notifications are off for this device'),\n toastOptions(),\n )\n } else if (outcome === 'denied') {\n toast.error(\n t('pwa.notifications.blocked', {}, 'Notifications are blocked in your browser settings'),\n toastOptions(),\n )\n } else {\n toast.error(\n t('pwa.notifications.failed', {}, 'Couldn’t change notifications. Try again.'),\n toastOptions(),\n )\n }\n }\n\n return (\n <>\n {canInstallPwa(state) && (\n <DropdownMenuItem data-slot=\"pwa-install\" onSelect={() => void installPwa(t)}>\n <Icon name=\"alfakit::panels.user-menu.install-app-item\" fallback={DownloadIcon} />{' '}\n {t('pwa.install', {}, 'Install app')}\n </DropdownMenuItem>\n )}\n {push && (\n <DropdownMenuItem\n data-slot=\"pwa-push\"\n disabled={state.pushBusy || state.push === 'denied'}\n onSelect={() => void togglePush()}\n >\n {state.push === 'subscribed' ? (\n <>\n <Icon name=\"alfakit::panels.user-menu.push-off-item\" fallback={BellOffIcon} />{' '}\n {t('pwa.notifications.disable', {}, 'Turn off notifications')}\n </>\n ) : (\n <>\n <Icon name=\"alfakit::panels.user-menu.push-on-item\" fallback={BellIcon} />{' '}\n {t('pwa.notifications.enable', {}, 'Turn on notifications')}\n </>\n )}\n </DropdownMenuItem>\n )}\n </>\n )\n}\n"],"mappings":";;;;;;;;;;;AAmBA,MAAM,kBAAkB;;AAGxB,eAAsB,WAAW,GAAmC;CAClE,IAAK,MAAM,iBAAiB,MAAO,OACjC,MAAM,KACJ,EACE,gBACA,CAAC,GACD,0EACF,GACA,aAAa,EAAE,UAAU,IAAM,CAAC,CAClC;AAEJ;;;;;AAMA,SAAgB,aAAa;CAC3B,MAAM,QAAQ,SAAS;CACvB,MAAM,IAAI,KAAK;CACf,MAAM,MAAM,OAAO;CACnB,MAAM,EAAE,oBAAoB,SAAS,QAAQ;CAE7C,gBAAgB;EACd,SAAS,GAAG;CACd,GAAG,CAAC,GAAG,CAAC;CAER,gBAAgB;EACd,IAAI,CAAC,iBAAiB;EAEtB,MACE,EAAE,wBAAwB,CAAC,GAAG,4BAA4B,GAC1D,aAAa;GACX,IAAI;GACJ,UAAU;GACV,QAAQ;IAAE,OAAO,EAAE,cAAc,CAAC,GAAG,QAAQ;IAAG,eAAe,eAAe;GAAE;EAClF,CAAC,CACH;CACF,GAAG,CAAC,iBAAiB,CAAC,CAAC;CAEvB,OAAO;AACT;;AAGA,SAAgB,eAAe;CAC7B,MAAM,QAAQ,SAAS;CACvB,MAAM,IAAI,KAAK;CACf,MAAM,QAAQ,SAAS,QAAQ;CAE/B,IAAI,CAAC,OAAO,KAAK,OAAO;CAExB,MAAM,OAAO,MAAM,IAAI,QAAQ,MAAM,SAAS;CAE9C,MAAM,aAAa,YAAY;EAC7B,MAAM,UACJ,MAAM,SAAS,eAAe,MAAM,oBAAoB,IAAI,MAAM,gBAAgB;EAEpF,IAAI,YAAY,cACd,MAAM,QACJ,EAAE,6BAA6B,CAAC,GAAG,sCAAsC,GACzE,aAAa,CACf;OACK,IAAI,YAAY,gBACrB,MAAM,QACJ,EAAE,8BAA8B,CAAC,GAAG,uCAAuC,GAC3E,aAAa,CACf;OACK,IAAI,YAAY,UACrB,MAAM,MACJ,EAAE,6BAA6B,CAAC,GAAG,oDAAoD,GACvF,aAAa,CACf;OAEA,MAAM,MACJ,EAAE,4BAA4B,CAAC,GAAG,2CAA2C,GAC7E,aAAa,CACf;CAEJ;CAEA,OACE,qBAAA,YAAA,EAAA,UAAA,CACG,cAAc,KAAK,KAClB,qBAAC,kBAAD;EAAkB,aAAU;EAAc,gBAAgB,KAAK,WAAW,CAAC;EAA3E,UAAA;GACE,oBAAC,MAAD;IAAM,MAAK;IAA6C,UAAU;GAAe,CAAA;GAAE;GAClF,EAAE,eAAe,CAAC,GAAG,aAAa;EACnB;CAEnB,CAAA,GAAA,QACC,oBAAC,kBAAD;EACE,aAAU;EACV,UAAU,MAAM,YAAY,MAAM,SAAS;EAC3C,gBAAgB,KAAK,WAAW;EAE/B,UAAA,MAAM,SAAS,eACd,qBAAA,YAAA,EAAA,UAAA;GACE,oBAAC,MAAD;IAAM,MAAK;IAA0C,UAAU;GAAc,CAAA;GAAE;GAC9E,EAAE,6BAA6B,CAAC,GAAG,wBAAwB;EAC5D,EAAA,CAAA,IAEF,qBAAA,YAAA,EAAA,UAAA;GACE,oBAAC,MAAD;IAAM,MAAK;IAAyC,UAAU;GAAW,CAAA;GAAE;GAC1E,EAAE,4BAA4B,CAAC,GAAG,uBAAuB;EAC1D,EAAA,CAAA;CAEY,CAAA,CAEpB,EAAA,CAAA;AAEN"}
@@ -140,7 +140,7 @@ function ShortcutsOverlay({ open, onOpenChange, customizable, endpoint }) {
140
140
  type: "button",
141
141
  variant: "ghost",
142
142
  size: "xs",
143
- "aria-label": t("shortcuts.reset_label", { label: item.label }, `Restore the default shortcut for ${item.label}`),
143
+ "aria-label": t("shortcuts.reset_label", { label: item.label }, `Reset the shortcut for ${item.label}`),
144
144
  onClick: () => void save(item.id, null),
145
145
  children: t("shortcuts.reset", {}, "Reset")
146
146
  })
@@ -1 +1 @@
1
- {"version":3,"file":"shortcuts-overlay.js","names":[],"sources":["../../../src/components/panel/shortcuts-overlay.tsx"],"sourcesContent":["import type { ShortcutListGroup, ShortcutListItem } from '@alfakit/core'\nimport {\n ShortcutRecorder,\n flattenActionGroup,\n saveShortcutOverrides,\n shortcutOverlayGroups,\n shortcuts,\n} from '@alfakit/core'\nimport { useEffect, useState, useSyncExternalStore } from 'react'\nimport { usePageSchema, useT } from '../../context'\nimport { Button } from '../ui/button'\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog'\nimport { ShortcutKeys } from './shortcut-keys'\n\nconst subscribe = (listener: () => void) => shortcuts.subscribe(listener)\nconst version = () => shortcuts.getVersion()\n\n/**\n * The `?` overlay: every shortcut available on the page, grouped; users may change their\n * keys (stored on the server). `KeyboardShortcuts` loads it the first time it opens.\n */\nexport function ShortcutsOverlay({\n open,\n onOpenChange,\n customizable,\n endpoint,\n}: {\n open: boolean\n onOpenChange: (open: boolean) => void\n customizable: boolean\n endpoint?: string\n}) {\n const t = useT()\n const page = usePageSchema()\n useSyncExternalStore(subscribe, version, version)\n const [recording, setRecording] = useState<string | null>(null)\n const [message, setMessage] = useState('')\n\n const groups = shortcutOverlayGroups(\n shortcuts.list(),\n flattenActionGroup(page?.headerActions ?? []),\n )\n\n const save = async (id: string, keys: string[] | null) => {\n if (!endpoint) return\n\n try {\n shortcuts.setOverrides(await saveShortcutOverrides(endpoint, { [id]: keys }))\n setMessage(t('shortcuts.saved', {}, 'Shortcut saved.'))\n } catch {\n setMessage(t('shortcuts.save_failed', {}, 'The shortcut could not be saved.'))\n }\n }\n\n // While recording, the keys pressed are the new binding: one key (with modifiers) or a\n // sequence (\"g o\"), finished a second after the last key (Escape cancels). Listening in\n // the capture phase keeps the keys from closing the dialog or running another shortcut.\n useEffect(() => {\n if (!recording) return\n\n shortcuts.setPaused(true)\n\n const recorder = new ShortcutRecorder({\n onDone: (binding) => {\n setRecording(null)\n\n const conflict = shortcuts.conflict([binding], recording)\n\n if (conflict) {\n setMessage(\n t(\n 'shortcuts.conflict',\n { label: conflict.label },\n `Already used by “${conflict.label}”.`,\n ),\n )\n\n return\n }\n\n void save(recording, [binding])\n },\n })\n\n const listener = (event: KeyboardEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (event.key === 'Escape') {\n recorder.cancel()\n setRecording(null)\n setMessage(t('shortcuts.cancelled', {}, 'Change cancelled.'))\n\n return\n }\n\n recorder.press(event)\n }\n\n window.addEventListener('keydown', listener, true)\n\n return () => {\n recorder.cancel()\n window.removeEventListener('keydown', listener, true)\n shortcuts.setPaused(false)\n }\n // `save` and `t` only change with the endpoint and the locale.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recording])\n\n const groupLabel = (group: ShortcutListGroup['group']) =>\n ({\n general: t('shortcuts.groups.general', {}, 'General'),\n navigation: t('shortcuts.groups.navigation', {}, 'Navigation'),\n commands: t('shortcuts.groups.commands', {}, 'Commands'),\n actions: t('shortcuts.groups.actions', {}, 'Actions on this page'),\n })[group]\n\n const change = (item: ShortcutListItem) => {\n setMessage(\n t(\n 'shortcuts.recording',\n { label: item.label },\n `Press the new keys for “${item.label}”. Escape cancels.`,\n ),\n )\n setRecording(item.id)\n }\n\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n setRecording(null)\n setMessage('')\n onOpenChange(next)\n }}\n >\n <DialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-lg\">\n <div data-slot=\"shortcuts-overlay\" className=\"flex flex-col gap-4\">\n <DialogHeader>\n <DialogTitle>{t('shortcuts.title', {}, 'Keyboard shortcuts')}</DialogTitle>\n <DialogDescription>\n {t('shortcuts.description', {}, 'Shortcuts available on this page.')}\n </DialogDescription>\n </DialogHeader>\n <div role=\"status\" aria-live=\"polite\" className=\"text-muted-foreground min-h-5 text-sm\">\n {message}\n </div>\n <div className=\"flex flex-col gap-5\">\n {groups.map((group) => (\n <section\n key={group.group}\n data-group={group.group}\n aria-labelledby={`shortcuts-${group.group}`}\n >\n <h3\n id={`shortcuts-${group.group}`}\n className=\"text-muted-foreground mb-2 text-xs font-medium uppercase\"\n >\n {groupLabel(group.group)}\n </h3>\n <ul className=\"flex flex-col divide-y rounded-md border\">\n {group.items.map((item) => (\n <li\n key={item.id}\n data-shortcut={item.id}\n className=\"flex items-center gap-3 px-3 py-2 text-sm\"\n >\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {recording === item.id ? (\n <span className=\"text-primary text-xs\">\n {t('shortcuts.press_keys', {}, 'Press keys…')}\n </span>\n ) : item.keys.length === 0 ? (\n <span className=\"text-muted-foreground text-xs\">\n {t('shortcuts.off', {}, 'Off')}\n </span>\n ) : (\n <span className=\"flex flex-wrap items-center justify-end gap-2\">\n {item.keys.map((key) => (\n <ShortcutKeys key={key} binding={key} />\n ))}\n </span>\n )}\n {customizable && item.customizable && (\n <span className=\"flex shrink-0 gap-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={t(\n 'shortcuts.change_label',\n { label: item.label },\n `Change the shortcut for ${item.label}`,\n )}\n onClick={() => change(item)}\n >\n {t('shortcuts.change', {}, 'Change')}\n </Button>\n {item.keys.length > 0 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={t(\n 'shortcuts.turn_off_label',\n { label: item.label },\n `Turn off the shortcut for ${item.label}`,\n )}\n onClick={() => void save(item.id, [])}\n >\n {t('shortcuts.turn_off', {}, 'Turn off')}\n </Button>\n )}\n {item.customized && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={t(\n 'shortcuts.reset_label',\n { label: item.label },\n `Restore the default shortcut for ${item.label}`,\n )}\n onClick={() => void save(item.id, null)}\n >\n {t('shortcuts.reset', {}, 'Reset')}\n </Button>\n )}\n </span>\n )}\n </li>\n ))}\n </ul>\n </section>\n ))}\n </div>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"shortcuts-overlay.js","names":[],"sources":["../../../src/components/panel/shortcuts-overlay.tsx"],"sourcesContent":["import type { ShortcutListGroup, ShortcutListItem } from '@alfakit/core'\nimport {\n ShortcutRecorder,\n flattenActionGroup,\n saveShortcutOverrides,\n shortcutOverlayGroups,\n shortcuts,\n} from '@alfakit/core'\nimport { useEffect, useState, useSyncExternalStore } from 'react'\nimport { usePageSchema, useT } from '../../context'\nimport { Button } from '../ui/button'\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog'\nimport { ShortcutKeys } from './shortcut-keys'\n\nconst subscribe = (listener: () => void) => shortcuts.subscribe(listener)\nconst version = () => shortcuts.getVersion()\n\n/**\n * The `?` overlay: every shortcut available on the page, grouped; users may change their\n * keys (stored on the server). `KeyboardShortcuts` loads it the first time it opens.\n */\nexport function ShortcutsOverlay({\n open,\n onOpenChange,\n customizable,\n endpoint,\n}: {\n open: boolean\n onOpenChange: (open: boolean) => void\n customizable: boolean\n endpoint?: string\n}) {\n const t = useT()\n const page = usePageSchema()\n useSyncExternalStore(subscribe, version, version)\n const [recording, setRecording] = useState<string | null>(null)\n const [message, setMessage] = useState('')\n\n const groups = shortcutOverlayGroups(\n shortcuts.list(),\n flattenActionGroup(page?.headerActions ?? []),\n )\n\n const save = async (id: string, keys: string[] | null) => {\n if (!endpoint) return\n\n try {\n shortcuts.setOverrides(await saveShortcutOverrides(endpoint, { [id]: keys }))\n setMessage(t('shortcuts.saved', {}, 'Shortcut saved.'))\n } catch {\n setMessage(t('shortcuts.save_failed', {}, 'The shortcut could not be saved.'))\n }\n }\n\n // While recording, the keys pressed are the new binding: one key (with modifiers) or a\n // sequence (\"g o\"), finished a second after the last key (Escape cancels). Listening in\n // the capture phase keeps the keys from closing the dialog or running another shortcut.\n useEffect(() => {\n if (!recording) return\n\n shortcuts.setPaused(true)\n\n const recorder = new ShortcutRecorder({\n onDone: (binding) => {\n setRecording(null)\n\n const conflict = shortcuts.conflict([binding], recording)\n\n if (conflict) {\n setMessage(\n t(\n 'shortcuts.conflict',\n { label: conflict.label },\n `Already used by “${conflict.label}”.`,\n ),\n )\n\n return\n }\n\n void save(recording, [binding])\n },\n })\n\n const listener = (event: KeyboardEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (event.key === 'Escape') {\n recorder.cancel()\n setRecording(null)\n setMessage(t('shortcuts.cancelled', {}, 'Change cancelled.'))\n\n return\n }\n\n recorder.press(event)\n }\n\n window.addEventListener('keydown', listener, true)\n\n return () => {\n recorder.cancel()\n window.removeEventListener('keydown', listener, true)\n shortcuts.setPaused(false)\n }\n // `save` and `t` only change with the endpoint and the locale.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recording])\n\n const groupLabel = (group: ShortcutListGroup['group']) =>\n ({\n general: t('shortcuts.groups.general', {}, 'General'),\n navigation: t('shortcuts.groups.navigation', {}, 'Navigation'),\n commands: t('shortcuts.groups.commands', {}, 'Commands'),\n actions: t('shortcuts.groups.actions', {}, 'Actions on this page'),\n })[group]\n\n const change = (item: ShortcutListItem) => {\n setMessage(\n t(\n 'shortcuts.recording',\n { label: item.label },\n `Press the new keys for “${item.label}”. Escape cancels.`,\n ),\n )\n setRecording(item.id)\n }\n\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n setRecording(null)\n setMessage('')\n onOpenChange(next)\n }}\n >\n <DialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-lg\">\n <div data-slot=\"shortcuts-overlay\" className=\"flex flex-col gap-4\">\n <DialogHeader>\n <DialogTitle>{t('shortcuts.title', {}, 'Keyboard shortcuts')}</DialogTitle>\n <DialogDescription>\n {t('shortcuts.description', {}, 'Shortcuts available on this page.')}\n </DialogDescription>\n </DialogHeader>\n <div role=\"status\" aria-live=\"polite\" className=\"text-muted-foreground min-h-5 text-sm\">\n {message}\n </div>\n <div className=\"flex flex-col gap-5\">\n {groups.map((group) => (\n <section\n key={group.group}\n data-group={group.group}\n aria-labelledby={`shortcuts-${group.group}`}\n >\n <h3\n id={`shortcuts-${group.group}`}\n className=\"text-muted-foreground mb-2 text-xs font-medium uppercase\"\n >\n {groupLabel(group.group)}\n </h3>\n <ul className=\"flex flex-col divide-y rounded-md border\">\n {group.items.map((item) => (\n <li\n key={item.id}\n data-shortcut={item.id}\n className=\"flex items-center gap-3 px-3 py-2 text-sm\"\n >\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {recording === item.id ? (\n <span className=\"text-primary text-xs\">\n {t('shortcuts.press_keys', {}, 'Press keys…')}\n </span>\n ) : item.keys.length === 0 ? (\n <span className=\"text-muted-foreground text-xs\">\n {t('shortcuts.off', {}, 'Off')}\n </span>\n ) : (\n <span className=\"flex flex-wrap items-center justify-end gap-2\">\n {item.keys.map((key) => (\n <ShortcutKeys key={key} binding={key} />\n ))}\n </span>\n )}\n {customizable && item.customizable && (\n <span className=\"flex shrink-0 gap-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={t(\n 'shortcuts.change_label',\n { label: item.label },\n `Change the shortcut for ${item.label}`,\n )}\n onClick={() => change(item)}\n >\n {t('shortcuts.change', {}, 'Change')}\n </Button>\n {item.keys.length > 0 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={t(\n 'shortcuts.turn_off_label',\n { label: item.label },\n `Turn off the shortcut for ${item.label}`,\n )}\n onClick={() => void save(item.id, [])}\n >\n {t('shortcuts.turn_off', {}, 'Turn off')}\n </Button>\n )}\n {item.customized && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={t(\n 'shortcuts.reset_label',\n { label: item.label },\n `Reset the shortcut for ${item.label}`,\n )}\n onClick={() => void save(item.id, null)}\n >\n {t('shortcuts.reset', {}, 'Reset')}\n </Button>\n )}\n </span>\n )}\n </li>\n ))}\n </ul>\n </section>\n ))}\n </div>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":"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"}
@@ -117,7 +117,7 @@ function ImageCell({ column, cell }) {
117
117
  className: cn("flex items-center", schema.wrap && "flex-wrap", schema.stacked ? usesDefaultImageStack(schema) && "-space-x-2" : "gap-1.5", schema.wrap && schema.stacked && "gap-y-1.5"),
118
118
  children: [urls.slice(0, limit).map((url, index) => /* @__PURE__ */ jsx("img", {
119
119
  src: url,
120
- alt: cell.alt ?? "",
120
+ alt: cell.alt ?? (index === 0 ? column.label ?? "" : ""),
121
121
  style: {
122
122
  height: schema.height ?? size,
123
123
  width: schema.width ?? (schema.square || schema.circular ? size : void 0),
@@ -126,7 +126,7 @@ function ImageCell({ column, cell }) {
126
126
  ...extraProps(cell.extraImgAttributes, cn("object-cover", schema.circular ? "rounded-full" : "rounded-md", schema.stacked && usesDefaultImageStack(schema) && "ring-background ring-2"))
127
127
  }, index)), schema.limitedRemainingText && urls.length > limit && /* @__PURE__ */ jsxs("span", {
128
128
  "data-slot": "image-remaining",
129
- className: cn("text-muted-foreground ms-1", remainingTextSizeClass(schema.limitedRemainingTextSize)),
129
+ className: cn("text-muted-foreground", schema.stacked && usesDefaultImageStack(schema) ? "ms-3" : "ms-1", remainingTextSizeClass(schema.limitedRemainingTextSize)),
130
130
  children: ["+", urls.length - limit]
131
131
  })]
132
132
  });