Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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