@carlonicora/nextjs-jsonapi 3.3.9 → 3.4.1

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.
Files changed (175) hide show
  1. package/dist/{BlockNoteEditor-BYMRWUVG.mjs → BlockNoteEditor-3D5D7YL7.mjs} +2 -2
  2. package/dist/{BlockNoteEditor-USRMFZGP.js → BlockNoteEditor-F6TGKMTW.js} +9 -9
  3. package/dist/{BlockNoteEditor-USRMFZGP.js.map → BlockNoteEditor-F6TGKMTW.js.map} +1 -1
  4. package/dist/billing/index.js +347 -347
  5. package/dist/billing/index.js.map +1 -1
  6. package/dist/billing/index.mjs +27 -27
  7. package/dist/billing/index.mjs.map +1 -1
  8. package/dist/{chunk-CFZW6GU4.mjs → chunk-G5LSXG5K.mjs} +2572 -2482
  9. package/dist/chunk-G5LSXG5K.mjs.map +1 -0
  10. package/dist/{chunk-5WFYQE22.js → chunk-KT4CTWCV.js} +825 -735
  11. package/dist/chunk-KT4CTWCV.js.map +1 -0
  12. package/dist/client/index.d.mts +5 -0
  13. package/dist/client/index.d.ts +5 -0
  14. package/dist/client/index.js +2 -2
  15. package/dist/client/index.mjs +1 -1
  16. package/dist/components/index.d.mts +13 -7
  17. package/dist/components/index.d.ts +13 -7
  18. package/dist/components/index.js +2 -2
  19. package/dist/components/index.mjs +1 -1
  20. package/dist/contexts/index.d.mts +13 -1
  21. package/dist/contexts/index.d.ts +13 -1
  22. package/dist/contexts/index.js +6 -2
  23. package/dist/contexts/index.js.map +1 -1
  24. package/dist/contexts/index.mjs +5 -1
  25. package/dist/features/help/index.js +35 -35
  26. package/dist/features/help/index.js.map +1 -1
  27. package/dist/features/help/index.mjs +6 -6
  28. package/dist/features/help/index.mjs.map +1 -1
  29. package/dist/features/tokenusage/index.js +135 -129
  30. package/dist/features/tokenusage/index.js.map +1 -1
  31. package/dist/features/tokenusage/index.mjs +74 -68
  32. package/dist/features/tokenusage/index.mjs.map +1 -1
  33. package/package.json +35 -35
  34. package/scripts/check-rtl-classes.mjs +60 -0
  35. package/scripts/check-rtl-classes.test.ts +25 -0
  36. package/src/components/TableCellAvatar.tsx +1 -1
  37. package/src/components/containers/PageContainer.tsx +1 -1
  38. package/src/components/containers/ReactMarkdownContainer.tsx +20 -4
  39. package/src/components/containers/RoundPageContainer.tsx +7 -7
  40. package/src/components/contents/AttributeElement.tsx +1 -1
  41. package/src/components/editors/BlockNoteDiffInlineContent.tsx +2 -2
  42. package/src/components/editors/BlockNoteEditorMentionHoverCard.tsx +1 -0
  43. package/src/components/forms/CommonDeleter.tsx +1 -1
  44. package/src/components/forms/CommonEditorButtons.tsx +1 -1
  45. package/src/components/forms/DatePickerPopover.tsx +2 -2
  46. package/src/components/forms/DateRangeSelector.tsx +2 -2
  47. package/src/components/forms/EditorSheet.tsx +18 -9
  48. package/src/components/forms/EntityMultiSelector.tsx +7 -7
  49. package/src/components/forms/EntitySelector.tsx +8 -8
  50. package/src/components/forms/FileUploader.tsx +1 -0
  51. package/src/components/forms/FormCheckbox.tsx +3 -3
  52. package/src/components/forms/FormDateTime.tsx +3 -3
  53. package/src/components/forms/FormFieldWrapper.tsx +1 -1
  54. package/src/components/forms/FormPlaceAutocomplete.tsx +2 -2
  55. package/src/components/forms/GdprConsentCheckbox.tsx +1 -1
  56. package/src/components/forms/MultiFileUploader.tsx +1 -1
  57. package/src/components/forms/PasswordInput.tsx +1 -1
  58. package/src/components/forms/__tests__/FormCheckbox.test.tsx +4 -4
  59. package/src/components/navigations/Header.tsx +2 -2
  60. package/src/components/navigations/PageSection.tsx +1 -1
  61. package/src/components/pages/PageContainerContentDetails.tsx +1 -1
  62. package/src/components/pages/PageContentContainer.tsx +5 -2
  63. package/src/components/tables/ContentListTable.tsx +4 -4
  64. package/src/components/tables/ContentTableSearch.tsx +5 -5
  65. package/src/contexts/DirectionContext.tsx +25 -0
  66. package/src/contexts/__tests__/DirectionContext.test.tsx +22 -0
  67. package/src/contexts/index.ts +1 -0
  68. package/src/features/ai-connection/components/lists/AiConnectionTypeCard.tsx +1 -1
  69. package/src/features/assistant/components/containers/AssistantPageContainer.tsx +1 -1
  70. package/src/features/assistant/components/parts/AssistantBlockNoteComposer.tsx +2 -2
  71. package/src/features/assistant/components/parts/AssistantComposer.tsx +2 -2
  72. package/src/features/assistant/components/parts/AssistantEmptyState.tsx +1 -1
  73. package/src/features/assistant/components/parts/AssistantSidebar.tsx +3 -3
  74. package/src/features/assistant-action/components/ApprovalActionCard.tsx +1 -1
  75. package/src/features/assistant-action/components/AssistantContainerWithApprovals.tsx +1 -1
  76. package/src/features/assistant-message/components/MessageItem.tsx +3 -3
  77. package/src/features/assistant-message/components/__tests__/MessageItem.spec.tsx +1 -0
  78. package/src/features/assistant-message/components/parts/MessageSourcesPanel.tsx +2 -2
  79. package/src/features/assistant-message/components/parts/RelevanceMeter.tsx +1 -1
  80. package/src/features/assistant-message/components/parts/tabs/SuggestedQuestionsTab.tsx +1 -1
  81. package/src/features/auth/components/buttons/GoogleSignInButton.tsx +1 -1
  82. package/src/features/auth/components/forms/Register.tsx +2 -2
  83. package/src/features/auth/components/two-factor/BackupCodesDialog.tsx +3 -3
  84. package/src/features/auth/utils/__tests__/clearClientStorage.spec.ts +4 -4
  85. package/src/features/billing/components/cards/BillingUsageSummaryCard.tsx +1 -1
  86. package/src/features/billing/components/cards/CustomerInfoCard.tsx +1 -1
  87. package/src/features/billing/components/cards/InvoicesSummaryCard.tsx +1 -1
  88. package/src/features/billing/components/cards/PaymentMethodSummaryCard.tsx +1 -1
  89. package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +1 -1
  90. package/src/features/billing/components/containers/BillingDashboardContainer.tsx +1 -1
  91. package/src/features/billing/stripe-customer/components/details/PaymentMethodCard.tsx +1 -1
  92. package/src/features/billing/stripe-invoice/components/details/InvoiceDetails.tsx +6 -6
  93. package/src/features/billing/stripe-invoice/components/lists/InvoicesList.tsx +2 -2
  94. package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +3 -3
  95. package/src/features/billing/stripe-product/hooks/useStripeProductTableStructure.tsx +2 -2
  96. package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +4 -4
  97. package/src/features/billing/stripe-subscription/components/widgets/PricingCard.tsx +2 -2
  98. package/src/features/billing/stripe-subscription/components/widgets/SubscriptionConfirmation.tsx +1 -1
  99. package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +2 -2
  100. package/src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx +2 -2
  101. package/src/features/company/components/forms/CompanyDeleter.tsx +4 -4
  102. package/src/features/content/hooks/useContentTableStructure.tsx +1 -1
  103. package/src/features/feature/components/forms/FormFeatures.tsx +2 -2
  104. package/src/features/help/components/HelpAssistantSheet.tsx +3 -2
  105. package/src/features/help/components/HelpHint.tsx +2 -2
  106. package/src/features/help/components/HelpTOC.tsx +1 -1
  107. package/src/features/how-to/components/containers/HowToCommand.tsx +2 -2
  108. package/src/features/how-to/components/containers/HowToCommandViewer.tsx +5 -5
  109. package/src/features/how-to/components/forms/HowToEditor.tsx +3 -3
  110. package/src/features/how-to/components/forms/HowToSelector.tsx +5 -5
  111. package/src/features/notification/components/lists/NotificationsList.tsx +2 -2
  112. package/src/features/notification/components/modals/NotificationModal.tsx +1 -1
  113. package/src/features/oauth/components/OAuthClientCard.tsx +2 -2
  114. package/src/features/oauth/components/OAuthClientDetail.tsx +2 -2
  115. package/src/features/oauth/components/OAuthClientList.tsx +3 -3
  116. package/src/features/oauth/components/OAuthRedirectUriInput.tsx +1 -1
  117. package/src/features/oauth/components/OAuthScopeSelector.tsx +2 -2
  118. package/src/features/onboarding/components/OnboardingCard.tsx +1 -7
  119. package/src/features/rbac/components/RbacByRoleContainer.tsx +3 -3
  120. package/src/features/rbac/components/RbacContainer.tsx +3 -3
  121. package/src/features/referral/components/ReferralWidget.tsx +1 -0
  122. package/src/features/role/components/forms/FormRoles.tsx +1 -1
  123. package/src/features/role/components/forms/RemoveUserFromRole.tsx +1 -1
  124. package/src/features/role/components/forms/UserRoleAdd.tsx +1 -1
  125. package/src/features/tokenusage/components/TokenUsageBreakdownTable.tsx +9 -9
  126. package/src/features/tokenusage/components/TokenUsageRankedBar.tsx +8 -3
  127. package/src/features/tokenusage/components/TokenUsageTimelineChart.tsx +5 -1
  128. package/src/features/user/components/details/UserStandaloneDetails.tsx +1 -1
  129. package/src/features/user/components/forms/UserReactivator.tsx +2 -2
  130. package/src/features/user/components/forms/UserResentInvitationEmail.tsx +1 -1
  131. package/src/features/user/components/forms/UserSelector.tsx +7 -7
  132. package/src/features/user/components/lists/ContributorsList.tsx +1 -1
  133. package/src/features/user/components/lists/UserListInAdd.tsx +1 -1
  134. package/src/features/user/components/widgets/UserSearchPopover.tsx +5 -5
  135. package/src/features/user/hooks/useUserTableStructure.tsx +1 -1
  136. package/src/features/waitlist/components/forms/WaitlistQuestionnaireRenderer.tsx +1 -1
  137. package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +1 -1
  138. package/src/hooks/useCustomD3Graph.tsx +16 -0
  139. package/src/lib/__tests__/direction.test.ts +18 -0
  140. package/src/lib/direction.ts +8 -0
  141. package/src/shadcnui/custom/multi-select.tsx +6 -6
  142. package/src/shadcnui/custom/multiple-selector.tsx +6 -2
  143. package/src/shadcnui/ui/accordion.tsx +1 -1
  144. package/src/shadcnui/ui/alert-dialog.tsx +2 -1
  145. package/src/shadcnui/ui/alert.tsx +2 -2
  146. package/src/shadcnui/ui/avatar.tsx +1 -1
  147. package/src/shadcnui/ui/badge.tsx +1 -1
  148. package/src/shadcnui/ui/breadcrumb.tsx +1 -1
  149. package/src/shadcnui/ui/button.tsx +4 -4
  150. package/src/shadcnui/ui/calendar.tsx +9 -8
  151. package/src/shadcnui/ui/carousel.tsx +18 -11
  152. package/src/shadcnui/ui/combobox.tsx +3 -3
  153. package/src/shadcnui/ui/command.tsx +2 -2
  154. package/src/shadcnui/ui/context-menu.tsx +15 -11
  155. package/src/shadcnui/ui/dialog.tsx +2 -1
  156. package/src/shadcnui/ui/drawer.tsx +19 -3
  157. package/src/shadcnui/ui/dropdown-menu.tsx +12 -10
  158. package/src/shadcnui/ui/field.tsx +2 -2
  159. package/src/shadcnui/ui/input-group.tsx +3 -3
  160. package/src/shadcnui/ui/input-otp.tsx +2 -0
  161. package/src/shadcnui/ui/navigation-menu.tsx +2 -2
  162. package/src/shadcnui/ui/progress.tsx +1 -1
  163. package/src/shadcnui/ui/radio-group.tsx +1 -0
  164. package/src/shadcnui/ui/resizable.tsx +1 -0
  165. package/src/shadcnui/ui/scroll-area.tsx +1 -1
  166. package/src/shadcnui/ui/select.tsx +2 -2
  167. package/src/shadcnui/ui/sheet.tsx +11 -4
  168. package/src/shadcnui/ui/sidebar.tsx +37 -20
  169. package/src/shadcnui/ui/sonner.tsx +6 -1
  170. package/src/shadcnui/ui/switch.tsx +1 -1
  171. package/src/shadcnui/ui/table.tsx +2 -2
  172. package/src/shadcnui/ui/tabs.tsx +1 -1
  173. package/dist/chunk-5WFYQE22.js.map +0 -1
  174. package/dist/chunk-CFZW6GU4.mjs.map +0 -1
  175. /package/dist/{BlockNoteEditor-BYMRWUVG.mjs.map → BlockNoteEditor-3D5D7YL7.mjs.map} +0 -0
