Skip to content

feat(konsta): foundation — KonstaProvider, hooks, theme CSS removal#333

Merged
hessius merged 3 commits intoversion/2.4.0from
feat/konsta-foundation
Mar 27, 2026
Merged

feat(konsta): foundation — KonstaProvider, hooks, theme CSS removal#333
hessius merged 3 commits intoversion/2.4.0from
feat/konsta-foundation

Conversation

@hessius
Copy link
Copy Markdown
Owner

@hessius hessius commented Mar 27, 2026

Summary

Lays the groundwork for Konsta UI integration (#332).

Changes

  • Install konsta@5.0.8 — Tailwind-native mobile UI components
  • KonstaProvider in App.tsx — wraps content when mobile or settings toggle is on
  • useKonstaOverride hook — single gate: isMobile() || forcedViaSettings
  • usePlatformTheme refactored — resolves KonstaTheme ('ios'|'material') instead of CSS classes
  • Removed custom ios-theme.css and material-theme.css (Konsta replaces them)
  • i18n keys for Konsta UI toggle in all 6 locales

Testing

  • ✅ 416 frontend tests pass
  • ✅ Build clean
  • ✅ Lint: 0 errors (27 warnings, same baseline)

Part of #332

…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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.tsx gated 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.

Comment on lines +13 to +30
// 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
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +35 to +46
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
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 17 to 25
/** 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'
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +26 to +30
export function useKonstaOverride() {
const isMobile = useIsMobile()
const forced = useSyncExternalStore(subscribe, getStoredValue, () => false)
return isMobile || forced
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

hessius and others added 2 commits March 27, 2026 21:03
- 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>
@hessius hessius merged commit 5a6153a into version/2.4.0 Mar 27, 2026
4 checks passed
@hessius hessius deleted the feat/konsta-foundation branch March 27, 2026 20:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants