@alfakit/react 0.1.1 → 0.1.2
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 +8 -0
- package/dist/components/actions/action-button.d.ts.map +1 -1
- package/dist/components/actions/action-button.js +5 -4
- package/dist/components/actions/action-button.js.map +1 -1
- package/dist/components/actions/action-modals.d.ts.map +1 -1
- package/dist/components/actions/action-modals.js +2 -5
- package/dist/components/actions/action-modals.js.map +1 -1
- package/dist/components/fields/color-picker.js +1 -0
- package/dist/components/fields/color-picker.js.map +1 -1
- package/dist/components/fields/file-upload.js +11 -10
- package/dist/components/fields/file-upload.js.map +1 -1
- package/dist/components/fields/one-time-code-input.js +1 -1
- package/dist/components/fields/one-time-code-input.js.map +1 -1
- package/dist/components/fields/repeater-table.js +12 -1
- package/dist/components/fields/repeater-table.js.map +1 -1
- package/dist/components/fields/select.js +3 -3
- package/dist/components/fields/select.js.map +1 -1
- package/dist/components/file-manager/file-manager.js +1 -1
- package/dist/components/file-manager/file-manager.js.map +1 -1
- package/dist/components/form/form-block.d.ts.map +1 -1
- package/dist/components/form/form-block.js +13 -1
- package/dist/components/form/form-block.js.map +1 -1
- package/dist/components/form/form-context.js +6 -2
- package/dist/components/form/form-context.js.map +1 -1
- package/dist/components/layout/fused-group.js +1 -0
- package/dist/components/layout/fused-group.js.map +1 -1
- package/dist/components/layout/tabs.d.ts.map +1 -1
- package/dist/components/layout/tabs.js +54 -15
- package/dist/components/layout/tabs.js.map +1 -1
- package/dist/components/panel/global-search.js +1 -0
- package/dist/components/panel/global-search.js.map +1 -1
- package/dist/components/panel/shortcut-keys.js +1 -0
- package/dist/components/panel/shortcut-keys.js.map +1 -1
- package/dist/components/panel/tenant-menu.js +2 -2
- package/dist/components/panel/tenant-menu.js.map +1 -1
- package/dist/components/table/cells.js +9 -1
- package/dist/components/table/cells.js.map +1 -1
- package/dist/components/table/grouping.js +11 -1
- package/dist/components/table/grouping.js.map +1 -1
- package/dist/components.css +347 -0
- package/dist/eject-manifest.json +2 -2
- package/dist/standalone/alfakit-toaster.d.ts.map +1 -1
- package/dist/standalone/alfakit-toaster.js +8 -1
- package/dist/standalone/alfakit-toaster.js.map +1 -1
- package/dist/standalone.css +47 -0
- package/dist/starter-kit/widgets-loaded.js +1 -1
- package/dist/starter-kit/widgets-loaded.js.map +1 -1
- package/dist/starter-kit/widgets-triggers.js +1 -0
- package/dist/starter-kit/widgets-triggers.js.map +1 -1
- package/dist/themes.css +2 -334
- package/package.json +2 -2
- package/src/components/actions/action-button.tsx +7 -4
- package/src/components/actions/action-modals.tsx +3 -1
- package/src/components/fields/color-picker.tsx +1 -0
- package/src/components/fields/file-upload.tsx +21 -10
- package/src/components/fields/one-time-code-input.tsx +1 -1
- package/src/components/fields/repeater-table.tsx +12 -1
- package/src/components/fields/select.tsx +7 -3
- package/src/components/file-manager/file-manager.tsx +1 -1
- package/src/components/form/form-block.tsx +24 -0
- package/src/components/form/form-context.tsx +2 -5
- package/src/components/layout/fused-group.tsx +5 -1
- package/src/components/layout/tabs.tsx +72 -17
- package/src/components/panel/global-search.tsx +1 -1
- package/src/components/panel/shortcut-keys.tsx +4 -1
- package/src/components/panel/tenant-menu.tsx +2 -2
- package/src/components/table/cells.tsx +18 -3
- package/src/components/table/grouping.tsx +15 -1
- package/src/standalone/alfakit-toaster.tsx +21 -2
- package/src/starter-kit/widgets-loaded.tsx +1 -1
- package/src/starter-kit/widgets-triggers.tsx +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-block.js","names":["FormBlock"],"sources":["../../../src/components/form/form-block.tsx"],"sourcesContent":["import type { ActionContext, ActionSchema, FormBlock as FormBlockSchema } from '@alfakit/core'\nimport {\n FormStore,\n applyFormRefresh,\n formActionsVisible,\n formIdentity,\n formRefreshFor,\n isImplicitSubmit,\n isLeavingVisit,\n pageFlash,\n pageFormName,\n registerConflictForm,\n registerFormSubmitter,\n registerPageForm,\n resolvePartialProps,\n wizardProgressStore,\n} from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { useCallback, useEffect, useMemo, useRef } from 'react'\nimport { useActionRunner, useAlfakitProps, usePageSchema, usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { ActionList } from '../actions/action-list'\nimport { showToast } from '../notifications/flash-toasts'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { FormProvider } from './form-context'\n\n/**\n * A page form: its state lives in a FormStore, and its buttons are actions that post the\n * current values through the action endpoint.\n */\nexport function FormBlock({ block }: { block: FormBlockSchema }) {\n const panel = usePanel()\n const page = usePageSchema()\n const props = useAlfakitProps()\n const runner = useActionRunner()\n const t = useT()\n const token = page?.token ?? ''\n\n // The store survives reloads of the same form (table sorting/paging, polling,\n // relation-manager actions all deliver a new `block` object); only a different form\n // identity starts from server state again. The token is re-issued per response, so it\n // is swapped in place instead of being part of the identity.\n const identity = formIdentity(page, block)\n\n const store = useMemo(\n () =>\n new FormStore(block, {\n token,\n component: block.component,\n refreshUrl: panel.endpoints.schemaRefresh,\n optionsUrl: panel.endpoints.schemaOptions,\n validateUrl: panel.endpoints.schemaValidate,\n // A halted wizard step shows what its hook sent.\n onNotifications: (notifications) => notifications.forEach((n) => showToast(n)),\n // Live dashboard filters are applied by the refresh; the widgets reload to follow.\n onRefreshed: () => {\n if (latestBlock.current.liveFilters)\n router.reload({ only: resolvePartialProps(['page']) })\n },\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [identity],\n )\n\n useEffect(() => () => store.destroy(), [store])\n\n // Named forms of custom pages (`<PageForm name>`) publish their store for page method calls.\n useEffect(() => {\n const name = pageFormName(block.component)\n\n return name ? registerPageForm(name, store) : undefined\n }, [block.component, store])\n\n // Edit pages: the conflict banner diffs and merges the main form (`detectConflicts()`).\n useEffect(\n () => (block.component === 'form' ? registerConflictForm(store) : undefined),\n [block.component, store],\n )\n\n useEffect(() => {\n store.setToken(token)\n }, [store, token])\n\n useEffect(() => {\n store.setErrors((props.errors as Record<string, string>) ?? {})\n }, [props.errors, store])\n\n const state = useStore(store.store)\n const context = useMemo<ActionContext>(() => ({ token, component: 'page' }), [token])\n const submitting = useRef(false)\n // After this form's own successful submit, refill from the server's new state. The new\n // block may arrive before or after Inertia's `success` event, so handle both orders.\n const latestBlock = useRef(block)\n const submittedBlock = useRef<FormBlockSchema | null>(null)\n const refillPending = useRef(false)\n\n useEffect(() => {\n latestBlock.current = block\n\n if (refillPending.current) {\n refillPending.current = false\n store.fill(block)\n }\n }, [block, store])\n\n const actions: ActionSchema[] = useMemo(\n () => block.actions.map((action) => ({ ...action, hasModal: false })),\n [block.actions],\n )\n\n const trigger = useCallback(\n async (action: ActionSchema, ctx: ActionContext) => {\n if (action.url && !action.hasAction) {\n return runner.trigger(action, ctx)\n }\n\n submitting.current = true\n submittedBlock.current = latestBlock.current\n store.setProcessing(true)\n runner.execute(action, ctx, store.state.values)\n },\n [runner, store],\n )\n\n // Buttons elsewhere with `formId('form')` (a header \"Save\") submit through this form.\n useEffect(() => registerFormSubmitter(block.component, trigger), [block.component, trigger])\n\n // `refreshFormData([...])`: an action changed the record; copy those fields in.\n useEffect(\n () =>\n router.on('success', (event) => {\n const refresh = formRefreshFor(pageFlash(event.detail.page), block.component)\n\n if (refresh) applyFormRefresh(store, refresh)\n }),\n [block.component, store],\n )\n\n // Wizard forms (`HasWizard` pages) show their buttons on the last step only.\n const progressStore = useMemo(() => wizardProgressStore(store), [store])\n const progress = useStore(progressStore)\n const showActions = formActionsVisible(block, progress)\n\n // Track this form's own submit visit so unrelated reloads finishing meanwhile (polling,\n // relation-manager paging) neither end its processing state nor count as its success.\n const submitVisit = useRef<string | null>(null)\n\n useEffect(() => {\n const offStart = router.on('start', (event) => {\n const visit = event.detail.visit\n\n if (submitting.current && !submitVisit.current && visit.method !== 'get') {\n submitVisit.current = visit.id\n }\n })\n\n const offSuccess = router.on('success', (event) => {\n const id = submitVisit.current\n\n if (!id || (event.detail.visitId ?? id) !== id) return\n\n // A halted action (rate limit, `halt()`) saved nothing: keep what the user typed.\n if (pageFlash(event.detail.page).alfakitAction?.status === 'halted') return\n\n if (latestBlock.current !== submittedBlock.current) {\n store.fill(latestBlock.current)\n } else {\n refillPending.current = true\n }\n })\n\n const offFinish = router.on('finish', (event) => {\n if (submitVisit.current && event.detail.visit.id !== submitVisit.current) return\n\n store.setProcessing(false)\n submitting.current = false\n submitVisit.current = null\n })\n\n return () => {\n offStart()\n offSuccess()\n offFinish()\n }\n }, [store])\n\n useEffect(() => {\n if (!panel.unsavedChangesAlerts) {\n return\n }\n\n return router.on('before', (event) => {\n if (\n store.isDirty &&\n !submitting.current &&\n isLeavingVisit(event.detail.visit) &&\n !window.confirm(t('unsaved_changes', {}, 'You have unsaved changes. Leave anyway?'))\n ) {\n event.preventDefault()\n }\n })\n }, [panel.unsavedChangesAlerts, store, t])\n\n const primary = actions[0]\n\n return (\n <FormProvider store={store}>\n <form\n data-slot=\"form-block\"\n className=\"flex flex-col gap-6\"\n onSubmit={(event) => {\n event.preventDefault()\n\n if (primary && showActions) {\n void trigger(primary, context)\n }\n }}\n onKeyDown={(event) => {\n // Enter in a text field runs the primary action (actions are type=\"button\").\n if (primary && showActions && isImplicitSubmit(event.nativeEvent, event.currentTarget)) {\n event.preventDefault()\n void trigger(primary, context)\n }\n }}\n >\n <fieldset disabled={state.processing || block.operation === 'view'} className=\"contents\">\n <SchemaRenderer components={state.components} columns={state.columns} />\n </fieldset>\n {actions.length > 0 && showActions && (\n <div data-slot=\"form-actions\" className=\"border-t pt-6\">\n <ActionList actions={actions} context={context} onTrigger={trigger} />\n </div>\n )}\n </form>\n </FormProvider>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"form-block.js","names":["FormBlock"],"sources":["../../../src/components/form/form-block.tsx"],"sourcesContent":["import type { ActionContext, ActionSchema, FormBlock as FormBlockSchema } from '@alfakit/core'\nimport {\n FormStore,\n applyFormRefresh,\n formActionsVisible,\n formIdentity,\n formRefreshFor,\n isImplicitSubmit,\n isLeavingVisit,\n pageFlash,\n pageFormName,\n registerConflictForm,\n registerFormSubmitter,\n registerPageForm,\n resolvePartialProps,\n shouldResyncForm,\n wizardProgressStore,\n} from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { useCallback, useEffect, useMemo, useRef } from 'react'\nimport { useActionRunner, useAlfakitProps, usePageSchema, usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { ActionList } from '../actions/action-list'\nimport { showToast } from '../notifications/flash-toasts'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { FormProvider } from './form-context'\n\n/**\n * A page form: its state lives in a FormStore, and its buttons are actions that post the\n * current values through the action endpoint.\n */\nexport function FormBlock({ block }: { block: FormBlockSchema }) {\n const panel = usePanel()\n const page = usePageSchema()\n const props = useAlfakitProps()\n const runner = useActionRunner()\n const t = useT()\n const token = page?.token ?? ''\n\n // The store survives reloads of the same form (table sorting/paging, polling,\n // relation-manager actions all deliver a new `block` object); only a different form\n // identity starts from server state again. The token is re-issued per response, so it\n // is swapped in place instead of being part of the identity.\n const identity = formIdentity(page, block)\n\n const store = useMemo(\n () =>\n new FormStore(block, {\n token,\n component: block.component,\n refreshUrl: panel.endpoints.schemaRefresh,\n optionsUrl: panel.endpoints.schemaOptions,\n validateUrl: panel.endpoints.schemaValidate,\n // A halted wizard step shows what its hook sent.\n onNotifications: (notifications) => notifications.forEach((n) => showToast(n)),\n // Live dashboard filters are applied by the refresh; the widgets reload to follow.\n onRefreshed: () => {\n if (latestBlock.current.liveFilters)\n router.reload({ only: resolvePartialProps(['page']) })\n },\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [identity],\n )\n\n useEffect(() => () => store.destroy(), [store])\n\n // Named forms of custom pages (`<PageForm name>`) publish their store for page method calls.\n useEffect(() => {\n const name = pageFormName(block.component)\n\n return name ? registerPageForm(name, store) : undefined\n }, [block.component, store])\n\n // Edit pages: the conflict banner diffs and merges the main form (`detectConflicts()`).\n useEffect(\n () => (block.component === 'form' ? registerConflictForm(store) : undefined),\n [block.component, store],\n )\n\n useEffect(() => {\n store.setToken(token)\n }, [store, token])\n\n useEffect(() => {\n store.setErrors((props.errors as Record<string, string>) ?? {})\n }, [props.errors, store])\n\n const state = useStore(store.store)\n const context = useMemo<ActionContext>(() => ({ token, component: 'page' }), [token])\n const submitting = useRef(false)\n // After this form's own successful submit, refill from the server's new state. The new\n // block may arrive before or after Inertia's `success` event, so handle both orders.\n const latestBlock = useRef(block)\n const submittedBlock = useRef<FormBlockSchema | null>(null)\n const refillPending = useRef(false)\n // Another action succeeded and its new block has not arrived yet.\n const resyncPending = useRef(false)\n // The block when the latest non-GET visit started, to tell whether its response arrived.\n const visitBlock = useRef<FormBlockSchema | null>(null)\n\n useEffect(() => {\n latestBlock.current = block\n\n if (refillPending.current) {\n refillPending.current = false\n store.fill(block)\n }\n\n if (resyncPending.current) {\n resyncPending.current = false\n\n if (!store.isDirty) store.fill(block)\n }\n }, [block, store])\n\n const actions: ActionSchema[] = useMemo(\n () => block.actions.map((action) => ({ ...action, hasModal: false })),\n [block.actions],\n )\n\n const trigger = useCallback(\n async (action: ActionSchema, ctx: ActionContext) => {\n if (action.url && !action.hasAction) {\n return runner.trigger(action, ctx)\n }\n\n submitting.current = true\n submittedBlock.current = latestBlock.current\n store.setProcessing(true)\n runner.execute(action, ctx, store.state.values)\n },\n [runner, store],\n )\n\n // Buttons elsewhere with `formId('form')` (a header \"Save\") submit through this form.\n useEffect(() => registerFormSubmitter(block.component, trigger), [block.component, trigger])\n\n // `refreshFormData([...])`: an action changed the record; copy those fields in.\n useEffect(\n () =>\n router.on('success', (event) => {\n const refresh = formRefreshFor(pageFlash(event.detail.page), block.component)\n\n if (refresh) applyFormRefresh(store, refresh)\n }),\n [block.component, store],\n )\n\n // Wizard forms (`HasWizard` pages) show their buttons on the last step only.\n const progressStore = useMemo(() => wizardProgressStore(store), [store])\n const progress = useStore(progressStore)\n const showActions = formActionsVisible(block, progress)\n\n // Track this form's own submit visit so unrelated reloads finishing meanwhile (polling,\n // relation-manager paging) neither end its processing state nor count as its success.\n const submitVisit = useRef<string | null>(null)\n\n useEffect(() => {\n const offStart = router.on('start', (event) => {\n const visit = event.detail.visit\n\n if (submitting.current && !submitVisit.current && visit.method !== 'get') {\n submitVisit.current = visit.id\n }\n\n if (visit.method !== 'get') visitBlock.current = latestBlock.current\n })\n\n const offSuccess = router.on('success', (event) => {\n const id = submitVisit.current\n\n // Another action changed the record: show the server's new form unless it holds edits.\n if (!id && shouldResyncForm(pageFlash(event.detail.page), store)) {\n if (latestBlock.current !== visitBlock.current) {\n store.fill(latestBlock.current)\n } else {\n resyncPending.current = true\n }\n\n return\n }\n\n if (!id || (event.detail.visitId ?? id) !== id) return\n\n // A halted action (rate limit, `halt()`) saved nothing: keep what the user typed.\n if (pageFlash(event.detail.page).alfakitAction?.status === 'halted') return\n\n if (latestBlock.current !== submittedBlock.current) {\n store.fill(latestBlock.current)\n } else {\n refillPending.current = true\n }\n })\n\n const offFinish = router.on('finish', (event) => {\n if (submitVisit.current && event.detail.visit.id !== submitVisit.current) return\n\n store.setProcessing(false)\n submitting.current = false\n submitVisit.current = null\n })\n\n return () => {\n offStart()\n offSuccess()\n offFinish()\n }\n }, [store])\n\n useEffect(() => {\n if (!panel.unsavedChangesAlerts) {\n return\n }\n\n return router.on('before', (event) => {\n if (\n store.isDirty &&\n !submitting.current &&\n isLeavingVisit(event.detail.visit) &&\n !window.confirm(t('unsaved_changes', {}, 'You have unsaved changes. Leave anyway?'))\n ) {\n event.preventDefault()\n }\n })\n }, [panel.unsavedChangesAlerts, store, t])\n\n const primary = actions[0]\n\n return (\n <FormProvider store={store}>\n <form\n data-slot=\"form-block\"\n className=\"flex flex-col gap-6\"\n onSubmit={(event) => {\n event.preventDefault()\n\n if (primary && showActions) {\n void trigger(primary, context)\n }\n }}\n onKeyDown={(event) => {\n // Enter in a text field runs the primary action (actions are type=\"button\").\n if (primary && showActions && isImplicitSubmit(event.nativeEvent, event.currentTarget)) {\n event.preventDefault()\n void trigger(primary, context)\n }\n }}\n >\n <fieldset disabled={state.processing || block.operation === 'view'} className=\"contents\">\n <SchemaRenderer components={state.components} columns={state.columns} />\n </fieldset>\n {actions.length > 0 && showActions && (\n <div data-slot=\"form-actions\" className=\"border-t pt-6\">\n <ActionList actions={actions} context={context} onTrigger={trigger} />\n </div>\n )}\n </form>\n </FormProvider>\n )\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useStore, useStoreSelector } from "../../hooks/use-store.js";
|
|
2
|
-
import { fieldAria } from "@alfakit/core";
|
|
2
|
+
import { fieldAria, fieldDomId } from "@alfakit/core";
|
|
3
3
|
import { createContext, useCallback, useContext, useMemo } from "react";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
//#region src/components/form/form-context.tsx
|
|
@@ -36,7 +36,11 @@ function useField(field) {
|
|
|
36
36
|
const error = useStoreSelector(store.store, (state) => state.errors[path]?.[0]);
|
|
37
37
|
const setValue = useCallback((next) => store.setValue(path, next), [store, path]);
|
|
38
38
|
const blur = useCallback(() => store.blur(path), [store, path]);
|
|
39
|
-
const id = useMemo(() => field.id ??
|
|
39
|
+
const id = useMemo(() => field.id ?? fieldDomId(store.idPrefix, path), [
|
|
40
|
+
field.id,
|
|
41
|
+
store,
|
|
42
|
+
path
|
|
43
|
+
]);
|
|
40
44
|
const aria = useMemo(() => fieldAria(id, {
|
|
41
45
|
hint: field.hint,
|
|
42
46
|
helperText: field.helperText,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-context.js","names":[],"sources":["../../../src/components/form/form-context.tsx"],"sourcesContent":["import type { BaseField, FieldAriaAttributes, FormState, FormStore } from '@alfakit/core'\nimport { fieldAria } from '@alfakit/core'\nimport { createContext, useCallback, useContext, useMemo, type ReactNode } from 'react'\nimport { useStore, useStoreSelector } from '../../hooks/use-store'\n\nconst FormStoreContext = createContext<FormStore | null>(null)\n\nexport function FormProvider({ store, children }: { store: FormStore; children: ReactNode }) {\n return <FormStoreContext.Provider value={store}>{children}</FormStoreContext.Provider>\n}\n\nexport function useFormStore(): FormStore {\n const store = useContext(FormStoreContext)\n\n if (!store) {\n throw new Error('Form fields must be rendered inside a FormProvider.')\n }\n\n return store\n}\n\nexport function useOptionalFormStore(): FormStore | null {\n return useContext(FormStoreContext)\n}\n\nexport function useFormState(): FormState {\n return useStore(useFormStore().store)\n}\n\nexport interface FieldBinding<T = unknown> {\n value: T\n setValue: (value: T) => void\n blur: () => void\n error: string | undefined\n id: string\n /** The field cannot be interacted with at all (`disabled()`). */\n disabled: boolean\n /** The field shows its value but the user cannot change it (`readOnly()`). */\n readOnly: boolean\n /** `aria-describedby` (hint, helper text, error), `aria-invalid` and `aria-required` for the control. */\n aria: FieldAriaAttributes\n}\n\n/** Bind a field schema (a built-in or custom field) to the form store. */\nexport function useField<T = unknown>(field: BaseField): FieldBinding<T> {\n const store = useFormStore()\n const path = field.statePath\n\n const value = useStoreSelector(store.store, (state) => {\n let current: unknown = state.values\n\n for (const segment of path.split('.')) {\n if (current === null || current === undefined || typeof current !== 'object') {\n return undefined as T\n }\n\n current = (current as Record<string, unknown>)[segment]\n }\n\n return current as T\n })\n\n const error = useStoreSelector(store.store, (state) => state.errors[path]?.[0])\n\n const setValue = useCallback((next: T) => store.setValue(path, next), [store, path])\n const blur = useCallback(() => store.blur(path), [store, path])\n\n const id = useMemo(
|
|
1
|
+
{"version":3,"file":"form-context.js","names":[],"sources":["../../../src/components/form/form-context.tsx"],"sourcesContent":["import type { BaseField, FieldAriaAttributes, FormState, FormStore } from '@alfakit/core'\nimport { fieldAria, fieldDomId } from '@alfakit/core'\nimport { createContext, useCallback, useContext, useMemo, type ReactNode } from 'react'\nimport { useStore, useStoreSelector } from '../../hooks/use-store'\n\nconst FormStoreContext = createContext<FormStore | null>(null)\n\nexport function FormProvider({ store, children }: { store: FormStore; children: ReactNode }) {\n return <FormStoreContext.Provider value={store}>{children}</FormStoreContext.Provider>\n}\n\nexport function useFormStore(): FormStore {\n const store = useContext(FormStoreContext)\n\n if (!store) {\n throw new Error('Form fields must be rendered inside a FormProvider.')\n }\n\n return store\n}\n\nexport function useOptionalFormStore(): FormStore | null {\n return useContext(FormStoreContext)\n}\n\nexport function useFormState(): FormState {\n return useStore(useFormStore().store)\n}\n\nexport interface FieldBinding<T = unknown> {\n value: T\n setValue: (value: T) => void\n blur: () => void\n error: string | undefined\n id: string\n /** The field cannot be interacted with at all (`disabled()`). */\n disabled: boolean\n /** The field shows its value but the user cannot change it (`readOnly()`). */\n readOnly: boolean\n /** `aria-describedby` (hint, helper text, error), `aria-invalid` and `aria-required` for the control. */\n aria: FieldAriaAttributes\n}\n\n/** Bind a field schema (a built-in or custom field) to the form store. */\nexport function useField<T = unknown>(field: BaseField): FieldBinding<T> {\n const store = useFormStore()\n const path = field.statePath\n\n const value = useStoreSelector(store.store, (state) => {\n let current: unknown = state.values\n\n for (const segment of path.split('.')) {\n if (current === null || current === undefined || typeof current !== 'object') {\n return undefined as T\n }\n\n current = (current as Record<string, unknown>)[segment]\n }\n\n return current as T\n })\n\n const error = useStoreSelector(store.store, (state) => state.errors[path]?.[0])\n\n const setValue = useCallback((next: T) => store.setValue(path, next), [store, path])\n const blur = useCallback(() => store.blur(path), [store, path])\n\n const id = useMemo(() => field.id ?? fieldDomId(store.idPrefix, path), [field.id, store, path])\n\n const aria = useMemo(\n () =>\n fieldAria(\n id,\n { hint: field.hint, helperText: field.helperText, required: field.required },\n error,\n ),\n [id, field.hint, field.helperText, field.required, error],\n )\n\n // Fields stay enabled during a live refresh: the store keeps edits made while the request\n // is in flight, so locking inputs would only swallow keystrokes.\n return {\n value,\n setValue,\n blur,\n error,\n id,\n disabled: Boolean(field.disabled),\n readOnly: Boolean(field.readOnly),\n aria,\n }\n}\n"],"mappings":";;;;;AAKA,MAAM,mBAAmB,cAAgC,IAAI;AAE7D,SAAgB,aAAa,EAAE,OAAO,YAAuD;CAC3F,OAAO,oBAAC,iBAAiB,UAAlB;EAA2B,OAAO;EAAQ;CAAoC,CAAA;AACvF;AAEA,SAAgB,eAA0B;CACxC,MAAM,QAAQ,WAAW,gBAAgB;CAEzC,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,qDAAqD;CAGvE,OAAO;AACT;AAEA,SAAgB,uBAAyC;CACvD,OAAO,WAAW,gBAAgB;AACpC;AAEA,SAAgB,eAA0B;CACxC,OAAO,SAAS,aAAa,CAAC,CAAC,KAAK;AACtC;;AAiBA,SAAgB,SAAsB,OAAmC;CACvE,MAAM,QAAQ,aAAa;CAC3B,MAAM,OAAO,MAAM;CAEnB,MAAM,QAAQ,iBAAiB,MAAM,QAAQ,UAAU;EACrD,IAAI,UAAmB,MAAM;EAE7B,KAAK,MAAM,WAAW,KAAK,MAAM,GAAG,GAAG;GACrC,IAAI,YAAY,QAAQ,YAAY,KAAA,KAAa,OAAO,YAAY,UAClE;GAGF,UAAW,QAAoC;EACjD;EAEA,OAAO;CACT,CAAC;CAED,MAAM,QAAQ,iBAAiB,MAAM,QAAQ,UAAU,MAAM,OAAO,KAAK,GAAG,EAAE;CAE9E,MAAM,WAAW,aAAa,SAAY,MAAM,SAAS,MAAM,IAAI,GAAG,CAAC,OAAO,IAAI,CAAC;CACnF,MAAM,OAAO,kBAAkB,MAAM,KAAK,IAAI,GAAG,CAAC,OAAO,IAAI,CAAC;CAE9D,MAAM,KAAK,cAAc,MAAM,MAAM,WAAW,MAAM,UAAU,IAAI,GAAG;EAAC,MAAM;EAAI;EAAO;CAAI,CAAC;CAE9F,MAAM,OAAO,cAET,UACE,IACA;EAAE,MAAM,MAAM;EAAM,YAAY,MAAM;EAAY,UAAU,MAAM;CAAS,GAC3E,KACF,GACF;EAAC;EAAI,MAAM;EAAM,MAAM;EAAY,MAAM;EAAU;CAAK,CAC1D;CAIA,OAAO;EACL;EACA;EACA;EACA;EACA;EACA,UAAU,QAAQ,MAAM,QAAQ;EAChC,UAAU,QAAQ,MAAM,QAAQ;EAChC;CACF;AACF"}
|
|
@@ -23,6 +23,7 @@ function FusedGroupComponent({ component }) {
|
|
|
23
23
|
...extraProps(component.extraAttributes, "flex flex-col gap-1.5"),
|
|
24
24
|
children: [component.label && /* @__PURE__ */ jsx("span", {
|
|
25
25
|
id: labelId,
|
|
26
|
+
"data-slot": "label",
|
|
26
27
|
className: "text-sm leading-none font-medium select-none",
|
|
27
28
|
children: component.label
|
|
28
29
|
}), store ? /* @__PURE__ */ jsx(FusedGroupBody, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fused-group.js","names":[],"sources":["../../../src/components/layout/fused-group.tsx"],"sourcesContent":["import type { FormStore, FusedGroupSchema } from '@alfakit/core'\nimport { collectFieldStatePaths, gridColumnsClasses } from '@alfakit/core'\nimport { useId, useMemo } from 'react'\nimport { useStoreSelector } from '../../hooks/use-store'\nimport { extraProps } from '../../lib/attributes'\nimport { cn } from '../../lib/utils'\nimport { useOptionalFormStore } from '../form/form-context'\nimport { FusedGroupContext } from '../schema/fused-group-context'\nimport { RenderComponent } from '../schema/schema-renderer'\n\n/**\n * Fields fused into one control: one border around them (styled by `themes.css`), the\n * group label above, and the fields' validation errors below.\n */\nexport function FusedGroupComponent({ component }: { component: FusedGroupSchema }) {\n const labelId = `${useId()}-label`\n const store = useOptionalFormStore()\n const paths = useMemo(() => collectFieldStatePaths(component.schema), [component.schema])\n\n return (\n <div\n data-slot=\"fused-group\"\n role=\"group\"\n aria-labelledby={component.label ? labelId : undefined}\n {...extraProps(component.extraAttributes, 'flex flex-col gap-1.5')}\n >\n {component.label && (\n <span
|
|
1
|
+
{"version":3,"file":"fused-group.js","names":[],"sources":["../../../src/components/layout/fused-group.tsx"],"sourcesContent":["import type { FormStore, FusedGroupSchema } from '@alfakit/core'\nimport { collectFieldStatePaths, gridColumnsClasses } from '@alfakit/core'\nimport { useId, useMemo } from 'react'\nimport { useStoreSelector } from '../../hooks/use-store'\nimport { extraProps } from '../../lib/attributes'\nimport { cn } from '../../lib/utils'\nimport { useOptionalFormStore } from '../form/form-context'\nimport { FusedGroupContext } from '../schema/fused-group-context'\nimport { RenderComponent } from '../schema/schema-renderer'\n\n/**\n * Fields fused into one control: one border around them (styled by `themes.css`), the\n * group label above, and the fields' validation errors below.\n */\nexport function FusedGroupComponent({ component }: { component: FusedGroupSchema }) {\n const labelId = `${useId()}-label`\n const store = useOptionalFormStore()\n const paths = useMemo(() => collectFieldStatePaths(component.schema), [component.schema])\n\n return (\n <div\n data-slot=\"fused-group\"\n role=\"group\"\n aria-labelledby={component.label ? labelId : undefined}\n {...extraProps(component.extraAttributes, 'flex flex-col gap-1.5')}\n >\n {component.label && (\n <span\n id={labelId}\n data-slot=\"label\"\n className=\"text-sm leading-none font-medium select-none\"\n >\n {component.label}\n </span>\n )}\n {store ? (\n <FusedGroupBody component={component} store={store} paths={paths} />\n ) : (\n <FusedGroupControls component={component} />\n )}\n </div>\n )\n}\n\nfunction FusedGroupBody({\n component,\n store,\n paths,\n}: {\n component: FusedGroupSchema\n store: FormStore\n paths: string[]\n}) {\n // A joined string keeps the selected snapshot stable between renders.\n const joined = useStoreSelector(store.store, (state) =>\n paths\n .map((path) => state.errors[path]?.[0])\n .filter((message): message is string => Boolean(message))\n .join('\\n'),\n )\n const errors = joined === '' ? [] : joined.split('\\n')\n\n return (\n <>\n <FusedGroupControls component={component} invalid={errors.length > 0} />\n {errors.length > 0 && (\n // The fields announce their own errors; this list is the visible copy.\n <div aria-hidden=\"true\" className=\"flex flex-col gap-0.5\">\n {errors.map((message, index) => (\n <p key={index} className=\"text-destructive text-xs\">\n {message}\n </p>\n ))}\n </div>\n )}\n </>\n )\n}\n\nfunction FusedGroupControls({\n component,\n invalid = false,\n}: {\n component: FusedGroupSchema\n invalid?: boolean\n}) {\n return (\n <FusedGroupContext.Provider value={true}>\n <div\n data-slot=\"fused-group-controls\"\n data-invalid={invalid ? '' : undefined}\n className={cn('grid', gridColumnsClasses(component.columns ?? { default: 1 }))}\n >\n {(component.schema ?? []).map((child) => (\n <RenderComponent key={child.key} component={child} />\n ))}\n </div>\n </FusedGroupContext.Provider>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,SAAgB,oBAAoB,EAAE,aAA8C;CAClF,MAAM,UAAU,GAAG,MAAM,EAAE;CAC3B,MAAM,QAAQ,qBAAqB;CACnC,MAAM,QAAQ,cAAc,uBAAuB,UAAU,MAAM,GAAG,CAAC,UAAU,MAAM,CAAC;CAExF,OACE,qBAAC,OAAD;EACE,aAAU;EACV,MAAK;EACL,mBAAiB,UAAU,QAAQ,UAAU,KAAA;EAC7C,GAAI,WAAW,UAAU,iBAAiB,uBAAuB;EAJnE,UAAA,CAMG,UAAU,SACT,oBAAC,QAAD;GACE,IAAI;GACJ,aAAU;GACV,WAAU;GAET,UAAA,UAAU;EACP,CAAA,GAEP,QACC,oBAAC,gBAAD;GAA2B;GAAkB;GAAc;EAAQ,CAAA,IAEnE,oBAAC,oBAAD,EAA+B,UAAY,CAAA,CAE1C;;AAET;AAEA,SAAS,eAAe,EACtB,WACA,OACA,SAKC;CAED,MAAM,SAAS,iBAAiB,MAAM,QAAQ,UAC5C,MACG,KAAK,SAAS,MAAM,OAAO,KAAK,GAAG,EAAE,CAAC,CACtC,QAAQ,YAA+B,QAAQ,OAAO,CAAC,CAAC,CACxD,KAAK,IAAI,CACd;CACA,MAAM,SAAS,WAAW,KAAK,CAAC,IAAI,OAAO,MAAM,IAAI;CAErD,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,oBAAD;EAA+B;EAAW,SAAS,OAAO,SAAS;CAAI,CAAA,GACtE,OAAO,SAAS,KAEf,oBAAC,OAAD;EAAK,eAAY;EAAO,WAAU;EAC/B,UAAA,OAAO,KAAK,SAAS,UACpB,oBAAC,KAAD;GAAe,WAAU;GACtB,UAAA;EACA,GAFK,KAEL,CACJ;CACE,CAAA,CAEP,EAAA,CAAA;AAEN;AAEA,SAAS,mBAAmB,EAC1B,WACA,UAAU,SAIT;CACD,OACE,oBAAC,kBAAkB,UAAnB;EAA4B,OAAO;EACjC,UAAA,oBAAC,OAAD;GACE,aAAU;GACV,gBAAc,UAAU,KAAK,KAAA;GAC7B,WAAW,GAAG,QAAQ,mBAAmB,UAAU,WAAW,EAAE,SAAS,EAAE,CAAC,CAAC;GAE3E,WAAA,UAAU,UAAU,CAAC,EAAA,CAAG,KAAK,UAC7B,oBAAC,iBAAD,EAAiC,WAAW,MAAQ,GAA9B,MAAM,GAAwB,CACrD;EACE,CAAA;CACqB,CAAA;AAEhC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabs.d.ts","names":[],"sources":["../../../src/components/layout/tabs.tsx"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"tabs.d.ts","names":[],"sources":["../../../src/components/layout/tabs.tsx"],"mappings":";;wBAmCgB,gBAAgB;EAAe,WAAW;oBAAY,IAAA"}
|
|
@@ -1,37 +1,60 @@
|
|
|
1
|
+
import { useT } from "../../context.js";
|
|
1
2
|
import { cn } from "../../lib/utils.js";
|
|
2
3
|
import { badgeColorClasses, colorClass } from "../../lib/colors.js";
|
|
3
4
|
import { Icon } from "../../lib/icons.js";
|
|
4
5
|
import { Badge } from "../ui/badge.js";
|
|
6
|
+
import { useStoreSelector } from "../../hooks/use-store.js";
|
|
5
7
|
import { extraProps } from "../../lib/attributes.js";
|
|
6
8
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs.js";
|
|
9
|
+
import { useOptionalFormStore } from "../form/form-context.js";
|
|
7
10
|
import { DeferredSchema, useDeferredBadge } from "./deferred.js";
|
|
8
11
|
import { SchemaRenderer } from "../schema/schema-renderer.js";
|
|
9
12
|
import { useHydrated } from "../../hooks/use-hydrated.js";
|
|
10
|
-
import { readPersisted, schemaGapClass, writePersisted } from "@alfakit/core";
|
|
13
|
+
import { componentHasErrors, readPersisted, schemaGapClass, tabFromQuery, tabQueryValue, tabToReveal, writePersisted } from "@alfakit/core";
|
|
11
14
|
import { usePage } from "@inertiajs/react";
|
|
12
15
|
import { useEffect, useState } from "react";
|
|
13
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
16
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
14
17
|
//#region src/components/layout/tabs.tsx
|
|
18
|
+
const NO_ERRORS = {};
|
|
19
|
+
const NO_FORM_STORE = {
|
|
20
|
+
get: () => ({ errors: NO_ERRORS }),
|
|
21
|
+
subscribe: () => () => {}
|
|
22
|
+
};
|
|
23
|
+
const TAB_INVALID_CLASSES = "text-destructive data-[state=active]:text-destructive dark:text-destructive dark:data-[state=active]:text-destructive";
|
|
15
24
|
function TabsComponent({ component }) {
|
|
16
25
|
const tabs = component.schema ?? [];
|
|
17
26
|
const url = usePage().url;
|
|
18
|
-
const fromQuery = component.queryStringKey ? new URL(url, "http://localhost").searchParams.get(component.queryStringKey) : null;
|
|
19
|
-
const [initial] = useState(() =>
|
|
27
|
+
const fromQuery = component.queryStringKey ? tabFromQuery(tabs, new URL(url, "http://localhost").searchParams.get(component.queryStringKey)) : null;
|
|
28
|
+
const [initial] = useState(() => fromQuery ?? tabs[Math.max(0, component.activeTab - 1)]?.key ?? tabs[0]?.key ?? "");
|
|
20
29
|
const persist = component.persistTab;
|
|
21
30
|
const hydrated = useHydrated();
|
|
22
31
|
const [chosen, setChosen] = useState(null);
|
|
23
32
|
const stored = hydrated && persist && !fromQuery ? readPersisted(persist.key, persist.storage) : null;
|
|
24
33
|
const active = chosen ?? (stored && tabs.some((tab) => tab.key === stored) ? stored : initial);
|
|
34
|
+
const t = useT();
|
|
35
|
+
const store = useOptionalFormStore();
|
|
36
|
+
const errors = useStoreSelector(store?.store ?? NO_FORM_STORE, (state) => state.errors);
|
|
37
|
+
const [seenErrors, setSeenErrors] = useState({});
|
|
38
|
+
if (seenErrors !== errors) {
|
|
39
|
+
setSeenErrors(errors);
|
|
40
|
+
const reveal = tabToReveal(tabs, active, seenErrors, errors);
|
|
41
|
+
if (reveal !== null) setChosen(reveal);
|
|
42
|
+
}
|
|
25
43
|
const changeTab = (value) => {
|
|
26
44
|
setChosen(value);
|
|
27
45
|
if (persist) writePersisted(persist.key, value, persist.storage);
|
|
28
46
|
};
|
|
29
47
|
useEffect(() => {
|
|
30
48
|
if (!component.queryStringKey || typeof window === "undefined") return;
|
|
49
|
+
const tab = tabs.find((item) => item.key === active);
|
|
31
50
|
const url = new URL(window.location.href);
|
|
32
|
-
url.searchParams.set(component.queryStringKey, active);
|
|
51
|
+
url.searchParams.set(component.queryStringKey, tab ? tabQueryValue(tab) : active);
|
|
33
52
|
window.history.replaceState(window.history.state, "", url);
|
|
34
|
-
}, [
|
|
53
|
+
}, [
|
|
54
|
+
active,
|
|
55
|
+
component.queryStringKey,
|
|
56
|
+
tabs
|
|
57
|
+
]);
|
|
35
58
|
return /* @__PURE__ */ jsxs(Tabs, {
|
|
36
59
|
"data-surface": component.contained ? "tabs" : void 0,
|
|
37
60
|
value: active,
|
|
@@ -41,15 +64,31 @@ function TabsComponent({ component }) {
|
|
|
41
64
|
children: [/* @__PURE__ */ jsx(TabsList, {
|
|
42
65
|
"data-scrollable": component.scrollable === false ? "false" : void 0,
|
|
43
66
|
className: cn(component.contained && "m-2", component.vertical ? "self-start" : component.scrollable === false ? "h-auto max-w-full flex-wrap justify-start" : "relative max-w-full overflow-x-auto"),
|
|
44
|
-
children: tabs.map((tab) =>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
67
|
+
children: tabs.map((tab) => {
|
|
68
|
+
const invalid = componentHasErrors(tab, errors);
|
|
69
|
+
return /* @__PURE__ */ jsxs(TabsTrigger, {
|
|
70
|
+
value: tab.key,
|
|
71
|
+
"data-invalid": invalid || void 0,
|
|
72
|
+
className: invalid ? TAB_INVALID_CLASSES : void 0,
|
|
73
|
+
children: [
|
|
74
|
+
tab.icon && tab.iconPosition !== "after" && /* @__PURE__ */ jsx(Icon, { name: tab.icon }),
|
|
75
|
+
tab.label,
|
|
76
|
+
tab.icon && tab.iconPosition === "after" && /* @__PURE__ */ jsx(Icon, { name: tab.icon }),
|
|
77
|
+
/* @__PURE__ */ jsx(TabBadge, { tab }),
|
|
78
|
+
invalid && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
79
|
+
"aria-hidden": "true",
|
|
80
|
+
className: "bg-destructive size-1.5 shrink-0 rounded-full"
|
|
81
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
82
|
+
className: "sr-only",
|
|
83
|
+
children: [
|
|
84
|
+
"(",
|
|
85
|
+
t("translatable.has_errors"),
|
|
86
|
+
")"
|
|
87
|
+
]
|
|
88
|
+
})] })
|
|
89
|
+
]
|
|
90
|
+
}, tab.key);
|
|
91
|
+
})
|
|
53
92
|
}), tabs.map((tab) => /* @__PURE__ */ jsx(TabsContent, {
|
|
54
93
|
value: tab.key,
|
|
55
94
|
forceMount: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabs.js","names":[],"sources":["../../../src/components/layout/tabs.tsx"],"sourcesContent":["import type { TabSchema, TabsSchema } from '@alfakit/core'\nimport {
|
|
1
|
+
{"version":3,"file":"tabs.js","names":[],"sources":["../../../src/components/layout/tabs.tsx"],"sourcesContent":["import type { TabSchema, TabsSchema } from '@alfakit/core'\nimport {\n componentHasErrors,\n readPersisted,\n schemaGapClass,\n tabFromQuery,\n tabQueryValue,\n tabToReveal,\n writePersisted,\n} from '@alfakit/core'\nimport { usePage } from '@inertiajs/react'\nimport { useEffect, useState } from 'react'\nimport { useT } from '../../context'\nimport { useHydrated } from '../../hooks/use-hydrated'\nimport { useStoreSelector } from '../../hooks/use-store'\nimport { extraProps } from '../../lib/attributes'\nimport { badgeColorClasses, colorClass } from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { useOptionalFormStore } from '../form/form-context'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { Badge } from '../ui/badge'\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'\nimport { DeferredSchema, useDeferredBadge } from './deferred'\n\n// Used when tabs render outside a form. Module-level so the snapshot stays stable.\nconst NO_ERRORS: Record<string, string[]> = {}\nconst NO_FORM_STORE = {\n get: () => ({ errors: NO_ERRORS }),\n subscribe: () => () => {},\n}\n\nconst TAB_INVALID_CLASSES =\n 'text-destructive data-[state=active]:text-destructive dark:text-destructive dark:data-[state=active]:text-destructive'\n\nexport function TabsComponent({ component }: { component: TabsSchema }) {\n const tabs = component.schema ?? []\n // The page URL (not `window.location`) so the server renders the same tab as the browser.\n const url = usePage().url\n // The tab's readable value (`?tab=billing-address`), or its key in links from older versions.\n const fromQuery = component.queryStringKey\n ? tabFromQuery(\n tabs,\n new URL(url, 'http://localhost').searchParams.get(component.queryStringKey),\n )\n : null\n const [initial] = useState<string>(\n () => fromQuery ?? tabs[Math.max(0, component.activeTab - 1)]?.key ?? tabs[0]?.key ?? '',\n )\n\n const persist = component.persistTab\n const hydrated = useHydrated()\n const [chosen, setChosen] = useState<string | null>(null)\n // `persistTab()`: read only after hydration (the server can't see browser storage); a\n // tab named in the query string wins.\n const stored =\n hydrated && persist && !fromQuery ? readPersisted(persist.key, persist.storage) : null\n const active = chosen ?? (stored && tabs.some((tab) => tab.key === stored) ? stored : initial)\n\n // Tabs holding a field with a validation error are marked, and when a submit brings new\n // errors while the open tab has none, the first tab with an error opens.\n const t = useT()\n const store = useOptionalFormStore()\n const errors = useStoreSelector(store?.store ?? NO_FORM_STORE, (state) => state.errors)\n const [seenErrors, setSeenErrors] = useState<Record<string, string[]>>({})\n\n if (seenErrors !== errors) {\n setSeenErrors(errors)\n\n const reveal = tabToReveal(tabs, active, seenErrors, errors)\n\n if (reveal !== null) setChosen(reveal)\n }\n\n const changeTab = (value: string) => {\n setChosen(value)\n\n if (persist) writePersisted(persist.key, value, persist.storage)\n }\n\n useEffect(() => {\n if (!component.queryStringKey || typeof window === 'undefined') return\n\n const tab = tabs.find((item) => item.key === active)\n const url = new URL(window.location.href)\n url.searchParams.set(component.queryStringKey, tab ? tabQueryValue(tab) : active)\n window.history.replaceState(window.history.state, '', url)\n }, [active, component.queryStringKey, tabs])\n\n return (\n <Tabs\n data-surface={component.contained ? 'tabs' : undefined}\n value={active}\n onValueChange={changeTab}\n orientation={component.vertical ? 'vertical' : 'horizontal'}\n\n {...extraProps(\n component.extraAttributes,\n cn(\n component.contained && 'bg-card rounded-xl border shadow-xs',\n component.vertical && 'flex-row',\n ),\n )}\n >\n <TabsList\n data-scrollable={component.scrollable === false ? 'false' : undefined}\n className={cn(\n component.contained && 'm-2',\n component.vertical\n ? 'self-start'\n : component.scrollable === false\n ? 'h-auto max-w-full flex-wrap justify-start'\n : 'relative max-w-full overflow-x-auto',\n )}\n >\n {tabs.map((tab) => {\n const invalid = componentHasErrors(tab, errors)\n\n return (\n <TabsTrigger\n key={tab.key}\n value={tab.key}\n data-invalid={invalid || undefined}\n className={invalid ? TAB_INVALID_CLASSES : undefined}\n >\n {tab.icon && tab.iconPosition !== 'after' && <Icon name={tab.icon} />}\n {tab.label}\n {tab.icon && tab.iconPosition === 'after' && <Icon name={tab.icon} />}\n <TabBadge tab={tab} />\n {invalid && (\n <>\n <span\n aria-hidden=\"true\"\n className=\"bg-destructive size-1.5 shrink-0 rounded-full\"\n />\n <span className=\"sr-only\">({t('translatable.has_errors')})</span>\n </>\n )}\n </TabsTrigger>\n )\n })}\n </TabsList>\n {tabs.map((tab) => (\n <TabsContent\n key={tab.key}\n value={tab.key}\n forceMount\n className={cn('data-[state=inactive]:hidden', component.contained ? 'p-6 pt-4' : 'pt-4')}\n >\n <DeferredSchema component={tab}>\n {(loaded) => (\n <SchemaRenderer\n components={loaded.schema ?? []}\n columns={loaded.columns}\n attributes={loaded.extraAttributes}\n className={schemaGapClass(loaded) || undefined}\n />\n )}\n </DeferredSchema>\n </TabsContent>\n ))}\n </Tabs>\n )\n}\n\n/** A tab's badge; `deferBadge()` badges appear once loaded. */\nfunction TabBadge({ tab }: { tab: TabSchema }) {\n const { badge, badgeColor, badgeTooltip } = useDeferredBadge(tab)\n\n if (badge === undefined || badge === null) return null\n\n return (\n <Badge\n variant=\"outline\"\n title={badgeTooltip ?? undefined}\n className={cn(\n 'ms-1 px-1.5 py-0',\n colorClass(badgeColorClasses, badgeColor ?? undefined, 'gray'),\n )}\n >\n {badge}\n </Badge>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA0BA,MAAM,YAAsC,CAAC;AAC7C,MAAM,gBAAgB;CACpB,YAAY,EAAE,QAAQ,UAAU;CAChC,uBAAuB,CAAC;AAC1B;AAEA,MAAM,sBACJ;AAEF,SAAgB,cAAc,EAAE,aAAwC;CACtE,MAAM,OAAO,UAAU,UAAU,CAAC;CAElC,MAAM,MAAM,QAAQ,CAAC,CAAC;CAEtB,MAAM,YAAY,UAAU,iBACxB,aACE,MACA,IAAI,IAAI,KAAK,kBAAkB,CAAC,CAAC,aAAa,IAAI,UAAU,cAAc,CAC5E,IACA;CACJ,MAAM,CAAC,WAAW,eACV,aAAa,KAAK,KAAK,IAAI,GAAG,UAAU,YAAY,CAAC,EAAE,EAAE,OAAO,KAAK,EAAE,EAAE,OAAO,EACxF;CAEA,MAAM,UAAU,UAAU;CAC1B,MAAM,WAAW,YAAY;CAC7B,MAAM,CAAC,QAAQ,aAAa,SAAwB,IAAI;CAGxD,MAAM,SACJ,YAAY,WAAW,CAAC,YAAY,cAAc,QAAQ,KAAK,QAAQ,OAAO,IAAI;CACpF,MAAM,SAAS,WAAW,UAAU,KAAK,MAAM,QAAQ,IAAI,QAAQ,MAAM,IAAI,SAAS;CAItF,MAAM,IAAI,KAAK;CACf,MAAM,QAAQ,qBAAqB;CACnC,MAAM,SAAS,iBAAiB,OAAO,SAAS,gBAAgB,UAAU,MAAM,MAAM;CACtF,MAAM,CAAC,YAAY,iBAAiB,SAAmC,CAAC,CAAC;CAEzE,IAAI,eAAe,QAAQ;EACzB,cAAc,MAAM;EAEpB,MAAM,SAAS,YAAY,MAAM,QAAQ,YAAY,MAAM;EAE3D,IAAI,WAAW,MAAM,UAAU,MAAM;CACvC;CAEA,MAAM,aAAa,UAAkB;EACnC,UAAU,KAAK;EAEf,IAAI,SAAS,eAAe,QAAQ,KAAK,OAAO,QAAQ,OAAO;CACjE;CAEA,gBAAgB;EACd,IAAI,CAAC,UAAU,kBAAkB,OAAO,WAAW,aAAa;EAEhE,MAAM,MAAM,KAAK,MAAM,SAAS,KAAK,QAAQ,MAAM;EACnD,MAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;EACxC,IAAI,aAAa,IAAI,UAAU,gBAAgB,MAAM,cAAc,GAAG,IAAI,MAAM;EAChF,OAAO,QAAQ,aAAa,OAAO,QAAQ,OAAO,IAAI,GAAG;CAC3D,GAAG;EAAC;EAAQ,UAAU;EAAgB;CAAI,CAAC;CAE3C,OACE,qBAAC,MAAD;EACE,gBAAc,UAAU,YAAY,SAAS,KAAA;EAC7C,OAAO;EACP,eAAe;EACf,aAAa,UAAU,WAAW,aAAa;EAE/C,GAAI,WACF,UAAU,iBACV,GACE,UAAU,aAAa,uCACvB,UAAU,YAAY,UACxB,CACF;EAZF,UAAA,CAcE,oBAAC,UAAD;GACE,mBAAiB,UAAU,eAAe,QAAQ,UAAU,KAAA;GAC5D,WAAW,GACT,UAAU,aAAa,OACvB,UAAU,WACN,eACA,UAAU,eAAe,QACvB,8CACA,qCACR;GAEC,UAAA,KAAK,KAAK,QAAQ;IACjB,MAAM,UAAU,mBAAmB,KAAK,MAAM;IAE9C,OACE,qBAAC,aAAD;KAEE,OAAO,IAAI;KACX,gBAAc,WAAW,KAAA;KACzB,WAAW,UAAU,sBAAsB,KAAA;KAJ7C,UAAA;MAMG,IAAI,QAAQ,IAAI,iBAAiB,WAAW,oBAAC,MAAD,EAAM,MAAM,IAAI,KAAO,CAAA;MACnE,IAAI;MACJ,IAAI,QAAQ,IAAI,iBAAiB,WAAW,oBAAC,MAAD,EAAM,MAAM,IAAI,KAAO,CAAA;MACpE,oBAAC,UAAD,EAAe,IAAM,CAAA;MACpB,WACC,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,QAAD;OACE,eAAY;OACZ,WAAU;MACX,CAAA,GACD,qBAAC,QAAD;OAAM,WAAU;OAAhB,UAAA;QAA0B;QAAE,EAAE,yBAAyB;QAAE;OAAO;MAChE,CAAA,CAAA,EAAA,CAAA;KAEO;IAlBN,GAAA,IAAI,GAkBE;GAEjB,CAAC;EACO,CAAA,GACT,KAAK,KAAK,QACT,oBAAC,aAAD;GAEE,OAAO,IAAI;GACX,YAAA;GACA,WAAW,GAAG,gCAAgC,UAAU,YAAY,aAAa,MAAM;GAEvF,UAAA,oBAAC,gBAAD;IAAgB,WAAW;IACvB,WAAA,WACA,oBAAC,gBAAD;KACE,YAAY,OAAO,UAAU,CAAC;KAC9B,SAAS,OAAO;KAChB,YAAY,OAAO;KACnB,WAAW,eAAe,MAAM,KAAK,KAAA;IACtC,CAAA;GAEW,CAAA;EACL,GAfN,IAAI,GAeE,CACd,CACG;;AAEV;;AAGA,SAAS,SAAS,EAAE,OAA2B;CAC7C,MAAM,EAAE,OAAO,YAAY,iBAAiB,iBAAiB,GAAG;CAEhE,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM,OAAO;CAElD,OACE,oBAAC,OAAD;EACE,SAAQ;EACR,OAAO,gBAAgB,KAAA;EACvB,WAAW,GACT,oBACA,WAAW,mBAAmB,cAAc,KAAA,GAAW,MAAM,CAC/D;EAEC,UAAA;CACI,CAAA;AAEX"}
|
|
@@ -96,6 +96,7 @@ function GlobalSearch({ variant = "topbar" }) {
|
|
|
96
96
|
children: suffix.text
|
|
97
97
|
}),
|
|
98
98
|
hydrated && suffix?.type === "keyBinding" && /* @__PURE__ */ jsx("kbd", {
|
|
99
|
+
dir: "ltr",
|
|
99
100
|
className: "bg-muted hidden rounded px-1 font-mono text-[10px] sm:inline",
|
|
100
101
|
children: formatKeyBinding(suffix.keyBinding)
|
|
101
102
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"global-search.js","names":[],"sources":["../../../src/components/panel/global-search.tsx"],"sourcesContent":["import { formatKeyBinding, globalSearchSuffix, SHORTCUT_IDS, shortcuts } from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { SearchIcon } from 'lucide-react'\nimport { useEffect, useState, type ComponentType } from 'react'\nimport { usePanel, useT } from '../../context'\nimport { useHydrated } from '../../hooks/use-hydrated'\nimport { Icon } from '../../lib/icons'\nimport { Button } from '../ui/button'\nimport { Slot } from './slot'\n\ntype PaletteComponent = ComponentType<{ open: boolean; onOpenChange: (open: boolean) => void }>\n\nlet loadedPalette: PaletteComponent | undefined\nlet loadingPalette: Promise<PaletteComponent> | undefined\n\n/** The palette's code: loaded the first time it opens (or ahead, on hover and focus). */\nfunction loadPalette(): Promise<PaletteComponent> {\n loadingPalette ??= import('./global-search-palette').then(\n (module) => (loadedPalette = module.GlobalSearchPalette),\n (error: unknown) => {\n loadingPalette = undefined\n throw error\n },\n )\n\n return loadingPalette\n}\n\nconst preloadPalette = () => void loadPalette().catch(() => undefined)\n\n/**\n * The search field and its command palette (`GlobalSearchPalette`, loaded on first open):\n * global search results plus (with `alfakit.commandPalette`) the page's header actions,\n * panel commands, navigation, \"create\" links, recent records, theme switching and the\n * shortcuts overlay. `variant=\"sidebar\"` is the full-width field in the sidebar header\n * (`globalSearch(position: GlobalSearchPosition::Sidebar)`).\n */\nexport function GlobalSearch({ variant = 'topbar' }: { variant?: 'topbar' | 'sidebar' }) {\n const t = useT()\n const panel = usePanel()\n const hydrated = useHydrated()\n const [open, setOpen] = useState(false)\n // Undefined on the server and during hydration (nothing loads there), so the markup matches.\n const [Palette, setPalette] = useState<PaletteComponent | undefined>(() => loadedPalette)\n const config = panel.globalSearch\n const palette = panel.commandPalette\n const endpoint = config ? panel.endpoints.globalSearch : undefined\n const placeholder =\n config?.placeholder ??\n palette?.placeholder ??\n t('palette.placeholder', {}, 'Type a command or search…')\n // Without global search the palette still opens with ⌘K / Ctrl+K.\n const suffix = globalSearchSuffix(\n config ?? (palette ? { keyBindings: ['mod+k'], placeholder } : undefined),\n )\n const keys = JSON.stringify(config?.keyBindings ?? ['mod+k'])\n const label = t('shortcuts.open_palette', {}, 'Open the command palette')\n\n // The palette's key bindings (`globalSearchKeyBindings()`): essential, so they keep working\n // with `keyboardShortcuts(false)`; users may change them in the shortcuts overlay.\n useEffect(() => {\n if (!config && !palette) return\n\n return shortcuts.register({\n id: SHORTCUT_IDS.palette,\n keys: JSON.parse(keys) as string[],\n label,\n group: 'general',\n essential: true,\n handler: () => setOpen(true),\n })\n }, [config, palette, keys, label])\n\n // Opening before the code has arrived (a click or `mod+k` right after the page loads)\n // keeps `open` set; the palette mounts open once it has loaded.\n useEffect(() => {\n if (!open || Palette) return\n\n let active = true\n\n loadPalette().then(\n (component) => active && setPalette(() => component),\n () => active && setOpen(false),\n )\n\n return () => {\n active = false\n }\n }, [open, Palette])\n\n // A palette opened while its code was still loading closes when the page changes.\n useEffect(() => router.on('navigate', () => setOpen(false)), [])\n\n if ((!config || !endpoint) && !palette) {\n return null\n }\n\n return (\n <>\n <Slot name=\"global-search.before\" />\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n data-variant={variant}\n className={\n variant === 'sidebar'\n ? 'text-muted-foreground w-full justify-start gap-2 px-2.5 font-normal group-data-[collapsible=icon]:size-8 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0'\n : 'text-muted-foreground size-8 justify-center gap-2 px-0 font-normal sm:w-56 sm:justify-start sm:px-2.5'\n }\n onClick={() => setOpen(true)}\n onPointerEnter={preloadPalette}\n onFocus={preloadPalette}\n >\n <Icon name=\"alfakit::panels.global-search.field\" fallback={SearchIcon} className=\"size-4\" />\n <span\n className={\n variant === 'sidebar'\n ? 'flex-1 truncate text-start group-data-[collapsible=icon]:sr-only'\n : 'sr-only sm:not-sr-only sm:flex-1 sm:text-start'\n }\n >\n {placeholder}\n </span>\n {/* `globalSearchFieldSuffix()`, or the first key binding: it depends on the visitor's\n platform (⌘ or Ctrl), so it renders after hydration. */}\n {suffix?.type === 'text' && (\n <span\n data-slot=\"global-search-suffix\"\n className=\"text-muted-foreground hidden text-[10px] sm:inline\"\n >\n {suffix.text}\n </span>\n )}\n {hydrated && suffix?.type === 'keyBinding' && (\n <kbd className=\"bg-muted hidden rounded px-1 font-mono text-[10px] sm:inline\">\n {formatKeyBinding(suffix.keyBinding)}\n </kbd>\n )}\n </Button>\n <Slot name=\"global-search.after\" />\n {/* The dialog's code loads on first open (click or shortcut); hovering or focusing the\n field fetches it ahead. Not rendered on the server, like the closed dialog before it. */}\n {Palette && <Palette open={open} onOpenChange={setOpen} />}\n </>\n )\n}\n"],"mappings":";;;;;;;;;;;AAYA,IAAI;AACJ,IAAI;;AAGJ,SAAS,cAAyC;CAChD,mBAAmB,OAAO,6BAA0B,CAAC,MAClD,WAAY,gBAAgB,OAAO,sBACnC,UAAmB;EAClB,iBAAiB,KAAA;EACjB,MAAM;CACR,CACF;CAEA,OAAO;AACT;AAEA,MAAM,uBAAuB,KAAK,YAAY,CAAC,CAAC,YAAY,KAAA,CAAS;;;;;;;;AASrE,SAAgB,aAAa,EAAE,UAAU,YAAgD;CACvF,MAAM,IAAI,KAAK;CACf,MAAM,QAAQ,SAAS;CACvB,MAAM,WAAW,YAAY;CAC7B,MAAM,CAAC,MAAM,WAAW,SAAS,KAAK;CAEtC,MAAM,CAAC,SAAS,cAAc,eAA6C,aAAa;CACxF,MAAM,SAAS,MAAM;CACrB,MAAM,UAAU,MAAM;CACtB,MAAM,WAAW,SAAS,MAAM,UAAU,eAAe,KAAA;CACzD,MAAM,cACJ,QAAQ,eACR,SAAS,eACT,EAAE,uBAAuB,CAAC,GAAG,2BAA2B;CAE1D,MAAM,SAAS,mBACb,WAAW,UAAU;EAAE,aAAa,CAAC,OAAO;EAAG;CAAY,IAAI,KAAA,EACjE;CACA,MAAM,OAAO,KAAK,UAAU,QAAQ,eAAe,CAAC,OAAO,CAAC;CAC5D,MAAM,QAAQ,EAAE,0BAA0B,CAAC,GAAG,0BAA0B;CAIxE,gBAAgB;EACd,IAAI,CAAC,UAAU,CAAC,SAAS;EAEzB,OAAO,UAAU,SAAS;GACxB,IAAI,aAAa;GACjB,MAAM,KAAK,MAAM,IAAI;GACrB;GACA,OAAO;GACP,WAAW;GACX,eAAe,QAAQ,IAAI;EAC7B,CAAC;CACH,GAAG;EAAC;EAAQ;EAAS;EAAM;CAAK,CAAC;CAIjC,gBAAgB;EACd,IAAI,CAAC,QAAQ,SAAS;EAEtB,IAAI,SAAS;EAEb,YAAY,CAAC,CAAC,MACX,cAAc,UAAU,iBAAiB,SAAS,SAC7C,UAAU,QAAQ,KAAK,CAC/B;EAEA,aAAa;GACX,SAAS;EACX;CACF,GAAG,CAAC,MAAM,OAAO,CAAC;CAGlB,gBAAgB,OAAO,GAAG,kBAAkB,QAAQ,KAAK,CAAC,GAAG,CAAC,CAAC;CAE/D,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC,SAC7B,OAAO;CAGT,OACE,qBAAA,YAAA,EAAA,UAAA;EACE,oBAAC,MAAD,EAAM,MAAK,uBAAwB,CAAA;EACnC,qBAAC,QAAD;GACE,MAAK;GACL,SAAQ;GACR,MAAK;GACL,gBAAc;GACd,WACE,YAAY,YACR,6LACA;GAEN,eAAe,QAAQ,IAAI;GAC3B,gBAAgB;GAChB,SAAS;GAZX,UAAA;IAcE,oBAAC,MAAD;KAAM,MAAK;KAAsC,UAAU;KAAY,WAAU;IAAU,CAAA;IAC3F,oBAAC,QAAD;KACE,WACE,YAAY,YACR,qEACA;KAGL,UAAA;IACG,CAAA;IAGL,QAAQ,SAAS,UAChB,oBAAC,QAAD;KACE,aAAU;KACV,WAAU;KAET,UAAA,OAAO;IACJ,CAAA;IAEP,YAAY,QAAQ,SAAS,gBAC5B,oBAAC,OAAD;KAAK,WAAU;
|
|
1
|
+
{"version":3,"file":"global-search.js","names":[],"sources":["../../../src/components/panel/global-search.tsx"],"sourcesContent":["import { formatKeyBinding, globalSearchSuffix, SHORTCUT_IDS, shortcuts } from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { SearchIcon } from 'lucide-react'\nimport { useEffect, useState, type ComponentType } from 'react'\nimport { usePanel, useT } from '../../context'\nimport { useHydrated } from '../../hooks/use-hydrated'\nimport { Icon } from '../../lib/icons'\nimport { Button } from '../ui/button'\nimport { Slot } from './slot'\n\ntype PaletteComponent = ComponentType<{ open: boolean; onOpenChange: (open: boolean) => void }>\n\nlet loadedPalette: PaletteComponent | undefined\nlet loadingPalette: Promise<PaletteComponent> | undefined\n\n/** The palette's code: loaded the first time it opens (or ahead, on hover and focus). */\nfunction loadPalette(): Promise<PaletteComponent> {\n loadingPalette ??= import('./global-search-palette').then(\n (module) => (loadedPalette = module.GlobalSearchPalette),\n (error: unknown) => {\n loadingPalette = undefined\n throw error\n },\n )\n\n return loadingPalette\n}\n\nconst preloadPalette = () => void loadPalette().catch(() => undefined)\n\n/**\n * The search field and its command palette (`GlobalSearchPalette`, loaded on first open):\n * global search results plus (with `alfakit.commandPalette`) the page's header actions,\n * panel commands, navigation, \"create\" links, recent records, theme switching and the\n * shortcuts overlay. `variant=\"sidebar\"` is the full-width field in the sidebar header\n * (`globalSearch(position: GlobalSearchPosition::Sidebar)`).\n */\nexport function GlobalSearch({ variant = 'topbar' }: { variant?: 'topbar' | 'sidebar' }) {\n const t = useT()\n const panel = usePanel()\n const hydrated = useHydrated()\n const [open, setOpen] = useState(false)\n // Undefined on the server and during hydration (nothing loads there), so the markup matches.\n const [Palette, setPalette] = useState<PaletteComponent | undefined>(() => loadedPalette)\n const config = panel.globalSearch\n const palette = panel.commandPalette\n const endpoint = config ? panel.endpoints.globalSearch : undefined\n const placeholder =\n config?.placeholder ??\n palette?.placeholder ??\n t('palette.placeholder', {}, 'Type a command or search…')\n // Without global search the palette still opens with ⌘K / Ctrl+K.\n const suffix = globalSearchSuffix(\n config ?? (palette ? { keyBindings: ['mod+k'], placeholder } : undefined),\n )\n const keys = JSON.stringify(config?.keyBindings ?? ['mod+k'])\n const label = t('shortcuts.open_palette', {}, 'Open the command palette')\n\n // The palette's key bindings (`globalSearchKeyBindings()`): essential, so they keep working\n // with `keyboardShortcuts(false)`; users may change them in the shortcuts overlay.\n useEffect(() => {\n if (!config && !palette) return\n\n return shortcuts.register({\n id: SHORTCUT_IDS.palette,\n keys: JSON.parse(keys) as string[],\n label,\n group: 'general',\n essential: true,\n handler: () => setOpen(true),\n })\n }, [config, palette, keys, label])\n\n // Opening before the code has arrived (a click or `mod+k` right after the page loads)\n // keeps `open` set; the palette mounts open once it has loaded.\n useEffect(() => {\n if (!open || Palette) return\n\n let active = true\n\n loadPalette().then(\n (component) => active && setPalette(() => component),\n () => active && setOpen(false),\n )\n\n return () => {\n active = false\n }\n }, [open, Palette])\n\n // A palette opened while its code was still loading closes when the page changes.\n useEffect(() => router.on('navigate', () => setOpen(false)), [])\n\n if ((!config || !endpoint) && !palette) {\n return null\n }\n\n return (\n <>\n <Slot name=\"global-search.before\" />\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n data-variant={variant}\n className={\n variant === 'sidebar'\n ? 'text-muted-foreground w-full justify-start gap-2 px-2.5 font-normal group-data-[collapsible=icon]:size-8 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0'\n : 'text-muted-foreground size-8 justify-center gap-2 px-0 font-normal sm:w-56 sm:justify-start sm:px-2.5'\n }\n onClick={() => setOpen(true)}\n onPointerEnter={preloadPalette}\n onFocus={preloadPalette}\n >\n <Icon name=\"alfakit::panels.global-search.field\" fallback={SearchIcon} className=\"size-4\" />\n <span\n className={\n variant === 'sidebar'\n ? 'flex-1 truncate text-start group-data-[collapsible=icon]:sr-only'\n : 'sr-only sm:not-sr-only sm:flex-1 sm:text-start'\n }\n >\n {placeholder}\n </span>\n {/* `globalSearchFieldSuffix()`, or the first key binding: it depends on the visitor's\n platform (⌘ or Ctrl), so it renders after hydration. */}\n {suffix?.type === 'text' && (\n <span\n data-slot=\"global-search-suffix\"\n className=\"text-muted-foreground hidden text-[10px] sm:inline\"\n >\n {suffix.text}\n </span>\n )}\n {hydrated && suffix?.type === 'keyBinding' && (\n <kbd dir=\"ltr\" className=\"bg-muted hidden rounded px-1 font-mono text-[10px] sm:inline\">\n {formatKeyBinding(suffix.keyBinding)}\n </kbd>\n )}\n </Button>\n <Slot name=\"global-search.after\" />\n {/* The dialog's code loads on first open (click or shortcut); hovering or focusing the\n field fetches it ahead. Not rendered on the server, like the closed dialog before it. */}\n {Palette && <Palette open={open} onOpenChange={setOpen} />}\n </>\n )\n}\n"],"mappings":";;;;;;;;;;;AAYA,IAAI;AACJ,IAAI;;AAGJ,SAAS,cAAyC;CAChD,mBAAmB,OAAO,6BAA0B,CAAC,MAClD,WAAY,gBAAgB,OAAO,sBACnC,UAAmB;EAClB,iBAAiB,KAAA;EACjB,MAAM;CACR,CACF;CAEA,OAAO;AACT;AAEA,MAAM,uBAAuB,KAAK,YAAY,CAAC,CAAC,YAAY,KAAA,CAAS;;;;;;;;AASrE,SAAgB,aAAa,EAAE,UAAU,YAAgD;CACvF,MAAM,IAAI,KAAK;CACf,MAAM,QAAQ,SAAS;CACvB,MAAM,WAAW,YAAY;CAC7B,MAAM,CAAC,MAAM,WAAW,SAAS,KAAK;CAEtC,MAAM,CAAC,SAAS,cAAc,eAA6C,aAAa;CACxF,MAAM,SAAS,MAAM;CACrB,MAAM,UAAU,MAAM;CACtB,MAAM,WAAW,SAAS,MAAM,UAAU,eAAe,KAAA;CACzD,MAAM,cACJ,QAAQ,eACR,SAAS,eACT,EAAE,uBAAuB,CAAC,GAAG,2BAA2B;CAE1D,MAAM,SAAS,mBACb,WAAW,UAAU;EAAE,aAAa,CAAC,OAAO;EAAG;CAAY,IAAI,KAAA,EACjE;CACA,MAAM,OAAO,KAAK,UAAU,QAAQ,eAAe,CAAC,OAAO,CAAC;CAC5D,MAAM,QAAQ,EAAE,0BAA0B,CAAC,GAAG,0BAA0B;CAIxE,gBAAgB;EACd,IAAI,CAAC,UAAU,CAAC,SAAS;EAEzB,OAAO,UAAU,SAAS;GACxB,IAAI,aAAa;GACjB,MAAM,KAAK,MAAM,IAAI;GACrB;GACA,OAAO;GACP,WAAW;GACX,eAAe,QAAQ,IAAI;EAC7B,CAAC;CACH,GAAG;EAAC;EAAQ;EAAS;EAAM;CAAK,CAAC;CAIjC,gBAAgB;EACd,IAAI,CAAC,QAAQ,SAAS;EAEtB,IAAI,SAAS;EAEb,YAAY,CAAC,CAAC,MACX,cAAc,UAAU,iBAAiB,SAAS,SAC7C,UAAU,QAAQ,KAAK,CAC/B;EAEA,aAAa;GACX,SAAS;EACX;CACF,GAAG,CAAC,MAAM,OAAO,CAAC;CAGlB,gBAAgB,OAAO,GAAG,kBAAkB,QAAQ,KAAK,CAAC,GAAG,CAAC,CAAC;CAE/D,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC,SAC7B,OAAO;CAGT,OACE,qBAAA,YAAA,EAAA,UAAA;EACE,oBAAC,MAAD,EAAM,MAAK,uBAAwB,CAAA;EACnC,qBAAC,QAAD;GACE,MAAK;GACL,SAAQ;GACR,MAAK;GACL,gBAAc;GACd,WACE,YAAY,YACR,6LACA;GAEN,eAAe,QAAQ,IAAI;GAC3B,gBAAgB;GAChB,SAAS;GAZX,UAAA;IAcE,oBAAC,MAAD;KAAM,MAAK;KAAsC,UAAU;KAAY,WAAU;IAAU,CAAA;IAC3F,oBAAC,QAAD;KACE,WACE,YAAY,YACR,qEACA;KAGL,UAAA;IACG,CAAA;IAGL,QAAQ,SAAS,UAChB,oBAAC,QAAD;KACE,aAAU;KACV,WAAU;KAET,UAAA,OAAO;IACJ,CAAA;IAEP,YAAY,QAAQ,SAAS,gBAC5B,oBAAC,OAAD;KAAK,KAAI;KAAM,WAAU;KACtB,UAAA,iBAAiB,OAAO,UAAU;IAChC,CAAA;GAED;;EACR,oBAAC,MAAD,EAAM,MAAK,sBAAuB,CAAA;EAGjC,WAAW,oBAAC,SAAD;GAAe;GAAM,cAAc;EAAU,CAAA;CACzD,EAAA,CAAA;AAEN"}
|
|
@@ -21,6 +21,7 @@ function ShortcutKeys({ binding, className }) {
|
|
|
21
21
|
className: "text-muted-foreground text-[10px]",
|
|
22
22
|
children: t("shortcuts.then", {}, "then")
|
|
23
23
|
}), /* @__PURE__ */ jsx("kbd", {
|
|
24
|
+
dir: "ltr",
|
|
24
25
|
className: "bg-muted text-muted-foreground rounded border px-1.5 font-mono text-[10px] leading-5",
|
|
25
26
|
children: step
|
|
26
27
|
})]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shortcut-keys.js","names":[],"sources":["../../../src/components/panel/shortcut-keys.tsx"],"sourcesContent":["import { shortcutSteps } from '@alfakit/core'\nimport { useT } from '../../context'\nimport { useHydrated } from '../../hooks/use-hydrated'\nimport { cn } from '../../lib/utils'\n\n/**\n * A key binding as `<kbd>` keys (`G` then `D`, `⌘K`). The keys depend on the visitor's\n * platform (⌘ or Ctrl), so they render after hydration.\n */\nexport function ShortcutKeys({ binding, className }: { binding: string; className?: string }) {\n const t = useT()\n const hydrated = useHydrated()\n\n if (!hydrated) return null\n\n const steps = shortcutSteps(binding)\n\n return (\n <span data-slot=\"shortcut-keys\" className={cn('inline-flex items-center gap-1', className)}>\n {steps.map((step, index) => (\n <span key={index} className=\"inline-flex items-center gap-1\">\n {index > 0 && (\n <span className=\"text-muted-foreground text-[10px]\">\n {t('shortcuts.then', {}, 'then')}\n </span>\n )}\n <kbd
|
|
1
|
+
{"version":3,"file":"shortcut-keys.js","names":[],"sources":["../../../src/components/panel/shortcut-keys.tsx"],"sourcesContent":["import { shortcutSteps } from '@alfakit/core'\nimport { useT } from '../../context'\nimport { useHydrated } from '../../hooks/use-hydrated'\nimport { cn } from '../../lib/utils'\n\n/**\n * A key binding as `<kbd>` keys (`G` then `D`, `⌘K`). The keys depend on the visitor's\n * platform (⌘ or Ctrl), so they render after hydration.\n */\nexport function ShortcutKeys({ binding, className }: { binding: string; className?: string }) {\n const t = useT()\n const hydrated = useHydrated()\n\n if (!hydrated) return null\n\n const steps = shortcutSteps(binding)\n\n return (\n <span data-slot=\"shortcut-keys\" className={cn('inline-flex items-center gap-1', className)}>\n {steps.map((step, index) => (\n <span key={index} className=\"inline-flex items-center gap-1\">\n {index > 0 && (\n <span className=\"text-muted-foreground text-[10px]\">\n {t('shortcuts.then', {}, 'then')}\n </span>\n )}\n <kbd\n dir=\"ltr\"\n className=\"bg-muted text-muted-foreground rounded border px-1.5 font-mono text-[10px] leading-5\"\n >\n {step}\n </kbd>\n </span>\n ))}\n </span>\n )\n}\n"],"mappings":";;;;;;;;;;AASA,SAAgB,aAAa,EAAE,SAAS,aAAsD;CAC5F,MAAM,IAAI,KAAK;CAGf,IAAI,CAFa,YAEL,GAAG,OAAO;CAEtB,MAAM,QAAQ,cAAc,OAAO;CAEnC,OACE,oBAAC,QAAD;EAAM,aAAU;EAAgB,WAAW,GAAG,kCAAkC,SAAS;EACtF,UAAA,MAAM,KAAK,MAAM,UAChB,qBAAC,QAAD;GAAkB,WAAU;GAA5B,UAAA,CACG,QAAQ,KACP,oBAAC,QAAD;IAAM,WAAU;IACb,UAAA,EAAE,kBAAkB,CAAC,GAAG,MAAM;GAC3B,CAAA,GAER,oBAAC,OAAD;IACE,KAAI;IACJ,WAAU;IAET,UAAA;GACE,CAAA,CACD;EAZK,GAAA,KAYL,CACP;CACG,CAAA;AAEV"}
|
|
@@ -161,7 +161,7 @@ function TenantMenu({ variant = "sidebar" }) {
|
|
|
161
161
|
fallback: SettingsIcon
|
|
162
162
|
}),
|
|
163
163
|
" ",
|
|
164
|
-
t("tenancy.profile", {}, "Settings")
|
|
164
|
+
tenancy.profileLabel ?? t("tenancy.profile", {}, "Settings")
|
|
165
165
|
]
|
|
166
166
|
})
|
|
167
167
|
}),
|
|
@@ -183,7 +183,7 @@ function TenantMenu({ variant = "sidebar" }) {
|
|
|
183
183
|
fallback: PlusIcon
|
|
184
184
|
}),
|
|
185
185
|
" ",
|
|
186
|
-
t("tenancy.register", {}, "New")
|
|
186
|
+
tenancy.registrationLabel ?? t("tenancy.register", {}, "New")
|
|
187
187
|
]
|
|
188
188
|
})
|
|
189
189
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant-menu.js","names":[],"sources":["../../../src/components/panel/tenant-menu.tsx"],"sourcesContent":["import { filterTenants, initials, tenantNameText } from '@alfakit/core'\nimport { Link, router } from '@inertiajs/react'\nimport { CheckIcon, ChevronsUpDownIcon, PlusIcon, SearchIcon, SettingsIcon } from 'lucide-react'\nimport { useState } from 'react'\nimport { usePanel, useT } from '../../context'\nimport { Icon } from '../../lib/icons'\nimport { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../ui/dropdown-menu'\nimport { SidebarMenuButton } from '../ui/sidebar'\nimport { MenuItemEntries, MenuItemEntry } from './menu-item'\nimport { TenantName } from './nav-label'\n\n/**\n * The tenant switcher: a sidebar header button, or a compact button in the top bar of the\n * `topbar` layout (`variant=\"topbar\"`), where the store name shows from `lg` up.\n */\nexport function TenantMenu({ variant = 'sidebar' }: { variant?: 'sidebar' | 'topbar' }) {\n const t = useT()\n const { tenancy, brandName } = usePanel()\n const [search, setSearch] = useState('')\n\n if (!tenancy || !tenancy.current) {\n return null\n }\n\n const current = tenancy.current\n // `tenantSwitcher(false)` sends no tenants; `searchableTenantMenu()` adds a search field.\n const tenants = tenancy.searchable ? filterTenants(tenancy.tenants, search) : tenancy.tenants\n const hasSwitcher = tenancy.switcher !== false && tenancy.tenants.length > 0\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n {variant === 'topbar' ? (\n <button\n type=\"button\"\n data-slot=\"tenant-menu\"\n className=\"hover:bg-accent data-[state=open]:bg-accent inline-flex h-9 max-w-60 shrink-0 items-center gap-2 rounded-md px-2 text-sm font-medium\"\n >\n <Avatar className=\"size-6 rounded-md\">\n {current.avatar && <AvatarImage src={current.avatar} alt=\"\" />}\n <AvatarFallback className=\"rounded-md text-[10px]\">\n {initials(tenantNameText(current))}\n </AvatarFallback>\n </Avatar>\n <span className=\"sr-only min-w-0 truncate lg:not-sr-only lg:grid lg:text-start lg:leading-tight\">\n {current.label && (\n <span data-slot=\"tenant-menu-label\" className=\"text-muted-foreground text-[10px]\">\n {current.label}{' '}\n </span>\n )}\n <TenantName tenant={current} className=\"truncate\" />\n </span>\n <Icon\n name=\"alfakit::panels.tenant-menu.toggle-button\"\n fallback={ChevronsUpDownIcon}\n className=\"text-muted-foreground size-4 shrink-0\"\n />\n </button>\n ) : (\n <SidebarMenuButton size=\"lg\" className=\"data-[state=open]:bg-sidebar-accent\">\n <Avatar className=\"size-8 rounded-lg\">\n {current.avatar && <AvatarImage src={current.avatar} alt={tenantNameText(current)} />}\n <AvatarFallback className=\"rounded-lg\">\n {initials(tenantNameText(current))}\n </AvatarFallback>\n </Avatar>\n <div className=\"grid flex-1 text-start text-sm leading-tight\">\n {/* `HasCurrentTenantLabel`: a caption above the name instead of the brand. */}\n {current.label && (\n <span\n data-slot=\"tenant-menu-label\"\n className=\"text-muted-foreground truncate text-xs\"\n >\n {current.label}\n </span>\n )}\n <TenantName tenant={current} className=\"truncate font-medium\" />\n {!current.label && (\n <span className=\"text-muted-foreground truncate text-xs\">{brandName}</span>\n )}\n </div>\n <Icon\n name=\"alfakit::panels.tenant-menu.toggle-button\"\n fallback={ChevronsUpDownIcon}\n className=\"ms-auto size-4\"\n />\n </SidebarMenuButton>\n )}\n </DropdownMenuTrigger>\n <DropdownMenuContent\n className=\"min-w-56 rounded-lg\"\n side=\"bottom\"\n align=\"start\"\n sideOffset={4}\n >\n {hasSwitcher && (\n <DropdownMenuLabel className=\"text-muted-foreground text-xs\">\n {t('tenancy.switch', {}, 'Switch')}\n </DropdownMenuLabel>\n )}\n {hasSwitcher && tenancy.searchable && (\n <div data-slot=\"tenant-menu-search\" className=\"relative px-1 pb-1\">\n <SearchIcon\n className=\"text-muted-foreground pointer-events-none absolute start-3 top-1/2 size-3.5 -translate-y-1/2\"\n aria-hidden=\"true\"\n />\n <input\n type=\"search\"\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n // Keep the menu's type-ahead from taking the keys.\n onKeyDown={(event) => event.stopPropagation()}\n placeholder={t('tenancy.search', {}, 'Search')}\n aria-label={t('tenancy.search', {}, 'Search')}\n className=\"border-input focus-visible:ring-ring/50 h-8 w-full rounded-md border bg-transparent ps-7 pe-2 text-sm outline-none focus-visible:ring-[3px]\"\n />\n </div>\n )}\n {hasSwitcher && tenancy.searchable && tenants.length === 0 && (\n <p className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n {t('no_results', {}, 'No results.')}\n </p>\n )}\n {tenants.map((tenant) => (\n <DropdownMenuItem key={String(tenant.id)} onSelect={() => router.visit(tenant.url)}>\n <Avatar className=\"size-6 rounded-sm\">\n {tenant.avatar && <AvatarImage src={tenant.avatar} alt={tenantNameText(tenant)} />}\n <AvatarFallback className=\"rounded-sm text-[10px]\">\n {initials(tenantNameText(tenant))}\n </AvatarFallback>\n </Avatar>\n <TenantName tenant={tenant} className=\"flex-1 truncate\" />\n {String(tenant.id) === String(current.id) && <CheckIcon className=\"size-4\" />}\n </DropdownMenuItem>\n ))}\n {hasSwitcher && (tenancy.registrationUrl || tenancy.profileUrl || tenancy.menu?.length) && (\n <DropdownMenuSeparator />\n )}\n {tenancy.profile ? (\n <MenuItemEntry item={tenancy.profile} menu=\"tenantMenu\" />\n ) : (\n tenancy.profileUrl && (\n <DropdownMenuItem asChild>\n <Link href={tenancy.profileUrl} className=\"w-full\">\n <Icon name=\"alfakit::panels.tenant-menu.profile-button\" fallback={SettingsIcon} />{' '}\n {t('tenancy.profile', {}, 'Settings')}\n </Link>\n </DropdownMenuItem>\n )\n )}\n {tenancy.menu && <MenuItemEntries items={tenancy.menu} menu=\"tenantMenu\" />}\n {tenancy.registration ? (\n <MenuItemEntry item={tenancy.registration} menu=\"tenantMenu\" />\n ) : (\n tenancy.registrationUrl && (\n <DropdownMenuItem asChild>\n <Link href={tenancy.registrationUrl} className=\"w-full\">\n <Icon name=\"alfakit::panels.tenant-menu.registration-button\" fallback={PlusIcon} />{' '}\n {t('tenancy.register', {}, 'New')}\n </Link>\n </DropdownMenuItem>\n )\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tenant-menu.js","names":[],"sources":["../../../src/components/panel/tenant-menu.tsx"],"sourcesContent":["import { filterTenants, initials, tenantNameText } from '@alfakit/core'\nimport { Link, router } from '@inertiajs/react'\nimport { CheckIcon, ChevronsUpDownIcon, PlusIcon, SearchIcon, SettingsIcon } from 'lucide-react'\nimport { useState } from 'react'\nimport { usePanel, useT } from '../../context'\nimport { Icon } from '../../lib/icons'\nimport { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../ui/dropdown-menu'\nimport { SidebarMenuButton } from '../ui/sidebar'\nimport { MenuItemEntries, MenuItemEntry } from './menu-item'\nimport { TenantName } from './nav-label'\n\n/**\n * The tenant switcher: a sidebar header button, or a compact button in the top bar of the\n * `topbar` layout (`variant=\"topbar\"`), where the store name shows from `lg` up.\n */\nexport function TenantMenu({ variant = 'sidebar' }: { variant?: 'sidebar' | 'topbar' }) {\n const t = useT()\n const { tenancy, brandName } = usePanel()\n const [search, setSearch] = useState('')\n\n if (!tenancy || !tenancy.current) {\n return null\n }\n\n const current = tenancy.current\n // `tenantSwitcher(false)` sends no tenants; `searchableTenantMenu()` adds a search field.\n const tenants = tenancy.searchable ? filterTenants(tenancy.tenants, search) : tenancy.tenants\n const hasSwitcher = tenancy.switcher !== false && tenancy.tenants.length > 0\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n {variant === 'topbar' ? (\n <button\n type=\"button\"\n data-slot=\"tenant-menu\"\n className=\"hover:bg-accent data-[state=open]:bg-accent inline-flex h-9 max-w-60 shrink-0 items-center gap-2 rounded-md px-2 text-sm font-medium\"\n >\n <Avatar className=\"size-6 rounded-md\">\n {current.avatar && <AvatarImage src={current.avatar} alt=\"\" />}\n <AvatarFallback className=\"rounded-md text-[10px]\">\n {initials(tenantNameText(current))}\n </AvatarFallback>\n </Avatar>\n <span className=\"sr-only min-w-0 truncate lg:not-sr-only lg:grid lg:text-start lg:leading-tight\">\n {current.label && (\n <span data-slot=\"tenant-menu-label\" className=\"text-muted-foreground text-[10px]\">\n {current.label}{' '}\n </span>\n )}\n <TenantName tenant={current} className=\"truncate\" />\n </span>\n <Icon\n name=\"alfakit::panels.tenant-menu.toggle-button\"\n fallback={ChevronsUpDownIcon}\n className=\"text-muted-foreground size-4 shrink-0\"\n />\n </button>\n ) : (\n <SidebarMenuButton size=\"lg\" className=\"data-[state=open]:bg-sidebar-accent\">\n <Avatar className=\"size-8 rounded-lg\">\n {current.avatar && <AvatarImage src={current.avatar} alt={tenantNameText(current)} />}\n <AvatarFallback className=\"rounded-lg\">\n {initials(tenantNameText(current))}\n </AvatarFallback>\n </Avatar>\n <div className=\"grid flex-1 text-start text-sm leading-tight\">\n {/* `HasCurrentTenantLabel`: a caption above the name instead of the brand. */}\n {current.label && (\n <span\n data-slot=\"tenant-menu-label\"\n className=\"text-muted-foreground truncate text-xs\"\n >\n {current.label}\n </span>\n )}\n <TenantName tenant={current} className=\"truncate font-medium\" />\n {!current.label && (\n <span className=\"text-muted-foreground truncate text-xs\">{brandName}</span>\n )}\n </div>\n <Icon\n name=\"alfakit::panels.tenant-menu.toggle-button\"\n fallback={ChevronsUpDownIcon}\n className=\"ms-auto size-4\"\n />\n </SidebarMenuButton>\n )}\n </DropdownMenuTrigger>\n <DropdownMenuContent\n className=\"min-w-56 rounded-lg\"\n side=\"bottom\"\n align=\"start\"\n sideOffset={4}\n >\n {hasSwitcher && (\n <DropdownMenuLabel className=\"text-muted-foreground text-xs\">\n {t('tenancy.switch', {}, 'Switch')}\n </DropdownMenuLabel>\n )}\n {hasSwitcher && tenancy.searchable && (\n <div data-slot=\"tenant-menu-search\" className=\"relative px-1 pb-1\">\n <SearchIcon\n className=\"text-muted-foreground pointer-events-none absolute start-3 top-1/2 size-3.5 -translate-y-1/2\"\n aria-hidden=\"true\"\n />\n <input\n type=\"search\"\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n // Keep the menu's type-ahead from taking the keys.\n onKeyDown={(event) => event.stopPropagation()}\n placeholder={t('tenancy.search', {}, 'Search')}\n aria-label={t('tenancy.search', {}, 'Search')}\n className=\"border-input focus-visible:ring-ring/50 h-8 w-full rounded-md border bg-transparent ps-7 pe-2 text-sm outline-none focus-visible:ring-[3px]\"\n />\n </div>\n )}\n {hasSwitcher && tenancy.searchable && tenants.length === 0 && (\n <p className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n {t('no_results', {}, 'No results.')}\n </p>\n )}\n {tenants.map((tenant) => (\n <DropdownMenuItem key={String(tenant.id)} onSelect={() => router.visit(tenant.url)}>\n <Avatar className=\"size-6 rounded-sm\">\n {tenant.avatar && <AvatarImage src={tenant.avatar} alt={tenantNameText(tenant)} />}\n <AvatarFallback className=\"rounded-sm text-[10px]\">\n {initials(tenantNameText(tenant))}\n </AvatarFallback>\n </Avatar>\n <TenantName tenant={tenant} className=\"flex-1 truncate\" />\n {String(tenant.id) === String(current.id) && <CheckIcon className=\"size-4\" />}\n </DropdownMenuItem>\n ))}\n {hasSwitcher && (tenancy.registrationUrl || tenancy.profileUrl || tenancy.menu?.length) && (\n <DropdownMenuSeparator />\n )}\n {tenancy.profile ? (\n <MenuItemEntry item={tenancy.profile} menu=\"tenantMenu\" />\n ) : (\n tenancy.profileUrl && (\n <DropdownMenuItem asChild>\n <Link href={tenancy.profileUrl} className=\"w-full\">\n <Icon name=\"alfakit::panels.tenant-menu.profile-button\" fallback={SettingsIcon} />{' '}\n {tenancy.profileLabel ?? t('tenancy.profile', {}, 'Settings')}\n </Link>\n </DropdownMenuItem>\n )\n )}\n {tenancy.menu && <MenuItemEntries items={tenancy.menu} menu=\"tenantMenu\" />}\n {tenancy.registration ? (\n <MenuItemEntry item={tenancy.registration} menu=\"tenantMenu\" />\n ) : (\n tenancy.registrationUrl && (\n <DropdownMenuItem asChild>\n <Link href={tenancy.registrationUrl} className=\"w-full\">\n <Icon name=\"alfakit::panels.tenant-menu.registration-button\" fallback={PlusIcon} />{' '}\n {tenancy.registrationLabel ?? t('tenancy.register', {}, 'New')}\n </Link>\n </DropdownMenuItem>\n )\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"],"mappings":"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"}
|
|
@@ -13,7 +13,7 @@ import { Switch } from "../ui/switch.js";
|
|
|
13
13
|
import { FormattedText } from "../entries/text-entry.js";
|
|
14
14
|
import { columnRegistryKey, filterSelectColumnOptions, isOptionGroup, optionText, remainingTextSizeClass, resolvePartialProps, searchTableColumnOptions, selectColumnEmptyMessage, stackedImageStyle, updateTableCell, usesDefaultImageStack } from "@alfakit/core";
|
|
15
15
|
import { router } from "@inertiajs/react";
|
|
16
|
-
import { createElement, useEffect, useRef, useState } from "react";
|
|
16
|
+
import { createElement, useEffect, useId, useRef, useState } from "react";
|
|
17
17
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
18
18
|
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react";
|
|
19
19
|
//#region src/components/table/cells.tsx
|
|
@@ -374,6 +374,7 @@ function NativeSelectCell(props) {
|
|
|
374
374
|
const schema = column;
|
|
375
375
|
const { update, saving, error } = useCellUpdate(props);
|
|
376
376
|
const [value, setValue] = useState(cell.state === null || cell.state === void 0 ? "" : String(cell.state));
|
|
377
|
+
const errorId = useId();
|
|
377
378
|
return /* @__PURE__ */ jsxs("span", {
|
|
378
379
|
onClick: (event) => event.stopPropagation(),
|
|
379
380
|
children: [/* @__PURE__ */ jsxs("select", {
|
|
@@ -383,7 +384,9 @@ function NativeSelectCell(props) {
|
|
|
383
384
|
setValue(event.target.value);
|
|
384
385
|
update(event.target.value === "" ? null : event.target.value);
|
|
385
386
|
},
|
|
387
|
+
"aria-label": column.label || column.name,
|
|
386
388
|
"aria-invalid": error ? true : void 0,
|
|
389
|
+
"aria-describedby": error ? errorId : void 0,
|
|
387
390
|
className: cn("border-input dark:bg-input/30 focus-visible:ring-ring/50 h-8 min-w-32 rounded-md border bg-transparent px-2 text-sm shadow-xs outline-none focus-visible:ring-[3px]", error && "border-destructive"),
|
|
388
391
|
children: [schema.selectablePlaceholder && /* @__PURE__ */ jsx("option", {
|
|
389
392
|
value: "",
|
|
@@ -394,6 +397,7 @@ function NativeSelectCell(props) {
|
|
|
394
397
|
children: option.label
|
|
395
398
|
}, option.value))]
|
|
396
399
|
}), error && /* @__PURE__ */ jsx("span", {
|
|
400
|
+
id: errorId,
|
|
397
401
|
className: "text-destructive block text-xs",
|
|
398
402
|
children: error
|
|
399
403
|
})]
|
|
@@ -406,6 +410,7 @@ function TextInputCell(props) {
|
|
|
406
410
|
const [value, setValue] = useState(cell.state === null || cell.state === void 0 ? "" : String(cell.state));
|
|
407
411
|
const initial = cell.state === null || cell.state === void 0 ? "" : String(cell.state);
|
|
408
412
|
const grouped = Boolean(cell.prefix || cell.suffix);
|
|
413
|
+
const errorId = useId();
|
|
409
414
|
const field = /* @__PURE__ */ jsxs("span", {
|
|
410
415
|
className: cn("relative block", grouped && "min-w-0 flex-1"),
|
|
411
416
|
children: [
|
|
@@ -425,7 +430,9 @@ function TextInputCell(props) {
|
|
|
425
430
|
step: schema.step,
|
|
426
431
|
inputMode: schema.inputMode,
|
|
427
432
|
disabled: Boolean(cell.disabled) || saving,
|
|
433
|
+
"aria-label": column.label || column.name,
|
|
428
434
|
"aria-invalid": error ? true : void 0,
|
|
435
|
+
"aria-describedby": error ? errorId : void 0,
|
|
429
436
|
className: cn("h-8 min-w-32", schema.prefixIcon && "ps-8", schema.suffixIcon && "pe-8", grouped && "h-full rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent"),
|
|
430
437
|
onChange: (event) => setValue(event.target.value),
|
|
431
438
|
onBlur: () => value !== initial && void update(value),
|
|
@@ -461,6 +468,7 @@ function TextInputCell(props) {
|
|
|
461
468
|
})
|
|
462
469
|
]
|
|
463
470
|
}) : field, error && /* @__PURE__ */ jsx("span", {
|
|
471
|
+
id: errorId,
|
|
464
472
|
className: "text-destructive block text-xs",
|
|
465
473
|
children: error
|
|
466
474
|
})]
|