@@ -15,7 +15,7 @@ type FormCheckboxProps = {
15
15
  export function FormCheckbox({ form, id, name, labelBefore, description, isRequired }: FormCheckboxProps) {
16
16
  const simpleLabel = () => {
17
17
  return (
18
- <FieldLabel htmlFor={id} className={`font-normal ${labelBefore ? "" : "ml-3"}`}>
18
+ <FieldLabel htmlFor={id} className={`font-normal ${labelBefore ? "" : "ms-3"}`}>
19
19
  {name}
20
20
  </FieldLabel>
21
21
  );
@@ -38,10 +38,10 @@ export function FormCheckbox({ form, id, name, labelBefore, description, isRequi
38
38
  {(field) => (
39
39
  <div className="flex gap-x-4">
40
40
  {labelBefore && label()}
41
- {labelBefore && isRequired && <span className="ml-1 text-destructive">*</span>}
41
+ {labelBefore && isRequired && <span className="ms-1 text-destructive">*</span>}
42
42
  <Checkbox id={id} checked={field.value ?? false} onCheckedChange={field.onChange} />
43
43
  {!labelBefore && label()}
44
- {!labelBefore && isRequired && <span className="ml-1 text-destructive">*</span>}
44
+ {!labelBefore && isRequired && <span className="ms-1 text-destructive">*</span>}
45
45
  </div>
46
46
  )}
47
47
  </FormFieldWrapper>
@@ -117,7 +117,7 @@ export function FormDateTime({
117
117
  render={
118
118
  <Button
119
119
  variant={"outline"}
120
- className={cn("w-full pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
120
+ className={cn("w-full ps-3 text-start font-normal", !field.value && "text-muted-foreground")}
121
121
  />
122
122
  }
123
123
  >
@@ -126,11 +126,11 @@ export function FormDateTime({
126
126
  ) : (
127
127
  <span>{placeholder ? placeholder : t(`common.pick_date_time`)}</span>
128
128
  )}
129
- <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
129
+ <CalendarIcon className="ms-auto h-4 w-4 opacity-50" />
130
130
  </PopoverTrigger>
131
131
  {field.value && allowEmpty !== false && (
132
132
  <CircleXIcon
133
- className="text-muted hover:text-destructive ml-2 h-6 w-6 cursor-pointer"
133
+ className="text-muted hover:text-destructive ms-2 h-6 w-6 cursor-pointer"
134
134
  onClick={() => {
135
135
  if (onChange) onChange(undefined);
136
136
  form.setValue(id, "");
@@ -43,7 +43,7 @@ export function FormFieldWrapper<T extends FieldValues>({
43
43
  {label && (
44
44
  <FieldLabel>
45
45
  {label}
46
- {isRequired && <span className="ml-1 text-destructive">*</span>}
46
+ {isRequired && <span className="ms-1 text-destructive">*</span>}
47
47
  </FieldLabel>
48
48
  )}
49
49
  {children(field, fieldState)}
@@ -315,14 +315,14 @@ export function FormPlaceAutocomplete({
315
315
 
316
316
  {/* Loading indicator */}
317
317
  {isLoading && (
318
- <div className="absolute right-3 top-1/2 -translate-y-1/2">
318
+ <div className="absolute end-3 top-1/2 -translate-y-1/2">
319
319
  <div className="border-primary h-4 w-4 animate-spin rounded-full border-2 border-t-transparent"></div>
320
320
  </div>
321
321
  )}
322
322
 
323
323
  {/* Suggestions dropdown */}
324
324
  {showSuggestions && suggestions.length > 0 && (
325
- <div className="bg-background absolute left-0 right-0 top-full z-50 mt-1 max-h-60 overflow-auto rounded-md border shadow-lg">
325
+ <div className="bg-background absolute start-0 end-0 top-full z-50 mt-1 max-h-60 overflow-auto rounded-md border shadow-lg">
326
326
  {suggestions.map((suggestion, index) => (
327
327
  <div
328
328
  key={suggestion.place_id || index}
@@ -28,7 +28,7 @@ export function GdprConsentCheckbox<T extends FieldValues>({
28
28
  <div className="space-y-1 leading-none">
29
29
  <span className="text-sm font-normal">
30
30
  {label}
31
- {required && <span className="ml-1 text-destructive">*</span>}
31
+ {required && <span className="ms-1 text-destructive">*</span>}
32
32
  </span>
33
33
  {description && <FieldDescription className="text-xs">{description}</FieldDescription>}
34
34
  </div>
@@ -87,7 +87,7 @@ const FilePreviewItem = ({ file, onRemoveClick }: { file: File; onRemoveClick: (
87
87
  <div className="text-muted-foreground group relative aspect-square h-full overflow-hidden rounded-lg border">
88
88
  <button
89
89
  onClick={onRemoveClick}
90
- className="absolute top-1.5 right-1.5 z-20 rounded-full bg-black/60 p-1 text-white opacity-0 transition-all duration-200 ease-in-out group-hover:opacity-100 hover:bg-red-600 focus:outline-none"
90
+ className="absolute top-1.5 end-1.5 z-20 rounded-full bg-black/60 p-1 text-white opacity-0 transition-all duration-200 ease-in-out group-hover:opacity-100 hover:bg-red-600 focus:outline-none"
91
91
  aria-label="Remove file"
92
92
  >
93
93
  <XIcon className="h-3.5 w-3.5" />
@@ -20,7 +20,7 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
20
20
  type="button"
21
21
  variant="ghost"
22
22
  size="sm"
23
- className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
23
+ className="absolute end-0 top-0 h-full px-3 py-2 hover:bg-transparent"
24
24
  onClick={() => setShowPassword((prev) => !prev)}
25
25
  disabled={disabled}
26
26
  >
@@ -107,7 +107,7 @@ describe.skip("FormCheckbox", () => {
107
107
  expect(labelIndex).toBeLessThan(checkboxIndex);
108
108
  });
109
109
 
110
- it("should apply ml-3 class to label when label is after checkbox", () => {
110
+ it("should apply ms-3 class to label when label is after checkbox", () => {
111
111
  render(
112
112
  <FormWrapper defaultValues={{ active: false }}>
113
113
  {(form) => <FormCheckbox form={form} id="active" name="Active" />}
@@ -115,10 +115,10 @@ describe.skip("FormCheckbox", () => {
115
115
  );
116
116
 
117
117
  const label = screen.getByText("Active");
118
- expect(label).toHaveClass("ml-3");
118
+ expect(label).toHaveClass("ms-3");
119
119
  });
120
120
 
121
- it("should not apply ml-3 class when labelBefore is true", () => {
121
+ it("should not apply ms-3 class when labelBefore is true", () => {
122
122
  render(
123
123
  <FormWrapper defaultValues={{ active: false }}>
124
124
  {(form) => <FormCheckbox form={form} id="active" name="Active" labelBefore={true} />}
@@ -126,7 +126,7 @@ describe.skip("FormCheckbox", () => {
126
126
  );
127
127
 
128
128
  const label = screen.getByText("Active");
129
- expect(label).not.toHaveClass("ml-3");
129
+ expect(label).not.toHaveClass("ms-3");
130
130
  });
131
131
  });
132
132
 
@@ -37,8 +37,8 @@ export function Header({ children, mobileChildren, leftContent, logo, className
37
37
  <header
38
38
  className={`bg-sidebar sticky top-0 z-10 flex h-[var(--app-header-h,calc(3rem_+_env(safe-area-inset-top)))] flex-col items-center justify-start gap-x-4 pt-[env(safe-area-inset-top)] ${className ?? ""}`}
39
39
  >
40
- <div className="bg-sidebar flex h-12 w-full flex-row items-center justify-between pl-2 pr-4">
41
- {isMobile && logo && <div className="flex shrink-0 flex-row items-center pr-1">{logo}</div>}
40
+ <div className="bg-sidebar flex h-12 w-full flex-row items-center justify-between ps-2 pe-4">
41
+ {isMobile && logo && <div className="flex shrink-0 flex-row items-center pe-1">{logo}</div>}
42
42
  <SidebarTrigger aria-label="Toggle sidebar" id="sidebar-trigger" />
43
43
  {leftContent}
44
44
  <div className="flex w-full flex-row items-center justify-start">
@@ -47,7 +47,7 @@ export function PageSection({ children, title, options, open, small, onToggle }:
47
47
  {isOpen ? (
48
48
  <ChevronDownIcon className={`text-primary h-4 w-4`} />
49
49
  ) : (
50
- <ChevronRightIcon className="text-primary h-4 w-4" />
50
+ <ChevronRightIcon className="text-primary h-4 w-4 rtl:rotate-180" />
51
51
  )}
52
52
  <h2 className={`flex w-full ${small === true ? `text-sm` : `text-lg`} text-primary font-semibold`}>
53
53
  {title}
@@ -64,7 +64,7 @@ export function PageContainerContentDetails({ items, section, module, id }: Page
64
64
  </TabsList>
65
65
  </div>
66
66
 
67
- <div className="pr-4">
67
+ <div className="pe-4">
68
68
  {items.map((item) => (
69
69
  <TabsContent key={item.title} value={item.title}>
70
70
  {item.content}
@@ -30,6 +30,7 @@ export function PageContentContainer({ header, details, footer, content, fullBle
30
30
  // Returns saved layout and callback to save changes
31
31
  const { defaultLayout, onLayoutChanged } = useDefaultLayout({
32
32
  id: layoutId,
33
+ // rtl-ok: persisted panel identifiers, not CSS
33
34
  panelIds: ["left-panel", "right-panel"],
34
35
  storage: typeof window !== "undefined" ? localStorage : undefined,
35
36
  });
@@ -47,7 +48,7 @@ export function PageContentContainer({ header, details, footer, content, fullBle
47
48
 
48
49
  return (
49
50
  <div className="flex h-[calc(100dvh-4rem)] w-full flex-col transition-opacity duration-150 animate-in fade-in">
50
- {header && <div className="mb-4 flex w-full shrink-0 border-b pr-4">{header}</div>}
51
+ {header && <div className="mb-4 flex w-full shrink-0 border-b pe-4">{header}</div>}
51
52
  <div className="min-h-0 flex-1">
52
53
  {details || footer ? (
53
54
  <ResizablePanelGroup
@@ -57,18 +58,20 @@ export function PageContentContainer({ header, details, footer, content, fullBle
57
58
  onLayoutChanged={onLayoutChanged}
58
59
  >
59
60
  <ResizablePanel
61
+ // rtl-ok: persisted panel identifier, not CSS
60
62
  id="left-panel"
61
63
  defaultSize={leftPanelDefaultSize}
62
64
  minSize={leftPanelMinSize}
63
65
  maxSize={leftPanelMaxSize}
64
66
  >
65
- <div className={`@container flex h-full flex-col ${isMobile ? "pb-4" : "pr-4"}`}>
67
+ <div className={`@container flex h-full flex-col ${isMobile ? "pb-4" : "pe-4"}`}>
66
68
  <div className="flex-1 overflow-y-auto">{details}</div>
67
69
  {footer && <div className="flex flex-col gap-y-2 pt-2 pb-2">{footer}</div>}
68
70
  </div>
69
71
  </ResizablePanel>
70
72
  <ResizableHandle withHandle />
71
73
  <ResizablePanel
74
+ // rtl-ok: persisted panel identifier, not CSS
72
75
  id="right-panel"
73
76
  defaultSize={rightPanelDefaultSize}
74
77
  className={cn("w-full", isMobile ? "pt-4" : "")}
@@ -159,7 +159,7 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
159
159
  {props.title && (
160
160
  <TableRow>
161
161
  <TableHead
162
- className="bg-card rounded-t-lg text-primary p-4 text-left font-bold"
162
+ className="bg-card rounded-t-lg text-primary p-4 text-start font-bold"
163
163
  colSpan={tableColumns.length}
164
164
  >
165
165
  <div className="flex w-full items-center justify-between gap-x-2">
@@ -271,7 +271,7 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
271
271
  {showFooter && (
272
272
  <TableFooter>
273
273
  <TableRow>
274
- <TableCell colSpan={tableColumns.length} className="bg-card py-4 text-right">
274
+ <TableCell colSpan={tableColumns.length} className="bg-card py-4 text-end">
275
275
  <div className="flex items-center justify-end space-x-2">
276
276
  <Button
277
277
  variant="outline"
@@ -286,7 +286,7 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
286
286
  {pendingDirection === "prev" ? (
287
287
  <Loader2 className="h-4 w-4 animate-spin" />
288
288
  ) : (
289
- <ChevronLeft className="h-4 w-4" />
289
+ <ChevronLeft className="h-4 w-4 rtl:rotate-180" />
290
290
  )}
291
291
  </Button>
292
292
  {data.pageInfo && (
@@ -307,7 +307,7 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
307
307
  {pendingDirection === "next" ? (
308
308
  <Loader2 className="h-4 w-4 animate-spin" />
309
309
  ) : (
310
- <ChevronRight className="h-4 w-4" />
310
+ <ChevronRight className="h-4 w-4 rtl:rotate-180" />
311
311
  )}
312
312
  </Button>
313
313
  </div>
@@ -74,13 +74,13 @@ export function ContentTableSearch({ data }: ContentTableSearchProps) {
74
74
  Labels reuse keys that exist in every consuming app; a new package-level key would
75
75
  throw MISSING_MESSAGE wherever it had not been added. */}
76
76
  {isExpanded ? (
77
- <Search aria-hidden className="text-muted-foreground absolute top-1 left-1 size-4" />
77
+ <Search aria-hidden className="text-muted-foreground absolute top-1 start-1 size-4" />
78
78
  ) : (
79
79
  <Button
80
80
  data-testid="content-table-search-trigger"
81
81
  variant="ghost"
82
82
  size="icon-sm"
83
- className="text-muted-foreground hover:text-foreground absolute top-0 left-0"
83
+ className="text-muted-foreground hover:text-foreground absolute top-0 start-0"
84
84
  onClick={handleSearchIconClick}
85
85
  >
86
86
  <Search className="size-4" />
@@ -100,19 +100,19 @@ export function ContentTableSearch({ data }: ContentTableSearchProps) {
100
100
  onBlur={handleBlur}
101
101
  placeholder={t(`ui.search.placeholder_global`)}
102
102
  type="text"
103
- className="border-border/50 focus-visible:border-border h-10 w-full pr-8 pl-8 text-xs shadow-none focus-visible:ring-0"
103
+ className="border-border/50 focus-visible:border-border h-10 w-full pe-8 ps-8 text-xs shadow-none focus-visible:ring-0"
104
104
  onChange={(e) => setSearchTerm(e.target.value)}
105
105
  value={searchTerm}
106
106
  />
107
107
  )}
108
108
  {isExpanded && isSearching && (
109
- <RefreshCw className="text-muted-foreground absolute top-1 right-1 h-4 w-4 animate-spin" />
109
+ <RefreshCw className="text-muted-foreground absolute top-1 end-1 h-4 w-4 animate-spin" />
110
110
  )}
111
111
  {isExpanded && !isSearching && searchTermRef.current && (
112
112
  <Button
113
113
  variant="ghost"
114
114
  size="icon-sm"
115
- className="text-muted-foreground hover:text-foreground absolute top-0 right-0"
115
+ className="text-muted-foreground hover:text-foreground absolute top-0 end-0"
116
116
  onClick={handleClear}
117
117
  >
118
118
  <X className="size-4" />
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { DirectionProvider as BaseUIDirectionProvider } from "@base-ui/react/direction-provider";
5
+
6
+ export type Direction = "ltr" | "rtl";
7
+
8
+ const DirectionContext = React.createContext<Direction>("ltr");
9
+
10
+ /**
11
+ * App-owned direction. The consumer sets <html dir> AND mounts this provider
12
+ * with the same value; the package never derives direction from locale.
13
+ * Wraps Base UI's DirectionProvider so popups resolve align/side logically.
14
+ */
15
+ export function DirectionProvider({ dir = "ltr", children }: { dir?: Direction; children: React.ReactNode }) {
16
+ return (
17
+ <DirectionContext.Provider value={dir}>
18
+ <BaseUIDirectionProvider direction={dir}>{children}</BaseUIDirectionProvider>
19
+ </DirectionContext.Provider>
20
+ );
21
+ }
22
+
23
+ export function useDir(): Direction {
24
+ return React.useContext(DirectionContext);
25
+ }
@@ -0,0 +1,22 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { DirectionProvider, useDir } from "../DirectionContext";
4
+
5
+ function Probe() {
6
+ return <span data-testid="dir">{useDir()}</span>;
7
+ }
8
+
9
+ describe("DirectionContext", () => {
10
+ it("defaults to ltr without a provider", () => {
11
+ render(<Probe />);
12
+ expect(screen.getByTestId("dir").textContent).toBe("ltr");
13
+ });
14
+ it("provides rtl", () => {
15
+ render(
16
+ <DirectionProvider dir="rtl">
17
+ <Probe />
18
+ </DirectionProvider>,
19
+ );
20
+ expect(screen.getByTestId("dir").textContent).toBe("rtl");
21
+ });
22
+ });
@@ -8,6 +8,7 @@ export * from "../features/how-to/contexts/HowToContext";
8
8
  export * from "../features/rbac/contexts/RbacContext";
9
9
  export * from "../features/assistant/contexts/AssistantContext";
10
10
  export * from "./CommonContext";
11
+ export * from "./DirectionContext";
11
12
  export * from "./HeaderChildrenContext";
12
13
  export * from "./HeaderLeftContentContext";
13
14
  export * from "./HeaderLogoContext";
@@ -123,7 +123,7 @@ export function AiConnectionTypeCard({ connectionType }: AiConnectionTypeCardPro
123
123
  className={cn("flex items-center gap-3 px-3 py-2", readOnly && "opacity-50")}
124
124
  data-testid={`ai-connection-row-${connection.id}`}
125
125
  >
126
- <span className="text-muted-foreground w-5 text-right text-xs tabular-nums">{index + 1}</span>
126
+ <span className="text-muted-foreground w-5 text-end text-xs tabular-nums">{index + 1}</span>
127
127
  <div className="flex min-w-0 flex-1 flex-col">
128
128
  <span className="truncate text-sm">{connection.name}</span>
129
129
  <span className="text-muted-foreground truncate text-xs">
@@ -70,7 +70,7 @@ export function AssistantPageContainer({
70
70
  details={
71
71
  // The panel supplies its own padding and border; cancel both so the
72
72
  // sidebar reads as the panel body rather than a card inside it.
73
- <div className="-m-4 flex h-full min-h-0 [&>aside]:w-full [&>aside]:border-r-0 [&>aside]:bg-transparent">
73
+ <div className="-m-4 flex h-full min-h-0 [&>aside]:w-full [&>aside]:border-e-0 [&>aside]:bg-transparent">
74
74
  <AssistantSidebar
75
75
  threads={ctx.threads}
76
76
  activeId={ctx.assistant?.id}
@@ -126,10 +126,10 @@ export function AssistantBlockNoteComposer({
126
126
  />
127
127
  </div>
128
128
  <Button onClick={submit} disabled={!canSend} size="sm" className="h-8">
129
- <ArrowUp className="mr-1 h-4 w-4" /> {t("features.assistant.send")}
129
+ <ArrowUp className="me-1 h-4 w-4" /> {t("features.assistant.send")}
130
130
  </Button>
131
131
  </div>
132
- <div className="text-muted-foreground text-right text-xs">{t("features.assistant.keyboard_hint")}</div>
132
+ <div className="text-muted-foreground text-end text-xs">{t("features.assistant.keyboard_hint")}</div>
133
133
  </div>
134
134
  );
135
135
  }
@@ -47,10 +47,10 @@ export function AssistantComposer({ onSend, disabled, value: controlled, onValue
47
47
  className="min-h-[48px] resize-none border-0 bg-transparent focus-visible:ring-0"
48
48
  />
49
49
  <Button onClick={submit} disabled={!canSend} size="sm" className="h-8">
50
- <ArrowUp className="mr-1 h-4 w-4" /> {t("ui.buttons.save")}
50
+ <ArrowUp className="me-1 h-4 w-4" /> {t("ui.buttons.save")}
51
51
  </Button>
52
52
  </div>
53
- <div className="text-muted-foreground text-right text-xs">{t("features.assistant.keyboard_hint")}</div>
53
+ <div className="text-muted-foreground text-end text-xs">{t("features.assistant.keyboard_hint")}</div>
54
54
  </div>
55
55
  );
56
56
  }
@@ -55,7 +55,7 @@ export function AssistantEmptyState({ onSend, operatorMode = false, onOperatorMo
55
55
  <button
56
56
  key={k}
57
57
  type="button"
58
- className="border-border bg-muted/30 hover:bg-muted rounded-lg border p-3 text-left text-sm"
58
+ className="border-border bg-muted/30 hover:bg-muted rounded-lg border p-3 text-start text-sm"
59
59
  onClick={() => setDraft(text)}
60
60
  >
61
61
  {text}
@@ -29,7 +29,7 @@ export function AssistantSidebar({ threads, activeId, onSelect, onNew }: Props)
29
29
  type="button"
30
30
  onClick={() => onSelect(thread.id)}
31
31
  className={
32
- "block w-full truncate rounded-md px-2 py-1.5 text-left text-sm " +
32
+ "block w-full truncate rounded-md px-2 py-1.5 text-start text-sm " +
33
33
  (activeId === thread.id ? "bg-primary/10 text-primary" : "hover:bg-muted text-foreground")
34
34
  }
35
35
  >
@@ -41,10 +41,10 @@ export function AssistantSidebar({ threads, activeId, onSelect, onNew }: Props)
41
41
  };
42
42
 
43
43
  return (
44
- <aside className="bg-muted/30 flex w-64 flex-col border-r">
44
+ <aside className="bg-muted/30 flex w-64 flex-col border-e">
45
45
  <div className="border-b p-3">
46
46
  <Button onClick={onNew} className="w-full" size="sm">
47
- <Plus className="mr-1 h-4 w-4" /> {t("features.assistant.new")}
47
+ <Plus className="me-1 h-4 w-4" /> {t("features.assistant.new")}
48
48
  </Button>
49
49
  </div>
50
50
  <div className="flex-1 overflow-y-auto p-2">
@@ -103,7 +103,7 @@ export function ApprovalActionCard({ actionId, summary, onResolved }: Props) {
103
103
  <ShieldAlert className="text-warning h-3.5 w-3.5" />
104
104
  <span>{t("features.assistant.approval.title")}</span>
105
105
  {status && status !== "pending" && (
106
- <Badge variant={STATUS_VARIANT[status]} className="ml-auto tracking-normal normal-case">
106
+ <Badge variant={STATUS_VARIANT[status]} className="ms-auto tracking-normal normal-case">
107
107
  {t(STATUS_LABEL_KEY[status])}
108
108
  </Badge>
109
109
  )}
@@ -24,7 +24,7 @@ export function AssistantContainerWithApprovals() {
24
24
  onResolved={ctx.appendResolvedMessage}
25
25
  />
26
26
  ) : (
27
- <div className="bg-muted text-foreground rounded-2xl rounded-bl-sm px-3.5 py-2.5 text-sm leading-relaxed">
27
+ <div className="bg-muted text-foreground rounded-2xl rounded-es-sm px-3.5 py-2.5 text-sm leading-relaxed">
28
28
  {message.content}
29
29
  </div>
30
30
  )
@@ -82,7 +82,7 @@ export function MessageItem({
82
82
  renders invisible here, so force the on-primary colour and lean on
83
83
  weight for the affordance (links are never underlined in this UI).
84
84
  */}
85
- <div className="bg-primary text-primary-foreground max-w-[72%] rounded-2xl rounded-br-sm px-3.5 py-2 text-sm [&_a]:text-primary-foreground [&_a]:font-semibold [&_p]:m-0">
85
+ <div className="bg-primary text-primary-foreground max-w-[72%] rounded-2xl rounded-ee-sm px-3.5 py-2 text-sm [&_a]:text-primary-foreground [&_a]:font-semibold [&_p]:m-0">
86
86
  <ReactMarkdown remarkPlugins={[remarkGfm]} urlTransform={mentionUrlTransform} components={markdownComponents}>
87
87
  {message.content}
88
88
  </ReactMarkdown>
@@ -104,7 +104,7 @@ export function MessageItem({
104
104
 
105
105
  return (
106
106
  <div className="flex min-w-0 max-w-[78%] flex-col gap-1.5">
107
- <div className="text-muted-foreground flex items-center gap-2 pl-1 text-xs">
107
+ <div className="text-muted-foreground flex items-center gap-2 ps-1 text-xs">
108
108
  <span className="flex h-4 w-4 items-center justify-center rounded-full bg-gradient-to-br from-blue-400 to-violet-500 text-white">
109
109
  <Sparkles className="h-2.5 w-2.5" />
110
110
  </span>
@@ -117,7 +117,7 @@ export function MessageItem({
117
117
  // zeroed): restore flow spacing between sibling blocks and give
118
118
  // blockquotes a callout treatment — assistant answers use them for
119
119
  // proposals and asides that must stand apart from factual prose.
120
- <div className="bg-muted text-foreground rounded-2xl rounded-bl-sm px-3.5 py-2.5 text-sm leading-relaxed [&>*+*]:mt-3 [&_blockquote]:border-l-2 [&_blockquote]:border-primary/50 [&_blockquote]:bg-primary/5 [&_blockquote]:rounded-md [&_blockquote]:px-3 [&_blockquote]:py-1.5">
120
+ <div className="bg-muted text-foreground rounded-2xl rounded-es-sm px-3.5 py-2.5 text-sm leading-relaxed [&>*+*]:mt-3 [&_blockquote]:border-s-2 [&_blockquote]:border-primary/50 [&_blockquote]:bg-primary/5 [&_blockquote]:rounded-md [&_blockquote]:px-3 [&_blockquote]:py-1.5">
121
121
  <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents} urlTransform={mentionUrlTransform}>
122
122
  {message.content}
123
123
  </ReactMarkdown>
@@ -59,6 +59,7 @@ function buildMessageStub(p: {
59
59
  }
60
60
 
61
61
  describe("MessageItem", () => {
62
+ // rtl-ok: prose in the test name, not a CSS class
62
63
  it("user message is in a right-aligned bubble with plain text", () => {
63
64
  const msg = buildMessageStub({ role: "user", content: "hello" });
64
65
  render(<MessageItem message={msg} isLatestAssistant={false} onSelectFollowUp={vi.fn()} />);
@@ -96,7 +96,7 @@ export function MessageSourcesPanel({ message, isLatestAssistant, onSelectFollow
96
96
  aria-controls={panelId}
97
97
  className="text-primary inline-flex items-center gap-1 text-xs font-medium"
98
98
  >
99
- {open ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
99
+ {open ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3 rtl:rotate-180" />}
100
100
  {open
101
101
  ? t("features.assistant.message.sources.toggle_hide")
102
102
  : t("features.assistant.message.sources.toggle", { count: total })}
@@ -109,7 +109,7 @@ export function MessageSourcesPanel({ message, isLatestAssistant, onSelectFollow
109
109
  {visibleTabs.map((key) => (
110
110
  <TabsTrigger key={key} value={key}>
111
111
  {t(`features.assistant.message.sources.tabs.${key === "suggested" ? "suggested_questions" : key}`)}
112
- <span className="text-muted-foreground ml-1.5 text-xs">
112
+ <span className="text-muted-foreground ms-1.5 text-xs">
113
113
  {key === "suggested" && suggestionsCount}
114
114
  {key === "references" && refsCount}
115
115
  {key === "citations" && citationsCount}
@@ -18,7 +18,7 @@ export function RelevanceMeter({ value, className = "" }: Props) {
18
18
  aria-label={`Relevance ${label}`}
19
19
  className={`bg-muted relative mx-auto flex h-5 w-20 items-center justify-center overflow-hidden rounded border ${className}`}
20
20
  >
21
- <div className="bg-accent absolute top-0 left-0 h-full" style={{ width: `${pct}%` }} />
21
+ <div className="bg-accent absolute top-0 start-0 h-full" style={{ width: `${pct}%` }} />
22
22
  <span
23
23
  className={`relative text-xs ${pct < 40 ? "text-muted-foreground" : "text-accent-foreground font-semibold"}`}
24
24
  >
@@ -14,7 +14,7 @@ export function SuggestedQuestionsTab({ questions, onSelect }: Props) {
14
14
  key={q}
15
15
  type="button"
16
16
  onClick={() => onSelect(q)}
17
- className="border-border bg-muted/30 hover:bg-muted rounded-md border px-3 py-1.5 text-left text-sm"
17
+ className="border-border bg-muted/30 hover:bg-muted rounded-md border px-3 py-1.5 text-start text-sm"
18
18
  >
19
19
  {q}
20
20
  </button>
@@ -27,7 +27,7 @@ export function GoogleSignInButton({ referralCode }: GoogleSignInButtonProps) {
27
27
  variant="outline"
28
28
  data-testid="page-login-button-google"
29
29
  >
30
- <svg className="mr-2 h-5 w-5" viewBox="0 0 24 24">
30
+ <svg className="me-2 h-5 w-5" viewBox="0 0 24 24">
31
31
  <path
32
32
  fill="#4285F4"
33
33
  d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
@@ -304,7 +304,7 @@ export default function Register() {
304
304
  variant="outline"
305
305
  type="button"
306
306
  >
307
- <svg className="mr-2 h-5 w-5" viewBox="0 0 24 24">
307
+ <svg className="me-2 h-5 w-5" viewBox="0 0 24 24">
308
308
  <path
309
309
  fill="#4285F4"
310
310
  d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
@@ -332,7 +332,7 @@ export default function Register() {
332
332
  className="flex w-full"
333
333
  >
334
334
  <Button className="w-full" variant="outline" type="button">
335
- <svg className="mr-2 h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
335
+ <svg className="me-2 h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
336
336
  <path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
337
337
  </svg>
338
338
  {t("waitlist.buttons.register_with_discord")}
@@ -101,7 +101,7 @@ export function BackupCodesDialog({ remainingCodes, onRegenerate, trigger }: Bac
101
101
  <AlertDialogTrigger
102
102
  render={
103
103
  <Button variant="outline" className="w-full">
104
- <RefreshCw className="h-4 w-4 mr-2" />
104
+ <RefreshCw className="h-4 w-4 me-2" />
105
105
  {t("auth.two_factor.generate_new_codes")}
106
106
  </Button>
107
107
  }
@@ -131,11 +131,11 @@ export function BackupCodesDialog({ remainingCodes, onRegenerate, trigger }: Bac
131
131
  </div>
132
132
  <div className="flex gap-2">
133
133
  <Button variant="outline" className="flex-1" onClick={handleCopyAll}>
134
- <Copy className="h-4 w-4 mr-2" />
134
+ <Copy className="h-4 w-4 me-2" />
135
135
  {t("auth.two_factor.copy_all")}
136
136
  </Button>
137
137
  <Button variant="outline" className="flex-1" onClick={handleDownload}>
138
- <Download className="h-4 w-4 mr-2" />
138
+ <Download className="h-4 w-4 me-2" />
139
139
  {t("auth.two_factor.download")}
140
140
  </Button>
141
141
  </div>
@@ -63,18 +63,18 @@ describe("clearClientStorage", () => {
63
63
  describe("Scenario: Handles keys with special characters", () => {
64
64
  it("should properly remove keys containing colons", () => {
65
65
  const keys = [
66
- "react-resizable-panels:page-content-container-desktop:left-panel:right-panel",
67
- "react-resizable-panels:page-content-container-mobile:left-panel:right-panel",
66
+ "react-resizable-panels:page-content-container-desktop:left-panel:right-panel", // rtl-ok: localStorage key, not a CSS class
67
+ "react-resizable-panels:page-content-container-mobile:left-panel:right-panel", // rtl-ok: localStorage key, not a CSS class
68
68
  ];
69
69
 
70
70
  clearClientStorage(keys);
71
71
 
72
72
  expect(window.localStorage.removeItem).toHaveBeenCalledTimes(2);
73
73
  expect(window.localStorage.removeItem).toHaveBeenCalledWith(
74
- "react-resizable-panels:page-content-container-desktop:left-panel:right-panel",
74
+ "react-resizable-panels:page-content-container-desktop:left-panel:right-panel", // rtl-ok: localStorage key, not a CSS class
75
75
  );
76
76
  expect(window.localStorage.removeItem).toHaveBeenCalledWith(
77
- "react-resizable-panels:page-content-container-mobile:left-panel:right-panel",
77
+ "react-resizable-panels:page-content-container-mobile:left-panel:right-panel", // rtl-ok: localStorage key, not a CSS class
78
78
  );
79
79
  });
80
80
  });
@@ -87,7 +87,7 @@ export function BillingUsageSummaryCard({
87
87
  {meters.length > 1 && <p className="text-xs text-muted-foreground">Across {meters.length} meters</p>}
88
88
  <span className="flex items-center text-xs text-muted-foreground">
89
89
  View details
90
- <ChevronRight className="h-3 w-3 ml-1" />
90
+ <ChevronRight className="h-3 w-3 ms-1 rtl:rotate-180" />
91
91
  </span>
92
92
  </div>
93
93
  )}