@carlonicora/nextjs-jsonapi 4.1.1 → 4.2.0
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/dist/{AssistantMessageInterface-CubxuBIJ.d.ts → AssistantMessageInterface-DHJJO_Z_.d.ts} +1 -1
- package/dist/{AssistantMessageInterface-sHjI2zPN.d.mts → AssistantMessageInterface-S7oyERTT.d.mts} +1 -1
- package/dist/{BlockNoteEditor-VOYKZOP5.mjs → BlockNoteEditor-HXAHDJA7.mjs} +34 -27
- package/dist/BlockNoteEditor-HXAHDJA7.mjs.map +1 -0
- package/dist/{BlockNoteEditor-BENYTTCZ.js → BlockNoteEditor-LLCZDFLZ.js} +50 -43
- package/dist/BlockNoteEditor-LLCZDFLZ.js.map +1 -0
- package/dist/{BlockNoteViewer-USLR4Y7C.js → BlockNoteViewer-77CCTC2U.js} +3 -3
- package/dist/{BlockNoteViewer-USLR4Y7C.js.map → BlockNoteViewer-77CCTC2U.js.map} +1 -1
- package/dist/{BlockNoteViewer-Y2ZM4ESA.mjs → BlockNoteViewer-MDPO6BXN.mjs} +2 -2
- package/dist/{auth.interface-Df4vXbNz.d.mts → auth.interface-CeFCz9NU.d.mts} +1 -1
- package/dist/{auth.interface-BzI4onI_.d.ts → auth.interface-D1R7VVky.d.ts} +1 -1
- package/dist/billing/index.d.mts +40 -6
- package/dist/billing/index.d.ts +40 -6
- package/dist/billing/index.js +806 -774
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +494 -462
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-6ISF5KNQ.mjs → chunk-2VXDZRYY.mjs} +1103 -816
- package/dist/chunk-2VXDZRYY.mjs.map +1 -0
- package/dist/{chunk-4BZJ4LAB.js → chunk-4ORLPEK7.js} +136 -104
- package/dist/chunk-4ORLPEK7.js.map +1 -0
- package/dist/{chunk-ZZ6E33FO.mjs → chunk-6DRSSW5O.mjs} +36 -4
- package/dist/chunk-6DRSSW5O.mjs.map +1 -0
- package/dist/{chunk-KS6WZP2E.js → chunk-6SMJRAE3.js} +4 -4
- package/dist/{chunk-KS6WZP2E.js.map → chunk-6SMJRAE3.js.map} +1 -1
- package/dist/{chunk-WQ77CUUZ.js → chunk-GPXYQMOZ.js} +7 -7
- package/dist/{chunk-WQ77CUUZ.js.map → chunk-GPXYQMOZ.js.map} +1 -1
- package/dist/{chunk-46S3KPLU.mjs → chunk-ILFFC6BG.mjs} +2 -2
- package/dist/{chunk-HZVVRIJQ.mjs → chunk-IUS2RHCX.mjs} +2 -2
- package/dist/{chunk-F7RNBGHS.js → chunk-KJMMBUOA.js} +3 -3
- package/dist/{chunk-F7RNBGHS.js.map → chunk-KJMMBUOA.js.map} +1 -1
- package/dist/{chunk-ZO3QQ5CQ.mjs → chunk-MQDR6MEE.mjs} +2 -2
- package/dist/{chunk-ORU7KLCY.js → chunk-VDVIUDY5.js} +1445 -1158
- package/dist/chunk-VDVIUDY5.js.map +1 -0
- package/dist/client/index.d.mts +20 -6
- package/dist/client/index.d.ts +20 -6
- package/dist/client/index.js +6 -4
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +5 -3
- package/dist/components/index.d.mts +59 -21
- package/dist/components/index.d.ts +59 -21
- package/dist/components/index.js +4 -4
- package/dist/components/index.mjs +3 -3
- package/dist/{config-CEAq4Ozy.d.mts → config-BP2vyddw.d.mts} +5 -0
- package/dist/{config-DydDHCfb.d.ts → config-BhW2QrRD.d.ts} +5 -0
- package/dist/contexts/index.d.mts +4 -4
- package/dist/contexts/index.d.ts +4 -4
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +17 -9
- package/dist/core/index.d.ts +17 -9
- package/dist/core/index.js +4 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +3 -1
- package/dist/features/analytics/index.d.mts +1 -1
- package/dist/features/analytics/index.d.ts +1 -1
- package/dist/features/analytics/index.js +122 -122
- package/dist/features/analytics/index.mjs +4 -4
- package/dist/features/analytics/server.js +4 -4
- package/dist/features/analytics/server.mjs +2 -2
- package/dist/features/help/index.d.mts +1 -1
- package/dist/features/help/index.d.ts +1 -1
- package/dist/features/help/index.js +42 -40
- package/dist/features/help/index.js.map +1 -1
- package/dist/features/help/index.mjs +8 -6
- package/dist/features/help/index.mjs.map +1 -1
- package/dist/features/tokenusage/index.js +125 -125
- package/dist/features/tokenusage/index.mjs +4 -4
- package/dist/index.d.mts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4 -2
- package/dist/{notification.interface-P_pNfzZQ.d.mts → notification.interface-BB6GPpZY.d.mts} +1 -1
- package/dist/{notification.interface-BNCvnVCW.d.ts → notification.interface-CmAGrijJ.d.ts} +1 -1
- package/dist/{s3.service-CbUa6a-8.d.ts → s3.service-C-4YpEMu.d.ts} +7 -3
- package/dist/{s3.service-DAp8FbuG.d.mts → s3.service-dVj-nxMl.d.mts} +7 -3
- package/dist/server/index.d.mts +4 -4
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +2 -2
- package/dist/{useSocket-BHVIyigI.d.ts → useSocket-B0H9pmmH.d.ts} +1 -1
- package/dist/{useSocket-B9gTbuCj.d.mts → useSocket-B5RQb6v2.d.mts} +1 -1
- package/dist/{user.interface-CrmwJLYW.d.ts → user.interface-C0ZdsBZj.d.ts} +2 -0
- package/dist/{user.interface-CSaGWFbg.d.mts → user.interface-CIWGlbqM.d.mts} +2 -0
- package/package.json +1 -1
- package/scripts/vitest-slow-tests.reporter.spec.ts +54 -0
- package/src/components/containers/RoundPageContainer.tsx +3 -1
- package/src/components/containers/RoundPageContainerTitle.tsx +5 -2
- package/src/components/editors/BlockNoteEditor.tsx +31 -15
- package/src/components/editors/BlockNoteEditorAiActions.tsx +11 -7
- package/src/components/editors/__tests__/BlockNoteEditorAiActions.test.ts +10 -3
- package/src/components/errors/ErrorDetails.tsx +5 -2
- package/src/components/forms/CommonAssociationForm.tsx +1 -1
- package/src/components/forms/EntityMultiSelector.tsx +6 -2
- package/src/components/forms/EntitySelector.tsx +4 -1
- package/src/components/forms/FileUploader.tsx +3 -2
- package/src/components/forms/MultiFileUploader.tsx +3 -1
- package/src/components/navigations/Breadcrumb.tsx +3 -2
- package/src/components/navigations/Header.tsx +3 -1
- package/src/components/navigations/MobileNavigationBar.tsx +3 -1
- package/src/components/navigations/__tests__/Header.spec.tsx +2 -2
- package/src/components/tables/cells/cell.id.tsx +13 -2
- package/src/core/abstracts/AbstractService.ts +3 -3
- package/src/core/abstracts/ClientAbstractService.ts +3 -2
- package/src/features/assistant-action/components/ApprovalActionCard.tsx +74 -5
- package/src/features/assistant-action/components/__tests__/ApprovalActionCard.spec.tsx +81 -0
- package/src/features/assistant-message/components/parts/RelevanceMeter.tsx +4 -1
- package/src/features/assistant-message/components/parts/tabs/UsersTab.tsx +2 -1
- package/src/features/auth/components/buttons/GoogleSignInButton.tsx +4 -1
- package/src/features/auth/components/details/LandingComponent.tsx +1 -1
- package/src/features/auth/components/forms/ForgotPassword.tsx +1 -1
- package/src/features/auth/components/forms/Login.tsx +1 -1
- package/src/features/auth/components/forms/Register.tsx +1 -1
- package/src/features/auth/components/forms/ResetPassword.tsx +2 -2
- package/src/features/auth/components/two-factor/BackupCodesDialog.tsx +1 -1
- package/src/features/auth/components/two-factor/PasskeyButton.tsx +1 -1
- package/src/features/billing/components/cards/BillingUsageSummaryCard.tsx +21 -11
- package/src/features/billing/components/cards/CustomerInfoCard.tsx +16 -13
- package/src/features/billing/components/cards/InvoicesSummaryCard.tsx +33 -21
- package/src/features/billing/components/cards/PaymentMethodSummaryCard.tsx +20 -10
- package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +35 -22
- package/src/features/billing/components/containers/BillingDashboardContainer.tsx +24 -24
- package/src/features/billing/components/utils/__tests__/billing-formatters.spec.ts +63 -0
- package/src/features/billing/components/utils/currency.ts +67 -10
- package/src/features/billing/components/utils/date.ts +9 -4
- package/src/features/billing/components/utils/format-options.ts +20 -0
- package/src/features/billing/components/utils/index.ts +3 -1
- package/src/features/billing/components/utils/useBillingFormatOptions.ts +11 -0
- package/src/features/billing/components/widgets/BillingAlertBanner.tsx +8 -9
- package/src/features/billing/contexts/PriceContext.tsx +18 -6
- package/src/features/billing/i18n-keys.ts +212 -0
- package/src/features/billing/stripe-customer/components/containers/PaymentMethodsContainer.tsx +8 -8
- package/src/features/billing/stripe-customer/components/details/PaymentMethodCard.tsx +11 -9
- package/src/features/billing/stripe-customer/components/forms/PaymentMethodEditor.tsx +4 -4
- package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +9 -7
- package/src/features/billing/stripe-invoice/components/containers/InvoicesContainer.tsx +10 -8
- package/src/features/billing/stripe-invoice/components/details/InvoiceDetails.tsx +34 -29
- package/src/features/billing/stripe-invoice/components/lists/InvoicesList.tsx +12 -9
- package/src/features/billing/stripe-invoice/components/widgets/InvoiceStatusBadge.tsx +9 -6
- package/src/features/billing/stripe-price/components/details/PriceDetails.tsx +4 -3
- package/src/features/billing/stripe-price/components/forms/PriceArchiver.tsx +3 -1
- package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +3 -1
- package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +6 -5
- package/src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx +12 -7
- package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.service.ts +3 -1
- package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +10 -6
- package/src/features/billing/stripe-subscription/components/details/SubscriptionDetails.tsx +44 -29
- package/src/features/billing/stripe-subscription/components/forms/CancelSubscriptionDialog.tsx +14 -11
- package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +20 -17
- package/src/features/billing/stripe-subscription/components/widgets/IntervalToggle.tsx +4 -2
- package/src/features/billing/stripe-subscription/components/widgets/PricingCard.tsx +16 -7
- package/src/features/billing/stripe-subscription/components/widgets/ProductPricingList.tsx +3 -1
- package/src/features/billing/stripe-subscription/components/widgets/ProductPricingRow.tsx +3 -1
- package/src/features/billing/stripe-subscription/components/widgets/ProrationPreview.tsx +12 -7
- package/src/features/billing/stripe-subscription/components/widgets/SubscriptionConfirmation.tsx +11 -8
- package/src/features/billing/stripe-subscription/components/widgets/SubscriptionStatusBadge.tsx +13 -10
- package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +8 -6
- package/src/features/billing/stripe-subscription/components/wizards/WizardProgressIndicator.tsx +7 -4
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +4 -2
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepPlanSelection.tsx +3 -1
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +37 -28
- package/src/features/billing/stripe-subscription/hooks/useConfirmSubscriptionPayment.ts +9 -7
- package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +20 -9
- package/src/features/billing/stripe-usage/components/containers/UsageContainer.tsx +6 -6
- package/src/features/billing/stripe-usage/components/details/UsageSummaryCard.tsx +24 -9
- package/src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx +16 -12
- package/src/features/company/components/details/TokenStatusIndicator.tsx +7 -5
- package/src/features/company/components/forms/CompanyEditor.tsx +2 -2
- package/src/features/help/components/HelpArticleBody.tsx +1 -1
- package/src/features/help/components/HelpHeader.tsx +1 -1
- package/src/features/help/components/HelpSideNav.tsx +1 -1
- package/src/features/help/i18n-keys.ts +2 -0
- package/src/features/notification/components/common/NotificationErrorBoundary.tsx +17 -12
- package/src/features/notification/components/containers/NotificationsListContainer.tsx +4 -2
- package/src/features/notification/components/notifications/Notification.tsx +6 -3
- package/src/features/notification/contexts/NotificationContext.tsx +9 -4
- package/src/features/notification/contexts/__tests__/NotificationContext.spec.tsx +13 -0
- package/src/features/notification/hooks/useNotificationTableStructure.tsx +4 -2
- package/src/features/oauth/components/OAuthClientCard.tsx +13 -7
- package/src/features/oauth/components/OAuthClientDetail.tsx +34 -30
- package/src/features/oauth/components/OAuthClientForm.tsx +24 -24
- package/src/features/oauth/components/OAuthClientList.tsx +10 -6
- package/src/features/oauth/components/OAuthClientSecretDisplay.tsx +10 -8
- package/src/features/oauth/components/OAuthRedirectUriInput.tsx +9 -13
- package/src/features/oauth/components/OAuthScopeSelector.tsx +15 -6
- package/src/features/oauth/components/__tests__/OAuthClientForm.spec.tsx +11 -9
- package/src/features/oauth/components/consent/OAuthScopeList.tsx +4 -2
- package/src/features/oauth/hooks/__tests__/useOAuthScopeText.spec.ts +30 -0
- package/src/features/oauth/hooks/index.ts +1 -0
- package/src/features/oauth/hooks/useOAuthClient.ts +11 -5
- package/src/features/oauth/hooks/useOAuthClients.ts +9 -3
- package/src/features/oauth/hooks/useOAuthConsent.ts +11 -5
- package/src/features/oauth/hooks/useOAuthScopeText.ts +33 -0
- package/src/features/rbac/components/RbacByRoleContainer.tsx +7 -6
- package/src/features/rbac/components/RbacContainer.tsx +7 -6
- package/src/features/rbac/contexts/RbacContext.tsx +3 -3
- package/src/features/user/components/containers/UserContainer.tsx +3 -1
- package/src/features/user/data/__tests__/user.test.ts +66 -0
- package/src/features/user/data/user.interface.ts +2 -0
- package/src/features/user/data/user.service.ts +9 -0
- package/src/features/user/data/user.ts +7 -0
- package/src/features/waitlist/components/forms/WaitlistForm.tsx +1 -1
- package/src/features/waitlist/components/lists/WaitlistList.tsx +1 -1
- package/src/i18n/__tests__/config.test.ts +21 -1
- package/src/i18n/config.ts +13 -0
- package/src/i18n/index.ts +1 -0
- package/src/shadcnui/custom/multi-select.tsx +47 -14
- package/src/shadcnui/custom/multiple-selector.tsx +14 -2
- package/src/shadcnui/ui/__tests__/use-ui-text.test.tsx +76 -0
- package/src/shadcnui/ui/breadcrumb.tsx +9 -2
- package/src/shadcnui/ui/carousel.tsx +15 -4
- package/src/shadcnui/ui/dialog.tsx +15 -2
- package/src/shadcnui/ui/sheet.tsx +6 -1
- package/src/shadcnui/ui/sidebar.tsx +33 -7
- package/src/shadcnui/ui/spinner.tsx +6 -1
- package/src/shadcnui/ui/use-ui-text.ts +49 -0
- package/src/utils/__tests__/date-formatter.test.ts +24 -1
- package/src/utils/date-formatter.ts +16 -0
- package/src/utils/index.ts +1 -1
- package/dist/BlockNoteEditor-BENYTTCZ.js.map +0 -1
- package/dist/BlockNoteEditor-VOYKZOP5.mjs.map +0 -1
- package/dist/chunk-4BZJ4LAB.js.map +0 -1
- package/dist/chunk-6ISF5KNQ.mjs.map +0 -1
- package/dist/chunk-ORU7KLCY.js.map +0 -1
- package/dist/chunk-ZZ6E33FO.mjs.map +0 -1
- /package/dist/{BlockNoteViewer-Y2ZM4ESA.mjs.map → BlockNoteViewer-MDPO6BXN.mjs.map} +0 -0
- /package/dist/{chunk-46S3KPLU.mjs.map → chunk-ILFFC6BG.mjs.map} +0 -0
- /package/dist/{chunk-HZVVRIJQ.mjs.map → chunk-IUS2RHCX.mjs.map} +0 -0
- /package/dist/{chunk-ZO3QQ5CQ.mjs.map → chunk-MQDR6MEE.mjs.map} +0 -0
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "../ui/command";
|
|
19
19
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
|
20
20
|
import { Separator } from "../ui/separator";
|
|
21
|
+
import { useUiText } from "../ui/use-ui-text";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Variants for the multi-select component to handle different styles.
|
|
@@ -91,10 +92,25 @@ interface MultiSelectProps
|
|
|
91
92
|
|
|
92
93
|
/**
|
|
93
94
|
* Placeholder text to be displayed when no values are selected.
|
|
94
|
-
* Optional, defaults to "Select options".
|
|
95
|
+
* Optional, defaults to `ui.labels.select_options`, else "Select options".
|
|
95
96
|
*/
|
|
96
97
|
placeholder?: string;
|
|
97
98
|
|
|
99
|
+
/** Placeholder of the search input. Defaults to `ui.search.placeholder_global`, else "Search...". */
|
|
100
|
+
searchPlaceholder?: string;
|
|
101
|
+
|
|
102
|
+
/** Text of the select-all row. Defaults to `ui.labels.select_all`, else "(Select All)". */
|
|
103
|
+
selectAllLabel?: string;
|
|
104
|
+
|
|
105
|
+
/** Text of the clear row. Defaults to `ui.buttons.clear`, else "Clear". */
|
|
106
|
+
clearLabel?: string;
|
|
107
|
+
|
|
108
|
+
/** Text of the close row. Defaults to `ui.labels.close`, else "Close". */
|
|
109
|
+
closeLabel?: string;
|
|
110
|
+
|
|
111
|
+
/** Text of the overflow badge. Defaults to `ui.labels.more_count`, else "+ {count} more". */
|
|
112
|
+
formatMoreLabel?: (count: number) => string;
|
|
113
|
+
|
|
98
114
|
/**
|
|
99
115
|
* Animation duration in seconds for the visual effects (e.g., bouncing badges).
|
|
100
116
|
* Optional, defaults to 0 (no animation).
|
|
@@ -134,13 +150,13 @@ interface MultiSelectProps
|
|
|
134
150
|
|
|
135
151
|
/**
|
|
136
152
|
* Text to display when loading.
|
|
137
|
-
* Optional, defaults to "Searching...".
|
|
153
|
+
* Optional, defaults to `ui.labels.searching`, else "Searching...".
|
|
138
154
|
*/
|
|
139
155
|
loadingText?: string;
|
|
140
156
|
|
|
141
157
|
/**
|
|
142
158
|
* Text to display when no results are found.
|
|
143
|
-
* Optional, defaults to "No results found.".
|
|
159
|
+
* Optional, defaults to `ui.search.no_results_found`, else "No results found.".
|
|
144
160
|
*/
|
|
145
161
|
emptyText?: string;
|
|
146
162
|
|
|
@@ -166,21 +182,38 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
|
|
166
182
|
variant,
|
|
167
183
|
defaultValue = [],
|
|
168
184
|
value,
|
|
169
|
-
placeholder
|
|
185
|
+
placeholder: placeholderProp,
|
|
170
186
|
animation = 0,
|
|
171
187
|
maxCount = 3,
|
|
172
188
|
modalPopover = false,
|
|
173
189
|
className,
|
|
174
190
|
onSearchChange,
|
|
175
191
|
loading = false,
|
|
176
|
-
loadingText
|
|
177
|
-
emptyText
|
|
192
|
+
loadingText: loadingTextProp,
|
|
193
|
+
emptyText: emptyTextProp,
|
|
194
|
+
searchPlaceholder: searchPlaceholderProp,
|
|
195
|
+
selectAllLabel: selectAllLabelProp,
|
|
196
|
+
clearLabel: clearLabelProp,
|
|
197
|
+
closeLabel: closeLabelProp,
|
|
198
|
+
formatMoreLabel,
|
|
178
199
|
hideSelectAll = false,
|
|
179
200
|
isOptionSelected,
|
|
180
201
|
...props
|
|
181
202
|
},
|
|
182
203
|
_ref,
|
|
183
204
|
) => {
|
|
205
|
+
// Chrome text: an explicit prop wins, else the app's translation, else English.
|
|
206
|
+
const uiText = useUiText();
|
|
207
|
+
const placeholder = placeholderProp ?? uiText("ui.labels.select_options", "Select options");
|
|
208
|
+
const loadingText = loadingTextProp ?? uiText("ui.labels.searching", "Searching...");
|
|
209
|
+
const emptyText = emptyTextProp ?? uiText("ui.search.no_results_found", "No results found.");
|
|
210
|
+
const searchPlaceholder = searchPlaceholderProp ?? uiText("ui.search.placeholder_global", "Search...");
|
|
211
|
+
const selectAllLabel = selectAllLabelProp ?? uiText("ui.labels.select_all", "(Select All)");
|
|
212
|
+
const clearLabel = clearLabelProp ?? uiText("ui.buttons.clear", "Clear");
|
|
213
|
+
const closeLabel = closeLabelProp ?? uiText("ui.labels.close", "Close");
|
|
214
|
+
const moreLabel = (count: number) =>
|
|
215
|
+
formatMoreLabel ? formatMoreLabel(count) : uiText("ui.labels.more_count", "+ {count} more", { count });
|
|
216
|
+
|
|
184
217
|
// Using internal state only if value prop is not provided (uncontrolled mode)
|
|
185
218
|
const [internalSelectedValues, setInternalSelectedValues] = React.useState<string[]>(defaultValue);
|
|
186
219
|
|
|
@@ -331,7 +364,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
|
|
331
364
|
)}
|
|
332
365
|
style={{ animationDuration: `${animation}s` }}
|
|
333
366
|
>
|
|
334
|
-
{
|
|
367
|
+
{moreLabel(selectedValues.length - maxCount)}
|
|
335
368
|
<XCircle
|
|
336
369
|
data-remove-button
|
|
337
370
|
className="ms-2 h-4 w-4 cursor-pointer"
|
|
@@ -381,7 +414,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
|
|
381
414
|
<Command filter={commandFilterByKeywords}>
|
|
382
415
|
<CommandInput
|
|
383
416
|
autoFocus
|
|
384
|
-
placeholder=
|
|
417
|
+
placeholder={searchPlaceholder}
|
|
385
418
|
onKeyDown={handleInputKeyDown}
|
|
386
419
|
onValueChange={onSearchChange}
|
|
387
420
|
/>
|
|
@@ -397,7 +430,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
|
|
397
430
|
<CommandItem
|
|
398
431
|
key="all"
|
|
399
432
|
value={SELECT_ALL_VALUE}
|
|
400
|
-
keywords={[
|
|
433
|
+
keywords={[selectAllLabel]}
|
|
401
434
|
onSelect={toggleAll}
|
|
402
435
|
className="cursor-pointer hover:bg-muted data-selected:hover:bg-muted bg-transparent data-selected:bg-transparent"
|
|
403
436
|
>
|
|
@@ -411,7 +444,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
|
|
411
444
|
>
|
|
412
445
|
<CheckIcon className="h-4 w-4" />
|
|
413
446
|
</div>
|
|
414
|
-
<span>
|
|
447
|
+
<span>{selectAllLabel}</span>
|
|
415
448
|
</CommandItem>
|
|
416
449
|
)}
|
|
417
450
|
{options.map((option) => {
|
|
@@ -449,22 +482,22 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
|
|
449
482
|
<>
|
|
450
483
|
<CommandItem
|
|
451
484
|
value={CLEAR_VALUE}
|
|
452
|
-
keywords={[
|
|
485
|
+
keywords={[clearLabel]}
|
|
453
486
|
onSelect={handleClear}
|
|
454
487
|
className="flex-1 cursor-pointer justify-center"
|
|
455
488
|
>
|
|
456
|
-
|
|
489
|
+
{clearLabel}
|
|
457
490
|
</CommandItem>
|
|
458
491
|
<Separator orientation="vertical" className="flex h-full min-h-6" />
|
|
459
492
|
</>
|
|
460
493
|
)}
|
|
461
494
|
<CommandItem
|
|
462
495
|
value={CLOSE_VALUE}
|
|
463
|
-
keywords={[
|
|
496
|
+
keywords={[closeLabel]}
|
|
464
497
|
onSelect={() => setIsPopoverOpen(false)}
|
|
465
498
|
className="max-w-full flex-1 cursor-pointer justify-center hover:bg-muted data-selected:hover:bg-muted bg-transparent data-selected:bg-transparent"
|
|
466
499
|
>
|
|
467
|
-
|
|
500
|
+
{closeLabel}
|
|
468
501
|
</CommandItem>
|
|
469
502
|
</div>
|
|
470
503
|
</CommandGroup>
|
|
@@ -9,6 +9,7 @@ import { forwardRef, useEffect } from "react";
|
|
|
9
9
|
import { cn } from "../../utils/cn";
|
|
10
10
|
import { Badge } from "../ui/badge";
|
|
11
11
|
import { Command, CommandGroup, CommandItem, CommandList, commandFilterByKeywords } from "../ui/command";
|
|
12
|
+
import { useUiText } from "../ui/use-ui-text";
|
|
12
13
|
|
|
13
14
|
export interface Option {
|
|
14
15
|
value: string;
|
|
@@ -58,6 +59,10 @@ export interface MultipleSelectorProps {
|
|
|
58
59
|
onMaxSelected?: (maxLimit: number) => void;
|
|
59
60
|
/** Hide the placeholder when there are options selected. */
|
|
60
61
|
hidePlaceholderWhenSelected?: boolean;
|
|
62
|
+
/** Text of the creatable row. Defaults to `ui.labels.create_option`, else `Create "{value}"`. */
|
|
63
|
+
formatCreateLabel?: (value: string) => string;
|
|
64
|
+
/** Text of the overflow badge. Defaults to `ui.labels.more_count`, else "+{count} more". */
|
|
65
|
+
formatMoreLabel?: (count: number) => string;
|
|
61
66
|
disabled?: boolean;
|
|
62
67
|
/** Group the options base on provided key. */
|
|
63
68
|
groupBy?: string;
|
|
@@ -204,6 +209,8 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|
|
204
209
|
maxSelected = Number.MAX_SAFE_INTEGER,
|
|
205
210
|
onMaxSelected,
|
|
206
211
|
hidePlaceholderWhenSelected,
|
|
212
|
+
formatCreateLabel,
|
|
213
|
+
formatMoreLabel,
|
|
207
214
|
disabled,
|
|
208
215
|
groupBy,
|
|
209
216
|
className,
|
|
@@ -220,6 +227,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|
|
220
227
|
}: MultipleSelectorProps,
|
|
221
228
|
ref: React.Ref<MultipleSelectorRef>,
|
|
222
229
|
) => {
|
|
230
|
+
const uiText = useUiText();
|
|
223
231
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
224
232
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
225
233
|
const [open, setOpen] = React.useState(false);
|
|
@@ -431,7 +439,9 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|
|
431
439
|
onChange?.(newOptions);
|
|
432
440
|
}}
|
|
433
441
|
>
|
|
434
|
-
{
|
|
442
|
+
{formatCreateLabel
|
|
443
|
+
? formatCreateLabel(inputValue)
|
|
444
|
+
: uiText("ui.labels.create_option", 'Create "{value}"', { value: inputValue })}
|
|
435
445
|
</CommandItem>
|
|
436
446
|
);
|
|
437
447
|
|
|
@@ -547,7 +557,9 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|
|
547
557
|
})}
|
|
548
558
|
{hiddenCount > 0 && (
|
|
549
559
|
<Badge className={cn("bg-transparent text-foreground hover:bg-transparent", badgeClassName)}>
|
|
550
|
-
{
|
|
560
|
+
{formatMoreLabel
|
|
561
|
+
? formatMoreLabel(hiddenCount)
|
|
562
|
+
: uiText("ui.labels.more_count", "+{count} more", { count: hiddenCount })}
|
|
551
563
|
</Badge>
|
|
552
564
|
)}
|
|
553
565
|
<CommandPrimitive.Input
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { render, renderHook, screen } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { configureI18n, type I18nConfig } from "../../../i18n/config";
|
|
4
|
+
import { Spinner } from "../spinner";
|
|
5
|
+
import { useUiText } from "../use-ui-text";
|
|
6
|
+
|
|
7
|
+
const catalogue: Record<string, string> = {
|
|
8
|
+
"ui.labels.loading": "Caricamento",
|
|
9
|
+
"ui.labels.more_count": "+{count} altri",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** A next-intl-shaped `t`: `has` probes, missing keys come back as the key path. */
|
|
13
|
+
function useCatalogueTranslations() {
|
|
14
|
+
const t = (key: string, values?: Record<string, unknown>) =>
|
|
15
|
+
(catalogue[key] ?? key).replace(/\{(\w+)\}/g, (match, name: string) =>
|
|
16
|
+
values && name in values ? String(values[name]) : match,
|
|
17
|
+
);
|
|
18
|
+
(t as unknown as { has: (key: string) => boolean }).has = (key: string) => key in catalogue;
|
|
19
|
+
return t;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function configure(useTranslations: I18nConfig["useTranslations"]) {
|
|
23
|
+
configureI18n({ useTranslations } as unknown as I18nConfig);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
configureI18n(null as unknown as I18nConfig);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("useUiText", () => {
|
|
31
|
+
it("returns the English fallback when i18n is not configured", () => {
|
|
32
|
+
const { result } = renderHook(() => useUiText());
|
|
33
|
+
expect(result.current("ui.labels.close", "Close")).toBe("Close");
|
|
34
|
+
expect(result.current("ui.labels.more_count", "+ {count} more", { count: 2 })).toBe("+ 2 more");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns the app's translation when the key exists", () => {
|
|
38
|
+
configure(useCatalogueTranslations);
|
|
39
|
+
const { result } = renderHook(() => useUiText());
|
|
40
|
+
expect(result.current("ui.labels.loading", "Loading")).toBe("Caricamento");
|
|
41
|
+
expect(result.current("ui.labels.more_count", "+ {count} more", { count: 3 })).toBe("+3 altri");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns the English fallback when the app lacks the key", () => {
|
|
45
|
+
configure(useCatalogueTranslations);
|
|
46
|
+
const { result } = renderHook(() => useUiText());
|
|
47
|
+
expect(result.current("ui.labels.close", "Close")).toBe("Close");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns the English fallback when a has-less t hands back the key path or throws", () => {
|
|
51
|
+
configure(() => (key: string) => key);
|
|
52
|
+
expect(renderHook(() => useUiText()).result.current("ui.labels.close", "Close")).toBe("Close");
|
|
53
|
+
|
|
54
|
+
configure(() => () => {
|
|
55
|
+
throw new Error("missing");
|
|
56
|
+
});
|
|
57
|
+
expect(renderHook(() => useUiText()).result.current("ui.labels.close", "Close")).toBe("Close");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("Spinner label", () => {
|
|
62
|
+
it("announces the translated label, and an explicit aria-label still wins", () => {
|
|
63
|
+
configure(useCatalogueTranslations);
|
|
64
|
+
const { unmount } = render(<Spinner />);
|
|
65
|
+
expect(screen.getByRole("status")).toHaveAttribute("aria-label", "Caricamento");
|
|
66
|
+
unmount();
|
|
67
|
+
|
|
68
|
+
render(<Spinner aria-label="Saving" />);
|
|
69
|
+
expect(screen.getByRole("status")).toHaveAttribute("aria-label", "Saving");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("keeps the English label when i18n is not configured", () => {
|
|
73
|
+
render(<Spinner />);
|
|
74
|
+
expect(screen.getByRole("status")).toHaveAttribute("aria-label", "Loading");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -66,7 +66,14 @@ function BreadcrumbSeparator({ children, className, ...props }: React.ComponentP
|
|
|
66
66
|
);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function BreadcrumbEllipsis({
|
|
69
|
+
function BreadcrumbEllipsis({
|
|
70
|
+
className,
|
|
71
|
+
label = "More",
|
|
72
|
+
...props
|
|
73
|
+
}: React.ComponentProps<"span"> & {
|
|
74
|
+
/** Screen-reader text. Plain prop (no lookup): this file has no "use client" and may render on the server. */
|
|
75
|
+
label?: string;
|
|
76
|
+
}) {
|
|
70
77
|
return (
|
|
71
78
|
<span
|
|
72
79
|
data-slot="breadcrumb-ellipsis"
|
|
@@ -76,7 +83,7 @@ function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<"span"
|
|
|
76
83
|
{...props}
|
|
77
84
|
>
|
|
78
85
|
<MoreHorizontalIcon />
|
|
79
|
-
<span className="sr-only">
|
|
86
|
+
<span className="sr-only">{label}</span>
|
|
80
87
|
</span>
|
|
81
88
|
);
|
|
82
89
|
}
|
|
@@ -7,6 +7,7 @@ import { cn } from "@/lib/utils";
|
|
|
7
7
|
import { Button } from "@/components/ui/button";
|
|
8
8
|
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
|
9
9
|
import { useDir } from "../../contexts/DirectionContext";
|
|
10
|
+
import { useUiText } from "./use-ui-text";
|
|
10
11
|
|
|
11
12
|
type CarouselApi = UseEmblaCarouselType[1];
|
|
12
13
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
|
@@ -162,9 +163,14 @@ function CarouselPrevious({
|
|
|
162
163
|
className,
|
|
163
164
|
variant = "outline",
|
|
164
165
|
size = "icon-sm",
|
|
166
|
+
label,
|
|
165
167
|
...props
|
|
166
|
-
}: React.ComponentProps<typeof Button>
|
|
168
|
+
}: React.ComponentProps<typeof Button> & {
|
|
169
|
+
/** Screen-reader name. Defaults to `ui.labels.previous_slide`, else "Previous slide". */
|
|
170
|
+
label?: string;
|
|
171
|
+
}) {
|
|
167
172
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
|
173
|
+
const uiText = useUiText();
|
|
168
174
|
|
|
169
175
|
return (
|
|
170
176
|
<Button
|
|
@@ -184,7 +190,7 @@ function CarouselPrevious({
|
|
|
184
190
|
{...props}
|
|
185
191
|
>
|
|
186
192
|
<ChevronLeftIcon className={orientation === "horizontal" ? "rtl:rotate-180" : undefined} />
|
|
187
|
-
<span className="sr-only">Previous slide</span>
|
|
193
|
+
<span className="sr-only">{label ?? uiText("ui.labels.previous_slide", "Previous slide")}</span>
|
|
188
194
|
</Button>
|
|
189
195
|
);
|
|
190
196
|
}
|
|
@@ -193,9 +199,14 @@ function CarouselNext({
|
|
|
193
199
|
className,
|
|
194
200
|
variant = "outline",
|
|
195
201
|
size = "icon-sm",
|
|
202
|
+
label,
|
|
196
203
|
...props
|
|
197
|
-
}: React.ComponentProps<typeof Button>
|
|
204
|
+
}: React.ComponentProps<typeof Button> & {
|
|
205
|
+
/** Screen-reader name. Defaults to `ui.labels.next_slide`, else "Next slide". */
|
|
206
|
+
label?: string;
|
|
207
|
+
}) {
|
|
198
208
|
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
|
209
|
+
const uiText = useUiText();
|
|
199
210
|
|
|
200
211
|
return (
|
|
201
212
|
<Button
|
|
@@ -215,7 +226,7 @@ function CarouselNext({
|
|
|
215
226
|
{...props}
|
|
216
227
|
>
|
|
217
228
|
<ChevronRightIcon className={orientation === "horizontal" ? "rtl:rotate-180" : undefined} />
|
|
218
|
-
<span className="sr-only">Next slide</span>
|
|
229
|
+
<span className="sr-only">{label ?? uiText("ui.labels.next_slide", "Next slide")}</span>
|
|
219
230
|
</Button>
|
|
220
231
|
);
|
|
221
232
|
}
|
|
@@ -6,6 +6,7 @@ import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
|
|
|
6
6
|
import { cn } from "@/lib/utils";
|
|
7
7
|
import { Button } from "@/components/ui/button";
|
|
8
8
|
import { XIcon } from "lucide-react";
|
|
9
|
+
import { useUiText } from "./use-ui-text";
|
|
9
10
|
|
|
10
11
|
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
|
11
12
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
|
@@ -40,10 +41,14 @@ function DialogContent({
|
|
|
40
41
|
className,
|
|
41
42
|
children,
|
|
42
43
|
showCloseButton = true,
|
|
44
|
+
closeLabel,
|
|
43
45
|
...props
|
|
44
46
|
}: DialogPrimitive.Popup.Props & {
|
|
45
47
|
showCloseButton?: boolean;
|
|
48
|
+
/** Screen-reader name of the close button. Defaults to `ui.labels.close`, else "Close". */
|
|
49
|
+
closeLabel?: string;
|
|
46
50
|
}) {
|
|
51
|
+
const uiText = useUiText();
|
|
47
52
|
return (
|
|
48
53
|
<DialogPortal>
|
|
49
54
|
<DialogOverlay />
|
|
@@ -65,7 +70,7 @@ function DialogContent({
|
|
|
65
70
|
render={<Button variant="ghost" className="absolute top-2 end-2" size="icon-sm" />}
|
|
66
71
|
>
|
|
67
72
|
<XIcon />
|
|
68
|
-
<span className="sr-only">Close</span>
|
|
73
|
+
<span className="sr-only">{closeLabel ?? uiText("ui.labels.close", "Close")}</span>
|
|
69
74
|
</DialogPrimitive.Close>
|
|
70
75
|
)}
|
|
71
76
|
</DialogPrimitive.Popup>
|
|
@@ -80,11 +85,15 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
80
85
|
function DialogFooter({
|
|
81
86
|
className,
|
|
82
87
|
showCloseButton = false,
|
|
88
|
+
closeLabel,
|
|
83
89
|
children,
|
|
84
90
|
...props
|
|
85
91
|
}: React.ComponentProps<"div"> & {
|
|
86
92
|
showCloseButton?: boolean;
|
|
93
|
+
/** Text of the footer close button. Defaults to `ui.labels.close`, else "Close". */
|
|
94
|
+
closeLabel?: string;
|
|
87
95
|
}) {
|
|
96
|
+
const uiText = useUiText();
|
|
88
97
|
return (
|
|
89
98
|
<div
|
|
90
99
|
data-slot="dialog-footer"
|
|
@@ -92,7 +101,11 @@ function DialogFooter({
|
|
|
92
101
|
{...props}
|
|
93
102
|
>
|
|
94
103
|
{children}
|
|
95
|
-
{showCloseButton &&
|
|
104
|
+
{showCloseButton && (
|
|
105
|
+
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
|
106
|
+
{closeLabel ?? uiText("ui.labels.close", "Close")}
|
|
107
|
+
</DialogPrimitive.Close>
|
|
108
|
+
)}
|
|
96
109
|
</div>
|
|
97
110
|
);
|
|
98
111
|
}
|
|
@@ -8,6 +8,7 @@ import { resolvePhysicalSide, type LogicalSide } from "@/lib/direction";
|
|
|
8
8
|
import { Button } from "@/components/ui/button";
|
|
9
9
|
import { XIcon } from "lucide-react";
|
|
10
10
|
import { useDir } from "../../contexts/DirectionContext";
|
|
11
|
+
import { useUiText } from "./use-ui-text";
|
|
11
12
|
|
|
12
13
|
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
|
|
13
14
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
|
@@ -44,12 +45,16 @@ function SheetContent({
|
|
|
44
45
|
// Logical default: identical to "right" in LTR, mirrors to the left in RTL.
|
|
45
46
|
side = "end",
|
|
46
47
|
showCloseButton = true,
|
|
48
|
+
closeLabel,
|
|
47
49
|
...props
|
|
48
50
|
}: SheetPrimitive.Popup.Props & {
|
|
49
51
|
side?: LogicalSide;
|
|
50
52
|
showCloseButton?: boolean;
|
|
53
|
+
/** Screen-reader name of the close button. Defaults to `ui.labels.close`, else "Close". */
|
|
54
|
+
closeLabel?: string;
|
|
51
55
|
}) {
|
|
52
56
|
const dir = useDir();
|
|
57
|
+
const uiText = useUiText();
|
|
53
58
|
const physicalSide = resolvePhysicalSide(side, dir);
|
|
54
59
|
|
|
55
60
|
return (
|
|
@@ -85,7 +90,7 @@ function SheetContent({
|
|
|
85
90
|
}
|
|
86
91
|
>
|
|
87
92
|
<XIcon />
|
|
88
|
-
<span className="sr-only">Close</span>
|
|
93
|
+
<span className="sr-only">{closeLabel ?? uiText("ui.labels.close", "Close")}</span>
|
|
89
94
|
</SheetPrimitive.Close>
|
|
90
95
|
)}
|
|
91
96
|
</SheetPrimitive.Popup>
|
|
@@ -16,6 +16,7 @@ import { resolvePhysicalSide } from "@/lib/direction";
|
|
|
16
16
|
import { cn } from "@/lib/utils";
|
|
17
17
|
import { PanelLeftIcon } from "lucide-react";
|
|
18
18
|
import { useDir } from "../../contexts/DirectionContext";
|
|
19
|
+
import { useUiText } from "./use-ui-text";
|
|
19
20
|
import type { SessionSnapshot } from "../../server/ServerSession";
|
|
20
21
|
import { mapThenable } from "../../utils/map-thenable";
|
|
21
22
|
import { getViewportContext } from "../../utils/use-mobile";
|
|
@@ -262,6 +263,7 @@ function Sidebar({
|
|
|
262
263
|
}) {
|
|
263
264
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
|
264
265
|
const dir = useDir();
|
|
266
|
+
const uiText = useUiText();
|
|
265
267
|
// Logical "start"/"end" is resolved once, here. Everything downstream
|
|
266
268
|
// (data-side, the positioning ternaries, the mobile SheetContent) consumes
|
|
267
269
|
// the resolved PHYSICAL side, so "left"/"right" callers are unchanged.
|
|
@@ -295,8 +297,10 @@ function Sidebar({
|
|
|
295
297
|
side={side}
|
|
296
298
|
>
|
|
297
299
|
<SheetHeader className="sr-only">
|
|
298
|
-
<SheetTitle>Sidebar</SheetTitle>
|
|
299
|
-
<SheetDescription>
|
|
300
|
+
<SheetTitle>{uiText("ui.labels.sidebar", "Sidebar")}</SheetTitle>
|
|
301
|
+
<SheetDescription>
|
|
302
|
+
{uiText("ui.labels.sidebar_description", "Displays the mobile sidebar.")}
|
|
303
|
+
</SheetDescription>
|
|
300
304
|
</SheetHeader>
|
|
301
305
|
<div className="flex h-full w-full flex-col">{children}</div>
|
|
302
306
|
</SheetContent>
|
|
@@ -352,8 +356,21 @@ function Sidebar({
|
|
|
352
356
|
);
|
|
353
357
|
}
|
|
354
358
|
|
|
355
|
-
function SidebarTrigger({
|
|
359
|
+
function SidebarTrigger({
|
|
360
|
+
className,
|
|
361
|
+
onClick,
|
|
362
|
+
label,
|
|
363
|
+
...props
|
|
364
|
+
}: React.ComponentProps<typeof Button> & {
|
|
365
|
+
/**
|
|
366
|
+
* Accessible name of the toggle. Falls back to an explicit `aria-label`, then
|
|
367
|
+
* `ui.labels.toggle_sidebar`, then "Toggle Sidebar".
|
|
368
|
+
*/
|
|
369
|
+
label?: string;
|
|
370
|
+
}) {
|
|
356
371
|
const { toggleSidebar } = useSidebar();
|
|
372
|
+
const uiText = useUiText();
|
|
373
|
+
const text = label ?? props["aria-label"] ?? uiText("ui.labels.toggle_sidebar", "Toggle Sidebar");
|
|
357
374
|
|
|
358
375
|
return (
|
|
359
376
|
<Button
|
|
@@ -369,22 +386,31 @@ function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<t
|
|
|
369
386
|
{...props}
|
|
370
387
|
>
|
|
371
388
|
<PanelLeftIcon className="rtl:rotate-180" />
|
|
372
|
-
<span className="sr-only">
|
|
389
|
+
<span className="sr-only">{text}</span>
|
|
373
390
|
</Button>
|
|
374
391
|
);
|
|
375
392
|
}
|
|
376
393
|
|
|
377
|
-
function SidebarRail({
|
|
394
|
+
function SidebarRail({
|
|
395
|
+
className,
|
|
396
|
+
label,
|
|
397
|
+
...props
|
|
398
|
+
}: React.ComponentProps<"button"> & {
|
|
399
|
+
/** Accessible name and tooltip of the rail. Defaults to `ui.labels.toggle_sidebar`, else "Toggle Sidebar". */
|
|
400
|
+
label?: string;
|
|
401
|
+
}) {
|
|
378
402
|
const { toggleSidebar } = useSidebar();
|
|
403
|
+
const uiText = useUiText();
|
|
404
|
+
const text = label ?? uiText("ui.labels.toggle_sidebar", "Toggle Sidebar");
|
|
379
405
|
|
|
380
406
|
return (
|
|
381
407
|
<button
|
|
382
408
|
data-sidebar="rail"
|
|
383
409
|
data-slot="sidebar-rail"
|
|
384
|
-
aria-label=
|
|
410
|
+
aria-label={text}
|
|
385
411
|
tabIndex={-1}
|
|
386
412
|
onClick={toggleSidebar}
|
|
387
|
-
title=
|
|
413
|
+
title={text}
|
|
388
414
|
className={cn(
|
|
389
415
|
// rtl-ok: the whole rail block is keyed to the resolved physical side
|
|
390
416
|
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex", // rtl-ok: keyed to resolved physical side
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
4
|
import { Loader2Icon } from "lucide-react";
|
|
3
5
|
import * as React from "react";
|
|
4
6
|
|
|
5
7
|
import { cn } from "@/lib/utils";
|
|
8
|
+
import { useUiText } from "./use-ui-text";
|
|
6
9
|
|
|
7
10
|
const spinnerVariants = cva("animate-spin", {
|
|
8
11
|
variants: {
|
|
@@ -22,11 +25,13 @@ function Spinner({
|
|
|
22
25
|
size,
|
|
23
26
|
...props
|
|
24
27
|
}: React.ComponentProps<typeof Loader2Icon> & VariantProps<typeof spinnerVariants>) {
|
|
28
|
+
// An explicit `aria-label` in `props` is spread below and wins over this.
|
|
29
|
+
const uiText = useUiText();
|
|
25
30
|
return (
|
|
26
31
|
<Loader2Icon
|
|
27
32
|
data-slot="spinner"
|
|
28
33
|
role="status"
|
|
29
|
-
aria-label="Loading"
|
|
34
|
+
aria-label={uiText("ui.labels.loading", "Loading")}
|
|
30
35
|
className={cn(spinnerVariants({ size, className }))}
|
|
31
36
|
{...props}
|
|
32
37
|
/>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { useI18nTranslations } from "../../i18n/config";
|
|
5
|
+
|
|
6
|
+
/** The injected `t`, plus next-intl's optional non-throwing `has` probe. */
|
|
7
|
+
type Translate = ((key: string, values?: Record<string, unknown>) => string) & {
|
|
8
|
+
has?: (key: string) => boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Fills `{name}` placeholders in an English fallback. */
|
|
12
|
+
function interpolate(text: string, values?: Record<string, unknown>): string {
|
|
13
|
+
if (!values) return text;
|
|
14
|
+
return text.replace(/\{(\w+)\}/g, (match, name: string) => (name in values ? String(values[name]) : match));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Chrome text for the shadcnui primitives (the "Close" behind a dialog's X,
|
|
19
|
+
* the sidebar toggle's label, a multi-select's "Search..."), in the active
|
|
20
|
+
* language.
|
|
21
|
+
*
|
|
22
|
+
* The primitives stay framework-agnostic: they never import next-intl. This
|
|
23
|
+
* reads the `useTranslations` the host app passed to `configureI18n()`, and
|
|
24
|
+
* returns the English `fallback` whenever that cannot produce the text — i18n
|
|
25
|
+
* not configured, the app has no such key, or the lookup throws. An app that
|
|
26
|
+
* never defines the key therefore renders exactly what it rendered before.
|
|
27
|
+
*
|
|
28
|
+
* Every primitive that uses this also takes an explicit label prop; a value
|
|
29
|
+
* passed there always wins over the lookup.
|
|
30
|
+
*/
|
|
31
|
+
export function useUiText(): (key: string, fallback: string, values?: Record<string, unknown>) => string {
|
|
32
|
+
const t = useI18nTranslations() as Translate;
|
|
33
|
+
|
|
34
|
+
return useCallback(
|
|
35
|
+
(key: string, fallback: string, values?: Record<string, unknown>): string => {
|
|
36
|
+
if (typeof t.has === "function" && !t.has(key)) return interpolate(fallback, values);
|
|
37
|
+
try {
|
|
38
|
+
const text = t(key, values);
|
|
39
|
+
// The unconfigured fallback, and next-intl's default for a missing
|
|
40
|
+
// key, both hand back the key path itself.
|
|
41
|
+
if (!text || text === key || text.endsWith(`.${key}`)) return interpolate(fallback, values);
|
|
42
|
+
return text;
|
|
43
|
+
} catch {
|
|
44
|
+
return interpolate(fallback, values);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
[t],
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { formatDate, FormatOption } from "../date-formatter";
|
|
2
|
+
import { dateFormatLocale, formatDate, FormatOption } from "../date-formatter";
|
|
3
3
|
|
|
4
4
|
describe("formatDate", () => {
|
|
5
5
|
beforeEach(() => {
|
|
@@ -158,4 +158,27 @@ describe("formatDate", () => {
|
|
|
158
158
|
expect(result).toBe("15/06/2024");
|
|
159
159
|
});
|
|
160
160
|
});
|
|
161
|
+
|
|
162
|
+
describe("timeSince in other languages", () => {
|
|
163
|
+
it("keeps the English wording for en-GB", () => {
|
|
164
|
+
expect(formatDate(new Date("2024-06-15T09:00:00Z"), "timeSince", "en-GB")).toBe("3 hours ago");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("uses the locale's relative-time wording for Italian", () => {
|
|
168
|
+
expect(formatDate(new Date("2024-06-15T09:00:00Z"), "timeSince", "it")).toBe("3 ore fa");
|
|
169
|
+
expect(formatDate(new Date("2024-06-15T11:59:30Z"), "timeSince", "it")).toBe("30 secondi fa");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("dateFormatLocale", () => {
|
|
174
|
+
it("maps English and missing locales to en-GB", () => {
|
|
175
|
+
expect(dateFormatLocale("en")).toBe("en-GB");
|
|
176
|
+
expect(dateFormatLocale("en-US")).toBe("en-GB");
|
|
177
|
+
expect(dateFormatLocale(undefined)).toBe("en-GB");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("passes other locales through", () => {
|
|
181
|
+
expect(dateFormatLocale("it")).toBe("it");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
161
184
|
});
|