feat(konsta): foundation — KonstaProvider, hooks, theme CSS removal#333
feat(konsta): foundation — KonstaProvider, hooks, theme CSS removal#333hessius merged 3 commits intoversion/2.4.0from
Conversation
…ove custom theme CSS
- Install konsta@5.0.8 (Tailwind-native mobile UI components)
- Add KonstaProvider wrapper in App.tsx (active on mobile or when forced via settings)
- Create useKonstaOverride hook (isMobile || settings toggle)
- Refactor usePlatformTheme to resolve KonstaTheme ('ios'|'material')
- Remove custom ios-theme.css and material-theme.css (replaced by Konsta)
- Remove theme CSS imports from main.tsx
- Add @import 'konsta/react/theme.css' to index.css
- Add USE_KONSTA_UI storage key to constants
- Add i18n keys for Konsta UI toggle in all 6 locales
Part of #332
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Lays foundation for Konsta UI integration by introducing a Konsta wrapper path (mobile or user-forced) and removing the app’s custom iOS/Material CSS themes in favor of Konsta’s theme CSS.
Changes:
- Added Konsta dependency + global Konsta theme CSS import.
- Added Konsta wrapper in
App.tsxgated by a new override hook. - Refactored platform theme hook to return a Konsta theme selection (and removed old platform-theme CSS files + imports).
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/web/src/styles/material-theme.css | Removes custom Material theme overrides (replaced by Konsta). |
| apps/web/src/styles/ios-theme.css | Removes custom iOS theme overrides (replaced by Konsta). |
| apps/web/src/main.tsx | Stops importing removed platform theme CSS files. |
| apps/web/src/lib/constants.ts | Adds a storage key for the Konsta UI toggle. |
| apps/web/src/index.css | Imports Konsta’s theme CSS globally. |
| apps/web/src/hooks/usePlatformTheme.ts | Refactors theme resolution to return konstaTheme. |
| apps/web/src/hooks/useKonstaOverride.ts | Adds Konsta gating + settings toggle hooks. |
| apps/web/src/App.tsx | Wraps app content in KonstaApp when enabled. |
| apps/web/public/locales/*/translation.json | Adds i18n strings for the Konsta UI toggle. |
| apps/web/package.json | Adds konsta dependency. |
| apps/web/bun.lock | Locks konsta and bumps @chromatic-com/storybook. |
| // Subscribe to storage events so multiple tabs stay in sync | ||
| function subscribe(callback: () => void) { | ||
| const handler = (e: StorageEvent) => { | ||
| if (e.key === STORAGE_KEYS.USE_KONSTA_UI) callback() | ||
| } | ||
| window.addEventListener('storage', handler) | ||
| return () => window.removeEventListener('storage', handler) | ||
| } | ||
|
|
||
| /** | ||
| * Returns true if Konsta UI components should render. | ||
| * True when: viewport is mobile OR the settings toggle is forced on. | ||
| */ | ||
| export function useKonstaOverride() { | ||
| const isMobile = useIsMobile() | ||
| const forced = useSyncExternalStore(subscribe, getStoredValue, () => false) | ||
| return isMobile || forced | ||
| } |
There was a problem hiding this comment.
useSyncExternalStore here only re-subscribes on storage events, but storage does not fire in the same tab that calls localStorage.setItem. As a result, toggling Konsta UI in Settings in the current tab won’t update useKonstaOverride() immediately (likely requires a reload or unrelated re-render). Fix by implementing a small shared store that (1) writes to localStorage and (2) notifies same-tab subscribers (e.g., keep an in-memory emitter and/or dispatch a custom event; optionally also use BroadcastChannel for cross-tab). Then have both read (getSnapshot) and subscribe use that store so same-tab updates propagate.
There was a problem hiding this comment.
Fixed in b40de19 — added KONSTA_CHANGED custom event dispatched from useKonstaToggle.setEnabled(), with subscribe() listening for both storage (cross-tab) and KONSTA_CHANGED (same-tab) events. Follows the same pattern as aiPreferences.ts.
| export function useKonstaToggle() { | ||
| const [enabled, setEnabledState] = useState(getStoredValue) | ||
|
|
||
| const setEnabled = useCallback((value: boolean) => { | ||
| setEnabledState(value) | ||
| try { | ||
| localStorage.setItem(STORAGE_KEYS.USE_KONSTA_UI, String(value)) | ||
| } catch { /* noop */ } | ||
| }, []) | ||
|
|
||
| return { enabled, setEnabled } as const | ||
| } |
There was a problem hiding this comment.
useKonstaToggle keeps its own state initialized from localStorage but doesn’t subscribe to external changes (e.g., toggled in another tab, or via a different Settings surface). This can lead to stale UI state. Prefer to drive enabled from the same external-store mechanism as useKonstaOverride (or the shared store suggested above) so reads/writes are consistent and automatically synced.
There was a problem hiding this comment.
Acknowledged. For v2.4.0 the single Settings surface is the only writer, so staleness risk is minimal. useKonstaToggle state is write-through (always syncs to localStorage + dispatches event). Will promote to useSyncExternalStore if we add a second toggle surface.
| /** Resolve which Konsta theme to use based on preference + detected platform. */ | ||
| function resolveKonstaTheme(pref: PlatformTheme, detected: DetectedPlatform): KonstaTheme { | ||
| if (pref === 'ios') return 'ios' | ||
| if (pref === 'material') return 'material' | ||
| if (pref === 'none') return 'material' // default fallback | ||
| // auto | ||
| if (detected === 'ios') return 'ios' | ||
| return 'material' | ||
| } |
There was a problem hiding this comment.
Mapping PlatformTheme = 'none' to KonstaTheme = 'material' is semantically inconsistent: if a user selects “none” (previously meaning no platform theming), they still get Material when Konsta is enabled. Consider returning KonstaTheme | null (or a separate shouldUseKonstaTheme flag) so the caller can honor “none” by not applying a Konsta theme/wrapper (or by falling back to the app’s default styling). If “none” is intended to mean “material default”, renaming the option/value would avoid confusion.
There was a problem hiding this comment.
Added clarifying comment in b40de19. The 'none' option historically disabled custom CSS themes (now deleted). KonstaProvider always requires a valid theme prop, so 'none' falls back to 'material' as sensible default. Option will likely be removed in a future cleanup.
| export function useKonstaOverride() { | ||
| const isMobile = useIsMobile() | ||
| const forced = useSyncExternalStore(subscribe, getStoredValue, () => false) | ||
| return isMobile || forced | ||
| } |
There was a problem hiding this comment.
The Konsta gating behavior should be covered by tests, especially (a) forced toggle changes immediately affecting useKonstaOverride() in the same tab/session, and (b) cross-tab synchronization via the storage event (if kept). Adding tests here will prevent regressions where the UI doesn’t update after toggling.
There was a problem hiding this comment.
Added in b40de19 — useKonstaOverride.test.ts covers: mobile detection, forced toggle via localStorage, same-tab reactivity (custom event dispatch), and useKonstaToggle state management. 7 tests total.
- Fix same-tab reactivity: dispatch custom event from useKonstaToggle so useKonstaOverride re-renders without page refresh - Add clarifying comment for 'none' → 'material' theme mapping - Add unit tests for useKonstaOverride and useKonstaToggle hooks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Lays the groundwork for Konsta UI integration (#332).
Changes
konsta@5.0.8— Tailwind-native mobile UI componentsisMobile() || forcedViaSettingsKonstaTheme('ios'|'material') instead of CSS classesios-theme.cssandmaterial-theme.css(Konsta replaces them)Testing
Part of #332