@open-mercato/ui 0.6.5-develop.5382.1.f542de69af → 0.6.5
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/.turbo/turbo-build.log +1 -1
- package/dist/backend/AppShell.js +15 -8
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/DataTable.js +10 -9
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/charts/KpiCard.js +22 -14
- package/dist/backend/charts/KpiCard.js.map +2 -2
- package/dist/backend/charts/Sparkline.js +75 -0
- package/dist/backend/charts/Sparkline.js.map +7 -0
- package/dist/backend/charts/index.js +4 -1
- package/dist/backend/charts/index.js.map +2 -2
- package/dist/backend/dashboard/DashboardScreen.js +1 -2
- package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
- package/dist/backend/injection/InjectionSpot.js +23 -6
- package/dist/backend/injection/InjectionSpot.js.map +2 -2
- package/dist/primitives/avatar.js +2 -2
- package/dist/primitives/avatar.js.map +2 -2
- package/dist/primitives/pagination.js +3 -3
- package/dist/primitives/pagination.js.map +2 -2
- package/package.json +5 -6
- package/src/backend/AppShell.tsx +18 -8
- package/src/backend/DataTable.tsx +16 -9
- package/src/backend/__tests__/AppShell.test.tsx +61 -1
- package/src/backend/__tests__/DataTable.stickyResponsive.test.tsx +99 -0
- package/src/backend/charts/KpiCard.tsx +22 -12
- package/src/backend/charts/Sparkline.tsx +87 -0
- package/src/backend/charts/index.ts +2 -1
- package/src/backend/dashboard/DashboardScreen.tsx +1 -2
- package/src/backend/injection/InjectionSpot.tsx +27 -7
- package/src/backend/injection/__tests__/InjectionSpot.remount.test.tsx +140 -0
- package/src/primitives/__tests__/alert.test.tsx +16 -0
- package/src/primitives/__tests__/avatar.test.tsx +36 -1
- package/src/primitives/__tests__/pagination.test.tsx +31 -0
- package/src/primitives/avatar.tsx +8 -2
- package/src/primitives/pagination.tsx +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/injection/InjectionSpot.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type {\n InjectionSpotId,\n InjectionWidgetModule,\n WidgetInjectionEventHandlers,\n WidgetBeforeDeleteResult,\n WidgetBeforeSaveResult,\n FieldChangeResult,\n NavigateGuardResult,\n} from '@open-mercato/shared/modules/widgets/injection'\nimport {\n getInjectionRegistryVersion,\n loadInjectionWidgetsForSpot,\n subscribeToInjectionRegistryChanges,\n type LoadedInjectionWidget,\n} from '@open-mercato/shared/modules/widgets/injection-loader'\nimport { getWidgetSharedState } from './WidgetSharedState'\n\nexport type InjectionSpotProps<TContext = unknown, TData = unknown> = {\n spotId: InjectionSpotId\n context: TContext\n data?: TData\n onDataChange?: (data: TData) => void\n disabled?: boolean\n onEvent?: (\n event: keyof WidgetInjectionEventHandlers<TContext, TData>,\n widgetId: string,\n ) => void\n widgetsOverride?: LoadedWidget[]\n}\n\n/**\n * Transformer events use pipeline dispatch: output of widget N becomes input of widget N+1.\n */\nconst TRANSFORMER_EVENTS = new Set<string>([\n 'transformFormData',\n 'transformDisplayData',\n 'transformValidation',\n])\n\ntype LoadedWidget = {\n widgetId: string\n module: InjectionWidgetModule<any, any>\n moduleId: string\n key: string\n placement?: LoadedInjectionWidget['placement']\n}\n\nexport type LoadedInjectionSpotWidget = LoadedWidget\n\nfunction injectSharedStateIntoContext<TContext>(context: TContext, moduleId: string): TContext {\n const sharedState = getWidgetSharedState(moduleId)\n if (typeof context === 'object' && context !== null && !Array.isArray(context)) {\n return {\n ...(context as Record<string, unknown>),\n sharedState,\n } as TContext\n }\n return {\n value: context,\n sharedState,\n } as TContext\n}\n\nexport function useInjectionWidgets<TContext = unknown>(\n spotId: InjectionSpotId | null | undefined,\n options?: {\n context?: TContext\n triggerOnLoad?: boolean\n onEvent?: (event: 'onLoad', widgetId: string) => void\n }\n) {\n const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion())\n const loadedRef = React.useRef(false)\n\n React.useEffect(() => {\n return subscribeToInjectionRegistryChanges(() => {\n setRegistryVersion(getInjectionRegistryVersion())\n })\n }, [])\n\n React.useEffect(() => {\n if (!spotId) {\n setWidgets([])\n setLoading(false)\n setError(null)\n return\n }\n let mounted = true\n const load = async () => {\n try {\n setLoading(true)\n setError(null)\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n const widgetList: LoadedWidget[] = loaded.map((w) => ({\n widgetId: w.metadata.id,\n module: w,\n moduleId: w.moduleId,\n key: w.key,\n placement: w.placement,\n }))\n setWidgets(widgetList)\n \n // Trigger onLoad for all widgets\n if (!loadedRef.current && options?.triggerOnLoad) {\n loadedRef.current = true\n for (const widget of widgetList) {\n if (widget.module.eventHandlers?.onLoad) {\n try {\n const widgetContext = injectSharedStateIntoContext(options.context as TContext, widget.moduleId)\n await widget.module.eventHandlers.onLoad(widgetContext)\n options.onEvent?.('onLoad', widget.widgetId)\n } catch (err) {\n console.error(`[InjectionSpot] Error in onLoad for widget ${widget.widgetId}:`, err)\n }\n }\n }\n }\n } catch (err) {\n if (!mounted) return\n console.error(`[InjectionSpot] Failed to load widgets for spot ${spotId}:`, err)\n setError(err instanceof Error ? err.message : String(err))\n } finally {\n if (mounted) setLoading(false)\n }\n }\n load()\n return () => {\n mounted = false\n }\n }, [spotId, options?.context, options?.triggerOnLoad, options?.onEvent, registryVersion])\n\n return { widgets, loading, error }\n}\n\nexport function InjectionSpot<TContext = unknown, TData = unknown>({\n spotId,\n context,\n data,\n onDataChange,\n disabled,\n onEvent,\n widgetsOverride,\n}: InjectionSpotProps<TContext, TData>) {\n const useSpotId = widgetsOverride ? null : spotId\n const { widgets, loading, error } = useInjectionWidgets<TContext>(useSpotId, {\n context,\n triggerOnLoad: !widgetsOverride,\n onEvent: onEvent ? (event, id) => onEvent(event, id) : undefined,\n })\n const effectiveWidgets = widgetsOverride ?? widgets\n const effectiveLoading = widgetsOverride ? false : loading\n const effectiveError = widgetsOverride ? null : error\n\n if (effectiveLoading) {\n return null\n }\n\n if (effectiveError) {\n console.error(`[InjectionSpot] Error loading widgets for spot ${spotId}:`, effectiveError)\n return null\n }\n\n if (effectiveWidgets.length === 0) {\n return null\n }\n\n return (\n <>\n {effectiveWidgets.map((widget) => {\n const { Widget } = widget.module\n return (\n <Widget\n key={widget.widgetId}\n context={injectSharedStateIntoContext(context, widget.moduleId)}\n data={data}\n onDataChange={onDataChange}\n disabled={disabled}\n />\n )\n })}\n </>\n )\n}\n\n/**\n * Hook to trigger injection widget events imperatively\n */\nexport function useInjectionSpotEvents<TContext = unknown, TData = unknown>(spotId: InjectionSpotId, prefetchedWidgets?: LoadedWidget[]) {\n const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])\n const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion())\n\n React.useEffect(() => {\n return subscribeToInjectionRegistryChanges(() => {\n setRegistryVersion(getInjectionRegistryVersion())\n })\n }, [])\n\n React.useEffect(() => {\n if (prefetchedWidgets && prefetchedWidgets.length) {\n setWidgets(prefetchedWidgets)\n return\n }\n let mounted = true\n const load = async () => {\n try {\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n setWidgets(\n loaded.map((w) => ({\n widgetId: w.metadata.id,\n module: w,\n moduleId: w.moduleId,\n key: w.key,\n placement: w.placement,\n }))\n )\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Failed to load widgets for spot ${spotId}:`, err)\n }\n }\n load()\n return () => {\n mounted = false\n }\n }, [spotId, prefetchedWidgets, registryVersion])\n\n const triggerEvent = React.useCallback(\n async (\n event: keyof WidgetInjectionEventHandlers<TContext, TData>,\n data: TData,\n context: TContext,\n meta?: {\n error?: unknown\n fieldId?: string\n fieldValue?: unknown\n originalData?: TData\n target?: unknown\n visible?: boolean\n appEvent?: unknown\n }\n ): Promise<{\n ok: boolean\n message?: string\n fieldErrors?: Record<string, string>\n requestHeaders?: Record<string, string>\n details?: unknown\n data?: TData\n applyToForm?: boolean\n fieldChange?: {\n value?: unknown\n sideEffects?: Record<string, unknown>\n messages?: Array<{ text: string; severity: 'info' | 'warning' | 'error' }>\n }\n }> => {\n const normalizeBeforeSave = (\n result: WidgetBeforeSaveResult,\n ): { ok: boolean; message?: string; fieldErrors?: Record<string, string>; requestHeaders?: Record<string, string>; details?: unknown } => {\n if (result === false) return { ok: false }\n if (result === true || typeof result === 'undefined') return { ok: true }\n if (result && typeof result === 'object') {\n const ok = typeof result.ok === 'boolean' ? result.ok : true\n const message = typeof result.message === 'string' ? result.message : undefined\n const fieldErrors =\n result.fieldErrors && typeof result.fieldErrors === 'object'\n ? Object.fromEntries(\n Object.entries(result.fieldErrors).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n const requestHeaders =\n result.requestHeaders && typeof result.requestHeaders === 'object'\n ? Object.fromEntries(\n Object.entries(result.requestHeaders).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n return { ok, message, fieldErrors, requestHeaders, details: result.details }\n }\n return { ok: true }\n }\n\n const normalizeBeforeDelete = (\n result: WidgetBeforeDeleteResult,\n ): { ok: boolean; message?: string; fieldErrors?: Record<string, string>; requestHeaders?: Record<string, string>; details?: unknown } => {\n if (result === false) return { ok: false }\n if (result === true || typeof result === 'undefined') return { ok: true }\n if (result && typeof result === 'object') {\n const ok = typeof result.ok === 'boolean' ? result.ok : true\n const message = typeof result.message === 'string' ? result.message : undefined\n const fieldErrors =\n result.fieldErrors && typeof result.fieldErrors === 'object'\n ? Object.fromEntries(\n Object.entries(result.fieldErrors).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n const requestHeaders =\n result.requestHeaders && typeof result.requestHeaders === 'object'\n ? Object.fromEntries(\n Object.entries(result.requestHeaders).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n return { ok, message, fieldErrors, requestHeaders, details: result.details }\n }\n return { ok: true }\n }\n\n // --- Transformer events: pipeline dispatch ---\n // Output of widget N becomes input of widget N+1\n if (TRANSFORMER_EVENTS.has(event)) {\n let pipelineData = data\n let applyToForm = false\n for (const widget of widgets) {\n const handler = widget.module.eventHandlers?.[event]\n if (!handler) continue\n try {\n const widgetContext = injectSharedStateIntoContext(context, widget.moduleId)\n let handlerResult: unknown\n if (event === 'transformValidation') {\n handlerResult = await (handler as any)(pipelineData, meta?.originalData ?? data, widgetContext)\n } else {\n handlerResult = await (handler as any)(pipelineData, widgetContext)\n }\n if (\n event === 'transformFormData' &&\n handlerResult !== null &&\n typeof handlerResult === 'object' &&\n 'applyToForm' in handlerResult &&\n (handlerResult as { applyToForm: unknown }).applyToForm === true &&\n 'data' in handlerResult\n ) {\n pipelineData = (handlerResult as { data: TData }).data\n applyToForm = true\n } else {\n pipelineData = handlerResult as TData\n }\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Error in ${event} for widget ${widget.widgetId}:`, err)\n }\n }\n return { ok: true, data: pipelineData, applyToForm }\n }\n\n // --- Action events: sequential dispatch ---\n const mergedRequestHeaders: Record<string, string> = {}\n let hasRequestHeaders = false\n let fieldValue = meta?.fieldValue\n let fieldSideEffects: Record<string, unknown> | undefined\n let fieldMessages: Array<{ text: string; severity: 'info' | 'warning' | 'error' }> | undefined\n\n for (const widget of widgets) {\n const eventHandlers = widget.module.eventHandlers\n // Check operation filter \u2014 skip widget if current operation is filtered out\n const operationFilter = eventHandlers?.filter?.operations\n if (operationFilter) {\n const currentOperation = (context as Record<string, unknown>)?.operation as string | undefined\n if (currentOperation && !operationFilter.includes(currentOperation as 'create' | 'update' | 'delete')) {\n continue\n }\n }\n let handler = eventHandlers?.[event]\n // Delete-to-save fallback chain\n if (!handler && event === 'onBeforeDelete') handler = eventHandlers?.onBeforeSave as typeof handler\n if (!handler && event === 'onDelete') handler = eventHandlers?.onSave as typeof handler\n if (!handler && event === 'onAfterDelete') handler = eventHandlers?.onAfterSave as typeof handler\n if (handler) {\n try {\n const widgetContext = injectSharedStateIntoContext(context, widget.moduleId)\n const result =\n event === 'onDeleteError'\n ? await (handler as any)(data, widgetContext, meta?.error)\n : event === 'onFieldChange'\n ? await (handler as any)(meta?.fieldId, fieldValue, data, widgetContext)\n : event === 'onBeforeNavigate'\n ? await (handler as any)(meta?.target, widgetContext)\n : event === 'onVisibilityChange'\n ? await (handler as any)(meta?.visible, widgetContext)\n : event === 'onAppEvent'\n ? await (handler as any)(meta?.appEvent, widgetContext)\n : await (handler as any)(data, widgetContext)\n if (event === 'onBeforeSave') {\n const normalized = normalizeBeforeSave(result as WidgetBeforeSaveResult)\n if (!normalized.ok) {\n console.log(`[useInjectionSpotEvents] Widget ${widget.widgetId} prevented ${event}`)\n return normalized\n }\n if (normalized.requestHeaders && Object.keys(normalized.requestHeaders).length > 0) {\n Object.assign(mergedRequestHeaders, normalized.requestHeaders)\n hasRequestHeaders = true\n }\n }\n if (event === 'onBeforeDelete') {\n const normalized = normalizeBeforeDelete(result as WidgetBeforeDeleteResult)\n if (!normalized.ok) {\n console.log(`[useInjectionSpotEvents] Widget ${widget.widgetId} prevented ${event}`)\n return normalized\n }\n if (normalized.requestHeaders && Object.keys(normalized.requestHeaders).length > 0) {\n Object.assign(mergedRequestHeaders, normalized.requestHeaders)\n hasRequestHeaders = true\n }\n }\n if (event === 'onBeforeNavigate') {\n const navResult = result as NavigateGuardResult | undefined\n if (navResult && navResult.ok === false) {\n return { ok: false, message: navResult.message }\n }\n }\n if (event === 'onFieldChange') {\n const changeResult = result as FieldChangeResult | void\n if (changeResult?.value !== undefined) {\n fieldValue = changeResult.value\n }\n if (changeResult?.sideEffects && typeof changeResult.sideEffects === 'object') {\n fieldSideEffects = { ...(fieldSideEffects ?? {}), ...changeResult.sideEffects }\n }\n if (changeResult?.message?.text) {\n fieldMessages = [...(fieldMessages ?? []), changeResult.message]\n }\n }\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Error in ${event} for widget ${widget.widgetId}:`, err)\n if (event === 'onBeforeSave' || event === 'onBeforeDelete' || event === 'onBeforeNavigate') {\n const message =\n err instanceof Error\n ? err.message || 'Validation blocked'\n : typeof err === 'string'\n ? err\n : undefined\n return { ok: false, message }\n }\n }\n }\n }\n if ((event === 'onBeforeSave' || event === 'onBeforeDelete') && hasRequestHeaders) {\n return { ok: true, requestHeaders: mergedRequestHeaders }\n }\n if (event === 'onFieldChange') {\n return {\n ok: true,\n fieldChange: {\n value: fieldValue,\n sideEffects: fieldSideEffects,\n messages: fieldMessages,\n },\n }\n }\n return { ok: true }\n },\n [widgets]\n )\n\n return { triggerEvent, widgets }\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type {\n InjectionSpotId,\n InjectionWidgetModule,\n WidgetInjectionEventHandlers,\n WidgetBeforeDeleteResult,\n WidgetBeforeSaveResult,\n FieldChangeResult,\n NavigateGuardResult,\n} from '@open-mercato/shared/modules/widgets/injection'\nimport {\n getInjectionRegistryVersion,\n loadInjectionWidgetsForSpot,\n subscribeToInjectionRegistryChanges,\n type LoadedInjectionWidget,\n} from '@open-mercato/shared/modules/widgets/injection-loader'\nimport { getWidgetSharedState } from './WidgetSharedState'\n\nexport type InjectionSpotProps<TContext = unknown, TData = unknown> = {\n spotId: InjectionSpotId\n context: TContext\n data?: TData\n onDataChange?: (data: TData) => void\n disabled?: boolean\n onEvent?: (\n event: keyof WidgetInjectionEventHandlers<TContext, TData>,\n widgetId: string,\n ) => void\n widgetsOverride?: LoadedWidget[]\n}\n\n/**\n * Transformer events use pipeline dispatch: output of widget N becomes input of widget N+1.\n */\nconst TRANSFORMER_EVENTS = new Set<string>([\n 'transformFormData',\n 'transformDisplayData',\n 'transformValidation',\n])\n\ntype LoadedWidget = {\n widgetId: string\n module: InjectionWidgetModule<any, any>\n moduleId: string\n key: string\n placement?: LoadedInjectionWidget['placement']\n}\n\nexport type LoadedInjectionSpotWidget = LoadedWidget\n\nfunction injectSharedStateIntoContext<TContext>(context: TContext, moduleId: string): TContext {\n const sharedState = getWidgetSharedState(moduleId)\n if (typeof context === 'object' && context !== null && !Array.isArray(context)) {\n return {\n ...(context as Record<string, unknown>),\n sharedState,\n } as TContext\n }\n return {\n value: context,\n sharedState,\n } as TContext\n}\n\nexport function useInjectionWidgets<TContext = unknown>(\n spotId: InjectionSpotId | null | undefined,\n options?: {\n context?: TContext\n triggerOnLoad?: boolean\n onEvent?: (event: 'onLoad', widgetId: string) => void\n }\n) {\n const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion())\n const loadedRef = React.useRef(false)\n const contextRef = React.useRef(options?.context)\n const triggerOnLoadRef = React.useRef(options?.triggerOnLoad)\n const onEventRef = React.useRef(options?.onEvent)\n\n React.useEffect(() => {\n contextRef.current = options?.context\n triggerOnLoadRef.current = options?.triggerOnLoad\n onEventRef.current = options?.onEvent\n })\n\n React.useEffect(() => {\n return subscribeToInjectionRegistryChanges(() => {\n setRegistryVersion(getInjectionRegistryVersion())\n })\n }, [])\n\n React.useEffect(() => {\n if (!spotId) {\n setWidgets([])\n setLoading(false)\n setError(null)\n return\n }\n let mounted = true\n const load = async () => {\n try {\n setLoading(true)\n setError(null)\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n const widgetList: LoadedWidget[] = loaded.map((w) => ({\n widgetId: w.metadata.id,\n module: w,\n moduleId: w.moduleId,\n key: w.key,\n placement: w.placement,\n }))\n setWidgets(widgetList)\n\n // Trigger onLoad for all widgets\n if (!loadedRef.current && triggerOnLoadRef.current) {\n loadedRef.current = true\n for (const widget of widgetList) {\n if (widget.module.eventHandlers?.onLoad) {\n try {\n const widgetContext = injectSharedStateIntoContext(contextRef.current as TContext, widget.moduleId)\n await widget.module.eventHandlers.onLoad(widgetContext)\n onEventRef.current?.('onLoad', widget.widgetId)\n } catch (err) {\n console.error(`[InjectionSpot] Error in onLoad for widget ${widget.widgetId}:`, err)\n }\n }\n }\n }\n } catch (err) {\n if (!mounted) return\n console.error(`[InjectionSpot] Failed to load widgets for spot ${spotId}:`, err)\n setError(err instanceof Error ? err.message : String(err))\n } finally {\n if (mounted) setLoading(false)\n }\n }\n load()\n return () => {\n mounted = false\n }\n // context/triggerOnLoad/onEvent are read from refs so only a real registry-version bump reloads the spot\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [spotId, registryVersion])\n\n return { widgets, loading, error }\n}\n\nexport function InjectionSpot<TContext = unknown, TData = unknown>({\n spotId,\n context,\n data,\n onDataChange,\n disabled,\n onEvent,\n widgetsOverride,\n}: InjectionSpotProps<TContext, TData>) {\n const useSpotId = widgetsOverride ? null : spotId\n const onEventRef = React.useRef(onEvent)\n React.useEffect(() => {\n onEventRef.current = onEvent\n })\n const hasOnEvent = Boolean(onEvent)\n const stableOnEvent = React.useMemo(\n () => (hasOnEvent ? (event: 'onLoad', id: string) => onEventRef.current?.(event, id) : undefined),\n [hasOnEvent],\n )\n const { widgets, loading, error } = useInjectionWidgets<TContext>(useSpotId, {\n context,\n triggerOnLoad: !widgetsOverride,\n onEvent: stableOnEvent,\n })\n const effectiveWidgets = widgetsOverride ?? widgets\n const effectiveLoading = widgetsOverride ? false : loading\n const effectiveError = widgetsOverride ? null : error\n\n if (effectiveLoading && effectiveWidgets.length === 0) {\n return null\n }\n\n if (effectiveError) {\n console.error(`[InjectionSpot] Error loading widgets for spot ${spotId}:`, effectiveError)\n return null\n }\n\n if (effectiveWidgets.length === 0) {\n return null\n }\n\n return (\n <>\n {effectiveWidgets.map((widget) => {\n const { Widget } = widget.module\n return (\n <Widget\n key={widget.widgetId}\n context={injectSharedStateIntoContext(context, widget.moduleId)}\n data={data}\n onDataChange={onDataChange}\n disabled={disabled}\n />\n )\n })}\n </>\n )\n}\n\n/**\n * Hook to trigger injection widget events imperatively\n */\nexport function useInjectionSpotEvents<TContext = unknown, TData = unknown>(spotId: InjectionSpotId, prefetchedWidgets?: LoadedWidget[]) {\n const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])\n const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion())\n\n React.useEffect(() => {\n return subscribeToInjectionRegistryChanges(() => {\n setRegistryVersion(getInjectionRegistryVersion())\n })\n }, [])\n\n React.useEffect(() => {\n if (prefetchedWidgets && prefetchedWidgets.length) {\n setWidgets(prefetchedWidgets)\n return\n }\n let mounted = true\n const load = async () => {\n try {\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n setWidgets(\n loaded.map((w) => ({\n widgetId: w.metadata.id,\n module: w,\n moduleId: w.moduleId,\n key: w.key,\n placement: w.placement,\n }))\n )\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Failed to load widgets for spot ${spotId}:`, err)\n }\n }\n load()\n return () => {\n mounted = false\n }\n }, [spotId, prefetchedWidgets, registryVersion])\n\n const triggerEvent = React.useCallback(\n async (\n event: keyof WidgetInjectionEventHandlers<TContext, TData>,\n data: TData,\n context: TContext,\n meta?: {\n error?: unknown\n fieldId?: string\n fieldValue?: unknown\n originalData?: TData\n target?: unknown\n visible?: boolean\n appEvent?: unknown\n }\n ): Promise<{\n ok: boolean\n message?: string\n fieldErrors?: Record<string, string>\n requestHeaders?: Record<string, string>\n details?: unknown\n data?: TData\n applyToForm?: boolean\n fieldChange?: {\n value?: unknown\n sideEffects?: Record<string, unknown>\n messages?: Array<{ text: string; severity: 'info' | 'warning' | 'error' }>\n }\n }> => {\n const normalizeBeforeSave = (\n result: WidgetBeforeSaveResult,\n ): { ok: boolean; message?: string; fieldErrors?: Record<string, string>; requestHeaders?: Record<string, string>; details?: unknown } => {\n if (result === false) return { ok: false }\n if (result === true || typeof result === 'undefined') return { ok: true }\n if (result && typeof result === 'object') {\n const ok = typeof result.ok === 'boolean' ? result.ok : true\n const message = typeof result.message === 'string' ? result.message : undefined\n const fieldErrors =\n result.fieldErrors && typeof result.fieldErrors === 'object'\n ? Object.fromEntries(\n Object.entries(result.fieldErrors).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n const requestHeaders =\n result.requestHeaders && typeof result.requestHeaders === 'object'\n ? Object.fromEntries(\n Object.entries(result.requestHeaders).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n return { ok, message, fieldErrors, requestHeaders, details: result.details }\n }\n return { ok: true }\n }\n\n const normalizeBeforeDelete = (\n result: WidgetBeforeDeleteResult,\n ): { ok: boolean; message?: string; fieldErrors?: Record<string, string>; requestHeaders?: Record<string, string>; details?: unknown } => {\n if (result === false) return { ok: false }\n if (result === true || typeof result === 'undefined') return { ok: true }\n if (result && typeof result === 'object') {\n const ok = typeof result.ok === 'boolean' ? result.ok : true\n const message = typeof result.message === 'string' ? result.message : undefined\n const fieldErrors =\n result.fieldErrors && typeof result.fieldErrors === 'object'\n ? Object.fromEntries(\n Object.entries(result.fieldErrors).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n const requestHeaders =\n result.requestHeaders && typeof result.requestHeaders === 'object'\n ? Object.fromEntries(\n Object.entries(result.requestHeaders).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n return { ok, message, fieldErrors, requestHeaders, details: result.details }\n }\n return { ok: true }\n }\n\n // --- Transformer events: pipeline dispatch ---\n // Output of widget N becomes input of widget N+1\n if (TRANSFORMER_EVENTS.has(event)) {\n let pipelineData = data\n let applyToForm = false\n for (const widget of widgets) {\n const handler = widget.module.eventHandlers?.[event]\n if (!handler) continue\n try {\n const widgetContext = injectSharedStateIntoContext(context, widget.moduleId)\n let handlerResult: unknown\n if (event === 'transformValidation') {\n handlerResult = await (handler as any)(pipelineData, meta?.originalData ?? data, widgetContext)\n } else {\n handlerResult = await (handler as any)(pipelineData, widgetContext)\n }\n if (\n event === 'transformFormData' &&\n handlerResult !== null &&\n typeof handlerResult === 'object' &&\n 'applyToForm' in handlerResult &&\n (handlerResult as { applyToForm: unknown }).applyToForm === true &&\n 'data' in handlerResult\n ) {\n pipelineData = (handlerResult as { data: TData }).data\n applyToForm = true\n } else {\n pipelineData = handlerResult as TData\n }\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Error in ${event} for widget ${widget.widgetId}:`, err)\n }\n }\n return { ok: true, data: pipelineData, applyToForm }\n }\n\n // --- Action events: sequential dispatch ---\n const mergedRequestHeaders: Record<string, string> = {}\n let hasRequestHeaders = false\n let fieldValue = meta?.fieldValue\n let fieldSideEffects: Record<string, unknown> | undefined\n let fieldMessages: Array<{ text: string; severity: 'info' | 'warning' | 'error' }> | undefined\n\n for (const widget of widgets) {\n const eventHandlers = widget.module.eventHandlers\n // Check operation filter \u2014 skip widget if current operation is filtered out\n const operationFilter = eventHandlers?.filter?.operations\n if (operationFilter) {\n const currentOperation = (context as Record<string, unknown>)?.operation as string | undefined\n if (currentOperation && !operationFilter.includes(currentOperation as 'create' | 'update' | 'delete')) {\n continue\n }\n }\n let handler = eventHandlers?.[event]\n // Delete-to-save fallback chain\n if (!handler && event === 'onBeforeDelete') handler = eventHandlers?.onBeforeSave as typeof handler\n if (!handler && event === 'onDelete') handler = eventHandlers?.onSave as typeof handler\n if (!handler && event === 'onAfterDelete') handler = eventHandlers?.onAfterSave as typeof handler\n if (handler) {\n try {\n const widgetContext = injectSharedStateIntoContext(context, widget.moduleId)\n const result =\n event === 'onDeleteError'\n ? await (handler as any)(data, widgetContext, meta?.error)\n : event === 'onFieldChange'\n ? await (handler as any)(meta?.fieldId, fieldValue, data, widgetContext)\n : event === 'onBeforeNavigate'\n ? await (handler as any)(meta?.target, widgetContext)\n : event === 'onVisibilityChange'\n ? await (handler as any)(meta?.visible, widgetContext)\n : event === 'onAppEvent'\n ? await (handler as any)(meta?.appEvent, widgetContext)\n : await (handler as any)(data, widgetContext)\n if (event === 'onBeforeSave') {\n const normalized = normalizeBeforeSave(result as WidgetBeforeSaveResult)\n if (!normalized.ok) {\n console.log(`[useInjectionSpotEvents] Widget ${widget.widgetId} prevented ${event}`)\n return normalized\n }\n if (normalized.requestHeaders && Object.keys(normalized.requestHeaders).length > 0) {\n Object.assign(mergedRequestHeaders, normalized.requestHeaders)\n hasRequestHeaders = true\n }\n }\n if (event === 'onBeforeDelete') {\n const normalized = normalizeBeforeDelete(result as WidgetBeforeDeleteResult)\n if (!normalized.ok) {\n console.log(`[useInjectionSpotEvents] Widget ${widget.widgetId} prevented ${event}`)\n return normalized\n }\n if (normalized.requestHeaders && Object.keys(normalized.requestHeaders).length > 0) {\n Object.assign(mergedRequestHeaders, normalized.requestHeaders)\n hasRequestHeaders = true\n }\n }\n if (event === 'onBeforeNavigate') {\n const navResult = result as NavigateGuardResult | undefined\n if (navResult && navResult.ok === false) {\n return { ok: false, message: navResult.message }\n }\n }\n if (event === 'onFieldChange') {\n const changeResult = result as FieldChangeResult | void\n if (changeResult?.value !== undefined) {\n fieldValue = changeResult.value\n }\n if (changeResult?.sideEffects && typeof changeResult.sideEffects === 'object') {\n fieldSideEffects = { ...(fieldSideEffects ?? {}), ...changeResult.sideEffects }\n }\n if (changeResult?.message?.text) {\n fieldMessages = [...(fieldMessages ?? []), changeResult.message]\n }\n }\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Error in ${event} for widget ${widget.widgetId}:`, err)\n if (event === 'onBeforeSave' || event === 'onBeforeDelete' || event === 'onBeforeNavigate') {\n const message =\n err instanceof Error\n ? err.message || 'Validation blocked'\n : typeof err === 'string'\n ? err\n : undefined\n return { ok: false, message }\n }\n }\n }\n }\n if ((event === 'onBeforeSave' || event === 'onBeforeDelete') && hasRequestHeaders) {\n return { ok: true, requestHeaders: mergedRequestHeaders }\n }\n if (event === 'onFieldChange') {\n return {\n ok: true,\n fieldChange: {\n value: fieldValue,\n sideEffects: fieldSideEffects,\n messages: fieldMessages,\n },\n }\n }\n return { ok: true }\n },\n [widgets]\n )\n\n return { triggerEvent, widgets }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -171,10 +171,10 @@ const Avatar = React.forwardRef((props, ref) => {
|
|
|
171
171
|
);
|
|
172
172
|
});
|
|
173
173
|
Avatar.displayName = "Avatar";
|
|
174
|
-
function AvatarStack({ children, max = 4, size = "md", className }) {
|
|
174
|
+
function AvatarStack({ children, max = 4, size = "md", className, overflowCount = 0 }) {
|
|
175
175
|
const items = React.Children.toArray(children);
|
|
176
176
|
const visible = items.slice(0, max);
|
|
177
|
-
const overflow = items.length - max;
|
|
177
|
+
const overflow = Math.max(0, items.length - max) + Math.max(0, overflowCount);
|
|
178
178
|
return /* @__PURE__ */ jsxs(
|
|
179
179
|
"div",
|
|
180
180
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/avatar.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\n\n/**\n * User / entity avatar with auto-generated initials, optional photo,\n * optional icon override, optional **status indicator** (online/away/\n * busy/away dot per Figma `Bottom Status [1.1]`), optional **badge**\n * slot for top-right affordances (verified check, premium icon, X to\n * remove \u2014 per Figma `Top Status [1.1]`), and optional outer **ring**\n * (accent / status-colored).\n *\n * Phase B.4 rewrite per Figma `Avatars` page (`210:4129`):\n * - `Avatar [1.1]` (`245:18786`)\n * - `Bottom Status [1.1]` (`245:18721`) \u2014 5 dot tones\n * - `Top Status [1.1]` (`245:18697`) \u2014 6 icon-badge variants\n * - `Avatar Group [1.1]` (`581:6198`) + `Compact Avatar Group [1.1]`\n * (`2906:14962`) \u2014 see `AvatarStack`.\n *\n * Backward compatibility (5 import sites \u2014 customers linking\n * adapters + LinkEntityDialog + ds-v5 demo): existing\n * `<Avatar src label size variant icon />` API stays callable\n * verbatim. New optional props (`status`, `statusPosition`, `ring`,\n * `badge`, `badgeClassName`) are additive.\n */\n\nconst avatarVariants = cva(\n 'inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full font-semibold select-none',\n {\n variants: {\n size: {\n xs: 'size-5 text-xs',\n sm: 'size-7 text-xs',\n md: 'size-9 text-sm',\n lg: 'size-12 text-base',\n xl: 'size-16 text-xl',\n },\n variant: {\n default: 'bg-primary/10 text-primary',\n monochrome: 'bg-muted text-muted-foreground',\n },\n },\n defaultVariants: {\n size: 'md',\n variant: 'default',\n },\n },\n)\n\nfunction computeInitials(label: string): string {\n const trimmed = label.trim()\n if (!trimmed.length) return '?'\n const parts = trimmed.split(/\\s+/).filter(Boolean)\n if (parts.length === 1) {\n return parts[0].slice(0, 2).toUpperCase()\n }\n const first = parts[0][0] ?? ''\n const last = parts[parts.length - 1][0] ?? ''\n return (first + last).toUpperCase()\n}\n\n// Status tone \u2192 background color class. Surface stays opaque + ringed\n// by the page background (`ring-background`) so the badge reads as\n// an overlay on top of the avatar circle.\nexport type AvatarStatus =\n | 'online'\n | 'offline'\n | 'busy'\n | 'away'\n | 'success'\n | 'warning'\n | 'error'\n | 'info'\n\nconst AVATAR_STATUS_BG: Record<AvatarStatus, string> = {\n online: 'bg-status-success-icon',\n offline: 'bg-muted-foreground',\n busy: 'bg-status-error-icon',\n away: 'bg-status-warning-icon',\n success: 'bg-status-success-icon',\n warning: 'bg-status-warning-icon',\n error: 'bg-status-error-icon',\n info: 'bg-status-info-icon',\n}\n\n// Status dot size per avatar size \u2014 slightly less than 1/3 of the\n// avatar diameter so the dot reads but doesn't overpower.\nconst AVATAR_STATUS_SIZE: Record<NonNullable<VariantProps<typeof avatarVariants>['size']>, string> = {\n xs: 'size-1.5',\n sm: 'size-2',\n md: 'size-2.5',\n lg: 'size-3',\n xl: 'size-4',\n}\n\n// Outer ring tones (story-style highlight, selected, status outline).\nexport type AvatarRingTone = 'accent' | 'success' | 'warning' | 'error' | 'muted'\n\nconst AVATAR_RING_CLASS: Record<AvatarRingTone, string> = {\n accent: 'ring-2 ring-accent-indigo ring-offset-2 ring-offset-background',\n success: 'ring-2 ring-status-success-icon ring-offset-2 ring-offset-background',\n warning: 'ring-2 ring-status-warning-icon ring-offset-2 ring-offset-background',\n error: 'ring-2 ring-status-error-icon ring-offset-2 ring-offset-background',\n muted: 'ring-2 ring-input ring-offset-2 ring-offset-background',\n}\n\nexport type AvatarProps = {\n label: string\n src?: string | null\n icon?: React.ReactNode\n ariaLabel?: string\n /** Bottom-right (default) or top-right status dot. */\n status?: AvatarStatus\n /** Where the status dot sits. @default 'bottom-right' */\n statusPosition?: 'bottom-right' | 'top-right'\n /** Outer ring affordance \u2014 pass `true` for accent (alias for\n * `'accent'`) or a specific tone. */\n ring?: boolean | AvatarRingTone\n /** Custom top-right badge slot \u2014 overrides `status` when both are\n * set with `statusPosition='top-right'`. Use for verified check\n * icons, premium markers, \"X to remove\" affordances per Figma\n * `Top Status [1.1]`. The badge content is rendered inside a\n * `rounded-full ring-2 ring-background` shell sized relative to\n * the avatar \u2014 pass a simple icon (`<Check className=\"size-3\" />`)\n * for the cleanest look. */\n badge?: React.ReactNode\n /** Override the badge wrapper className (size, bg, etc.). */\n badgeClassName?: string\n} & VariantProps<typeof avatarVariants> &\n Omit<React.HTMLAttributes<HTMLDivElement>, 'role' | 'aria-label'>\n\ntype AvatarCircleProps = Omit<\n AvatarProps,\n 'status' | 'statusPosition' | 'ring' | 'badge' | 'badgeClassName'\n> & {\n className?: string\n}\n\n// Inner div (the actual avatar circle). Implemented as forwardRef so\n// the outer Avatar wrapper can forward refs through to the rendered\n// `<div role=\"img\">` regardless of whether it lives directly inside\n// Avatar or inside the decorated `<span data-slot=\"avatar-root\">`\n// wrapper.\nconst AvatarCircle = React.forwardRef<HTMLDivElement, AvatarCircleProps>(function AvatarCircle(\n { className, label, src, icon, size, variant, ariaLabel, ...rest },\n ref,\n) {\n const initials = React.useMemo(() => computeInitials(label), [label])\n return (\n <div\n ref={ref}\n role=\"img\"\n aria-label={ariaLabel ?? label}\n data-slot=\"avatar\"\n className={cn(avatarVariants({ size, variant }), className)}\n {...rest}\n >\n {src ? (\n <img src={src} alt=\"\" className=\"size-full object-cover\" aria-hidden=\"true\" />\n ) : icon ? (\n <span aria-hidden=\"true\" className=\"flex items-center justify-center [&>svg]:size-[55%]\">\n {icon}\n </span>\n ) : (\n <span aria-hidden=\"true\">{initials}</span>\n )}\n </div>\n )\n})\n\nexport const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>((props, ref) => {\n const {\n className,\n label,\n src,\n icon,\n size = 'md',\n variant,\n ariaLabel,\n status,\n statusPosition = 'bottom-right',\n ring,\n badge,\n badgeClassName,\n ...rest\n } = props\n\n const resolvedRingTone: AvatarRingTone | null = ring === true ? 'accent' : ring || null\n const showStatus = status !== undefined\n const showBadge =\n badge !== undefined && (statusPosition === 'top-right' || !showStatus)\n const showTopStatus = showStatus && statusPosition === 'top-right' && !showBadge\n const showBottomStatus = showStatus && statusPosition === 'bottom-right' && badge === undefined\n\n // Plain avatar (no overlay decorations) \u2014 preserve the original\n // single-element output for backward compat.\n if (!showStatus && !showBadge && !resolvedRingTone) {\n return (\n <AvatarCircle\n ref={ref as React.Ref<HTMLDivElement>}\n className={className}\n label={label}\n src={src}\n icon={icon}\n size={size}\n variant={variant}\n ariaLabel={ariaLabel}\n {...rest}\n />\n )\n }\n\n const ringClass = resolvedRingTone ? AVATAR_RING_CLASS[resolvedRingTone] : ''\n const statusSize = AVATAR_STATUS_SIZE[size ?? 'md']\n const statusPositionClass =\n statusPosition === 'top-right'\n ? '-right-0 -top-0 translate-x-1/4 -translate-y-1/4'\n : '-right-0 -bottom-0 translate-x-1/4 translate-y-1/4'\n const badgePositionClass = '-right-0 -top-0 translate-x-1/4 -translate-y-1/4'\n\n return (\n <span\n data-slot=\"avatar-root\"\n className={cn('relative inline-flex shrink-0 rounded-full', ringClass)}\n >\n <AvatarCircle\n ref={ref as React.Ref<HTMLDivElement>}\n className={className}\n label={label}\n src={src}\n icon={icon}\n size={size}\n variant={variant}\n ariaLabel={ariaLabel}\n {...rest}\n />\n {showBottomStatus || showTopStatus ? (\n <span\n data-slot=\"avatar-status\"\n data-status={status}\n data-position={statusPosition}\n aria-hidden=\"true\"\n className={cn(\n 'absolute inline-block rounded-full ring-2 ring-background',\n statusSize,\n AVATAR_STATUS_BG[status as AvatarStatus],\n statusPositionClass,\n )}\n />\n ) : null}\n {showBadge ? (\n <span\n data-slot=\"avatar-badge\"\n aria-hidden=\"true\"\n className={cn(\n 'absolute inline-flex items-center justify-center rounded-full ring-2 ring-background bg-background text-foreground',\n // Default size ~40% of avatar; consumer overrides via badgeClassName.\n size === 'xs' || size === 'sm' ? 'size-3.5' : size === 'md' ? 'size-4' : size === 'lg' ? 'size-5' : 'size-6',\n badgePositionClass,\n badgeClassName,\n )}\n >\n {badge}\n </span>\n ) : null}\n </span>\n )\n})\n\nAvatar.displayName = 'Avatar'\n\nexport { avatarVariants }\n\nexport type AvatarStackProps = {\n children: React.ReactNode\n max?: number\n size?: VariantProps<typeof avatarVariants>['size']\n className?: string\n}\n\nexport function AvatarStack({ children, max = 4, size = 'md', className }: AvatarStackProps) {\n const items = React.Children.toArray(children)\n const visible = items.slice(0, max)\n const overflow = items.length - max\n\n return (\n <div\n data-slot=\"avatar-stack\"\n className={cn('flex items-center [&>*:not(:first-child)]:-ml-2 [&>*]:ring-2 [&>*]:ring-background', className)}\n >\n {visible}\n {overflow > 0 && (\n <Avatar label={`+${overflow}`} size={size} variant=\"monochrome\" className=\"-ml-2\" />\n )}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\n\n/**\n * User / entity avatar with auto-generated initials, optional photo,\n * optional icon override, optional **status indicator** (online/away/\n * busy/away dot per Figma `Bottom Status [1.1]`), optional **badge**\n * slot for top-right affordances (verified check, premium icon, X to\n * remove \u2014 per Figma `Top Status [1.1]`), and optional outer **ring**\n * (accent / status-colored).\n *\n * Phase B.4 rewrite per Figma `Avatars` page (`210:4129`):\n * - `Avatar [1.1]` (`245:18786`)\n * - `Bottom Status [1.1]` (`245:18721`) \u2014 5 dot tones\n * - `Top Status [1.1]` (`245:18697`) \u2014 6 icon-badge variants\n * - `Avatar Group [1.1]` (`581:6198`) + `Compact Avatar Group [1.1]`\n * (`2906:14962`) \u2014 see `AvatarStack`.\n *\n * Backward compatibility (5 import sites \u2014 customers linking\n * adapters + LinkEntityDialog + ds-v5 demo): existing\n * `<Avatar src label size variant icon />` API stays callable\n * verbatim. New optional props (`status`, `statusPosition`, `ring`,\n * `badge`, `badgeClassName`) are additive.\n */\n\nconst avatarVariants = cva(\n 'inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full font-semibold select-none',\n {\n variants: {\n size: {\n xs: 'size-5 text-xs',\n sm: 'size-7 text-xs',\n md: 'size-9 text-sm',\n lg: 'size-12 text-base',\n xl: 'size-16 text-xl',\n },\n variant: {\n default: 'bg-primary/10 text-primary',\n monochrome: 'bg-muted text-muted-foreground',\n },\n },\n defaultVariants: {\n size: 'md',\n variant: 'default',\n },\n },\n)\n\nfunction computeInitials(label: string): string {\n const trimmed = label.trim()\n if (!trimmed.length) return '?'\n const parts = trimmed.split(/\\s+/).filter(Boolean)\n if (parts.length === 1) {\n return parts[0].slice(0, 2).toUpperCase()\n }\n const first = parts[0][0] ?? ''\n const last = parts[parts.length - 1][0] ?? ''\n return (first + last).toUpperCase()\n}\n\n// Status tone \u2192 background color class. Surface stays opaque + ringed\n// by the page background (`ring-background`) so the badge reads as\n// an overlay on top of the avatar circle.\nexport type AvatarStatus =\n | 'online'\n | 'offline'\n | 'busy'\n | 'away'\n | 'success'\n | 'warning'\n | 'error'\n | 'info'\n\nconst AVATAR_STATUS_BG: Record<AvatarStatus, string> = {\n online: 'bg-status-success-icon',\n offline: 'bg-muted-foreground',\n busy: 'bg-status-error-icon',\n away: 'bg-status-warning-icon',\n success: 'bg-status-success-icon',\n warning: 'bg-status-warning-icon',\n error: 'bg-status-error-icon',\n info: 'bg-status-info-icon',\n}\n\n// Status dot size per avatar size \u2014 slightly less than 1/3 of the\n// avatar diameter so the dot reads but doesn't overpower.\nconst AVATAR_STATUS_SIZE: Record<NonNullable<VariantProps<typeof avatarVariants>['size']>, string> = {\n xs: 'size-1.5',\n sm: 'size-2',\n md: 'size-2.5',\n lg: 'size-3',\n xl: 'size-4',\n}\n\n// Outer ring tones (story-style highlight, selected, status outline).\nexport type AvatarRingTone = 'accent' | 'success' | 'warning' | 'error' | 'muted'\n\nconst AVATAR_RING_CLASS: Record<AvatarRingTone, string> = {\n accent: 'ring-2 ring-accent-indigo ring-offset-2 ring-offset-background',\n success: 'ring-2 ring-status-success-icon ring-offset-2 ring-offset-background',\n warning: 'ring-2 ring-status-warning-icon ring-offset-2 ring-offset-background',\n error: 'ring-2 ring-status-error-icon ring-offset-2 ring-offset-background',\n muted: 'ring-2 ring-input ring-offset-2 ring-offset-background',\n}\n\nexport type AvatarProps = {\n label: string\n src?: string | null\n icon?: React.ReactNode\n ariaLabel?: string\n /** Bottom-right (default) or top-right status dot. */\n status?: AvatarStatus\n /** Where the status dot sits. @default 'bottom-right' */\n statusPosition?: 'bottom-right' | 'top-right'\n /** Outer ring affordance \u2014 pass `true` for accent (alias for\n * `'accent'`) or a specific tone. */\n ring?: boolean | AvatarRingTone\n /** Custom top-right badge slot \u2014 overrides `status` when both are\n * set with `statusPosition='top-right'`. Use for verified check\n * icons, premium markers, \"X to remove\" affordances per Figma\n * `Top Status [1.1]`. The badge content is rendered inside a\n * `rounded-full ring-2 ring-background` shell sized relative to\n * the avatar \u2014 pass a simple icon (`<Check className=\"size-3\" />`)\n * for the cleanest look. */\n badge?: React.ReactNode\n /** Override the badge wrapper className (size, bg, etc.). */\n badgeClassName?: string\n} & VariantProps<typeof avatarVariants> &\n Omit<React.HTMLAttributes<HTMLDivElement>, 'role' | 'aria-label'>\n\ntype AvatarCircleProps = Omit<\n AvatarProps,\n 'status' | 'statusPosition' | 'ring' | 'badge' | 'badgeClassName'\n> & {\n className?: string\n}\n\n// Inner div (the actual avatar circle). Implemented as forwardRef so\n// the outer Avatar wrapper can forward refs through to the rendered\n// `<div role=\"img\">` regardless of whether it lives directly inside\n// Avatar or inside the decorated `<span data-slot=\"avatar-root\">`\n// wrapper.\nconst AvatarCircle = React.forwardRef<HTMLDivElement, AvatarCircleProps>(function AvatarCircle(\n { className, label, src, icon, size, variant, ariaLabel, ...rest },\n ref,\n) {\n const initials = React.useMemo(() => computeInitials(label), [label])\n return (\n <div\n ref={ref}\n role=\"img\"\n aria-label={ariaLabel ?? label}\n data-slot=\"avatar\"\n className={cn(avatarVariants({ size, variant }), className)}\n {...rest}\n >\n {src ? (\n <img src={src} alt=\"\" className=\"size-full object-cover\" aria-hidden=\"true\" />\n ) : icon ? (\n <span aria-hidden=\"true\" className=\"flex items-center justify-center [&>svg]:size-[55%]\">\n {icon}\n </span>\n ) : (\n <span aria-hidden=\"true\">{initials}</span>\n )}\n </div>\n )\n})\n\nexport const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>((props, ref) => {\n const {\n className,\n label,\n src,\n icon,\n size = 'md',\n variant,\n ariaLabel,\n status,\n statusPosition = 'bottom-right',\n ring,\n badge,\n badgeClassName,\n ...rest\n } = props\n\n const resolvedRingTone: AvatarRingTone | null = ring === true ? 'accent' : ring || null\n const showStatus = status !== undefined\n const showBadge =\n badge !== undefined && (statusPosition === 'top-right' || !showStatus)\n const showTopStatus = showStatus && statusPosition === 'top-right' && !showBadge\n const showBottomStatus = showStatus && statusPosition === 'bottom-right' && badge === undefined\n\n // Plain avatar (no overlay decorations) \u2014 preserve the original\n // single-element output for backward compat.\n if (!showStatus && !showBadge && !resolvedRingTone) {\n return (\n <AvatarCircle\n ref={ref as React.Ref<HTMLDivElement>}\n className={className}\n label={label}\n src={src}\n icon={icon}\n size={size}\n variant={variant}\n ariaLabel={ariaLabel}\n {...rest}\n />\n )\n }\n\n const ringClass = resolvedRingTone ? AVATAR_RING_CLASS[resolvedRingTone] : ''\n const statusSize = AVATAR_STATUS_SIZE[size ?? 'md']\n const statusPositionClass =\n statusPosition === 'top-right'\n ? '-right-0 -top-0 translate-x-1/4 -translate-y-1/4'\n : '-right-0 -bottom-0 translate-x-1/4 translate-y-1/4'\n const badgePositionClass = '-right-0 -top-0 translate-x-1/4 -translate-y-1/4'\n\n return (\n <span\n data-slot=\"avatar-root\"\n className={cn('relative inline-flex shrink-0 rounded-full', ringClass)}\n >\n <AvatarCircle\n ref={ref as React.Ref<HTMLDivElement>}\n className={className}\n label={label}\n src={src}\n icon={icon}\n size={size}\n variant={variant}\n ariaLabel={ariaLabel}\n {...rest}\n />\n {showBottomStatus || showTopStatus ? (\n <span\n data-slot=\"avatar-status\"\n data-status={status}\n data-position={statusPosition}\n aria-hidden=\"true\"\n className={cn(\n 'absolute inline-block rounded-full ring-2 ring-background',\n statusSize,\n AVATAR_STATUS_BG[status as AvatarStatus],\n statusPositionClass,\n )}\n />\n ) : null}\n {showBadge ? (\n <span\n data-slot=\"avatar-badge\"\n aria-hidden=\"true\"\n className={cn(\n 'absolute inline-flex items-center justify-center rounded-full ring-2 ring-background bg-background text-foreground',\n // Default size ~40% of avatar; consumer overrides via badgeClassName.\n size === 'xs' || size === 'sm' ? 'size-3.5' : size === 'md' ? 'size-4' : size === 'lg' ? 'size-5' : 'size-6',\n badgePositionClass,\n badgeClassName,\n )}\n >\n {badge}\n </span>\n ) : null}\n </span>\n )\n})\n\nAvatar.displayName = 'Avatar'\n\nexport { avatarVariants }\n\nexport type AvatarStackProps = {\n children: React.ReactNode\n max?: number\n size?: VariantProps<typeof avatarVariants>['size']\n className?: string\n /**\n * Additional hidden items not represented by `children` \u2014 e.g. a server-truncated list that\n * already returned only the top N owners. Added to the children-derived overflow so the\n * \"+N\" badge reflects the true number of omitted items, not just the ones dropped locally.\n */\n overflowCount?: number\n}\n\nexport function AvatarStack({ children, max = 4, size = 'md', className, overflowCount = 0 }: AvatarStackProps) {\n const items = React.Children.toArray(children)\n const visible = items.slice(0, max)\n const overflow = Math.max(0, items.length - max) + Math.max(0, overflowCount)\n\n return (\n <div\n data-slot=\"avatar-stack\"\n className={cn('flex items-center [&>*:not(:first-child)]:-ml-2 [&>*]:ring-2 [&>*]:ring-background', className)}\n >\n {visible}\n {overflow > 0 && (\n <Avatar label={`+${overflow}`} size={size} variant=\"monochrome\" className=\"-ml-2\" />\n )}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["AvatarCircle"]
|
|
7
7
|
}
|
|
@@ -114,7 +114,7 @@ const Pagination = React.forwardRef(
|
|
|
114
114
|
ref,
|
|
115
115
|
"data-slot": "pagination",
|
|
116
116
|
"aria-label": props["aria-label"] ?? t("ui.pagination.landmark.ariaLabel", "Pagination"),
|
|
117
|
-
className: cn("flex w-full items-center justify-between gap-6", className),
|
|
117
|
+
className: cn("flex w-full flex-wrap items-center justify-between gap-x-6 gap-y-2", className),
|
|
118
118
|
...props,
|
|
119
119
|
children: [
|
|
120
120
|
showInfo ? /* @__PURE__ */ jsx(
|
|
@@ -129,7 +129,7 @@ const Pagination = React.forwardRef(
|
|
|
129
129
|
"div",
|
|
130
130
|
{
|
|
131
131
|
"data-slot": "pagination-controls",
|
|
132
|
-
className: "
|
|
132
|
+
className: "flex flex-wrap items-center justify-center gap-2",
|
|
133
133
|
children: [
|
|
134
134
|
showFirstLast ? /* @__PURE__ */ jsx(
|
|
135
135
|
"button",
|
|
@@ -159,7 +159,7 @@ const Pagination = React.forwardRef(
|
|
|
159
159
|
"ol",
|
|
160
160
|
{
|
|
161
161
|
"data-slot": "pagination-pages",
|
|
162
|
-
className: "
|
|
162
|
+
className: "flex flex-wrap items-center justify-center gap-2 list-none",
|
|
163
163
|
children: items.map((entry, index) => {
|
|
164
164
|
if (entry === "ellipsis-left" || entry === "ellipsis-right") {
|
|
165
165
|
return /* @__PURE__ */ jsx(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/pagination.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport {\n ChevronLeft,\n ChevronRight,\n ChevronsLeft,\n ChevronsRight,\n} from 'lucide-react'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n CompactSelectTrigger,\n Select,\n SelectContent,\n SelectItem,\n SelectValue,\n} from './compact-select'\n\n/**\n * Page navigation primitive per Figma `Pagination Group [1.1]` (DS Open\n * Mercato componentSet `199985:4135`).\n *\n * Layout (Figma `Basic` variant):\n *\n * [Left] \"Page 2 of 16\"\n * [Center] \u23EE \u25C0 [1][2]\u2026[N-1][N] \u25B6 \u23ED\n * [Right] \"7 / page\" CompactSelect\n *\n * Figma's three boolean variant props are mapped 1:1 to React props:\n *\n * \uD83E\uDD47 First / Last \u2192 `showFirstLast` (default true)\n * \u23ED\uFE0F Next / Previous \u2192 `showPrevNext` (default true)\n * \uD83E\uDDEA Advanced \u2192 `showInfo` + `showPageSize` (default both true)\n *\n * The numeric page list uses the same ellipsis algorithm Material /\n * MUI / shadcn ship: `boundaryCount` pages at each end (default 1),\n * `siblingCount` pages on either side of the current page (default 1),\n * collapse the rest into `\u2026` placeholders.\n *\n * ```tsx\n * const [page, setPage] = React.useState(1)\n * const [pageSize, setPageSize] = React.useState(25)\n * <Pagination\n * page={page}\n * pageSize={pageSize}\n * total={items.length}\n * onPageChange={setPage}\n * onPageSizeChange={setPageSize}\n * />\n *\n * // Compact (no first/last, no page-size select)\n * <Pagination\n * page={page}\n * pageSize={20}\n * total={120}\n * onPageChange={setPage}\n * showFirstLast={false}\n * showPageSize={false}\n * />\n * ```\n */\n\n/**\n * Build a stable list of pages + ellipsis placeholders that fits a\n * pagination row of `boundaryCount + 2 + siblingCount * 2 + 1` cells.\n * Returns `number` entries (1-indexed) and `'ellipsis-left'` /\n * `'ellipsis-right'` placeholders that the renderer paints as `\u2026`.\n *\n * Exported for tests + consumer reuse (e.g. a server-side rendered\n * pagination indicator that needs the same shape).\n */\nexport function buildPaginationItems(\n page: number,\n totalPages: number,\n siblingCount: number,\n boundaryCount: number,\n): Array<number | 'ellipsis-left' | 'ellipsis-right'> {\n if (totalPages <= 0) return []\n // When everything fits, just list every page.\n const totalSlots = boundaryCount * 2 + siblingCount * 2 + 3\n if (totalPages <= totalSlots) {\n return Array.from({ length: totalPages }, (_, i) => i + 1)\n }\n const startPages = Array.from({ length: boundaryCount }, (_, i) => i + 1)\n const endPages = Array.from(\n { length: boundaryCount },\n (_, i) => totalPages - boundaryCount + 1 + i,\n )\n const siblingStart = Math.max(\n Math.min(page - siblingCount, totalPages - boundaryCount - siblingCount * 2 - 1),\n boundaryCount + 2,\n )\n const siblingEnd = Math.min(\n Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2),\n endPages.length > 0 ? endPages[0] - 2 : totalPages - 1,\n )\n const middle: Array<number> = []\n for (let i = siblingStart; i <= siblingEnd; i += 1) middle.push(i)\n\n // Bridge between the start boundary and the sibling window. The\n // ellipsis is only worth showing when \u22652 page numbers fall between\n // them; if exactly 1 page is missing, render that single number\n // instead of \"\u2026\" (cleaner UX \u2014 same width, no information loss).\n const result: Array<number | 'ellipsis-left' | 'ellipsis-right'> = []\n for (const p of startPages) result.push(p)\n if (siblingStart > boundaryCount + 2) {\n result.push('ellipsis-left')\n } else if (siblingStart === boundaryCount + 2) {\n result.push(boundaryCount + 1)\n }\n for (const p of middle) result.push(p)\n const firstEnd = endPages[0] ?? totalPages\n if (siblingEnd < firstEnd - 2) {\n result.push('ellipsis-right')\n } else if (siblingEnd === firstEnd - 2) {\n result.push(firstEnd - 1)\n }\n for (const p of endPages) result.push(p)\n return result\n}\n\nconst cellVariants = cva(\n 'inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-sm font-medium outline-none transition-colors tabular-nums ' +\n 'focus-visible:shadow-focus ' +\n 'disabled:cursor-not-allowed disabled:opacity-50',\n {\n variants: {\n selected: {\n true: 'bg-muted text-foreground',\n false: 'bg-background text-foreground hover:bg-muted/40',\n },\n },\n defaultVariants: { selected: false },\n },\n)\n\nconst navButtonVariants = cva(\n 'inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground outline-none transition-colors ' +\n 'hover:bg-muted/40 hover:text-foreground ' +\n 'focus-visible:shadow-focus ' +\n 'disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-muted-foreground',\n)\n\nexport type PaginationProps = React.HTMLAttributes<HTMLDivElement> & {\n /** Current 1-indexed page. */\n page: number\n /** Items per page. */\n pageSize: number\n /** Total item count. Used to derive `Math.ceil(total / pageSize)` pages. */\n total: number\n /** Called when the user changes page. */\n onPageChange: (next: number) => void\n /** Called when the user changes page size. Optional \u2014 when omitted,\n * the \"X / page\" select is hidden. */\n onPageSizeChange?: (next: number) => void\n /** Page size options for the select. Default `[10, 25, 50, 100]`. */\n pageSizeOptions?: readonly number[]\n /** Show the left \"Page X of Y\" indicator. Default `true`. */\n showInfo?: boolean\n /** Show the right \"X / page\" select. Default `true` when\n * `onPageSizeChange` is provided; ignored otherwise. */\n showPageSize?: boolean\n /** Show \u23EE / \u23ED first / last buttons. Default `true`. */\n showFirstLast?: boolean\n /** Show \u25C0 / \u25B6 prev / next buttons. Default `true`. */\n showPrevNext?: boolean\n /** Pages on either side of the current page in the page list. Default `1`. */\n siblingCount?: number\n /** Pages pinned at each end of the page list. Default `1`. */\n boundaryCount?: number\n /** Block all interactions. Default `false`. */\n disabled?: boolean\n /** ARIA label for the navigation landmark. Default `\"Pagination\"`. */\n 'aria-label'?: string\n /** Format the \"Page X of Y\" label. */\n formatPageInfo?: (page: number, totalPages: number) => string\n /** Format the \"X / page\" label. */\n formatPageSizeLabel?: (pageSize: number) => string\n}\n\nexport const Pagination = React.forwardRef<HTMLDivElement, PaginationProps>(\n (\n {\n className,\n page,\n pageSize,\n total,\n onPageChange,\n onPageSizeChange,\n pageSizeOptions = [10, 25, 50, 100],\n showInfo = true,\n showPageSize: showPageSizeProp,\n showFirstLast = true,\n showPrevNext = true,\n siblingCount = 1,\n boundaryCount = 1,\n disabled = false,\n formatPageInfo,\n formatPageSizeLabel,\n ...props\n },\n ref,\n ) => {\n const t = useT()\n const resolvedFormatPageInfo =\n formatPageInfo ??\n ((p: number, total: number) =>\n t('ui.pagination.info.pageOf', 'Page {page} of {total}', { page: p, total }))\n const resolvedFormatPageSizeLabel =\n formatPageSizeLabel ??\n ((size: number) =>\n t('ui.pagination.itemsPerPage.label', '{size} / page', { size }))\n const totalPages = Math.max(1, Math.ceil(total / Math.max(1, pageSize)))\n const safePage = Math.min(Math.max(1, page), totalPages)\n const items = React.useMemo(\n () => buildPaginationItems(safePage, totalPages, siblingCount, boundaryCount),\n [safePage, totalPages, siblingCount, boundaryCount],\n )\n const showPageSize = showPageSizeProp ?? Boolean(onPageSizeChange)\n\n const goTo = React.useCallback(\n (next: number) => {\n if (disabled) return\n const bounded = Math.min(Math.max(1, next), totalPages)\n if (bounded !== safePage) onPageChange(bounded)\n },\n [disabled, onPageChange, safePage, totalPages],\n )\n\n return (\n <nav\n ref={ref}\n data-slot=\"pagination\"\n aria-label={props['aria-label'] ?? t('ui.pagination.landmark.ariaLabel', 'Pagination')}\n className={cn('flex w-full items-center justify-between gap-6', className)}\n {...props}\n >\n {showInfo ? (\n <div\n data-slot=\"pagination-info\"\n className=\"shrink-0 text-sm text-muted-foreground tabular-nums\"\n >\n {resolvedFormatPageInfo(safePage, totalPages)}\n </div>\n ) : (\n <div />\n )}\n\n <div\n data-slot=\"pagination-controls\"\n className=\"inline-flex items-center gap-2\"\n >\n {showFirstLast ? (\n <button\n type=\"button\"\n data-slot=\"pagination-first\"\n aria-label={t('ui.pagination.first.ariaLabel', 'First page')}\n disabled={disabled || safePage <= 1}\n onClick={() => goTo(1)}\n className={cn(navButtonVariants())}\n >\n <ChevronsLeft aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n {showPrevNext ? (\n <button\n type=\"button\"\n data-slot=\"pagination-prev\"\n aria-label={t('ui.pagination.previous.ariaLabel', 'Previous page')}\n disabled={disabled || safePage <= 1}\n onClick={() => goTo(safePage - 1)}\n className={cn(navButtonVariants())}\n >\n <ChevronLeft aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n\n <ol\n data-slot=\"pagination-pages\"\n className=\"inline-flex items-center gap-2 list-none\"\n >\n {items.map((entry, index) => {\n if (entry === 'ellipsis-left' || entry === 'ellipsis-right') {\n return (\n <li\n key={`${entry}-${index}`}\n data-slot=\"pagination-ellipsis\"\n aria-hidden=\"true\"\n className=\"inline-flex size-8 items-center justify-center text-sm text-muted-foreground\"\n >\n \u2026\n </li>\n )\n }\n const selected = entry === safePage\n return (\n <li key={entry}>\n <button\n type=\"button\"\n data-slot=\"pagination-page\"\n data-state={selected ? 'on' : 'off'}\n aria-current={selected ? 'page' : undefined}\n aria-label={\n selected\n ? t('ui.pagination.page.currentAriaLabel', 'Page {page}, current page', { page: entry })\n : t('ui.pagination.page.goToAriaLabel', 'Go to page {page}', { page: entry })\n }\n disabled={disabled}\n onClick={() => goTo(entry)}\n className={cn(cellVariants({ selected }))}\n >\n {entry}\n </button>\n </li>\n )\n })}\n </ol>\n\n {showPrevNext ? (\n <button\n type=\"button\"\n data-slot=\"pagination-next\"\n aria-label={t('ui.pagination.next.ariaLabel', 'Next page')}\n disabled={disabled || safePage >= totalPages}\n onClick={() => goTo(safePage + 1)}\n className={cn(navButtonVariants())}\n >\n <ChevronRight aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n {showFirstLast ? (\n <button\n type=\"button\"\n data-slot=\"pagination-last\"\n aria-label={t('ui.pagination.last.ariaLabel', 'Last page')}\n disabled={disabled || safePage >= totalPages}\n onClick={() => goTo(totalPages)}\n className={cn(navButtonVariants())}\n >\n <ChevronsRight aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n </div>\n\n {showPageSize && onPageSizeChange ? (\n <div data-slot=\"pagination-page-size\" className=\"shrink-0\">\n <Select\n value={String(pageSize)}\n onValueChange={(next) => onPageSizeChange(Number(next))}\n disabled={disabled}\n >\n <CompactSelectTrigger aria-label={t('ui.pagination.itemsPerPage.ariaLabel', 'Items per page')}>\n <SelectValue />\n </CompactSelectTrigger>\n <SelectContent>\n {pageSizeOptions.map((option) => (\n <SelectItem key={option} value={String(option)}>\n {resolvedFormatPageSizeLabel(option)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n ) : (\n <div />\n )}\n </nav>\n )\n },\n)\nPagination.displayName = 'Pagination'\n\nexport { cellVariants as paginationCellVariants, navButtonVariants as paginationNavVariants }\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport {\n ChevronLeft,\n ChevronRight,\n ChevronsLeft,\n ChevronsRight,\n} from 'lucide-react'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n CompactSelectTrigger,\n Select,\n SelectContent,\n SelectItem,\n SelectValue,\n} from './compact-select'\n\n/**\n * Page navigation primitive per Figma `Pagination Group [1.1]` (DS Open\n * Mercato componentSet `199985:4135`).\n *\n * Layout (Figma `Basic` variant):\n *\n * [Left] \"Page 2 of 16\"\n * [Center] \u23EE \u25C0 [1][2]\u2026[N-1][N] \u25B6 \u23ED\n * [Right] \"7 / page\" CompactSelect\n *\n * Figma's three boolean variant props are mapped 1:1 to React props:\n *\n * \uD83E\uDD47 First / Last \u2192 `showFirstLast` (default true)\n * \u23ED\uFE0F Next / Previous \u2192 `showPrevNext` (default true)\n * \uD83E\uDDEA Advanced \u2192 `showInfo` + `showPageSize` (default both true)\n *\n * The numeric page list uses the same ellipsis algorithm Material /\n * MUI / shadcn ship: `boundaryCount` pages at each end (default 1),\n * `siblingCount` pages on either side of the current page (default 1),\n * collapse the rest into `\u2026` placeholders.\n *\n * ```tsx\n * const [page, setPage] = React.useState(1)\n * const [pageSize, setPageSize] = React.useState(25)\n * <Pagination\n * page={page}\n * pageSize={pageSize}\n * total={items.length}\n * onPageChange={setPage}\n * onPageSizeChange={setPageSize}\n * />\n *\n * // Compact (no first/last, no page-size select)\n * <Pagination\n * page={page}\n * pageSize={20}\n * total={120}\n * onPageChange={setPage}\n * showFirstLast={false}\n * showPageSize={false}\n * />\n * ```\n */\n\n/**\n * Build a stable list of pages + ellipsis placeholders that fits a\n * pagination row of `boundaryCount + 2 + siblingCount * 2 + 1` cells.\n * Returns `number` entries (1-indexed) and `'ellipsis-left'` /\n * `'ellipsis-right'` placeholders that the renderer paints as `\u2026`.\n *\n * Exported for tests + consumer reuse (e.g. a server-side rendered\n * pagination indicator that needs the same shape).\n */\nexport function buildPaginationItems(\n page: number,\n totalPages: number,\n siblingCount: number,\n boundaryCount: number,\n): Array<number | 'ellipsis-left' | 'ellipsis-right'> {\n if (totalPages <= 0) return []\n // When everything fits, just list every page.\n const totalSlots = boundaryCount * 2 + siblingCount * 2 + 3\n if (totalPages <= totalSlots) {\n return Array.from({ length: totalPages }, (_, i) => i + 1)\n }\n const startPages = Array.from({ length: boundaryCount }, (_, i) => i + 1)\n const endPages = Array.from(\n { length: boundaryCount },\n (_, i) => totalPages - boundaryCount + 1 + i,\n )\n const siblingStart = Math.max(\n Math.min(page - siblingCount, totalPages - boundaryCount - siblingCount * 2 - 1),\n boundaryCount + 2,\n )\n const siblingEnd = Math.min(\n Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2),\n endPages.length > 0 ? endPages[0] - 2 : totalPages - 1,\n )\n const middle: Array<number> = []\n for (let i = siblingStart; i <= siblingEnd; i += 1) middle.push(i)\n\n // Bridge between the start boundary and the sibling window. The\n // ellipsis is only worth showing when \u22652 page numbers fall between\n // them; if exactly 1 page is missing, render that single number\n // instead of \"\u2026\" (cleaner UX \u2014 same width, no information loss).\n const result: Array<number | 'ellipsis-left' | 'ellipsis-right'> = []\n for (const p of startPages) result.push(p)\n if (siblingStart > boundaryCount + 2) {\n result.push('ellipsis-left')\n } else if (siblingStart === boundaryCount + 2) {\n result.push(boundaryCount + 1)\n }\n for (const p of middle) result.push(p)\n const firstEnd = endPages[0] ?? totalPages\n if (siblingEnd < firstEnd - 2) {\n result.push('ellipsis-right')\n } else if (siblingEnd === firstEnd - 2) {\n result.push(firstEnd - 1)\n }\n for (const p of endPages) result.push(p)\n return result\n}\n\nconst cellVariants = cva(\n 'inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-sm font-medium outline-none transition-colors tabular-nums ' +\n 'focus-visible:shadow-focus ' +\n 'disabled:cursor-not-allowed disabled:opacity-50',\n {\n variants: {\n selected: {\n true: 'bg-muted text-foreground',\n false: 'bg-background text-foreground hover:bg-muted/40',\n },\n },\n defaultVariants: { selected: false },\n },\n)\n\nconst navButtonVariants = cva(\n 'inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground outline-none transition-colors ' +\n 'hover:bg-muted/40 hover:text-foreground ' +\n 'focus-visible:shadow-focus ' +\n 'disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-muted-foreground',\n)\n\nexport type PaginationProps = React.HTMLAttributes<HTMLDivElement> & {\n /** Current 1-indexed page. */\n page: number\n /** Items per page. */\n pageSize: number\n /** Total item count. Used to derive `Math.ceil(total / pageSize)` pages. */\n total: number\n /** Called when the user changes page. */\n onPageChange: (next: number) => void\n /** Called when the user changes page size. Optional \u2014 when omitted,\n * the \"X / page\" select is hidden. */\n onPageSizeChange?: (next: number) => void\n /** Page size options for the select. Default `[10, 25, 50, 100]`. */\n pageSizeOptions?: readonly number[]\n /** Show the left \"Page X of Y\" indicator. Default `true`. */\n showInfo?: boolean\n /** Show the right \"X / page\" select. Default `true` when\n * `onPageSizeChange` is provided; ignored otherwise. */\n showPageSize?: boolean\n /** Show \u23EE / \u23ED first / last buttons. Default `true`. */\n showFirstLast?: boolean\n /** Show \u25C0 / \u25B6 prev / next buttons. Default `true`. */\n showPrevNext?: boolean\n /** Pages on either side of the current page in the page list. Default `1`. */\n siblingCount?: number\n /** Pages pinned at each end of the page list. Default `1`. */\n boundaryCount?: number\n /** Block all interactions. Default `false`. */\n disabled?: boolean\n /** ARIA label for the navigation landmark. Default `\"Pagination\"`. */\n 'aria-label'?: string\n /** Format the \"Page X of Y\" label. */\n formatPageInfo?: (page: number, totalPages: number) => string\n /** Format the \"X / page\" label. */\n formatPageSizeLabel?: (pageSize: number) => string\n}\n\nexport const Pagination = React.forwardRef<HTMLDivElement, PaginationProps>(\n (\n {\n className,\n page,\n pageSize,\n total,\n onPageChange,\n onPageSizeChange,\n pageSizeOptions = [10, 25, 50, 100],\n showInfo = true,\n showPageSize: showPageSizeProp,\n showFirstLast = true,\n showPrevNext = true,\n siblingCount = 1,\n boundaryCount = 1,\n disabled = false,\n formatPageInfo,\n formatPageSizeLabel,\n ...props\n },\n ref,\n ) => {\n const t = useT()\n const resolvedFormatPageInfo =\n formatPageInfo ??\n ((p: number, total: number) =>\n t('ui.pagination.info.pageOf', 'Page {page} of {total}', { page: p, total }))\n const resolvedFormatPageSizeLabel =\n formatPageSizeLabel ??\n ((size: number) =>\n t('ui.pagination.itemsPerPage.label', '{size} / page', { size }))\n const totalPages = Math.max(1, Math.ceil(total / Math.max(1, pageSize)))\n const safePage = Math.min(Math.max(1, page), totalPages)\n const items = React.useMemo(\n () => buildPaginationItems(safePage, totalPages, siblingCount, boundaryCount),\n [safePage, totalPages, siblingCount, boundaryCount],\n )\n const showPageSize = showPageSizeProp ?? Boolean(onPageSizeChange)\n\n const goTo = React.useCallback(\n (next: number) => {\n if (disabled) return\n const bounded = Math.min(Math.max(1, next), totalPages)\n if (bounded !== safePage) onPageChange(bounded)\n },\n [disabled, onPageChange, safePage, totalPages],\n )\n\n return (\n <nav\n ref={ref}\n data-slot=\"pagination\"\n aria-label={props['aria-label'] ?? t('ui.pagination.landmark.ariaLabel', 'Pagination')}\n className={cn('flex w-full flex-wrap items-center justify-between gap-x-6 gap-y-2', className)}\n {...props}\n >\n {showInfo ? (\n <div\n data-slot=\"pagination-info\"\n className=\"shrink-0 text-sm text-muted-foreground tabular-nums\"\n >\n {resolvedFormatPageInfo(safePage, totalPages)}\n </div>\n ) : (\n <div />\n )}\n\n <div\n data-slot=\"pagination-controls\"\n className=\"flex flex-wrap items-center justify-center gap-2\"\n >\n {showFirstLast ? (\n <button\n type=\"button\"\n data-slot=\"pagination-first\"\n aria-label={t('ui.pagination.first.ariaLabel', 'First page')}\n disabled={disabled || safePage <= 1}\n onClick={() => goTo(1)}\n className={cn(navButtonVariants())}\n >\n <ChevronsLeft aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n {showPrevNext ? (\n <button\n type=\"button\"\n data-slot=\"pagination-prev\"\n aria-label={t('ui.pagination.previous.ariaLabel', 'Previous page')}\n disabled={disabled || safePage <= 1}\n onClick={() => goTo(safePage - 1)}\n className={cn(navButtonVariants())}\n >\n <ChevronLeft aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n\n <ol\n data-slot=\"pagination-pages\"\n className=\"flex flex-wrap items-center justify-center gap-2 list-none\"\n >\n {items.map((entry, index) => {\n if (entry === 'ellipsis-left' || entry === 'ellipsis-right') {\n return (\n <li\n key={`${entry}-${index}`}\n data-slot=\"pagination-ellipsis\"\n aria-hidden=\"true\"\n className=\"inline-flex size-8 items-center justify-center text-sm text-muted-foreground\"\n >\n \u2026\n </li>\n )\n }\n const selected = entry === safePage\n return (\n <li key={entry}>\n <button\n type=\"button\"\n data-slot=\"pagination-page\"\n data-state={selected ? 'on' : 'off'}\n aria-current={selected ? 'page' : undefined}\n aria-label={\n selected\n ? t('ui.pagination.page.currentAriaLabel', 'Page {page}, current page', { page: entry })\n : t('ui.pagination.page.goToAriaLabel', 'Go to page {page}', { page: entry })\n }\n disabled={disabled}\n onClick={() => goTo(entry)}\n className={cn(cellVariants({ selected }))}\n >\n {entry}\n </button>\n </li>\n )\n })}\n </ol>\n\n {showPrevNext ? (\n <button\n type=\"button\"\n data-slot=\"pagination-next\"\n aria-label={t('ui.pagination.next.ariaLabel', 'Next page')}\n disabled={disabled || safePage >= totalPages}\n onClick={() => goTo(safePage + 1)}\n className={cn(navButtonVariants())}\n >\n <ChevronRight aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n {showFirstLast ? (\n <button\n type=\"button\"\n data-slot=\"pagination-last\"\n aria-label={t('ui.pagination.last.ariaLabel', 'Last page')}\n disabled={disabled || safePage >= totalPages}\n onClick={() => goTo(totalPages)}\n className={cn(navButtonVariants())}\n >\n <ChevronsRight aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n </div>\n\n {showPageSize && onPageSizeChange ? (\n <div data-slot=\"pagination-page-size\" className=\"shrink-0\">\n <Select\n value={String(pageSize)}\n onValueChange={(next) => onPageSizeChange(Number(next))}\n disabled={disabled}\n >\n <CompactSelectTrigger aria-label={t('ui.pagination.itemsPerPage.ariaLabel', 'Items per page')}>\n <SelectValue />\n </CompactSelectTrigger>\n <SelectContent>\n {pageSizeOptions.map((option) => (\n <SelectItem key={option} value={String(option)}>\n {resolvedFormatPageSizeLabel(option)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n ) : (\n <div />\n )}\n </nav>\n )\n },\n)\nPagination.displayName = 'Pagination'\n\nexport { cellVariants as paginationCellVariants, navButtonVariants as paginationNavVariants }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["total"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.5
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"@dnd-kit/core": "^6.3.1",
|
|
136
136
|
"@dnd-kit/sortable": "^10.0.0",
|
|
137
137
|
"@dnd-kit/utilities": "^3.2.2",
|
|
138
|
-
"@mdxeditor/editor": "^4.0.
|
|
138
|
+
"@mdxeditor/editor": "^4.0.3",
|
|
139
139
|
"@radix-ui/react-accordion": "^1.2.13",
|
|
140
140
|
"@radix-ui/react-dialog": "^1.1.16",
|
|
141
141
|
"@radix-ui/react-popover": "^1.1.16",
|
|
@@ -154,13 +154,13 @@
|
|
|
154
154
|
"remark-gfm": "^4.0.1"
|
|
155
155
|
},
|
|
156
156
|
"peerDependencies": {
|
|
157
|
-
"@open-mercato/shared": "0.6.5
|
|
157
|
+
"@open-mercato/shared": "0.6.5",
|
|
158
158
|
"react": ">=18.0.0",
|
|
159
159
|
"react-dom": ">=18.0.0",
|
|
160
160
|
"react-is": ">=18.0.0"
|
|
161
161
|
},
|
|
162
162
|
"devDependencies": {
|
|
163
|
-
"@open-mercato/shared": "0.6.5
|
|
163
|
+
"@open-mercato/shared": "0.6.5",
|
|
164
164
|
"@testing-library/dom": "^10.4.1",
|
|
165
165
|
"@testing-library/jest-dom": "^6.9.1",
|
|
166
166
|
"@testing-library/react": "^16.3.1",
|
|
@@ -181,6 +181,5 @@
|
|
|
181
181
|
"type": "git",
|
|
182
182
|
"url": "https://github.com/open-mercato/open-mercato",
|
|
183
183
|
"directory": "packages/ui"
|
|
184
|
-
}
|
|
185
|
-
"stableVersion": "0.6.4"
|
|
184
|
+
}
|
|
186
185
|
}
|
package/src/backend/AppShell.tsx
CHANGED
|
@@ -150,6 +150,11 @@ function resolveInjectedMenuLabel(
|
|
|
150
150
|
return item.label ?? item.id
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
function shouldBypassLogoOptimization(src?: string | null): boolean {
|
|
154
|
+
const value = src ?? ''
|
|
155
|
+
return /^https?:\/\//.test(value) || /^\/api\/attachments\/(?:image|file)\//.test(value)
|
|
156
|
+
}
|
|
157
|
+
|
|
153
158
|
function mergeSidebarItemsWithInjected(
|
|
154
159
|
items: SidebarItem[],
|
|
155
160
|
injectedItems: InjectionMenuItem[],
|
|
@@ -444,6 +449,11 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
444
449
|
const { items: topbarInjectedMenuItems } = useInjectedMenuItems('menu:topbar:actions')
|
|
445
450
|
useEventBridge() // SSE DOM Event Bridge — singleton SSE connection for real-time server events
|
|
446
451
|
const resolvedProductName = productName ?? t('appShell.productName')
|
|
452
|
+
const resolvedLogo = chromePayload?.brand?.logo?.src ? chromePayload.brand.logo : logo
|
|
453
|
+
const resolvedBrandName = chromePayload?.brand?.logo?.src
|
|
454
|
+
? chromePayload.brand.name ?? resolvedProductName
|
|
455
|
+
: resolvedProductName
|
|
456
|
+
const resolvedLogoBypassesOptimization = shouldBypassLogoOptimization(resolvedLogo?.src)
|
|
447
457
|
const [mobileOpen, setMobileOpen] = React.useState(false)
|
|
448
458
|
// When the mobile drawer opens on a settings/profile route, it follows the
|
|
449
459
|
// section sidebar by default. Set to 'main' to force-show the main nav even
|
|
@@ -700,8 +710,8 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
700
710
|
className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}
|
|
701
711
|
aria-label={t('appShell.goToDashboard')}
|
|
702
712
|
>
|
|
703
|
-
<Image src={
|
|
704
|
-
{!compact && <span className="truncate text-sm font-medium text-foreground">{
|
|
713
|
+
<Image src={resolvedLogo?.src ?? "/open-mercato.svg"} alt={resolvedLogo?.alt ?? resolvedBrandName} width={40} height={40} className="rounded-full shrink-0" unoptimized={resolvedLogoBypassesOptimization ? true : undefined} />
|
|
714
|
+
{!compact && <span className="truncate text-sm font-medium text-foreground">{resolvedBrandName}</span>}
|
|
705
715
|
</Link>
|
|
706
716
|
</div>
|
|
707
717
|
)}
|
|
@@ -833,8 +843,8 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
833
843
|
className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}
|
|
834
844
|
aria-label={t('appShell.goToDashboard')}
|
|
835
845
|
>
|
|
836
|
-
<Image src={
|
|
837
|
-
{!compact && <span className="truncate text-sm font-medium text-foreground">{
|
|
846
|
+
<Image src={resolvedLogo?.src ?? "/open-mercato.svg"} alt={resolvedLogo?.alt ?? resolvedBrandName} width={40} height={40} className="rounded-full shrink-0" unoptimized={resolvedLogoBypassesOptimization ? true : undefined} />
|
|
847
|
+
{!compact && <span className="truncate text-sm font-medium text-foreground">{resolvedBrandName}</span>}
|
|
838
848
|
</Link>
|
|
839
849
|
</div>
|
|
840
850
|
) : null}
|
|
@@ -899,8 +909,8 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
899
909
|
className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}
|
|
900
910
|
aria-label={t('appShell.goToDashboard')}
|
|
901
911
|
>
|
|
902
|
-
<Image src={
|
|
903
|
-
{!compact && <span className="truncate text-sm font-medium text-foreground">{
|
|
912
|
+
<Image src={resolvedLogo?.src ?? "/open-mercato.svg"} alt={resolvedLogo?.alt ?? resolvedBrandName} width={40} height={40} className="rounded-full shrink-0" unoptimized={resolvedLogoBypassesOptimization ? true : undefined} />
|
|
913
|
+
{!compact && <span className="truncate text-sm font-medium text-foreground">{resolvedBrandName}</span>}
|
|
904
914
|
</Link>
|
|
905
915
|
</div>
|
|
906
916
|
)}
|
|
@@ -1392,8 +1402,8 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
1392
1402
|
<aside className="absolute left-0 top-0 flex h-full w-[280px] max-w-[85vw] flex-col bg-background border-r shadow-lg overflow-hidden">
|
|
1393
1403
|
<div className="shrink-0 flex items-center justify-between gap-2 border-b px-4 py-3">
|
|
1394
1404
|
<Link href="/backend" className="flex items-center gap-2 min-w-0 text-sm font-semibold" onClick={() => setMobileOpen(false)} aria-label={t('appShell.goToDashboard')}>
|
|
1395
|
-
<Image src={
|
|
1396
|
-
<span className="truncate">{
|
|
1405
|
+
<Image src={resolvedLogo?.src ?? "/open-mercato.svg"} alt={resolvedLogo?.alt ?? resolvedBrandName} width={28} height={28} className="rounded shrink-0" unoptimized={resolvedLogoBypassesOptimization ? true : undefined} />
|
|
1406
|
+
<span className="truncate">{resolvedBrandName}</span>
|
|
1397
1407
|
</Link>
|
|
1398
1408
|
<IconButton variant="ghost" size="sm" onClick={() => setMobileOpen(false)} aria-label={t('appShell.closeMenu')}>
|
|
1399
1409
|
<X className="size-4" />
|
|
@@ -264,6 +264,8 @@ export type DataTableProps<T> = {
|
|
|
264
264
|
extensionTableId?: string
|
|
265
265
|
stickyFirstColumn?: boolean
|
|
266
266
|
stickyActionsColumn?: boolean
|
|
267
|
+
/** Horizontal alignment of the row-actions (kebab) column header + cell. Defaults to 'right'. */
|
|
268
|
+
actionsColumnAlign?: 'right' | 'center'
|
|
267
269
|
virtualized?: boolean
|
|
268
270
|
virtualizedMaxHeight?: number | string
|
|
269
271
|
virtualizedOverscan?: number
|
|
@@ -365,10 +367,14 @@ const EMPTY_FILTER_VALUES: FilterValues = Object.freeze({}) as FilterValues
|
|
|
365
367
|
// sticky right-0 → shadow falls to the LEFT (use `before:` + `-left-2` + `to-l`)
|
|
366
368
|
// sticky left-0 → shadow falls to the RIGHT (use `after:` + `-right-2` + `to-r`)
|
|
367
369
|
// `foreground/8` matches the `--shadow-md` token opacity (8%) and is theme-aware.
|
|
370
|
+
// Column pinning (and these shadows) is md-and-up only: below `md` the pinned
|
|
371
|
+
// first column + actions column can be wider than the whole viewport, which
|
|
372
|
+
// leaves the scrollable middle columns no visible window at all — narrow
|
|
373
|
+
// screens fall back to plain horizontal scroll so every column stays reachable.
|
|
368
374
|
const STICKY_RIGHT_SHADOW_CLASS =
|
|
369
|
-
'before:absolute before:inset-y-0 before:-left-2 before:w-2 before:bg-gradient-to-l before:from-foreground/8 before:to-transparent before:pointer-events-none'
|
|
375
|
+
'md:before:absolute md:before:inset-y-0 md:before:-left-2 md:before:w-2 md:before:bg-gradient-to-l md:before:from-foreground/8 md:before:to-transparent md:before:pointer-events-none'
|
|
370
376
|
const STICKY_LEFT_SHADOW_CLASS =
|
|
371
|
-
'after:absolute after:inset-y-0 after:-right-2 after:w-2 after:bg-gradient-to-r after:from-foreground/8 after:to-transparent after:pointer-events-none'
|
|
377
|
+
'md:after:absolute md:after:inset-y-0 md:after:-right-2 md:after:w-2 md:after:bg-gradient-to-r md:after:from-foreground/8 md:after:to-transparent md:after:pointer-events-none'
|
|
372
378
|
|
|
373
379
|
type BulkActionExecuteResult = {
|
|
374
380
|
ok: boolean
|
|
@@ -1000,6 +1006,7 @@ export function DataTable<T>({
|
|
|
1000
1006
|
extensionTableId: extensionTableIdProp,
|
|
1001
1007
|
stickyFirstColumn = false,
|
|
1002
1008
|
stickyActionsColumn = false,
|
|
1009
|
+
actionsColumnAlign = 'right',
|
|
1003
1010
|
virtualized = false,
|
|
1004
1011
|
virtualizedMaxHeight,
|
|
1005
1012
|
virtualizedOverscan = 10,
|
|
@@ -2705,12 +2712,12 @@ export function DataTable<T>({
|
|
|
2705
2712
|
const columnMeta = (header.column.columnDef as any)?.meta
|
|
2706
2713
|
const priority = resolvePriority(header.column)
|
|
2707
2714
|
const isFirstDataColumn = headerIndex === 0
|
|
2708
|
-
const stickyClass = stickyFirstColumn && isFirstDataColumn ? ` sticky left-0 z-10 bg-background ${STICKY_LEFT_SHADOW_CLASS}` : ''
|
|
2715
|
+
const stickyClass = stickyFirstColumn && isFirstDataColumn ? ` md:sticky md:left-0 md:z-10 md:bg-background ${STICKY_LEFT_SHADOW_CLASS}` : ''
|
|
2709
2716
|
const headerCellContent = header.isPlaceholder ? null : (
|
|
2710
2717
|
<Button
|
|
2711
2718
|
variant="ghost"
|
|
2712
2719
|
type="button"
|
|
2713
|
-
className={`h-auto p-0 font-medium ${sortable && header.column.getCanSort?.() ? 'cursor-pointer select-none' : ''}`}
|
|
2720
|
+
className={`h-auto p-0 has-[>svg]:px-0 font-medium ${sortable && header.column.getCanSort?.() ? 'cursor-pointer select-none' : ''}`}
|
|
2714
2721
|
onClick={() => sortable && header.column.toggleSorting?.(header.column.getIsSorted() === 'asc')}
|
|
2715
2722
|
>
|
|
2716
2723
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
@@ -2737,8 +2744,8 @@ export function DataTable<T>({
|
|
|
2737
2744
|
{rowActions || injectedRowActions.length > 0 ? (
|
|
2738
2745
|
<TableHead
|
|
2739
2746
|
className={cn(
|
|
2740
|
-
'w-0 text-right',
|
|
2741
|
-
stickyActionsColumn && `sticky right-0 z-20 bg-background ${STICKY_RIGHT_SHADOW_CLASS}`,
|
|
2747
|
+
actionsColumnAlign === 'center' ? 'w-0 text-center' : 'w-0 text-right',
|
|
2748
|
+
stickyActionsColumn && `md:sticky md:right-0 md:z-20 md:bg-background ${STICKY_RIGHT_SHADOW_CLASS}`,
|
|
2742
2749
|
)}
|
|
2743
2750
|
>
|
|
2744
2751
|
{t('ui.dataTable.actionsColumn', 'Actions')}
|
|
@@ -2860,7 +2867,7 @@ export function DataTable<T>({
|
|
|
2860
2867
|
) : content
|
|
2861
2868
|
|
|
2862
2869
|
return (
|
|
2863
|
-
<TableCell key={cell.id} className={responsiveClass(priority, columnMeta?.hidden) + (isStickyCell ? ` sticky left-0 z-10 bg-background ${STICKY_LEFT_SHADOW_CLASS}` : '')}>
|
|
2870
|
+
<TableCell key={cell.id} className={responsiveClass(priority, columnMeta?.hidden) + (isStickyCell ? ` md:sticky md:left-0 md:z-10 md:bg-background ${STICKY_LEFT_SHADOW_CLASS}` : '')}>
|
|
2864
2871
|
{wrappedContent}
|
|
2865
2872
|
</TableCell>
|
|
2866
2873
|
)
|
|
@@ -2868,8 +2875,8 @@ export function DataTable<T>({
|
|
|
2868
2875
|
{rowActions || injectedRowActions.length > 0 ? (
|
|
2869
2876
|
<TableCell
|
|
2870
2877
|
className={cn(
|
|
2871
|
-
'text-right whitespace-nowrap',
|
|
2872
|
-
stickyActionsColumn && `sticky right-0 z-10 bg-background ${STICKY_RIGHT_SHADOW_CLASS}`,
|
|
2878
|
+
actionsColumnAlign === 'center' ? 'text-center whitespace-nowrap' : 'text-right whitespace-nowrap',
|
|
2879
|
+
stickyActionsColumn && `md:sticky md:right-0 md:z-10 md:bg-background ${STICKY_RIGHT_SHADOW_CLASS}`,
|
|
2873
2880
|
)}
|
|
2874
2881
|
data-actions-cell
|
|
2875
2882
|
>
|