@carlonicora/nextjs-jsonapi 3.3.9 → 3.4.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.
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 +12 -6
  17. package/dist/components/index.d.ts +12 -6
  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 +2 -2
  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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "3.3.9",
3
+ "version": "3.4.0",
4
4
  "description": "Next.js JSON:API client with server/client support and caching",
5
5
  "author": "Carlo Nicora",
6
6
  "license": "GPL-3.0-or-later",
@@ -73,7 +73,7 @@
73
73
  "build": "rm -rf dist && NODE_OPTIONS='--max-old-space-size=8192' tsup && tsc -p scripts/generate-web-module/tsconfig.json",
74
74
  "dev": "NODE_OPTIONS='--max-old-space-size=8192' tsup --watch",
75
75
  "clean": "rm -rf dist",
76
- "lint": "eslint \"src/**/*.ts\" \"src/**/*.tsx\" --fix",
76
+ "lint": "eslint \"src/**/*.ts\" \"src/**/*.tsx\" --fix && node scripts/check-rtl-classes.mjs",
77
77
  "format": "prettier --write \"src/**/*.ts\"",
78
78
  "prepublishOnly": "pnpm build",
79
79
  "prepare": "tsc -p scripts/generate-web-module/tsconfig.json",
@@ -0,0 +1,60 @@
1
+ // Lint-time guard: physical directional Tailwind classes / CSS props are
2
+ // forbidden in src/ — use logical equivalents, or escape with `rtl-ok`.
3
+ import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const PATTERNS = [
8
+ /(?<![-\w:[])(?:ml|mr|pl|pr)-(?:\[|\d|auto|px)/g,
9
+ /(?<![-\w:[])-?(?:left|right)-(?:\[|\d|px|full)/g,
10
+ /(?<![-\w])text-(?:left|right)(?![-\w])/g,
11
+ /(?<![-\w])rounded-(?:[lr]|tl|tr|bl|br)(?![a-z])/g,
12
+ /(?<![-\w])border-[lr](?![a-z])/g,
13
+ /(?<![-\w])float-(?:left|right)(?![-\w])/g,
14
+ /(?<![-\w])scroll-(?:ml|mr|pl|pr)-/g,
15
+ /\b(?:marginLeft|marginRight|paddingLeft|paddingRight)\s*[:=]/g,
16
+ ];
17
+
18
+ export function findViolations(source, file) {
19
+ if (/__tests__|\.(test|spec)\.[tj]sx?$/.test(file)) return [];
20
+ const out = [];
21
+ const lines = source.split("\n");
22
+ lines.forEach((line, i) => {
23
+ if (line.includes("rtl-ok") || (i > 0 && lines[i - 1].includes("rtl-ok"))) return;
24
+ if (line.includes("data-[side=")) return; // rtl-ok by design (resolved physical side)
25
+ for (const p of PATTERNS) {
26
+ for (const m of line.matchAll(p)) out.push({ file, line: i + 1, match: m[0] });
27
+ }
28
+ });
29
+ return out;
30
+ }
31
+
32
+ function* walk(dir) {
33
+ for (const name of readdirSync(dir)) {
34
+ const p = join(dir, name);
35
+ if (statSync(p).isDirectory()) yield* walk(p);
36
+ else if (/\.(ts|tsx)$/.test(name)) yield p;
37
+ }
38
+ }
39
+
40
+ // `process.argv[1]` is not symlink-resolved by node while `import.meta.url` is,
41
+ // so compare real paths — otherwise the CLI silently does nothing.
42
+ function isDirectInvocation() {
43
+ try {
44
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ if (isDirectInvocation()) {
51
+ const root = join(fileURLToPath(new URL(".", import.meta.url)), "..", "src");
52
+ const all = [];
53
+ for (const f of walk(root)) all.push(...findViolations(readFileSync(f, "utf8"), f));
54
+ if (all.length) {
55
+ for (const v of all) console.error(`${v.file}:${v.line} ${v.match}`);
56
+ console.error(`\n${all.length} physical directional class(es). Use logical utilities or add an rtl-ok comment.`);
57
+ process.exit(1);
58
+ }
59
+ console.log("check-rtl-classes: clean");
60
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { findViolations } from "./check-rtl-classes.mjs";
3
+
4
+ const hits = (src: string) => findViolations(src, "x.tsx").map((v) => v.match);
5
+
6
+ describe("check-rtl-classes", () => {
7
+ it("flags physical directional utilities", () => {
8
+ expect(hits(`<div className="ml-2 pr-4 text-left rounded-l-md border-r left-0 -right-4" />`)).toHaveLength(7);
9
+ expect(hits(`<li style={{ paddingLeft: "1rem" }} />`)).toHaveLength(1);
10
+ });
11
+ it("ignores logical, symmetric, compound and rtl-variant classes", () => {
12
+ expect(
13
+ hits(
14
+ `<div className="ms-2 pe-4 text-start rounded-s-md border-e start-0 inset-x-2 space-x-2 divide-x translate-x-1/2 slide-in-from-left-10 rounded-lg border-x blur-md rtl:rotate-180 rtl:-translate-x-2 data-[side=left]:left-0" />`,
15
+ ),
16
+ ).toHaveLength(0);
17
+ });
18
+ it("honours rtl-ok escapes on the same or previous line", () => {
19
+ expect(hits(`{/* rtl-ok */}\n<div className="ml-2" />`)).toHaveLength(0);
20
+ expect(hits(`<div className="ml-2" /> {/* rtl-ok */}`)).toHaveLength(0);
21
+ });
22
+ it("skips test files", () => {
23
+ expect(findViolations(`"ml-2"`, "src/x/__tests__/y.test.tsx")).toHaveLength(0);
24
+ });
25
+ });
@@ -10,7 +10,7 @@ type TableCellAvatarProps = {
10
10
 
11
11
  export function TableCellAvatar({ image, name, className }: TableCellAvatarProps) {
12
12
  return (
13
- <Avatar className={cn("mr-2 h-10 w-10 shrink-0", className)}>
13
+ <Avatar className={cn("me-2 h-10 w-10 shrink-0", className)}>
14
14
  {image ? <AvatarImage src={image} alt={name} /> : null}
15
15
  <AvatarFallback className="text-xs">{getInitials(name)}</AvatarFallback>
16
16
  </Avatar>
@@ -19,7 +19,7 @@ export function PageContainer({ children, testId, className }: PageContainerProp
19
19
  <Header leftContent={headerLeftContent} logo={headerLogo} mobileChildren={headerMobileChildren}>
20
20
  {headerChildren}
21
21
  </Header>
22
- <main className={cn(`flex w-full flex-1 flex-col gap-y-4 pt-4 pl-4 pr-0`, className)}>{children}</main>
22
+ <main className={cn(`flex w-full flex-1 flex-col gap-y-4 pt-4 ps-4 pe-0`, className)}>{children}</main>
23
23
  </div>
24
24
  );
25
25
  }
@@ -57,7 +57,7 @@ export function ReactMarkdownContainer({
57
57
  li: ({ children }) => <li className={size === "small" ? "text-xs" : ""}>{children}</li>,
58
58
  table: ({ children }) => <table className="w-full table-auto border-collapse border">{children}</table>,
59
59
  th: ({ children }) => (
60
- <th className={`border px-4 py-2 text-left ${size === "small" ? "px-2 py-1 text-xs" : ""}`}>
60
+ <th className={`border px-4 py-2 text-start ${size === "small" ? "px-2 py-1 text-xs" : ""}`}>
61
61
  {children}
62
62
  </th>
63
63
  ),
@@ -65,9 +65,25 @@ export function ReactMarkdownContainer({
65
65
  <td className={`border px-4 py-2 ${size === "small" ? "px-2 py-1 text-xs" : ""}`}>{children}</td>
66
66
  ),
67
67
  tr: ({ children }) => <tr className="even:bg-gray-50">{children}</tr>,
68
- ul: ({ children }) => <ul className={`list-disc ${size === "small" ? "pl-3" : "pl-4"}`}>{children}</ul>,
68
+ ul: ({ children }) => <ul className={`list-disc ${size === "small" ? "ps-3" : "ps-4"}`}>{children}</ul>,
69
69
  ol: ({ children }) => (
70
- <ol className={`list-decimal ${size === "small" ? "pl-3" : "pl-4"}`}>{children}</ol>
70
+ <ol className={`list-decimal ${size === "small" ? "ps-3" : "ps-4"}`}>{children}</ol>
71
+ ),
72
+ // Code is not prose: identifiers, paths and command lines read
73
+ // left-to-right in every locale, so both the fenced block and the
74
+ // inline span are deliberate LTR islands. `node` is react-markdown's
75
+ // hast node — it must not reach the DOM element.
76
+ // rtl-ok: deliberate LTR island (code)
77
+ pre: ({ children, node: _node, ...props }) => (
78
+ <pre dir="ltr" {...props}>
79
+ {children}
80
+ </pre>
81
+ ),
82
+ // rtl-ok: deliberate LTR island (code)
83
+ code: ({ children, node: _node, ...props }) => (
84
+ <code dir="ltr" {...props}>
85
+ {children}
86
+ </code>
71
87
  ),
72
88
  h1: ({ children }) => (
73
89
  <h1 className={size === "small" ? "my-1 mt-2 text-sm font-bold" : "my-2 mt-4 text-3xl font-semibold"}>
@@ -98,7 +114,7 @@ export function ReactMarkdownContainer({
98
114
  </div>
99
115
 
100
116
  {collapsible && !isExpanded && showExpandButton && (
101
- <div className="pointer-events-none absolute right-0 bottom-0 left-0 h-12 bg-gradient-to-t from-white to-transparent" />
117
+ <div className="pointer-events-none absolute end-0 bottom-0 start-0 h-12 bg-gradient-to-t from-white to-transparent" />
102
118
  )}
103
119
  </div>
104
120
 
@@ -96,10 +96,10 @@ type RoundPageContainerProps = {
96
96
  };
97
97
 
98
98
  // Rail trigger class: override the horizontal TabsTrigger defaults for a
99
- // vertical, left-aligned, dark-filled active state. tailwind-merge inside cn()
99
+ // vertical, start-aligned, dark-filled active state. tailwind-merge inside cn()
100
100
  // resolves the conflicts with the base classes.
101
101
  const railTriggerClass = cn(
102
- "flex w-full items-center justify-start rounded-md px-3 py-1.5 text-left text-sm leading-tight whitespace-normal",
102
+ "flex w-full items-center justify-start rounded-md px-3 py-1.5 text-start text-sm leading-tight whitespace-normal",
103
103
  "text-muted-foreground",
104
104
  "hover:bg-muted hover:text-foreground",
105
105
  "data-[state=active]:bg-foreground data-[state=active]:text-background",
@@ -237,7 +237,7 @@ export function RoundPageContainer({
237
237
  // render 78px tall; below the bar it became a visible gap. The
238
238
  // swipe-up gesture region is unaffected either way — it is a gesture
239
239
  // area, not painted chrome.
240
- isMobile ? "gap-1 p-1 pt-0" : "p-2 pt-0 pl-0",
240
+ isMobile ? "gap-1 p-1 pt-0" : "p-2 pt-0 ps-0",
241
241
  )}
242
242
  >
243
243
  {/* `min-h-0 flex-1`, NOT `h-full`: the bar below is an in-flow sibling in
@@ -280,7 +280,7 @@ export function RoundPageContainer({
280
280
  // render 78px tall; below the bar it became a visible gap. The
281
281
  // swipe-up gesture region is unaffected either way — it is a gesture
282
282
  // area, not painted chrome.
283
- isMobile ? "gap-1 p-1 pt-0" : "p-2 pt-0 pl-0",
283
+ isMobile ? "gap-1 p-1 pt-0" : "p-2 pt-0 ps-0",
284
284
  )}
285
285
  >
286
286
  {/* `min-h-0 flex-1`, NOT `h-full`: MobileNavigationBar below is an in-flow
@@ -323,7 +323,7 @@ export function RoundPageContainer({
323
323
  {/* Flush-left section rail — md and up */}
324
324
  <aside
325
325
  data-testid="round-page-rail"
326
- className="hidden shrink-0 border-r p-4 md:flex md:w-56 md:flex-col md:overflow-y-auto"
326
+ className="hidden shrink-0 border-e p-4 md:flex md:w-56 md:flex-col md:overflow-y-auto"
327
327
  >
328
328
  {/* `w-full` is load-bearing: TabsList's base variant carries
329
329
  `w-fit`, which shrinks the rail list to its widest label.
@@ -506,7 +506,7 @@ export function RoundPageContainer({
506
506
  {details &&
507
507
  (isMobile ? (
508
508
  <Sheet open={showDetails} onOpenChange={setShowDetails}>
509
- <SheetContent side="right">
509
+ <SheetContent side="end">
510
510
  <SheetHeader>
511
511
  <SheetTitle>{detailsTitle ?? "Details"}</SheetTitle>
512
512
  </SheetHeader>
@@ -533,7 +533,7 @@ export function RoundPageContainer({
533
533
  // vertically too. Making this the containing block lets
534
534
  // `overflow-hidden` clip them again.
535
535
  "relative flex h-full shrink-0 flex-col overflow-hidden transition-all duration-300 ease-in-out",
536
- showDetails ? "w-96 border-l opacity-100" : "ml-0 w-0 border-l-0 opacity-0",
536
+ showDetails ? "w-96 border-s opacity-100" : "ms-0 w-0 border-s-0 opacity-0",
537
537
  )}
538
538
  >
539
539
  {detailsTitle && (
@@ -14,7 +14,7 @@ export function AttributeElement({ inline, title, value, className }: AttributeE
14
14
  return (
15
15
  <div className={cn(`flex ${inline === true ? "flex-row" : "flex-col"} my-1 justify-start`, className)}>
16
16
  {title && (
17
- <div className={`${inline === true ? "min-w-48 pr-4" : "w-full"} text-xs text-muted-foreground`}>{title}</div>
17
+ <div className={`${inline === true ? "min-w-48 pe-4" : "w-full"} text-xs text-muted-foreground`}>{title}</div>
18
18
  )}
19
19
  {value && <div className="flex w-full flex-col text-sm">{value}</div>}
20
20
  </div>
@@ -51,7 +51,7 @@ export const diffWordInlineContentSpec = createReactInlineContentSpec(
51
51
  if (diffType === "buttons") {
52
52
  const diffIdList = diffId.split(",");
53
53
  return (
54
- <span className="ml-2 inline-flex items-center gap-1">
54
+ <span className="ms-2 inline-flex items-center gap-1">
55
55
  <Button
56
56
  size="sm"
57
57
  variant="ghost"
@@ -108,7 +108,7 @@ export const diffWordInlineContentSpec = createReactInlineContentSpec(
108
108
  </span>
109
109
 
110
110
  {!accepted && !rejected && (diffType === "added" || diffType === "removed") && (
111
- <span className="absolute -top-8 left-0 z-10 hidden items-center gap-1 rounded border bg-white p-1 shadow-lg group-hover:flex">
111
+ <span className="absolute -top-8 start-0 z-10 hidden items-center gap-1 rounded border bg-white p-1 shadow-lg group-hover:flex">
112
112
  <Button
113
113
  size="sm"
114
114
  variant="ghost"
@@ -87,6 +87,7 @@ export function BlockNoteEditorMentionHoverCard({
87
87
  style={{
88
88
  position: "fixed",
89
89
  top: rect.bottom + 4,
90
+ // rtl-ok: viewport-anchored floating position
90
91
  left: rect.left,
91
92
  zIndex: 50,
92
93
  }}
@@ -107,7 +107,7 @@ export function CommonDeleter({
107
107
  )}
108
108
  <div className="flex justify-end">
109
109
  <Button
110
- className="mr-2"
110
+ className="me-2"
111
111
  variant={"outline"}
112
112
  type={`button`}
113
113
  onClick={() => setOpen(false)}
@@ -45,7 +45,7 @@ export function CommonEditorButtons({
45
45
  return (
46
46
  <div className="flex justify-end">
47
47
  <Button
48
- className="mr-2"
48
+ className="me-2"
49
49
  variant={"outline"}
50
50
  type={`button`}
51
51
  onClick={() => setOpen(false)}
@@ -163,10 +163,10 @@ export const DatePickerPopover = ({
163
163
  value={inputValue}
164
164
  onChange={(e) => handleInputChange(e.target.value)}
165
165
  placeholder={datePlaceholder}
166
- className="pr-16"
166
+ className="pe-16"
167
167
  onClick={(e) => e.stopPropagation()}
168
168
  />
169
- <div className="absolute right-1 top-1/2 flex -translate-y-1/2 items-center space-x-1">
169
+ <div className="absolute end-1 top-1/2 flex -translate-y-1/2 items-center space-x-1">
170
170
  <button
171
171
  type="button"
172
172
  className="flex h-8 w-8 items-center justify-center rounded-md hover:bg-muted"
@@ -94,7 +94,7 @@ export function DateRangeSelector({ onDateChange, avoidSettingDates, showPreviou
94
94
  <Button
95
95
  id="date"
96
96
  variant={"outline"}
97
- className={cn("w-[300px] justify-start text-left font-normal", !date && "text-muted-foreground")}
97
+ className={cn("w-[300px] justify-start text-start font-normal", !date && "text-muted-foreground")}
98
98
  >
99
99
  <CalendarIcon />
100
100
  {date?.from ? (
@@ -122,7 +122,7 @@ export function DateRangeSelector({ onDateChange, avoidSettingDates, showPreviou
122
122
  <Button
123
123
  id="date"
124
124
  variant={"outline"}
125
- className={cn("w-[300px] justify-start text-left font-normal", !date && "text-muted-foreground")}
125
+ className={cn("w-[300px] justify-start text-start font-normal", !date && "text-muted-foreground")}
126
126
  />
127
127
  }
128
128
  >
@@ -57,12 +57,20 @@ function resolveTriggerIsNativeButton(trigger: ReactNode): boolean {
57
57
  return true;
58
58
  }
59
59
 
60
+ /**
61
+ * The sheet opens on the inline end (`side="end"`), which SheetContent resolves
62
+ * to the physical `right` in LTR and `left` in RTL — and its own width classes
63
+ * are keyed to that resolved physical side. Each size therefore has to be
64
+ * declared for BOTH sides, or an RTL sheet would fall back to SheetContent's
65
+ * base `data-[side=left]:sm:max-w-sm`.
66
+ */
67
+ // rtl-ok: keyed to the resolved physical side, not to a writing direction
60
68
  const sizeClasses: Record<EditorSheetSize, string> = {
61
- sm: "data-[side=right]:sm:max-w-2xl",
62
- md: "data-[side=right]:sm:max-w-3xl",
63
- lg: "data-[side=right]:sm:max-w-5xl",
64
- xl: "data-[side=right]:sm:max-w-7xl",
65
- "2xl": "data-[side=right]:sm:!max-w-[min(96rem,90vw)]",
69
+ sm: "data-[side=right]:sm:max-w-2xl data-[side=left]:sm:max-w-2xl",
70
+ md: "data-[side=right]:sm:max-w-3xl data-[side=left]:sm:max-w-3xl",
71
+ lg: "data-[side=right]:sm:max-w-5xl data-[side=left]:sm:max-w-5xl",
72
+ xl: "data-[side=right]:sm:max-w-7xl data-[side=left]:sm:max-w-7xl",
73
+ "2xl": "data-[side=right]:sm:!max-w-[min(96rem,90vw)] data-[side=left]:sm:!max-w-[min(96rem,90vw)]",
66
74
  };
67
75
 
68
76
  export type EditorSheetProps<T extends FieldValues> = {
@@ -106,7 +114,7 @@ export type EditorSheetProps<T extends FieldValues> = {
106
114
  closeWithoutConfirm: (open: boolean) => void;
107
115
  }) => ReactNode;
108
116
 
109
- /** Rendered on the right-hand side of the header, next to the title/description
117
+ /** Rendered on the inline-end side of the header, next to the title/description
110
118
  * block. This sits OUTSIDE the <form> element — interactive elements here must
111
119
  * use onClick handlers, never type="submit". */
112
120
  actions?: ReactNode;
@@ -271,11 +279,12 @@ export function EditorSheet<T extends FieldValues>({
271
279
  )}
272
280
  </SheetTrigger>
273
281
  ))}
274
- <SheetContent side="right" className={sizeClasses[size]}>
282
+ <SheetContent side="end" className={sizeClasses[size]}>
275
283
  <SheetHeader className="border-b px-6 py-4">
276
284
  {actions ? (
277
- // pr-10 clears the SheetContent close button (absolute top-4 right-4).
278
- <div className="flex items-start justify-between gap-x-4 pr-10">
285
+ // pe-10 clears the SheetContent close button, which sits at the
286
+ // inline end of the sheet (absolute top-4 end-4).
287
+ <div className="flex items-start justify-between gap-x-4 pe-10">
279
288
  <div className="flex min-w-0 flex-col gap-y-1.5">
280
289
  <SheetTitle>{headerTitle}</SheetTitle>
281
290
  <SheetDescription>{headerDescription}</SheetDescription>
@@ -229,32 +229,32 @@ export function EntityMultiSelector<T extends { id: string }>({
229
229
  >
230
230
  {selectedValues.length > 0 ? (
231
231
  <>
232
- <span className="text-foreground min-w-0 flex-1 truncate text-left">{triggerSummary}</span>
232
+ <span className="text-foreground min-w-0 flex-1 truncate text-start">{triggerSummary}</span>
233
233
  <span className="bg-primary/10 text-primary shrink-0 rounded-full px-1.5 py-0.5 text-xs font-medium">
234
234
  {selectedValues.length}
235
235
  </span>
236
236
  </>
237
237
  ) : (
238
- <span className="text-muted-foreground flex-1 text-left">{placeholder}</span>
238
+ <span className="text-muted-foreground flex-1 text-start">{placeholder}</span>
239
239
  )}
240
240
  <ChevronDownIcon className="text-muted-foreground size-3.5 shrink-0" />
241
241
  </div>
242
242
  </PopoverTrigger>
243
243
  <PopoverContent className="w-(--anchor-width) flex flex-col gap-0 p-0" align="start">
244
244
  <div className="relative p-1.5">
245
- <SearchIcon className="text-muted-foreground pointer-events-none absolute top-1/2 left-3.5 size-3.5 -translate-y-1/2" />
245
+ <SearchIcon className="text-muted-foreground pointer-events-none absolute top-1/2 start-3.5 size-3.5 -translate-y-1/2" />
246
246
  <Input
247
247
  ref={searchInputRef}
248
248
  placeholder={placeholder}
249
249
  type="text"
250
- className="h-8 w-full pr-7 pl-7 text-xs"
250
+ className="h-8 w-full pe-7 ps-7 text-xs"
251
251
  value={searchTerm}
252
252
  onChange={(e) => setSearchTerm(e.target.value)}
253
253
  />
254
254
  {searchTerm && (
255
255
  <button
256
256
  type="button"
257
- className="text-muted-foreground hover:text-foreground absolute top-1/2 right-3.5 -translate-y-1/2"
257
+ className="text-muted-foreground hover:text-foreground absolute top-1/2 end-3.5 -translate-y-1/2"
258
258
  onClick={() => setSearchTerm("")}
259
259
  >
260
260
  <XIcon className="size-3.5" />
@@ -271,7 +271,7 @@ export function EntityMultiSelector<T extends { id: string }>({
271
271
  <button
272
272
  key={option.id}
273
273
  type="button"
274
- className="hover:bg-muted flex w-full items-center gap-2.5 rounded-md px-2.5 py-1.5 text-left text-xs/relaxed"
274
+ className="hover:bg-muted flex w-full items-center gap-2.5 rounded-md px-2.5 py-1.5 text-start text-xs/relaxed"
275
275
  onClick={() => toggleEntity(option)}
276
276
  >
277
277
  {renderOption && option.entityData ? (
@@ -313,7 +313,7 @@ export function EntityMultiSelector<T extends { id: string }>({
313
313
  <Badge
314
314
  key={value.id}
315
315
  variant="outline"
316
- className="h-auto gap-1.5 rounded-md px-2.5 py-1 pr-1.5 text-xs"
316
+ className="h-auto gap-1.5 rounded-md px-2.5 py-1 pe-1.5 text-xs"
317
317
  >
318
318
  {stableGetFormValueLabel(value)}
319
319
  <button
@@ -169,7 +169,7 @@ export default function EntitySelector<T extends BaseEntity>({
169
169
  hasValue(effectiveValue) ? "" : "text-muted-foreground"
170
170
  } ${disabled ? "cursor-not-allowed opacity-50" : ""}`}
171
171
  >
172
- <span className="min-w-0 flex-1 truncate text-left">
172
+ <span className="min-w-0 flex-1 truncate text-start">
173
173
  {hasValue(effectiveValue)
174
174
  ? getSelectedItemDisplay
175
175
  ? getSelectedItemDisplay(effectiveValue)
@@ -178,7 +178,7 @@ export default function EntitySelector<T extends BaseEntity>({
178
178
  </span>
179
179
  {hasValue(effectiveValue) && !disabled && (
180
180
  <CircleX
181
- className="text-muted hover:text-destructive ml-auto h-4 w-4 shrink-0 cursor-pointer"
181
+ className="text-muted hover:text-destructive ms-auto h-4 w-4 shrink-0 cursor-pointer"
182
182
  onClick={(e) => {
183
183
  e.stopPropagation();
184
184
  e.preventDefault();
@@ -191,19 +191,19 @@ export default function EntitySelector<T extends BaseEntity>({
191
191
  <PopoverContent align="start" className="w-(--anchor-width) pointer-events-auto">
192
192
  <Command shouldFilter={false}>
193
193
  <div className="relative mb-2 w-full">
194
- <SearchIcon className="text-muted-foreground absolute top-2.5 left-2.5 h-4 w-4" />
194
+ <SearchIcon className="text-muted-foreground absolute top-2.5 start-2.5 h-4 w-4" />
195
195
  <Input
196
196
  placeholder={placeholder}
197
197
  type="text"
198
- className="w-full pr-8 pl-8"
198
+ className="w-full pe-8 ps-8"
199
199
  onChange={(e) => setSearchTerm(e.target.value)}
200
200
  value={searchTerm}
201
201
  />
202
202
  {isSearching ? (
203
- <RefreshCwIcon className="text-muted-foreground absolute top-2.5 right-2.5 h-4 w-4 animate-spin" />
203
+ <RefreshCwIcon className="text-muted-foreground absolute top-2.5 end-2.5 h-4 w-4 animate-spin" />
204
204
  ) : searchTermRef.current ? (
205
205
  <XIcon
206
- className="absolute top-2.5 right-2.5 h-4 w-4 cursor-pointer"
206
+ className="absolute top-2.5 end-2.5 h-4 w-4 cursor-pointer"
207
207
  onClick={() => {
208
208
  setSearchTerm("");
209
209
  search("");
@@ -221,9 +221,9 @@ export default function EntitySelector<T extends BaseEntity>({
221
221
  onSelect={handleCreateNew}
222
222
  >
223
223
  {isCreating ? (
224
- <RefreshCwIcon className="mr-2 h-4 w-4 animate-spin" />
224
+ <RefreshCwIcon className="me-2 h-4 w-4 animate-spin" />
225
225
  ) : (
226
- <Plus className="mr-2 h-4 w-4" />
226
+ <Plus className="me-2 h-4 w-4" />
227
227
  )}
228
228
  <span>{createNewLabel ? createNewLabel(searchTerm) : searchTerm.trim() || ""}</span>
229
229
  </CommandItem>
@@ -317,6 +317,7 @@ export const FileUploaderItem = forwardRef<HTMLDivElement, { index: number } & R
317
317
  </div>
318
318
  <button
319
319
  type="button"
320
+ // rtl-ok: already direction-aware — resolves the physical corner from the component's own `dir` prop
320
321
  className={cn("absolute", direction === "rtl" ? "top-1 left-1" : "top-1 right-1")}
321
322
  onClick={() => removeFileFromSet(index)}
322
323
  >
@@ -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