Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,150 changes: 1,917 additions & 233 deletions docs/redesign/mockups/Settings.dc.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "QueryCraft",
"version": "1.0.10",
"version": "1.0.11",
"description": "Inspect, edit, save, and replay URL query parameters on the active tab.",
"action": {
"default_popup": "src/popup/index.html",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "querycraft",
"version": "1.0.10",
"version": "1.0.11",
"description": "Inspect, edit, save, and replay URL query parameters on the active tab.",
"private": true,
"type": "module",
Expand Down
6 changes: 4 additions & 2 deletions public/theme-boot.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* Applies the saved theme and accent before first paint (docs/redesign §4.5).
* Applies the saved theme, accent and layout direction before first paint (docs/redesign §4.5).
* chrome.storage is async, so the popup would flash the wrong scheme; this
* reads the synchronous localStorage mirror that useTheme / useAccent keep.
* reads the synchronous localStorage mirror that useTheme / useAccent / useDirection keep.
* Loaded as a file, not inline: the MV3 content security policy blocks inline scripts.
*/
try {
Expand All @@ -15,6 +15,8 @@ try {
root.setAttribute('data-theme', scheme);
root.style.colorScheme = scheme;

if (localStorage.getItem('qc-dir') === 'rtl') root.dir = 'rtl';

var accent = JSON.parse(localStorage.getItem('qc-accent') || 'null');
if (accent) {
root.style.setProperty('--primary', accent.primary);
Expand Down
36 changes: 36 additions & 0 deletions src/__tests__/useDirection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, it, expect, afterEach } from 'vitest';
import { act, renderHook, waitFor } from '@testing-library/react';
import { useDirection, DIRECTION_MIRROR_KEY } from '@/hooks/useDirection';

afterEach(() => {
document.documentElement.removeAttribute('dir');
localStorage.clear();
});

describe('useDirection', () => {
it('is left-to-right by default', () => {
const { result } = renderHook(() => useDirection());

expect(result.current.direction).toBe('ltr');
expect(document.documentElement.dir).toBe('ltr');
});

it('applies the setting to <html>, persists it and mirrors it for the boot script', async () => {
const { result } = renderHook(() => useDirection());

act(() => result.current.setDirection('rtl'));

expect(document.documentElement.dir).toBe('rtl');
expect(localStorage.getItem(DIRECTION_MIRROR_KEY)).toBe('rtl');
await waitFor(() =>
expect(chrome.storage.local.set).toHaveBeenCalledWith({ 'qc.direction': 'rtl' }),
);
});

it('starts from the mirror, so the first render already has the right direction', () => {
localStorage.setItem(DIRECTION_MIRROR_KEY, 'rtl');
const { result } = renderHook(() => useDirection());

expect(result.current.direction).toBe('rtl');
});
});
2 changes: 1 addition & 1 deletion src/components/SavedLinksDrawer/SaveForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ function ReadOnlyUrl({
data-overflowing={overflowing || undefined}
className="max-h-20 overflow-y-auto data-overflowing:mask-b-from-[calc(100%-1.5rem)]"
>
<div className="relative">
<div className="relative" dir="ltr">
<div
aria-hidden="true"
className="whitespace-pre-wrap break-all px-[11px] font-mono text-code text-muted-foreground"
Expand Down
28 changes: 22 additions & 6 deletions src/components/SettingsDrawer/SettingsDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useRef, type CSSProperties } from 'react';
import { CheckIcon, MonitorIcon, MoonIcon, SunIcon, XIcon } from 'lucide-react';
import type { AccentColor, ThemePreference } from '@/types';
import type { AccentColor, TextDirection, ThemePreference } from '@/types';
import type { ResolvedTheme } from '@/hooks/useTheme';
import { ACCENTS } from '@/lib/accents';
import { readableOn } from '@/lib/contrast';
Expand All @@ -13,7 +13,7 @@ import { cn } from '@/lib/utils';
import { useReportSheetHeight } from '@/popup/popupHeight';
import { announce } from '@/components/LiveRegion';
import { Button } from '@/components/ui/button';
import { Field, FieldDescription } from '@/components/ui/field';
import { Field, FieldDescription, FieldLabel } from '@/components/ui/field';
import { Frame, FramePanel } from '@/components/ui/frame';
import { Kbd } from '@/components/ui/kbd';
import { RadioGroupPrimitive, RadioPrimitive } from '@/components/ui/radio-group';
Expand All @@ -37,6 +37,8 @@ interface SettingsDrawerProps {
/** The scheme in use: the OS's while the preference is 'system'. */
resolved: ResolvedTheme;
onPreferenceChange: (preference: ThemePreference) => void;
direction: TextDirection;
onDirectionChange: (direction: TextDirection) => void;
}

const SHEET_ID = 'settings';
Expand All @@ -63,6 +65,8 @@ export function SettingsDrawer({
preference,
resolved,
onPreferenceChange,
direction,
onDirectionChange,
}: SettingsDrawerProps) {
const checkedSwatchRef = useRef<HTMLButtonElement>(null);
useReportSheetHeight(open, SHEET_ID);
Expand All @@ -84,6 +88,11 @@ export function SettingsDrawer({
announce(`Theme: ${theme.label}`);
}

function handleDirectionChange(rtl: boolean) {
onDirectionChange(rtl ? 'rtl' : 'ltr');
announce(`Right-to-left layout: ${rtl ? 'on' : 'off'}`);
}

return (
<Sheet open={open} onOpenChange={(next) => !next && onClose()}>
<SheetPopup
Expand Down Expand Up @@ -142,10 +151,7 @@ export function SettingsDrawer({
);
})}
</RadioGroupPrimitive>
<FieldDescription>
Tints the primary button, switches and URL keys. Text on the accent is picked to keep
4.5:1.
</FieldDescription>
<FieldDescription>Tints the primary button, switches and URL keys.</FieldDescription>
</Field>

<section className="flex flex-col gap-2">
Expand Down Expand Up @@ -177,6 +183,16 @@ export function SettingsDrawer({

<Separator />

<Field>
<FieldLabel>
<Switch checked={direction === 'rtl'} onCheckedChange={handleDirectionChange} />
Right-to-left layout
</FieldLabel>
<FieldDescription>
Mirrors QueryCraft for right-to-left languages like Arabic and Hebrew.
</FieldDescription>
</Field>

<Field className="items-stretch">
<p className="font-medium text-sm/4">Theme</p>
<RadioGroupPrimitive
Expand Down
2 changes: 1 addition & 1 deletion src/components/UrlPreview/UrlPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function UrlPreview({ parsed, onUrlChange }: UrlPreviewProps) {

return (
<section aria-label="URL editor" className={styles.root}>
<div className={styles.editorWrapper}>
<div className={styles.editorWrapper} dir="ltr">
<div className={styles.highlight} aria-hidden="true">
{renderHighlighted(localValue)}
</div>
Expand Down
57 changes: 57 additions & 0 deletions src/hooks/useDirection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useCallback, useEffect, useState } from 'react';
import type { TextDirection } from '@/types';
import { storage } from '@/lib/storage';

/** Synchronous mirror read by public/theme-boot.js before first paint (§4.5). */
export const DIRECTION_MIRROR_KEY = 'qc-dir';

function readMirror(): TextDirection {
try {
return localStorage.getItem(DIRECTION_MIRROR_KEY) === 'rtl' ? 'rtl' : 'ltr';
} catch {
return 'ltr';
}
}

function writeMirror(direction: TextDirection): void {
try {
localStorage.setItem(DIRECTION_MIRROR_KEY, direction);
} catch {
// Storage blocked: the boot script falls back to LTR, nothing else breaks.
}
}

/** Layout direction setting, applied to <html> as `dir`. */
export function useDirection(): {
direction: TextDirection;
setDirection: (next: TextDirection) => void;
} {
const [direction, setDirectionState] = useState<TextDirection>(readMirror);

useEffect(() => {
let cancelled = false;
storage.getDirection().then(
(stored) => {
if (cancelled) return;
setDirectionState(stored);
writeMirror(stored);
},
() => undefined,
);
return () => {
cancelled = true;
};
}, []);

useEffect(() => {
document.documentElement.dir = direction;
}, [direction]);

const setDirection = useCallback((next: TextDirection) => {
setDirectionState(next);
writeMirror(next);
void storage.setDirection(next);
}, []);

return { direction, setDirection };
}
12 changes: 11 additions & 1 deletion src/lib/storage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SavedLink, Group, AccentColor, ThemePreference } from '@/types';
import type { SavedLink, Group, AccentColor, ThemePreference, TextDirection } from '@/types';

/**
* Adapter over chrome.storage.local.
Expand All @@ -13,6 +13,7 @@ const KEYS = {
groups: 'qc.groups',
accent: 'qc.accent',
themePreference: 'themePreference',
direction: 'qc.direction',
} as const;

const THEME_PREFERENCES: readonly ThemePreference[] = ['system', 'light', 'dark'];
Expand Down Expand Up @@ -74,4 +75,13 @@ export const storage = {
async setThemePreference(preference: ThemePreference): Promise<void> {
await set(KEYS.themePreference, preference);
},

/** 'ltr' unless 'rtl' was stored. */
async getDirection(): Promise<TextDirection> {
return (await get<unknown>(KEYS.direction, 'ltr')) === 'rtl' ? 'rtl' : 'ltr';
},

async setDirection(direction: TextDirection): Promise<void> {
await set(KEYS.direction, direction);
},
};
Loading
Loading