@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
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.1",
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",
@@ -117,17 +117,17 @@
117
117
  }
118
118
  },
119
119
  "dependencies": {
120
- "@base-ui/react": "1.6.0",
121
- "@blocknote/core": "^0.52.1",
122
- "@blocknote/react": "^0.52.1",
123
- "@blocknote/shadcn": "^0.52.1",
124
- "@blocknote/xl-ai": "^0.52.1",
120
+ "@base-ui/react": "1.7.0",
121
+ "@blocknote/core": "^0.54.0",
122
+ "@blocknote/react": "^0.54.0",
123
+ "@blocknote/shadcn": "^0.54.0",
124
+ "@blocknote/xl-ai": "^0.54.0",
125
125
  "@dnd-kit/core": "^6.3.1",
126
126
  "@dnd-kit/sortable": "^10.0.0",
127
127
  "@dnd-kit/utilities": "^3.2.2",
128
128
  "@floating-ui/react": "^0.27.20",
129
129
  "@handlewithcare/prosemirror-inputrules": "^0.1.4",
130
- "@hookform/resolvers": "^5.5.7",
130
+ "@hookform/resolvers": "^5.9.1",
131
131
  "@radix-ui/react-label": "^2.1.15",
132
132
  "@radix-ui/react-slot": "^1.3.3",
133
133
  "@simplewebauthn/browser": "^13.3.0",
@@ -135,7 +135,7 @@
135
135
  "class-variance-authority": "^0.7.1",
136
136
  "clsx": "^2.1.1",
137
137
  "cmdk": "^1.1.1",
138
- "codice-fiscale-js": "^2.3.23",
138
+ "codice-fiscale-js": "~2.3.23",
139
139
  "commander": "^15.0.0",
140
140
  "cookies-next": "^6.1.1",
141
141
  "d3": "^7.9.0",
@@ -143,30 +143,30 @@
143
143
  "date-fns": "^4.4.0",
144
144
  "embla-carousel-react": "^8.6.0",
145
145
  "fuse.js": "^7.5.0",
146
- "input-otp": "^1.4.2",
147
- "jotai": "^2.20.2",
146
+ "input-otp": "^1.5.0",
147
+ "jotai": "^2.20.3",
148
148
  "jotai-family": "^1.1.0",
149
- "lucide-react": "^1.28.0",
149
+ "lucide-react": "^1.34.0",
150
150
  "next-mdx-remote": "^6.0.0",
151
151
  "next-themes": "^0.4.6",
152
152
  "pako": "^3.0.1",
153
153
  "qrcode.react": "^4.2.0",
154
154
  "react-day-picker": "^10.0.1",
155
- "react-dropzone": "^19.1.1",
156
- "react-hook-form": "7.83.0",
155
+ "react-dropzone": "^20.1.1",
156
+ "react-hook-form": "7.86.0",
157
157
  "react-markdown": "^10.1.0",
158
- "react-resizable-panels": "^4.12.2",
158
+ "react-resizable-panels": "^4.12.3",
159
159
  "recharts": "^3.10.1",
160
160
  "rehype-autolink-headings": "^7.1.0",
161
161
  "rehype-slug": "^6.0.0",
162
162
  "remark-gfm": "^4.0.1",
163
- "shadcn": "^4.16.1",
164
- "shepherd.js": "^15.2.2",
163
+ "shadcn": "^4.19.0",
164
+ "shepherd.js": "^15.3.0",
165
165
  "socket.io-client": "^4.8.3",
166
- "sonner": "^2.0.7",
166
+ "sonner": "^2.0.8",
167
167
  "tailwind-merge": "^3.6.0",
168
168
  "tw-animate-css": "^1.4.0",
169
- "uuid": "^14.0.1",
169
+ "uuid": "^14.0.2",
170
170
  "vaul": "^1.1.2",
171
171
  "zod": "^4.4.3"
172
172
  },
@@ -175,40 +175,40 @@
175
175
  "@eslint/js": "^9.39.5",
176
176
  "@semantic-release/changelog": "^7.0.0",
177
177
  "@semantic-release/git": "^11.0.1",
178
- "@stripe/react-stripe-js": "^6.8.0",
179
- "@stripe/stripe-js": "^9.13.0",
178
+ "@stripe/react-stripe-js": "^6.8.2",
179
+ "@stripe/stripe-js": "^9.14.0",
180
180
  "@tanstack/react-table": "^8.21.3",
181
181
  "@testing-library/dom": "^10.4.1",
182
- "@testing-library/jest-dom": "^7.0.0",
182
+ "@testing-library/jest-dom": "^7.0.1",
183
183
  "@testing-library/react": "^16.3.2",
184
- "@testing-library/user-event": "^14.6.1",
184
+ "@testing-library/user-event": "^14.6.6",
185
185
  "@types/d3": "^7.4.3",
186
186
  "@types/dagre": "^0.7.54",
187
- "@types/node": "^26.1.2",
187
+ "@types/node": "^26.3.0",
188
188
  "@types/qrcode.react": "^3.0.0",
189
189
  "@types/react": "19.2.18",
190
- "@types/react-dom": "19.2.4",
191
- "@typescript-eslint/eslint-plugin": "^8.65.0",
192
- "@typescript-eslint/parser": "^8.65.0",
193
- "@vitejs/plugin-react": "^6.0.5",
194
- "@vitest/coverage-v8": "^4.1.10",
195
- "@vitest/ui": "^4.1.10",
190
+ "@types/react-dom": "19.2.5",
191
+ "@typescript-eslint/eslint-plugin": "^8.68.0",
192
+ "@typescript-eslint/parser": "^8.68.0",
193
+ "@vitejs/plugin-react": "^6.1.0",
194
+ "@vitest/coverage-v8": "^4.1.11",
195
+ "@vitest/ui": "^4.1.11",
196
196
  "conventional-changelog-conventionalcommits": "9.3.1",
197
197
  "eslint": "^9.39.5",
198
198
  "eslint-config-prettier": "^10.1.8",
199
199
  "eslint-plugin-prettier": "^5.5.6",
200
- "globals": "^17.8.0",
200
+ "globals": "^17.11.0",
201
201
  "jsdom": "^30.0.1",
202
- "next": "16.3.0",
203
- "next-intl": "^4.13.4",
202
+ "next": "16.3.2",
203
+ "next-intl": "^4.13.7",
204
204
  "prettier": "^3.9.6",
205
205
  "react": "19.2.8",
206
206
  "react-dom": "19.2.8",
207
- "react-hook-form": "7.83.0",
207
+ "react-hook-form": "7.86.0",
208
208
  "semantic-release": "^25.0.8",
209
209
  "tsup": "^8.5.1",
210
210
  "typescript": "^5.9.3",
211
- "vitest": "^4.1.10"
211
+ "vitest": "^4.1.11"
212
212
  },
213
213
  "keywords": [
214
214
  "nextjs",
@@ -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
  >