Browse / github.com/storytold/artcraft / patch
frontend: lang/dir on the document and logical start/end styling for RTL
frontend: lang/dir on the document and logical start/end styling for RTL Groundwork for right-to-left interface languages in the web frontend, from the language-support plan in storytold/craft-libs#198 (track 8, N1). Nothing changes for English: the document stays `lang="en" dir="ltr"`. **Document language** - `setDocumentLanguage(tag, dir?)` in `@storyteller/common` sets `lang` and `dir` on `<html>`, deriving the direction from the tag (`Intl.Locale` text info, with a language/script list as fallback). The app calls it once at startup with the default `en`. **Logical styling** - 422 physical Tailwind classes become logical: `ml/mr` to `ms/me`, `pl/pr` to `ps/pe`
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- aiongg (imported from pull request #2519, open)
- Base
800217ed04d10fc12706c28e6e91ae2a6b286092- Size
- +635 −435 in 148 files
- Review
- pending
- Shared
- 2026-10-28
- Id
546c05efad11bbee4d664893
How it went
No other operator has reported using it yet.
Diff
raw · frontend/apps/artcraft/app/index.html frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx frontend/apps/artcraft/app/src/components/generation-feed/CreateEmptyState.tsx frontend/apps/artcraft/app/src/components/generation-feed/DesktopCreatePageShell.tsx frontend/apps/artcraft/app/src/components/reusable/ConfirmationModal.tsx frontend/apps/artcraft/app/src/components/reusable/CostModal.tsx frontend/apps/artcraft/app/src/components/reusable/UploadModalMedia/UploadFilesMedia.tsx frontend/apps/artcraft/app/src/components/reusable/UploadModalMedia/UploadImageMedia.tsx frontend/apps/artcraft/app/src/components/signaled/TopBar/AppsQuickMenu.tsx frontend/apps/artcraft/app/src/components/signaled/TopBar/ProfileDropdown.tsx frontend/apps/artcraft/app/src/components/signaled/TopBar/SceneTitleInput.tsx frontend/apps/artcraft/app/src/components/signaled/TopBar/TaskQueue.tsx frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx frontend/apps/artcraft/app/src/index.tsx frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx frontend/apps/artcraft/app/src/pages/PageApps/AppsIndexPage.tsx frontend/apps/artcraft/app/src/pages/PageBackgroundChange/PageBackgroundChange.tsx frontend/apps/artcraft/app/src/pages/PageDraw/BaseImageSelector.tsx frontend/apps/artcraft/app/src/pages/PageImageWatermarkRemover/ImageWatermarkRemover.tsx frontend/apps/artcraft/app/src/pages/PageStoryboard/Storyboard.tsx frontend/apps/artcraft/app/src/pages/PageVideoFrameExtractor/VideoFrameExtractor.tsx frontend/apps/artcraft/app/src/pages/PageVideoWatermarkRemover/VideoWatermarkRemover.tsx frontend/apps/artcraft/app/src/styles/base.css frontend/libs/common/pnpm-lock.yaml frontend/libs/common/src/lib/utils/document-language.spec.ts frontend/libs/common/src/lib/utils/document-language.ts frontend/libs/common/src/lib/utils/index.ts frontend/libs/components/audio-player/src/lib/WaveformAudioPlayer.tsx frontend/libs/components/button-dropdown/src/lib/button-dropdown.tsx frontend/libs/components/button-icon-select/src/lib/button-icon-select.tsx frontend/libs/components/camera-settings-modal/src/lib/camera-settings-modal.tsx frontend/libs/components/file-uploader/src/lib/drag-and-drop-zone.tsx frontend/libs/components/gallery-modal/src/lib/DragGhost.tsx frontend/libs/components/gallery-modal/src/lib/GalleryDraggableItem.tsx frontend/libs/components/gallery-modal/src/lib/GalleryFolderChip.tsx frontend/libs/components/gallery-modal/src/lib/GalleryItemMenuItems.tsx frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx frontend/libs/components/generation-list/src/lib/FailedRow.tsx frontend/libs/components/generation-list/src/lib/GalleryCard.tsx frontend/libs/components/generation-list/src/lib/GalleryRow.tsx frontend/libs/components/generation-list/src/lib/GenerationGridView.tsx frontend/libs/components/generation-list/src/lib/PendingCard.tsx frontend/libs/components/generation-list/src/lib/PendingRow.tsx frontend/libs/components/help-menu/src/lib/help-menu.tsx frontend/libs/components/input/src/lib/input.tsx frontend/libs/components/keybinds/src/lib/settings/KeybindsSettings.tsx frontend/libs/components/label/src/lib/label.tsx frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx frontend/libs/components/lightbox-modal/src/lib/tags/TagChipInput.tsx frontend/libs/components/list-dropdown/src/lib/ListDropdown.tsx frontend/libs/components/login-modal/src/lib/DesktopLoginBridge.tsx frontend/libs/components/login-modal/src/lib/artcraft-signup.tsx frontend/libs/components/login-modal/src/lib/login-modal.tsx frontend/libs/components/modal/src/lib/modal.tsx frontend/libs/components/model-selector/src/lib/classy-model-selector.tsx frontend/libs/components/moodboard/src/lib/BoardPicker.tsx frontend/libs/components/moodboard/src/lib/MoodboardWorkspace.tsx frontend/libs/components/moodboard/src/lib/canvas/Moodboard.tsx frontend/libs/components/moodboard/src/lib/canvas/overlays/RecenterIndicator.tsx frontend/libs/components/moodboard/src/lib/grid/BoardCard.tsx frontend/libs/components/moodboard/src/lib/grid/BoardGridToolbar.tsx frontend/libs/components/moodboard/src/lib/grid/ItemInspector.tsx frontend/libs/components/moodboard/src/lib/grid/PresentationView.tsx frontend/libs/components/moodboard/src/lib/grid/SectionHeader.tsx frontend/libs/components/moodboard/src/lib/grid/SelectionBar.tsx frontend/libs/components/moodboard/src/lib/grid/SmartSearchBar.tsx frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx frontend/libs/components/pagedraw/src/lib/PageDraw.tsx frontend/libs/components/pagedraw/src/lib/PromptEditor/ImagePreview.tsx frontend/libs/components/pagedraw/src/lib/components/ui/InpaintToolBar.tsx frontend/libs/components/pagedraw/src/lib/components/ui/SideToolbar.tsx frontend/libs/components/pagescene/src/lib/Stage3DBody.tsx frontend/libs/components/pagescene/src/lib/comps/AssetMenu/AnimationsModal.tsx frontend/libs/components/pagescene/src/lib/comps/AssetMenu/AssetMenu.tsx frontend/libs/components/pagescene/src/lib/comps/AssetMenu/AssetModal.tsx frontend/libs/components/pagescene/src/lib/comps/AssetMenu/shared/ItemElement.tsx frontend/libs/components/pagescene/src/lib/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx frontend/libs/components/pagescene/src/lib/comps/ControlsTopButtons/ControlsTopButtons.tsx frontend/libs/components/pagescene/src/lib/comps/ControlsTopButtons/LoadUserScenes/LoadUserScenes.tsx frontend/libs/components/pagescene/src/lib/comps/ControlsTopButtons/NewSceneFromTemplate/NewSceneFromTemplate.tsx frontend/libs/components/pagescene/src/lib/comps/ControlsTopButtons/ScenePicker/SceneCard.tsx frontend/libs/components/pagescene/src/lib/comps/EditorLoadingBar.tsx frontend/libs/components/pagescene/src/lib/comps/EntranceDebugPanel.tsx frontend/libs/components/pagescene/src/lib/comps/Outliner/Outliner.tsx frontend/libs/components/pagescene/src/lib/comps/PerfStatsOverlay.tsx frontend/libs/components/pagescene/src/lib/comps/PreviewBox/PreviewBox.tsx frontend/libs/components/pagescene/src/lib/comps/PreviewEngineCamera/PreviewEngineCamera.tsx frontend/libs/components/pagescene/src/lib/comps/RenderOverlay/RenderOverlay.tsx frontend/libs/components/pagescene/src/lib/comps/Timeline/TimelineClipRow.tsx frontend/libs/components/pagescene/src/lib/comps/Timeline/TimelineEditor.tsx frontend/libs/components/popover/src/lib/popover.tsx frontend/libs/components/pricing-modal/src/lib/cost-breakdown-modal.tsx frontend/libs/components/pricing-modal/src/lib/credits-modal.tsx frontend/libs/components/pricing-modal/src/lib/pricing-modal.tsx frontend/libs/components/pricing-table/src/lib/pricing-promo-banner.tsx frontend/libs/components/promptbox/src/lib/CharactersModal.tsx frontend/libs/components/promptbox/src/lib/ImagePromptRow.tsx frontend/libs/components/promptbox/src/lib/MentionChipMenu.tsx frontend/libs/components/promptbox/src/lib/MentionTextarea.tsx frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx frontend/libs/components/promptbox/src/lib/PromptBox3D.tsx frontend/libs/components/promptbox/src/lib/PromptBoxAudio.tsx frontend/libs/components/promptbox/src/lib/PromptBoxEdit.tsx frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx frontend/libs/components/promptbox/src/lib/PromptFullscreenButton.tsx frontend/libs/components/promptbox/src/lib/common/AudioReferenceRow.tsx frontend/libs/components/promptbox/src/lib/deck/DeckCard.tsx frontend/libs/components/promptbox/src/lib/deck/ReferenceDeck.tsx frontend/libs/components/select/src/lib/select.tsx frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/ArtcraftAccountBlock.tsx frontend/libs/components/settings-modal/src/lib/panes/AppearanceSettingsPane.tsx frontend/libs/components/settings-modal/src/lib/settings-modal.tsx frontend/libs/components/switch/src/lib/switch.tsx frontend/libs/components/upload-modal/src/lib/UploadModal3D/UploadFiles3D.tsx frontend/libs/components/upload-modal/src/lib/UploadModalImage/UploadFilesImage.tsx frontend/libs/components/upload-modal/src/lib/UploadModalSplat/UploadFilesSplat.tsx frontend/libs/components/vfx/src/lib/PromptBoxVFX.tsx frontend/libs/components/vfx/src/lib/VFXResultCard.tsx frontend/libs/components/video-editor/src/lib/components/editor/export-button.tsx frontend/libs/components/video-editor/src/lib/components/editor/mobile-gate.tsx frontend/libs/components/video-editor/src/lib/components/editor/scenes-view.tsx frontend/libs/components/video-editor/src/lib/components/section.tsx frontend/libs/components/video-editor/src/lib/components/ui/alert.tsx frontend/libs/components/video-editor/src/lib/components/ui/context-menu.tsx frontend/libs/components/video-editor/src/lib/components/ui/dialog.tsx frontend/libs/components/video-editor/src/lib/components/ui/dropdown-menu.tsx frontend/libs/components/video-editor/src/lib/components/ui/font-picker.tsx frontend/libs/components/video-editor/src/lib/components/ui/input.tsx frontend/libs/components/video-editor/src/lib/components/ui/resizable.tsx frontend/libs/components/video-editor/src/lib/components/ui/select.tsx frontend/libs/components/video-editor/src/lib/components/ui/sheet.tsx frontend/libs/components/video-editor/src/lib/components/ui/split-button.tsx frontend/libs/components/video-editor/src/lib/components/ui/switch.tsx frontend/libs/components/video-editor/src/lib/components/ui/textarea.tsx frontend/libs/components/video-editor/src/lib/panels/assets/draggable-item.tsx frontend/libs/components/video-editor/src/lib/panels/assets/tabbar.tsx frontend/libs/components/video-editor/src/lib/panels/assets/views/assets.tsx frontend/libs/components/video-editor/src/lib/panels/assets/views/base-panel.tsx frontend/libs/components/video-editor/src/lib/panels/assets/views/settings/background.tsx frontend/libs/components/video-editor/src/lib/panels/assets/views/settings/index.tsx frontend/libs/components/video-editor/src/lib/panels/properties/index.tsx frontend/libs/components/video-editor/src/lib/project/components/delete-project-dialog.tsx frontend/libs/components/video-editor/src/lib/sounds/components/assets-view.tsx frontend/libs/components/video-editor/src/lib/stickers/components/assets-view.tsx frontend/libs/components/video-editor/src/lib/timeline/components/timeline-element.tsx frontend/libs/components/viewer-3d/src/lib/viewer-3d.tsx frontend/libs/markdown-content/src/lib/pages/NewsPost.tsx
@@ -1,5 +1,5 @@ <!doctype html>-<html lang="en">+<html lang="en" dir="ltr"> <head> <title>ArtCraft</title>@@ -527,7 +527,7 @@ export const ImageTo3DExperience = ({ delay={100} zIndex={50} content={- <div className="flex flex-col gap-1.5 text-left">+ <div className="flex flex-col gap-1.5 text-start"> <Button variant="primary" icon={UploadIcon}@@ -621,7 +621,7 @@ export const ImageTo3DExperience = ({ {!isUploading && ( <button type="button"- className="absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-[3px] bg-black/60 text-white opacity-0 transition-all hover:bg-black group-hover:opacity-100"+ className="absolute end-2 top-2 flex h-6 w-6 items-center justify-center rounded-[3px] bg-black/60 text-white opacity-0 transition-all hover:bg-black group-hover:opacity-100" onClick={(event) => { event.stopPropagation(); setUploadedPreview(null);@@ -644,10 +644,11 @@ export const ImageTo3DExperience = ({ const renderTextMode = () => ( <div> <textarea+ dir="auto" ref={textareaRef} id={promptInputId} rows={1}- className="text-md max-h-[5.5em] w-full resize-none overflow-y-auto rounded-[3px] bg-transparent pr-2 pt-1 text-base-fg placeholder-base-fg/60 focus:outline-none"+ className="text-md max-h-[5.5em] w-full resize-none overflow-y-auto rounded-[3px] bg-transparent pe-2 pt-1 text-base-fg placeholder-base-fg/60 focus:outline-none" value={prompt} placeholder="Describe any object you want to generate from scratch..." onChange={(event) => setPrompt(event.target.value)}@@ -669,7 +670,7 @@ export const ImageTo3DExperience = ({ delay={100} zIndex={50} content={- <div className="flex flex-col gap-1.5 text-left">+ <div className="flex flex-col gap-1.5 text-start"> <Button variant="primary" icon={UploadIcon}@@ -740,6 +741,7 @@ export const ImageTo3DExperience = ({ </Tooltip> </div> <textarea+ dir="auto" ref={worldTextareaRef} rows={2} className="w-full resize-none overflow-y-auto rounded-[3px] border border-white/15 bg-ui-controls px-3 py-2.5 text-base text-base-fg placeholder-base-fg/60 transition-colors focus:border-white/60 focus:outline-none"@@ -780,7 +782,7 @@ export const ImageTo3DExperience = ({ {!img.isUploading && ( <button type="button"- className="absolute right-[2px] top-[2px] flex h-4 w-4 cursor-pointer items-center justify-center rounded-[3px] bg-black/60 text-white opacity-0 transition-colors hover:bg-red/70 group-hover:opacity-100"+ className="absolute end-[2px] top-[2px] flex h-4 w-4 cursor-pointer items-center justify-center rounded-[3px] bg-black/60 text-white opacity-0 transition-colors hover:bg-red/70 group-hover:opacity-100" onClick={(e) => { e.stopPropagation(); removeWorldImage(img.id);@@ -799,7 +801,7 @@ export const ImageTo3DExperience = ({ delay={100} zIndex={50} content={- <div className="flex flex-col gap-1.5 text-left">+ <div className="flex flex-col gap-1.5 text-start"> <Button variant="primary" icon={UploadIcon}@@ -865,6 +867,7 @@ export const ImageTo3DExperience = ({ {addButton} </div> <textarea+ dir="auto" ref={worldTextareaRef} rows={1} className="flex-1 resize-none overflow-y-auto rounded-[3px] border border-white/15 bg-ui-controls px-3 py-2 text-sm text-base-fg placeholder-base-fg/60 transition-colors focus:border-white/60 focus:outline-none"@@ -887,6 +890,7 @@ export const ImageTo3DExperience = ({ </div> {imageCountRow} <textarea+ dir="auto" ref={worldTextareaRef} rows={2} className="w-full resize-none overflow-y-auto rounded-[3px] border border-white/15 bg-ui-controls px-3 py-2.5 text-base text-base-fg placeholder-base-fg/60 transition-colors focus:border-white/60 focus:outline-none"@@ -930,7 +934,7 @@ export const ImageTo3DExperience = ({ <div className="relative z-10 h-full w-full p-8"> {!hasResults && (- <div className="pointer-events-none absolute left-0 top-[calc(50%-280px)] flex w-full justify-center">+ <div className="pointer-events-none absolute start-0 top-[calc(50%-280px)] flex w-full justify-center"> <CreateEmptyState title={title} subtitle={subtitle}@@ -958,7 +962,7 @@ export const ImageTo3DExperience = ({ className="h-full" /> {activeResult?.modelUrl && activeResult?.mediaToken && (- <div className="absolute right-4 top-4 z-10 flex gap-2">+ <div className="absolute end-4 top-4 z-10 flex gap-2"> <Button variant="primary" className="min-w-[120px]"@@ -1031,7 +1035,7 @@ export const ImageTo3DExperience = ({ key={result.id} onClick={() => setSelectedResultId(result.id)} className={twMerge(- "group flex w-full items-center gap-3 rounded-none border p-2 text-left transition-all hover:bg-white/5",+ "group flex w-full items-center gap-3 rounded-none border p-2 text-start transition-all hover:bg-white/5", isSelected ? "border-white bg-white/10" : "border-white/10 bg-transparent hover:border-white/30",@@ -1152,7 +1156,7 @@ export const ImageTo3DExperience = ({ </div> </animated.div> - <div className="absolute bottom-4 right-4 z-20 flex items-center gap-2">+ <div className="absolute bottom-4 end-4 z-20 flex items-center gap-2"> <CostCalculatorButton modelPage={ variant === "world" ? WORLD_MODEL_PAGE : OBJECT_MODEL_PAGE@@ -1207,7 +1211,7 @@ export const ImageTo3DExperience = ({ > <button type="button"- className="absolute right-4 top-4 flex h-8 w-8 items-center justify-center rounded-[3px] border border-white/15 bg-black/60 text-white transition-colors hover:border-white/30 hover:bg-black"+ className="absolute end-4 top-4 flex h-8 w-8 items-center justify-center rounded-[3px] border border-white/15 bg-black/60 text-white transition-colors hover:border-white/30 hover:bg-black" onClick={() => setPreviewImage(null)} > <XIcon className="h-4 w-4" />@@ -2,10 +2,10 @@ import { type ReactNode } from "react"; import { twMerge } from "tailwind-merge"; const CORNERS = [- "top-0 left-0",- "top-0 right-0",- "bottom-0 left-0",- "bottom-0 right-0",+ "top-0 start-0",+ "top-0 end-0",+ "bottom-0 start-0",+ "bottom-0 end-0", ]; interface CreateEmptyStateProps {@@ -27,7 +27,7 @@ export function CreateEmptyState({ return ( <section className={twMerge(- "create-empty-section relative w-full max-w-3xl px-6 text-left sm:px-10",+ "create-empty-section relative w-full max-w-3xl px-6 text-start sm:px-10", compact ? "py-8" : "py-10 sm:py-14", className, )}@@ -100,7 +100,7 @@ export function DesktopCreatePageShell({ {promptBox} {bottomRight && (- <div className="absolute bottom-4 right-4 z-20 flex items-center gap-2">+ <div className="absolute bottom-4 end-4 z-20 flex items-center gap-2"> {bottomRight} </div> )}@@ -64,7 +64,7 @@ export const ConfirmationModal = ({ leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" >- <Dialog.Panel className="w-full max-w-md transform overflow-hidden border border-white/15 bg-ui-modal p-5 text-left align-middle transition-all">+ <Dialog.Panel className="w-full max-w-md transform overflow-hidden border border-white/15 bg-ui-modal p-5 text-start align-middle transition-all"> <Dialog.Title as="h4" className="mb-4 font-display text-xl tracking-tight text-white"@@ -60,7 +60,7 @@ export const CostModal = ({ credits = 1, onClose }: CostModalProps) => { {credits} Credits </span> </div>- <div className="text-right text-xs text-base-fg/60">+ <div className="text-end text-xs text-base-fg/60"> ≈ {formattedPrice} {currencyOption.value} </div> </div>@@ -86,7 +86,7 @@ export const UploadFilesMedia = ({ <div className="relative m-auto aspect-square w-full overflow-hidden border border-white/15 bg-ui-controls"> {!assetFile.value && (- <h6 className="absolute left-0 top-1/2 -mt-5 w-full text-center">+ <h6 className="absolute start-0 top-1/2 -mt-5 w-full text-center"> File Preview </h6> )}@@ -87,7 +87,7 @@ export const UploadImageMedia = ({ <div className="relative m-auto aspect-square w-full overflow-hidden border border-white/15 bg-ui-controls"> {!assetFile.value && (- <H6 className="absolute left-0 top-1/2 -mt-5 w-full text-center">+ <H6 className="absolute start-0 top-1/2 -mt-5 w-full text-center"> File Preview </H6> )}@@ -38,7 +38,7 @@ const AppsQuickMenuSection = ({ onClick={() => goToApp(app.action)} disabled={!app.action} className={twMerge(- "group flex w-full items-center gap-3 rounded-[3px] px-2 py-2 text-left transition-colors",+ "group flex w-full items-center gap-3 rounded-[3px] px-2 py-2 text-start transition-colors", app.action ? "cursor-pointer hover:bg-white/[0.08]" : "cursor-default opacity-60",@@ -56,7 +56,7 @@ export default function ProfileDropdown() { > <Menu.Items static- className="absolute right-0 z-50 mt-2 w-48 origin-top-right overflow-hidden rounded-[3px] border border-white/15 bg-[#101014] focus:outline-none"+ className="absolute end-0 z-50 mt-2 w-48 origin-top-right overflow-hidden rounded-[3px] border border-white/15 bg-[#101014] focus:outline-none rtl:origin-top-left" > <Menu.Item key={0}> {({ active }) => (@@ -89,7 +89,7 @@ export const SceneTitleInput = ({ pageName }: Props) => { <div className={twMerge( "flex w-full items-center justify-center gap-1.5",- isSaving && "ml-3",+ isSaving && "ms-3", )} data-tauri-drag-region >@@ -112,11 +112,16 @@ export const SceneTitleInput = ({ pageName }: Props) => { onClick={handleShowInput} data-tauri-drag-region="false" >- <span className="truncate">{scene.value.title || ""}</span>- <PencilIcon className="ml-2 shrink-0 text-sm opacity-50" />+ <span dir="auto" className="truncate">+ {scene.value.title || ""}+ </span>+ <PencilIcon className="ms-2 shrink-0 text-sm opacity-50" /> </button> ) : (- <div className="max-w-[280px] truncate border border-transparent px-3 py-1.5 text-sm font-semibold text-white/80">+ <div+ dir="auto"+ className="max-w-[280px] truncate border border-transparent px-3 py-1.5 text-sm font-semibold text-white/80"+ > {scene.value.title || ""} </div> )}@@ -133,6 +138,7 @@ export const SceneTitleInput = ({ pageName }: Props) => { isSaving && "outline-white/30", )} isError={!isValid}+ dir="auto" value={scene.value.title || ""} onChange={handleChangeSceneTitle} onBlur={validateSceneTitle}@@ -141,7 +141,7 @@ const PromptLine = ({ : undefined } >- {prompt}+ <bdi>{prompt}</bdi> </div> </div> </Tooltip>@@ -213,7 +213,7 @@ const InProgressCard = ({ size="lg" /> </div> {task.refImageUrls!.length > 1 && (- <div className="absolute bottom-0.5 right-0.5 bg-black/60 px-1 text-[9px] text-white/80">+ <div className="absolute bottom-0.5 end-0.5 bg-black/60 px-1 text-[9px] text-white/80"> +{task.refImageUrls!.length - 1} </div> )}@@ -250,12 +250,12 @@ const InProgressCard = ({ </Tooltip> )} </div>- <div className="ml-2 shrink-0 text-xs tabular-nums text-base-fg/60">+ <div className="ms-2 shrink-0 text-xs tabular-nums text-base-fg/60"> {progressPercent}% </div> </div> {task.subtitle && (- <div className="mt-0.5 truncate text-xs text-base-fg opacity-60">+ <div dir="auto" className="mt-0.5 truncate text-xs text-base-fg opacity-60"> {task.subtitle} </div> )}@@ -274,7 +274,7 @@ const InProgressCard = ({ )} {task.prompt && <PromptLine prompt={task.prompt} className="mt-0" />} </div>- <div className="ml-auto flex shrink-0 items-center gap-1">+ <div className="ms-auto flex shrink-0 items-center gap-1"> {task.prompt && <CopyPromptButton prompt={task.prompt} />} {onDismiss && ( <button@@ -337,7 +337,7 @@ const CompletedCard = ({ {task.title} </div> {task.subtitle && (- <div className="mt-0.5 truncate text-xs text-base-fg opacity-60">+ <div dir="auto" className="mt-0.5 truncate text-xs text-base-fg opacity-60"> {task.subtitle} </div> )}@@ -348,7 +348,7 @@ const CompletedCard = ({ )} {task.prompt && <PromptLine prompt={task.prompt} />} </div>- <div className="ml-auto flex shrink-0 items-center gap-1">+ <div className="ms-auto flex shrink-0 items-center gap-1"> {task.prompt && <CopyPromptButton prompt={task.prompt} />} {onDismiss && ( <button@@ -416,7 +416,7 @@ const FailedCard = ({ size="lg" /> </div> {task.refImageUrls!.length > 1 && (- <div className="absolute bottom-0.5 right-0.5 bg-black/60 px-1 text-[9px] text-white/80">+ <div className="absolute bottom-0.5 end-0.5 bg-black/60 px-1 text-[9px] text-white/80"> +{task.refImageUrls!.length - 1} </div> )}@@ -441,7 +441,7 @@ const FailedCard = ({ </div> </div> {task.subtitle && (- <div className="mt-0.5 truncate text-xs text-base-fg opacity-60">+ <div dir="auto" className="mt-0.5 truncate text-xs text-base-fg opacity-60"> {task.subtitle} </div> )}@@ -486,7 +486,7 @@ const FailedCard = ({ )} {task.prompt && <PromptLine prompt={task.prompt} />} </div>- <div className="ml-auto flex shrink-0 items-center gap-1">+ <div className="ms-auto flex shrink-0 items-center gap-1"> {task.prompt && <CopyPromptButton prompt={task.prompt} />} {onDismiss && ( <button@@ -1008,7 +1008,7 @@ export const TaskQueue = () => { <Tooltip content="Task Queue" position="bottom" closeOnClick={true}> <div className="relative"> {badgeCount > 0 && (- <div className="absolute -right-1 -top-1 z-20 flex h-[15px] min-w-[15px] items-center justify-center bg-white px-1 font-mono text-[10px] font-semibold text-black ring-2 ring-[#0b0b0c]">+ <div className="absolute -end-1 -top-1 z-20 flex h-[15px] min-w-[15px] items-center justify-center bg-white px-1 font-mono text-[10px] font-semibold text-black ring-2 ring-[#0b0b0c]"> {badgeCount} </div> )}@@ -1069,7 +1069,7 @@ export const TaskQueue = () => { > <XIcon - className="mr-1" />+ className="me-1" /> Clear failed </button> </div>@@ -1094,7 +1094,7 @@ export const TaskQueue = () => { > <XIcon - className="mr-1" />+ className="me-1" /> Clear completed </button> </div>@@ -1171,31 +1171,31 @@ export const TaskQueue = () => { className="flex h-9 items-center justify-center bg-green-500/10 px-3 text-green-500 hover:bg-green-500/20" onClick={() => handleClearCompleted()} >- <BrushIcon className="mr-1.5" />+ <BrushIcon className="me-1.5" /> Clear completed </Button> <Button className="flex h-9 items-center justify-center bg-orange-500/10 px-3 text-orange-500 hover:bg-orange-500/20" onClick={() => handleClearStale()} >- <Trash2Icon className="mr-1.5" />+ <Trash2Icon className="me-1.5" /> Clear stale </Button> <Button className="flex h-9 items-center justify-center bg-red-500/10 px-3 text-red-400 hover:bg-red-500/20" onClick={() => handleClearFailed()} >- <Trash2Icon className="mr-1.5" />+ <Trash2Icon className="me-1.5" /> Clear failed </Button> <Button className="flex h-9 items-center justify-center bg-red-500/10 px-3 text-red-500 hover:bg-red-500/20" onClick={() => handleRemoveAll()} >- <BombIcon className="mr-1.5" />+ <BombIcon className="me-1.5" /> Remove all </Button>- <div className="mr-2 h-4 w-[1px] bg-base-fg/10" />+ <div className="me-2 h-4 w-[1px] bg-base-fg/10" /> <CloseButton onClick={() => setModalOpen(false)} /> </div> </div>@@ -169,7 +169,7 @@ const CreditsCoinWithStatus = ({ <CoinsIcon className="text-[11px] text-primary" /> {showBadge && ( <span- className={`absolute -right-1.5 -top-1.5 flex h-3 w-3 items-center justify-center ring-1 ring-ui-panel ${badgeColorClass}`}+ className={`absolute -end-1.5 -top-1.5 flex h-3 w-3 items-center justify-center ring-1 ring-ui-panel ${badgeColorClass}`} > <DynamicIcon icon={badgeIconDef} className="text-[7px]" /> </span>@@ -427,7 +427,7 @@ export const TopBar = ({ pageName }: Props) => { return ( <> <header- className="fixed left-0 top-0 z-[60] w-full border-b border-ui-border bg-ui-panel"+ className="fixed start-0 top-0 z-[60] w-full border-b border-ui-border bg-ui-panel" data-tauri-drag-region > <nav@@ -439,7 +439,7 @@ export const TopBar = ({ pageName }: Props) => { className={`flex items-center gap-3 ${platform === "macos" ? "ml-14" : ""}`} data-tauri-drag-region >- {/* <div className="mr-2" data-tauri-drag-region>+ {/* <div className="me-2" data-tauri-drag-region> <span className="sr-only" data-tauri-drag-region> ArtCraft </span>@@ -13,6 +13,7 @@ import { SyncStorytellerApiConfig } from "./api/SyncStorytellerApiConfig"; import { posthog } from "posthog-js"; import { SoundManager } from "@storyteller/soundboard"; import { useModelsStore } from "@storyteller/tauri-api";+import { setDocumentLanguage } from "@storyteller/common"; // TODO(bt,2025-04-19): Make these configurable const ENV = {@@ -97,6 +98,8 @@ const GlobalSettingsManager = ({ env }: { env: Record<string, string> }) => { }; // TODO: Replace environment variables from `root.tsx`+setDocumentLanguage();+ createRoot(document.getElementById("root")!).render( <> <StrictMode>@@ -569,7 +569,7 @@ export const Angles = () => { </div> {/* ── History stack (right side, like PageEdit) ── */}- <div className="absolute right-4 top-1/2 z-10 -translate-y-1/2">+ <div className="absolute end-4 top-1/2 z-10 -translate-y-1/2"> <HistoryStack onClear={handleHistoryClear} imageBundles={allBundles}@@ -645,7 +645,7 @@ export const Angles = () => { suffix="°" /> </div>- <span className="w-10 shrink-0 text-left font-mono text-xs tabular-nums text-base-fg/70">+ <span className="w-10 shrink-0 text-start font-mono text-xs tabular-nums text-base-fg/70"> {rotationToDisplay(angleConfig.rotation)}° </span> </div>@@ -664,7 +664,7 @@ export const Angles = () => { suffix="°" /> </div>- <span className="w-9 shrink-0 text-left font-mono text-xs tabular-nums text-base-fg/70">+ <span className="w-9 shrink-0 text-start font-mono text-xs tabular-nums text-base-fg/70"> {angleConfig.tilt}° </span> </div>@@ -682,7 +682,7 @@ export const Angles = () => { onChange={handleZoomSlider} /> </div>- <span className="w-9 shrink-0 text-left font-mono text-xs tabular-nums text-base-fg/70">+ <span className="w-9 shrink-0 text-start font-mono text-xs tabular-nums text-base-fg/70"> {angleConfig.zoom} </span> </div>@@ -710,8 +710,8 @@ export const Angles = () => { className={twMerge( "size-3.5 rounded-full bg-white transition", generateFromBestAngles- ? "translate-x-[18px]"- : "translate-x-1",+ ? "translate-x-[18px] rtl:-translate-x-[18px]"+ : "translate-x-1 rtl:-translate-x-1", )} /> </Switch>@@ -737,7 +737,7 @@ export const Angles = () => { </div> {/* ── Cost calculator + Help (bottom-right) ── */}- <div className="absolute bottom-4 right-4 z-20 flex items-center gap-2">+ <div className="absolute bottom-4 end-4 z-20 flex items-center gap-2"> <CostCalculatorButton modelPage={ANGLES_MODEL_PAGE} /> <HelpMenuButton /> </div>@@ -66,7 +66,7 @@ function AppCard({ app }: { app: FullAppItem }) { onClick={() => goToApp(app.action)} disabled={!enabled} className={twMerge(- "group relative flex h-full rounded-[3px] border border-ui-border bg-white/5 p-5 text-left transition-colors duration-150 focus-visible:border-primary",+ "group relative flex h-full rounded-[3px] border border-ui-border bg-white/5 p-5 text-start transition-colors duration-150 focus-visible:border-primary", enabled ? twMerge("cursor-pointer", palette.hoverStyle) : "cursor-default opacity-60", )} >@@ -146,7 +146,7 @@ export const PageBackgroundChange = () => { <div aria-hidden- className="pointer-events-none fixed bottom-0 left-0 right-0 z-20 h-72 bg-gradient-to-t from-ui-background via-ui-background/85 to-transparent"+ className="pointer-events-none fixed bottom-0 start-0 end-0 z-20 h-72 bg-gradient-to-t from-ui-background via-ui-background/85 to-transparent" /> <div@@ -152,7 +152,7 @@ export const BaseImageSelector = ({ /> </div> </div>- <div className="fixed bottom-6 right-6 z-20 flex items-center gap-2">+ <div className="fixed bottom-6 end-6 z-20 flex items-center gap-2"> <CostCalculatorButton modelPage={ModelPage.ImageEditor} /> <HelpMenuButton /> </div>@@ -142,7 +142,7 @@ export const ImageWatermarkRemover = () => { setImageUrl(""); setImageDimensions(null); }}- className="absolute right-3 top-3 z-10 border border-red/50 px-3 py-1.5 text-sm hover:border-red/80 hover:bg-red/80"+ className="absolute end-3 top-3 z-10 border border-red/50 px-3 py-1.5 text-sm hover:border-red/80 hover:bg-red/80" > Switch Image </Button>@@ -65,7 +65,7 @@ interface MetadataEditorProps { } const MetadataEditor = ({ board, onUpdate, onDelete, onUpload, onSketch }: MetadataEditorProps) => (- <aside className="flex w-[280px] shrink-0 flex-col border-r border-ui-panel-border bg-ui-panel">+ <aside className="flex w-[280px] shrink-0 flex-col border-e border-ui-panel-border bg-ui-panel"> <div className="flex-1 overflow-y-auto p-4"> <div className="flex flex-col gap-3"> <div className="hud-label text-base-fg/40">@@ -86,6 +86,7 @@ const MetadataEditor = ({ board, onUpdate, onDelete, onUpload, onSketch }: Metad <div> <label className="mb-1 block font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">Dialogue</label> <textarea+ dir="auto" value={board.dialogue} onChange={(e) => onUpdate({ dialogue: e.target.value })} placeholder="Character dialogue..."@@ -97,6 +98,7 @@ const MetadataEditor = ({ board, onUpdate, onDelete, onUpload, onSketch }: Metad <div> <label className="mb-1 block font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">Action</label> <textarea+ dir="auto" value={board.action} onChange={(e) => onUpdate({ action: e.target.value })} placeholder="On-screen action..."@@ -108,6 +110,7 @@ const MetadataEditor = ({ board, onUpdate, onDelete, onUpload, onSketch }: Metad <div> <label className="mb-1 block font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">Notes</label> <textarea+ dir="auto" value={board.notes} onChange={(e) => onUpdate({ notes: e.target.value })} placeholder="Director notes..."@@ -401,7 +404,7 @@ const SketchCanvas = ({ initialImageDataUrl, onSave, onExit }: SketchCanvasProps <button onClick={onExit} title="Done sketching"- className="ml-auto flex h-8 items-center gap-1.5 rounded-[3px] bg-white px-3 text-xs font-medium text-black transition-colors hover:bg-white/85"+ className="ms-auto flex h-8 items-center gap-1.5 rounded-[3px] bg-white px-3 text-xs font-medium text-black transition-colors hover:bg-white/85" > <CheckIcon className="text-xs" /> Done@@ -481,7 +484,7 @@ const ThumbnailItem = ({ <ImagesIcon className="text-lg text-base-fg/20" /> </div> )}- <div className="absolute bottom-0 left-0 right-0 bg-black/60 px-1.5 py-0.5">+ <div className="absolute bottom-0 start-0 end-0 bg-black/60 px-1.5 py-0.5"> <span className="font-mono text-[10px] tabular-nums text-white/85">{board.shotNumber}</span> </div> </div>@@ -769,11 +772,16 @@ const Timeline = ({ <div className="flex min-w-0 flex-1 flex-col justify-center gap-0.5 px-1.5"> <span className="truncate text-[10px] font-semibold text-base-fg/80"> {board.shotNumber}- {board.title ? ` · ${board.title}` : ""}+ {board.title && (+ <>+ {" · "}+ <bdi>{board.title}</bdi>+ </>+ )} </span> {board.dialogue && ( <span className="truncate text-[9px] italic text-base-fg/40">- "{board.dialogue}"+ "<bdi>{board.dialogue}</bdi>" </span> )} <span className="font-mono text-[9px] tabular-nums text-base-fg/30">{board.duration.toFixed(1)}s</span>@@ -1203,7 +1211,7 @@ export const Storyboard = () => { onSketch={handleSketchOpen} /> ) : (- <div className="w-[280px] shrink-0 border-r border-ui-panel-border bg-ui-panel" />+ <div className="w-[280px] shrink-0 border-e border-ui-panel-border bg-ui-panel" /> )} {isSketchMode && selectedBoard ? ( <SketchCanvas@@ -500,7 +500,7 @@ export const VideoFrameExtractor = () => { setSavedFrames(new Set()); setConvertingFrames(new Set()); }}- className="absolute right-3 top-3 z-10 border border-red/50 px-3 py-1.5 text-sm hover:border-red/80 hover:bg-red/80"+ className="absolute end-3 top-3 z-10 border border-red/50 px-3 py-1.5 text-sm hover:border-red/80 hover:bg-red/80" > Switch Video </Button>@@ -513,7 +513,7 @@ export const VideoFrameExtractor = () => { playsInline crossOrigin="anonymous" />- <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent p-4">+ <div className="absolute bottom-0 start-0 end-0 bg-gradient-to-t from-black/90 to-transparent p-4"> <div ref={progressBarRef} className="group relative mb-3 h-3 cursor-pointer bg-white/15"@@ -241,7 +241,7 @@ export const VideoWatermarkRemover = () => { setVideoUrl(""); setCurrentTime(0); }}- className="absolute right-3 top-3 z-10 border border-red/50 px-3 py-1.5 text-sm hover:border-red/80 hover:bg-red/80"+ className="absolute end-3 top-3 z-10 border border-red/50 px-3 py-1.5 text-sm hover:border-red/80 hover:bg-red/80" > Switch Video </Button>@@ -253,7 +253,7 @@ export const VideoWatermarkRemover = () => { preload="metadata" playsInline />- <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent p-4">+ <div className="absolute bottom-0 start-0 end-0 bg-gradient-to-t from-black/90 to-transparent p-4"> <div ref={progressBarRef} className="group relative mb-3 h-3 cursor-pointer bg-white/15"@@ -463,6 +463,14 @@ img { ); } +/* The browser puts the resize corner on the side of the text direction, which+ follows the typed text (dir="auto"), not the document. */+.promptbox-resize-wrap:has(:is(textarea, [contenteditable]):dir(rtl))::after {+ right: auto;+ left: 2px;+ transform: scaleX(-1);+}+ .scrollbar-hidden { -ms-overflow-style: none; /* IE and Edge */new file mode 100644@@ -0,0 +1,9 @@+lockfileVersion: '9.0'++settings:+ autoInstallPeers: true+ excludeLinksFromLockfile: false++importers:++ .: {}new file mode 100644@@ -0,0 +1,47 @@+import { afterEach, describe, expect, it, vi } from "vitest";+import { directionOfLanguage, setDocumentLanguage } from "./document-language";++describe("directionOfLanguage", () => {+ it("finds right-to-left languages and scripts", () => {+ expect(directionOfLanguage("ar")).toBe("rtl");+ expect(directionOfLanguage("he-IL")).toBe("rtl");+ expect(directionOfLanguage("pa-Arab")).toBe("rtl");+ expect(directionOfLanguage("en")).toBe("ltr");+ expect(directionOfLanguage("az-Latn")).toBe("ltr");+ expect(directionOfLanguage("not a tag")).toBe("ltr");+ });++ describe("without Intl.Locale text info", () => {+ afterEach(() => vi.unstubAllGlobals());++ it("falls back to the language and script lists", () => {+ vi.stubGlobal("Intl", { ...Intl, Locale: class {} });+ expect(directionOfLanguage("ur-PK")).toBe("rtl");+ expect(directionOfLanguage("uz-Arab")).toBe("rtl");+ expect(directionOfLanguage("ks-Deva")).toBe("ltr");+ expect(directionOfLanguage("de")).toBe("ltr");+ });++ it("ignores extension and private-use subtags", () => {+ vi.stubGlobal("Intl", { ...Intl, Locale: class {} });+ expect(directionOfLanguage("fa-u-nu-latn")).toBe("rtl");+ expect(directionOfLanguage("ur-PK-u-nu-latn")).toBe("rtl");+ expect(directionOfLanguage("uz-Arab-u-nu-latn")).toBe("rtl");+ expect(directionOfLanguage("en-x-arab")).toBe("ltr");+ });+ });+});++describe("setDocumentLanguage", () => {+ it("defaults to English, left to right", () => {+ setDocumentLanguage();+ expect(document.documentElement.lang).toBe("en");+ expect(document.documentElement.dir).toBe("ltr");+ });++ it("sets the direction from the language", () => {+ setDocumentLanguage("fa");+ expect(document.documentElement.lang).toBe("fa");+ expect(document.documentElement.dir).toBe("rtl");+ });+});new file mode 100644@@ -0,0 +1,52 @@+export type TextDirection = "ltr" | "rtl";++export const DEFAULT_UI_LANGUAGE = "en";++// Fallback for webviews without Intl.Locale text info. Languages whose usual+// script is written right to left, and scripts that are.+const RTL_LANGUAGES = new Set([+ "ar", "arc", "ckb", "dv", "fa", "he", "iw", "ks", "lrc", "mzn", "nqo",+ "ps", "sd", "syr", "ug", "ur", "yi",+]);+const RTL_SCRIPTS = new Set([+ "adlm", "arab", "hebr", "mand", "nkoo", "rohg", "syrc", "thaa",+]);++/** Writing direction of a BCP 47 language tag such as `ar`, `he-IL` or `pa-Arab`. */+export function directionOfLanguage(tag: string): TextDirection {+ try {+ const locale = new Intl.Locale(tag) as Intl.Locale & {+ getTextInfo?: () => { direction?: string };+ textInfo?: { direction?: string };+ };+ const info = locale.getTextInfo?.() ?? locale.textInfo;+ if (info?.direction === "rtl" || info?.direction === "ltr") {+ return info.direction;+ }+ } catch {+ return "ltr";+ }+ const [language, ...rest] = tag.toLowerCase().split(/[-_]/);+ // Stop at the first singleton (`-u-` Unicode extensions, `-x-` private use):+ // their keys and values, such as `nu-latn`, are not scripts.+ const singleton = rest.findIndex((subtag) => subtag.length === 1);+ const subtags = singleton < 0 ? rest : rest.slice(0, singleton);+ const script = subtags.find((subtag) => /^[a-z]{4}$/.test(subtag));+ if (script) {+ return RTL_SCRIPTS.has(script) ? "rtl" : "ltr";+ }+ return RTL_LANGUAGES.has(language) ? "rtl" : "ltr";+}++/**+ * Sets `lang` and `dir` on the document root from the UI language. Tailwind's+ * `rtl:` variant and the logical (start/end) classes follow `dir`.+ */+export function setDocumentLanguage(+ tag: string = DEFAULT_UI_LANGUAGE,+ dir: TextDirection = directionOfLanguage(tag),+ root: HTMLElement = document.documentElement,+): void {+ root.lang = tag;+ root.dir = dir;+}@@ -1,3 +1,4 @@ export * from "./thumbnail-utils"; export * from "./media-format"; export * from "./referral";+export * from "./document-language";@@ -263,7 +263,7 @@ export function WaveformAudioPlayer({ > <DynamicIcon icon={isPlaying ? PauseIcon : PlayIcon}- className={`${compact ? "h-3 w-3" : "h-3.5 w-3.5"} ${isPlaying ? "" : "ml-0.5"}`}+ className={`${compact ? "h-3 w-3" : "h-3.5 w-3.5"} ${isPlaying ? "" : "ms-0.5"}`} /> </button> @@ -315,7 +315,7 @@ export function WaveformAudioPlayer({ /> </button> {showVolume && (- <div className="absolute bottom-full right-0 z-20 mb-2 w-28 rounded-lg border border-white/10 bg-neutral-900/95 px-3 py-2.5 shadow-xl backdrop-blur">+ <div className="absolute bottom-full end-0 z-20 mb-2 w-28 rounded-lg border border-white/10 bg-neutral-900/95 px-3 py-2.5 shadow-xl backdrop-blur"> <SliderV2 min={0} max={100}@@ -77,7 +77,7 @@ export const ButtonDropdown = ({ return ( <div className="relative">- <Menu as="div" className="inline-block text-left">+ <Menu as="div" className="inline-block text-start"> <Menu.Button as="div"> <Button className={className}@@ -102,7 +102,7 @@ export const ButtonDropdown = ({ static className={twMerge( "absolute z-20 mt-2 w-max min-w-48 overflow-hidden rounded-[3px] border border-ui-panel-border bg-ui-controls p-1.5 text-base-fg focus:outline-none",- align === "left" ? "left-0" : "right-0",+ align === "left" ? "start-0" : "end-0", )} > <div>@@ -130,11 +130,11 @@ export const ButtonDropdown = ({ {option.icon && ( <DynamicIcon icon={option.icon}- className="mr-2 h-4 w-4 shrink-0"+ className="me-2 h-4 w-4 shrink-0" /> )} <div className="grow text-start">{option.label}</div>- <div className="ml-6 font-mono text-[11px] font-normal text-white/45">+ <div className="ms-6 font-mono text-[11px] font-normal text-white/45"> {option.description && option.description} </div> {showSelected && (@@ -143,7 +143,7 @@ export const ButtonDropdown = ({ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"- className="ml-3 flex h-5"+ className="ms-3 flex h-5" > <path opacity="1"@@ -40,7 +40,7 @@ export function ButtonIconSelect({ }; return (- <div className="flex space-x-1">+ <div className="flex space-x-1 rtl:space-x-reverse"> {options.map(({ value, icon, text, tooltip }) => tooltip ? ( <Tooltip@@ -115,7 +115,7 @@ export const CameraSettingsModal = ({ showClose={false} > <div className="grid h-full grid-cols-12 gap-3">- <div className="relative col-span-4 p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-white/15">+ <div className="relative col-span-4 p-3 pt-2 after:absolute after:end-0 after:top-0 after:h-full after:w-px after:bg-white/15"> <div className="flex items-center justify-between gap-2.5 py-0.5"> <h2 className="hud-label text-base-fg/60">Camera</h2> <Tooltip content="Add camera" position="top" delay={200}>@@ -132,7 +132,7 @@ export const CameraSettingsModal = ({ {cameras.map((camera) => ( <button key={camera.id}- className={`h-9 w-full rounded-[3px] border-l-2 p-2 text-left transition-colors duration-100 ${+ className={`h-9 w-full rounded-[3px] border-s-2 p-2 text-start transition-colors duration-100 ${ camera.id === selectedCameraId ? "border-white bg-white/10" : "border-transparent hover:bg-white/5"@@ -40,12 +40,13 @@ export const DragAndDropZone = ({ <div className="flex cursor-pointer items-center justify-between gap-3.5 rounded-none border border-dashed border-white/20 bg-white/[0.02] p-3 transition-colors hover:border-white/40 hover:bg-white/[0.05]"> <DynamicIcon icon={fileIcon} className="text-4xl" /> <div className="flex grow flex-col gap-0">- <p className="font-medium">+ <p dir="auto" className="font-medium"> {file.name.slice(0, file.name.lastIndexOf("."))} </p> <p className="flex items-center gap-2 text-sm font-normal"> <span className="opacity-50">- {`${fileName} file size: ${fileSize} `}+ <bdi>{fileName}</bdi>+ {` file size: ${fileSize} `} </span> <u className="transition-all hover:text-white/80">Change File</u> </p>@@ -143,7 +143,7 @@ export function DragGhost({ /> {state.indicator && ( <div- className={`absolute left-1.5 top-1.5 flex h-5 w-5 items-center justify-center shadow-md ring-1 ring-black/30 ${+ className={`absolute start-1.5 top-1.5 flex h-5 w-5 items-center justify-center shadow-md ring-1 ring-black/30 ${ state.indicator === "ok" ? "bg-emerald-500" : "bg-red-500" }`} >@@ -271,7 +271,10 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({ <MusicIcon className="text-lg text-white/70" /> </div> </div>- <p className="line-clamp-2 shrink-0 px-2.5 pb-2.5 text-left text-xs leading-snug text-white/75">+ <p+ dir="auto"+ className="line-clamp-2 shrink-0 px-2.5 pb-2.5 text-start text-xs leading-snug text-white/75"+ > {audioPromptText || item.label} </p> </div>@@ -309,12 +312,12 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({ </> )} {selected && (- <div className="absolute left-2 top-2 flex h-6 w-6 items-center justify-center rounded-[3px] bg-primary">+ <div className="absolute start-2 top-2 flex h-6 w-6 items-center justify-center rounded-[3px] bg-primary"> <CheckIcon className="text-sm" /> </div> )} {disabled && (- <div className="absolute left-2 top-2 bg-black/70 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-white/80">+ <div className="absolute start-2 top-2 bg-black/70 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-white/80"> Added </div> )}@@ -339,7 +342,7 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({ // Tile chrome stays under sticky page headers (e.g. the library // filter bar at z-10): z-[2]/z-[1] keep the in-tile ordering // (menu/checkbox above badges) without escaping the page layers.- className="absolute right-2 top-2 z-[2] opacity-0 group-hover:opacity-100 [@media(pointer:coarse)]:opacity-100 transition-opacity duration-75"+ className="absolute end-2 top-2 z-[2] opacity-0 group-hover:opacity-100 [@media(pointer:coarse)]:opacity-100 transition-opacity duration-75" onPointerDown={(e) => e.stopPropagation()} onClick={(e) => { e.stopPropagation();@@ -409,7 +412,7 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({ {mode !== "select" && onBulkSelectToggle && ( <div className={twMerge(- "absolute left-2 top-2 z-[2] flex h-5 w-5 items-center justify-center rounded-[3px] border-2 cursor-pointer transition-all duration-100",+ "absolute start-2 top-2 z-[2] flex h-5 w-5 items-center justify-center rounded-[3px] border-2 cursor-pointer transition-all duration-100", bulkSelected ? "bg-primary border-primary" : "border-white/60 bg-black/40 hover:border-white",@@ -428,7 +431,7 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({ )} {/* Media class badge on hover — bottom-left */} {!disableTooltipAndBadge && item.mediaClass && (- <div className="pointer-events-none absolute left-2 bottom-2 z-[1] bg-black/50 backdrop-blur-lg px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-white opacity-0 group-hover:opacity-100 transition-opacity duration-150">+ <div className="pointer-events-none absolute start-2 bottom-2 z-[1] bg-black/50 backdrop-blur-lg px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-white opacity-0 group-hover:opacity-100 transition-opacity duration-150"> {item.mediaClass === "dimensional" || item.mediaClass === "mesh" || item.mediaClass === "splat"@@ -438,7 +441,7 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({ )} {/* Upload badge — bottom-right (always visible, even in select mode) */} {item.isUpload && (- <div className="pointer-events-none absolute right-2 bottom-2 z-[1] flex h-5 w-5 items-center justify-center bg-black/50 backdrop-blur-lg text-white">+ <div className="pointer-events-none absolute end-2 bottom-2 z-[1] flex h-5 w-5 items-center justify-center bg-black/50 backdrop-blur-lg text-white"> <UploadIcon className="text-[10px]" /> </div> )}@@ -117,7 +117,7 @@ export const GalleryFolderChip: React.FC<GalleryFolderChipProps> = ({ {hasStar && ( <StarIcon - className="absolute left-2 top-2 text-sm text-amber-400 drop-shadow-[0_1px_2px_rgba(0,0,0,0.6)]" />+ className="absolute start-2 top-2 text-sm text-amber-400 drop-shadow-[0_1px_2px_rgba(0,0,0,0.6)]" /> )} {/* Name + subfolder count */}@@ -135,8 +135,9 @@ export const GalleryFolderChip: React.FC<GalleryFolderChipProps> = ({ style={colorCode ? { color: colorCode } : undefined} /> )} <span+ dir="auto" className={twMerge(- "line-clamp-2 break-words text-left text-sm font-medium",+ "line-clamp-2 break-words text-start text-sm font-medium", hasArt ? "text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.7)]" : "text-base-fg/90",@@ -148,7 +149,7 @@ export const GalleryFolderChip: React.FC<GalleryFolderChipProps> = ({ {childCount > 0 && ( <span className={twMerge(- "text-left text-[10px]",+ "text-start text-[10px]", hasArt ? "text-white/70" : "text-base-fg/40", )} >@@ -168,7 +169,7 @@ export const GalleryFolderChip: React.FC<GalleryFolderChipProps> = ({ e.stopPropagation(); openMenuAt(e.currentTarget as HTMLElement); }}- className="absolute right-1.5 top-1.5 flex h-7 w-7 items-center justify-center rounded-[3px] bg-black/40 text-white opacity-0 transition-opacity hover:bg-black/60 group-hover/chip:opacity-100 [@media(pointer:coarse)]:opacity-100"+ className="absolute end-1.5 top-1.5 flex h-7 w-7 items-center justify-center rounded-[3px] bg-black/40 text-white opacity-0 transition-opacity hover:bg-black/60 group-hover/chip:opacity-100 [@media(pointer:coarse)]:opacity-100" > <EllipsisIcon className="text-sm" /> </span>@@ -146,7 +146,7 @@ export const GalleryItemMenuItems: React.FC<GalleryItemMenuItemsProps> = ({ /> </div> ) : (- <div className="absolute left-full top-0 -ml-1 pl-2 z-50">+ <div className="absolute start-full top-0 -ms-1 ps-2 z-50"> <div className="max-h-64 overflow-y-auto w-max min-w-36 rounded-[3px] border border-ui-panel-border bg-ui-panel p-1 shadow-xl"> <FolderList item={item}@@ -438,6 +438,7 @@ function TagCloudView({ <div className="flex max-w-3xl flex-wrap items-baseline justify-center gap-x-5 gap-y-3 select-none"> {words.map(({ tag, weight }) => ( <button+ dir="auto" key={tag.tag_token} type="button" onClick={() => onOpen(tag.tag_token)}@@ -2593,7 +2594,7 @@ export const GalleryModal = React.memo( > {mode === "view" && ( <Modal.DragHandle>- <div className="absolute left-0 top-0 z-[50] h-[60px] w-full cursor-move" />+ <div className="absolute start-0 top-0 z-[50] h-[60px] w-full cursor-move" /> </Modal.DragHandle> )} <div className="relative flex h-full flex-col">@@ -2643,7 +2644,7 @@ export const GalleryModal = React.memo( </button> <span className="text-base-fg/30">/</span> <TagIcon className="text-sm text-violet-400" />- <h2 className="text-lg font-semibold truncate max-w-[16rem]">+ <h2 dir="auto" className="text-lg font-semibold truncate max-w-[16rem]"> {activeTag.tag_value} </h2> <span className="text-base-fg/40 text-sm">@@ -2688,6 +2689,7 @@ export const GalleryModal = React.memo( <React.Fragment key={crumb.id}> <span className="text-base-fg/30">/</span> <button+ dir="auto" type="button" onClick={() => handleOpenFolder(crumb.id)} className="text-base-fg/50 hover:text-base-fg text-sm transition-colors truncate max-w-[10rem]"@@ -2714,6 +2716,7 @@ export const GalleryModal = React.memo( /> ) : ( <h2+ dir="auto" className="text-lg font-semibold cursor-pointer rounded-[3px] hover:bg-ui-controls/30 px-1.5 py-0 transition-colors truncate max-w-[20rem]" onClick={() => handleStartRename(activeFolderId!)} >@@ -2743,7 +2746,7 @@ export const GalleryModal = React.memo( onClick={() => setFolderMenuOpen(false)} /> <FolderContextMenuItems- className="absolute left-0 top-full mt-1"+ className="absolute start-0 top-full mt-1" folderId={activeFolderId!} hasStar={activeFolder?.hasStar} colorCode={activeFolder?.colorCode}@@ -2819,7 +2822,7 @@ export const GalleryModal = React.memo( </div> )} </div>- <div className="flex justify-end gap-2 items-center flex-wrap ml-auto">+ <div className="flex justify-end gap-2 items-center flex-wrap ms-auto"> {/* Refresh button */} <Tooltip position="top"@@ -2899,7 +2902,7 @@ export const GalleryModal = React.memo( {/* ── Filter sidebar ── shown on both tabs; clicking a filter jumps back to the library, clicking a folder into the browser */} {!hideFilter && (- <div className="w-52 min-w-[13rem] border-r border-ui-panel-border bg-ui-panel flex flex-col overflow-hidden">+ <div className="w-52 min-w-[13rem] border-e border-ui-panel-border bg-ui-panel flex flex-col overflow-hidden"> {/* Filter items — pinned; they never scroll out of view */} <div className="flex flex-col px-1.5 pt-2 pb-1 flex-shrink-0"> {SIDEBAR_FILTERS.map((f) => {@@ -3001,7 +3004,7 @@ export const GalleryModal = React.memo( /> <span className="truncate">{folder.name}</span> {folder.hasStar && (- <StarIcon className="ml-auto text-[10px] text-amber-400" />+ <StarIcon className="ms-auto text-[10px] text-amber-400" /> )} </button> ))@@ -3033,7 +3036,7 @@ export const GalleryModal = React.memo( > <TagIcon className="text-xs w-4 text-violet-400" /> <span className="truncate">{tag.tag_value}</span>- <span className="ml-auto text-[10px] text-base-fg/40">+ <span className="ms-auto text-[10px] text-base-fg/40"> {tag.use_count} </span> </button>@@ -3050,7 +3053,7 @@ export const GalleryModal = React.memo( > <EllipsisIcon className="text-xs w-4" /> <span>All tags</span>- <span className="ml-auto text-[10px] text-base-fg/40">+ <span className="ms-auto text-[10px] text-base-fg/40"> {sortedTags.length} </span> </button>@@ -3576,7 +3579,7 @@ export const GalleryModal = React.memo( className="fixed inset-0 z-[59]" onClick={() => setBulkFolderPopoverOpen(false)} />- <div className="absolute bottom-full mb-2 right-0 w-56 rounded-[3px] border border-ui-panel-border bg-ui-panel p-2 shadow-xl z-[60]">+ <div className="absolute bottom-full mb-2 end-0 w-56 rounded-[3px] border border-ui-panel-border bg-ui-panel p-2 shadow-xl z-[60]"> {/* Folders */} <div className="text-[11px] font-semibold uppercase tracking-wider text-base-fg/40 px-2 py-1"> Folders@@ -61,7 +61,9 @@ export const FailedRow = memo(function FailedRow({ {recreateSlot} </div> {prompt && (- <p className="mt-0.5 truncate text-xs text-white/45">{prompt}</p>+ <p dir="auto" className="mt-0.5 truncate text-xs text-white/45">+ {prompt}+ </p> )} <div className="mt-1 flex items-center gap-1.5 text-xs text-white/40"> {iconPath && (@@ -71,7 +73,9 @@ export const FailedRow = memo(function FailedRow({ className="h-3 w-3 shrink-0 icon-auto-contrast" /> )}- <span className="truncate">{modelLabel}</span>+ <span dir="auto" className="truncate">+ {modelLabel}+ </span> {failureMessage && failureMessage !== failureReason && ( <> <span className="text-white/20">·</span>@@ -189,7 +189,10 @@ export const GalleryCard = memo(function GalleryCard({ > {isAudio ? ( <div className="flex h-full flex-col bg-gradient-to-br from-white/[0.08] via-white/[0.04] to-white/[0.02] p-3">- <p className="line-clamp-2 shrink-0 text-sm leading-snug text-white/85">+ <p+ dir="auto"+ className="line-clamp-2 shrink-0 text-sm leading-snug text-white/85"+ > {title || item.label} </p> <div className="flex min-h-0 flex-1 items-center justify-center">@@ -221,7 +224,7 @@ export const GalleryCard = memo(function GalleryCard({ {/* Selection checkbox chip (select mode only) */} {selectMode && item.fullImage && ( <div- className={`pointer-events-none absolute left-2 top-2 z-10 flex h-5 w-5 items-center justify-center rounded-[3px] border transition-colors ${+ className={`pointer-events-none absolute start-2 top-2 z-10 flex h-5 w-5 items-center justify-center rounded-[3px] border transition-colors ${ selected ? "border-primary-400 bg-primary-400 text-white" : "border-white/60 bg-black/40 text-transparent"@@ -235,7 +238,7 @@ export const GalleryCard = memo(function GalleryCard({ in the lightbox). Top-right so it never collides with the selection checkbox chip at top-left. */} {lastViewed && (- <div className="pointer-events-none absolute right-2 top-2 z-10 flex items-center gap-1 rounded-[3px] bg-black/70 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-white/80">+ <div className="pointer-events-none absolute end-2 top-2 z-10 flex items-center gap-1 rounded-[3px] bg-black/70 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-white/80"> <EyeIcon /> Last viewed </div>@@ -133,14 +133,14 @@ export const GalleryRow = memo(function GalleryRow({ {isVideo && item.thumbnail && ( <div className="pointer-events-none absolute inset-0 flex items-center justify-center"> <span className="flex h-6 w-6 items-center justify-center rounded-full bg-black/55 backdrop-blur-sm">- <PlayIcon className="ml-0.5 text-[9px] text-white/90" />+ <PlayIcon className="ms-0.5 text-[9px] text-white/90" /> </span> </div> )} {/* Persistent "Last viewed" badge (stays until another item is opened in the lightbox). */} {lastViewed && (- <div className="pointer-events-none absolute left-1 top-1 z-10 flex items-center gap-1 rounded-full bg-black/70 px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-wide text-white/80">+ <div className="pointer-events-none absolute start-1 top-1 z-10 flex items-center gap-1 rounded-full bg-black/70 px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-wide text-white/80"> <EyeIcon /> Last viewed </div>@@ -155,7 +155,7 @@ export const GalleryRow = memo(function GalleryRow({ </div> ) : ( <div className="flex items-start gap-2">- <p className="line-clamp-3 min-w-0 flex-1 text-sm leading-snug text-white/90">+ <p dir="auto" className="line-clamp-3 min-w-0 flex-1 text-sm leading-snug text-white/90"> {title || item.label} </p> {title && (@@ -183,7 +183,7 @@ export const GalleryRow = memo(function GalleryRow({ className="h-3 w-3 shrink-0 icon-auto-contrast" /> )}- <span className="max-w-[40vw] truncate sm:max-w-none">+ <span dir="auto" className="max-w-[40vw] truncate sm:max-w-none"> {modelDisplayName ?? mediaLabel} </span> {modelDisplayName && (@@ -201,7 +201,7 @@ export const GalleryRow = memo(function GalleryRow({ {actionsSlot} </div> )}- <span className="ml-auto shrink-0 whitespace-nowrap text-white/40">+ <span className="ms-auto shrink-0 whitespace-nowrap text-white/40"> {timeAgo} </span> </div>@@ -23,9 +23,9 @@ const BREAKPOINT_COLS = { }; // 12px gap on both axes (≈ Tailwind gap-3).-// ml-[-12px] on container offsets the first column's pl-[8px].-const MASONRY_CLASS = "flex w-auto ml-[-12px]";-const COLUMN_CLASS = "pl-[8px]";+// ms-[-12px] on container offsets the first column's ps-[8px].+const MASONRY_CLASS = "flex w-auto ms-[-12px]";+const COLUMN_CLASS = "ps-[8px]"; // ── Component ────────────────────────────────────────────────────────────── @@ -39,7 +39,7 @@ export const PendingCard = memo(function PendingCard({ <div className="group relative aspect-square w-full overflow-hidden rounded-[3px] bg-white/[0.03]"> <div className="animate-shimmer h-full w-full" /> {batchCount != null && batchCount > 1 && (- <div className="absolute left-2 right-2 top-2 z-10 bg-black/60 px-2.5 py-1.5 text-center text-[10px] leading-snug text-white/70 backdrop-blur-sm">+ <div className="absolute start-2 end-2 top-2 z-10 bg-black/60 px-2.5 py-1.5 text-center text-[10px] leading-snug text-white/70 backdrop-blur-sm"> Generating {batchCount} {batchNoun(mediaClass)} · Results may appear one at a time </div>@@ -57,7 +57,10 @@ export const PendingCard = memo(function PendingCard({ </div> <div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent px-3 pb-2.5 pt-8"> <div className="flex items-start gap-2">- <p className="line-clamp-3 min-w-0 flex-1 text-xs leading-relaxed text-white/80">+ <p+ dir="auto"+ className="line-clamp-3 min-w-0 flex-1 text-xs leading-relaxed text-white/80"+ > {prompt} </p> {recreateSlot}@@ -53,7 +53,7 @@ export const PendingRow = memo(function PendingRow({ <div className="min-w-0 flex-1"> <div className="flex items-start gap-2"> {prompt.trim() ? (- <p className="line-clamp-3 min-w-0 flex-1 text-sm leading-snug text-white/90">+ <p dir="auto" className="line-clamp-3 min-w-0 flex-1 text-sm leading-snug text-white/90"> {prompt} </p> ) : (@@ -57,7 +57,7 @@ const NewsView = ({ onBack }: { onBack: () => void }) => { if (selectedPost) { return (- <div className="flex flex-col h-[500px] overflow-y-auto pr-2 custom-scrollbar">+ <div className="flex flex-col h-[500px] overflow-y-auto pe-2 custom-scrollbar"> <button onClick={() => setSelectedSlug(null)} className="self-start mb-4 text-sm flex items-center gap-2 text-white/60 hover:text-white transition-colors"@@ -106,7 +106,7 @@ const NewsView = ({ onBack }: { onBack: () => void }) => { } return (- <div className="flex flex-col gap-4 mt-2 h-[500px] overflow-y-auto pr-2 custom-scrollbar">+ <div className="flex flex-col gap-4 mt-2 h-[500px] overflow-y-auto pe-2 custom-scrollbar"> {items.map((item) => ( <div key={item.slug}@@ -122,7 +122,7 @@ const NewsView = ({ onBack }: { onBack: () => void }) => { className="opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all text-white" /> </h3> {item.date && (- <span className="font-mono text-[11px] uppercase tracking-[0.12em] text-white/40 whitespace-nowrap ml-2">+ <span className="font-mono text-[11px] uppercase tracking-[0.12em] text-white/40 whitespace-nowrap ms-2"> {item.date} </span> )}@@ -372,7 +372,7 @@ export function HelpMenuButton({ key={item.id} type="button" onClick={() => viewTutorial(item)}- className="group block overflow-hidden rounded-none border border-white/15 bg-white/5 transition-colors hover:border-white/25 hover:bg-white/[0.07] text-left"+ className="group block overflow-hidden rounded-none border border-white/15 bg-white/5 transition-colors hover:border-white/25 hover:bg-white/[0.07] text-start" > <div className="aspect-video w-full overflow-hidden"> <img@@ -38,7 +38,7 @@ export const Input = React.forwardRef( {icon && ( <DynamicIcon icon={icon}- className={twMerge("text-md absolute pl-3 pt-3", iconClassName)}+ className={twMerge("text-md absolute ps-3 pt-3", iconClassName)} /> )} <input@@ -49,7 +49,7 @@ export const Input = React.forwardRef( "bg-ui-panel text-base-fg placeholder-base-fg/40", "border border-ui-panel-border transition-colors duration-150 ease-in-out hover:border-white/40 focus:border-white focus:!outline-none", "disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:border-ui-panel-border",- icon && "pl-10",+ icon && "ps-10", isError && "border-red-500 focus:border-red-500", inputClassName, )}@@ -357,7 +357,7 @@ function PresetDropdown({ aria-haspopup="listbox" aria-expanded={open} className={twMerge(- "flex w-full items-center gap-3 rounded-none bg-white/[0.04] p-1.5 text-left ring-1 ring-white/15 transition-all hover:bg-white/[0.06] active:scale-[0.995]",+ "flex w-full items-center gap-3 rounded-none bg-white/[0.04] p-1.5 text-start ring-1 ring-white/15 transition-all hover:bg-white/[0.06] active:scale-[0.995]", EASE, open && "ring-primary/40", )}@@ -370,7 +370,7 @@ function PresetDropdown({ </div> <ChevronIcon className={twMerge(- "mr-3 h-4 w-4 shrink-0 text-base-fg/40 transition-transform duration-300",+ "me-3 h-4 w-4 shrink-0 text-base-fg/40 transition-transform duration-300", EASE, open ? "-rotate-90" : "rotate-90", )}@@ -381,7 +381,7 @@ function PresetDropdown({ role="listbox" aria-label="Preset" className={twMerge(- "absolute left-0 right-0 top-full z-20 mt-2 origin-top rounded-none bg-ui-modal shadow-[0_20px_50px_-16px_rgba(0,0,0,0.7)] ring-1 ring-white/10 transition-all duration-200 overflow-hidden",+ "absolute start-0 end-0 top-full z-20 mt-2 origin-top rounded-none bg-ui-modal shadow-[0_20px_50px_-16px_rgba(0,0,0,0.7)] ring-1 ring-white/10 transition-all duration-200 overflow-hidden", EASE, open ? "scale-100 opacity-100"@@ -402,7 +402,7 @@ function PresetDropdown({ setOpen(false); }} className={twMerge(- "flex w-full items-start gap-3 px-3 py-2.5 text-left transition-colors",+ "flex w-full items-start gap-3 px-3 py-2.5 text-start transition-colors", EASE, selected ? "bg-primary/[0.08]" : "hover:bg-white/[0.04]", )}@@ -494,7 +494,7 @@ function NameSearchField({ "focus-within:bg-ui-modal/80 focus-within:ring-primary/50", )} >- <SearchIcon className="ml-3 h-4 w-4 shrink-0 text-base-fg/35 transition-colors group-focus-within:text-primary" />+ <SearchIcon className="ms-3 h-4 w-4 shrink-0 text-base-fg/35 transition-colors group-focus-within:text-primary" /> <input type="search" value={value}@@ -508,7 +508,7 @@ function NameSearchField({ onClick={() => onChange("")} aria-label="Clear search" className={twMerge(- "mr-1.5 grid h-7 w-7 shrink-0 place-items-center rounded-[3px] text-base-fg/40 transition-all hover:bg-white/10 hover:text-base-fg active:scale-90",+ "me-1.5 grid h-7 w-7 shrink-0 place-items-center rounded-[3px] text-base-fg/40 transition-all hover:bg-white/10 hover:text-base-fg active:scale-90", EASE, )} >@@ -541,7 +541,7 @@ function KeySearchField({ <button type="button" onClick={onArm}- className="flex h-10 grow items-center gap-2 px-3 text-left"+ className="flex h-10 grow items-center gap-2 px-3 text-start" > <KeyIcon className={twMerge(@@ -570,7 +570,7 @@ function KeySearchField({ onClick={onClear} aria-label="Clear key filter" className={twMerge(- "mr-1.5 grid h-7 w-7 shrink-0 place-items-center rounded-[3px] text-base-fg/40 transition-all hover:bg-white/10 hover:text-base-fg active:scale-90",+ "me-1.5 grid h-7 w-7 shrink-0 place-items-center rounded-[3px] text-base-fg/40 transition-all hover:bg-white/10 hover:text-base-fg active:scale-90", EASE, )} >@@ -610,14 +610,14 @@ function CollapsibleSurface({ {/* The toggle and the (destructive) section reset are sibling buttons — a nested button is invalid HTML and a mis-click on "reset" must never also collapse the section. */}- <div className="flex w-full items-center gap-3 pr-4">+ <div className="flex w-full items-center gap-3 pe-4"> <button type="button" onClick={locked ? undefined : onToggle} aria-expanded={open} disabled={locked} className={twMerge(- "flex min-w-0 grow items-center gap-3 py-3 pl-4 text-left transition-colors",+ "flex min-w-0 grow items-center gap-3 py-3 ps-4 text-start transition-colors", EASE, locked ? "cursor-default" : "hover:bg-white/[0.03]", )}@@ -796,7 +796,7 @@ function TimedFill({ <span aria-hidden="true" className={twMerge(- "pointer-events-none absolute inset-0 origin-left",+ "pointer-events-none absolute inset-0 origin-left rtl:origin-right", className, )} style={{@@ -21,7 +21,7 @@ export const Label = ({ {...rest} > {children}- {required && <span className="ml-0.5 text-red-400">*</span>}+ {required && <span className="ms-0.5 text-red-400">*</span>} </label> ); @@ -555,7 +555,7 @@ export function LightboxModal({ > {/* Invisible drag handle strip at the very top for moving */} <Modal.DragHandle>- <div className="absolute left-0 top-0 z-20 h-12 w-full cursor-move" />+ <div className="absolute start-0 top-0 z-20 h-12 w-full cursor-move" /> </Modal.DragHandle> {/* content grid */}@@ -737,7 +737,7 @@ export function LightboxModal({ <div className="flex h-full w-[280px] shrink-0 flex-col"> <div className="flex-1 overflow-y-auto space-y-5 text-base-fg min-h-0 pb-2"> {creator ? (- <div className="sticky top-0 z-10 flex items-center gap-3 bg-ui-modal pb-3 pr-20 border-b border-white/10">+ <div className="sticky top-0 z-10 flex items-center gap-3 bg-ui-modal pb-3 pe-20 border-b border-white/10"> {creator.core_info ? ( <Gravatar size={36}@@ -755,7 +755,7 @@ export function LightboxModal({ </div> )} <div className="flex flex-col gap-1 min-w-0">- <span className="text-base-fg text-sm font-semibold leading-none truncate">+ <span dir="auto" className="text-base-fg text-sm font-semibold leading-none truncate"> {creator.display_name} </span> <span className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">@@ -764,7 +764,7 @@ export function LightboxModal({ </div> </div> ) : promptLoading ? (- <div className="sticky top-0 z-10 flex items-center gap-3 bg-ui-modal pb-3 pr-20 border-b border-white/10 animate-pulse">+ <div className="sticky top-0 z-10 flex items-center gap-3 bg-ui-modal pb-3 pe-20 border-b border-white/10 animate-pulse"> <div className="h-9 w-9 shrink-0 bg-white/10" /> <div className="flex flex-col gap-1.5 min-w-0 flex-1"> <div className="h-3.5 w-24 bg-white/10" />@@ -826,6 +826,7 @@ export function LightboxModal({ ) : ( <div ref={promptRef}+ dir="auto" className={twMerge( !isPromptExpanded && "line-clamp-4", )}@@ -930,7 +931,7 @@ export function LightboxModal({ <div className="flex flex-col gap-1.5 min-w-[100px]"> {!isAudio && !isVideoRef && ( <button- className="text-xs text-left text-base-fg/80 hover:text-base-fg transition-colors py-1 px-1 rounded-[3px] hover:bg-white/5"+ className="text-xs text-start text-base-fg/80 hover:text-base-fg transition-colors py-1 px-1 rounded-[3px] hover:bg-white/5" onClick={(e) => { e.stopPropagation(); setRefPreviewUrl(fullSize);@@ -938,13 +939,13 @@ export function LightboxModal({ > <SearchIcon - className="mr-1.5" />+ className="me-1.5" /> Preview image </button> )} {onNavigateToMedia && ( <button- className="text-xs text-left text-base-fg/80 hover:text-base-fg transition-colors py-1 px-1 rounded-[3px] hover:bg-white/5"+ className="text-xs text-start text-base-fg/80 hover:text-base-fg transition-colors py-1 px-1 rounded-[3px] hover:bg-white/5" onClick={(e) => { e.stopPropagation(); onNavigateToMedia(@@ -954,7 +955,7 @@ export function LightboxModal({ > <LogOutIcon - className="mr-1.5" />+ className="me-1.5" /> View as media </button> )}@@ -1266,7 +1267,7 @@ export function LightboxModal({ expandable={false} > <Modal.DragHandle>- <div className="absolute left-0 top-0 z-20 h-12 w-full cursor-move" />+ <div className="absolute start-0 top-0 z-20 h-12 w-full cursor-move" /> </Modal.DragHandle> <div className="relative flex items-center justify-center overflow-hidden bg-black"> <img@@ -1333,7 +1334,7 @@ function InfoSection({ alt="Model creator logo" className="h-4 w-4 invert" />- <span>{getModelDisplayName(modelType)}</span>+ <span dir="auto">{getModelDisplayName(modelType)}</span> </> } />@@ -1384,7 +1385,7 @@ function InfoRow({ label, value }: { label: string; value: ReactNode }) { <span className="shrink-0 pt-0.5 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60"> {label} </span>- <span className="min-w-0 text-right text-sm text-base-fg font-medium flex items-center justify-end gap-2">+ <span className="min-w-0 text-end text-sm text-base-fg font-medium flex items-center justify-end gap-2"> {value} </span> </div>@@ -134,7 +134,7 @@ export function TagChipInput({ }; const chipClass = (value: string) =>- `flex items-center gap-1 break-all text-left rounded-[3px] border border-white/15 bg-white/5 px-2.5 py-1 text-[11px] font-medium text-base-fg/80 transition-all ${+ `flex items-center gap-1 break-all text-start rounded-[3px] border border-white/15 bg-white/5 px-2.5 py-1 text-[11px] font-medium text-base-fg/80 transition-all ${ flashValue === value ? "ring-2 ring-primary" : "" }`; @@ -150,7 +150,7 @@ export function TagChipInput({ disabled ? ( <span key={value.toLowerCase()} className={chipClass(value)}> <TagIcon className="h-2.5 w-2.5 text-base-fg/40" />- {value}+ <bdi>{value}</bdi> </span> ) : ( <button@@ -163,7 +163,7 @@ export function TagChipInput({ title={`Remove "${value}"`} className={`group/tag ${chipClass(value)} hover:bg-red/15 hover:text-red focus:outline-none focus-visible:ring-2 focus-visible:ring-red`} >- {value}+ <bdi>{value}</bdi> <XIcon className="h-2.5 w-2.5" /> </button> ),@@ -171,6 +171,7 @@ export function TagChipInput({ {!disabled && ( <input ref={inputRef}+ dir="auto" value={draft} onChange={(e) => { setDraft(e.target.value);@@ -192,7 +193,7 @@ export function TagChipInput({ {dropdownOpen && ( <div- className={`absolute left-0 right-0 z-30 max-h-48 overflow-y-auto rounded-none border border-ui-panel-border bg-ui-panel p-1 shadow-xl ${+ className={`absolute start-0 end-0 z-30 max-h-48 overflow-y-auto rounded-none border border-ui-panel-border bg-ui-panel p-1 shadow-xl ${ dropUp ? "bottom-full mb-1" : "top-full mt-1" }`} >@@ -209,8 +210,8 @@ export function TagChipInput({ }`} > <TagIcon className="text-[10px] text-base-fg/40" />- <span className="truncate">{suggestion.value}</span>- <span className="ml-auto text-[11px] text-base-fg/40">+ <span dir="auto" className="truncate">{suggestion.value}</span>+ <span className="ms-auto text-[11px] text-base-fg/40"> {suggestion.useCount} </span> </button>@@ -16,9 +16,9 @@ export const ListDropdown = ({ list, onSelect }: ListDropdownProps) => { return ( <Listbox value={selected} onChange={setSelected}> <div className="relative mt-1">- <Listbox.Button className="relative h-10 w-full cursor-pointer rounded-[3px] border border-ui-controls-border bg-ui-controls py-2 pl-3 pr-10 text-left text-base-fg outline-none outline-offset-0 transition-colors duration-150 ease-in-out hover:border-white/40 focus:!outline-none sm:text-sm">+ <Listbox.Button className="relative h-10 w-full cursor-pointer rounded-[3px] border border-ui-controls-border bg-ui-controls py-2 ps-3 pe-10 text-start text-base-fg outline-none outline-offset-0 transition-colors duration-150 ease-in-out hover:border-white/40 focus:!outline-none sm:text-sm"> <span className="block truncate">{Object.keys(selected)[0]}</span>- <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">+ <span className="pointer-events-none absolute inset-y-0 end-0 flex items-center pe-2"> <ChevronDownIcon aria-hidden="true" /> </span> </Listbox.Button>@@ -33,7 +33,7 @@ export const ListDropdown = ({ list, onSelect }: ListDropdownProps) => { <Listbox.Option key={itemIdx} className={({ active, selected }) =>- `relative cursor-pointer select-none py-2 pl-10 pr-4 transition-colors duration-150 ${+ `relative cursor-pointer select-none py-2 ps-10 pe-4 transition-colors duration-150 ${ active ? "bg-white text-black" : selected@@ -52,7 +52,7 @@ export const ListDropdown = ({ list, onSelect }: ListDropdownProps) => { {Object.values(item)[0]} </span> {selected ? (- <span className="absolute inset-y-0 left-0 flex items-center pl-3">+ <span className="absolute inset-y-0 start-0 flex items-center ps-3"> <CheckIcon aria-hidden="true" /> </span> ) : null}@@ -113,7 +113,7 @@ export function DesktopLoginBridge({ onSuccess, onStart, onActiveChange }: { onS const remaining = challenge ? Math.max(0, Math.ceil((Date.parse(challenge.expires_at) - now) / 1000)) : 0; return <section aria-label="Website login" className="mb-6 border border-white/15 bg-white/[0.03] p-4 text-center">- {isActive && <div className="mb-4 text-left">+ {isActive && <div className="mb-4 text-start"> <button type="button" className="inline-flex items-center gap-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 transition-colors hover:text-white" onClick={back}> <ArrowLeftIcon size={16} aria-hidden="true" /> Back </button>@@ -132,12 +132,12 @@ export const ArtCraftSignUp = ({ placeholder="Min. 8 characters" required autoComplete="off"- inputClassName={`${FIELD_INPUT} pr-12`}+ inputClassName={`${FIELD_INPUT} pe-12`} /> <button type="button" onClick={() => setShowPassword((v) => !v)}- className="absolute right-4 top-1/2 -translate-y-1/2 text-white/30 transition-colors hover:text-white/60"+ className="absolute end-4 top-1/2 -translate-y-1/2 text-white/30 transition-colors hover:text-white/60" tabIndex={-1} > <DynamicIcon icon={showPassword ? EyeOffIcon : EyeIcon} />@@ -155,12 +155,12 @@ export const ArtCraftSignUp = ({ placeholder="Re-enter password" required autoComplete="off"- inputClassName={`${FIELD_INPUT} pr-12`}+ inputClassName={`${FIELD_INPUT} pe-12`} /> <button type="button" onClick={() => setShowConfirm((v) => !v)}- className="absolute right-4 top-1/2 -translate-y-1/2 text-white/30 transition-colors hover:text-white/60"+ className="absolute end-4 top-1/2 -translate-y-1/2 text-white/30 transition-colors hover:text-white/60" tabIndex={-1} > <DynamicIcon icon={showConfirm ? EyeOffIcon : EyeIcon} />@@ -15,10 +15,10 @@ const DEFAULT_SHOWCASE_VIDEO = "https://frontend-cdn.fakeyou.com/videos/knight-video.mp4"; const LOGIN_SUCCESS_DURATION_MS = 3000; const FRAME_CORNERS = [- "top-0 left-0",- "top-0 right-0",- "bottom-0 left-0",- "bottom-0 right-0",+ "top-0 start-0",+ "top-0 end-0",+ "bottom-0 start-0",+ "bottom-0 end-0", ]; interface LoginModalProps {@@ -245,7 +245,7 @@ export function LoginModal({ <div className="relative flex w-full flex-col lg:w-1/2"> <div className="flex flex-1 flex-col justify-center px-6 py-10 sm:px-10 sm:py-12"> <div className="w-full">- <div className="mb-8 text-left">+ <div className="mb-8 text-start"> <img src="/resources/logo/artcraft-icon.png" alt="ArtCraft"@@ -294,7 +294,7 @@ export function LoginModal({ </div> {/* ── Showcase pane (desktop only) ── */}- <div className="relative hidden border-l border-white/15 lg:block lg:w-1/2">+ <div className="relative hidden border-s border-white/15 lg:block lg:w-1/2"> <LoginShowcase videoUrl={videoUrl} /> </div> </>@@ -1076,7 +1076,7 @@ export const Modal = ({ > <animated.div className={twMerge(- "w-full max-w-lg rounded-none relative border border-ui-panel-border bg-ui-modal text-left align-middle z-[70]",+ "w-full max-w-lg rounded-none relative border border-ui-panel-border bg-ui-modal text-start align-middle z-[70]", childPadding && !expanded ? "p-4" : "", className, "!transition-none", // Always disable CSS transitions for spring animations@@ -1170,7 +1170,7 @@ export const Modal = ({ {renderResizeHandles()} </div> {(showClose || expandable) && (- <div className="absolute top-0 right-0 m-2.5 z-[80] flex items-center gap-2">+ <div className="absolute top-0 end-0 m-2.5 z-[80] flex items-center gap-2"> {expandable && <Modal.ExpandButton />} {showClose && <CloseButton onClick={onClose} />} </div>@@ -109,7 +109,7 @@ function ProviderTooltipContent({ type="button" className={`group flex cursor-pointer items-center justify-between rounded-[3px] px-2 py-2 transition-all ${ selectedProvider === p- ? "bg-ui-controls/70 border-l-4 border-primary"+ ? "bg-ui-controls/70 border-s-4 border-primary" : "hover:bg-ui-controls/50" }`} >@@ -118,7 +118,7 @@ function ProviderTooltipContent({ {getProviderDisplayName(p)} </span> {selectedProvider === p && (- <span className="ml-2 flex h-5 w-5 shrink-0 items-center justify-center text-white">+ <span className="ms-2 flex h-5 w-5 shrink-0 items-center justify-center text-white"> <CheckIcon aria-hidden="true" className="h-4 w-4" strokeWidth={2.5} /> </span> )}@@ -229,7 +229,7 @@ export function ClassyModelSelector({ : undefined; const iconProvider = prov ?? allowedProviders[0]; return iconProvider ? (- <div className="mr-1 p-1.5 bg-ui-controls/60 group-hover:bg-ui-controls/80 transition-colors">+ <div className="me-1 p-1.5 bg-ui-controls/60 group-hover:bg-ui-controls/80 transition-colors"> <span className="text-base-fg/70 group-hover:text-base-fg/90 text-lg"> {getProviderIcon(iconProvider)} </span>@@ -241,7 +241,7 @@ export function ClassyModelSelector({ isSameModel(item.model as Model | undefined, selectedModel) && selectedProvider && hasMultipleProviders ? (- <div className="mr-1 p-1.5 bg-primary/60 group-hover:bg-primary/80 transition-colors">+ <div className="me-1 p-1.5 bg-primary/60 group-hover:bg-primary/80 transition-colors"> <span className="text-base-fg/70 group-hover:text-base-fg/90 text-lg"> {getProviderIcon(selectedProvider)} </span>@@ -313,7 +313,7 @@ export function ClassyModelSelector({ mode="hoverSelect" maxListHeight={maxListHeight} {...popoverProps}- buttonClassName="rounded-[3px] bg-ui-controls text-left shadow-sm px-3 py-1 gap-3 border border-ui-controls-border"+ buttonClassName="rounded-[3px] bg-ui-controls text-start shadow-sm px-3 py-1 gap-3 border border-ui-controls-border" renderTrigger={(selectedItem) => { const modelTitle = selectedItem?.label ?? selectedModel?.selectorName ?? "";@@ -79,7 +79,7 @@ export const BoardPicker = () => { </button> {open && (- <div className="glass absolute left-0 top-full z-50 mt-1.5 w-64 rounded-2xl border border-ui-divider p-1.5 shadow-[0_12px_32px_-12px_rgba(0,0,0,0.6)]">+ <div className="glass absolute start-0 top-full z-50 mt-1.5 w-64 rounded-2xl border border-ui-divider p-1.5 shadow-[0_12px_32px_-12px_rgba(0,0,0,0.6)]"> <div className="max-h-72 overflow-y-auto"> {orderedBoards.map((board) => ( <div@@ -92,9 +92,10 @@ export const BoardPicker = () => { ].join(" ")} > <button+ dir="auto" type="button" onClick={() => handleSwitch(board.id)}- className="min-w-0 flex-1 truncate px-3 py-2 text-left text-sm text-base-fg focus:outline-none"+ className="min-w-0 flex-1 truncate px-3 py-2 text-start text-sm text-base-fg focus:outline-none" > {board.name} </button>@@ -103,7 +104,7 @@ export const BoardPicker = () => { title={`Delete ${board.name}`} aria-label={`Delete ${board.name}`} onClick={() => void handleDelete(board.id)}- className="mr-1 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-base-fg/40 opacity-0 transition-opacity hover:bg-red-500/20 hover:text-red-400 focus:opacity-100 group-hover:opacity-100"+ className="me-1 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-base-fg/40 opacity-0 transition-opacity hover:bg-red-500/20 hover:text-red-400 focus:opacity-100 group-hover:opacity-100" > <Trash2Icon className="h-3 w-3" /> </button>@@ -96,7 +96,7 @@ const TopLeftCluster = ({ mode: ViewMode; onChange: (m: ViewMode) => void; }) => (- <div className="glass absolute left-3 top-3 z-40 flex items-center gap-0.5 rounded-2xl border border-ui-divider p-1 shadow-[0_8px_24px_-12px_rgba(0,0,0,0.45)]">+ <div className="glass absolute start-3 top-3 z-40 flex items-center gap-0.5 rounded-2xl border border-ui-divider p-1 shadow-[0_8px_24px_-12px_rgba(0,0,0,0.45)]"> <BoardPicker /> <div className="mx-0.5 h-5 w-px bg-ui-divider" /> {OPTIONS.map((opt) => {@@ -190,7 +190,7 @@ const TopRightCluster = ({ onSave?: () => void; endSlot?: ReactNode; }) => (- <div className="pointer-events-none absolute right-3 top-3 z-40 flex items-center gap-2">+ <div className="pointer-events-none absolute end-3 top-3 z-40 flex items-center gap-2"> <div className="glass pointer-events-auto flex items-center rounded-xl border border-ui-divider p-0.5 shadow-[0_8px_24px_-12px_rgba(0,0,0,0.45)]"> {saveStatus !== null && onSave && ( <SaveButton status={saveStatus} onSave={onSave} />@@ -44,7 +44,7 @@ export const Moodboard = ({ adapter }: Props) => { onGalleryClick={triggerGallery} /> )} */}- <div className="absolute left-0 right-0 top-0 z-10">+ <div className="absolute start-0 end-0 top-0 z-10"> <MoodboardToolbar onUploadClick={triggerUpload} onGalleryClick={triggerGallery}@@ -22,7 +22,7 @@ export const RecenterIndicator = () => { type="button" onClick={() => fitToContent()} title="Zoom to fit (Shift+1)"- className="pointer-events-auto absolute bottom-4 right-4 z-20 flex items-center gap-2 rounded-full border border-white/20 bg-black/70 px-3 py-2 text-sm text-white/90 shadow-lg backdrop-blur-sm transition-colors hover:bg-black/80"+ className="pointer-events-auto absolute bottom-4 end-4 z-20 flex items-center gap-2 rounded-full border border-white/20 bg-black/70 px-3 py-2 text-sm text-white/90 shadow-lg backdrop-blur-sm transition-colors hover:bg-black/80" > <MinimizeIcon /> <span>Content off-screen — recenter</span>@@ -147,7 +147,7 @@ const BoardCardInner = ({ container, where blur filters trigger GPU repaints on every scroll frame. A solid scrim keeps the icons legible instead. */} {item.rating > 0 && (- <div className="absolute bottom-2 left-2 flex items-center gap-0.5 rounded-full bg-black/65 px-2 py-1">+ <div className="absolute bottom-2 start-2 flex items-center gap-0.5 rounded-full bg-black/65 px-2 py-1"> {Array.from({ length: item.rating }).map((_, i) => ( <StarIcon key={i}@@ -160,7 +160,7 @@ const BoardCardInner = ({ {/* Selection check — visible when selected or on hover. */} <div className={[- "absolute left-2 top-2 flex h-6 w-6 items-center justify-center rounded-full",+ "absolute start-2 top-2 flex h-6 w-6 items-center justify-center rounded-full", "border transition-all duration-150", selected ? "border-primary bg-primary text-white opacity-100"@@ -172,7 +172,7 @@ const BoardCardInner = ({ {/* Action rail — solid scrim pill (no blur in the scroll container), fades up on hover. */}- <div className="absolute right-2 top-2 flex translate-y-1 items-center gap-1 rounded-full border border-white/15 bg-black/65 p-0.5 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100">+ <div className="absolute end-2 top-2 flex translate-y-1 items-center gap-1 rounded-full border border-white/15 bg-black/65 p-0.5 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"> <RailButton label="Open" icon={Maximize2Icon}@@ -255,7 +255,7 @@ const CardContent = ({ case "color": return ( <div className="h-full w-full" style={{ background: item.color }}>- <span className="absolute bottom-2 left-2 rounded-full bg-black/40 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider text-white backdrop-blur">+ <span className="absolute bottom-2 start-2 rounded-full bg-black/40 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider text-white backdrop-blur"> {item.color} </span> </div>@@ -303,7 +303,7 @@ const CardContent = ({ <Favicon url={item.url} className="h-4 w-4" /> </span> <span className="flex min-w-0 flex-col">- <span className="truncate text-xs font-medium text-base-fg/90">+ <span dir="auto" className="truncate text-xs font-medium text-base-fg/90"> {item.title || hostnameOf(item.url)} </span> <span className="truncate text-[10px] text-base-fg/45">@@ -346,6 +346,7 @@ const NoteEditor = ({ onDoubleClick={(e) => e.stopPropagation()} > <textarea+ dir="auto" ref={ref} value={draft} onChange={(e) => setDraft(e.target.value)}@@ -66,9 +66,9 @@ export const BoardGridToolbar = ({ return ( <div className="pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center px-3 pt-3"> <ToolbarShell className="max-w-full">- <div className="flex min-w-0 flex-col pl-1 pr-2">+ <div className="flex min-w-0 flex-col ps-1 pe-2"> <BoardTitle name={boardName} onRename={onRenameBoard} />- <span className="pl-1.5 text-[11px] leading-tight text-base-fg/45">+ <span className="ps-1.5 text-[11px] leading-tight text-base-fg/45"> {itemCount} {itemCount === 1 ? "item" : "items"} </span> </div>@@ -216,13 +216,14 @@ const BoardTitle = ({ return ( <button+ dir="auto" type="button" title="Rename board" onClick={() => { setDraft(name); setEditing(true); }}- className="truncate rounded-md px-1.5 py-0.5 text-left text-sm font-semibold leading-tight text-base-fg transition-colors hover:bg-base-fg/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"+ className="truncate rounded-md px-1.5 py-0.5 text-start text-sm font-semibold leading-tight text-base-fg transition-colors hover:bg-base-fg/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary" > {name} </button>@@ -109,12 +109,12 @@ export const ItemInspector = ({ > {/* Invisible drag strip along the top, matching the shared lightbox. */} <Modal.DragHandle>- <div className="absolute left-0 top-0 z-20 h-12 w-full cursor-move rounded-t-xl" />+ <div className="absolute start-0 top-0 z-20 h-12 w-full cursor-move rounded-t-xl" /> </Modal.DragHandle> <div className="flex h-full gap-4"> {/* Media panel — flexible width, dark plate, contained media. */}- <div className="group/nav relative flex h-full flex-1 items-center justify-center overflow-hidden rounded-l-xl bg-black/30">+ <div className="group/nav relative flex h-full flex-1 items-center justify-center overflow-hidden rounded-s-xl bg-black/30"> <Media item={item} /> {hasPrev && <NavArrow side="left" onClick={onPrev} />}@@ -124,7 +124,7 @@ export const ItemInspector = ({ {/* Info + actions — fixed width, scrollable body, buttons pinned bottom. */} <div className="flex h-full w-[280px] shrink-0 flex-col"> <div className="min-h-0 flex-1 space-y-5 overflow-y-auto pb-2 text-base-fg">- <div className="flex items-center gap-2.5 border-b border-white/5 pb-3 pr-10">+ <div className="flex items-center gap-2.5 border-b border-white/5 pb-3 pe-10"> <span className="rounded-full border border-ui-panel-border px-2.5 py-1 text-[10px] font-medium uppercase tracking-[0.18em] text-base-fg/55"> {KIND_LABEL[item.kind]} </span>@@ -161,7 +161,7 @@ export const ItemInspector = ({ onClick={() => onRemoveTag(tag)} className="group/tag flex items-center gap-1 rounded-full bg-base-fg/10 px-2.5 py-1 text-[11px] font-medium text-base-fg/80 transition-colors hover:bg-red/15 hover:text-red focus:outline-none focus-visible:ring-2 focus-visible:ring-red" >- {tag}+ <bdi>{tag}</bdi> <XIcon className="h-2.5 w-2.5" /> </button> ))}@@ -304,7 +304,7 @@ const Media = ({ item }: { item: BoardItem }) => { <Favicon url={item.url} className="h-5 w-5" /> </span> <div className="min-w-0">- <p className="text-base font-medium text-base-fg">+ <p dir="auto" className="text-base font-medium text-base-fg"> {item.title || hostnameOf(item.url)} </p> <p className="mt-1 truncate text-sm text-base-fg/55">{item.url}</p>@@ -40,7 +40,7 @@ export const PresentationView = ({ items, onClose }: Props) => { type="button" aria-label="Exit presentation" onClick={onClose}- className="absolute right-5 top-5 z-20 flex h-10 w-10 items-center justify-center rounded-full border border-white/15 bg-white/5 text-white/80 backdrop-blur-md transition-colors hover:bg-white/15 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white/70"+ className="absolute end-5 top-5 z-20 flex h-10 w-10 items-center justify-center rounded-full border border-white/15 bg-white/5 text-white/80 backdrop-blur-md transition-colors hover:bg-white/15 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white/70" > <XIcon className="h-4 w-4" /> </button>@@ -130,7 +130,7 @@ const Stage = ({ item }: { item: BoardItem }) => { className="max-h-[60vh] rounded-2xl object-contain" /> )}- <span className="text-xl font-medium text-white">+ <span dir="auto" className="text-xl font-medium text-white"> {item.title || item.url} </span> </a>@@ -81,6 +81,7 @@ export const SectionHeader = ({ /> ) : ( <button+ dir="auto" type="button" onClick={startEdit} disabled={!editable}@@ -175,7 +175,7 @@ const MenuItem = ({ role="menuitem" onClick={onClick} className={[- "flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left text-sm transition-colors",+ "flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-start text-sm transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", accent ? "text-primary hover:bg-ui-controls/50"@@ -47,6 +47,7 @@ export const SmartSearchBar = ({ const active = activeTags.includes(tag); return ( <button+ dir="auto" key={tag} type="button" onClick={() => onToggleTag(tag)}@@ -308,7 +308,7 @@ export const HistoryStack = ({ /> )} <div- className="absolute -right-0 -top-0 flex h-5 w-5 items-center justify-center rounded-bl-lg bg-red/50 opacity-0 transition-opacity hover:bg-red/80 group-hover:opacity-100"+ className="absolute -end-0 -top-0 flex h-5 w-5 items-center justify-center rounded-es-lg bg-red/50 opacity-0 transition-opacity hover:bg-red/80 group-hover:opacity-100" onClick={(e) => { e.stopPropagation(); showActionReminder({@@ -1229,7 +1229,7 @@ const PageDraw = ({ <> <div className={`fixed inset-0 -z-10 ${backgroundClassName}`} /> <div- className={`preserve-aspect-ratio fixed right-4 top-[calc(50%_-_var(--pd-bottom-reserve,0px))] z-10 -translate-y-1/2 transform ${+ className={`preserve-aspect-ratio fixed end-4 top-[calc(50%_-_var(--pd-bottom-reserve,0px))] z-10 -translate-y-1/2 transform ${ isSelecting ? "pointer-events-none" : "pointer-events-auto" }`} >@@ -1292,7 +1292,7 @@ const PageDraw = ({ /> </div> <SideToolbar- className="fixed left-0 top-[calc(50%_-_var(--pd-bottom-reserve,0px))] z-10 -translate-y-1/2 transform"+ className="fixed start-0 top-[calc(50%_-_var(--pd-bottom-reserve,0px))] z-10 -translate-y-1/2 transform" onSelect={handleSelectTool} onActivateShapeTool={handleActivateShapeTool} onPaintBrush={handlePaintBrush}@@ -1351,7 +1351,7 @@ const PageDraw = ({ </ContextMenuContainer> </div> {showBottomRightControls && (- <div className="absolute bottom-4 right-4 z-20 flex items-center gap-2">+ <div className="absolute bottom-4 end-4 z-20 flex items-center gap-2"> <CostCalculatorButton modelPage={PAGE_ID} /> <HelpMenuButton /> </div>@@ -55,7 +55,7 @@ const ImagePreview: React.FC<ImagePreviewProps> = ({ image, onUpdate, onRemove } {showControls && ( <> <button - className="absolute top-1 right-1 bg-black bg-opacity-50 rounded-full p-0.5 text-white hover:bg-opacity-70 transition-colors"+ className="absolute top-1 end-1 bg-black bg-opacity-50 rounded-full p-0.5 text-white hover:bg-opacity-70 transition-colors" onClick={() => onRemove(image.id)} > <X size={16} />@@ -67,10 +67,10 @@ const ImagePreview: React.FC<ImagePreviewProps> = ({ image, onUpdate, onRemove } onMouseDown={handleMouseDown} > <div - className="absolute left-0 right-0 h-0.5 bg-white" + className="absolute start-0 end-0 h-0.5 bg-white" style={{ bottom: `${image.weight * 100}%` }} />- <div className="absolute bottom-1 right-1 text-xs font-medium text-white">+ <div className="absolute bottom-1 end-1 text-xs font-medium text-white"> {image.weight.toFixed(1)} </div> </div>@@ -18,7 +18,7 @@ const InpaintToolBar = ({ <div className="glass flex w-[400px] items-center gap-2 rounded-xl p-2"> <div className="flex rounded-lg border border-white/10 bg-white/5"> <button- className={`rounded-l-lg px-4 py-2 text-sm font-medium transition-colors ${+ className={`rounded-s-lg px-4 py-2 text-sm font-medium transition-colors ${ operation === "add" ? "bg-white/20 text-white" : "text-white/60 hover:bg-white/10 hover:text-white"@@ -28,7 +28,7 @@ const InpaintToolBar = ({ Add </button> <button- className={`rounded-r-lg px-4 py-2 text-sm font-medium transition-colors ${+ className={`rounded-e-lg px-4 py-2 text-sm font-medium transition-colors ${ operation === "minus" ? "bg-white/20 text-white" : "text-white/60 hover:bg-white/10 hover:text-white"@@ -173,7 +173,7 @@ const SideToolbar: React.FC<SideToolbarProps> = ({ extra?: React.ReactNode, ) => ( <div className={`glass relative w-fit rounded-2xl p-4 shadow-lg`}>- <button className="bg-ui-controls/60 text-base-fg hover:bg-ui-controls/80 absolute left-4 top-4 flex h-8 w-8 items-center justify-center rounded-full">+ <button className="bg-ui-controls/60 text-base-fg hover:bg-ui-controls/80 absolute start-4 top-4 flex h-8 w-8 items-center justify-center rounded-full"> <PipetteIcon size="0.875em" /> </button> @@ -234,7 +234,7 @@ const SideToolbar: React.FC<SideToolbarProps> = ({ id: "select", label: "Select & Move", icon: (- <MousePointerIcon className="pl-0.5 text-lg" />+ <MousePointerIcon className="ps-0.5 text-lg" /> ), onClick: () => { onSelect();@@ -381,7 +381,7 @@ const SideToolbar: React.FC<SideToolbarProps> = ({ return ( <aside- className={`glass ml-4 flex flex-col items-center gap-3 rounded-xl p-1.5 shadow-lg ${className}`}+ className={`glass ms-4 flex flex-col items-center gap-3 rounded-xl p-1.5 shadow-lg ${className}`} > {/* Shape Color Picker - appears above toolbar when shape is selected */} {selectedColorableNodes.length > 0 && (@@ -400,7 +400,7 @@ const SideToolbar: React.FC<SideToolbarProps> = ({ {open === "shape-color" && ( <div onMouseLeave={() => setOpen(null)}- className="absolute left-14 top-1/2 -translate-y-1/2 rounded-xl border border-white/10 bg-ui-panel transition-all duration-200 ease-in-out"+ className="absolute start-14 top-1/2 -translate-y-1/2 rounded-xl border border-white/10 bg-ui-panel transition-all duration-200 ease-in-out" > {ShapePopout} </div>@@ -426,7 +426,7 @@ const SideToolbar: React.FC<SideToolbarProps> = ({ const toolActionId = TOOL_ACTION_IDS[id]; const toolBinding = toolActionId ? forAction(toolActionId)[0] : undefined; const keyBadge = toolBinding ? (- <span className="pointer-events-none absolute bottom-0 right-0 min-w-[13px] rounded-[4px] bg-black/60 px-0.5 text-center text-[9px] font-medium leading-[13px] text-white/80">+ <span className="pointer-events-none absolute bottom-0 end-0 min-w-[13px] rounded-[4px] bg-black/60 px-0.5 text-center text-[9px] font-medium leading-[13px] text-white/80"> {formatBinding(toolBinding)} </span> ) : null;@@ -589,7 +589,7 @@ const SideToolbar: React.FC<SideToolbarProps> = ({ onMouseLeave={() => { setOpen(null); }}- className="absolute left-14 top-1/2 -translate-y-1/2 rounded-xl border border-white/10 bg-ui-panel transition-all duration-200 ease-in-out"+ className="absolute start-14 top-1/2 -translate-y-1/2 rounded-xl border border-white/10 bg-ui-panel transition-all duration-200 ease-in-out" > {popout} </div>@@ -485,14 +485,14 @@ export const Stage3DBody = ({ <CompletionModal /> <div- className="absolute left-0 top-0 w-full"+ className="absolute start-0 top-0 w-full" onClick={handleOverlayClick} > <div className="grid grid-cols-2 gap-x-4 gap-y-1 2xl:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]"> <div className="min-w-0 flex flex-col items-start gap-2"> {!isRecord && <ControlsTopButtons />} {!isRecord && topBarStartSlot && (- <div className="pl-3">{topBarStartSlot}</div>+ <div className="ps-3">{topBarStartSlot}</div> )} </div> <div className="col-span-2 row-start-2 flex flex-col items-center 2xl:col-span-1 2xl:col-start-2 2xl:row-start-1">@@ -501,7 +501,7 @@ export const Stage3DBody = ({ <Controls3D showImageTo3DButton={showImageTo3DButton} /> )} </div>- <div className="col-start-2 row-start-1 flex min-w-0 flex-wrap items-start justify-end gap-2 pr-3 pt-3 2xl:col-start-3">+ <div className="col-start-2 row-start-1 flex min-w-0 flex-wrap items-start justify-end gap-2 pe-3 pt-3 2xl:col-start-3"> {/* Camera cluster: which camera drives the viewport + the render-frame aspect ratio. */} {!isRecord && <CameraStatusPill />}@@ -515,7 +515,7 @@ export const Stage3DBody = ({ {/* Outliner: vertically centered against the viewport. Sized in vh — deliberately independent of the camera aspect ratio. */} <div- className={`absolute left-4 top-1/2 -translate-y-1/2${+ className={`absolute start-4 top-1/2 -translate-y-1/2${ isRecord ? " hidden" : "" }`} >@@ -523,7 +523,7 @@ export const Stage3DBody = ({ </div> <div- className="absolute bottom-0 left-0 right-0"+ className="absolute bottom-0 start-0 end-0" onClick={handleOverlayClick} > {/* Always mounted: PreviewEngineCamera owns the cam-view canvas@@ -531,7 +531,7 @@ export const Stage3DBody = ({ tears down + recreates the whole engine (wiping the scene). Hide with CSS in record instead. */} <div- className={`absolute bottom-20 mb-4 ml-4 flex origin-bottom-left flex-col gap-2${+ className={`absolute bottom-20 mb-4 ms-4 flex origin-bottom-left rtl:origin-bottom-right flex-col gap-2${ isRecord ? " hidden" : "" }`} style={{ transform: `scale(${getScale()})` }}@@ -550,27 +550,27 @@ export const Stage3DBody = ({ {!isRecord && !timelineExpanded && ( <div className="absolute bottom-4 left-1/2 w-[90vw] max-w-3xl -translate-x-1/2"> <TimelineBar />- <ExitCameraView className="absolute -right-2 top-1/2 -translate-y-1/2 translate-x-full" />+ <ExitCameraView className="absolute -end-2 top-1/2 -translate-y-1/2 translate-x-full rtl:-translate-x-full" /> </div> )} {!isRecord && timelineExpanded && ( <> <TimelineEditor />- <ExitCameraView className="absolute bottom-4 right-4" />+ <ExitCameraView className="absolute bottom-4 end-4" /> </> )} {isRecord && <RecordControls />} <LoadingDots- className="absolute left-0 top-0 z-50"+ className="absolute start-0 top-0 z-50" isShowing={editorLoader.isShowing} type="bricks" message={editorLoader.message} /> {modelSelectorPlacement === "bottom-left" && (- <div className="absolute bottom-6 left-6 z-20 flex items-center gap-3">+ <div className="absolute bottom-6 start-6 z-20 flex items-center gap-3"> <ClassyModelSelector items={stage3dModelList} page={PAGE_ID}@@ -583,7 +583,7 @@ export const Stage3DBody = ({ </div> )} {(showCostCalculator || showHelpMenu) && (- <div className="absolute bottom-4 right-4 z-20 flex items-center gap-2">+ <div className="absolute bottom-4 end-4 z-20 flex items-center gap-2"> {showCostCalculator && ( <CostCalculatorButton modelPage={PAGE_ID} /> )}@@ -224,10 +224,10 @@ export const AnimationsModal = () => { contentHidden={assetDraggingUnder && !reopenAfterDrag} > <Modal.DragHandle>- <div className="absolute left-0 top-0 z-[50] h-[46px] w-full cursor-move" />+ <div className="absolute start-0 top-0 z-[50] h-[46px] w-full cursor-move" /> </Modal.DragHandle> <div className="grid h-full grid-cols-12 gap-3">- <div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:bg-white/10">+ <div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:end-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:bg-white/10"> <div className="flex h-10 items-center justify-between gap-2.5"> <h2 className="font-display text-lg tracking-tight text-white"> Animations@@ -240,7 +240,7 @@ export const AnimationsModal = () => { key={tab.id} variant={activeTab === tab.id ? "primary" : "secondary"} className={twMerge(- "w-full justify-start rounded-[3px] border border-transparent bg-transparent px-3.5 py-2.5 text-left hover:bg-white/15",+ "w-full justify-start rounded-[3px] border border-transparent bg-transparent px-3.5 py-2.5 text-start hover:bg-white/15", activeTab === tab.id && "border-white/15 bg-white/10 text-white hover:bg-white/10", )}@@ -248,7 +248,7 @@ export const AnimationsModal = () => { > <DynamicIcon icon={tab.icon}- className="mr-2 opacity-70"+ className="me-2 opacity-70" /> {tab.label} </Button>@@ -281,18 +281,18 @@ export const AnimationsModal = () => { ref={searchInputRef} placeholder="Search animations" className="relative z-[51] grow"- inputClassName="bg-white/5 pr-10 placeholder-white/40"+ inputClassName="bg-white/5 pe-10 placeholder-white/40" icon={SearchIcon} value={searchTerm} onChange={(e: ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value) }- iconClassName="pointer-events-none left-3 top-1/2 h-4 w-4 -translate-y-1/2 p-0 text-white/50"+ iconClassName="pointer-events-none start-3 top-1/2 h-4 w-4 -translate-y-1/2 p-0 text-white/50" /> {searchTerm && ( <CloseButton onClick={clearSearch}- className="absolute right-2.5 top-1/2 z-[51] h-4 w-4 -translate-y-1/2 bg-white/10 text-[10px] hover:bg-white/20"+ className="absolute end-2.5 top-1/2 z-[51] h-4 w-4 -translate-y-1/2 bg-white/10 text-[10px] hover:bg-white/20" /> )} </div>@@ -20,7 +20,7 @@ export const AssetMenu = () => { return ( <>- <div className="glass absolute left-2 top-1/2 flex -translate-y-1/2 flex-col gap-1 rounded-lg p-1">+ <div className="glass absolute start-2 top-1/2 flex -translate-y-1/2 flex-col gap-1 rounded-lg p-1"> <Tooltip content="Add 3D object to scene" position="right" delay={100}> <Button icon={BoxIcon}@@ -54,10 +54,10 @@ const AllTabSection = ({ }) => ( <div className="mb-0"> <div className="mb-2 flex items-center justify-between">- <h3 className="text-md ml-2 font-semibold opacity-90">{label}</h3>+ <h3 className="text-md ms-2 font-semibold opacity-90">{label}</h3> <Button variant="ghost"- className="mr-3 h-6 gap-1 px-1.5 py-0 text-[10px] tracking-[0.08em] leading-none"+ className="me-3 h-6 gap-1 px-1.5 py-0 text-[10px] tracking-[0.08em] leading-none" onClick={onViewAll} > View all@@ -503,10 +503,10 @@ export const AssetModal = () => { contentHidden={assetDraggingUnder && !reopenAfterDrag} > <Modal.DragHandle>- <div className="absolute left-0 top-0 z-[50] h-[46px] w-full cursor-move" />+ <div className="absolute start-0 top-0 z-[50] h-[46px] w-full cursor-move" /> </Modal.DragHandle> <div className="grid h-full grid-cols-12 gap-3">- <div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:bg-white/10">+ <div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:end-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:bg-white/10"> <div className="flex h-10 items-center justify-between gap-2.5"> <h2 className="font-display text-lg tracking-tight text-white"> ArtCraft Presets@@ -519,7 +519,7 @@ export const AssetModal = () => { key={tab.id} variant={activeAssetTab === tab.id ? "primary" : "secondary"} className={twMerge(- "w-full justify-start rounded-[3px] border border-transparent bg-transparent px-3.5 py-2.5 text-left hover:bg-white/15",+ "w-full justify-start rounded-[3px] border border-transparent bg-transparent px-3.5 py-2.5 text-start hover:bg-white/15", activeAssetTab === tab.id && "border-white/15 bg-white/10 text-white hover:bg-white/10", )}@@ -527,7 +527,7 @@ export const AssetModal = () => { > <DynamicIcon icon={tab.icon}- className="mr-2 opacity-70"+ className="me-2 opacity-70" /> {tab.label} </Button>@@ -551,18 +551,18 @@ export const AssetModal = () => { ref={searchInputRef} placeholder="Search" className="relative z-[51] grow"- inputClassName="bg-white/5 pr-10 placeholder-white/40"+ inputClassName="bg-white/5 pe-10 placeholder-white/40" icon={SearchIcon} value={searchTerm} onChange={(e: ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value) }- iconClassName="pointer-events-none left-3 top-1/2 h-4 w-4 -translate-y-1/2 p-0 text-white/50"+ iconClassName="pointer-events-none start-3 top-1/2 h-4 w-4 -translate-y-1/2 p-0 text-white/50" /> {searchTerm && ( <CloseButton onClick={clearSearch}- className="absolute right-2.5 top-1/2 z-[51] h-4 w-4 -translate-y-1/2 bg-white/10 text-[10px] hover:bg-white/20"+ className="absolute end-2.5 top-1/2 z-[51] h-4 w-4 -translate-y-1/2 bg-white/10 text-[10px] hover:bg-white/20" /> )} </div>@@ -105,7 +105,7 @@ export const ItemElement = ({ item }: Props) => { ? patchExpressionObjectType(item.media_type) : item.media_type.toUpperCase() }- className="absolute right-0 mr-[3px] mt-[3px]"+ className="absolute end-0 me-[3px] mt-[3px]" /> )} @@ -140,18 +140,18 @@ export const ItemElement = ({ item }: Props) => { <div className="text-shadow-md absolute inset-0 flex items-center justify-center bg-brand-primary-950/50 text-[13px] font-medium text-white opacity-0 transition-opacity duration-200 group-hover:opacity-100"> {item.type === AssetType.ANIMATION ? ( <>- <FootprintsIcon className="mr-1.5" />+ <FootprintsIcon className="me-1.5" /> Add to Character </> ) : ( <>- <MoveIcon className="mr-1.5" />+ <MoveIcon className="me-1.5" /> Drag to Scene </> )} </div> </div>- <div className="pointer-events-none w-full select-none truncate py-1.5 text-start text-[13px] text-white/80 transition-all duration-200">+ <div dir="auto" className="pointer-events-none w-full select-none truncate py-1.5 text-start text-[13px] text-white/80 transition-all duration-200"> {item.name || item.media_id} </div> </div>@@ -305,7 +305,7 @@ export const ControlPanelSceneObject = () => { as="div" show={isShowing} className={twMerge(- "border border-ui-panel-border bg-ui-controls absolute bottom-16 right-0 mb-4 mr-4 flex h-fit w-56 origin-bottom-right flex-col gap-2 rounded-none p-3.5 text-white",+ "border border-ui-panel-border bg-ui-controls absolute bottom-16 end-0 mb-4 me-4 flex h-fit w-56 origin-bottom-right rtl:origin-bottom-left flex-col gap-2 rounded-none p-3.5 text-white", )} enter="transition-opacity duration-150" enterFrom="opacity-0"@@ -240,7 +240,7 @@ export const ControlsTopButtons = () => { }; return (- <div className="flex flex-col gap-2 pl-3 pt-3">+ <div className="flex flex-col gap-2 ps-3 pt-3"> <div className="flex items-center gap-1.5"> <ButtonDropdown label="File"@@ -136,7 +136,7 @@ export const LoadUserScenes = ({ onSceneSelect }: LoadSceneProps) => { )} <div- className="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-10 bg-gradient-to-t from-ui-panel to-transparent transition-opacity duration-200"+ className="pointer-events-none absolute bottom-0 start-0 end-0 z-10 h-10 bg-gradient-to-t from-ui-panel to-transparent transition-opacity duration-200" style={{ opacity: bottomGradientOpacity }} /> </div>@@ -114,7 +114,7 @@ export const NewSceneFromTemplate = ({ </div> )} <div- className="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-10 bg-gradient-to-t from-ui-panel to-transparent transition-opacity duration-200"+ className="pointer-events-none absolute bottom-0 start-0 end-0 z-10 h-10 bg-gradient-to-t from-ui-panel to-transparent transition-opacity duration-200" style={{ opacity: bottomGradientOpacity }} /> </div>@@ -61,10 +61,10 @@ export const SceneCard: React.FC<SceneCardProps> = ({ onError={() => setLoadError(true)} loading="lazy" />- <div className="absolute left-0 top-0 h-full w-full bg-gradient-to-t from-black/80 to-transparent" />- <div className="absolute bottom-[8px] left-[10px] text-start text-sm drop-shadow-md">+ <div className="absolute start-0 top-0 h-full w-full bg-gradient-to-t from-black/80 to-transparent" />+ <div className="absolute bottom-[8px] start-[10px] text-start text-sm drop-shadow-md"> <div className="flex flex-col">- <span className="w-60 truncate text-sm font-medium">+ <span dir="auto" className="w-60 truncate text-sm font-medium"> {scene.name} </span> {showDate && (@@ -75,7 +75,7 @@ export const SceneCard: React.FC<SceneCardProps> = ({ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"- className={`absolute right-1.5 top-1.5 h-[22px] w-[22px] transition-opacity duration-200 ease-in-out ${+ className={`absolute end-1.5 top-1.5 h-[22px] w-[22px] transition-opacity duration-200 ease-in-out ${ selectedSceneId === scene.token ? "opacity-100" : "opacity-0" }`} >@@ -11,7 +11,7 @@ export const EditorLoadingBar = () => { <LoadingBar id="editor-loading-bar" show={editorLoader.isShowing}- wrapperClassName="absolute top-0 left-0 z-[80]"+ wrapperClassName="absolute top-0 start-0 z-[80]" innerWrapperClassName="max-w-screen-sm" hasSpinner progressData={{@@ -34,7 +34,7 @@ export function EntranceDebugPanel() { const splatSec = (ENTRANCE_BASE_DURATIONS.splat * scale).toFixed(2); return (- <div className="pointer-events-auto absolute right-2 top-16 z-50 w-56 rounded-lg border border-white/10 bg-black/70 p-3 text-xs text-white/80 backdrop-blur-md">+ <div className="pointer-events-auto absolute end-2 top-16 z-50 w-56 rounded-lg border border-white/10 bg-black/70 p-3 text-xs text-white/80 backdrop-blur-md"> <div className="mb-2 flex items-center justify-between font-medium text-white/90"> <span>Entrance speed</span> <span className="tabular-nums">{scale.toFixed(2)}×</span>@@ -71,7 +71,7 @@ const OutlinerRow = ({ item }: { item: OutlinerItem }) => { <DynamicIcon icon={item.icon} /> </div> <span className="min-w-0">- <span className="block truncate text-[13px] font-semibold text-white">+ <span dir="auto" className="block truncate text-[13px] font-semibold text-white"> {item.name} </span> <span className="block truncate text-xs text-white/50">@@ -47,7 +47,7 @@ export function PerfStatsOverlay() { return ( <div ref={containerRef}- className="pointer-events-none absolute bottom-2 left-2 z-50"+ className="pointer-events-none absolute bottom-2 start-2 z-50" /> ); }@@ -19,11 +19,11 @@ export const PreviewBox = ({ imageUrl }: PreviewBoxProps) => { if (!imageUrl || dismissed) return null; return (- <div className="glass absolute bottom-6 right-6 z-20 flex w-48 flex-col gap-2 rounded-lg border border-white/10 p-2 shadow-lg">+ <div className="glass absolute bottom-6 end-6 z-20 flex w-48 flex-col gap-2 rounded-lg border border-white/10 p-2 shadow-lg"> <button type="button" onClick={() => setDismissed(true)}- className="absolute right-1 top-1 flex h-5 w-5 items-center justify-center rounded-full text-white/60 hover:bg-white/10 hover:text-white"+ className="absolute end-1 top-1 flex h-5 w-5 items-center justify-center rounded-full text-white/60 hover:bg-white/10 hover:text-white" aria-label="Hide preview" > <XIcon className="text-xs" />@@ -137,7 +137,7 @@ export const PreviewEngineCamera = () => { <div className="flex h-full w-full items-center justify-center bg-ui-controls"> <LoaderCircleIcon size="3em" className="animate-spin" /> </div>- <div className="absolute left-0 top-0 h-full w-full overflow-hidden">+ <div className="absolute start-0 top-0 h-full w-full overflow-hidden"> <CameraViewCanvas className="!h-full !w-full" /> </div> </div>@@ -21,7 +21,7 @@ export const RenderOverlay = () => { return ( <> <LoadingDots- className="absolute left-0 top-0 z-[60]"+ className="absolute start-0 top-0 z-[60]" isShowing={progress !== null} type="bricks" message={message}@@ -211,9 +211,10 @@ export const TimelineClipRow = ({ > {bakedClips.map((clipName, clipIndex) => ( <button+ dir="auto" key={clipIndex} type="button"- className="block w-full truncate px-2.5 py-1 text-left text-[11px] text-base-fg/90 hover:bg-white/10"+ className="block w-full truncate px-2.5 py-1 text-start text-[11px] text-base-fg/90 hover:bg-white/10" onClick={() => addBaked(clipIndex)} > {clipName}@@ -350,12 +350,12 @@ export const TimelineEditor = () => { > <SkipForwardIcon className="h-3.5 w-3.5" /> </button>- <span className="ml-2 font-mono tabular-nums text-xs text-base-fg/60">+ <span className="ms-2 font-mono tabular-nums text-xs text-base-fg/60"> {formatTimecodeFrames(playhead, fps)} / {formatTimecode(duration)} </span>- {/* ml-auto lives on a wrapper: Tooltip's className styles the popup+ {/* ms-auto lives on a wrapper: Tooltip's className styles the popup panel, and its trigger div is hard-coded to "relative". */}- <div className="ml-auto">+ <div className="ms-auto"> <Tooltip content={ focusSelected@@ -219,7 +219,7 @@ function InfoHint({ content }: { content: ReactNode }) { ref={anchorRef} type="button" aria-label="More info"- className="ml-0.5 inline-flex h-4 w-4 shrink-0 items-center justify-center text-base-fg/40 transition-colors hover:text-base-fg/80"+ className="ms-0.5 inline-flex h-4 w-4 shrink-0 items-center justify-center text-base-fg/40 transition-colors hover:text-base-fg/80" onClick={(e) => { // Don't let the click select the row. e.stopPropagation();@@ -406,12 +406,12 @@ function RichListRow({ )} </div> {item.trailing && (- <div className="ml-1 flex shrink-0 items-center">{item.trailing}</div>+ <div className="ms-1 flex shrink-0 items-center">{item.trailing}</div> )} {rightNode ?? (item.selected && (item.selectedRight ?? (- <span className="ml-1 flex h-5 w-5 shrink-0 items-center justify-center">+ <span className="ms-1 flex h-5 w-5 shrink-0 items-center justify-center"> <CheckIcon aria-hidden="true" className="h-4 w-4 text-white" strokeWidth={2.5} /> </span> )))}@@ -879,9 +879,9 @@ export const PopoverMenu = ({ }; const alignClasses = {- start: "left-0",+ start: "start-0", center: "left-1/2 -translate-x-1/2",- end: "right-0",+ end: "end-0", }; // Hover timers and refs@@ -1176,7 +1176,7 @@ export const PopoverMenu = ({ item={item} active={openTooltipIdx === index} rightNode={- <ChevronRightIcon className="ml-1 shrink-0 text-xs text-base-fg/50" />+ <ChevronRightIcon className="ms-1 shrink-0 text-xs text-base-fg/50" /> } /> </SubmenuFlyout>@@ -1247,7 +1247,7 @@ export const PopoverMenu = ({ ) : mode === "hoverSelect" ? ( <div className="relative flex flex-col text-sm text-base-fg overflow-visible"> {maxListHeight && canScrollUp && (- <div className="absolute top-0 left-0 right-0 z-20 flex justify-center bg-gradient-to-b from-ui-controls via-ui-controls/80 to-transparent py-1.5 pointer-events-none">+ <div className="absolute top-0 start-0 end-0 z-20 flex justify-center bg-gradient-to-b from-ui-controls via-ui-controls/80 to-transparent py-1.5 pointer-events-none"> <ChevronUpIcon className="text-base-fg/60 text-xs animate-bounce" /> </div> )}@@ -1288,7 +1288,7 @@ export const PopoverMenu = ({ className={twMerge( "group flex cursor-pointer items-start gap-2 rounded-[3px] px-2 py-2 transition-all", item.selected- ? "bg-white/10 border-l-2 border-white"+ ? "bg-white/10 border-s-2 border-white" : "hover:bg-white/5", !item.selected && openTooltipIdx === index ? "bg-white/5"@@ -1340,14 +1340,14 @@ export const PopoverMenu = ({ </div> {item.trailing && (- <div className="ml-2 mr-1 flex items-center">+ <div className="ms-2 me-1 flex items-center"> {item.trailing} </div> )} {item.selected && (item.selectedRight ?? (- <span className="text-white text-xl flex items-center justify-center mr-1">+ <span className="text-white text-xl flex items-center justify-center me-1"> <CheckIcon aria-hidden="true" className="h-4 w-4 shrink-0" strokeWidth={2.5} /> </span> ))}@@ -1406,7 +1406,7 @@ export const PopoverMenu = ({ })} </div> {maxListHeight && canScrollDown && (- <div className="absolute bottom-0 left-0 right-0 z-20 flex justify-center bg-gradient-to-t from-ui-controls via-ui-controls/80 to-transparent py-1.5 pointer-events-none">+ <div className="absolute bottom-0 start-0 end-0 z-20 flex justify-center bg-gradient-to-t from-ui-controls via-ui-controls/80 to-transparent py-1.5 pointer-events-none"> <ChevronDownIcon className="text-base-fg/60 text-xs animate-bounce" /> </div> )}@@ -1495,20 +1495,20 @@ export const PopoverMenu = ({ </div> {/* Optional trailing content on the right */} {item.trailing && (- <div className="ml-2 flex items-center">+ <div className="ms-2 flex items-center"> {item.trailing} </div> )} {/* Optional trailing content on the right */} {item.trailing && (- <div className="ml-2 flex items-center">+ <div className="ms-2 flex items-center"> {item.trailing} </div> )} {mode === "toggle" && ( <span- className="ml-2 flex h-5 w-5 shrink-0 items-center justify-center"+ className="ms-2 flex h-5 w-5 shrink-0 items-center justify-center" > {item.selected && ( <CheckIcon aria-hidden="true" className="h-4 w-4 text-white" strokeWidth={2.5} />@@ -1558,14 +1558,14 @@ export const PopoverMenu = ({ </div> {/* Optional trailing content on the right */} {item.trailing && (- <div className="ml-2 flex items-center">+ <div className="ms-2 flex items-center"> {item.trailing} </div> )} {mode === "toggle" && ( <span- className="ml-2 flex h-5 w-5 shrink-0 items-center justify-center"+ className="ms-2 flex h-5 w-5 shrink-0 items-center justify-center" > {item.selected && ( <CheckIcon aria-hidden="true" className="h-4 w-4 text-white" strokeWidth={2.5} />@@ -346,6 +346,7 @@ export function CostBreakdownModal({ activeTabId }: CostBreakdownModalProps) { <div className="flex justify-between items-center"> <span className="text-base-fg/60">Model</span> <span+ dir="auto" className="text-base-fg font-medium truncate max-w-[140px]" title={selectedModel.selectorName} >@@ -91,14 +91,14 @@ export function CreditsModal({ onClick={() => handlePurchase(pack)} disabled={purchasingId !== null} className={twMerge(- "group relative flex flex-col gap-6 border p-6 text-left transition-all disabled:cursor-not-allowed disabled:opacity-60",+ "group relative flex flex-col gap-6 border p-6 text-start transition-all disabled:cursor-not-allowed disabled:opacity-60", isPopular ? "border-white/60 bg-white/[0.06] hover:border-white" : "border-white/15 bg-white/[0.02] hover:border-white/30 hover:bg-white/[0.04]", )} > {pack.badge && (- <span className="absolute -top-2.5 right-3 bg-white px-2.5 py-0.5 font-mono text-[10px] font-semibold uppercase tracking-[0.12em] text-black">+ <span className="absolute -top-2.5 end-3 bg-white px-2.5 py-0.5 font-mono text-[10px] font-semibold uppercase tracking-[0.12em] text-black"> {pack.badge} </span> )}@@ -73,7 +73,7 @@ export function PricingContent({ title, subtitle }: PricingContentProps) { return ( <div className="min-h-0 flex-1 bg-[#101014] text-white"> <div className="bg-primary text-white">- <div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-1 py-2.5 pl-6 pr-14 md:pl-10">+ <div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-1 py-2.5 ps-6 pe-14 md:ps-10"> <p className="pricing-hud flex items-center gap-2 font-bold"> <TagIcon aria-hidden className="h-3.5 w-3.5" /> Limited-time offer@@ -156,7 +156,7 @@ export function PricingContent({ title, subtitle }: PricingContentProps) { Buy credits </Button> </div>- <div className="border-t border-white/15 p-6 md:border-l md:border-t-0 md:p-10">+ <div className="border-t border-white/15 p-6 md:border-s md:border-t-0 md:p-10"> <p className="pricing-hud text-white/40">† Footnote</p> <p className="mt-4 max-w-md leading-relaxed text-white/55"> ArtCraft can be used without paying for a subscription. You can@@ -16,7 +16,7 @@ export const PricingPromoBanner = ({ className }: PricingPromoBannerProps) => ( {/* Decorative glow, top-right */} <div aria-hidden- className="pointer-events-none absolute -right-16 -top-20 h-64 w-64 bg-primary/25 blur-3xl"+ className="pointer-events-none absolute -end-16 -top-20 h-64 w-64 bg-primary/25 blur-3xl" /> <div className="relative flex flex-col gap-3">@@ -276,7 +276,7 @@ const CharacterListView = ({ title: "Delete character?", message: ( <p className="text-sm text-white/70">- This will permanently delete <strong>{character.name}</strong>. This+ This will permanently delete <strong><bdi>{character.name}</bdi></strong>. This action cannot be undone. </p> ),@@ -390,7 +390,7 @@ const CharacterListView = ({ </div> </div> <div className="px-2 py-1.5 text-center">- <p className="truncate text-xs font-medium text-base-fg/50">+ <p dir="auto" className="truncate text-xs font-medium text-base-fg/50"> {pending.name} </p> </div>@@ -426,7 +426,7 @@ const CharacterListView = ({ )} </div> <div className="px-2 py-1.5">- <p className="truncate text-xs font-medium text-base-fg/80">+ <p dir="auto" className="truncate text-xs font-medium text-base-fg/80"> {character.name} </p> </div>@@ -435,7 +435,7 @@ const CharacterListView = ({ {/* View / Edit / Delete overlay buttons. Hover-revealed on desktop; always visible on touch devices (no hover). */} {(character.maybe_avatar?.cdn_url || isUserCreated) && (- <div className="absolute right-1.5 top-1.5 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)]:opacity-100">+ <div className="absolute end-1.5 top-1.5 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)]:opacity-100"> {character.maybe_avatar?.cdn_url && ( <button onClick={(e) => {@@ -606,6 +606,7 @@ const EditCharacterView = ({ <span className="font-normal text-base-fg/40">(optional)</span> </Label> <textarea+ dir="auto" id="edit-character-description" value={description} onChange={(e) => setDescription(e.target.value)}@@ -867,7 +868,7 @@ const NewCharacterView = ({ e.stopPropagation(); removeImage(0); }}- className="absolute right-2 top-2 flex h-7 w-7 items-center justify-center bg-black/60 text-white/80 opacity-0 transition-all group-hover:opacity-100 hover:bg-red-500"+ className="absolute end-2 top-2 flex h-7 w-7 items-center justify-center bg-black/60 text-white/80 opacity-0 transition-all group-hover:opacity-100 hover:bg-red-500" > <XIcon className="text-sm" /> </button>@@ -937,6 +938,7 @@ const NewCharacterView = ({ <span className="font-normal text-base-fg/40">(optional)</span> </Label> <textarea+ dir="auto" id="character-description" value={description} onChange={(e) => setDescription(e.target.value)}@@ -134,7 +134,7 @@ const AudioRefTile = ({ )} /> </button>- <div className="absolute bottom-0 left-0 right-0 flex items-center justify-center bg-black/70 py-0.5 text-[10px] font-bold text-white pointer-events-none">+ <div className="absolute bottom-0 start-0 end-0 flex items-center justify-center bg-black/70 py-0.5 text-[10px] font-bold text-white pointer-events-none"> #{index + 1} · {audio.duration}s </div> <button@@ -146,7 +146,7 @@ const AudioRefTile = ({ } onRemove(audio.id); }}- className="opacity-0 group-hover:opacity-100 absolute right-[2px] top-[2px] flex h-5 w-5 items-center justify-center rounded-full bg-black/50 hover:bg-red/70 text-white backdrop-blur-md transition-colors hover:bg-black cursor-pointer"+ className="opacity-0 group-hover:opacity-100 absolute end-[2px] top-[2px] flex h-5 w-5 items-center justify-center rounded-full bg-black/50 hover:bg-red/70 text-white backdrop-blur-md transition-colors hover:bg-black cursor-pointer" > <XIcon className="h-2.5 w-2.5" /> </button>@@ -287,7 +287,7 @@ export const ImagePromptRow = ({ className="h-full w-full object-cover" /> {isReferenceMode && (- <div className="absolute bottom-0 left-0 right-0 flex items-center justify-center bg-black/60 py-0.5 text-[11px] font-bold text-white">+ <div className="absolute bottom-0 start-0 end-0 flex items-center justify-center bg-black/60 py-0.5 text-[11px] font-bold text-white"> {index + 1} </div> )}@@ -302,7 +302,7 @@ export const ImagePromptRow = ({ onPointerDown={(e) => { e.stopPropagation(); }}- className="opacity-0 group-hover:opacity-100 absolute right-[2px] top-[2px] flex h-5 w-5 items-center justify-center rounded-full bg-black/50 hover:bg-red/70 text-white backdrop-blur-md transition-colors hover:bg-black cursor-pointer"+ className="opacity-0 group-hover:opacity-100 absolute end-[2px] top-[2px] flex h-5 w-5 items-center justify-center rounded-full bg-black/50 hover:bg-red/70 text-white backdrop-blur-md transition-colors hover:bg-black cursor-pointer" > <XIcon className="h-2.5 w-2.5" /> </button>@@ -865,7 +865,7 @@ export const ImagePromptRow = ({ )} <div className={twMerge(- "absolute left-0 glass w-full rounded-t-[3px] flex",+ "absolute start-0 glass w-full rounded-t-[3px] flex", showVideoReferenceSection ? "-top-[144px]" : "-top-[72px]", className, )}@@ -945,7 +945,7 @@ export const ImagePromptRow = ({ className="h-full w-full object-cover" /> {isReferenceMode && (- <div className="absolute bottom-0 left-0 right-0 flex items-center justify-center bg-black/60 py-0.5 text-[11px] font-bold text-white">+ <div className="absolute bottom-0 start-0 end-0 flex items-center justify-center bg-black/60 py-0.5 text-[11px] font-bold text-white"> {index + 1} </div> )}@@ -954,7 +954,7 @@ export const ImagePromptRow = ({ e.stopPropagation(); handleRemoveReference(image.id); }}- className="opacity-0 group-hover:opacity-100 absolute right-[2px] top-[2px] flex h-5 w-5 items-center justify-center rounded-full bg-black/50 hover:bg-red/70 text-white backdrop-blur-md transition-colors hover:bg-black cursor-pointer"+ className="opacity-0 group-hover:opacity-100 absolute end-[2px] top-[2px] flex h-5 w-5 items-center justify-center rounded-full bg-black/50 hover:bg-red/70 text-white backdrop-blur-md transition-colors hover:bg-black cursor-pointer" > <XIcon @@ -1077,7 +1077,7 @@ export const ImagePromptRow = ({ e.stopPropagation(); setEndFrameImage?.(undefined); }}- className="opacity-0 group-hover:opacity-100 absolute right-[2px] top-[2px] flex h-5 w-5 items-center justify-center rounded-full bg-black/50 hover:bg-red/70 text-white backdrop-blur-md transition-colors hover:bg-black cursor-pointer"+ className="opacity-0 group-hover:opacity-100 absolute end-[2px] top-[2px] flex h-5 w-5 items-center justify-center rounded-full bg-black/50 hover:bg-red/70 text-white backdrop-blur-md transition-colors hover:bg-black cursor-pointer" > <XIcon @@ -1185,7 +1185,7 @@ export const ImagePromptRow = ({ preload="metadata" className="h-full w-full object-cover" />- <div className="absolute bottom-0 left-0 right-0 flex items-center justify-center bg-black/70 py-0.5 text-[10px] font-bold text-white">+ <div className="absolute bottom-0 start-0 end-0 flex items-center justify-center bg-black/70 py-0.5 text-[10px] font-bold text-white"> {video.duration}s </div> <button@@ -1193,7 +1193,7 @@ export const ImagePromptRow = ({ e.stopPropagation(); handleRemoveVideo(video.id); }}- className="opacity-0 group-hover:opacity-100 absolute right-[2px] top-[2px] flex h-5 w-5 items-center justify-center rounded-full bg-black/50 hover:bg-red/70 text-white backdrop-blur-md transition-colors hover:bg-black cursor-pointer"+ className="opacity-0 group-hover:opacity-100 absolute end-[2px] top-[2px] flex h-5 w-5 items-center justify-center rounded-full bg-black/50 hover:bg-red/70 text-white backdrop-blur-md transition-colors hover:bg-black cursor-pointer" > <XIcon @@ -222,7 +222,7 @@ export function MentionChipMenu({ </span> </button> <div className="min-w-0">- <div className="truncate text-sm font-medium text-base-fg">+ <div dir="auto" className="truncate text-sm font-medium text-base-fg"> {currentName} </div> <div className="text-[11px] text-base-fg/50">@@ -233,12 +233,12 @@ export function MentionChipMenu({ <div className="my-1 border-t border-ui-panel-border" /> <button type="button" className={MENU_ROW} onClick={() => setView("replace")}> <RefreshCwIcon className="h-3.5 w-3.5 opacity-60" />- <span className="flex-1 text-left">Replace</span>+ <span className="flex-1 text-start">Replace</span> <span className="text-xs text-base-fg/40">{replaceItems.length}</span> </button> <button type="button" className={MENU_ROW} onClick={onPreview}> <EyeIcon className="h-3.5 w-3.5 opacity-60" />- <span className="flex-1 text-left">Preview</span>+ <span className="flex-1 text-start">Preview</span> </button> <div className="my-1 border-t border-ui-panel-border" /> <button@@ -247,7 +247,7 @@ export function MentionChipMenu({ onClick={onRemove} > <Trash2Icon className="h-3.5 w-3.5 opacity-60" />- <span className="flex-1 text-left">Remove</span>+ <span className="flex-1 text-start">Remove</span> </button> </> ) : (@@ -264,7 +264,7 @@ export function MentionChipMenu({ <div className="min-w-0"> <div className="text-sm font-medium text-base-fg">Replace</div> <div className="truncate text-[11px] text-base-fg/50">- Currently {currentName}+ Currently <bdi>{currentName}</bdi> </div> </div> </div>@@ -288,7 +288,7 @@ export function MentionChipMenu({ name={item.label} type={item.type} />- <span className="min-w-0 flex-1 truncate text-left">+ <span dir="auto" className="min-w-0 flex-1 truncate text-start"> {item.label.replace(/^@/, "")} </span> </button>@@ -1585,6 +1585,7 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>( <div ref={editorRef}+ dir="auto" contentEditable={!disabled} onInput={handleInput} onCompositionStart={handleCompositionStart}@@ -430,10 +430,11 @@ export const PromptBox2D = ({ <div className="promptbox-resize-wrap relative flex-1"> <textarea+ dir="auto" ref={textareaRef} rows={1} placeholder="Describe your image..."- className={`promptbox-scrollbar text-md min-h-[2.5em] w-full resize-y overflow-y-auto bg-transparent pr-8 text-base-fg placeholder-base-fg/60 focus:outline-none ${isExpanded ? "max-h-[500px]" : "max-h-[5.5em]"}`}+ className={`promptbox-scrollbar text-md min-h-[2.5em] w-full resize-y overflow-y-auto bg-transparent pr-8 rtl:pr-0 rtl:pl-8 text-base-fg placeholder-base-fg/60 focus:outline-none ${isExpanded ? "max-h-[500px]" : "max-h-[5.5em]"}`} value={prompt} onChange={handleChange} onPaste={handlePaste}@@ -443,7 +444,7 @@ export const PromptBox2D = ({ /> <PromptFullscreenButton onClick={openFullscreen} /> <span- className={`pointer-events-none absolute -bottom-1 right-4 text-[10px] tabular-nums ${isFinite(maxLen) && prompt.length > maxLen ? "text-red-500" : "text-base-fg/40"}`}+ className={`pointer-events-none absolute -bottom-1 end-4 text-[10px] tabular-nums ${isFinite(maxLen) && prompt.length > maxLen ? "text-red-500" : "text-base-fg/40"}`} > {prompt.length} / {isFinite(maxLen) ? maxLen : "∞"} </span>@@ -586,6 +587,7 @@ export const PromptBox2D = ({ } > <textarea+ dir="auto" placeholder="Describe your image..." className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded-[3px] bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none" value={prompt}@@ -728,10 +728,11 @@ export const PromptBox3D = ({ <div className="promptbox-resize-wrap relative flex-1"> <textarea+ dir="auto" ref={textareaRef} rows={1} placeholder="Describe your image..."- className={`promptbox-scrollbar text-md min-h-[2.5em] w-full resize-y overflow-y-auto bg-transparent pr-8 text-base-fg placeholder-base-fg/60 focus:outline-none ${isExpanded ? "max-h-[300px]" : "max-h-[5.5em]"}`}+ className={`promptbox-scrollbar text-md min-h-[2.5em] w-full resize-y overflow-y-auto bg-transparent pr-8 rtl:pr-0 rtl:pl-8 text-base-fg placeholder-base-fg/60 focus:outline-none ${isExpanded ? "max-h-[300px]" : "max-h-[5.5em]"}`} value={prompt} onChange={handleChange} onPaste={handlePaste}@@ -747,7 +748,7 @@ export const PromptBox3D = ({ /> <PromptFullscreenButton onClick={openFullscreen} /> <span- className={`pointer-events-none absolute -bottom-1 right-4 text-[10px] tabular-nums ${isFinite(maxLen) && prompt.length > maxLen ? "text-red-500" : "text-base-fg/40"}`}+ className={`pointer-events-none absolute -bottom-1 end-4 text-[10px] tabular-nums ${isFinite(maxLen) && prompt.length > maxLen ? "text-red-500" : "text-base-fg/40"}`} > {prompt.length} / {isFinite(maxLen) ? maxLen : "∞"} </span>@@ -1034,6 +1035,7 @@ export const PromptBox3D = ({ } > <textarea+ dir="auto" placeholder="Describe your image..." className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded-[3px] bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none" value={prompt}@@ -567,10 +567,11 @@ export const PromptBoxAudio = ({ <div className="flex gap-3"> <div className="promptbox-resize-wrap relative flex-1"> <textarea+ dir="auto" ref={textareaRef} rows={1} placeholder="Describe the music or sound you want..."- className="promptbox-scrollbar text-md min-h-[2.5em] w-full resize-y overflow-y-auto bg-transparent pr-8 text-base-fg placeholder-base-fg/60 transition-[height] duration-200 ease-out focus:outline-none"+ className="promptbox-scrollbar text-md min-h-[2.5em] w-full resize-y overflow-y-auto bg-transparent pr-8 rtl:pr-0 rtl:pl-8 text-base-fg placeholder-base-fg/60 transition-[height] duration-200 ease-out focus:outline-none" value={prompt} onChange={handleChange} onPaste={handlePaste}@@ -668,6 +669,7 @@ export const PromptBoxAudio = ({ } > <textarea+ dir="auto" placeholder="Describe the music or sound you want..." className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded-[3px] bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none" value={prompt}@@ -425,10 +425,11 @@ export const PromptBoxEdit = ({ )} <div className="promptbox-resize-wrap relative flex-1"> <textarea+ dir="auto" ref={textareaRef} rows={1} placeholder="Write what you want to change in your image and click generate..."- className={`promptbox-scrollbar text-md min-h-[2.5em] w-full resize-y overflow-y-auto bg-transparent pr-8 text-white placeholder-white placeholder:text-white/60 focus:outline-none ${isExpanded ? "max-h-[500px]" : "max-h-[5.5em]"}`}+ className={`promptbox-scrollbar text-md min-h-[2.5em] w-full resize-y overflow-y-auto bg-transparent pr-8 rtl:pr-0 rtl:pl-8 text-white placeholder-white placeholder:text-white/60 focus:outline-none ${isExpanded ? "max-h-[500px]" : "max-h-[5.5em]"}`} value={prompt} onChange={handleChange} onPaste={handlePaste}@@ -438,7 +439,7 @@ export const PromptBoxEdit = ({ /> <PromptFullscreenButton onClick={openFullscreen} /> <span- className={`pointer-events-none absolute -bottom-1 right-4 text-[10px] tabular-nums ${isFinite(maxLen) && prompt.length > maxLen ? "text-red-500" : "text-white/40"}`}+ className={`pointer-events-none absolute -bottom-1 end-4 text-[10px] tabular-nums ${isFinite(maxLen) && prompt.length > maxLen ? "text-red-500" : "text-white/40"}`} > {prompt.length} / {isFinite(maxLen) ? maxLen : "∞"} </span>@@ -589,6 +590,7 @@ export const PromptBoxEdit = ({ } > <textarea+ dir="auto" placeholder="Write what you want to change in your image and click generate..." className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded-[3px] bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none" value={prompt}@@ -484,10 +484,11 @@ export const PromptBoxImage = ({ <div className="promptbox-resize-wrap relative flex-1"> <textarea+ dir="auto" ref={textareaRef} rows={1} placeholder="Describe what you want in the image..."- className="promptbox-scrollbar text-md min-h-[2.5em] w-full resize-y overflow-y-auto bg-transparent pr-8 text-base-fg placeholder-base-fg/60 transition-[height] duration-200 ease-out focus:outline-none"+ className="promptbox-scrollbar text-md min-h-[2.5em] w-full resize-y overflow-y-auto bg-transparent pr-8 rtl:pr-0 rtl:pl-8 text-base-fg placeholder-base-fg/60 transition-[height] duration-200 ease-out focus:outline-none" value={prompt} onChange={handleChange} onPaste={handlePaste}@@ -497,7 +498,7 @@ export const PromptBoxImage = ({ /> <PromptFullscreenButton onClick={openFullscreen} /> <span- className={`pointer-events-none absolute -bottom-1 right-4 text-[10px] tabular-nums ${isFinite(maxLen) && prompt.length > maxLen ? "text-red-500" : "text-base-fg/40"}`}+ className={`pointer-events-none absolute -bottom-1 end-4 text-[10px] tabular-nums ${isFinite(maxLen) && prompt.length > maxLen ? "text-red-500" : "text-base-fg/40"}`} > {prompt.length} / {isFinite(maxLen) ? maxLen : "∞"} </span>@@ -625,6 +626,7 @@ export const PromptBoxImage = ({ } > <textarea+ dir="auto" placeholder="Describe what you want in the image..." className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded-[3px] bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none" value={prompt}@@ -1380,7 +1380,7 @@ export const PromptBoxVideo = ({ ? "Use @Image1, @Video1, @Audio1... to reference uploads in prompt..." : "Describe what you want to happen in the video..." }- className="promptbox-scrollbar text-md relative min-h-[2.5em] w-full resize-y overflow-y-auto bg-transparent pr-8 text-base-fg"+ className="promptbox-scrollbar text-md relative min-h-[2.5em] w-full resize-y overflow-y-auto bg-transparent pr-8 rtl:pr-0 rtl:pl-8 text-base-fg" onKeyDown={(e) => { if (e.key !== "Enter") return; const isSubmitCombo = enterToGenerate && !e.shiftKey;@@ -1400,10 +1400,11 @@ export const PromptBoxVideo = ({ /> ) : ( <textarea+ dir="auto" ref={textareaRef} rows={1} placeholder="Describe what you want to happen in the video..."- className="promptbox-scrollbar text-md relative min-h-[2.5em] w-full resize-y overflow-y-auto bg-transparent pr-8 text-base-fg placeholder-base-fg/60 focus:outline-none"+ className="promptbox-scrollbar text-md relative min-h-[2.5em] w-full resize-y overflow-y-auto bg-transparent pr-8 rtl:pr-0 rtl:pl-8 text-base-fg placeholder-base-fg/60 focus:outline-none" value={prompt} onChange={handleChange} onPaste={handlePaste}@@ -1414,7 +1415,7 @@ export const PromptBoxVideo = ({ )} <PromptFullscreenButton onClick={openFullscreen} /> <span- className={`pointer-events-none absolute -bottom-1 right-4 text-[10px] tabular-nums ${isFinite(maxLen) && prompt.length > maxLen ? "text-red-500" : "text-base-fg/40"}`}+ className={`pointer-events-none absolute -bottom-1 end-4 text-[10px] tabular-nums ${isFinite(maxLen) && prompt.length > maxLen ? "text-red-500" : "text-base-fg/40"}`} > {prompt.length} / {isFinite(maxLen) ? maxLen : "∞"} </span>@@ -1679,6 +1680,7 @@ export const PromptBoxVideo = ({ /> ) : ( <textarea+ dir="auto" placeholder="Describe what you want to happen in the video..." className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded-[3px] bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none" value={prompt}@@ -19,7 +19,7 @@ export const PromptFullscreenButton = ({ className, }: PromptFullscreenButtonProps) => { return (- <div className={`absolute right-0 top-0 z-10 ${className ?? ""}`}>+ <div className={`absolute end-0 top-0 z-10 ${className ?? ""}`}> <Tooltip content="Focus mode" position="top" delay={200}> <button type="button"@@ -220,7 +220,7 @@ export const AudioReferenceRow = forwardRef< ({referenceAudios.length}/{maxAudioCount}) </span> {audioRequired && referenceAudios.length === 0 && (- <span className="ml-1.5 text-xs font-medium text-red-500">+ <span className="ms-1.5 text-xs font-medium text-red-500"> required </span> )}@@ -346,7 +346,7 @@ function AudioRefTile({ > <DynamicIcon icon={isPlaying ? SquareIcon : PlayIcon}- className={`h-2 w-2 ${isPlaying ? "" : "ml-px"}`}+ className={`h-2 w-2 ${isPlaying ? "" : "ms-px"}`} /> </button> <span className="text-xs font-medium text-base-fg/80">@@ -143,18 +143,18 @@ export const DeckCard = ({ )} {item.kind === "video" && (- <div className="pointer-events-none absolute left-[3px] top-[3px] flex h-4 w-4 items-center justify-center bg-black/60 text-white">+ <div className="pointer-events-none absolute start-[3px] top-[3px] flex h-4 w-4 items-center justify-center bg-black/60 text-white"> <VideoIcon className="h-2.5 w-2.5" /> </div> )} {item.kind === "audio" && (- <div className="pointer-events-none absolute left-[3px] top-[3px] flex h-4 w-4 items-center justify-center bg-black/60 text-white">+ <div className="pointer-events-none absolute start-[3px] top-[3px] flex h-4 w-4 items-center justify-center bg-black/60 text-white"> <MusicIcon className="h-2.5 w-2.5" /> </div> )} {item.duration != null && (- <div className="pointer-events-none absolute bottom-0 left-0 right-0 flex items-center justify-center bg-black/70 py-0.5 text-[10px] font-bold text-white">+ <div className="pointer-events-none absolute bottom-0 start-0 end-0 flex items-center justify-center bg-black/70 py-0.5 text-[10px] font-bold text-white"> {item.duration}s </div> )}@@ -176,7 +176,7 @@ export const DeckCard = ({ }} onMouseDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}- className="absolute right-[2px] top-[2px] flex h-5 w-5 cursor-pointer items-center justify-center rounded-full bg-black/50 text-white opacity-0 backdrop-blur-md transition-colors hover:bg-red/70 group-hover:opacity-100"+ className="absolute end-[2px] top-[2px] flex h-5 w-5 cursor-pointer items-center justify-center rounded-full bg-black/50 text-white opacity-0 backdrop-blur-md transition-colors hover:bg-red/70 group-hover:opacity-100" > <XIcon className="h-2.5 w-2.5" /> </button>@@ -235,7 +235,7 @@ export const DeckAddMenu = ({ key={action.key} type="button" onClick={action.onSelect}- className="flex w-full items-center gap-2.5 rounded-[3px] px-2.5 py-1.5 text-left text-[13px] font-medium text-base-fg transition-colors hover:bg-white/10"+ className="flex w-full items-center gap-2.5 rounded-[3px] px-2.5 py-1.5 text-start text-[13px] font-medium text-base-fg transition-colors hover:bg-white/10" > <DynamicIcon icon={@@ -276,7 +276,7 @@ export const ReferenceDeck = ({ alwaysExpanded ? "relative max-w-full" : twMerge(- "absolute bottom-0 left-0 z-40 origin-bottom-left transition-all duration-200 ease-out",+ "absolute bottom-0 start-0 z-40 origin-bottom-left transition-all rtl:origin-bottom-right duration-200 ease-out", expanded ? "visible translate-y-0 scale-100 opacity-100" : "pointer-events-none invisible translate-y-1 scale-95 opacity-0 [transition:opacity_.2s,transform_.2s,visibility_0s_.2s]",@@ -439,7 +439,7 @@ export const ReferenceDeck = ({ /> ))} {overflowCount > 0 && (- <div className="pointer-events-none absolute -right-1 -top-1 z-10 rounded-full bg-primary px-1.5 py-0.5 text-[10px] font-bold text-white shadow">+ <div className="pointer-events-none absolute -end-1 -top-1 z-10 rounded-full bg-primary px-1.5 py-0.5 text-[10px] font-bold text-white shadow"> {items.length} </div> )}@@ -38,14 +38,14 @@ export const Select = ({ <> <ListboxButton id={id}- className="relative h-10 w-full cursor-pointer rounded-[3px] bg-ui-controls text-base-fg border border-ui-controls-border py-2 pl-3 pr-10 text-left outline-none outline-offset-0 transition-colors duration-150 ease-in-out sm:text-sm focus:!outline-none hover:border-white/40"+ className="relative h-10 w-full cursor-pointer rounded-[3px] bg-ui-controls text-base-fg border border-ui-controls-border py-2 ps-3 pe-10 text-start outline-none outline-offset-0 transition-colors duration-150 ease-in-out sm:text-sm focus:!outline-none hover:border-white/40" > <span className={twMerge("block truncate", !value && "opacity-50")} > {selectedOption.label} </span>- <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2.5">+ <span className="pointer-events-none absolute inset-y-0 end-0 flex items-center pe-2.5"> <ChevronDownIcon aria-hidden="true" /> </span> </ListboxButton>@@ -63,7 +63,7 @@ export const Select = ({ key={itemIdx} className={({ focus, selected }) => twMerge(- "relative cursor-pointer select-none py-2 pl-7 pr-2 transition-colors duration-150 ease-in-out",+ "relative cursor-pointer select-none py-2 ps-7 pe-2 transition-colors duration-150 ease-in-out", !selected ? "text-base-fg/90" : "text-base-fg", selected && "bg-white/10", focus && "bg-white text-black",@@ -81,7 +81,7 @@ export const Select = ({ {option.label} </span> {selected ? (- <span className="absolute inset-y-0 left-0 flex items-center pl-2.5">+ <span className="absolute inset-y-0 start-0 flex items-center ps-2.5"> <CheckIcon aria-hidden="true" className="text-xs" /> </span> ) : null}@@ -69,7 +69,7 @@ export const ArtcraftAccountBlock = ({ <p className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em]"> ArtCraft account </p>- <p className="truncate text-sm font-medium text-base-fg/80">+ <p dir="auto" className="truncate text-sm font-medium text-base-fg/80"> {artcraftSession?.display_name || "Not logged in"} </p> </div>@@ -90,7 +90,7 @@ export const AppearanceSettingsPane = () => { <button key={t.id} className={twMerge(- "group flex items-center gap-3 border p-2 text-left transition-colors duration-100",+ "group flex items-center gap-3 border p-2 text-start transition-colors duration-100", isSelected ? "border-base-fg/70 bg-base-fg/10" : "border-ui-panel-border bg-base-fg/[0.03] hover:border-base-fg/30 hover:bg-base-fg/[0.06]",@@ -103,7 +103,7 @@ export const AppearanceSettingsPane = () => { {t.label} </span> {isSelected && (- <CheckIcon aria-hidden className="mr-1 h-3.5 w-3.5" />+ <CheckIcon aria-hidden className="me-1 h-3.5 w-3.5" /> )} </button> );@@ -138,7 +138,7 @@ export const SettingsModal = ({ > <div className="h-[600px]"> <div className="grid h-full grid-cols-12 gap-3">- <div className="relative col-span-4 p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-ui-panel-border">+ <div className="relative col-span-4 p-3 pt-2 after:absolute after:end-0 after:top-0 after:h-full after:w-px after:bg-ui-panel-border"> <div className="flex items-center justify-between gap-2.5 py-0.5"> <h2 className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60"> Settings@@ -150,7 +150,7 @@ export const SettingsModal = ({ <button key={section.id} className={twMerge(- "h-9 w-full px-2 text-left transition-colors duration-100",+ "h-9 w-full px-2 text-start transition-colors duration-100", section.id === selectedSection ? "bg-base-fg/10 text-base-fg" : "text-base-fg/70 hover:bg-base-fg/[0.06] hover:text-base-fg",@@ -31,8 +31,8 @@ export function Switch({ enabled, setEnabled, id, label = "Enable notifications" className={clsx( "size-4 rounded-[2px] transition", checked- ? "translate-x-[22px] bg-black"- : "translate-x-1 bg-white/70",+ ? "translate-x-[22px] bg-black rtl:-translate-x-[22px]"+ : "translate-x-1 bg-white/70 rtl:-translate-x-1", )} /> </button>@@ -610,7 +610,7 @@ export const UploadFiles3D = ({ // chrome. Shared by the single and multi layouts. const animationPicker = (previewAnimations.length > 0 || previewHasRig) && (- <div className="pointer-events-auto absolute right-2 top-2 z-10 flex items-start gap-2">+ <div className="pointer-events-auto absolute end-2 top-2 z-10 flex items-start gap-2"> {previewHasRig && ( <button type="button"@@ -665,12 +665,12 @@ export const UploadFiles3D = ({ // or when its conversion failed (the sidebar only exists in multi mode). const currentEntry = fileEntries[previewIndex]; const convertingOverlay = isConvertingCurrent ? (- <h6 className="pointer-events-none absolute left-0 top-1/2 -mt-5 flex w-full items-center justify-center gap-2.5 text-center opacity-60">+ <h6 className="pointer-events-none absolute start-0 top-1/2 -mt-5 flex w-full items-center justify-center gap-2.5 text-center opacity-60"> <LoaderIcon className="animate-spin" /> Converting FBX to GLB... </h6> ) : currentEntry?.status === "error" && isFbx(currentEntry.file) ? (- <h6 className="pointer-events-none absolute left-0 top-1/2 -mt-5 w-full px-4 text-center text-red-400">+ <h6 className="pointer-events-none absolute start-0 top-1/2 -mt-5 w-full px-4 text-center text-red-400"> {currentEntry.errorMessage ?? "FBX conversion failed."} </h6> ) : null;@@ -706,7 +706,7 @@ export const UploadFiles3D = ({ }`} onClick={() => setPreviewIndex(i)} >- <span className="flex-1 truncate" title={entry.file.name}>+ <span dir="auto" className="flex-1 truncate" title={entry.file.name}> {entry.file.name.slice(0, entry.file.name.lastIndexOf("."))} </span> <span className="shrink-0">@@ -773,13 +773,13 @@ export const UploadFiles3D = ({ {animationPicker} {convertingOverlay} {!currentFile && (- <h6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 flex w-full items-center justify-center gap-2.5 text-center opacity-50">+ <h6 className="pointer-events-auto absolute start-0 top-1/2 -mt-5 flex w-full items-center justify-center gap-2.5 text-center opacity-50"> <BoxIcon /> Your model preview will appear here </h6> )} {previewStatus.type.includes("Error") && (- <h6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 w-full text-center">+ <h6 className="pointer-events-auto absolute start-0 top-1/2 -mt-5 w-full text-center"> {previewStatus.type} {previewStatus.message && <br />} {previewStatus.message}@@ -821,13 +821,13 @@ export const UploadFiles3D = ({ {animationPicker} {convertingOverlay} {!currentFile && (- <h6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 flex w-full items-center justify-center gap-2.5 text-center opacity-50">+ <h6 className="pointer-events-auto absolute start-0 top-1/2 -mt-5 flex w-full items-center justify-center gap-2.5 text-center opacity-50"> <BoxIcon /> Your model preview will appear here </h6> )} {previewStatus.type.includes("Error") && (- <h6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 w-full text-center">+ <h6 className="pointer-events-auto absolute start-0 top-1/2 -mt-5 w-full text-center"> {previewStatus.type} {previewStatus.message && <br />} {previewStatus.message}@@ -213,7 +213,7 @@ export const UploadFilesImage = ({ }`} onClick={() => setPreviewIndex(i)} >- <span className="truncate flex-1" title={entry.file.name}>+ <span dir="auto" className="truncate flex-1" title={entry.file.name}> {entry.file.name.slice(0, entry.file.name.lastIndexOf("."))} </span> <span className="shrink-0">@@ -342,7 +342,7 @@ export const UploadFilesSplat = ({ }`} onClick={() => setPreviewIndex(i)} >- <span className="truncate flex-1" title={entry.file.name}>+ <span dir="auto" className="truncate flex-1" title={entry.file.name}> {entry.file.name.slice(0, entry.file.name.lastIndexOf("."))} </span> <span className="shrink-0">@@ -404,13 +404,13 @@ export const UploadFilesSplat = ({ ref={canvasCallbackRef} /> {!currentFile && (- <h6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 flex w-full items-center justify-center gap-2.5 text-center opacity-50">+ <h6 className="pointer-events-auto absolute start-0 top-1/2 -mt-5 flex w-full items-center justify-center gap-2.5 text-center opacity-50"> <BoxIcon /> Your model preview will appear here </h6> )} {previewStatus.type.includes("Error") && (- <h6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 w-full text-center">+ <h6 className="pointer-events-auto absolute start-0 top-1/2 -mt-5 w-full text-center"> {previewStatus.type} {previewStatus.message && <br />} {previewStatus.message}@@ -450,13 +450,13 @@ export const UploadFilesSplat = ({ ref={canvasCallbackRef} /> {!currentFile && (- <h6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 flex w-full items-center justify-center gap-2.5 text-center opacity-50">+ <h6 className="pointer-events-auto absolute start-0 top-1/2 -mt-5 flex w-full items-center justify-center gap-2.5 text-center opacity-50"> <BoxIcon /> Your model preview will appear here </h6> )} {previewStatus.type.includes("Error") && (- <h6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 w-full text-center">+ <h6 className="pointer-events-auto absolute start-0 top-1/2 -mt-5 w-full text-center"> {previewStatus.type} {previewStatus.message && <br />} {previewStatus.message}@@ -270,6 +270,7 @@ export const PromptBoxVFX = ({ </button> </div> <textarea+ dir="auto" ref={promptTextareaRef} value={prompt} onChange={(e) => setPrompt(e.target.value)}@@ -400,7 +401,7 @@ export const PromptBoxVFX = ({ </button> </Tooltip> - <div className="ml-auto flex items-center gap-2">+ <div className="ms-auto flex items-center gap-2"> <PromptClearAllButton onClick={handleClearAll} disabled={!hasClearableContent}@@ -551,7 +552,7 @@ const UploadTile = ({ <button type="button" onClick={onClear}- className="absolute right-1 top-1 flex h-5 w-5 items-center justify-center rounded-full bg-black/60 text-white opacity-0 transition-opacity hover:bg-red-500/70 group-hover:opacity-100"+ className="absolute end-1 top-1 flex h-5 w-5 items-center justify-center rounded-full bg-black/60 text-white opacity-0 transition-opacity hover:bg-red-500/70 group-hover:opacity-100" > <XIcon className="h-2.5 w-2.5" /> </button>@@ -77,7 +77,7 @@ export const VFXResultCard = ({ </div> ) : null} {data.title && data.status === "complete" && (- <div className="absolute left-3 top-3 rounded-md bg-black/60 px-2 py-1 text-xs font-medium uppercase tracking-wider text-white/80 backdrop-blur-sm">+ <div className="absolute start-3 top-3 rounded-md bg-black/60 px-2 py-1 text-xs font-medium uppercase tracking-wider text-white/80 backdrop-blur-sm"> Output </div> )}@@ -180,7 +180,7 @@ const SidePanelMedia = ({ label, url, isVideo }: SidePanelMediaProps) => ( ) : ( <img src={url} alt={label} className="h-full w-full object-cover" /> )}- <span className="absolute left-2 top-2 rounded bg-black/60 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-white/80 backdrop-blur-sm">+ <span className="absolute start-2 top-2 rounded bg-black/60 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-white/80 backdrop-blur-sm"> {label} </span> </div>@@ -243,7 +243,7 @@ function ExportPopover({ }; return (- <PopoverContent className="bg-ui-controls mr-4 flex w-80 flex-col p-0">+ <PopoverContent className="bg-ui-controls me-4 flex w-80 flex-col p-0"> {exportResult && !exportResult.success ? ( <ExportError error={exportResult.error || "Unknown error occurred"}@@ -278,13 +278,13 @@ function ExportPopover({ } }} >- <div className="flex items-center space-x-2">+ <div className="flex items-center space-x-2 rtl:space-x-reverse"> <RadioGroupItem value="mp4" id="mp4" /> <Label htmlFor="mp4"> MP4 (H.264) - Better compatibility </Label> </div>- <div className="flex items-center space-x-2">+ <div className="flex items-center space-x-2 rtl:space-x-reverse"> <RadioGroupItem value="webm" id="webm" /> <Label htmlFor="webm"> WebM (VP9) - Smaller file size@@ -307,19 +307,19 @@ function ExportPopover({ } }} >- <div className="flex items-center space-x-2">+ <div className="flex items-center space-x-2 rtl:space-x-reverse"> <RadioGroupItem value="low" id="low" /> <Label htmlFor="low">Low - Smallest file size</Label> </div>- <div className="flex items-center space-x-2">+ <div className="flex items-center space-x-2 rtl:space-x-reverse"> <RadioGroupItem value="medium" id="medium" /> <Label htmlFor="medium">Medium - Balanced</Label> </div>- <div className="flex items-center space-x-2">+ <div className="flex items-center space-x-2 rtl:space-x-reverse"> <RadioGroupItem value="high" id="high" /> <Label htmlFor="high">High - Recommended</Label> </div>- <div className="flex items-center space-x-2">+ <div className="flex items-center space-x-2 rtl:space-x-reverse"> <RadioGroupItem value="very_high" id="very_high" /> <Label htmlFor="very_high"> Very high - Largest file size@@ -334,7 +334,7 @@ function ExportPopover({ <SectionTitle>Audio</SectionTitle> </SectionHeader> <SectionContent>- <div className="flex items-center space-x-2">+ <div className="flex items-center space-x-2 rtl:space-x-reverse"> <Checkbox id="include-audio" checked={shouldIncludeAudio}@@ -355,7 +355,7 @@ function ExportPopover({ </SectionHeader> <SectionContent> <div className="flex flex-col gap-2">- <div className="flex items-center space-x-2">+ <div className="flex items-center space-x-2 rtl:space-x-reverse"> <Checkbox id="save-to-disk" checked={saveToDisk}@@ -367,7 +367,7 @@ function ExportPopover({ Save to disk </Label> </div>- <div className="flex items-center space-x-2">+ <div className="flex items-center space-x-2 rtl:space-x-reverse"> <Checkbox id="save-to-library" checked={saveToLibrary}@@ -515,7 +515,7 @@ function DestinationRow({ </span> </div> {status === "error" && error && (- <p className="text-destructive ml-6 text-[11px]">{error}</p>+ <p className="text-destructive ms-6 text-[11px]">{error}</p> )} </div> );@@ -38,7 +38,7 @@ export function MobileGate({ children }: MobileGateProps) { <div className="bg-background relative flex h-full w-full flex-col overflow-hidden"> <Button variant="text"- className="absolute top-6 left-6 flex items-center gap-1 text-muted-foreground"+ className="absolute top-6 start-6 flex items-center gap-1 text-muted-foreground" onClick={handleGoBack} > <HugeiconsIcon icon={ArrowLeft01Icon} className="size-4" />@@ -156,7 +156,7 @@ export function ScenesView({ children }: { children: React.ReactNode }) { )} onClick={() => handleSceneSwitch(scene.id)} >- <span>{scene.name}</span>+ <span dir="auto">{scene.name}</span> <div className="flex items-center gap-2"> {((isSelectMode && selectedScenes.has(scene.id)) || (!isSelectMode && currentScene?.id === scene.id)) && (@@ -135,7 +135,7 @@ export function SectionHeader({ const innerContent = isInteractive ? ( <button type="button"- className="min-w-0 flex-1 flex items-center gap-2 h-full cursor-pointer text-left"+ className="min-w-0 flex-1 flex items-center gap-2 h-full cursor-pointer text-start" onClick={handleClick} > {leading}@@ -5,7 +5,7 @@ import { HugeiconsIcon } from "@hugeicons/react"; import { Alert02Icon } from "@hugeicons/core-free-icons"; const alertVariants = cva(- "relative w-full rounded-lg border p-5 py-4.5 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-destructive [&>svg~*]:pl-7",+ "relative w-full rounded-lg border p-5 py-4.5 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:start-4 [&>svg]:top-4 [&>svg]:text-destructive [&>svg~*]:ps-7", { variants: { variant: {@@ -68,7 +68,7 @@ const ContextMenuSubTrigger = React.forwardRef< className={cn( contextMenuItemVariants({ variant }), "data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",- inset && "pl-8",+ inset && "ps-8", className, )} {...props}@@ -79,7 +79,7 @@ const ContextMenuSubTrigger = React.forwardRef< {children} <HugeiconsIcon icon={ArrowRightIcon}- className="ml-auto text-muted-foreground/80"+ className="ms-auto text-muted-foreground/80" /> </ContextMenuPrimitive.SubTrigger> ),@@ -148,19 +148,19 @@ const ContextMenuItem = React.forwardRef< ref={ref} className={cn( contextMenuItemVariants({ variant }),- shouldInsetContent && "pl-8",+ shouldInsetContent && "ps-8", className, )} {...props} > {icon && (- <span className="absolute left-3 flex size-3.5 items-center justify-center text-muted-foreground [&_svg]:size-3.5 [&_svg]:shrink-0">+ <span className="absolute start-3 flex size-3.5 items-center justify-center text-muted-foreground [&_svg]:size-3.5 [&_svg]:shrink-0"> {icon} </span> )} {children} {textRight && (- <span className="ml-auto text-[0.60rem] tracking-widest text-muted-foreground/80 mb-0.5">+ <span className="ms-auto text-[0.60rem] tracking-widest text-muted-foreground/80 mb-0.5"> {textRight} </span> )}@@ -185,13 +185,13 @@ const ContextMenuCheckboxItem = React.forwardRef< ref={ref} className={cn( contextMenuItemVariants({ variant }),- "pr-2 pl-8",+ "pe-2 ps-8", className, )} checked={checked} {...props} >- <span className="absolute left-3 flex size-3.5 items-center justify-center">+ <span className="absolute start-3 flex size-3.5 items-center justify-center"> <ContextMenuPrimitive.ItemIndicator> <HugeiconsIcon icon={Tick02Icon} className="size-4" /> </ContextMenuPrimitive.ItemIndicator>@@ -215,10 +215,10 @@ const ContextMenuRadioItem = React.forwardRef< >(({ className, children, variant = "default", icon, ...props }, ref) => ( <ContextMenuPrimitive.RadioItem ref={ref}- className={cn(contextMenuItemVariants({ variant }), "pr-2 pl-8", className)}+ className={cn(contextMenuItemVariants({ variant }), "pe-2 ps-8", className)} {...props} >- <span className="absolute left-2 flex size-3.5 items-center justify-center">+ <span className="absolute start-2 flex size-3.5 items-center justify-center"> <ContextMenuPrimitive.ItemIndicator> <HugeiconsIcon icon={CircleIcon} className="size-2 fill-current" /> </ContextMenuPrimitive.ItemIndicator>@@ -242,7 +242,7 @@ const ContextMenuLabel = React.forwardRef< ref={ref} className={cn( "flex items-center gap-2 px-3 py-1.5 text-sm font-semibold text-foreground",- inset && "pl-8",+ inset && "ps-8", className, )} {...props}@@ -274,7 +274,7 @@ const ContextMenuShortcut = ({ return ( <span className={cn(- "ml-auto text-xs tracking-widest text-muted-foreground opacity-60",+ "ms-auto text-xs tracking-widest text-muted-foreground opacity-60", className, )} {...props}@@ -64,7 +64,7 @@ const DialogContent = React.forwardRef< {...props} > {children}- <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-6 right-6 cursor-pointer opacity-70 hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">+ <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-6 end-6 cursor-pointer opacity-70 hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> <X className="size-5 text-muted-foreground" /> <span className="sr-only">Close</span> </DialogPrimitive.Close>@@ -79,7 +79,7 @@ const DialogHeader = ({ }: React.HTMLAttributes<HTMLDivElement>) => ( <div className={cn(- "flex flex-col space-y-2 text-left border-b border-ui-panel-border p-6",+ "flex flex-col space-y-2 text-start border-b border-ui-panel-border p-6", className, )} {...props}@@ -63,13 +63,13 @@ const DropdownMenuSubTrigger = React.forwardRef< className={cn( dropdownMenuItemVariants({ variant }), "data-[state=open]:bg-muted data-[state=open]:text-foreground",- inset && "pl-8",+ inset && "ps-8", className, )} {...props} > {children}- <ChevronRight className="ml-auto" />+ <ChevronRight className="ms-auto" /> </DropdownMenuPrimitive.SubTrigger> )); DropdownMenuSubTrigger.displayName =@@ -162,7 +162,7 @@ const DropdownMenuItem = React.forwardRef< data-has-icon={icon ? "" : undefined} className={cn( dropdownMenuItemVariants({ variant }),- inset && "pl-8",+ inset && "ps-8", className, )} {...props}@@ -184,7 +184,7 @@ const DropdownMenuCheckboxItem = React.forwardRef< ref={ref} className={cn( dropdownMenuItemVariants({ variant }),- "pr-8 pl-2",+ "pe-8 ps-2", className, )} checked={checked}@@ -194,7 +194,7 @@ const DropdownMenuCheckboxItem = React.forwardRef< {...props} > {children}- <span className="absolute right-2 flex size-3.5 items-center justify-center">+ <span className="absolute end-2 flex size-3.5 items-center justify-center"> <DropdownMenuPrimitive.ItemIndicator> <Check className="size-4" /> </DropdownMenuPrimitive.ItemIndicator>@@ -215,12 +215,12 @@ const DropdownMenuRadioItem = React.forwardRef< ref={ref} className={cn( dropdownMenuItemVariants({ variant }),- "pr-2 pl-8",+ "pe-2 ps-8", className, )} {...props} >- <span className="absolute left-2 flex size-3.5 items-center justify-center">+ <span className="absolute start-2 flex size-3.5 items-center justify-center"> <DropdownMenuPrimitive.ItemIndicator> <Circle className="size-2 fill-current" /> </DropdownMenuPrimitive.ItemIndicator>@@ -240,7 +240,7 @@ const DropdownMenuLabel = React.forwardRef< ref={ref} className={cn( "px-2 pb-1 pt-0.5 text-[11px] font-bold uppercase tracking-wider text-muted-foreground",- inset && "pl-8",+ inset && "ps-8", className, )} {...props}@@ -266,7 +266,7 @@ const DropdownMenuShortcut = ({ }: React.HTMLAttributes<HTMLSpanElement>) => { return ( <span- className={cn("ml-auto text-xs tracking-widest opacity-60", className)}+ className={cn("ms-auto text-xs tracking-widest opacity-60", className)} {...props} /> );@@ -117,14 +117,14 @@ export function FontPicker({ }} > <div className="relative px-3 py-1.5">- <Search className="absolute left-3 top-1/2 -translate-y-1/2 size-3.5 shrink-0 opacity-50" />+ <Search className="absolute start-3 top-1/2 -translate-y-1/2 size-3.5 shrink-0 opacity-50" /> <Input ref={searchInputRef} placeholder={`Search ${activeTabLabel}...`} value={search} onChange={(event) => setSearch(event.target.value)} size="xs"- className="w-full pl-5 bg-transparent border-none! shadow-none!"+ className="w-full ps-5 bg-transparent border-none! shadow-none!" /> </div> <div className="flex border-b px-3">@@ -75,7 +75,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>( const hasIcons = showPasswordToggle || showClear; const iconCount = Number(showPasswordToggle) + Number(showClear); const paddingRight =- iconCount === 2 ? "pr-20" : iconCount === 1 ? "pr-10" : "";+ iconCount === 2 ? "pe-20" : iconCount === 1 ? "pe-10" : ""; return ( <div@@ -110,7 +110,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>( e.preventDefault(); onClear?.(); }}- className="text-muted-foreground absolute top-0 right-0 h-full px-3 !opacity-100"+ className="text-muted-foreground absolute top-0 end-0 h-full px-3 !opacity-100" aria-label="Clear input" > <X className="!size-[0.85]" />@@ -123,7 +123,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>( onClick={() => onShowPasswordChange?.(!showPassword)} className={cn( "text-muted-foreground hover:text-foreground absolute top-0 h-full px-3",- showClear ? "right-10" : "right-0",+ showClear ? "end-10" : "end-0", )} aria-label={showPassword ? "Hide password" : "Show password"} >@@ -28,7 +28,7 @@ const ResizableHandle = ({ }) => ( <ResizablePrimitive.PanelResizeHandle className={cn(- "focus-visible:ring-ring relative flex w-px items-center justify-center bg-transparent after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",+ "focus-visible:ring-ring relative flex w-px items-center justify-center bg-transparent after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:start-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90", className, )} {...props}@@ -185,10 +185,10 @@ const SelectItem = React.forwardRef< >(({ className, children, variant = "default", ...props }, ref) => ( <SelectPrimitive.Item ref={ref}- className={cn(selectItemVariants({ variant }), "pl-6 pr-2", className)}+ className={cn(selectItemVariants({ variant }), "ps-6 pe-2", className)} {...props} >- <span className="absolute left-1.5 flex size-3.5 items-center justify-center">+ <span className="absolute start-1.5 flex size-3.5 items-center justify-center"> <SelectPrimitive.ItemIndicator> <Check className="size-3.5" /> </SelectPrimitive.ItemIndicator>@@ -85,7 +85,7 @@ const SheetContent = React.forwardRef< }} {...props} >- <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 cursor-pointer rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">+ <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 end-4 cursor-pointer rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> <X className="size-5" /> <span className="sr-only">Close</span> </SheetPrimitive.Close>@@ -101,7 +101,7 @@ const SheetHeader = ({ }: React.HTMLAttributes<HTMLDivElement>) => ( <div className={cn(- "flex flex-col space-y-2 text-center sm:text-left",+ "flex flex-col space-y-2 text-center sm:text-start", className, )} {...props}@@ -115,7 +115,7 @@ const SheetFooter = ({ }: React.HTMLAttributes<HTMLDivElement>) => ( <div className={cn(- "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4",+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4 sm:rtl:space-x-reverse", className, )} {...props}@@ -61,14 +61,14 @@ SplitButtonSide.displayName = "SplitButtonSide"; const SplitButtonLeft = forwardRef<HTMLButtonElement, SplitButtonSideProps>( ({ ...props }, ref) => {- return <SplitButtonSide ref={ref} paddingClass="pl-3 pr-2" {...props} />;+ return <SplitButtonSide ref={ref} paddingClass="ps-3 pe-2" {...props} />; }, ); SplitButtonLeft.displayName = "SplitButtonLeft"; const SplitButtonRight = forwardRef<HTMLButtonElement, SplitButtonSideProps>( ({ ...props }, ref) => {- return <SplitButtonSide ref={ref} paddingClass="pl-2 pr-3" {...props} />;+ return <SplitButtonSide ref={ref} paddingClass="ps-2 pe-3" {...props} />; }, ); SplitButtonRight.displayName = "SplitButtonRight";@@ -19,7 +19,7 @@ const Switch = React.forwardRef< > <SwitchPrimitives.Thumb className={cn(- "bg-background pointer-events-none block size-4 rounded-full shadow-lg ring-0 data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",+ "bg-background pointer-events-none block size-4 rounded-full shadow-lg ring-0 data-[state=checked]:translate-x-4 rtl:data-[state=checked]:-translate-x-4 data-[state=unchecked]:translate-x-0", )} /> </SwitchPrimitives.Root>@@ -8,6 +8,7 @@ const Textarea = React.forwardRef< >(({ className, ...props }, ref) => { return ( <textarea+ dir="auto" className={cn( "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input bg-accent flex min-h-[60px] w-full rounded-md border px-3 py-2 resize-none text-base shadow-xs outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "focus-visible:border-primary focus-visible:ring-0 focus-visible:ring-primary/10",@@ -126,7 +126,8 @@ export function DraggableItem({ </AspectRatio> {shouldShowLabel && ( <span- className="text-muted-foreground w-full truncate text-left text-[0.7rem]"+ dir="auto"+ className="text-muted-foreground w-full truncate text-start text-[0.7rem]" title={name} > <span className="sr-only">{name}</span>@@ -158,7 +159,10 @@ export function DraggableItem({ <div className="size-6 shrink-0 overflow-hidden rounded-sm"> {preview} </div>- <span className="w-full flex-1 truncate text-sm text-left">+ <span+ dir="auto"+ className="w-full flex-1 truncate text-sm text-start"+ > {name} </span> </button>@@ -212,7 +216,7 @@ function PlusButton({ <Button size="icon" className={cn(- "bg-background hover:bg-background text-foreground absolute right-2 bottom-2 size-5",+ "bg-background hover:bg-background text-foreground absolute end-2 bottom-2 size-5", className, )} onClick={(e) => {@@ -97,7 +97,7 @@ function FadeOverlay({ return ( <div className={cn(- "pointer-events-none absolute right-0 left-0 h-6",+ "pointer-events-none absolute end-0 start-0 h-6", direction === "top" && show ? "from-background top-0 bg-linear-to-b to-transparent" : "from-background bottom-0 bg-linear-to-t to-transparent",@@ -498,7 +498,7 @@ function MediaDurationBadge({ duration }: { duration?: number }) { if (!duration) return null; return (- <div className="absolute right-1 bottom-1 rounded bg-black/70 px-1 text-xs text-white">+ <div className="absolute end-1 bottom-1 rounded bg-black/70 px-1 text-xs text-white"> {formatDuration({ duration })} </div> );@@ -32,7 +32,7 @@ export function PanelView({ {...rest} > {!hideHeader && (- <div className="bg-background h-11 shrink-0 pl-3 pr-2 flex items-center justify-between border-b border-ui-panel-border">+ <div className="bg-background h-11 shrink-0 ps-3 pe-2 flex items-center justify-between border-b border-ui-panel-border"> {title && ( <span className="text-muted-foreground text-sm">{title}</span> )}@@ -73,7 +73,7 @@ const BlurPreview = memo( ref={canvasRef} className="absolute inset-0 h-full w-full object-cover" />- <div className="absolute right-1 bottom-1 left-1 text-center">+ <div className="absolute end-1 bottom-1 start-1 text-center"> <span className="rounded bg-black/50 px-1 text-xs text-white"> {blur.label} </span>@@ -165,7 +165,7 @@ function CustomColorPreview({ style={{ background: CUSTOM_COLOR_SWATCH_BACKGROUND }} /> <span- className="absolute right-1 bottom-1 size-5 rounded-sm border border-white/70 shadow-sm"+ className="absolute end-1 bottom-1 size-5 rounded-sm border border-white/70 shadow-sm" style={{ backgroundColor: currentBackgroundColor }} /> </button>@@ -234,7 +234,7 @@ export function SettingsView() { <Section showTopBorder={false}> <SectionHeader> <SectionTitle className="flex-1">Name</SectionTitle>- <span className="text-sm truncate">+ <span dir="auto" className="text-sm truncate"> {activeProject.metadata.name} </span> </SectionHeader>@@ -64,7 +64,7 @@ export function PropertiesPanel() { return ( <div className="panel bg-background flex h-full overflow-hidden rounded-lg border border-ui-panel-border"> <TooltipProvider delayDuration={0}>- <div className="flex shrink-0 flex-col gap-0.5 border-r border-ui-panel-border p-1 scrollbar-hidden overflow-y-auto">+ <div className="flex shrink-0 flex-col gap-0.5 border-e border-ui-panel-border p-1 scrollbar-hidden overflow-y-auto"> {visibleTabs.map((tab) => ( <Tooltip key={tab.id}> <TooltipTrigger asChild>@@ -39,7 +39,10 @@ export function DeleteProjectDialog({ {singleName ? ( <> {"Delete '"}- <span className="inline-block max-w-[300px] truncate align-bottom">+ <span+ dir="auto"+ className="inline-block max-w-[300px] truncate align-bottom"+ > {singleName} </span> {"'?"}@@ -54,8 +57,14 @@ export function DeleteProjectDialog({ <AlertTitle>Warning</AlertTitle> <AlertDescription> This will permanently delete{" "}- {singleName ? `"${singleName}"` : `${count} projects`} and all- associated files.+ {singleName ? (+ <>+ "<bdi>{singleName}</bdi>"+ </>+ ) : (+ `${count} projects`+ )}{" "}+ and all associated files. </AlertDescription> </Alert> <div className="flex flex-col gap-3">@@ -510,7 +510,7 @@ function AudioItem({ sound, isPlaying, onPlay }: AudioItemProps) { <div className="group flex items-center gap-3 opacity-100 hover:opacity-75"> <button type="button"- className="flex min-w-0 flex-1 items-center gap-3 text-left"+ className="flex min-w-0 flex-1 items-center gap-3 text-start" onClick={handleClick} > <div className="bg-accent relative flex size-12 shrink-0 items-center justify-center overflow-hidden rounded-md">@@ -523,14 +523,19 @@ function AudioItem({ sound, isPlaying, onPlay }: AudioItemProps) { </div> <div className="min-w-0 flex-1 overflow-hidden">- <p className="truncate text-sm font-medium">{sound.name}</p>- <span className="text-muted-foreground block truncate text-xs">+ <p dir="auto" className="truncate text-sm font-medium">+ {sound.name}+ </p>+ <span+ dir="auto"+ className="text-muted-foreground block truncate text-xs"+ > {sound.username} </span> </div> </button> - <div className="flex items-center gap-3 pr-2">+ <div className="flex items-center gap-3 pe-2"> <Button variant="text" size="icon"@@ -276,7 +276,7 @@ function StickerSection({ <div /> )} - <div className="ml-auto flex items-center gap-2">+ <div className="ms-auto flex items-center gap-2"> {section.id === "recent" && ( <Button onClick={onClearRecent}@@ -1042,7 +1042,7 @@ function AudioElementContent({ return ( <div className="group/audio relative size-full"> <div className="flex size-full items-center pl-2">- <span className="text-foreground/80 truncate text-xs">+ <span dir="auto" className="text-foreground/80 truncate text-xs"> {element.name} </span> </div>@@ -1098,7 +1098,7 @@ function TiledMediaContent({ if (!imageUrl) { return (- <span className="text-foreground/80 truncate text-xs">+ <span dir="auto" className="text-foreground/80 truncate text-xs"> {element.name} </span> );@@ -1155,7 +1155,7 @@ function MediaElementHeader({ > {leading && <div className="pl-1">{leading}</div>} {name && (- <span className="truncate px-1.5 text-[0.6rem] leading-tight text-white/75">+ <span dir="auto" className="truncate px-1.5 text-[0.6rem] leading-tight text-white/75"> {name} </span> )}@@ -560,7 +560,7 @@ export function Viewer3D({ return ( <div- className={`relative h-full w-full overflow-hidden rounded-l-xl bg-[#282828] ${className}`}+ className={`relative h-full w-full overflow-hidden rounded-s-xl bg-[#282828] ${className}`} onMouseDown={stopPropagation} onMouseMove={stopPropagation} onMouseUp={stopPropagation}@@ -580,7 +580,7 @@ export function Viewer3D({ the animation picker (models that ship clips). Models with neither get no extra chrome. */} {showViewer && (hasRig || animationNames.length > 0) && (- <div className="absolute right-2.5 top-2.5 z-20 flex items-start gap-2">+ <div className="absolute end-2.5 top-2.5 z-20 flex items-start gap-2"> {hasRig && ( <button type="button"@@ -58,7 +58,7 @@ export const NewsPost = ({ slug: propSlug, basePath }: NewsPostProps) => { </h1> {post.description && (- <p className="text-xl text-white/70 mb-8 leading-relaxed font-light border-l-4 border-primary pl-4">+ <p className="text-xl text-white/70 mb-8 leading-relaxed font-light border-s-4 border-primary ps-4"> {post.description} </p> )}