@alfakit/core 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +1 -0
- package/dist/actions.js.map +1 -1
- package/dist/calendar.d.ts.map +1 -1
- package/dist/calendar.js +3 -0
- package/dist/calendar.js.map +1 -1
- package/dist/form.d.ts +6 -0
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +27 -3
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +7 -7
- package/dist/page-actions.d.ts.map +1 -1
- package/dist/page-actions.js +30 -1
- package/dist/page-actions.js.map +1 -1
- package/dist/prefetch.d.ts +3 -1
- package/dist/prefetch.d.ts.map +1 -1
- package/dist/prefetch.js +37 -3
- package/dist/prefetch.js.map +1 -1
- package/dist/pwa.d.ts +12 -0
- package/dist/pwa.d.ts.map +1 -1
- package/dist/pwa.js +28 -1
- package/dist/pwa.js.map +1 -1
- package/dist/rich-editor.d.ts +11 -0
- package/dist/rich-editor.d.ts.map +1 -1
- package/dist/rich-editor.js +29 -1
- package/dist/rich-editor.js.map +1 -1
- package/dist/schema/panel.d.ts +7 -0
- package/dist/schema/panel.d.ts.map +1 -1
- package/dist/schema/panel.js.map +1 -1
- package/dist/shortcuts.d.ts +27 -0
- package/dist/shortcuts.d.ts.map +1 -1
- package/dist/shortcuts.js +53 -2
- package/dist/shortcuts.js.map +1 -1
- package/dist/starter-kit.d.ts +27 -2
- package/dist/starter-kit.d.ts.map +1 -1
- package/dist/starter-kit.js +32 -2
- package/dist/starter-kit.js.map +1 -1
- package/dist/table-layout.d.ts +6 -2
- package/dist/table-layout.d.ts.map +1 -1
- package/dist/table-layout.js +5 -3
- package/dist/table-layout.js.map +1 -1
- package/dist/table.d.ts.map +1 -1
- package/dist/table.js +5 -2
- package/dist/table.js.map +1 -1
- package/dist/undo.d.ts +2 -0
- package/dist/undo.d.ts.map +1 -1
- package/dist/undo.js +21 -1
- package/dist/undo.js.map +1 -1
- package/package.json +1 -1
- package/themes.css +4 -0
package/dist/schema/panel.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"panel.js","names":[],"sources":["../../src/schema/panel.ts"],"sourcesContent":["import type { ActionSchema, AnyActionSchema } from './actions'\nimport type { CommandPaletteSchema, KeyboardShortcutsSchema } from './command-palette'\nimport type { Color, IconName, RecordKey, Width } from './common'\nimport type { SchemaComponent } from './components'\nimport type { DebugSchema } from './debug'\nimport type { DevToolsSchema } from './dev-tools'\nimport type { FormBlock, InfolistBlock, LinksBlock, SchemaBlock } from './form'\nimport type { NotificationSchema } from './notifications'\nimport type { PwaSchema } from './pwa'\nimport type { ConflictsSchema, PresenceSchema, RealtimeSchema } from './realtime'\nimport type { DeferredTabBadges } from './resources'\nimport type { StandaloneComponentSchema } from './standalone'\nimport type { RelationManagerSchema, RelationManagersBlock, TableBlock } from './table'\nimport type { WidgetSchema, WidgetsBlock } from './widgets'\n\nexport interface NavigationItemSchema {\n /** Text, or sanitised HTML when `labelHtml` is true (an `Htmlable` label). */\n label: string\n /** `label` is sanitised HTML: render it as markup (use `htmlLabelToText()` for text). */\n labelHtml?: boolean\n icon?: IconName\n /**\n * Split navigation (`alfakit.navigation` is an Inertia once prop): the icon of the item\n * while active, swapped in by `resolvePanelNavigation()`.\n */\n activeIcon?: IconName\n url?: string\n openInNewTab?: boolean\n active?: boolean\n badge?: string | number\n badgeColor?: Color\n badgeTooltip?: string\n /** `Panel::deferNavigationBadges()`: the item has a badge that loads after the page. */\n deferredBadge?: boolean\n /** Set by `resolvePanelNavigation()` while a deferred badge is loading (show a placeholder). */\n badgePending?: boolean\n /** `NavigationItem::extraAttributes()`: HTML attributes for the item's link (`class` merges). */\n extraAttributes?: Record<string, string | number | boolean | null>\n children?: NavigationItemSchema[]\n}\n\nexport interface NavigationGroupSchema {\n label?: string | null\n icon?: IconName\n collapsible: boolean\n collapsed?: boolean\n items: NavigationItemSchema[]\n /** `NavigationGroup::extraSidebarAttributes()`: HTML attributes for the group in the sidebar. */\n extraSidebarAttributes?: Record<string, string | number | boolean | null>\n /** `NavigationGroup::extraTopbarAttributes()`: HTML attributes for the group in the top navigation. */\n extraTopbarAttributes?: Record<string, string | number | boolean | null>\n}\n\n/** A navigation item's badge, from `navigationState` or the deferred `alfakitNavigationBadges` prop. */\nexport interface NavigationBadgeSchema {\n badge: string | number\n badgeColor?: Color\n badgeTooltip?: string\n}\n\n/**\n * The per-request part of the panel navigation, whose structure (`navigation`) is an\n * Inertia once prop. Items are addressed by position: `group.item[.child…]` (`0.2.1`).\n */\nexport interface NavigationStateSchema {\n /** The active items (an active child's ancestors included). */\n active?: string[]\n /** Badges, unless `Panel::deferNavigationBadges()` defers them. */\n badges?: Record<string, NavigationBadgeSchema>\n}\n\n/** One `starterKit()` panel's navigation, as shared with the host application's pages. */\n/** One `starterKit()` panel's dashboard widgets on a host page (`alfakitWidgets` prop). */\nexport interface StarterKitDashboardWidgetsSchema {\n /** The panel id. */\n id: string\n /** A standalone widgets payload bound to the panel (lazy loading and polling included). */\n component: StandaloneComponentSchema\n}\n\nexport interface StarterKitNavigationSchema {\n /** The panel id. */\n id: string\n /** The panel's home URL. */\n url: string\n brandName: string\n /** Empty for tenancy panels outside a tenant (their items link to tenant routes). */\n groups: NavigationGroupSchema[]\n /** What the drop-in header components render before they load. */\n widgets?: StarterKitWidgetsSchema\n}\n\n/**\n * The `widgets` of a `starterKit()` panel's shared navigation: what `<AlfakitSearch />`,\n * `<AlfakitNotifications />`, `<AlfakitTenantSwitcher />` and `<AlfakitLanguageSwitcher />`\n * show on any host page before their code and the panel data (`endpoint`) have loaded.\n */\nexport interface StarterKitWidgetsSchema {\n /** GET: the panel data the components render with (the `alfakit` prop of the panel's pages). */\n endpoint: string\n /** Global search or the command palette (tenancy panels: only inside a tenant). */\n search?: { keyBindings: string[]; placeholder: string }\n /** `databaseNotifications()`: the bell's label and the user's unread count. */\n notifications?: { label: string; unreadCount: number }\n /** Tenancy panels: the user's tenants; `current` is null on the host's own pages. */\n tenancy?: {\n current?: TenantSchema | null\n tenants: TenantSchema[]\n registrationUrl?: string\n /** The registration page's own label (`RegisterTenant::getLabel()`). */\n registrationLabel?: string\n searchable?: true\n }\n /**\n * `<AlfakitLanguageSwitcher />`: the button's label (\"Change language\") and the current\n * language, when the panel has a language switcher with two or more languages\n * (`locales()`). The languages and the locale endpoint come with the panel data.\n */\n language?: { label: string; current: LocaleSchema }\n /** While impersonating: the banner the header components show on the host's own pages. */\n impersonation?: ImpersonationSchema\n /**\n * `AiPlugin::copilot()` (alfasix/alfakit-ai, `$panel->starterKitWidget('copilot')`): the\n * label and shortcuts of `<AlfakitCopilot />` before its code loads. Tenancy panels: only\n * inside a tenant.\n */\n copilot?: { label: string; keyBindings: string[] }\n}\n\n/** A page's secondary navigation: cluster pages or a record's pages. */\nexport interface SubNavigationSchema {\n position: 'start' | 'end' | 'top'\n groups: NavigationGroupSchema[]\n}\n\n/**\n * An entry of the user or tenant menu: a link (`url`, `method: 'post'` for POST links) or\n * an `Action` with server logic or a modal (`action`), run through the action endpoint\n * with the component `userMenu` / `tenantMenu` and the page's token.\n */\nexport interface MenuItemSchema {\n label?: string\n icon?: IconName\n url?: string\n openInNewTab?: boolean\n color?: Color\n method?: 'post'\n action?: ActionSchema\n /** Group index (nested arrays in `userMenuItems()`); a separator divides groups. */\n group?: number\n}\n\n/** Action endpoint components of the menus (`Panel::USER_MENU_COMPONENT`, …). */\nexport const USER_MENU_COMPONENT = 'userMenu'\nexport const TENANT_MENU_COMPONENT = 'tenantMenu'\n\nexport interface PanelUserSchema {\n id: RecordKey\n name: string\n email?: string | null\n avatar?: string | null\n}\n\nexport interface TenantSchema {\n id: RecordKey\n /** Text, or sanitised HTML when `nameHtml` is true (an `Htmlable` `getAlfakitName()`). */\n name: string\n /** `name` is sanitised HTML: render it as markup (use `htmlLabelToText()` for text). */\n nameHtml?: boolean\n avatar?: string | null\n url: string\n /** `HasCurrentTenantLabel`: a caption shown with the current tenant (\"Active team\"). */\n label?: string | null\n}\n\n/** The panel's design, see `Panel::theme()`. */\nexport interface PanelThemeSchema {\n name: string\n /** Value of `data-alfakit-theme`, base theme first: `\"alfakit acme\"`. */\n attribute: string\n sidebar: 'inset' | 'floating' | 'flush'\n density: 'comfortable' | 'compact'\n radius?: string | null\n font?: string | null\n /** CSS value for `--alfakit-font`. */\n fontCss?: string | null\n /** CSS value for `--alfakit-heading-font`. */\n headingFontCss?: string | null\n /** Stylesheet that loads the font, if any. */\n fontUrl?: string | null\n /** Every font stylesheet to load (body and heading fonts). */\n fontUrls?: string[]\n /** Structure and component styles; mirrored as `data-*` attributes on `<html>`. */\n design: PanelDesignSchema\n /**\n * Icon aliases replaced with `Icons::register()` (`alfakit::panels.sidebar.collapse-button`\n * → a Lucide name, image URL or sanitised SVG). The chrome falls back to its own icon.\n */\n icons?: Record<string, string> | null\n}\n\nexport interface PanelDesignSchema {\n layout: 'sidebar' | 'topbar' | 'rail'\n nav: 'soft' | 'pill' | 'bar' | 'underline'\n header: 'plain' | 'banner' | 'band' | 'compact'\n surfaces: 'bordered' | 'elevated' | 'flat' | 'ring'\n inputs: 'outline' | 'filled' | 'underline'\n buttons: 'rounded' | 'pill' | 'square'\n tables: 'lined' | 'striped' | 'minimal'\n /**\n * Below the `md` breakpoint: `drawer` (default) or the `bottom` mobile preset (bottom\n * navigation bar, tables as stacked cards, slide-over modals, 44px touch targets and\n * sticky form actions). See `Panel::mobile()`.\n */\n mobile?: 'drawer' | 'bottom'\n}\n\n/**\n * One piece of slot content: server-rendered HTML or a schema component. `for` lists the\n * components (`relationManager:<class>`, `widget:<id>`) it is scoped to: such items only\n * render in a `<Slot scope=\"…\">` of a matching component. `when` (`Panel::slot(…, when:)`)\n * limits it to renders whose slot data matches: `{ column: 'title' }` only renders in the\n * `table.header-cell` slot of the `title` column; a list value matches any of its entries.\n */\nexport type SlotItemSchema = (\n { type: 'html'; html: string } | { type: 'component'; component: SchemaComponent }\n) & { for?: string[]; when?: Record<string, SlotDataValue | SlotDataValue[]> }\n\n/** A value in a slot's render data (`<Slot data={{ column: 'title' }}>`). */\nexport type SlotDataValue = string | number | boolean | null\n\n/** A language the panel can be displayed in. */\nexport interface LocaleSchema {\n /** Laravel-style code: `en`, `pt_BR`, `zh_Hant`. */\n code: string\n /** BCP 47 tag for `Intl` and the `lang` attribute: `pt-BR`. */\n tag: string\n /** The language's own name: `Deutsch`, `العربية`. */\n label: string\n direction: 'ltr' | 'rtl'\n}\n\n/** The current request's language, formats and the panel's language list. */\nexport interface LocalizationSchema extends LocaleSchema {\n fallbackLocale: string\n /** 0 = Sunday … 6 = Saturday. */\n firstDayOfWeek: number\n /** Default ISO 4217 currency. */\n currency: string\n /** IANA timezone dates are displayed in. */\n timezone: string\n /** Whether to show a language menu. */\n switcher: boolean\n /**\n * Where panel pages offer the menu (`Panel::languageSwitcher()`): the user menu (default\n * when absent), a top bar button, both, or neither. Auth pages keep their switcher.\n */\n switcherPosition?: LanguageSwitcherPosition\n /** Languages to offer in the menu (empty when the switcher is off). */\n locales: LocaleSchema[]\n}\n\nexport type LanguageSwitcherPosition = 'user-menu' | 'topbar' | 'both' | 'hidden'\n\nexport interface PanelEndpoints {\n action: string\n modal: string\n schemaRefresh: string\n schemaOptions: string\n /** Validates one wizard step's fields (`step` = the Step component's key). */\n schemaValidate?: string\n /** Loads `deferLoading()` children and `Tab::deferBadge()` badges (`{schema}::{key}`). */\n schemaDeferred?: string\n tableCell: string\n tableReorder: string\n /** Stores a user's column order (`reorderableColumns()`). */\n tableColumns?: string\n /** `Table::deferTotals()`: the record count and summaries, loaded after the rows. */\n tableTotals?: string\n /** `Table::savedViews()`: save, share, rename, delete and pick a default view. */\n tableViews?: string\n /** `Table::spreadsheet()`: batch save (or validate) grid edits. */\n tableCells?: string\n /** Fresh schemas of lazy and polled components by token (components outside panels). */\n components?: string\n locale: string\n /** Calls `#[Expose]` methods of custom pages (`usePageMethod()`). */\n pageCall?: string\n /** Edit pages: what changed since the form was loaded (`detectConflicts()`). */\n conflicts?: string\n upload: string\n uploadDestroy: string\n /** Starts a resumable chunked upload (`{uploadChunks}/{id}` takes chunks, `/complete` finishes). */\n uploadChunks?: string\n globalSearch: string\n /** Command palette: \"create\" links and recent records (GET). */\n commandPalette?: string\n /** Stores the user's customised keyboard shortcuts (POST `{shortcuts}`). */\n shortcuts?: string\n /** `Action::undoable()`: undoes an action from its toast (POST `{undo}`). */\n undo?: string\n /** `Dashboard::customizable()`: saves or resets the user's layout (POST `{token, layout}`). */\n dashboardLayout?: string\n notifications: string\n notificationsReadAll: string\n notificationsDestroyAll: string\n logout: string\n}\n\n/** Where toasts appear (`Panel::toastPosition()`); `start`/`end` follow the reading direction. */\nexport type ToastPosition =\n 'top-start' | 'top-center' | 'top-end' | 'bottom-start' | 'bottom-center' | 'bottom-end'\n\n/** Shown while impersonating another user (`ImpersonateAction`). */\nexport interface ImpersonationSchema {\n /** \"You are signed in as …\", translated on the server. */\n label: string\n leaveLabel: string\n /** POST here to return to the real user. */\n leaveUrl: string\n}\n\n/** The `alfakit` prop shared with every page of a panel. */\n/**\n * One `errorNotifications` entry: the text to show, or `hidden` (caught, nothing shown,\n * `hiddenErrorNotification()`) or `disabled` (default handling, `disabledErrorNotification()`).\n */\nexport interface ErrorNotificationEntrySchema {\n title?: string\n body?: string | null\n icon?: IconName\n hidden?: boolean\n disabled?: boolean\n}\n\n/** `errorNotifications`, keyed by HTTP status plus `default`. */\nexport type ErrorNotificationsSchema = Record<string, ErrorNotificationEntrySchema>\n\nexport interface PanelSchema {\n id: string\n /**\n * The schema contract version the server serialised (`SchemaContract::VERSION`). Renderers\n * compare its major part with `SCHEMA_CONTRACT_VERSION`; absent on servers before 1.0.\n */\n schemaContract?: string\n path: string\n url?: string\n /** Where \"G then D\" goes when it isn't `url`: the app's `/dashboard` for a starter kit panel without a dashboard page. */\n dashboardUrl?: string\n brandName: string\n brandLogo?: string\n darkModeBrandLogo?: string\n brandLogoHeight?: string\n favicon?: string\n /**\n * Colour custom properties without the `--alfakit-` prefix: `primary`, its readable\n * `primary-foreground` and, for palettes (`Color::Indigo`), `primary-50` … `primary-950`.\n */\n colors?: Record<string, string>\n /**\n * CSS for custom colour names (`colors(['brand' => …])`): `.alfakit-color-{name}` rules\n * that point the generic colour utilities of themes.css at `--alfakit-{name}`.\n */\n colorCss?: string\n darkMode: {\n enabled: boolean\n forced: boolean\n /** `defaultThemeMode()`; absent for `system`. */\n default?: 'light' | 'dark'\n /** `false` when `themeSwitcher(false)`: no switcher, always the default mode. */\n switcher?: false\n }\n /** `false` when `topbar(false)`: sidebar layouts render no top bar (a slim bar on phones). */\n topbar?: false\n sidebarCollapsibleOnDesktop: boolean\n sidebarFullyCollapsibleOnDesktop?: boolean\n topNavigation?: boolean\n maxContentWidth?: Width | string\n breadcrumbs: boolean\n unsavedChangesAlerts?: boolean\n /** `false` when `collapsibleNavigationGroups(false)`: groups never collapse. */\n collapsibleNavigationGroups?: false\n /** CSS lengths for the open and icon-only sidebar (`--sidebar-width`, `--sidebar-width-icon`). */\n sidebarWidth?: string\n collapsedSidebarWidth?: string\n /** Max width of `auth` layout pages (login, registration…). */\n simplePageMaxContentWidth?: Width | string\n /** `false` when `spa(false)`: every link loads a full page. */\n spa?: false\n /** URL patterns (`*` wildcard) that load with a full page request. */\n spaUrlExceptions?: string[]\n /**\n * Notifications shown instead of Inertia's error modal, keyed by HTTP status plus\n * `default`. Absent when disabled or in debug mode. A page's own `errorNotifications`\n * replaces it while that page is open.\n */\n errorNotifications?: ErrorNotificationsSchema\n theme: PanelThemeSchema\n /** Content inserted with `Panel::slot()`, keyed by slot name. */\n slots?: Record<string, SlotItemSchema[]>\n /**\n * Empty for `starterKit()` panels: their navigation is the `alfakitNavigation` page prop,\n * which every page of the host application carries (not only the panel's).\n */\n navigation: NavigationGroupSchema[]\n /**\n * The active items and badges of this request; `navigation` holds only the structure\n * (sent once). Read the navigation through `usePanel()`, which merges the two.\n */\n navigationState?: NavigationStateSchema\n /**\n * `Panel::starterKit()`: the panel runs inside a Laravel starter kit app. Its pages render\n * in the host's own layout (sidebar, header, appearance) and the host's shadcn tokens;\n * the panel's own chrome, theme attributes and colour scheme are not applied.\n */\n starterKit?: true\n user?: PanelUserSchema\n userMenu?: {\n items: MenuItemSchema[]\n profileUrl?: string\n logoutUrl: string\n /** The `profile` entry as customised in `userMenuItems()` (replaces the built-in link). */\n profile?: MenuItemSchema\n /** The customised `logout` entry, or `false` when it is hidden. */\n logout?: MenuItemSchema | false\n /**\n * `topbar` when `userMenu(position: UserMenuPosition::Topbar)`: sidebar layouts show the\n * menu in the top bar instead of the sidebar footer. Omitted for the sidebar.\n */\n position?: 'topbar'\n }\n tenancy?: {\n current?: TenantSchema | null\n tenants: TenantSchema[]\n registrationUrl?: string\n profileUrl?: string\n /** The registration page's own label (`RegisterTenant::getLabel()`); else \"New\". */\n registrationLabel?: string\n /** The profile page's own label (`EditTenantProfile::getLabel()`); else \"Settings\". */\n profileLabel?: string\n /** The customised `register` entry of `tenantMenuItems()`. */\n registration?: MenuItemSchema\n /** The customised `profile` entry of `tenantMenuItems()`. */\n profile?: MenuItemSchema\n menu?: MenuItemSchema[]\n /** `false` when `tenantSwitcher(false)`: `tenants` is empty and the menu lists no others. */\n switcher?: false\n /** `searchableTenantMenu()`, or more than 10 tenants: show a search field in the menu. */\n searchable?: boolean\n }\n databaseNotifications?: {\n pollingInterval?: string\n unreadCount: number\n /**\n * `sidebar` when `databaseNotifications(position: 'sidebar')` or `topbar(false)`: sidebar\n * layouts show the bell in the sidebar instead of the top bar. Omitted for the top bar.\n */\n position?: 'sidebar'\n }\n globalSearch?: {\n keyBindings: string[]\n placeholder: string\n /** `globalSearchDebounce()` in milliseconds; renderers default to 250. */\n debounce?: number\n /** `globalSearchFieldSuffix()`: text shown at the end of the field instead of the key hint. */\n suffix?: string\n /** `false` when `globalSearchFieldKeyBindingSuffix(false)`: no key binding hint. */\n keyBindingSuffix?: false\n /**\n * `sidebar` when `globalSearch(position: GlobalSearchPosition::Sidebar)` or\n * `topbar(false)`: sidebar layouts show the field in the sidebar header. Omitted for the top bar.\n */\n position?: 'sidebar'\n }\n endpoints: Partial<PanelEndpoints> & { logout: string }\n /** `alfakit.uploads.chunk_size` in bytes: larger files are uploaded in chunks. */\n uploadChunkSize?: number\n auth: { login?: string; register?: string; passwordReset?: string }\n flashKey: string\n /** Present while impersonating: render a banner with a way back. */\n impersonation?: ImpersonationSchema\n /**\n * `Panel::realtimeNotifications()`: the private channel to listen on with Laravel Echo;\n * with `Panel::realtime()` also the connection (`live`, `authEndpoint`, `echo`).\n */\n realtime?: RealtimeSchema\n /** Omitted for the default (`top-end`). */\n toastPosition?: ToastPosition\n /** The command palette's extras: page actions, panel commands, navigation, create links, recent records. */\n commandPalette?: CommandPaletteSchema\n /** Keyboard shortcuts (`false` with `keyboardShortcuts(false)`); absent for guests. */\n keyboardShortcuts?: KeyboardShortcutsSchema | false\n /** `Panel::pwa()`: service worker, install prompt and Web Push; absent when off. */\n pwa?: PwaSchema\n localization: LocalizationSchema\n csrf: string\n translations: Record<string, unknown>\n}\n\nexport interface BreadcrumbSchema {\n label: string\n url?: string\n}\n\nexport type ContentBlock =\n | FormBlock\n | InfolistBlock\n | SchemaBlock\n | TableBlock\n | RelationManagersBlock\n | WidgetsBlock\n | LinksBlock\n | { type: string; key: string; [key: string]: unknown }\n\nexport type PageLayout = 'app' | 'auth' | 'blank' | 'fullscreen'\n\n/** The `page` prop rendered by `Alfakit/Page`. */\nexport interface PageSchema {\n class: string\n layout: PageLayout\n title: string\n heading?: string\n subheading?: string\n breadcrumbs?: BreadcrumbSchema[]\n maxContentWidth?: Width | string\n /** `fullscreen` layout: whether to show the slim header, and where its exit link goes. */\n fullscreen?: { header: boolean; exitUrl?: string }\n /** Cluster or record sub-navigation. */\n subNavigation?: SubNavigationSchema\n headerActions?: AnyActionSchema[]\n /** `$headerActionsAlignment`; absent for `end` (the default). */\n headerActionsAlignment?: 'start' | 'center' | 'end' | 'between' | 'justify'\n /** `$extraBodyAttributes` / `getExtraBodyAttributes()`: set on `<body>` while the page is open. */\n extraBodyAttributes?: Record<string, string>\n content: ContentBlock[]\n /** Encrypted context token for internal endpoints. */\n token: string\n record?: RecordKey\n view?: { component: string; props: Record<string, unknown> }\n /**\n * An action whose modal the renderer opens once the page hydrated (`$defaultAction`,\n * `?defaultAction=`, `?tableAction=&tableActionRecord=`).\n */\n defaultAction?: DefaultActionSchema\n /**\n * Page override of the panel's `errorNotifications` (`$hasErrorNotifications`,\n * `setUpErrorNotifications()`): `false` turns them off on this page. Absent when the\n * page follows the panel.\n */\n errorNotifications?: ErrorNotificationsSchema | false\n /** Record pages with `Panel::realtime()`: who else is on the record. */\n presence?: PresenceSchema\n /** Edit pages with `detectConflicts()`: the version the form was loaded from. */\n conflicts?: ConflictsSchema\n}\n\n/** `PageSchema::defaultAction`: what to open and in which context. */\nexport interface DefaultActionSchema {\n action: ActionSchema\n /** Context component: `page`, `table`, `relationManager:…`. */\n component: string\n record?: RecordKey\n arguments?: Record<string, unknown>\n}\n\n/**\n * Fresh schemas returned by a poll, keyed by poll target: `table`, `relationManager:Class`\n * or `widget:<id or class>`.\n */\nexport type PolledComponents = Record<string, ContentBlock | WidgetSchema | RelationManagerSchema>\n\n/** Shape of `usePage().props` on an Alfakit page. */\nexport interface AlfakitPageProps {\n alfakit: PanelSchema\n page?: PageSchema\n /** Components refreshed by a poll (only present after one); merged into `page`. */\n alfakitPoll?: PolledComponents\n /**\n * Lazy widgets and relation managers (`Page::LAZY_PROP`), an Inertia deferred prop that\n * arrives right after the first response; `resolveLazyComponents()` puts them in `page`.\n */\n alfakitLazy?: PolledComponents\n /** Badges of `Tab::deferBadge()` list tabs, a deferred prop loaded after the page. */\n alfakitTabBadges?: DeferredTabBadges\n /**\n * `Panel::deferNavigationBadges()`: the navigation badges keyed by item id\n * (`navigationState`), a deferred prop loaded after the page.\n */\n alfakitNavigationBadges?: Record<string, NavigationBadgeSchema>\n alfakitAppearance?: 'light' | 'dark' | 'system'\n /** The stored sidebar state (`alfakit_sidebar_state` cookie); null when never toggled. */\n alfakitSidebarOpen?: boolean | null\n /**\n * The navigation of the application's `starterKit()` panels, shared with every Inertia\n * page (the host's own pages too) for the host sidebar. Null for guests.\n */\n alfakitNavigation?: StarterKitNavigationSchema[] | null\n /**\n * `starterKit()` panels: their dashboard widgets on the host's dashboard\n * (`starterKitDashboard()`), for `<AlfakitWidgets />`. Null on other pages, for guests\n * and users who may not open the panel.\n */\n alfakitWidgets?: StarterKitDashboardWidgetsSchema[] | null\n /**\n * The developer debug panel's data (`Page::DEBUG_PROP`): schema tree with visibility and\n * authorisation, evaluated closures and server timings. Only present while the panel's\n * debug panel is active; never in production.\n */\n alfakitDebug?: DebugSchema\n /**\n * The schema inspector's data (context identity, class links). Only present while the\n * panel's dev tools are on (`Panel::devTools()` with `app.debug`); never in production.\n */\n alfakitDevTools?: DevToolsSchema\n flash?: {\n alfakitNotifications?: NotificationSchema[]\n alfakitAction?: {\n name: string\n status: 'success' | 'halted' | 'cancelled'\n closeModal?: boolean\n deselectRecords?: boolean | null\n }\n [key: string]: unknown\n }\n errors?: Record<string, string>\n [key: string]: unknown\n}\n\nexport type { ActionSchema }\n"],"mappings":";;AAyJA,MAAa,sBAAsB;AACnC,MAAa,wBAAwB"}
|
|
1
|
+
{"version":3,"file":"panel.js","names":[],"sources":["../../src/schema/panel.ts"],"sourcesContent":["import type { ActionSchema, AnyActionSchema } from './actions'\nimport type { CommandPaletteSchema, KeyboardShortcutsSchema } from './command-palette'\nimport type { Color, IconName, RecordKey, Width } from './common'\nimport type { SchemaComponent } from './components'\nimport type { DebugSchema } from './debug'\nimport type { DevToolsSchema } from './dev-tools'\nimport type { FormBlock, InfolistBlock, LinksBlock, SchemaBlock } from './form'\nimport type { NotificationSchema } from './notifications'\nimport type { PwaSchema } from './pwa'\nimport type { ConflictsSchema, PresenceSchema, RealtimeSchema } from './realtime'\nimport type { DeferredTabBadges } from './resources'\nimport type { StandaloneComponentSchema } from './standalone'\nimport type { RelationManagerSchema, RelationManagersBlock, TableBlock } from './table'\nimport type { WidgetSchema, WidgetsBlock } from './widgets'\n\nexport interface NavigationItemSchema {\n /** Text, or sanitised HTML when `labelHtml` is true (an `Htmlable` label). */\n label: string\n /** `label` is sanitised HTML: render it as markup (use `htmlLabelToText()` for text). */\n labelHtml?: boolean\n icon?: IconName\n /**\n * Split navigation (`alfakit.navigation` is an Inertia once prop): the icon of the item\n * while active, swapped in by `resolvePanelNavigation()`.\n */\n activeIcon?: IconName\n url?: string\n openInNewTab?: boolean\n active?: boolean\n badge?: string | number\n badgeColor?: Color\n badgeTooltip?: string\n /** `Panel::deferNavigationBadges()`: the item has a badge that loads after the page. */\n deferredBadge?: boolean\n /** Set by `resolvePanelNavigation()` while a deferred badge is loading (show a placeholder). */\n badgePending?: boolean\n /** `NavigationItem::extraAttributes()`: HTML attributes for the item's link (`class` merges). */\n extraAttributes?: Record<string, string | number | boolean | null>\n children?: NavigationItemSchema[]\n}\n\nexport interface NavigationGroupSchema {\n label?: string | null\n icon?: IconName\n collapsible: boolean\n collapsed?: boolean\n items: NavigationItemSchema[]\n /** `NavigationGroup::extraSidebarAttributes()`: HTML attributes for the group in the sidebar. */\n extraSidebarAttributes?: Record<string, string | number | boolean | null>\n /** `NavigationGroup::extraTopbarAttributes()`: HTML attributes for the group in the top navigation. */\n extraTopbarAttributes?: Record<string, string | number | boolean | null>\n}\n\n/** A navigation item's badge, from `navigationState` or the deferred `alfakitNavigationBadges` prop. */\nexport interface NavigationBadgeSchema {\n badge: string | number\n badgeColor?: Color\n badgeTooltip?: string\n}\n\n/**\n * The per-request part of the panel navigation, whose structure (`navigation`) is an\n * Inertia once prop. Items are addressed by position: `group.item[.child…]` (`0.2.1`).\n */\nexport interface NavigationStateSchema {\n /** The active items (an active child's ancestors included). */\n active?: string[]\n /** Badges, unless `Panel::deferNavigationBadges()` defers them. */\n badges?: Record<string, NavigationBadgeSchema>\n}\n\n/** One `starterKit()` panel's navigation, as shared with the host application's pages. */\n/** One `starterKit()` panel's dashboard widgets on a host page (`alfakitWidgets` prop). */\nexport interface StarterKitDashboardWidgetsSchema {\n /** The panel id. */\n id: string\n /** A standalone widgets payload bound to the panel (lazy loading and polling included). */\n component: StandaloneComponentSchema\n}\n\nexport interface StarterKitNavigationSchema {\n /** The panel id. */\n id: string\n /** The panel's home URL. */\n url: string\n brandName: string\n /** Empty for tenancy panels outside a tenant (their items link to tenant routes). */\n groups: NavigationGroupSchema[]\n /**\n * The panel's own user menu links (`userMenuItems()`, plugin entries such as Billing's),\n * for `<AlfakitUserMenuItems />` in the kit's user menu. Links only (entries with server\n * logic or a modal need an AlfaKit page); omitted when there are none, and for tenancy\n * panels outside a tenant.\n */\n userMenu?: MenuItemSchema[]\n /** What the drop-in header components render before they load. */\n widgets?: StarterKitWidgetsSchema\n}\n\n/**\n * The `widgets` of a `starterKit()` panel's shared navigation: what `<AlfakitSearch />`,\n * `<AlfakitNotifications />`, `<AlfakitTenantSwitcher />` and `<AlfakitLanguageSwitcher />`\n * show on any host page before their code and the panel data (`endpoint`) have loaded.\n */\nexport interface StarterKitWidgetsSchema {\n /** GET: the panel data the components render with (the `alfakit` prop of the panel's pages). */\n endpoint: string\n /** Global search or the command palette (tenancy panels: only inside a tenant). */\n search?: { keyBindings: string[]; placeholder: string }\n /** `databaseNotifications()`: the bell's label and the user's unread count. */\n notifications?: { label: string; unreadCount: number }\n /** Tenancy panels: the user's tenants; `current` is null on the host's own pages. */\n tenancy?: {\n current?: TenantSchema | null\n tenants: TenantSchema[]\n registrationUrl?: string\n /** The registration page's own label (`RegisterTenant::getLabel()`). */\n registrationLabel?: string\n searchable?: true\n }\n /**\n * `<AlfakitLanguageSwitcher />`: the button's label (\"Change language\") and the current\n * language, when the panel has a language switcher with two or more languages\n * (`locales()`). The languages and the locale endpoint come with the panel data.\n */\n language?: { label: string; current: LocaleSchema }\n /** While impersonating: the banner the header components show on the host's own pages. */\n impersonation?: ImpersonationSchema\n /**\n * `AiPlugin::copilot()` (alfasix/alfakit-ai, `$panel->starterKitWidget('copilot')`): the\n * label and shortcuts of `<AlfakitCopilot />` before its code loads. Tenancy panels: only\n * inside a tenant.\n */\n copilot?: { label: string; keyBindings: string[] }\n}\n\n/** A page's secondary navigation: cluster pages or a record's pages. */\nexport interface SubNavigationSchema {\n position: 'start' | 'end' | 'top'\n groups: NavigationGroupSchema[]\n}\n\n/**\n * An entry of the user or tenant menu: a link (`url`, `method: 'post'` for POST links) or\n * an `Action` with server logic or a modal (`action`), run through the action endpoint\n * with the component `userMenu` / `tenantMenu` and the page's token.\n */\nexport interface MenuItemSchema {\n label?: string\n icon?: IconName\n url?: string\n openInNewTab?: boolean\n color?: Color\n method?: 'post'\n action?: ActionSchema\n /** Group index (nested arrays in `userMenuItems()`); a separator divides groups. */\n group?: number\n}\n\n/** Action endpoint components of the menus (`Panel::USER_MENU_COMPONENT`, …). */\nexport const USER_MENU_COMPONENT = 'userMenu'\nexport const TENANT_MENU_COMPONENT = 'tenantMenu'\n\nexport interface PanelUserSchema {\n id: RecordKey\n name: string\n email?: string | null\n avatar?: string | null\n}\n\nexport interface TenantSchema {\n id: RecordKey\n /** Text, or sanitised HTML when `nameHtml` is true (an `Htmlable` `getAlfakitName()`). */\n name: string\n /** `name` is sanitised HTML: render it as markup (use `htmlLabelToText()` for text). */\n nameHtml?: boolean\n avatar?: string | null\n url: string\n /** `HasCurrentTenantLabel`: a caption shown with the current tenant (\"Active team\"). */\n label?: string | null\n}\n\n/** The panel's design, see `Panel::theme()`. */\nexport interface PanelThemeSchema {\n name: string\n /** Value of `data-alfakit-theme`, base theme first: `\"alfakit acme\"`. */\n attribute: string\n sidebar: 'inset' | 'floating' | 'flush'\n density: 'comfortable' | 'compact'\n radius?: string | null\n font?: string | null\n /** CSS value for `--alfakit-font`. */\n fontCss?: string | null\n /** CSS value for `--alfakit-heading-font`. */\n headingFontCss?: string | null\n /** Stylesheet that loads the font, if any. */\n fontUrl?: string | null\n /** Every font stylesheet to load (body and heading fonts). */\n fontUrls?: string[]\n /** Structure and component styles; mirrored as `data-*` attributes on `<html>`. */\n design: PanelDesignSchema\n /**\n * Icon aliases replaced with `Icons::register()` (`alfakit::panels.sidebar.collapse-button`\n * → a Lucide name, image URL or sanitised SVG). The chrome falls back to its own icon.\n */\n icons?: Record<string, string> | null\n}\n\nexport interface PanelDesignSchema {\n layout: 'sidebar' | 'topbar' | 'rail'\n nav: 'soft' | 'pill' | 'bar' | 'underline'\n header: 'plain' | 'banner' | 'band' | 'compact'\n surfaces: 'bordered' | 'elevated' | 'flat' | 'ring'\n inputs: 'outline' | 'filled' | 'underline'\n buttons: 'rounded' | 'pill' | 'square'\n tables: 'lined' | 'striped' | 'minimal'\n /**\n * Below the `md` breakpoint: `drawer` (default) or the `bottom` mobile preset (bottom\n * navigation bar, tables as stacked cards, slide-over modals, 44px touch targets and\n * sticky form actions). See `Panel::mobile()`.\n */\n mobile?: 'drawer' | 'bottom'\n}\n\n/**\n * One piece of slot content: server-rendered HTML or a schema component. `for` lists the\n * components (`relationManager:<class>`, `widget:<id>`) it is scoped to: such items only\n * render in a `<Slot scope=\"…\">` of a matching component. `when` (`Panel::slot(…, when:)`)\n * limits it to renders whose slot data matches: `{ column: 'title' }` only renders in the\n * `table.header-cell` slot of the `title` column; a list value matches any of its entries.\n */\nexport type SlotItemSchema = (\n { type: 'html'; html: string } | { type: 'component'; component: SchemaComponent }\n) & { for?: string[]; when?: Record<string, SlotDataValue | SlotDataValue[]> }\n\n/** A value in a slot's render data (`<Slot data={{ column: 'title' }}>`). */\nexport type SlotDataValue = string | number | boolean | null\n\n/** A language the panel can be displayed in. */\nexport interface LocaleSchema {\n /** Laravel-style code: `en`, `pt_BR`, `zh_Hant`. */\n code: string\n /** BCP 47 tag for `Intl` and the `lang` attribute: `pt-BR`. */\n tag: string\n /** The language's own name: `Deutsch`, `العربية`. */\n label: string\n direction: 'ltr' | 'rtl'\n}\n\n/** The current request's language, formats and the panel's language list. */\nexport interface LocalizationSchema extends LocaleSchema {\n fallbackLocale: string\n /** 0 = Sunday … 6 = Saturday. */\n firstDayOfWeek: number\n /** Default ISO 4217 currency. */\n currency: string\n /** IANA timezone dates are displayed in. */\n timezone: string\n /** Whether to show a language menu. */\n switcher: boolean\n /**\n * Where panel pages offer the menu (`Panel::languageSwitcher()`): the user menu (default\n * when absent), a top bar button, both, or neither. Auth pages keep their switcher.\n */\n switcherPosition?: LanguageSwitcherPosition\n /** Languages to offer in the menu (empty when the switcher is off). */\n locales: LocaleSchema[]\n}\n\nexport type LanguageSwitcherPosition = 'user-menu' | 'topbar' | 'both' | 'hidden'\n\nexport interface PanelEndpoints {\n action: string\n modal: string\n schemaRefresh: string\n schemaOptions: string\n /** Validates one wizard step's fields (`step` = the Step component's key). */\n schemaValidate?: string\n /** Loads `deferLoading()` children and `Tab::deferBadge()` badges (`{schema}::{key}`). */\n schemaDeferred?: string\n tableCell: string\n tableReorder: string\n /** Stores a user's column order (`reorderableColumns()`). */\n tableColumns?: string\n /** `Table::deferTotals()`: the record count and summaries, loaded after the rows. */\n tableTotals?: string\n /** `Table::savedViews()`: save, share, rename, delete and pick a default view. */\n tableViews?: string\n /** `Table::spreadsheet()`: batch save (or validate) grid edits. */\n tableCells?: string\n /** Fresh schemas of lazy and polled components by token (components outside panels). */\n components?: string\n locale: string\n /** Calls `#[Expose]` methods of custom pages (`usePageMethod()`). */\n pageCall?: string\n /** Edit pages: what changed since the form was loaded (`detectConflicts()`). */\n conflicts?: string\n upload: string\n uploadDestroy: string\n /** Starts a resumable chunked upload (`{uploadChunks}/{id}` takes chunks, `/complete` finishes). */\n uploadChunks?: string\n globalSearch: string\n /** Command palette: \"create\" links and recent records (GET). */\n commandPalette?: string\n /** Stores the user's customised keyboard shortcuts (POST `{shortcuts}`). */\n shortcuts?: string\n /** `Action::undoable()`: undoes an action from its toast (POST `{undo}`). */\n undo?: string\n /** `Dashboard::customizable()`: saves or resets the user's layout (POST `{token, layout}`). */\n dashboardLayout?: string\n notifications: string\n notificationsReadAll: string\n notificationsDestroyAll: string\n logout: string\n}\n\n/** Where toasts appear (`Panel::toastPosition()`); `start`/`end` follow the reading direction. */\nexport type ToastPosition =\n 'top-start' | 'top-center' | 'top-end' | 'bottom-start' | 'bottom-center' | 'bottom-end'\n\n/** Shown while impersonating another user (`ImpersonateAction`). */\nexport interface ImpersonationSchema {\n /** \"You are signed in as …\", translated on the server. */\n label: string\n leaveLabel: string\n /** POST here to return to the real user. */\n leaveUrl: string\n}\n\n/** The `alfakit` prop shared with every page of a panel. */\n/**\n * One `errorNotifications` entry: the text to show, or `hidden` (caught, nothing shown,\n * `hiddenErrorNotification()`) or `disabled` (default handling, `disabledErrorNotification()`).\n */\nexport interface ErrorNotificationEntrySchema {\n title?: string\n body?: string | null\n icon?: IconName\n hidden?: boolean\n disabled?: boolean\n}\n\n/** `errorNotifications`, keyed by HTTP status plus `default`. */\nexport type ErrorNotificationsSchema = Record<string, ErrorNotificationEntrySchema>\n\nexport interface PanelSchema {\n id: string\n /**\n * The schema contract version the server serialised (`SchemaContract::VERSION`). Renderers\n * compare its major part with `SCHEMA_CONTRACT_VERSION`; absent on servers before 1.0.\n */\n schemaContract?: string\n path: string\n url?: string\n /** Where \"G then D\" goes when it isn't `url`: the app's `/dashboard` for a starter kit panel without a dashboard page. */\n dashboardUrl?: string\n brandName: string\n brandLogo?: string\n darkModeBrandLogo?: string\n brandLogoHeight?: string\n favicon?: string\n /**\n * Colour custom properties without the `--alfakit-` prefix: `primary`, its readable\n * `primary-foreground` and, for palettes (`Color::Indigo`), `primary-50` … `primary-950`.\n */\n colors?: Record<string, string>\n /**\n * CSS for custom colour names (`colors(['brand' => …])`): `.alfakit-color-{name}` rules\n * that point the generic colour utilities of themes.css at `--alfakit-{name}`.\n */\n colorCss?: string\n darkMode: {\n enabled: boolean\n forced: boolean\n /** `defaultThemeMode()`; absent for `system`. */\n default?: 'light' | 'dark'\n /** `false` when `themeSwitcher(false)`: no switcher, always the default mode. */\n switcher?: false\n }\n /** `false` when `topbar(false)`: sidebar layouts render no top bar (a slim bar on phones). */\n topbar?: false\n sidebarCollapsibleOnDesktop: boolean\n sidebarFullyCollapsibleOnDesktop?: boolean\n topNavigation?: boolean\n maxContentWidth?: Width | string\n breadcrumbs: boolean\n unsavedChangesAlerts?: boolean\n /** `false` when `collapsibleNavigationGroups(false)`: groups never collapse. */\n collapsibleNavigationGroups?: false\n /** CSS lengths for the open and icon-only sidebar (`--sidebar-width`, `--sidebar-width-icon`). */\n sidebarWidth?: string\n collapsedSidebarWidth?: string\n /** Max width of `auth` layout pages (login, registration…). */\n simplePageMaxContentWidth?: Width | string\n /** `false` when `spa(false)`: every link loads a full page. */\n spa?: false\n /** URL patterns (`*` wildcard) that load with a full page request. */\n spaUrlExceptions?: string[]\n /**\n * Notifications shown instead of Inertia's error modal, keyed by HTTP status plus\n * `default`. Absent when disabled or in debug mode. A page's own `errorNotifications`\n * replaces it while that page is open.\n */\n errorNotifications?: ErrorNotificationsSchema\n theme: PanelThemeSchema\n /** Content inserted with `Panel::slot()`, keyed by slot name. */\n slots?: Record<string, SlotItemSchema[]>\n /**\n * Empty for `starterKit()` panels: their navigation is the `alfakitNavigation` page prop,\n * which every page of the host application carries (not only the panel's).\n */\n navigation: NavigationGroupSchema[]\n /**\n * The active items and badges of this request; `navigation` holds only the structure\n * (sent once). Read the navigation through `usePanel()`, which merges the two.\n */\n navigationState?: NavigationStateSchema\n /**\n * `Panel::starterKit()`: the panel runs inside a Laravel starter kit app. Its pages render\n * in the host's own layout (sidebar, header, appearance) and the host's shadcn tokens;\n * the panel's own chrome, theme attributes and colour scheme are not applied.\n */\n starterKit?: true\n user?: PanelUserSchema\n userMenu?: {\n items: MenuItemSchema[]\n profileUrl?: string\n logoutUrl: string\n /** The `profile` entry as customised in `userMenuItems()` (replaces the built-in link). */\n profile?: MenuItemSchema\n /** The customised `logout` entry, or `false` when it is hidden. */\n logout?: MenuItemSchema | false\n /**\n * `topbar` when `userMenu(position: UserMenuPosition::Topbar)`: sidebar layouts show the\n * menu in the top bar instead of the sidebar footer. Omitted for the sidebar.\n */\n position?: 'topbar'\n }\n tenancy?: {\n current?: TenantSchema | null\n tenants: TenantSchema[]\n registrationUrl?: string\n profileUrl?: string\n /** The registration page's own label (`RegisterTenant::getLabel()`); else \"New\". */\n registrationLabel?: string\n /** The profile page's own label (`EditTenantProfile::getLabel()`); else \"Settings\". */\n profileLabel?: string\n /** The customised `register` entry of `tenantMenuItems()`. */\n registration?: MenuItemSchema\n /** The customised `profile` entry of `tenantMenuItems()`. */\n profile?: MenuItemSchema\n menu?: MenuItemSchema[]\n /** `false` when `tenantSwitcher(false)`: `tenants` is empty and the menu lists no others. */\n switcher?: false\n /** `searchableTenantMenu()`, or more than 10 tenants: show a search field in the menu. */\n searchable?: boolean\n }\n databaseNotifications?: {\n pollingInterval?: string\n unreadCount: number\n /**\n * `sidebar` when `databaseNotifications(position: 'sidebar')` or `topbar(false)`: sidebar\n * layouts show the bell in the sidebar instead of the top bar. Omitted for the top bar.\n */\n position?: 'sidebar'\n }\n globalSearch?: {\n keyBindings: string[]\n placeholder: string\n /** `globalSearchDebounce()` in milliseconds; renderers default to 250. */\n debounce?: number\n /** `globalSearchFieldSuffix()`: text shown at the end of the field instead of the key hint. */\n suffix?: string\n /** `false` when `globalSearchFieldKeyBindingSuffix(false)`: no key binding hint. */\n keyBindingSuffix?: false\n /**\n * `sidebar` when `globalSearch(position: GlobalSearchPosition::Sidebar)` or\n * `topbar(false)`: sidebar layouts show the field in the sidebar header. Omitted for the top bar.\n */\n position?: 'sidebar'\n }\n endpoints: Partial<PanelEndpoints> & { logout: string }\n /** `alfakit.uploads.chunk_size` in bytes: larger files are uploaded in chunks. */\n uploadChunkSize?: number\n auth: { login?: string; register?: string; passwordReset?: string }\n flashKey: string\n /** Present while impersonating: render a banner with a way back. */\n impersonation?: ImpersonationSchema\n /**\n * `Panel::realtimeNotifications()`: the private channel to listen on with Laravel Echo;\n * with `Panel::realtime()` also the connection (`live`, `authEndpoint`, `echo`).\n */\n realtime?: RealtimeSchema\n /** Omitted for the default (`top-end`). */\n toastPosition?: ToastPosition\n /** The command palette's extras: page actions, panel commands, navigation, create links, recent records. */\n commandPalette?: CommandPaletteSchema\n /** Keyboard shortcuts (`false` with `keyboardShortcuts(false)`); absent for guests. */\n keyboardShortcuts?: KeyboardShortcutsSchema | false\n /** `Panel::pwa()`: service worker, install prompt and Web Push; absent when off. */\n pwa?: PwaSchema\n localization: LocalizationSchema\n csrf: string\n translations: Record<string, unknown>\n}\n\nexport interface BreadcrumbSchema {\n label: string\n url?: string\n}\n\nexport type ContentBlock =\n | FormBlock\n | InfolistBlock\n | SchemaBlock\n | TableBlock\n | RelationManagersBlock\n | WidgetsBlock\n | LinksBlock\n | { type: string; key: string; [key: string]: unknown }\n\nexport type PageLayout = 'app' | 'auth' | 'blank' | 'fullscreen'\n\n/** The `page` prop rendered by `Alfakit/Page`. */\nexport interface PageSchema {\n class: string\n layout: PageLayout\n title: string\n heading?: string\n subheading?: string\n breadcrumbs?: BreadcrumbSchema[]\n maxContentWidth?: Width | string\n /** `fullscreen` layout: whether to show the slim header, and where its exit link goes. */\n fullscreen?: { header: boolean; exitUrl?: string }\n /** Cluster or record sub-navigation. */\n subNavigation?: SubNavigationSchema\n headerActions?: AnyActionSchema[]\n /** `$headerActionsAlignment`; absent for `end` (the default). */\n headerActionsAlignment?: 'start' | 'center' | 'end' | 'between' | 'justify'\n /** `$extraBodyAttributes` / `getExtraBodyAttributes()`: set on `<body>` while the page is open. */\n extraBodyAttributes?: Record<string, string>\n content: ContentBlock[]\n /** Encrypted context token for internal endpoints. */\n token: string\n record?: RecordKey\n view?: { component: string; props: Record<string, unknown> }\n /**\n * An action whose modal the renderer opens once the page hydrated (`$defaultAction`,\n * `?defaultAction=`, `?tableAction=&tableActionRecord=`).\n */\n defaultAction?: DefaultActionSchema\n /**\n * Page override of the panel's `errorNotifications` (`$hasErrorNotifications`,\n * `setUpErrorNotifications()`): `false` turns them off on this page. Absent when the\n * page follows the panel.\n */\n errorNotifications?: ErrorNotificationsSchema | false\n /** Record pages with `Panel::realtime()`: who else is on the record. */\n presence?: PresenceSchema\n /** Edit pages with `detectConflicts()`: the version the form was loaded from. */\n conflicts?: ConflictsSchema\n}\n\n/** `PageSchema::defaultAction`: what to open and in which context. */\nexport interface DefaultActionSchema {\n action: ActionSchema\n /** Context component: `page`, `table`, `relationManager:…`. */\n component: string\n record?: RecordKey\n arguments?: Record<string, unknown>\n}\n\n/**\n * Fresh schemas returned by a poll, keyed by poll target: `table`, `relationManager:Class`\n * or `widget:<id or class>`.\n */\nexport type PolledComponents = Record<string, ContentBlock | WidgetSchema | RelationManagerSchema>\n\n/** Shape of `usePage().props` on an Alfakit page. */\nexport interface AlfakitPageProps {\n alfakit: PanelSchema\n page?: PageSchema\n /** Components refreshed by a poll (only present after one); merged into `page`. */\n alfakitPoll?: PolledComponents\n /**\n * Lazy widgets and relation managers (`Page::LAZY_PROP`), an Inertia deferred prop that\n * arrives right after the first response; `resolveLazyComponents()` puts them in `page`.\n */\n alfakitLazy?: PolledComponents\n /** Badges of `Tab::deferBadge()` list tabs, a deferred prop loaded after the page. */\n alfakitTabBadges?: DeferredTabBadges\n /**\n * `Panel::deferNavigationBadges()`: the navigation badges keyed by item id\n * (`navigationState`), a deferred prop loaded after the page.\n */\n alfakitNavigationBadges?: Record<string, NavigationBadgeSchema>\n alfakitAppearance?: 'light' | 'dark' | 'system'\n /** The stored sidebar state (`alfakit_sidebar_state` cookie); null when never toggled. */\n alfakitSidebarOpen?: boolean | null\n /**\n * The navigation of the application's `starterKit()` panels, shared with every Inertia\n * page (the host's own pages too) for the host sidebar. Null for guests.\n */\n alfakitNavigation?: StarterKitNavigationSchema[] | null\n /**\n * `starterKit()` panels: their dashboard widgets on the host's dashboard\n * (`starterKitDashboard()`), for `<AlfakitWidgets />`. Null on other pages, for guests\n * and users who may not open the panel.\n */\n alfakitWidgets?: StarterKitDashboardWidgetsSchema[] | null\n /**\n * The developer debug panel's data (`Page::DEBUG_PROP`): schema tree with visibility and\n * authorisation, evaluated closures and server timings. Only present while the panel's\n * debug panel is active; never in production.\n */\n alfakitDebug?: DebugSchema\n /**\n * The schema inspector's data (context identity, class links). Only present while the\n * panel's dev tools are on (`Panel::devTools()` with `app.debug`); never in production.\n */\n alfakitDevTools?: DevToolsSchema\n flash?: {\n alfakitNotifications?: NotificationSchema[]\n alfakitAction?: {\n name: string\n status: 'success' | 'halted' | 'cancelled'\n closeModal?: boolean\n deselectRecords?: boolean | null\n }\n [key: string]: unknown\n }\n errors?: Record<string, string>\n [key: string]: unknown\n}\n\nexport type { ActionSchema }\n"],"mappings":";;AAgKA,MAAa,sBAAsB;AACnC,MAAa,wBAAwB"}
|
package/dist/shortcuts.d.ts
CHANGED
|
@@ -58,6 +58,33 @@ export declare function formatShortcut(binding: string): string;
|
|
|
58
58
|
* lone modifier and keys a binding can't hold.
|
|
59
59
|
*/
|
|
60
60
|
export declare function bindingFromEvent(event: KeyboardEvent): string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Whether two bindings can't both work: the same keys, or one is the start of the other's
|
|
63
|
+
* sequence ("g" would run before "g o" could finish).
|
|
64
|
+
*/
|
|
65
|
+
export declare function shortcutsClash(a: string, b: string): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Records a new binding in the shortcuts overlay: a key (with modifiers) or a sequence of up
|
|
68
|
+
* to `maxSteps` keys ("g o"). The binding is finished `timeout` ms after the last key (the
|
|
69
|
+
* matcher's sequence timeout) or at `maxSteps`. Escape is the caller's (cancel).
|
|
70
|
+
*/
|
|
71
|
+
export declare class ShortcutRecorder {
|
|
72
|
+
private readonly options;
|
|
73
|
+
private steps;
|
|
74
|
+
private timer;
|
|
75
|
+
constructor(options: {
|
|
76
|
+
onDone: (binding: string) => void;
|
|
77
|
+
onStep?: (steps: string[]) => void;
|
|
78
|
+
timeout?: number;
|
|
79
|
+
maxSteps?: number;
|
|
80
|
+
});
|
|
81
|
+
/** Record a keydown; returns false for keys a binding can't hold (lone modifiers). */
|
|
82
|
+
press(event: KeyboardEvent): boolean;
|
|
83
|
+
/** Stop without a binding. */
|
|
84
|
+
cancel(): void;
|
|
85
|
+
private finish;
|
|
86
|
+
private clearTimer;
|
|
87
|
+
}
|
|
61
88
|
export interface ShortcutRegistryOptions {
|
|
62
89
|
/** Milliseconds between the keys of a sequence (default 1000). */
|
|
63
90
|
sequenceTimeout?: number;
|
package/dist/shortcuts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shortcuts.d.ts","names":[],"sources":["../src/shortcuts.ts"],"mappings":";;;;;;;;;;;qBAkBa;;;;;;;wBAQG,kBAAkB;YAItB;iBAEK;;EAEf;;EAEA;EACA;EACA,QAAQ;;EAER,UAAU,OAAO;;EAEjB;;EAEA;;EAEA;;iBAGe,2BAA2B;;EAE1C;EACA;EACA;;;wBASc,gBAAgB;;wBAWhB,iBAAiB,QAAQ;;;;;wBAgDzB,oBAAoB,OAAO,eAAe,MAAM;;wBA4BhD,cAAc;;wBASd,eAAe;;;;;wBAQf,iBAAiB,OAAO;
|
|
1
|
+
{"version":3,"file":"shortcuts.d.ts","names":[],"sources":["../src/shortcuts.ts"],"mappings":";;;;;;;;;;;qBAkBa;;;;;;;wBAQG,kBAAkB;YAItB;iBAEK;;EAEf;;EAEA;EACA;EACA,QAAQ;;EAER,UAAU,OAAO;;EAEjB;;EAEA;;EAEA;;iBAGe,2BAA2B;;EAE1C;EACA;EACA;;;wBASc,gBAAgB;;wBAWhB,iBAAiB,QAAQ;;;;;wBAgDzB,oBAAoB,OAAO,eAAe,MAAM;;wBA4BhD,cAAc;;wBASd,eAAe;;;;;wBAQf,iBAAiB,OAAO;;;;;wBAsCxB,eAAe,WAAW;;;;;;qBAa7B;mBAMQ;UALX;UAEA;EAGW,YAAA;IACf,SAAS;IACT,UAAU;IACV;IACA;;;EAKJ,MAAM,OAAO;;EAqBb;UAKQ;UAUA;;iBAQO;;EAEf;;;;;;qBAaW;mBAWkB;mBAVZ;UACT;UACA;UACA;UACA;UACA;mBACS;UACT;UACA;EAEqB,YAAA,UAAS;;EAGtC,SAAS,YAAY;EAkBrB,IAAI;;EAKJ,QAAQ;;EAWR,aAAa,WAAW;EAWxB,gBAAgB;;EAKhB,WAAW;EAMX;;EAKA,UAAU;EAKV,QAAQ;;EASR,QAAQ;;EAYR,SAAS,gBAAgB,oBAAoB;;EAS7C,UAAU;;EAOV;;EAKA,cAAc,OAAO;UAkEb;UAYA;UAMA;UASA;UAWA;;;;;;qBAWG,WAAS;;;;;wBAMA,sBACpB,kBACA,SAAS,kCACR,QAAQ;;wBAUK,iBACd,OAAM;iBAaS;EACf;EACA;EACA;EACA;EACA;EACA;;iBAGe;EACf,OAAO;EACP,OAAO;;;;;;wBASO,sBACd,YAAY,sBACZ,UAAS;EACP;EACA;EACA;EACA;KAED"}
|
package/dist/shortcuts.js
CHANGED
|
@@ -114,6 +114,57 @@ function bindingFromEvent(event) {
|
|
|
114
114
|
return isValidShortcut(binding) ? binding : null;
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
|
+
* Whether two bindings can't both work: the same keys, or one is the start of the other's
|
|
118
|
+
* sequence ("g" would run before "g o" could finish).
|
|
119
|
+
*/
|
|
120
|
+
function shortcutsClash(a, b) {
|
|
121
|
+
const left = a.trim().toLowerCase().split(/\s+/);
|
|
122
|
+
const right = b.trim().toLowerCase().split(/\s+/);
|
|
123
|
+
const length = Math.min(left.length, right.length);
|
|
124
|
+
return left.slice(0, length).every((step, index) => step === right[index]);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Records a new binding in the shortcuts overlay: a key (with modifiers) or a sequence of up
|
|
128
|
+
* to `maxSteps` keys ("g o"). The binding is finished `timeout` ms after the last key (the
|
|
129
|
+
* matcher's sequence timeout) or at `maxSteps`. Escape is the caller's (cancel).
|
|
130
|
+
*/
|
|
131
|
+
var ShortcutRecorder = class {
|
|
132
|
+
options;
|
|
133
|
+
steps = [];
|
|
134
|
+
timer = null;
|
|
135
|
+
constructor(options) {
|
|
136
|
+
this.options = options;
|
|
137
|
+
}
|
|
138
|
+
/** Record a keydown; returns false for keys a binding can't hold (lone modifiers). */
|
|
139
|
+
press(event) {
|
|
140
|
+
const step = bindingFromEvent(event);
|
|
141
|
+
if (!step) return false;
|
|
142
|
+
this.steps.push(step);
|
|
143
|
+
this.options.onStep?.([...this.steps]);
|
|
144
|
+
this.clearTimer();
|
|
145
|
+
if (this.steps.length >= (this.options.maxSteps ?? 3)) this.finish();
|
|
146
|
+
else this.timer = setTimeout(() => this.finish(), this.options.timeout ?? 1e3);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
/** Stop without a binding. */
|
|
150
|
+
cancel() {
|
|
151
|
+
this.clearTimer();
|
|
152
|
+
this.steps = [];
|
|
153
|
+
}
|
|
154
|
+
finish() {
|
|
155
|
+
this.clearTimer();
|
|
156
|
+
const binding = this.steps.join(" ");
|
|
157
|
+
this.steps = [];
|
|
158
|
+
if (binding !== "") this.options.onDone(binding);
|
|
159
|
+
}
|
|
160
|
+
clearTimer() {
|
|
161
|
+
if (this.timer !== null) {
|
|
162
|
+
clearTimeout(this.timer);
|
|
163
|
+
this.timer = null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
117
168
|
* The shortcuts of the current page. Renderers register in effects (never while server
|
|
118
169
|
* rendering) and dispose on unmount; the registry listens on `window` while it has entries.
|
|
119
170
|
*/
|
|
@@ -197,7 +248,7 @@ var ShortcutRegistry = class {
|
|
|
197
248
|
}
|
|
198
249
|
/** Another shortcut already using one of these keys. */
|
|
199
250
|
conflict(keys, exceptId) {
|
|
200
|
-
return this.list().find((entry) => entry.id !== exceptId && entry.keys.some((key) => keys.
|
|
251
|
+
return this.list().find((entry) => entry.id !== exceptId && entry.keys.some((key) => keys.some((candidate) => shortcutsClash(key, candidate))));
|
|
201
252
|
}
|
|
202
253
|
/** Re-render hook for the overlay; returns an unsubscribe function. */
|
|
203
254
|
subscribe(listener) {
|
|
@@ -352,6 +403,6 @@ function shortcutOverlayGroups(registered, actions = []) {
|
|
|
352
403
|
}));
|
|
353
404
|
}
|
|
354
405
|
//#endregion
|
|
355
|
-
export { SHORTCUT_IDS, ShortcutRegistry, bindingFromEvent, commandShortcutId, focusTableSearch, formatShortcut, isEditableTarget, isValidShortcut, matchesShortcutStep, saveShortcutOverrides, shortcutOverlayGroups, shortcutSteps, shortcuts };
|
|
406
|
+
export { SHORTCUT_IDS, ShortcutRecorder, ShortcutRegistry, bindingFromEvent, commandShortcutId, focusTableSearch, formatShortcut, isEditableTarget, isValidShortcut, matchesShortcutStep, saveShortcutOverrides, shortcutOverlayGroups, shortcutSteps, shortcuts, shortcutsClash };
|
|
356
407
|
|
|
357
408
|
//# sourceMappingURL=shortcuts.js.map
|
package/dist/shortcuts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shortcuts.js","names":[],"sources":["../src/shortcuts.ts"],"sourcesContent":["import { requestJson } from './http'\nimport {\n formatKeyBinding,\n isMacLike,\n parseKeyBinding,\n type ParsedKeyBinding,\n} from './utils/keybindings'\n\n/**\n * Keyboard shortcuts: a registry of everything bound on the current page (built-ins, panel\n * commands' `shortcut()`), with the user's customised keys, sequences (`g d`), platform\n * modifiers (`mod` = ⌘ on macOS, Ctrl elsewhere) and no triggering while typing.\n *\n * Bindings are data: a step is `[modifier+]…key`, a binding is up to three steps separated\n * by spaces. Nothing is ever evaluated.\n */\n\n/** Ids of the built-in shortcuts. */\nexport const SHORTCUT_IDS = {\n palette: 'palette.open',\n search: 'search.focus',\n dashboard: 'go.dashboard',\n overlay: 'help.overlay',\n} as const\n\n/** Id of a panel command's shortcut. */\nexport function commandShortcutId(name: string): string {\n return `command:${name}`\n}\n\nexport type ShortcutGroup = 'general' | 'navigation' | 'commands' | 'actions'\n\nexport interface ShortcutDefinition {\n /** Stable id; the user's customised keys are stored under it. */\n id: string\n /** Default bindings: `mod+k`, `?`, `g d`. */\n keys: string[]\n label: string\n group?: ShortcutGroup\n /** Return `false` when there was nothing to do: the key then keeps its default behaviour. */\n handler: (event: KeyboardEvent | null) => boolean | void\n /** Also while typing in a field (by default only bindings with Ctrl/⌘/Alt are). */\n allowInInputs?: boolean\n /** Keeps working when the panel turns shortcuts off (the palette's own key bindings). */\n essential?: boolean\n /** `false`: the overlay doesn't offer to change it. */\n customizable?: boolean\n}\n\nexport interface RegisteredShortcut extends ShortcutDefinition {\n /** The keys in effect: the user's (an empty list = turned off) or the defaults. */\n keys: string[]\n defaultKeys: string[]\n customized: boolean\n}\n\nconst MODIFIER_KEYS = new Set(['shift', 'control', 'alt', 'meta', 'os', 'altgraph', 'capslock'])\n\nconst STEP_PATTERN =\n /^(?:(?:mod|ctrl|control|meta|cmd|command|alt|option|shift)\\+){0,4}(?:[a-z0-9]|f[1-9]|f1[0-2]|[/?.,;'[\\]\\\\=`-]|escape|esc|enter|tab|space|backspace|delete|arrowup|arrowdown|arrowleft|arrowright|home|end|pageup|pagedown)$/\n\n/** Whether a binding is well formed (the server applies the same rule). */\nexport function isValidShortcut(binding: string): boolean {\n if (binding === '' || binding.length > 60 || binding !== binding.toLowerCase()) {\n return false\n }\n\n const steps = binding.split(' ')\n\n return steps.length <= 3 && steps.every((step) => STEP_PATTERN.test(step))\n}\n\n/** Whether the event comes from a field the user is typing in. */\nexport function isEditableTarget(target: EventTarget | null): boolean {\n if (!target || typeof (target as HTMLElement).tagName !== 'string') {\n return false\n }\n\n const element = target as HTMLElement\n\n if (\n element.isContentEditable ||\n element.closest?.('[contenteditable=\"\"],[contenteditable=\"true\"]')\n ) {\n return true\n }\n\n if (element.tagName === 'TEXTAREA' || element.tagName === 'SELECT') {\n return true\n }\n\n if (element.tagName === 'INPUT') {\n const type = (element as HTMLInputElement).type\n return !['checkbox', 'radio', 'button', 'submit', 'reset', 'range', 'color', 'file'].includes(\n type,\n )\n }\n\n return element.getAttribute('role') === 'textbox' || element.getAttribute('role') === 'combobox'\n}\n\nfunction parseSteps(binding: string): ParsedKeyBinding[] {\n return binding\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map((step) => {\n const parsed = parseKeyBinding(step)\n\n return parsed.key === 'space' ? { ...parsed, key: ' ' } : parsed\n })\n}\n\nfunction hasModifier(step: ParsedKeyBinding | undefined): boolean {\n return Boolean(step && (step.mod || step.ctrl || step.meta || step.alt))\n}\n\n/**\n * Whether a keydown matches one step. Single punctuation keys (`?`, `/`) ignore Shift\n * unless the binding names it, since many layouts need Shift to type them.\n */\nexport function matchesShortcutStep(event: KeyboardEvent, step: ParsedKeyBinding): boolean {\n const key = (event.key ?? '').toLowerCase()\n const code = (event.code ?? '').toLowerCase()\n\n const keyMatches =\n key === step.key ||\n (step.key === 'escape' && key === 'esc') ||\n (/^[a-z]$/.test(step.key) && code === `key${step.key}`) ||\n (/^[0-9]$/.test(step.key) && code === `digit${step.key}`)\n\n if (!keyMatches) {\n return false\n }\n\n const mac = isMacLike()\n const wantsMeta = step.meta || (step.mod && mac)\n const wantsCtrl = step.ctrl || (step.mod && !mac)\n const punctuation = step.key.length === 1 && !/^[a-z0-9 ]$/.test(step.key)\n\n return (\n event.metaKey === wantsMeta &&\n event.ctrlKey === wantsCtrl &&\n event.altKey === step.alt &&\n (punctuation && !step.shift ? true : event.shiftKey === step.shift)\n )\n}\n\n/** The keys of a binding for display, one entry per step (`['G', 'D']`, `['⌘K']`). */\nexport function shortcutSteps(binding: string): string[] {\n return binding\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map((step) => (step === '?' || step === '/' ? step : formatKeyBinding(step)))\n}\n\n/** A binding for display: steps separated by spaces. */\nexport function formatShortcut(binding: string): string {\n return shortcutSteps(binding).join(' ')\n}\n\n/**\n * The binding a keydown would record in the overlay (\"press the new keys\"), or null for a\n * lone modifier and keys a binding can't hold.\n */\nexport function bindingFromEvent(event: KeyboardEvent): string | null {\n const key = (event.key ?? '').toLowerCase()\n\n if (key === '' || MODIFIER_KEYS.has(key) || key === 'dead' || key === 'unidentified') {\n return null\n }\n\n const mac = isMacLike()\n const parts: string[] = []\n\n if (mac ? event.metaKey : event.ctrlKey) parts.push('mod')\n if (mac && event.ctrlKey) parts.push('ctrl')\n if (!mac && event.metaKey) parts.push('meta')\n if (event.altKey) parts.push('alt')\n\n const named = key === ' ' ? 'space' : key\n const punctuation = named.length === 1 && !/^[a-z0-9]$/.test(named)\n // Letters and digits from `code`, so Alt/Option combinations don't record `å`.\n const fromCode = /^key[a-z]$/.test(event.code?.toLowerCase() ?? '')\n ? event.code.toLowerCase().slice(3)\n : /^digit[0-9]$/.test(event.code?.toLowerCase() ?? '')\n ? event.code.toLowerCase().slice(5)\n : null\n const finalKey = event.altKey && fromCode ? fromCode : named\n\n if (event.shiftKey && !punctuation) parts.push('shift')\n\n parts.push(finalKey)\n\n const binding = parts.join('+')\n\n return isValidShortcut(binding) ? binding : null\n}\n\nexport interface ShortcutRegistryOptions {\n /** Milliseconds between the keys of a sequence (default 1000). */\n sequenceTimeout?: number\n}\n\ninterface PendingSequence {\n id: string\n steps: ParsedKeyBinding[]\n index: number\n}\n\n/**\n * The shortcuts of the current page. Renderers register in effects (never while server\n * rendering) and dispose on unmount; the registry listens on `window` while it has entries.\n */\nexport class ShortcutRegistry {\n private readonly entries = new Map<string, ShortcutDefinition>()\n private overrides: Record<string, string[]> = {}\n private enabled = true\n private paused = false\n private pending: PendingSequence[] = []\n private timer: ReturnType<typeof setTimeout> | null = null\n private readonly listeners = new Set<() => void>()\n private detach: (() => void) | null = null\n private version = 0\n\n constructor(private readonly options: ShortcutRegistryOptions = {}) {}\n\n /** Add (or replace) a shortcut. Returns a disposer that removes this registration only. */\n register(definition: ShortcutDefinition): () => void {\n this.entries.set(definition.id, definition)\n this.attach()\n this.changed()\n\n return () => {\n if (this.entries.get(definition.id) === definition) {\n this.entries.delete(definition.id)\n this.changed()\n\n if (this.entries.size === 0) {\n this.detach?.()\n this.detach = null\n }\n }\n }\n }\n\n has(id: string): boolean {\n return this.entries.has(id)\n }\n\n /** Run a shortcut's handler as if its keys were pressed (e.g. from the palette). */\n trigger(id: string): boolean {\n const entry = this.entries.get(id)\n\n if (!entry) return false\n\n entry.handler(null)\n\n return true\n }\n\n /** The user's customised keys by id (from `alfakit.keyboardShortcuts.overrides`). */\n setOverrides(overrides: Record<string, string[]> | null | undefined): void {\n const next: Record<string, string[]> = {}\n\n for (const [id, keys] of Object.entries(overrides ?? {})) {\n if (Array.isArray(keys)) next[id] = keys.filter((key) => isValidShortcut(key))\n }\n\n this.overrides = next\n this.changed()\n }\n\n getOverrides(): Record<string, string[]> {\n return { ...this.overrides }\n }\n\n /** `keyboardShortcuts(false)`: only essential shortcuts (the palette's key bindings) stay. */\n setEnabled(enabled: boolean): void {\n this.enabled = enabled\n this.reset()\n this.changed()\n }\n\n isEnabled(): boolean {\n return this.enabled\n }\n\n /** Ignore keys while the overlay records a new binding. */\n setPaused(paused: boolean): void {\n this.paused = paused\n this.reset()\n }\n\n keysFor(id: string): string[] {\n const entry = this.entries.get(id)\n\n if (!entry) return []\n\n return this.overrides[id] ?? entry.keys\n }\n\n /** Every active shortcut with the keys in effect, in registration order. */\n list(): RegisteredShortcut[] {\n return [...this.entries.values()]\n .filter((entry) => this.enabled || entry.essential)\n .map((entry) => ({\n ...entry,\n keys: this.overrides[entry.id] ?? entry.keys,\n defaultKeys: entry.keys,\n customized: Object.prototype.hasOwnProperty.call(this.overrides, entry.id),\n }))\n }\n\n /** Another shortcut already using one of these keys. */\n conflict(keys: string[], exceptId?: string): RegisteredShortcut | undefined {\n return this.list().find(\n (entry) => entry.id !== exceptId && entry.keys.some((key) => keys.includes(key)),\n )\n }\n\n /** Re-render hook for the overlay; returns an unsubscribe function. */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n\n return () => this.listeners.delete(listener)\n }\n\n /** Bumped on every change (for `useSyncExternalStore`-style snapshots). */\n getVersion(): number {\n return this.version\n }\n\n /** Handle a keydown; returns whether a shortcut ran or a sequence continues. */\n handleKeydown(event: KeyboardEvent): boolean {\n if (this.paused || event.defaultPrevented || event.isComposing) {\n return false\n }\n\n const key = (event.key ?? '').toLowerCase()\n\n if (MODIFIER_KEYS.has(key)) {\n return false\n }\n\n const editable = isEditableTarget(event.target)\n\n if (this.pending.length > 0) {\n const advanced = this.pending.filter((pending) =>\n matchesShortcutStep(event, pending.steps[pending.index]!),\n )\n const complete = advanced.find((pending) => pending.index + 1 === pending.steps.length)\n\n if (complete) {\n this.reset()\n return this.run(complete.id, event)\n }\n\n this.reset()\n\n if (advanced.length > 0) {\n this.pending = advanced.map((pending) => ({ ...pending, index: pending.index + 1 }))\n this.startTimer()\n event.preventDefault()\n\n return true\n }\n }\n\n const started: PendingSequence[] = []\n\n for (const entry of this.entries.values()) {\n if (!this.enabled && !entry.essential) continue\n\n for (const binding of this.overrides[entry.id] ?? entry.keys) {\n const steps = parseSteps(binding)\n const first = steps[0]\n\n if (!first || (editable && !entry.allowInInputs && !hasModifier(first))) continue\n if (!matchesShortcutStep(event, first)) continue\n\n if (steps.length === 1) {\n this.reset()\n return this.run(entry.id, event)\n }\n\n if (!event.repeat) started.push({ id: entry.id, steps, index: 1 })\n }\n }\n\n if (started.length > 0) {\n this.pending = started\n this.startTimer()\n\n return true\n }\n\n return false\n }\n\n private run(id: string, event: KeyboardEvent): boolean {\n const entry = this.entries.get(id)\n\n if (!entry) return false\n\n if (entry.handler(event) === false) return false\n\n event.preventDefault()\n\n return true\n }\n\n private startTimer(): void {\n if (this.timer) clearTimeout(this.timer)\n\n this.timer = setTimeout(() => this.reset(), this.options.sequenceTimeout ?? 1000)\n }\n\n private reset(): void {\n this.pending = []\n\n if (this.timer) {\n clearTimeout(this.timer)\n this.timer = null\n }\n }\n\n private attach(): void {\n if (this.detach || typeof window === 'undefined') return\n\n const listener = (event: KeyboardEvent) => {\n this.handleKeydown(event)\n }\n\n window.addEventListener('keydown', listener)\n this.detach = () => window.removeEventListener('keydown', listener)\n }\n\n private changed(): void {\n this.version++\n\n for (const listener of this.listeners) listener()\n }\n}\n\n/**\n * The page's shortcut registry. Only touched from effects and event handlers, so a server\n * render never shares state between requests.\n */\nexport const shortcuts = new ShortcutRegistry()\n\n/**\n * Store customised keys (`null` restores the default, `[]` turns the shortcut off) and\n * return every stored override.\n */\nexport async function saveShortcutOverrides(\n endpoint: string,\n changes: Record<string, string[] | null>,\n): Promise<Record<string, string[]>> {\n const response = await requestJson<{ shortcuts: Record<string, string[]> }>(endpoint, {\n method: 'POST',\n body: { shortcuts: changes },\n })\n\n return response.shortcuts ?? {}\n}\n\n/** Focus the page's table search field (`/`). Returns whether one was found. */\nexport function focusTableSearch(\n root: ParentNode | null = typeof document === 'undefined' ? null : document,\n): boolean {\n const content = root?.querySelector('[data-slot=\"content\"]') ?? root\n const input = content?.querySelector<HTMLInputElement>('input[type=\"search\"]:not([disabled])')\n\n if (!input) return false\n\n input.focus()\n input.select?.()\n\n return true\n}\n\nexport interface ShortcutListItem {\n id: string\n label: string\n keys: string[]\n defaultKeys: string[]\n customized: boolean\n customizable: boolean\n}\n\nexport interface ShortcutListGroup {\n group: ShortcutGroup\n items: ShortcutListItem[]\n}\n\nconst GROUP_ORDER: ShortcutGroup[] = ['general', 'navigation', 'commands', 'actions']\n\n/**\n * What the overlay lists: registered shortcuts plus the page's action key bindings\n * (`keyBindings()`, listed only), grouped and in a stable order.\n */\nexport function shortcutOverlayGroups(\n registered: RegisteredShortcut[],\n actions: Array<{\n name: string\n label?: string | null\n keyBindings?: string[]\n disabled?: boolean\n }> = [],\n): ShortcutListGroup[] {\n const groups = new Map<ShortcutGroup, ShortcutListItem[]>()\n const add = (group: ShortcutGroup, item: ShortcutListItem) => {\n groups.set(group, [...(groups.get(group) ?? []), item])\n }\n\n for (const entry of registered) {\n add(entry.group ?? 'general', {\n id: entry.id,\n label: entry.label,\n keys: entry.keys,\n defaultKeys: entry.defaultKeys,\n customized: entry.customized,\n customizable: entry.customizable !== false,\n })\n }\n\n for (const action of actions) {\n if (!action.keyBindings?.length || action.disabled) continue\n\n add('actions', {\n id: `action:${action.name}`,\n label: action.label ?? action.name,\n keys: action.keyBindings,\n defaultKeys: action.keyBindings,\n customized: false,\n customizable: false,\n })\n }\n\n return GROUP_ORDER.filter((group) => groups.has(group)).map((group) => ({\n group,\n items: groups.get(group)!,\n }))\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"shortcuts.js","names":[],"sources":["../src/shortcuts.ts"],"sourcesContent":["import { requestJson } from './http'\nimport {\n formatKeyBinding,\n isMacLike,\n parseKeyBinding,\n type ParsedKeyBinding,\n} from './utils/keybindings'\n\n/**\n * Keyboard shortcuts: a registry of everything bound on the current page (built-ins, panel\n * commands' `shortcut()`), with the user's customised keys, sequences (`g d`), platform\n * modifiers (`mod` = ⌘ on macOS, Ctrl elsewhere) and no triggering while typing.\n *\n * Bindings are data: a step is `[modifier+]…key`, a binding is up to three steps separated\n * by spaces. Nothing is ever evaluated.\n */\n\n/** Ids of the built-in shortcuts. */\nexport const SHORTCUT_IDS = {\n palette: 'palette.open',\n search: 'search.focus',\n dashboard: 'go.dashboard',\n overlay: 'help.overlay',\n} as const\n\n/** Id of a panel command's shortcut. */\nexport function commandShortcutId(name: string): string {\n return `command:${name}`\n}\n\nexport type ShortcutGroup = 'general' | 'navigation' | 'commands' | 'actions'\n\nexport interface ShortcutDefinition {\n /** Stable id; the user's customised keys are stored under it. */\n id: string\n /** Default bindings: `mod+k`, `?`, `g d`. */\n keys: string[]\n label: string\n group?: ShortcutGroup\n /** Return `false` when there was nothing to do: the key then keeps its default behaviour. */\n handler: (event: KeyboardEvent | null) => boolean | void\n /** Also while typing in a field (by default only bindings with Ctrl/⌘/Alt are). */\n allowInInputs?: boolean\n /** Keeps working when the panel turns shortcuts off (the palette's own key bindings). */\n essential?: boolean\n /** `false`: the overlay doesn't offer to change it. */\n customizable?: boolean\n}\n\nexport interface RegisteredShortcut extends ShortcutDefinition {\n /** The keys in effect: the user's (an empty list = turned off) or the defaults. */\n keys: string[]\n defaultKeys: string[]\n customized: boolean\n}\n\nconst MODIFIER_KEYS = new Set(['shift', 'control', 'alt', 'meta', 'os', 'altgraph', 'capslock'])\n\nconst STEP_PATTERN =\n /^(?:(?:mod|ctrl|control|meta|cmd|command|alt|option|shift)\\+){0,4}(?:[a-z0-9]|f[1-9]|f1[0-2]|[/?.,;'[\\]\\\\=`-]|escape|esc|enter|tab|space|backspace|delete|arrowup|arrowdown|arrowleft|arrowright|home|end|pageup|pagedown)$/\n\n/** Whether a binding is well formed (the server applies the same rule). */\nexport function isValidShortcut(binding: string): boolean {\n if (binding === '' || binding.length > 60 || binding !== binding.toLowerCase()) {\n return false\n }\n\n const steps = binding.split(' ')\n\n return steps.length <= 3 && steps.every((step) => STEP_PATTERN.test(step))\n}\n\n/** Whether the event comes from a field the user is typing in. */\nexport function isEditableTarget(target: EventTarget | null): boolean {\n if (!target || typeof (target as HTMLElement).tagName !== 'string') {\n return false\n }\n\n const element = target as HTMLElement\n\n if (\n element.isContentEditable ||\n element.closest?.('[contenteditable=\"\"],[contenteditable=\"true\"]')\n ) {\n return true\n }\n\n if (element.tagName === 'TEXTAREA' || element.tagName === 'SELECT') {\n return true\n }\n\n if (element.tagName === 'INPUT') {\n const type = (element as HTMLInputElement).type\n return !['checkbox', 'radio', 'button', 'submit', 'reset', 'range', 'color', 'file'].includes(\n type,\n )\n }\n\n return element.getAttribute('role') === 'textbox' || element.getAttribute('role') === 'combobox'\n}\n\nfunction parseSteps(binding: string): ParsedKeyBinding[] {\n return binding\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map((step) => {\n const parsed = parseKeyBinding(step)\n\n return parsed.key === 'space' ? { ...parsed, key: ' ' } : parsed\n })\n}\n\nfunction hasModifier(step: ParsedKeyBinding | undefined): boolean {\n return Boolean(step && (step.mod || step.ctrl || step.meta || step.alt))\n}\n\n/**\n * Whether a keydown matches one step. Single punctuation keys (`?`, `/`) ignore Shift\n * unless the binding names it, since many layouts need Shift to type them.\n */\nexport function matchesShortcutStep(event: KeyboardEvent, step: ParsedKeyBinding): boolean {\n const key = (event.key ?? '').toLowerCase()\n const code = (event.code ?? '').toLowerCase()\n\n const keyMatches =\n key === step.key ||\n (step.key === 'escape' && key === 'esc') ||\n (/^[a-z]$/.test(step.key) && code === `key${step.key}`) ||\n (/^[0-9]$/.test(step.key) && code === `digit${step.key}`)\n\n if (!keyMatches) {\n return false\n }\n\n const mac = isMacLike()\n const wantsMeta = step.meta || (step.mod && mac)\n const wantsCtrl = step.ctrl || (step.mod && !mac)\n const punctuation = step.key.length === 1 && !/^[a-z0-9 ]$/.test(step.key)\n\n return (\n event.metaKey === wantsMeta &&\n event.ctrlKey === wantsCtrl &&\n event.altKey === step.alt &&\n (punctuation && !step.shift ? true : event.shiftKey === step.shift)\n )\n}\n\n/** The keys of a binding for display, one entry per step (`['G', 'D']`, `['⌘K']`). */\nexport function shortcutSteps(binding: string): string[] {\n return binding\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map((step) => (step === '?' || step === '/' ? step : formatKeyBinding(step)))\n}\n\n/** A binding for display: steps separated by spaces. */\nexport function formatShortcut(binding: string): string {\n return shortcutSteps(binding).join(' ')\n}\n\n/**\n * The binding a keydown would record in the overlay (\"press the new keys\"), or null for a\n * lone modifier and keys a binding can't hold.\n */\nexport function bindingFromEvent(event: KeyboardEvent): string | null {\n const key = (event.key ?? '').toLowerCase()\n\n if (key === '' || MODIFIER_KEYS.has(key) || key === 'dead' || key === 'unidentified') {\n return null\n }\n\n const mac = isMacLike()\n const parts: string[] = []\n\n if (mac ? event.metaKey : event.ctrlKey) parts.push('mod')\n if (mac && event.ctrlKey) parts.push('ctrl')\n if (!mac && event.metaKey) parts.push('meta')\n if (event.altKey) parts.push('alt')\n\n const named = key === ' ' ? 'space' : key\n const punctuation = named.length === 1 && !/^[a-z0-9]$/.test(named)\n // Letters and digits from `code`, so Alt/Option combinations don't record `å`.\n const fromCode = /^key[a-z]$/.test(event.code?.toLowerCase() ?? '')\n ? event.code.toLowerCase().slice(3)\n : /^digit[0-9]$/.test(event.code?.toLowerCase() ?? '')\n ? event.code.toLowerCase().slice(5)\n : null\n const finalKey = event.altKey && fromCode ? fromCode : named\n\n if (event.shiftKey && !punctuation) parts.push('shift')\n\n parts.push(finalKey)\n\n const binding = parts.join('+')\n\n return isValidShortcut(binding) ? binding : null\n}\n\n/**\n * Whether two bindings can't both work: the same keys, or one is the start of the other's\n * sequence (\"g\" would run before \"g o\" could finish).\n */\nexport function shortcutsClash(a: string, b: string): boolean {\n const left = a.trim().toLowerCase().split(/\\s+/)\n const right = b.trim().toLowerCase().split(/\\s+/)\n const length = Math.min(left.length, right.length)\n\n return left.slice(0, length).every((step, index) => step === right[index])\n}\n\n/**\n * Records a new binding in the shortcuts overlay: a key (with modifiers) or a sequence of up\n * to `maxSteps` keys (\"g o\"). The binding is finished `timeout` ms after the last key (the\n * matcher's sequence timeout) or at `maxSteps`. Escape is the caller's (cancel).\n */\nexport class ShortcutRecorder {\n private steps: string[] = []\n\n private timer: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n private readonly options: {\n onDone: (binding: string) => void\n onStep?: (steps: string[]) => void\n timeout?: number\n maxSteps?: number\n },\n ) {}\n\n /** Record a keydown; returns false for keys a binding can't hold (lone modifiers). */\n press(event: KeyboardEvent): boolean {\n const step = bindingFromEvent(event)\n\n if (!step) {\n return false\n }\n\n this.steps.push(step)\n this.options.onStep?.([...this.steps])\n this.clearTimer()\n\n if (this.steps.length >= (this.options.maxSteps ?? 3)) {\n this.finish()\n } else {\n this.timer = setTimeout(() => this.finish(), this.options.timeout ?? 1000)\n }\n\n return true\n }\n\n /** Stop without a binding. */\n cancel(): void {\n this.clearTimer()\n this.steps = []\n }\n\n private finish(): void {\n this.clearTimer()\n const binding = this.steps.join(' ')\n this.steps = []\n\n if (binding !== '') {\n this.options.onDone(binding)\n }\n }\n\n private clearTimer(): void {\n if (this.timer !== null) {\n clearTimeout(this.timer)\n this.timer = null\n }\n }\n}\n\nexport interface ShortcutRegistryOptions {\n /** Milliseconds between the keys of a sequence (default 1000). */\n sequenceTimeout?: number\n}\n\ninterface PendingSequence {\n id: string\n steps: ParsedKeyBinding[]\n index: number\n}\n\n/**\n * The shortcuts of the current page. Renderers register in effects (never while server\n * rendering) and dispose on unmount; the registry listens on `window` while it has entries.\n */\nexport class ShortcutRegistry {\n private readonly entries = new Map<string, ShortcutDefinition>()\n private overrides: Record<string, string[]> = {}\n private enabled = true\n private paused = false\n private pending: PendingSequence[] = []\n private timer: ReturnType<typeof setTimeout> | null = null\n private readonly listeners = new Set<() => void>()\n private detach: (() => void) | null = null\n private version = 0\n\n constructor(private readonly options: ShortcutRegistryOptions = {}) {}\n\n /** Add (or replace) a shortcut. Returns a disposer that removes this registration only. */\n register(definition: ShortcutDefinition): () => void {\n this.entries.set(definition.id, definition)\n this.attach()\n this.changed()\n\n return () => {\n if (this.entries.get(definition.id) === definition) {\n this.entries.delete(definition.id)\n this.changed()\n\n if (this.entries.size === 0) {\n this.detach?.()\n this.detach = null\n }\n }\n }\n }\n\n has(id: string): boolean {\n return this.entries.has(id)\n }\n\n /** Run a shortcut's handler as if its keys were pressed (e.g. from the palette). */\n trigger(id: string): boolean {\n const entry = this.entries.get(id)\n\n if (!entry) return false\n\n entry.handler(null)\n\n return true\n }\n\n /** The user's customised keys by id (from `alfakit.keyboardShortcuts.overrides`). */\n setOverrides(overrides: Record<string, string[]> | null | undefined): void {\n const next: Record<string, string[]> = {}\n\n for (const [id, keys] of Object.entries(overrides ?? {})) {\n if (Array.isArray(keys)) next[id] = keys.filter((key) => isValidShortcut(key))\n }\n\n this.overrides = next\n this.changed()\n }\n\n getOverrides(): Record<string, string[]> {\n return { ...this.overrides }\n }\n\n /** `keyboardShortcuts(false)`: only essential shortcuts (the palette's key bindings) stay. */\n setEnabled(enabled: boolean): void {\n this.enabled = enabled\n this.reset()\n this.changed()\n }\n\n isEnabled(): boolean {\n return this.enabled\n }\n\n /** Ignore keys while the overlay records a new binding. */\n setPaused(paused: boolean): void {\n this.paused = paused\n this.reset()\n }\n\n keysFor(id: string): string[] {\n const entry = this.entries.get(id)\n\n if (!entry) return []\n\n return this.overrides[id] ?? entry.keys\n }\n\n /** Every active shortcut with the keys in effect, in registration order. */\n list(): RegisteredShortcut[] {\n return [...this.entries.values()]\n .filter((entry) => this.enabled || entry.essential)\n .map((entry) => ({\n ...entry,\n keys: this.overrides[entry.id] ?? entry.keys,\n defaultKeys: entry.keys,\n customized: Object.prototype.hasOwnProperty.call(this.overrides, entry.id),\n }))\n }\n\n /** Another shortcut already using one of these keys. */\n conflict(keys: string[], exceptId?: string): RegisteredShortcut | undefined {\n return this.list().find(\n (entry) =>\n entry.id !== exceptId &&\n entry.keys.some((key) => keys.some((candidate) => shortcutsClash(key, candidate))),\n )\n }\n\n /** Re-render hook for the overlay; returns an unsubscribe function. */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n\n return () => this.listeners.delete(listener)\n }\n\n /** Bumped on every change (for `useSyncExternalStore`-style snapshots). */\n getVersion(): number {\n return this.version\n }\n\n /** Handle a keydown; returns whether a shortcut ran or a sequence continues. */\n handleKeydown(event: KeyboardEvent): boolean {\n if (this.paused || event.defaultPrevented || event.isComposing) {\n return false\n }\n\n const key = (event.key ?? '').toLowerCase()\n\n if (MODIFIER_KEYS.has(key)) {\n return false\n }\n\n const editable = isEditableTarget(event.target)\n\n if (this.pending.length > 0) {\n const advanced = this.pending.filter((pending) =>\n matchesShortcutStep(event, pending.steps[pending.index]!),\n )\n const complete = advanced.find((pending) => pending.index + 1 === pending.steps.length)\n\n if (complete) {\n this.reset()\n return this.run(complete.id, event)\n }\n\n this.reset()\n\n if (advanced.length > 0) {\n this.pending = advanced.map((pending) => ({ ...pending, index: pending.index + 1 }))\n this.startTimer()\n event.preventDefault()\n\n return true\n }\n }\n\n const started: PendingSequence[] = []\n\n for (const entry of this.entries.values()) {\n if (!this.enabled && !entry.essential) continue\n\n for (const binding of this.overrides[entry.id] ?? entry.keys) {\n const steps = parseSteps(binding)\n const first = steps[0]\n\n if (!first || (editable && !entry.allowInInputs && !hasModifier(first))) continue\n if (!matchesShortcutStep(event, first)) continue\n\n if (steps.length === 1) {\n this.reset()\n return this.run(entry.id, event)\n }\n\n if (!event.repeat) started.push({ id: entry.id, steps, index: 1 })\n }\n }\n\n if (started.length > 0) {\n this.pending = started\n this.startTimer()\n\n return true\n }\n\n return false\n }\n\n private run(id: string, event: KeyboardEvent): boolean {\n const entry = this.entries.get(id)\n\n if (!entry) return false\n\n if (entry.handler(event) === false) return false\n\n event.preventDefault()\n\n return true\n }\n\n private startTimer(): void {\n if (this.timer) clearTimeout(this.timer)\n\n this.timer = setTimeout(() => this.reset(), this.options.sequenceTimeout ?? 1000)\n }\n\n private reset(): void {\n this.pending = []\n\n if (this.timer) {\n clearTimeout(this.timer)\n this.timer = null\n }\n }\n\n private attach(): void {\n if (this.detach || typeof window === 'undefined') return\n\n const listener = (event: KeyboardEvent) => {\n this.handleKeydown(event)\n }\n\n window.addEventListener('keydown', listener)\n this.detach = () => window.removeEventListener('keydown', listener)\n }\n\n private changed(): void {\n this.version++\n\n for (const listener of this.listeners) listener()\n }\n}\n\n/**\n * The page's shortcut registry. Only touched from effects and event handlers, so a server\n * render never shares state between requests.\n */\nexport const shortcuts = new ShortcutRegistry()\n\n/**\n * Store customised keys (`null` restores the default, `[]` turns the shortcut off) and\n * return every stored override.\n */\nexport async function saveShortcutOverrides(\n endpoint: string,\n changes: Record<string, string[] | null>,\n): Promise<Record<string, string[]>> {\n const response = await requestJson<{ shortcuts: Record<string, string[]> }>(endpoint, {\n method: 'POST',\n body: { shortcuts: changes },\n })\n\n return response.shortcuts ?? {}\n}\n\n/** Focus the page's table search field (`/`). Returns whether one was found. */\nexport function focusTableSearch(\n root: ParentNode | null = typeof document === 'undefined' ? null : document,\n): boolean {\n const content = root?.querySelector('[data-slot=\"content\"]') ?? root\n const input = content?.querySelector<HTMLInputElement>('input[type=\"search\"]:not([disabled])')\n\n if (!input) return false\n\n input.focus()\n input.select?.()\n\n return true\n}\n\nexport interface ShortcutListItem {\n id: string\n label: string\n keys: string[]\n defaultKeys: string[]\n customized: boolean\n customizable: boolean\n}\n\nexport interface ShortcutListGroup {\n group: ShortcutGroup\n items: ShortcutListItem[]\n}\n\nconst GROUP_ORDER: ShortcutGroup[] = ['general', 'navigation', 'commands', 'actions']\n\n/**\n * What the overlay lists: registered shortcuts plus the page's action key bindings\n * (`keyBindings()`, listed only), grouped and in a stable order.\n */\nexport function shortcutOverlayGroups(\n registered: RegisteredShortcut[],\n actions: Array<{\n name: string\n label?: string | null\n keyBindings?: string[]\n disabled?: boolean\n }> = [],\n): ShortcutListGroup[] {\n const groups = new Map<ShortcutGroup, ShortcutListItem[]>()\n const add = (group: ShortcutGroup, item: ShortcutListItem) => {\n groups.set(group, [...(groups.get(group) ?? []), item])\n }\n\n for (const entry of registered) {\n add(entry.group ?? 'general', {\n id: entry.id,\n label: entry.label,\n keys: entry.keys,\n defaultKeys: entry.defaultKeys,\n customized: entry.customized,\n customizable: entry.customizable !== false,\n })\n }\n\n for (const action of actions) {\n if (!action.keyBindings?.length || action.disabled) continue\n\n add('actions', {\n id: `action:${action.name}`,\n label: action.label ?? action.name,\n keys: action.keyBindings,\n defaultKeys: action.keyBindings,\n customized: false,\n customizable: false,\n })\n }\n\n return GROUP_ORDER.filter((group) => groups.has(group)).map((group) => ({\n group,\n items: groups.get(group)!,\n }))\n}\n"],"mappings":"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"}
|
package/dist/starter-kit.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NotificationSchema } from "./schema/notifications.js";
|
|
2
|
-
import { AlfakitPageProps, BreadcrumbSchema, NavigationGroupSchema, PanelSchema, StarterKitNavigationSchema } from "./schema/panel.js";
|
|
2
|
+
import { AlfakitPageProps, BreadcrumbSchema, MenuItemSchema, NavigationGroupSchema, PanelSchema, StarterKitNavigationSchema } from "./schema/panel.js";
|
|
3
3
|
//#region src/starter-kit.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Starter kit mode (`Panel::starterKit()`): Alfakit pages render inside an application made
|
|
@@ -83,6 +83,30 @@ export interface StarterKitNavGroup {
|
|
|
83
83
|
}
|
|
84
84
|
/** A panel's navigation groups as plain items for the host's sidebar primitives. */
|
|
85
85
|
export declare function toStarterKitNavGroups(groups: NavigationGroupSchema[] | null | undefined): StarterKitNavGroup[];
|
|
86
|
+
/**
|
|
87
|
+
* A user menu entry of a `starterKit()` panel for the kit's own dropdown items
|
|
88
|
+
* (`<AlfakitUserMenuItems />`). `group` changes where a separator goes; `method: 'post'`
|
|
89
|
+
* links are sent as POST (like the kit's Log out).
|
|
90
|
+
*/
|
|
91
|
+
export interface StarterKitUserMenuItem {
|
|
92
|
+
key: string;
|
|
93
|
+
title: string;
|
|
94
|
+
href: string;
|
|
95
|
+
icon?: string;
|
|
96
|
+
openInNewTab: boolean;
|
|
97
|
+
method?: 'post';
|
|
98
|
+
group: number;
|
|
99
|
+
/** Whether a separator goes before this entry (it starts a new group). */
|
|
100
|
+
startsGroup: boolean;
|
|
101
|
+
}
|
|
102
|
+
/** A panel's shared user menu links as plain items for the host's dropdown primitives. */
|
|
103
|
+
export declare function toStarterKitUserMenuItems(items: MenuItemSchema[] | null | undefined): StarterKitUserMenuItem[];
|
|
104
|
+
/**
|
|
105
|
+
* The props of a custom page component (`Page::$component`) rendered by `Alfakit/Page` (a
|
|
106
|
+
* starter kit resolves only that page): its view data, with the page's live top-level props
|
|
107
|
+
* of the same names, so a partial reload (`reloadProps('items')`) reaches the component.
|
|
108
|
+
*/
|
|
109
|
+
export declare function customPageProps(viewProps: Record<string, unknown> | null | undefined, pageProps: Record<string, unknown> | null | undefined): Record<string, unknown>;
|
|
86
110
|
/**
|
|
87
111
|
* Record the Alfakit page being rendered (called by each renderer's starter kit root while
|
|
88
112
|
* it renders); `null` once the root unmounts, when a host page opens.
|
|
@@ -93,7 +117,8 @@ export declare function currentStarterKitPanel(): PanelSchema | null;
|
|
|
93
117
|
/**
|
|
94
118
|
* The listeners `createAlfakit()` installs, once per page load, for starter kit mode: reload
|
|
95
119
|
* on an expired CSRF token (419), the panel's error notifications instead of Inertia's error
|
|
96
|
-
* modal, a notification for failed internal requests
|
|
120
|
+
* modal, a notification for failed internal requests, the panel's SPA URL exceptions, and
|
|
121
|
+
* dropping prefetched pages after a save (and reloading pages Back restores after one).
|
|
97
122
|
* Returns false when they were already installed (or on the server).
|
|
98
123
|
*/
|
|
99
124
|
export declare function installStarterKitRuntime(show: (notification: NotificationSchema) => void, t: (key: string, replacements?: Record<string, string | number>, fallback?: string) => string): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"starter-kit.d.ts","names":[],"sources":["../src/starter-kit.ts"],"mappings":";;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"starter-kit.d.ts","names":[],"sources":["../src/starter-kit.ts"],"mappings":";;;;;;;;;;;;;;qBAmCa;;wBAGG,kBACd,OAAO,KAAK;;iBAMG;EACf;EACA;;;;;;;;wBASc,wBACd,aAAa,wCACZ;;;;;;;;wBAsBa,yBAAyB;wBAMzB,+BAA+B,iBAAiB;;;;;wBAUhD,sBAAsB,iBAAiB;;;;;wBAUvC,yBAAyB;;;;;;wBAWzB,6BAA6B;EAAY;GACvD,SAAS,KACT,iBACC;;;;;wBAWa,qBACd,gBACA,mBACC;;;;;iBAcc;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,UAAU;;iBAGK;EACf;EACA;EACA;EACA,OAAO;;;wBAmBO,sBACd,QAAQ,6CACP;;;;;;iBAkBc;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;;wBAIc,0BACd,OAAO,sCACN;;;;;;wBA2Ba,gBACd,WAAW,4CACX,WAAW,6CACV;;;;;wBA6Ba,mBAAmB,OAAO,QAAQ;;wBAalC,0BAA0B;;;;;;;;wBAW1B,yBACd,OAAO,cAAc,6BACrB,IAAI,aAAa,eAAe,iCAAiC"}
|
package/dist/starter-kit.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { debugRecorder } from "./debug.js";
|
|
2
|
+
import { installPrefetchInvalidation } from "./prefetch.js";
|
|
2
3
|
import { reloadOnExpiredPage } from "./http.js";
|
|
3
4
|
import { effectiveErrorNotifications, installErrorNotifications, resolveRequestErrorNotification, setRequestErrorReporter } from "./error-notifications.js";
|
|
4
5
|
import { navigationItemText } from "./navigation.js";
|
|
@@ -113,6 +114,33 @@ function toStarterKitNavGroups(groups) {
|
|
|
113
114
|
};
|
|
114
115
|
});
|
|
115
116
|
}
|
|
117
|
+
/** A panel's shared user menu links as plain items for the host's dropdown primitives. */
|
|
118
|
+
function toStarterKitUserMenuItems(items) {
|
|
119
|
+
const links = (items ?? []).filter((item) => typeof item.url === "string" && item.url !== "" && item.action === void 0);
|
|
120
|
+
return links.map((item, index) => {
|
|
121
|
+
const group = item.group ?? 0;
|
|
122
|
+
return {
|
|
123
|
+
key: `${index}:${item.label ?? ""}`,
|
|
124
|
+
title: item.label ?? item.url,
|
|
125
|
+
href: item.url,
|
|
126
|
+
icon: item.icon ?? void 0,
|
|
127
|
+
openInNewTab: item.openInNewTab === true,
|
|
128
|
+
method: item.method,
|
|
129
|
+
group,
|
|
130
|
+
startsGroup: index > 0 && group !== (links[index - 1].group ?? 0)
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The props of a custom page component (`Page::$component`) rendered by `Alfakit/Page` (a
|
|
136
|
+
* starter kit resolves only that page): its view data, with the page's live top-level props
|
|
137
|
+
* of the same names, so a partial reload (`reloadProps('items')`) reaches the component.
|
|
138
|
+
*/
|
|
139
|
+
function customPageProps(viewProps, pageProps) {
|
|
140
|
+
const merged = { ...viewProps ?? {} };
|
|
141
|
+
for (const key of Object.keys(merged)) if (pageProps && Object.prototype.hasOwnProperty.call(pageProps, key)) merged[key] = pageProps[key];
|
|
142
|
+
return merged;
|
|
143
|
+
}
|
|
116
144
|
/**
|
|
117
145
|
* What the browser-wide listeners of starter kit mode read: the open Alfakit page's panel
|
|
118
146
|
* and error notifications, or nothing while a page of the host application is open (its
|
|
@@ -140,13 +168,15 @@ function currentStarterKitPanel() {
|
|
|
140
168
|
/**
|
|
141
169
|
* The listeners `createAlfakit()` installs, once per page load, for starter kit mode: reload
|
|
142
170
|
* on an expired CSRF token (419), the panel's error notifications instead of Inertia's error
|
|
143
|
-
* modal, a notification for failed internal requests
|
|
171
|
+
* modal, a notification for failed internal requests, the panel's SPA URL exceptions, and
|
|
172
|
+
* dropping prefetched pages after a save (and reloading pages Back restores after one).
|
|
144
173
|
* Returns false when they were already installed (or on the server).
|
|
145
174
|
*/
|
|
146
175
|
function installStarterKitRuntime(show, t) {
|
|
147
176
|
if (typeof window === "undefined" || runtime.installed) return false;
|
|
148
177
|
runtime.installed = true;
|
|
149
178
|
reloadOnExpiredPage();
|
|
179
|
+
installPrefetchInvalidation();
|
|
150
180
|
installSpaUrlExceptions(() => runtime.panel);
|
|
151
181
|
installErrorNotifications(() => ({ errorNotifications: runtime.errorNotifications }), show);
|
|
152
182
|
setRequestErrorReporter((error) => {
|
|
@@ -156,6 +186,6 @@ function installStarterKitRuntime(show, t) {
|
|
|
156
186
|
return true;
|
|
157
187
|
}
|
|
158
188
|
//#endregion
|
|
159
|
-
export { STARTER_KIT_TOASTER_ID, currentStarterKitPanel, currentStarterKitPanelId, installStarterKitRuntime, isStarterKitPanel, preferCurrentStarterKitPanel, starterKitBreadcrumbsFromProps, starterKitNavigation, starterKitNavigations, syncStarterKitPage, toStarterKitBreadcrumbs, toStarterKitNavGroups, usesStarterKitHostLayout };
|
|
189
|
+
export { STARTER_KIT_TOASTER_ID, currentStarterKitPanel, currentStarterKitPanelId, customPageProps, installStarterKitRuntime, isStarterKitPanel, preferCurrentStarterKitPanel, starterKitBreadcrumbsFromProps, starterKitNavigation, starterKitNavigations, syncStarterKitPage, toStarterKitBreadcrumbs, toStarterKitNavGroups, toStarterKitUserMenuItems, usesStarterKitHostLayout };
|
|
160
190
|
|
|
161
191
|
//# sourceMappingURL=starter-kit.js.map
|
package/dist/starter-kit.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"starter-kit.js","names":[],"sources":["../src/starter-kit.ts"],"sourcesContent":["import { debugRecorder } from './debug'\nimport {\n effectiveErrorNotifications,\n installErrorNotifications,\n resolveRequestErrorNotification,\n setRequestErrorReporter,\n} from './error-notifications'\nimport { reloadOnExpiredPage } from './http'\nimport { navigationItemText } from './navigation'\nimport type { NotificationSchema } from './schema/notifications'\nimport type {\n AlfakitPageProps,\n BreadcrumbSchema,\n ErrorNotificationsSchema,\n NavigationGroupSchema,\n NavigationItemSchema,\n PanelSchema,\n StarterKitNavigationSchema,\n} from './schema/panel'\nimport { installSpaUrlExceptions } from './spa'\n\n/**\n * Starter kit mode (`Panel::starterKit()`): Alfakit pages render inside an application made\n * from Laravel's React, Vue or Svelte starter kit, in the host's own layout. These helpers are\n * shared by the three renderers; the host's sidebar reads the navigation through each\n * renderer's `useAlfakitNavigation()`.\n */\n\n/**\n * The sonner toaster Alfakit renders in starter kit mode. The host layout mounts its own\n * id-less `<Toaster />`; giving Alfakit's an id keeps each toast in one toaster even when the\n * host and Alfakit resolve the same sonner instance.\n */\nexport const STARTER_KIT_TOASTER_ID = 'alfakit'\n\n/** Whether the panel runs inside a starter kit application (`Panel::starterKit()`). */\nexport function isStarterKitPanel(\n panel: Pick<PanelSchema, 'starterKit'> | null | undefined,\n): boolean {\n return panel?.starterKit === true\n}\n\n/** A breadcrumb as the starter kits' `AppLayout` takes it (`BreadcrumbItem`). */\nexport interface StarterKitBreadcrumb {\n title: string\n href: string\n}\n\n/**\n * The page's breadcrumbs in the starter kits' shape: `{ title, href }` instead of\n * `{ label, url }`. The kits render the last crumb (the current page) as plain text and key\n * the list by `href`, so every `href` is unique: the last crumb without a URL keeps an empty\n * one, other crumbs without one (or repeating one) get a `#index` fragment.\n */\nexport function toStarterKitBreadcrumbs(\n breadcrumbs: BreadcrumbSchema[] | null | undefined,\n): StarterKitBreadcrumb[] {\n const list = breadcrumbs ?? []\n const seen = new Set<string>()\n\n return list.map((crumb, index) => {\n let href = crumb.url ?? (index === list.length - 1 ? '' : `#${index}`)\n\n if (seen.has(href)) href = `${href}#${index}`\n\n seen.add(href)\n\n return { title: crumb.label, href }\n })\n}\n\n/** The breadcrumbs of an Alfakit page from its Inertia props (`props.page.breadcrumbs`). */\n/**\n * Whether an Alfakit page renders inside the host layout (`createStarterKitPage({ layout })`).\n * Pages with the `auth` layout (sign-in, registration, password reset, multi-factor\n * challenge, email verification, `->login()` brought back after `starterKit()`) stand on\n * their own, like the host's auth pages: the host layout expects a signed-in user.\n */\nexport function usesStarterKitHostLayout(props: unknown): boolean {\n const page = (props as { page?: { layout?: string } } | null | undefined)?.page\n\n return page?.layout !== 'auth'\n}\n\nexport function starterKitBreadcrumbsFromProps(props: unknown): StarterKitBreadcrumb[] {\n const page = (props as { page?: { breadcrumbs?: BreadcrumbSchema[] } } | null | undefined)?.page\n\n return toStarterKitBreadcrumbs(page?.breadcrumbs)\n}\n\n/**\n * The shared `alfakitNavigation` prop of a page: one entry per `starterKit()` panel, or none\n * for guests and pages of other panels.\n */\nexport function starterKitNavigations(props: unknown): StarterKitNavigationSchema[] {\n const value = (props as { alfakitNavigation?: unknown } | null | undefined)?.alfakitNavigation\n\n return Array.isArray(value) ? (value as StarterKitNavigationSchema[]) : []\n}\n\n/**\n * The panel of the open AlfaKit page (`props.alfakit.id`), or undefined on the host's own\n * pages. With several `starterKit()` panels, components without a `panel` follow it.\n */\nexport function currentStarterKitPanelId(props: unknown): string | undefined {\n const id = (props as { alfakit?: { id?: unknown } } | null | undefined)?.alfakit?.id\n\n return typeof id === 'string' ? id : undefined\n}\n\n/**\n * Order panel entries for components without a `panel` id: the open AlfaKit page's panel\n * first (an `/app` page of a second `starterKit()` panel shows that panel's navigation and\n * tenant menu), then the others in registration order.\n */\nexport function preferCurrentStarterKitPanel<T extends { id: string }>(\n entries: T[],\n props: unknown,\n): T[] {\n const current = currentStarterKitPanelId(props)\n const index = current === undefined ? -1 : entries.findIndex((entry) => entry.id === current)\n\n return index <= 0 ? entries : [entries[index]!, ...entries.filter((_, i) => i !== index)]\n}\n\n/**\n * One panel's navigation, or null. Without an id: the open AlfaKit page's panel, else the\n * first starter kit panel.\n */\nexport function starterKitNavigation(\n props: unknown,\n panelId?: string,\n): StarterKitNavigationSchema | null {\n const navigations = starterKitNavigations(props)\n\n return (\n (panelId === undefined\n ? preferCurrentStarterKitPanel(navigations, props)[0]\n : navigations.find((navigation) => navigation.id === panelId)) ?? null\n )\n}\n\n/**\n * A navigation item flattened for a starter kit sidebar: the label as text and the item's\n * nested children. `title`/`href` match the kits' `NavItem` so the host can reuse its types.\n */\nexport interface StarterKitNavItem {\n key: string\n title: string\n href: string\n icon?: string\n isActive: boolean\n openInNewTab: boolean\n badge?: string | number\n badgeColor?: string\n badgeTooltip?: string\n children: StarterKitNavItem[]\n}\n\nexport interface StarterKitNavGroup {\n key: string\n label: string | null\n icon?: string\n items: StarterKitNavItem[]\n}\n\nfunction toNavItem(item: NavigationItemSchema, key: string): StarterKitNavItem {\n return {\n key,\n title: navigationItemText(item),\n href: item.url ?? '',\n icon: item.icon ?? undefined,\n isActive: item.active === true,\n openInNewTab: item.openInNewTab === true,\n badge: item.badge ?? undefined,\n badgeColor: item.badgeColor ?? undefined,\n badgeTooltip: item.badgeTooltip ?? undefined,\n children: (item.children ?? []).map((child, index) => toNavItem(child, `${key}.${index}`)),\n }\n}\n\n/** A panel's navigation groups as plain items for the host's sidebar primitives. */\nexport function toStarterKitNavGroups(\n groups: NavigationGroupSchema[] | null | undefined,\n): StarterKitNavGroup[] {\n return (groups ?? []).map((group, index) => {\n const key = `${index}:${group.label ?? ''}`\n\n return {\n key,\n label: group.label ?? null,\n icon: group.icon ?? undefined,\n items: group.items.map((item, itemIndex) => toNavItem(item, `${key}.${itemIndex}`)),\n }\n })\n}\n\n// ---------------------------------------------------------------- Browser runtime\n\n/**\n * What the browser-wide listeners of starter kit mode read: the open Alfakit page's panel\n * and error notifications, or nothing while a page of the host application is open (its\n * errors keep Inertia's default handling).\n */\nconst runtime: {\n panel: PanelSchema | null\n errorNotifications: ErrorNotificationsSchema | false | undefined\n installed: boolean\n} = { panel: null, errorNotifications: undefined, installed: false }\n\n/**\n * Record the Alfakit page being rendered (called by each renderer's starter kit root while\n * it renders); `null` once the root unmounts, when a host page opens.\n */\nexport function syncStarterKitPage(props: Partial<AlfakitPageProps> | null): void {\n const panel = props?.alfakit ?? null\n\n runtime.panel = panel\n runtime.errorNotifications = panel ? effectiveErrorNotifications(panel, props?.page) : undefined\n\n if (typeof window !== 'undefined') {\n // Internal requests and form stores are recorded only while the page carries the debug prop.\n debugRecorder.setEnabled(props?.alfakitDebug !== undefined)\n }\n}\n\n/** The panel of the open Alfakit page in starter kit mode, or null on host pages. */\nexport function currentStarterKitPanel(): PanelSchema | null {\n return runtime.panel\n}\n\n/**\n * The listeners `createAlfakit()` installs, once per page load, for starter kit mode: reload\n * on an expired CSRF token (419), the panel's error notifications instead of Inertia's error\n * modal, a notification for failed internal requests and the panel's SPA URL exceptions.\n * Returns false when they were already installed (or on the server).\n */\nexport function installStarterKitRuntime(\n show: (notification: NotificationSchema) => void,\n t: (key: string, replacements?: Record<string, string | number>, fallback?: string) => string,\n): boolean {\n if (typeof window === 'undefined' || runtime.installed) return false\n\n runtime.installed = true\n\n reloadOnExpiredPage()\n installSpaUrlExceptions(() => runtime.panel)\n installErrorNotifications(() => ({ errorNotifications: runtime.errorNotifications }), show)\n setRequestErrorReporter((error) => {\n const notification = resolveRequestErrorNotification(error, runtime.errorNotifications, t)\n\n if (notification) show(notification)\n })\n\n return true\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"starter-kit.js","names":[],"sources":["../src/starter-kit.ts"],"sourcesContent":["import { debugRecorder } from './debug'\nimport {\n effectiveErrorNotifications,\n installErrorNotifications,\n resolveRequestErrorNotification,\n setRequestErrorReporter,\n} from './error-notifications'\nimport { reloadOnExpiredPage } from './http'\nimport { navigationItemText } from './navigation'\nimport { installPrefetchInvalidation } from './prefetch'\nimport type { NotificationSchema } from './schema/notifications'\nimport type {\n AlfakitPageProps,\n BreadcrumbSchema,\n ErrorNotificationsSchema,\n MenuItemSchema,\n NavigationGroupSchema,\n NavigationItemSchema,\n PanelSchema,\n StarterKitNavigationSchema,\n} from './schema/panel'\nimport { installSpaUrlExceptions } from './spa'\n\n/**\n * Starter kit mode (`Panel::starterKit()`): Alfakit pages render inside an application made\n * from Laravel's React, Vue or Svelte starter kit, in the host's own layout. These helpers are\n * shared by the three renderers; the host's sidebar reads the navigation through each\n * renderer's `useAlfakitNavigation()`.\n */\n\n/**\n * The sonner toaster Alfakit renders in starter kit mode. The host layout mounts its own\n * id-less `<Toaster />`; giving Alfakit's an id keeps each toast in one toaster even when the\n * host and Alfakit resolve the same sonner instance.\n */\nexport const STARTER_KIT_TOASTER_ID = 'alfakit'\n\n/** Whether the panel runs inside a starter kit application (`Panel::starterKit()`). */\nexport function isStarterKitPanel(\n panel: Pick<PanelSchema, 'starterKit'> | null | undefined,\n): boolean {\n return panel?.starterKit === true\n}\n\n/** A breadcrumb as the starter kits' `AppLayout` takes it (`BreadcrumbItem`). */\nexport interface StarterKitBreadcrumb {\n title: string\n href: string\n}\n\n/**\n * The page's breadcrumbs in the starter kits' shape: `{ title, href }` instead of\n * `{ label, url }`. The kits render the last crumb (the current page) as plain text and key\n * the list by `href`, so every `href` is unique: the last crumb without a URL keeps an empty\n * one, other crumbs without one (or repeating one) get a `#index` fragment.\n */\nexport function toStarterKitBreadcrumbs(\n breadcrumbs: BreadcrumbSchema[] | null | undefined,\n): StarterKitBreadcrumb[] {\n const list = breadcrumbs ?? []\n const seen = new Set<string>()\n\n return list.map((crumb, index) => {\n let href = crumb.url ?? (index === list.length - 1 ? '' : `#${index}`)\n\n if (seen.has(href)) href = `${href}#${index}`\n\n seen.add(href)\n\n return { title: crumb.label, href }\n })\n}\n\n/** The breadcrumbs of an Alfakit page from its Inertia props (`props.page.breadcrumbs`). */\n/**\n * Whether an Alfakit page renders inside the host layout (`createStarterKitPage({ layout })`).\n * Pages with the `auth` layout (sign-in, registration, password reset, multi-factor\n * challenge, email verification, `->login()` brought back after `starterKit()`) stand on\n * their own, like the host's auth pages: the host layout expects a signed-in user.\n */\nexport function usesStarterKitHostLayout(props: unknown): boolean {\n const page = (props as { page?: { layout?: string } } | null | undefined)?.page\n\n return page?.layout !== 'auth'\n}\n\nexport function starterKitBreadcrumbsFromProps(props: unknown): StarterKitBreadcrumb[] {\n const page = (props as { page?: { breadcrumbs?: BreadcrumbSchema[] } } | null | undefined)?.page\n\n return toStarterKitBreadcrumbs(page?.breadcrumbs)\n}\n\n/**\n * The shared `alfakitNavigation` prop of a page: one entry per `starterKit()` panel, or none\n * for guests and pages of other panels.\n */\nexport function starterKitNavigations(props: unknown): StarterKitNavigationSchema[] {\n const value = (props as { alfakitNavigation?: unknown } | null | undefined)?.alfakitNavigation\n\n return Array.isArray(value) ? (value as StarterKitNavigationSchema[]) : []\n}\n\n/**\n * The panel of the open AlfaKit page (`props.alfakit.id`), or undefined on the host's own\n * pages. With several `starterKit()` panels, components without a `panel` follow it.\n */\nexport function currentStarterKitPanelId(props: unknown): string | undefined {\n const id = (props as { alfakit?: { id?: unknown } } | null | undefined)?.alfakit?.id\n\n return typeof id === 'string' ? id : undefined\n}\n\n/**\n * Order panel entries for components without a `panel` id: the open AlfaKit page's panel\n * first (an `/app` page of a second `starterKit()` panel shows that panel's navigation and\n * tenant menu), then the others in registration order.\n */\nexport function preferCurrentStarterKitPanel<T extends { id: string }>(\n entries: T[],\n props: unknown,\n): T[] {\n const current = currentStarterKitPanelId(props)\n const index = current === undefined ? -1 : entries.findIndex((entry) => entry.id === current)\n\n return index <= 0 ? entries : [entries[index]!, ...entries.filter((_, i) => i !== index)]\n}\n\n/**\n * One panel's navigation, or null. Without an id: the open AlfaKit page's panel, else the\n * first starter kit panel.\n */\nexport function starterKitNavigation(\n props: unknown,\n panelId?: string,\n): StarterKitNavigationSchema | null {\n const navigations = starterKitNavigations(props)\n\n return (\n (panelId === undefined\n ? preferCurrentStarterKitPanel(navigations, props)[0]\n : navigations.find((navigation) => navigation.id === panelId)) ?? null\n )\n}\n\n/**\n * A navigation item flattened for a starter kit sidebar: the label as text and the item's\n * nested children. `title`/`href` match the kits' `NavItem` so the host can reuse its types.\n */\nexport interface StarterKitNavItem {\n key: string\n title: string\n href: string\n icon?: string\n isActive: boolean\n openInNewTab: boolean\n badge?: string | number\n badgeColor?: string\n badgeTooltip?: string\n children: StarterKitNavItem[]\n}\n\nexport interface StarterKitNavGroup {\n key: string\n label: string | null\n icon?: string\n items: StarterKitNavItem[]\n}\n\nfunction toNavItem(item: NavigationItemSchema, key: string): StarterKitNavItem {\n return {\n key,\n title: navigationItemText(item),\n href: item.url ?? '',\n icon: item.icon ?? undefined,\n isActive: item.active === true,\n openInNewTab: item.openInNewTab === true,\n badge: item.badge ?? undefined,\n badgeColor: item.badgeColor ?? undefined,\n badgeTooltip: item.badgeTooltip ?? undefined,\n children: (item.children ?? []).map((child, index) => toNavItem(child, `${key}.${index}`)),\n }\n}\n\n/** A panel's navigation groups as plain items for the host's sidebar primitives. */\nexport function toStarterKitNavGroups(\n groups: NavigationGroupSchema[] | null | undefined,\n): StarterKitNavGroup[] {\n return (groups ?? []).map((group, index) => {\n const key = `${index}:${group.label ?? ''}`\n\n return {\n key,\n label: group.label ?? null,\n icon: group.icon ?? undefined,\n items: group.items.map((item, itemIndex) => toNavItem(item, `${key}.${itemIndex}`)),\n }\n })\n}\n\n/**\n * A user menu entry of a `starterKit()` panel for the kit's own dropdown items\n * (`<AlfakitUserMenuItems />`). `group` changes where a separator goes; `method: 'post'`\n * links are sent as POST (like the kit's Log out).\n */\nexport interface StarterKitUserMenuItem {\n key: string\n title: string\n href: string\n icon?: string\n openInNewTab: boolean\n method?: 'post'\n group: number\n /** Whether a separator goes before this entry (it starts a new group). */\n startsGroup: boolean\n}\n\n/** A panel's shared user menu links as plain items for the host's dropdown primitives. */\nexport function toStarterKitUserMenuItems(\n items: MenuItemSchema[] | null | undefined,\n): StarterKitUserMenuItem[] {\n const links = (items ?? []).filter(\n (item): item is MenuItemSchema & { url: string } =>\n typeof item.url === 'string' && item.url !== '' && item.action === undefined,\n )\n\n return links.map((item, index) => {\n const group = item.group ?? 0\n\n return {\n key: `${index}:${item.label ?? ''}`,\n title: item.label ?? item.url,\n href: item.url,\n icon: item.icon ?? undefined,\n openInNewTab: item.openInNewTab === true,\n method: item.method,\n group,\n startsGroup: index > 0 && group !== (links[index - 1]!.group ?? 0),\n }\n })\n}\n\n/**\n * The props of a custom page component (`Page::$component`) rendered by `Alfakit/Page` (a\n * starter kit resolves only that page): its view data, with the page's live top-level props\n * of the same names, so a partial reload (`reloadProps('items')`) reaches the component.\n */\nexport function customPageProps(\n viewProps: Record<string, unknown> | null | undefined,\n pageProps: Record<string, unknown> | null | undefined,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...(viewProps ?? {}) }\n\n for (const key of Object.keys(merged)) {\n if (pageProps && Object.prototype.hasOwnProperty.call(pageProps, key)) {\n merged[key] = pageProps[key]\n }\n }\n\n return merged\n}\n\n// ---------------------------------------------------------------- Browser runtime\n\n/**\n * What the browser-wide listeners of starter kit mode read: the open Alfakit page's panel\n * and error notifications, or nothing while a page of the host application is open (its\n * errors keep Inertia's default handling).\n */\nconst runtime: {\n panel: PanelSchema | null\n errorNotifications: ErrorNotificationsSchema | false | undefined\n installed: boolean\n} = { panel: null, errorNotifications: undefined, installed: false }\n\n/**\n * Record the Alfakit page being rendered (called by each renderer's starter kit root while\n * it renders); `null` once the root unmounts, when a host page opens.\n */\nexport function syncStarterKitPage(props: Partial<AlfakitPageProps> | null): void {\n const panel = props?.alfakit ?? null\n\n runtime.panel = panel\n runtime.errorNotifications = panel ? effectiveErrorNotifications(panel, props?.page) : undefined\n\n if (typeof window !== 'undefined') {\n // Internal requests and form stores are recorded only while the page carries the debug prop.\n debugRecorder.setEnabled(props?.alfakitDebug !== undefined)\n }\n}\n\n/** The panel of the open Alfakit page in starter kit mode, or null on host pages. */\nexport function currentStarterKitPanel(): PanelSchema | null {\n return runtime.panel\n}\n\n/**\n * The listeners `createAlfakit()` installs, once per page load, for starter kit mode: reload\n * on an expired CSRF token (419), the panel's error notifications instead of Inertia's error\n * modal, a notification for failed internal requests, the panel's SPA URL exceptions, and\n * dropping prefetched pages after a save (and reloading pages Back restores after one).\n * Returns false when they were already installed (or on the server).\n */\nexport function installStarterKitRuntime(\n show: (notification: NotificationSchema) => void,\n t: (key: string, replacements?: Record<string, string | number>, fallback?: string) => string,\n): boolean {\n if (typeof window === 'undefined' || runtime.installed) return false\n\n runtime.installed = true\n\n reloadOnExpiredPage()\n installPrefetchInvalidation()\n installSpaUrlExceptions(() => runtime.panel)\n installErrorNotifications(() => ({ errorNotifications: runtime.errorNotifications }), show)\n setRequestErrorReporter((error) => {\n const notification = resolveRequestErrorNotification(error, runtime.errorNotifications, t)\n\n if (notification) show(notification)\n })\n\n return true\n}\n"],"mappings":"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"}
|
package/dist/table-layout.d.ts
CHANGED
|
@@ -86,8 +86,12 @@ export declare function columnOrderChanged(applied: readonly string[], draft: re
|
|
|
86
86
|
export declare function keepMoveButtonFocus(button: HTMLElement | null | undefined): void;
|
|
87
87
|
/**
|
|
88
88
|
* Store the user's column order on the server (per user, in the session), then reload the
|
|
89
|
-
* table so it renders in the new order.
|
|
89
|
+
* table so it renders in the new order. `reload: false` when a table visit follows (Reset,
|
|
90
|
+
* Apply with visibility changes): that visit renders the new order, and a reload of the
|
|
91
|
+
* current URL running alongside it could finish last and bring the old columns back.
|
|
90
92
|
*/
|
|
91
|
-
export declare function saveColumnOrder(endpoint: string, token: string, component: string, columns: string[]
|
|
93
|
+
export declare function saveColumnOrder(endpoint: string, token: string, component: string, columns: string[], options?: {
|
|
94
|
+
reload?: boolean;
|
|
95
|
+
}): Promise<string[]>;
|
|
92
96
|
//#endregion
|
|
93
97
|
//# sourceMappingURL=table-layout.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table-layout.d.ts","names":[],"sources":["../src/table-layout.ts"],"mappings":";;;;;;;iBAmBiB;EACf;EACA;EACA,QAAQ;EACR,SAAS;;;;;;;wBAQK,qBACd,SAAS,gBACT,QAAQ,kCACP;;wBAgCa,gBAAgB,cAAc,QAAQ;;wBAKtC,kBAAkB,OAAO;;wBAwBzB,aAAa,QAAQ,KAAK;;wBAqB1B,wBAAwB;EACtC,cAAc;EACd,aAAa;;;;;;wBA8BC,wBACd,QAAQ,KAAK,iDACb;;wBAqBc,aAAa,QAAQ,KAAK;;wBAW1B,gBAAgB,MAAM,kBAAkB,SAAS;;wBASjD,qBAAqB,UAAU;;wBAK/B,gBAAgB,YAAY;;wBAS5B,sBACd,YAAY,yCACX,cAAc,QAAQ;EAAuB;;;;;;wBAQhC,cACd,YAAY,yCACX;;wBAoBa,kBACd,YAAY,yCACX;;wBAWa,WAAW,0BAA0B,cAAc;;wBAiBnD,aAAa,0BAA0B,cAAc;;;;;;iBAqBpD;EACf;EACA;EACA;;wBAsCc,sBACd,0BACA,cACA,kBAAkB,sBACjB;;;;;;wBAsBa,oBACd,0BACA,cACA,gBACA,kBAAkB;;wBAgCJ,eAAe;EAAY;GACzC,kBAAkB,KAClB,8CACC;;wBAaa,mBAAmB,4BAA4B;;;;;wBAQ/C,oBAAoB,QAAQ
|
|
1
|
+
{"version":3,"file":"table-layout.d.ts","names":[],"sources":["../src/table-layout.ts"],"mappings":";;;;;;;iBAmBiB;EACf;EACA;EACA,QAAQ;EACR,SAAS;;;;;;;wBAQK,qBACd,SAAS,gBACT,QAAQ,kCACP;;wBAgCa,gBAAgB,cAAc,QAAQ;;wBAKtC,kBAAkB,OAAO;;wBAwBzB,aAAa,QAAQ,KAAK;;wBAqB1B,wBAAwB;EACtC,cAAc;EACd,aAAa;;;;;;wBA8BC,wBACd,QAAQ,KAAK,iDACb;;wBAqBc,aAAa,QAAQ,KAAK;;wBAW1B,gBAAgB,MAAM,kBAAkB,SAAS;;wBASjD,qBAAqB,UAAU;;wBAK/B,gBAAgB,YAAY;;wBAS5B,sBACd,YAAY,yCACX,cAAc,QAAQ;EAAuB;;;;;;wBAQhC,cACd,YAAY,yCACX;;wBAoBa,kBACd,YAAY,yCACX;;wBAWa,WAAW,0BAA0B,cAAc;;wBAiBnD,aAAa,0BAA0B,cAAc;;;;;;iBAqBpD;EACf;EACA;EACA;;wBAsCc,sBACd,0BACA,cACA,kBAAkB,sBACjB;;;;;;wBAsBa,oBACd,0BACA,cACA,gBACA,kBAAkB;;wBAgCJ,eAAe;EAAY;GACzC,kBAAkB,KAClB,8CACC;;wBAaa,mBAAmB,4BAA4B;;;;;wBAQ/C,oBAAoB,QAAQ;;;;;;;wBA8BtB,gBACpB,kBACA,eACA,mBACA,mBACA;EAAW;IACV"}
|
package/dist/table-layout.js
CHANGED
|
@@ -259,9 +259,11 @@ function keepMoveButtonFocus(button) {
|
|
|
259
259
|
}
|
|
260
260
|
/**
|
|
261
261
|
* Store the user's column order on the server (per user, in the session), then reload the
|
|
262
|
-
* table so it renders in the new order.
|
|
262
|
+
* table so it renders in the new order. `reload: false` when a table visit follows (Reset,
|
|
263
|
+
* Apply with visibility changes): that visit renders the new order, and a reload of the
|
|
264
|
+
* current URL running alongside it could finish last and bring the old columns back.
|
|
263
265
|
*/
|
|
264
|
-
async function saveColumnOrder(endpoint, token, component, columns) {
|
|
266
|
+
async function saveColumnOrder(endpoint, token, component, columns, options = {}) {
|
|
265
267
|
const response = await requestJson(endpoint, {
|
|
266
268
|
method: "POST",
|
|
267
269
|
body: {
|
|
@@ -270,7 +272,7 @@ async function saveColumnOrder(endpoint, token, component, columns) {
|
|
|
270
272
|
columns
|
|
271
273
|
}
|
|
272
274
|
});
|
|
273
|
-
inertiaReload({ only: ["page"] });
|
|
275
|
+
if (options.reload ?? true) inertiaReload({ only: ["page"] });
|
|
274
276
|
return response.columns;
|
|
275
277
|
}
|
|
276
278
|
//#endregion
|