Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ jobs:
- name: Lint
run: pnpm lint

- name: RTL guard (no new physical-direction classes)
run: pnpm rtl:scan

- name: Type check
run: pnpm typecheck

Expand Down
54 changes: 51 additions & 3 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { webchartBrand } from '../src/brands/webchart';
import type { BrandConfig } from '../src/brands/types';
import { CodeLookup } from '../src/components/CodeLookup';
import { CodeLookupProvider } from '../src/components/CodeLookup/context';
import { isRtlLocale } from '../src/hooks/useDirection';

// Map of available brands
const brands: Record<string, BrandConfig> = {
Expand All @@ -29,6 +30,18 @@ const brands: Record<string, BrandConfig> = {
webchart: webchartBrand,
};

/*
* Resolve the effective text direction from the direction/locale globals.
* 'auto' derives it from the locale (rtl for ar/he/fa/ur).
*/
function resolveGlobalDirection(
globals: Record<string, unknown>
): 'ltr' | 'rtl' {
const direction = (globals?.direction as string) || 'auto';
if (direction === 'ltr' || direction === 'rtl') return direction;
return isRtlLocale((globals?.locale as string) || 'en') ? 'rtl' : 'ltr';
}

/*
* Global theme listener — ensures data-theme and brand styles are applied
* even on docs-only MDX pages (like Introduction) where no story decorator runs.
Expand All @@ -55,6 +68,16 @@ function applyGlobalTheme(globals: Record<string, unknown>) {
document.body.classList.remove('condensed');
}

// Apply text direction (RTL preview) at the document level so CSS logical
// properties and `rtl:` variants respond everywhere, including docs pages.
document.documentElement.setAttribute('dir', resolveGlobalDirection(globals));
// Keep the document language in sync with the locale global so screen
// readers and locale-sensitive text shaping reflect the selected locale.
document.documentElement.setAttribute(
'lang',
(globals?.locale as string) || 'en'
);

document.body.style.backgroundColor = semanticColors.background;
document.body.style.color = semanticColors.foreground;
applyBrandStyles(brand, isDark);
Expand Down Expand Up @@ -84,7 +107,13 @@ try {
const [key, value] = pair.split(':');
if (key && value) globals[key] = value;
}
if (globals.theme || globals.brand || globals.density) {
if (
globals.theme ||
globals.brand ||
globals.density ||
globals.direction ||
globals.locale
) {
applyGlobalTheme(globals);
}
} catch {
Expand Down Expand Up @@ -201,11 +230,13 @@ const withBrand: Decorator = (Story, context) => {
const semanticColors = isDark ? brand.colors.dark : brand.colors.light;

const isCondensed = context.globals.density === 'condensed';
const direction = context.globals.direction as string | undefined;
const locale = context.globals.locale as string | undefined;

useEffect(() => {
// Delegate to shared applyGlobalTheme to keep a single source of truth
applyGlobalTheme(context.globals);
}, [brand, isDark, isCondensed, semanticColors]);
}, [brand, isDark, isCondensed, semanticColors, direction, locale]);

// Load Google Fonts for the brand
const fontLink = useMemo(() => {
Expand Down Expand Up @@ -257,11 +288,14 @@ const withCodeLookup: Decorator = (Story, context) => {
const locale = (context.globals.locale as string) || 'en';
const userId = (context.globals.user as string) || 'anonymous';
const trusted = context.globals.device === 'trusted';
// Codify shards only exist for these locales; fall back to English for
// preview-only locales (e.g. the RTL Arabic sample).
const lookupLocale = ['en', 'es'].includes(locale) ? locale : 'en';
return (
<CodeLookupProvider
component={CodeLookup}
indexUrl="/codify"
locale={locale}
locale={lookupLocale}
memory={{ userId, storage: trusted ? 'local' : 'session' }}
>
<Story />
Expand All @@ -275,6 +309,7 @@ const preview: Preview = {
theme: 'light',
density: 'standard',
locale: 'en',
direction: 'auto',
user: 'anonymous',
device: 'public',
},
Expand Down Expand Up @@ -331,6 +366,19 @@ const preview: Preview = {
items: [
{ value: 'en', title: '🇺🇸', right: 'English' },
{ value: 'es', title: '🇪🇸', right: 'Español (sample)' },
{ value: 'ar', title: '🇸🇦', right: 'العربية (RTL sample)' },
],
},
},
direction: {
name: 'Direction',
description: 'Text direction (LTR/RTL preview)',
toolbar: {
icon: 'transfer',
items: [
{ value: 'auto', title: '🔁', right: 'Auto (from language)' },
{ value: 'ltr', title: '➡️', right: 'LTR' },
{ value: 'rtl', title: '⬅️', right: 'RTL' },
],
},
},
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@
"typecheck": "tsc --noEmit",
"lint": "eslint \"src/**/*.{ts,tsx}\"",
"lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
"rtl:scan": "node scripts/rtl-scan.mjs",
"rtl:scan:update": "node scripts/rtl-scan.mjs --update",
"format": "prettier --check \"src/**/*.{ts,tsx,css}\"",
"format:fix": "prettier --write \"src/**/*.{ts,tsx,css}\"",
"test": "vitest run",
Expand Down
128 changes: 128 additions & 0 deletions scripts/rtl-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
{
"src/components/AGGrid/EnhancedCellRenderers.tsx": 2,
"src/components/AI/AIChatModal.tsx": 7,
"src/components/AI/AIMessage.tsx": 1,
"src/components/AI/HeyOzwell/OzwellSettingsMenu.tsx": 1,
"src/components/AI/HeyOzwell/VisitScribe.tsx": 2,
"src/components/AI/HeyOzwell/VoiceManager.tsx": 1,
"src/components/AI/HeyOzwell/VoiceSetup.tsx": 1,
"src/components/AI/Reconciliation.tsx": 1,
"src/components/AddContactModal/AddContactModal.tsx": 2,
"src/components/AdditionalFields/AdditionalFields.tsx": 2,
"src/components/Alert/Alert.tsx": 4,
"src/components/AllergyList/AllergyList.tsx": 1,
"src/components/AppHeader/AppHeader.tsx": 5,
"src/components/Assessment/Assessment.tsx": 6,
"src/components/AudioPlayer/AudioPlayer.tsx": 1,
"src/components/AuthDialog/AuthDialog.tsx": 9,
"src/components/Autocomplete/Autocomplete.tsx": 1,
"src/components/Avatar/Avatar.tsx": 1,
"src/components/Badge/Badge.tsx": 1,
"src/components/BookingDialog/BookingDialog.tsx": 3,
"src/components/BusinessHours/BusinessHours.tsx": 2,
"src/components/BusinessHoursEditor/BusinessHoursEditor.tsx": 2,
"src/components/Card/Card.tsx": 6,
"src/components/CheckrIntegration/CheckrIntegration.tsx": 7,
"src/components/ClaimProviderForm/ClaimProviderForm.tsx": 2,
"src/components/CodeLookup/CodeLookup.tsx": 5,
"src/components/CommandPalette/CommandPalette.tsx": 10,
"src/components/ConditionEditor/ConditionEditor.tsx": 3,
"src/components/ConnectionStatus/ConnectionStatus.tsx": 4,
"src/components/CookieConsent/CookieConsent.tsx": 10,
"src/components/CountBadge/CountBadge.tsx": 9,
"src/components/CountryCodeDropdown/CountryCodeDropdown.tsx": 1,
"src/components/CreateInvoiceModal/CreateInvoiceModal.tsx": 3,
"src/components/CreateReferralModal/CreateReferralModal.tsx": 4,
"src/components/DashboardWidget/DashboardWidget.tsx": 9,
"src/components/DateInput/DateInput.tsx": 3,
"src/components/DateRangePicker/DateRangePicker.tsx": 7,
"src/components/DocumentScanner/DocumentDetectionOverlay.tsx": 16,
"src/components/DocumentScanner/FilePreview.tsx": 1,
"src/components/DocumentScanner/WebcamModal.tsx": 15,
"src/components/Dropdown/Dropdown.tsx": 1,
"src/components/EditUserRoleModal/EditUserRoleModal.tsx": 2,
"src/components/EmployeeProfile/EmployeeProfile.tsx": 1,
"src/components/EmployerContactCard/EmployerContactCard.tsx": 1,
"src/components/EmployerList/EmployerList.tsx": 3,
"src/components/EmployerPricingCard/EmployerPricingCard.tsx": 3,
"src/components/EmployerServiceModal/EmployerServiceModal.tsx": 11,
"src/components/EmployerView/EmployerView.tsx": 2,
"src/components/ErrorPage/ErrorPage.tsx": 1,
"src/components/FileManager/FileManager.tsx": 2,
"src/components/FloatingWindow/FloatingWindow.tsx": 13,
"src/components/HRISProviderSelector/HRISProviderSelector.tsx": 5,
"src/components/HealthSurveillance/HealthSurveillance.tsx": 3,
"src/components/HelpSupportPanel/HelpSupportPanel.tsx": 3,
"src/components/Input/Input.tsx": 1,
"src/components/InventoryManager/InventoryManager.tsx": 11,
"src/components/InviteUserModal/InviteUserModal.tsx": 2,
"src/components/InvoiceList/InvoiceList.tsx": 9,
"src/components/InvoicePaymentPage/InvoicePaymentPage.tsx": 5,
"src/components/InvoiceView/InvoiceView.tsx": 12,
"src/components/Label/Label.tsx": 1,
"src/components/LanguageSelector/LanguageSelector.tsx": 7,
"src/components/LoadingPage/LoadingPage.tsx": 1,
"src/components/Markdown/CsvBlock.tsx": 2,
"src/components/Markdown/SurveyBlock.tsx": 1,
"src/components/MediaEditor/MediaEditor.tsx": 8,
"src/components/MediaEditor/ScriptPanel.tsx": 2,
"src/components/MediaEditor/SpeedMarkerMenu.tsx": 2,
"src/components/MedicationList/MedicationList.tsx": 1,
"src/components/Messaging/AttachmentPicker.tsx": 3,
"src/components/Messaging/ConversationHeader.tsx": 4,
"src/components/Messaging/MessageBubble.tsx": 7,
"src/components/Messaging/MessageComposer.tsx": 7,
"src/components/Messaging/MessageList.tsx": 4,
"src/components/Messaging/MessageThread.tsx": 2,
"src/components/OnboardingWizard/OnboardingWizard.tsx": 6,
"src/components/OrderCard/OrderCard.tsx": 2,
"src/components/OrderConfirmationWizard/OrderConfirmationWizard.tsx": 5,
"src/components/OrderList/OrderList.tsx": 4,
"src/components/OrderLookupForm/OrderLookupForm.tsx": 2,
"src/components/OrderSidebar/OrderSidebar.tsx": 3,
"src/components/PatientHeader/PatientHeader.tsx": 3,
"src/components/PaymentHistoryTable/PaymentHistoryTable.tsx": 8,
"src/components/PendingClaimsTable/PendingClaimsTable.tsx": 5,
"src/components/PermissionsEditor/PermissionsEditor.tsx": 10,
"src/components/PresentingProblems/PresentingProblems.tsx": 3,
"src/components/ProblemList/ProblemList.tsx": 7,
"src/components/ProductVersion/ProductVersion.tsx": 1,
"src/components/ProviderSearchBar/ProviderSearchBar.tsx": 2,
"src/components/ProviderSearchFilters/ProviderSearchFilters.tsx": 15,
"src/components/ProviderSelector/ProviderSelector.tsx": 5,
"src/components/ProviderUsersTable/ProviderUsersTable.tsx": 2,
"src/components/QuickAction/QuickAction.tsx": 1,
"src/components/QuickLinksCard/QuickLinksCard.tsx": 3,
"src/components/RecurringServiceCard/RecurringServiceCard.tsx": 3,
"src/components/RejectionModal/RejectionModal.tsx": 3,
"src/components/ReportDashboard/ReportDashboard.tsx": 1,
"src/components/ResultsEntryForm/ResultsEntryForm.tsx": 9,
"src/components/RichTextEditor/RichTextEditor.tsx": 1,
"src/components/RowActionToolbar/RowActionToolbar.tsx": 5,
"src/components/ScheduleCalendar/ScheduleCalendar.tsx": 7,
"src/components/ServiceAccordion/ServiceAccordion.tsx": 4,
"src/components/ServiceBadge/ServiceBadge.tsx": 5,
"src/components/ServiceCard/ServiceCard.tsx": 1,
"src/components/ServiceGeneralSettings/ServiceGeneralSettings.tsx": 5,
"src/components/ServicePicker/ServicePicker.tsx": 6,
"src/components/ServicePricingManager/ServicePricingManager.tsx": 6,
"src/components/SetupServiceModal/SetupServiceModal.tsx": 8,
"src/components/Sheet/Sheet.tsx": 4,
"src/components/Sidebar/Sidebar.tsx": 14,
"src/components/SiteFooter/SiteFooter.tsx": 2,
"src/components/SiteHeader/SiteHeader.tsx": 10,
"src/components/Slider/Slider.tsx": 1,
"src/components/SuperChat/SuperChat.tsx": 1,
"src/components/SuperChat/SuperChatConversations.tsx": 2,
"src/components/SuperChat/parts.tsx": 1,
"src/components/SuperChat/plugins/code.tsx": 1,
"src/components/SuperChat/plugins/nitroTable.tsx": 1,
"src/components/SuperChat/render/createMarkdownRenderer.tsx": 1,
"src/components/Table/Table.tsx": 3,
"src/components/TableOfContents/TableOfContents.tsx": 4,
"src/components/Text/Text.tsx": 2,
"src/components/Timeline/Timeline.tsx": 1,
"src/components/Toast/Toast.tsx": 4,
"src/components/TranscriptView/TranscriptView.tsx": 1,
"src/components/WebChartReportViewer/WebChartReportViewer.tsx": 2
}
Loading
Loading