@open-mercato/ui 0.6.7 → 0.6.8-develop.6874.1.982d6097d8
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/AGENTS.md +4 -2
- package/agentic/standalone-guide.md +1 -1
- package/build.mjs +2 -54
- package/dist/ai/AiChat.js +26 -7
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/LoopDisabledBanner.js +1 -1
- package/dist/ai/LoopDisabledBanner.js.map +2 -2
- package/dist/ai/parts/ConfirmationCard.js +4 -4
- package/dist/ai/parts/ConfirmationCard.js.map +2 -2
- package/dist/ai/parts/FieldDiffCard.js +2 -2
- package/dist/ai/parts/FieldDiffCard.js.map +2 -2
- package/dist/ai/parts/MutationResultCard.js +3 -3
- package/dist/ai/parts/MutationResultCard.js.map +2 -2
- package/dist/ai/ui-parts/pending-phase3-placeholder.js +1 -1
- package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +2 -2
- package/dist/backend/AppShell.js +43 -4
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/AuthSessionGuard.js +14 -0
- package/dist/backend/AuthSessionGuard.js.map +2 -2
- package/dist/backend/BackendChromeProvider.js +6 -1
- package/dist/backend/BackendChromeProvider.js.map +2 -2
- package/dist/backend/CrudForm.js +6 -4
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +20 -18
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/JsonBuilder.js +3 -3
- package/dist/backend/JsonBuilder.js.map +2 -2
- package/dist/backend/NextStepCallout.js +4 -4
- package/dist/backend/NextStepCallout.js.map +1 -1
- package/dist/backend/PerspectiveSidebar.js +1 -1
- package/dist/backend/PerspectiveSidebar.js.map +2 -2
- package/dist/backend/ProfileDropdown.js +2 -1
- package/dist/backend/ProfileDropdown.js.map +2 -2
- package/dist/backend/RowActions.js +2 -2
- package/dist/backend/RowActions.js.map +2 -2
- package/dist/backend/confirm-dialog/ConfirmDialog.js +13 -1
- package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
- package/dist/backend/crud/useGroupOrder.js +14 -21
- package/dist/backend/crud/useGroupOrder.js.map +2 -2
- package/dist/backend/custom-fields/FieldDefinitionsManager.js +1 -1
- package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +2 -2
- package/dist/backend/dashboard/DashboardScreen.js +3 -3
- package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
- package/dist/backend/detail/AddressTiles.js +4 -5
- package/dist/backend/detail/AddressTiles.js.map +2 -2
- package/dist/backend/detail/AttachmentDeleteDialog.js +1 -1
- package/dist/backend/detail/AttachmentDeleteDialog.js.map +2 -2
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +1 -1
- package/dist/backend/detail/AttachmentsSection.js +1 -1
- package/dist/backend/detail/AttachmentsSection.js.map +2 -2
- package/dist/backend/detail/NotesSection.js +1 -1
- package/dist/backend/detail/NotesSection.js.map +2 -2
- package/dist/backend/detail/TagsSection.js +1 -1
- package/dist/backend/detail/TagsSection.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterPanel.js +1 -1
- package/dist/backend/filters/AdvancedFilterPanel.js.map +2 -2
- package/dist/backend/forms/FormHeader.js +1 -2
- package/dist/backend/forms/FormHeader.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +1 -25
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.js +3 -3
- package/dist/backend/icons/lucideRegistry.js.map +2 -2
- package/dist/backend/icons/lucideRegistryRuntime.js +28 -0
- package/dist/backend/icons/lucideRegistryRuntime.js.map +7 -0
- package/dist/backend/injection/spotIds.js +23 -18
- package/dist/backend/injection/spotIds.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js.map +1 -1
- package/dist/backend/messages/EmailThreadsPanel.js +1 -1
- package/dist/backend/messages/EmailThreadsPanel.js.map +2 -2
- package/dist/backend/perspectiveState.js +34 -0
- package/dist/backend/perspectiveState.js.map +7 -0
- package/dist/backend/schedule/ScheduleAgenda.js +3 -3
- package/dist/backend/schedule/ScheduleAgenda.js.map +1 -1
- package/dist/backend/schedule/ScheduleGrid.js +3 -3
- package/dist/backend/schedule/ScheduleGrid.js.map +1 -1
- package/dist/backend/schedule/ScheduleToolbar.js +87 -70
- package/dist/backend/schedule/ScheduleToolbar.js.map +2 -2
- package/dist/backend/sidebar/SidebarCustomizationEditor.js +30 -2
- package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
- package/dist/backend/sidebar/customization-helpers.js +30 -0
- package/dist/backend/sidebar/customization-helpers.js.map +2 -2
- package/dist/backend/utils/apiCall.js +21 -1
- package/dist/backend/utils/apiCall.js.map +2 -2
- package/dist/backend/utils/customFieldDefs.js.map +2 -2
- package/dist/backend/utils/customFieldForms.js +10 -9
- package/dist/backend/utils/customFieldForms.js.map +2 -2
- package/dist/backend/utils/fetchAllPages.js +57 -0
- package/dist/backend/utils/fetchAllPages.js.map +7 -0
- package/dist/backend/utils/nav.js +5 -2
- package/dist/backend/utils/nav.js.map +2 -2
- package/dist/backend/version-history/VersionHistoryPanel.js +1 -1
- package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
- package/dist/frontend/LanguageSwitcher.js +2 -1
- package/dist/frontend/LanguageSwitcher.js.map +2 -2
- package/dist/portal/PortalContext.js +1 -1
- package/dist/portal/PortalContext.js.map +2 -2
- package/dist/portal/hooks/portalBridgeStatus.js +25 -0
- package/dist/portal/hooks/portalBridgeStatus.js.map +7 -0
- package/dist/portal/hooks/usePortalEventBridge.js +6 -0
- package/dist/portal/hooks/usePortalEventBridge.js.map +2 -2
- package/dist/portal/hooks/usePortalNotifications.js +42 -4
- package/dist/portal/hooks/usePortalNotifications.js.map +2 -2
- package/dist/portal/utils/nav.js +2 -3
- package/dist/portal/utils/nav.js.map +2 -2
- package/dist/primitives/ErrorNotice.js +1 -1
- package/dist/primitives/ErrorNotice.js.map +2 -2
- package/dist/primitives/Notice.js +1 -1
- package/dist/primitives/Notice.js.map +1 -1
- package/dist/primitives/button.js +9 -4
- package/dist/primitives/button.js.map +2 -2
- package/dist/theme/ThemeProvider.js +1 -1
- package/dist/theme/ThemeProvider.js.map +2 -2
- package/dist/theme/index.js +3 -0
- package/dist/theme/index.js.map +2 -2
- package/dist/theme/theme-init-script.js +17 -0
- package/dist/theme/theme-init-script.js.map +7 -0
- package/figma/alert.figma.tsx +35 -0
- package/figma/badge.figma.tsx +33 -0
- package/figma/button.figma.tsx +33 -0
- package/figma/checkbox.figma.tsx +28 -0
- package/figma/drawer.figma.tsx +40 -0
- package/figma/input-variants.figma.tsx +58 -0
- package/figma/radio.figma.tsx +16 -0
- package/figma/select.figma.tsx +24 -0
- package/figma/status-badge.figma.tsx +24 -0
- package/figma/switch.figma.tsx +28 -0
- package/figma/tabs.figma.tsx +22 -0
- package/figma/tag.figma.tsx +27 -0
- package/figma.config.json +6 -0
- package/jest.config.cjs +2 -1
- package/package.json +21 -18
- package/scripts/lucideRegistrySource.cjs +133 -0
- package/src/ai/AiChat.tsx +36 -7
- package/src/ai/LoopDisabledBanner.tsx +1 -1
- package/src/ai/__tests__/AiChat.test.tsx +31 -0
- package/src/ai/parts/ConfirmationCard.tsx +4 -4
- package/src/ai/parts/FieldDiffCard.tsx +2 -2
- package/src/ai/parts/MutationResultCard.tsx +3 -3
- package/src/ai/ui-parts/pending-phase3-placeholder.tsx +2 -2
- package/src/backend/AppShell.tsx +55 -4
- package/src/backend/AuthSessionGuard.tsx +29 -0
- package/src/backend/BackendChromeProvider.tsx +19 -1
- package/src/backend/CrudForm.tsx +18 -4
- package/src/backend/DataTable.tsx +36 -19
- package/src/backend/JsonBuilder.tsx +3 -3
- package/src/backend/NextStepCallout.tsx +4 -4
- package/src/backend/PerspectiveSidebar.tsx +1 -1
- package/src/backend/ProfileDropdown.tsx +1 -0
- package/src/backend/RowActions.tsx +2 -2
- package/src/backend/__tests__/AppShell.test.tsx +65 -7
- package/src/backend/__tests__/AuthSessionGuard.test.tsx +52 -0
- package/src/backend/__tests__/BackendChromeProvider.test.tsx +98 -2
- package/src/backend/__tests__/CrudForm.fieldsetBinding.test.tsx +158 -0
- package/src/backend/__tests__/CrudForm.groupInjectionColumn.test.tsx +98 -0
- package/src/backend/__tests__/DataTable.metaHiddenLateColumns.test.tsx +105 -0
- package/src/backend/__tests__/DataTable.perspectiveStorage.test.ts +62 -1
- package/src/backend/__tests__/nav-utils.test.ts +139 -0
- package/src/backend/confirm-dialog/ConfirmDialog.tsx +16 -1
- package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +68 -1
- package/src/backend/crud/__tests__/useGroupOrder.test.ts +39 -1
- package/src/backend/crud/useGroupOrder.ts +30 -25
- package/src/backend/custom-fields/FieldDefinitionsManager.tsx +1 -1
- package/src/backend/dashboard/DashboardScreen.tsx +3 -3
- package/src/backend/detail/AddressTiles.tsx +3 -6
- package/src/backend/detail/AttachmentDeleteDialog.tsx +1 -1
- package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
- package/src/backend/detail/AttachmentsSection.tsx +1 -1
- package/src/backend/detail/NotesSection.tsx +1 -1
- package/src/backend/detail/TagsSection.tsx +1 -1
- package/src/backend/detail/__tests__/AttachmentAssignmentsEditor.test.tsx +21 -0
- package/src/backend/filters/AdvancedFilterPanel.tsx +1 -1
- package/src/backend/forms/FormHeader.tsx +1 -2
- package/src/backend/icons/__tests__/lucideRegistryGenerator.test.ts +177 -0
- package/src/backend/icons/lucideRegistry.generated.tsx +3 -37
- package/src/backend/icons/lucideRegistry.ts +2 -2
- package/src/backend/icons/lucideRegistryRuntime.tsx +36 -0
- package/src/backend/injection/__tests__/spotIds.extension-points.test.ts +38 -0
- package/src/backend/injection/spotIds.ts +23 -18
- package/src/backend/inputs/ComboboxInput.tsx +1 -1
- package/src/backend/messages/EmailThreadsPanel.tsx +1 -1
- package/src/backend/perspectiveState.ts +44 -0
- package/src/backend/schedule/ScheduleAgenda.tsx +3 -3
- package/src/backend/schedule/ScheduleGrid.tsx +3 -3
- package/src/backend/schedule/ScheduleToolbar.tsx +89 -68
- package/src/backend/sidebar/SidebarCustomizationEditor.tsx +42 -3
- package/src/backend/sidebar/__tests__/group-visibility.test.ts +131 -0
- package/src/backend/sidebar/customization-helpers.ts +47 -0
- package/src/backend/utils/__tests__/apiCall.test.ts +61 -0
- package/src/backend/utils/__tests__/customFieldForms.optionsPaging.test.ts +95 -0
- package/src/backend/utils/__tests__/fetchAllPages.test.ts +103 -0
- package/src/backend/utils/apiCall.ts +29 -1
- package/src/backend/utils/customFieldDefs.ts +1 -0
- package/src/backend/utils/customFieldForms.ts +15 -11
- package/src/backend/utils/fetchAllPages.ts +93 -0
- package/src/backend/utils/nav.ts +22 -2
- package/src/backend/version-history/VersionHistoryPanel.tsx +1 -1
- package/src/frontend/LanguageSwitcher.tsx +1 -0
- package/src/portal/PortalContext.tsx +1 -1
- package/src/portal/hooks/__tests__/usePortalEventBridge.test.tsx +66 -0
- package/src/portal/hooks/__tests__/usePortalNotifications.test.tsx +155 -0
- package/src/portal/hooks/portalBridgeStatus.ts +29 -0
- package/src/portal/hooks/usePortalEventBridge.ts +6 -0
- package/src/portal/hooks/usePortalNotifications.ts +55 -12
- package/src/portal/utils/__tests__/nav.test.ts +2 -2
- package/src/portal/utils/nav.ts +3 -4
- package/src/primitives/ErrorNotice.tsx +1 -1
- package/src/primitives/Notice.tsx +5 -5
- package/src/primitives/__tests__/button.test.tsx +70 -0
- package/src/primitives/button.tsx +10 -4
- package/src/theme/ThemeProvider.tsx +1 -2
- package/src/theme/__tests__/theme-init-script.test.ts +70 -0
- package/src/theme/index.ts +1 -0
- package/src/theme/theme-init-script.ts +23 -0
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'
|
|
4
4
|
import { apiCall } from '../../backend/utils/apiCall'
|
|
5
|
+
import {
|
|
6
|
+
PORTAL_BRIDGE_STATUS_DOM_NAME,
|
|
7
|
+
readPortalBridgeHealth,
|
|
8
|
+
type PortalBridgeStatusDetail,
|
|
9
|
+
} from './portalBridgeStatus'
|
|
5
10
|
|
|
6
11
|
export type UsePortalNotificationsResult = {
|
|
7
12
|
notifications: NotificationDto[]
|
|
@@ -28,13 +33,11 @@ async function fetchJson<T>(url: string, init?: RequestInit): Promise<T | null>
|
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
/**
|
|
31
|
-
* Portal notification hook
|
|
36
|
+
* Portal notification hook with SSE-first delivery and polling fallback.
|
|
32
37
|
*
|
|
33
|
-
*
|
|
34
|
-
* and
|
|
35
|
-
*
|
|
36
|
-
* Also listens for portal SSE events (`notifications.notification.created`)
|
|
37
|
-
* to trigger immediate refresh.
|
|
38
|
+
* Performs an initial list/count reconciliation, refreshes on notification,
|
|
39
|
+
* reconnect, and focus events, and polls every 8 seconds until the portal
|
|
40
|
+
* event bridge explicitly reports a healthy connection.
|
|
38
41
|
*/
|
|
39
42
|
export function usePortalNotifications(): UsePortalNotificationsResult {
|
|
40
43
|
const [notifications, setNotifications] = React.useState<NotificationDto[]>([])
|
|
@@ -66,18 +69,50 @@ export function usePortalNotifications(): UsePortalNotificationsResult {
|
|
|
66
69
|
setIsLoading(false)
|
|
67
70
|
}, [])
|
|
68
71
|
|
|
69
|
-
|
|
72
|
+
const [usePolling, setUsePolling] = React.useState(() => {
|
|
73
|
+
if (typeof window === 'undefined' || !('EventSource' in window)) {
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
return readPortalBridgeHealth() !== true
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
if (typeof window === 'undefined' || !('EventSource' in window)) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
const handleStatusChange = (event: Event) => {
|
|
84
|
+
const detail = (event as CustomEvent<PortalBridgeStatusDetail>).detail
|
|
85
|
+
if (detail && typeof detail.healthy === 'boolean') {
|
|
86
|
+
setUsePolling(!detail.healthy)
|
|
87
|
+
if (!detail.healthy) {
|
|
88
|
+
fetchAll()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
window.addEventListener(PORTAL_BRIDGE_STATUS_DOM_NAME, handleStatusChange)
|
|
93
|
+
return () => {
|
|
94
|
+
window.removeEventListener(PORTAL_BRIDGE_STATUS_DOM_NAME, handleStatusChange)
|
|
95
|
+
}
|
|
96
|
+
}, [fetchAll])
|
|
97
|
+
|
|
70
98
|
React.useEffect(() => {
|
|
71
99
|
fetchAll()
|
|
100
|
+
}, [fetchAll])
|
|
101
|
+
|
|
102
|
+
React.useEffect(() => {
|
|
103
|
+
if (!usePolling) return
|
|
72
104
|
const interval = setInterval(fetchAll, POLL_INTERVAL)
|
|
73
105
|
return () => clearInterval(interval)
|
|
74
|
-
}, [fetchAll])
|
|
106
|
+
}, [fetchAll, usePolling])
|
|
75
107
|
|
|
76
|
-
// Listen for portal SSE notification events
|
|
77
108
|
React.useEffect(() => {
|
|
78
|
-
const handler = (
|
|
79
|
-
const detail = (
|
|
80
|
-
if (
|
|
109
|
+
const handler = (event: Event) => {
|
|
110
|
+
const detail = (event as CustomEvent<{ id?: string }>).detail
|
|
111
|
+
if (
|
|
112
|
+
detail?.id === 'notifications.notification.created' ||
|
|
113
|
+
detail?.id === 'notifications.notification.batch_created' ||
|
|
114
|
+
detail?.id === 'om:portal-bridge:reconnected'
|
|
115
|
+
) {
|
|
81
116
|
fetchAll()
|
|
82
117
|
}
|
|
83
118
|
}
|
|
@@ -85,6 +120,14 @@ export function usePortalNotifications(): UsePortalNotificationsResult {
|
|
|
85
120
|
return () => window.removeEventListener('om:portal-event', handler)
|
|
86
121
|
}, [fetchAll])
|
|
87
122
|
|
|
123
|
+
React.useEffect(() => {
|
|
124
|
+
const onFocus = () => {
|
|
125
|
+
fetchAll()
|
|
126
|
+
}
|
|
127
|
+
window.addEventListener('focus', onFocus)
|
|
128
|
+
return () => window.removeEventListener('focus', onFocus)
|
|
129
|
+
}, [fetchAll])
|
|
130
|
+
|
|
88
131
|
const markAsRead = React.useCallback(async (id: string) => {
|
|
89
132
|
await fetchJson(`${BASE}/${id}/read`, { method: 'PUT' })
|
|
90
133
|
setNotifications((prev) =>
|
|
@@ -97,7 +97,7 @@ describe('buildPortalNav', () => {
|
|
|
97
97
|
expect(groups).toEqual([{ id: 'main', items: [expect.objectContaining({ label: 'Orders' })] }])
|
|
98
98
|
})
|
|
99
99
|
|
|
100
|
-
it('
|
|
100
|
+
it('does not let the deprecated portal-admin flag bypass effective features', () => {
|
|
101
101
|
const routes: FrontendRouteManifestEntry[] = [
|
|
102
102
|
makeRoute({
|
|
103
103
|
pattern: '/[orgSlug]/portal/orders',
|
|
@@ -107,7 +107,7 @@ describe('buildPortalNav', () => {
|
|
|
107
107
|
]
|
|
108
108
|
|
|
109
109
|
const groups = buildPortalNav({ routes, orgSlug: 'my-org', grantedFeatures: [], isPortalAdmin: true })
|
|
110
|
-
expect(groups
|
|
110
|
+
expect(groups).toEqual([])
|
|
111
111
|
})
|
|
112
112
|
|
|
113
113
|
it('ignores navHidden pages even when nav is declared', () => {
|
package/src/portal/utils/nav.ts
CHANGED
|
@@ -22,9 +22,9 @@ export type BuildPortalNavOptions = {
|
|
|
22
22
|
routes: readonly FrontendRouteManifestEntry[]
|
|
23
23
|
/** Current customer org slug — substituted into `[orgSlug]` patterns. */
|
|
24
24
|
orgSlug: string
|
|
25
|
-
/**
|
|
25
|
+
/** Concrete effective features granted to the current customer. */
|
|
26
26
|
grantedFeatures: readonly string[]
|
|
27
|
-
/**
|
|
27
|
+
/** @deprecated Portal admins must receive concrete effective features. */
|
|
28
28
|
isPortalAdmin?: boolean
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -75,7 +75,6 @@ export function buildPortalNav({
|
|
|
75
75
|
routes,
|
|
76
76
|
orgSlug,
|
|
77
77
|
grantedFeatures,
|
|
78
|
-
isPortalAdmin = false,
|
|
79
78
|
}: BuildPortalNavOptions): PortalNavGroup[] {
|
|
80
79
|
const mainItems: PortalNavItem[] = []
|
|
81
80
|
const accountItems: PortalNavItem[] = []
|
|
@@ -96,7 +95,7 @@ export function buildPortalNav({
|
|
|
96
95
|
const nav = route.nav!
|
|
97
96
|
|
|
98
97
|
const requireFeatures = route.requireCustomerFeatures ?? []
|
|
99
|
-
if (
|
|
98
|
+
if (requireFeatures.length) {
|
|
100
99
|
if (!hasAllFeatures(grantedFeatures as string[], requireFeatures as string[])) continue
|
|
101
100
|
}
|
|
102
101
|
|
|
@@ -12,7 +12,7 @@ export function ErrorNotice({ title, message, action }: {
|
|
|
12
12
|
const resolvedTitle = title ?? t('ui.errors.defaultTitle', 'Something went wrong')
|
|
13
13
|
const resolvedMessage = message ?? t('ui.errors.defaultMessage', 'Unable to load data. Please try again.')
|
|
14
14
|
return (
|
|
15
|
-
<Alert
|
|
15
|
+
<Alert status="error">
|
|
16
16
|
<AlertTitle>{resolvedTitle}</AlertTitle>
|
|
17
17
|
<AlertDescription>{resolvedMessage}</AlertDescription>
|
|
18
18
|
{action ? <div className="mt-2">{action}</div> : null}
|
|
@@ -6,12 +6,12 @@ import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
|
6
6
|
const logger = createLogger('ui')
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* @deprecated Use <Alert
|
|
9
|
+
* @deprecated Use <Alert status="error|warning|information"> instead.
|
|
10
10
|
* Migration guide: docs/design-system/migration-tables.md#j3-component-mapping
|
|
11
11
|
*
|
|
12
|
-
* Notice variant="error" → Alert
|
|
13
|
-
* Notice variant="warning" → Alert
|
|
14
|
-
* Notice variant="info" → Alert
|
|
12
|
+
* Notice variant="error" → Alert status="error"
|
|
13
|
+
* Notice variant="warning" → Alert status="warning"
|
|
14
|
+
* Notice variant="info" → Alert status="information"
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const variantStyles = {
|
|
@@ -51,7 +51,7 @@ export function Notice({
|
|
|
51
51
|
compact = false,
|
|
52
52
|
}: NoticeProps) {
|
|
53
53
|
if (process.env.NODE_ENV === 'development') {
|
|
54
|
-
logger.warn('<Notice> is deprecated. Use <Alert
|
|
54
|
+
logger.warn('<Notice> is deprecated. Use <Alert status="error|warning|information"> instead. See: docs/design-system/migration-tables.md')
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
const styles = variantStyles[variant]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { render } from '@testing-library/react'
|
|
5
|
+
import { Button, buttonVariants } from '../button'
|
|
6
|
+
|
|
7
|
+
type ButtonVariant = NonNullable<Parameters<typeof buttonVariants>[0]>['variant']
|
|
8
|
+
|
|
9
|
+
function classesOf(variant: ButtonVariant) {
|
|
10
|
+
return buttonVariants({ variant }).split(/\s+/).filter(Boolean)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('Button destructive variants', () => {
|
|
14
|
+
// The DS policy: `destructive` is the quiet trigger (red text on a calm
|
|
15
|
+
// surface) so a page full of delete buttons does not train users to ignore
|
|
16
|
+
// red; `destructive-solid` is the filled form, reserved for the single
|
|
17
|
+
// point-of-no-return confirmation inside a dialog. These tests pin both
|
|
18
|
+
// halves of that contract — swapping them back is a visual regression that
|
|
19
|
+
// no other test would catch.
|
|
20
|
+
|
|
21
|
+
it('destructive is quiet: destructive border and text on the page surface', () => {
|
|
22
|
+
const classes = classesOf('destructive')
|
|
23
|
+
expect(classes).toContain('border')
|
|
24
|
+
expect(classes).toContain('border-destructive')
|
|
25
|
+
expect(classes).toContain('bg-background')
|
|
26
|
+
expect(classes).toContain('text-destructive')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('destructive does not fill with the destructive surface', () => {
|
|
30
|
+
const classes = classesOf('destructive')
|
|
31
|
+
expect(classes).not.toContain('bg-destructive')
|
|
32
|
+
expect(classes).not.toContain('text-white')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('destructive-solid fills with the destructive surface and white glyph', () => {
|
|
36
|
+
const classes = classesOf('destructive-solid')
|
|
37
|
+
expect(classes).toContain('bg-destructive')
|
|
38
|
+
expect(classes).toContain('text-white')
|
|
39
|
+
expect(classes).not.toContain('border-destructive')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('destructive-solid ships no dark background override', () => {
|
|
43
|
+
// `--destructive` is a semantic token that already resolves per theme; a
|
|
44
|
+
// `dark:bg-*` override on top of it is what broke IconButton in #3507.
|
|
45
|
+
const classes = classesOf('destructive-solid')
|
|
46
|
+
expect(classes.some((cls) => cls.startsWith('dark:bg-'))).toBe(false)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('renders the quiet treatment on the element for variant="destructive"', () => {
|
|
50
|
+
const { getByRole } = render(<Button variant="destructive">Delete</Button>)
|
|
51
|
+
const className = getByRole('button').className
|
|
52
|
+
expect(className).toContain('text-destructive')
|
|
53
|
+
expect(className).toContain('border-destructive')
|
|
54
|
+
expect(className.split(/\s+/)).not.toContain('bg-destructive')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('renders the filled treatment on the element for variant="destructive-solid"', () => {
|
|
58
|
+
const { getByRole } = render(<Button variant="destructive-solid">Delete permanently</Button>)
|
|
59
|
+
const classNames = getByRole('button').className.split(/\s+/)
|
|
60
|
+
expect(classNames).toContain('bg-destructive')
|
|
61
|
+
expect(classNames).toContain('text-white')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('defaults to the primary variant when none is given', () => {
|
|
65
|
+
const { getByRole } = render(<Button>Save</Button>)
|
|
66
|
+
const classNames = getByRole('button').className.split(/\s+/)
|
|
67
|
+
expect(classNames).toContain('bg-primary')
|
|
68
|
+
expect(classNames).not.toContain('text-destructive')
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -9,14 +9,20 @@ const buttonVariants = cva(
|
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
11
11
|
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary-hover',
|
|
12
|
+
/* Destructive is quiet by design: red text on a calm surface. A wall
|
|
13
|
+
of solid red buttons trains users to ignore red. The filled form is
|
|
14
|
+
`destructive-solid`, reserved for the single point-of-no-return
|
|
15
|
+
confirmation inside a dialog — never for the button that opens it. */
|
|
12
16
|
destructive:
|
|
13
|
-
'
|
|
17
|
+
'border border-destructive bg-background text-destructive shadow-xs hover:bg-destructive/10 aria-invalid:ring-destructive dark:aria-invalid:ring-destructive',
|
|
18
|
+
'destructive-solid':
|
|
19
|
+
'bg-destructive text-white shadow-xs hover:bg-destructive/90 aria-invalid:ring-destructive dark:aria-invalid:ring-destructive',
|
|
14
20
|
'destructive-outline':
|
|
15
|
-
'border border-destructive
|
|
21
|
+
'border border-destructive bg-background text-destructive shadow-xs hover:bg-destructive/10',
|
|
16
22
|
'destructive-soft':
|
|
17
|
-
'bg-destructive/10 text-destructive hover:bg-destructive/15
|
|
23
|
+
'bg-destructive/10 text-destructive hover:bg-destructive/15',
|
|
18
24
|
'destructive-ghost':
|
|
19
|
-
'text-destructive hover:bg-destructive/10
|
|
25
|
+
'text-destructive hover:bg-destructive/10',
|
|
20
26
|
outline:
|
|
21
27
|
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
|
22
28
|
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { createContext, useContext } from 'react'
|
|
5
5
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
6
|
+
import { THEME_STORAGE_KEY } from './theme-init-script'
|
|
6
7
|
|
|
7
8
|
const logger = createLogger('ui').child({ component: 'ThemeProvider' })
|
|
8
9
|
|
|
@@ -16,8 +17,6 @@ type ThemeContextValue = {
|
|
|
16
17
|
|
|
17
18
|
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined)
|
|
18
19
|
|
|
19
|
-
const THEME_STORAGE_KEY = 'om-theme'
|
|
20
|
-
|
|
21
20
|
function getSystemTheme(): 'light' | 'dark' {
|
|
22
21
|
if (typeof window === 'undefined') return 'light'
|
|
23
22
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { THEME_INIT_SCRIPT, THEME_STORAGE_KEY } from '../theme-init-script'
|
|
2
|
+
|
|
3
|
+
function mockSystemTheme(prefersDark: boolean) {
|
|
4
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
5
|
+
writable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
value: (query: string) => ({
|
|
8
|
+
matches: query.includes('prefers-color-scheme: dark') ? prefersDark : false,
|
|
9
|
+
media: query,
|
|
10
|
+
addEventListener: () => {},
|
|
11
|
+
removeEventListener: () => {},
|
|
12
|
+
}),
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function runThemeInitScript() {
|
|
17
|
+
new Function(THEME_INIT_SCRIPT)()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('THEME_INIT_SCRIPT', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
document.documentElement.classList.remove('dark')
|
|
23
|
+
window.localStorage.clear()
|
|
24
|
+
jest.restoreAllMocks()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('applies the dark class when the stored preference is dark', () => {
|
|
28
|
+
window.localStorage.setItem(THEME_STORAGE_KEY, 'dark')
|
|
29
|
+
mockSystemTheme(false)
|
|
30
|
+
|
|
31
|
+
runThemeInitScript()
|
|
32
|
+
|
|
33
|
+
expect(document.documentElement.classList.contains('dark')).toBe(true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('keeps the light theme when the stored preference is light, even if the system prefers dark', () => {
|
|
37
|
+
window.localStorage.setItem(THEME_STORAGE_KEY, 'light')
|
|
38
|
+
mockSystemTheme(true)
|
|
39
|
+
|
|
40
|
+
runThemeInitScript()
|
|
41
|
+
|
|
42
|
+
expect(document.documentElement.classList.contains('dark')).toBe(false)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('falls back to the system preference when nothing is stored', () => {
|
|
46
|
+
mockSystemTheme(true)
|
|
47
|
+
|
|
48
|
+
runThemeInitScript()
|
|
49
|
+
|
|
50
|
+
expect(document.documentElement.classList.contains('dark')).toBe(true)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('stays on the light theme when nothing is stored and the system prefers light', () => {
|
|
54
|
+
mockSystemTheme(false)
|
|
55
|
+
|
|
56
|
+
runThemeInitScript()
|
|
57
|
+
|
|
58
|
+
expect(document.documentElement.classList.contains('dark')).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('degrades gracefully when localStorage is unavailable', () => {
|
|
62
|
+
jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
|
|
63
|
+
throw new Error('localStorage is blocked')
|
|
64
|
+
})
|
|
65
|
+
mockSystemTheme(true)
|
|
66
|
+
|
|
67
|
+
expect(() => runThemeInitScript()).not.toThrow()
|
|
68
|
+
expect(document.documentElement.classList.contains('dark')).toBe(false)
|
|
69
|
+
})
|
|
70
|
+
})
|
package/src/theme/index.ts
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const THEME_STORAGE_KEY = 'om-theme'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inline source for the root layout's theme initializer.
|
|
5
|
+
*
|
|
6
|
+
* It MUST be rendered as a plain, non-deferred `<script>` element so the browser
|
|
7
|
+
* executes it while parsing the document, before the first paint. Deferring it —
|
|
8
|
+
* for example through `next/script` with `strategy="beforeInteractive"`, which in
|
|
9
|
+
* the App Router only queues the code onto `self.__next_s` for the client runtime
|
|
10
|
+
* to replay once hydration starts — makes the page paint in light theme first and
|
|
11
|
+
* flash to dark afterwards.
|
|
12
|
+
*/
|
|
13
|
+
export const THEME_INIT_SCRIPT = `
|
|
14
|
+
(function () {
|
|
15
|
+
try {
|
|
16
|
+
var stored = localStorage.getItem(${JSON.stringify(THEME_STORAGE_KEY)});
|
|
17
|
+
var theme = stored === 'dark' ? 'dark'
|
|
18
|
+
: stored === 'light' ? 'light'
|
|
19
|
+
: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
20
|
+
if (theme === 'dark') document.documentElement.classList.add('dark');
|
|
21
|
+
} catch (error) {}
|
|
22
|
+
})();
|
|
23
|
+
`
|