diff --git a/.gitignore b/.gitignore
index 9408328..e711413 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,8 @@ app-example
# generated native folders
/ios
/android
+
+# IDE specific files
+.vscode/
+.idea/
+*.swp
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
deleted file mode 100644
index b7ed837..0000000
--- a/.vscode/extensions.json
+++ /dev/null
@@ -1 +0,0 @@
-{ "recommendations": ["expo.vscode-expo-tools"] }
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index ae7681a..0000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "editor.codeActionsOnSave": {
- "source.fixAll": "explicit",
- "source.organizeImports": "explicit",
- "source.sortMembers": "explicit"
- },
- "typescript.tsdk": "node_modules/typescript/lib",
- "cSpell.words": [
- "persistor",
- "scriptian"
- ]
-}
diff --git a/src/__tests__/utils/testUtils.tsx b/__tests__/utils/testUtils.tsx
similarity index 88%
rename from src/__tests__/utils/testUtils.tsx
rename to __tests__/utils/testUtils.tsx
index 70c34b0..7c13558 100644
--- a/src/__tests__/utils/testUtils.tsx
+++ b/__tests__/utils/testUtils.tsx
@@ -6,17 +6,17 @@ import {
import { ReactNode } from "react";
import { Provider } from "react-redux";
-import browserReducer from "../../features/browser/browserSlice";
-import { BrowserState, BrowserTab } from "../../features/browser/types";
-import scriptsReducer from "../../features/scripts/scriptsSlice";
+import browserReducer from "@/src/features/browser/browserSlice";
+import { BrowserState, BrowserTab } from "@/src/features/browser/types";
+import scriptsReducer from "@/src/features/scripts/scriptsSlice";
import {
ScriptExecution,
ScriptsState,
UserScript,
-} from "../../features/scripts/types";
-import settingsReducer from "../../features/settings/settingsSlice";
-import { SettingsState } from "../../features/settings/types";
-import { RootState } from "../../store";
+} from "@/src/features/scripts/types";
+import settingsReducer from "@/src/features/settings/settingsSlice";
+import { SettingsState } from "@/src/features/settings/types";
+import { RootState } from "@/src/store";
// Default states for each slice
export const defaultBrowserState: BrowserState = {
diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
index 58a5ff5..f8105be 100644
--- a/src/app/_layout.tsx
+++ b/src/app/_layout.tsx
@@ -1,3 +1,5 @@
+import { ThemeProvider } from "@/src/theme/ThemeContext";
+import { useTheme } from "@/src/theme/useTheme";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import React from "react";
@@ -17,16 +19,29 @@ const LoadingComponent = () => (
);
+const ThemedStatusBar = () => {
+ const theme = useTheme();
+ return ;
+};
+
+const AppContent = () => {
+ return (
+
+
+
+
+
+
+ );
+};
+
export default function RootLayout() {
return (
} persistor={persistor}>
-
-
-
-
-
-
+
+
+
);
diff --git a/src/app/index.tsx b/src/app/index.tsx
index f017d30..9bcf09d 100644
--- a/src/app/index.tsx
+++ b/src/app/index.tsx
@@ -1,15 +1,59 @@
-import { Text, View } from "react-native";
+import { useSettings } from "@/src/features/settings/hooks/useSettings";
+import { useTheme } from "@/src/theme/useTheme";
+import { Pressable, Text, View } from "react-native";
export default function Index() {
+ const theme = useTheme();
+ const settings = useSettings();
+
return (
- Scriptian Browser
+
+ The current theme color is {theme.name}.{"\n"}
+ The current theme mode is {settings.theme}.
+
+ settings.switchTheme("system")}>
+
+ System Theme
+
+
+ settings.switchTheme("light")}>
+
+ Light Theme
+
+
+ settings.switchTheme("dark")}>
+
+ Dark Theme
+
+
);
}
diff --git a/src/constants/Icons.ts b/src/constants/Icons.ts
new file mode 100644
index 0000000..9799262
--- /dev/null
+++ b/src/constants/Icons.ts
@@ -0,0 +1,80 @@
+/**
+ * A collection of icon name constants used throughout the application UI.
+ *
+ * @remarks
+ * - All icon names are marked as `const` to ensure type safety and prevent accidental modification.
+ * - The keys are grouped by their usage context for clarity.
+ *
+ * @example
+ * ```typescript
+ * import { Icons } from './constants/Icons';
+ *
+ * // Use an icon for a back button
+ * const backIcon = Icons.back; // "chevron-back"
+ * ```
+ */
+export const Icons = {
+ // Bottom toolbar navigation
+ back: "chevron-back" as const,
+ forward: "chevron-forward" as const,
+ share: "share-outline" as const,
+ bookmarks: "book-outline" as const,
+ tabs: "copy-outline" as const,
+
+ // Address bar icons
+ refresh: "refresh" as const,
+ stop: "close" as const,
+ secure: "lock-closed" as const,
+ insecure: "warning" as const,
+ search: "search" as const,
+
+ // Tab management
+ closeTab: "close" as const,
+ addTab: "add" as const,
+ tabOverview: "albums-outline" as const,
+
+ // Menu and settings
+ menu: "ellipsis-horizontal" as const,
+ settings: "settings-outline" as const,
+ more: "ellipsis-vertical" as const,
+
+ // Browser actions
+ reload: "refresh" as const,
+ home: "home-outline" as const,
+ downloads: "download-outline" as const,
+ history: "time-outline" as const,
+
+ // Script management
+ scripts: "code-outline" as const,
+ scriptActive: "code" as const,
+ scriptDisabled: "code-slash-outline" as const,
+
+ // Reader and accessibility
+ reader: "reader-outline" as const,
+ textSize: "text" as const,
+
+ // Bookmarks and favorites
+ bookmark: "bookmark-outline" as const,
+ bookmarkFilled: "bookmark" as const,
+ star: "star-outline" as const,
+ starFilled: "star" as const,
+
+ // General UI
+ check: "checkmark" as const,
+ error: "alert-circle" as const,
+ info: "information-circle" as const,
+ edit: "create-outline" as const,
+ delete: "trash-outline" as const,
+ copy: "copy-outline" as const,
+} as const;
+
+export type Icon = (typeof Icons)[keyof typeof Icons];
+
+export const IconSizes = {
+ small: 16,
+ medium: 20,
+ large: 24,
+ xlarge: 28,
+ toolbar: 24,
+ tab: 20,
+} as const;
diff --git a/src/features/browser/hooks/__tests__/useBrowser-test.ts b/src/features/browser/hooks/__tests__/useBrowser-test.ts
index 293e5d0..9cbcc0f 100644
--- a/src/features/browser/hooks/__tests__/useBrowser-test.ts
+++ b/src/features/browser/hooks/__tests__/useBrowser-test.ts
@@ -1,9 +1,9 @@
-import { act } from "@testing-library/react-native";
import {
createMockTab,
defaultBrowserState,
renderHookWithStore,
-} from "../../../../__tests__/utils/testUtils";
+} from "@/__tests__/utils/testUtils";
+import { act } from "@testing-library/react-native";
import { useBrowser } from "../useBrowser";
describe("useBrowser Hook", () => {
diff --git a/src/features/scripts/hooks/__tests__/useScripts-test.ts b/src/features/scripts/hooks/__tests__/useScripts-test.ts
index 9c2818e..b5f7363 100644
--- a/src/features/scripts/hooks/__tests__/useScripts-test.ts
+++ b/src/features/scripts/hooks/__tests__/useScripts-test.ts
@@ -1,11 +1,11 @@
-import { act } from "@testing-library/react-native";
import {
createMockExecution,
createMockScript,
defaultScriptsState,
defaultSettingsState,
renderHookWithStore,
-} from "../../../../__tests__/utils/testUtils";
+} from "@/__tests__/utils/testUtils";
+import { act } from "@testing-library/react-native";
import { useScripts } from "../useScripts";
describe("useScripts Hook", () => {
diff --git a/src/features/settings/hooks/useSettings.ts b/src/features/settings/hooks/useSettings.ts
index 7de49ad..1d303da 100644
--- a/src/features/settings/hooks/useSettings.ts
+++ b/src/features/settings/hooks/useSettings.ts
@@ -8,7 +8,7 @@ import {
toggleScriptsEnabled,
updateSettings,
} from "../settingsSlice";
-import { SettingsState } from "../types";
+import { SettingsState, ThemeMode } from "../types";
export const useSettings = () => {
const dispatch = useAppDispatch();
@@ -36,8 +36,12 @@ export const useSettings = () => {
dispatch(toggleAutoUpdateScripts());
};
- // Theme settings
- const switchTheme = (theme: "light" | "dark" | "system") => {
+ /**
+ * Switches the application's theme to the specified mode.
+ *
+ * @param theme - The desired theme mode to apply (e.g., 'system', 'light', 'dark').
+ */
+ const switchTheme = (theme: ThemeMode) => {
dispatch(setTheme(theme));
};
@@ -56,9 +60,6 @@ export const useSettings = () => {
};
// Computed values
- const isDarkMode = settings.theme === "dark";
- const isLightMode = settings.theme === "light";
- const isSystemMode = settings.theme === "system";
const executionTimeInSeconds = settings.maxExecutionTime / 1000;
return {
@@ -66,9 +67,6 @@ export const useSettings = () => {
...settings,
// Computed values
- isDarkMode,
- isLightMode,
- isSystemMode,
executionTimeInSeconds,
// Actions
diff --git a/src/features/settings/settingsSlice.ts b/src/features/settings/settingsSlice.ts
index cf88f49..bd0ff79 100644
--- a/src/features/settings/settingsSlice.ts
+++ b/src/features/settings/settingsSlice.ts
@@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
-import { SettingsState } from "./types";
+import { SettingsState, ThemeMode } from "./types";
const initialState: SettingsState = {
scriptsEnabled: true,
@@ -29,7 +29,7 @@ const settingsSlice = createSlice({
state.logExecutions = !state.logExecutions;
},
- setTheme: (state, action: PayloadAction<"light" | "dark" | "system">) => {
+ setTheme: (state, action: PayloadAction) => {
state.theme = action.payload;
},
diff --git a/src/features/settings/types.ts b/src/features/settings/types.ts
index a06eeb1..6229460 100644
--- a/src/features/settings/types.ts
+++ b/src/features/settings/types.ts
@@ -1,7 +1,9 @@
+export type ThemeMode = "light" | "dark" | "system";
+
export interface SettingsState {
scriptsEnabled: boolean;
maxExecutionTime: number;
logExecutions: boolean;
- theme: "light" | "dark" | "system";
+ theme: ThemeMode;
autoUpdateScripts: boolean;
}
diff --git a/src/theme/ThemeContext.tsx b/src/theme/ThemeContext.tsx
new file mode 100644
index 0000000..c7488ba
--- /dev/null
+++ b/src/theme/ThemeContext.tsx
@@ -0,0 +1,35 @@
+import { useAppSelector } from "@/src/store";
+import { createContext, useMemo } from "react";
+import { useColorScheme } from "react-native";
+import { themes } from "./themes";
+import { Theme } from "./types/theme";
+
+export const ThemeContext = createContext(themes.default);
+
+function getTheme(theme: string): Theme {
+ if (theme in themes) {
+ return themes[theme];
+ }
+ return themes.default;
+}
+
+export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const themeMode = useAppSelector((state) => state.settings.theme);
+ const colorScheme = useColorScheme();
+
+ const themeName: string = useMemo(() => {
+ if (themeMode === "system") {
+ return colorScheme === "light" ? "light" : "dark";
+ } else {
+ return themeMode;
+ }
+ }, [themeMode, colorScheme]);
+
+ const value = useMemo(() => getTheme(themeName), [themeName]);
+
+ return (
+ {children}
+ );
+};
diff --git a/src/theme/__tests__/ThemeContext-test.tsx b/src/theme/__tests__/ThemeContext-test.tsx
new file mode 100644
index 0000000..f631de0
--- /dev/null
+++ b/src/theme/__tests__/ThemeContext-test.tsx
@@ -0,0 +1,52 @@
+import { createMockStore } from "@/__tests__/utils/testUtils";
+import type { ThemeMode } from "@/src/features/settings/types";
+import { render } from "@testing-library/react-native";
+import React from "react";
+import { Text } from "react-native";
+import { Provider } from "react-redux";
+import { ThemeProvider } from "../ThemeContext";
+import { themes } from "../themes";
+import { useTheme } from "../useTheme";
+
+const renderWithStore = (
+ ui: React.ReactElement,
+ { theme = "light" }: { theme?: ThemeMode } = {}
+) => {
+ const store = createMockStore({
+ settings: {
+ theme,
+ scriptsEnabled: true,
+ maxExecutionTime: 10000,
+ logExecutions: false,
+ autoUpdateScripts: false,
+ },
+ });
+ return render({ui});
+};
+
+const ThemeNameConsumer = () => {
+ const theme = useTheme();
+ return {theme.name};
+};
+
+describe("ThemeProvider", () => {
+ it("provides the light theme when settings.theme is 'light'", () => {
+ const { getByText } = renderWithStore(
+
+
+ ,
+ { theme: "light" }
+ );
+ expect(getByText(themes.light.name)).toBeTruthy();
+ });
+
+ it("provides the dark theme when settings.theme is 'dark'", () => {
+ const { getByText } = renderWithStore(
+
+
+ ,
+ { theme: "dark" }
+ );
+ expect(getByText(themes.dark.name)).toBeTruthy();
+ });
+});
diff --git a/src/theme/themes/darkTheme.ts b/src/theme/themes/darkTheme.ts
new file mode 100644
index 0000000..3482403
--- /dev/null
+++ b/src/theme/themes/darkTheme.ts
@@ -0,0 +1,58 @@
+import { colors } from "../tokens/colors";
+import { spacing } from "../tokens/spacing";
+import { typography } from "../tokens/typography";
+import { type Theme } from "../types/theme";
+
+export const darkTheme: Theme = {
+ name: "dark",
+ statusBarStyle: "light",
+ typography: {
+ ...typography,
+ },
+ spacing: {
+ ...spacing,
+ },
+ colors: {
+ ...colors,
+ // Brand colors
+ primary: "#0A84FF",
+ primaryDark: "#409CFF",
+ secondary: "#FF9F0A",
+ secondaryDark: "#FF9F0A",
+
+ // Semantic colors
+ success: "#30D158",
+ warning: "#FF9F0A",
+ error: "#FF453A",
+ info: "#0A84FF",
+
+ // Neutral palette
+ white: "#1C1C1E",
+ gray100: "#000000",
+ gray200: "#3A3A3C",
+ gray300: "#38383A",
+ gray400: "#48484A",
+ gray500: "#8E8E93",
+ gray600: "#48484A",
+ gray700: "#2C2C2E",
+ gray800: "rgba(255, 255, 255, 0.05)",
+ gray900: "rgba(0, 0, 0, 0.3)",
+
+ // Text colors
+ textPrimary: "#FFFFFF",
+ textSecondary: "#8E8E93",
+ textInverse: "#1C1C1E",
+
+ // Backgrounds
+ background: "#000000",
+ backgroundAlt: "#1C1C1E",
+ backgroundInverse: "#FFFFFF",
+
+ // Borders
+ border: "#48484A",
+ borderStrong: "#38383A",
+
+ // Overlay
+ overlay: "rgba(0, 0, 0, 0.3)",
+ },
+};
diff --git a/src/theme/themes/index.ts b/src/theme/themes/index.ts
new file mode 100644
index 0000000..0546df3
--- /dev/null
+++ b/src/theme/themes/index.ts
@@ -0,0 +1,10 @@
+import { darkTheme } from "./darkTheme";
+import { lightTheme } from "./lightTheme";
+
+import type { Theme } from "../types/theme";
+
+export const themes: Record = {
+ default: lightTheme,
+ light: lightTheme,
+ dark: darkTheme,
+};
diff --git a/src/theme/themes/lightTheme.ts b/src/theme/themes/lightTheme.ts
new file mode 100644
index 0000000..5a9a586
--- /dev/null
+++ b/src/theme/themes/lightTheme.ts
@@ -0,0 +1,18 @@
+import { colors } from "../tokens/colors";
+import { spacing } from "../tokens/spacing";
+import { typography } from "../tokens/typography";
+import { type Theme } from "../types/theme";
+
+export const lightTheme: Theme = {
+ name: "light",
+ statusBarStyle: "dark",
+ typography: {
+ ...typography,
+ },
+ spacing: {
+ ...spacing,
+ },
+ colors: {
+ ...colors,
+ },
+};
diff --git a/src/theme/tokens/colors.ts b/src/theme/tokens/colors.ts
new file mode 100644
index 0000000..7d96da3
--- /dev/null
+++ b/src/theme/tokens/colors.ts
@@ -0,0 +1,43 @@
+export const colors = {
+ // Brand colors
+ primary: "#007AFF",
+ primaryDark: "#0051D5",
+ secondary: "#FF9500",
+ secondaryDark: "#FF9500",
+
+ // Semantic colors
+ success: "#34C759",
+ warning: "#FF9500",
+ error: "#FF3B30",
+ info: "#007AFF",
+
+ // Neutral palette
+ white: "#FFFFFF",
+ black: "#000000",
+ gray100: "#F2F2F7",
+ gray200: "#E5E5EA",
+ gray300: "#D1D1D6",
+ gray400: "#C6C6C8",
+ gray500: "#8E8E93",
+ gray600: "#C7C7CC",
+ gray700: "#F9F9F9",
+ gray800: "rgba(0, 0, 0, 0.05)",
+ gray900: "rgba(0, 0, 0, 0.1)",
+
+ // Text colors
+ textPrimary: "#000000",
+ textSecondary: "#8E8E93",
+ textInverse: "#FFFFFF",
+
+ // Backgrounds
+ background: "#F2F2F7",
+ backgroundAlt: "#FFFFFF",
+ backgroundInverse: "#1C1C1E",
+
+ // Borders
+ border: "#D1D1D6",
+ borderStrong: "#C6C6C8",
+
+ // Overlay
+ overlay: "rgba(0, 0, 0, 0.1)",
+};
diff --git a/src/theme/tokens/spacing.ts b/src/theme/tokens/spacing.ts
new file mode 100644
index 0000000..b56ba7a
--- /dev/null
+++ b/src/theme/tokens/spacing.ts
@@ -0,0 +1,13 @@
+export const spacing = {
+ none: 0,
+ xs: 4,
+ sm: 8,
+ md: 16,
+ lg: 24,
+ xl: 32,
+ xxl: 40,
+ xxxl: 64,
+ gutter: 16,
+ section: 24,
+ item: 8,
+};
diff --git a/src/theme/tokens/typography.ts b/src/theme/tokens/typography.ts
new file mode 100644
index 0000000..ff1db0b
--- /dev/null
+++ b/src/theme/tokens/typography.ts
@@ -0,0 +1,69 @@
+import { Platform } from "react-native";
+import { FontWeight } from "../types/typography";
+
+export const typography = {
+ fontFamily: Platform.OS === "ios" ? "San Francisco" : "Roboto",
+ fontSize: {
+ xs: 12,
+ sm: 14,
+ md: 16,
+ lg: 20,
+ xl: 24,
+ xxl: 32,
+ display: 40,
+ },
+ fontWeight: {
+ thin: "100" as FontWeight,
+ light: "300" as FontWeight,
+ regular: "400" as FontWeight,
+ medium: "500" as FontWeight,
+ bold: "700" as FontWeight,
+ black: "900" as FontWeight,
+ },
+ lineHeight: {
+ tight: 1.1,
+ normal: 1.4,
+ relaxed: 1.7,
+ },
+ letterSpacing: {
+ normal: 0,
+ wide: 0.5,
+ wider: 1,
+ },
+ h1: {
+ fontSize: 32,
+ fontWeight: "700" as FontWeight,
+ },
+ h2: {
+ fontSize: 28,
+ fontWeight: "700" as FontWeight,
+ },
+ h3: {
+ fontSize: 24,
+ fontWeight: "600" as FontWeight,
+ },
+ h4: {
+ fontSize: 20,
+ fontWeight: "600" as FontWeight,
+ },
+ h5: {
+ fontSize: 18,
+ fontWeight: "500" as FontWeight,
+ },
+ h6: {
+ fontSize: 16,
+ fontWeight: "500" as FontWeight,
+ },
+ body: {
+ fontSize: 16,
+ fontWeight: "400" as FontWeight,
+ },
+ caption: {
+ fontSize: 12,
+ fontWeight: "400" as FontWeight,
+ },
+ button: {
+ fontSize: 16,
+ fontWeight: "600" as FontWeight,
+ },
+};
diff --git a/src/theme/types/colorPalette.ts b/src/theme/types/colorPalette.ts
new file mode 100644
index 0000000..4cb55e5
--- /dev/null
+++ b/src/theme/types/colorPalette.ts
@@ -0,0 +1,31 @@
+export type ColorPalette = {
+ primary: string;
+ primaryDark: string;
+ secondary: string;
+ secondaryDark: string;
+ success: string;
+ warning: string;
+ error: string;
+ info: string;
+ white: string;
+ black: string;
+ gray100: string;
+ gray200: string;
+ gray300: string;
+ gray400: string;
+ gray500: string;
+ gray600: string;
+ gray700: string;
+ gray800: string;
+ gray900: string;
+ textPrimary: string;
+ textSecondary: string;
+ textInverse: string;
+ background: string;
+ backgroundAlt: string;
+ backgroundInverse: string;
+ border: string;
+ borderStrong: string;
+ overlay: string;
+ [key: string]: string;
+};
diff --git a/src/theme/types/spacing.ts b/src/theme/types/spacing.ts
new file mode 100644
index 0000000..e2f13c6
--- /dev/null
+++ b/src/theme/types/spacing.ts
@@ -0,0 +1,14 @@
+export type Spacing = {
+ none: number;
+ xs: number;
+ sm: number;
+ md: number;
+ lg: number;
+ xl: number;
+ xxl: number;
+ xxxl: number;
+ gutter: number;
+ section: number;
+ item: number;
+ [key: string]: number;
+};
diff --git a/src/theme/types/theme.ts b/src/theme/types/theme.ts
new file mode 100644
index 0000000..8caf638
--- /dev/null
+++ b/src/theme/types/theme.ts
@@ -0,0 +1,11 @@
+import { ColorPalette } from "./colorPalette";
+import { Spacing } from "./spacing";
+import { Typography } from "./typography";
+
+export type Theme = {
+ name: string;
+ statusBarStyle: "light" | "dark" | "auto";
+ colors: ColorPalette;
+ typography: Typography;
+ spacing: Spacing;
+};
diff --git a/src/theme/types/typography.ts b/src/theme/types/typography.ts
new file mode 100644
index 0000000..eca824a
--- /dev/null
+++ b/src/theme/types/typography.ts
@@ -0,0 +1,44 @@
+import { type TextStyle } from "react-native";
+
+export type FontWeight = TextStyle["fontWeight"];
+
+export type Typography = {
+ fontFamily: string;
+ fontSize: {
+ xs: number;
+ sm: number;
+ md: number;
+ lg: number;
+ xl: number;
+ xxl: number;
+ display: number;
+ };
+ fontWeight: {
+ thin: FontWeight;
+ light: FontWeight;
+ regular: FontWeight;
+ medium: FontWeight;
+ bold: FontWeight;
+ black: FontWeight;
+ };
+ lineHeight: {
+ tight: number;
+ normal: number;
+ relaxed: number;
+ };
+ letterSpacing: {
+ normal: number;
+ wide: number;
+ wider: number;
+ };
+ h1: TextStyle;
+ h2: TextStyle;
+ h3: TextStyle;
+ h4: TextStyle;
+ h5: TextStyle;
+ h6: TextStyle;
+ body: TextStyle;
+ caption: TextStyle;
+ button: TextStyle;
+ [key: string]: any;
+};
diff --git a/src/theme/useTheme.ts b/src/theme/useTheme.ts
new file mode 100644
index 0000000..404b464
--- /dev/null
+++ b/src/theme/useTheme.ts
@@ -0,0 +1,4 @@
+import { useContext } from "react";
+import { ThemeContext } from "./ThemeContext";
+
+export const useTheme = () => useContext(ThemeContext);