Move all three skills into skills/; add root README; fix paths
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
312742cfef
commit
d95e5d452d
84 files changed
+97
-31
No files matched your search
@@ -0,0 +1,91 @@
|
||||
/* EXAMPLE ONLY: an extract from the Superbill Review prototype, kept as reference for porting. ECW/eClinicalWorks, Tebra, 'Example Client', SPB_* tables and patient/ticket wording are that project's examples. Don't copy them into a new project. */
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Two named appearances, not a palette picker.
|
||||
*
|
||||
* The original control was a row of six raw colour dots that changed which hue
|
||||
* the confirm/approve buttons used. It read as decoration sitting next to the
|
||||
* confidence-band legend — two colour controls side by side, only one of which
|
||||
* meant anything. The accent is now fixed to the brand, and this control does
|
||||
* the thing a reviewer actually wants: choose the light level for the room.
|
||||
*
|
||||
* There was a third mode, `eye` — the light palette under a warm sepia filter,
|
||||
* for long shifts. It is gone. It was a third appearance to keep in sync
|
||||
* forever, it tinted the scanned page a reviewer is trying to read accurately,
|
||||
* and both the OS and the monitor already offer the same warmth system-wide.
|
||||
*/
|
||||
export type Mode = 'light' | 'dark'
|
||||
|
||||
const MODE_KEY = 'superbill.theme.mode'
|
||||
const MODES: { value: Mode; label: string; hint: string }[] = [
|
||||
{ value: 'light', label: 'Light', hint: 'Bright rooms' },
|
||||
{ value: 'dark', label: 'Dark', hint: 'Low-light rooms' },
|
||||
]
|
||||
|
||||
type ThemeContextValue = {
|
||||
mode: Mode
|
||||
setMode: (m: Mode) => void
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null)
|
||||
|
||||
function readStored<T extends string>(key: string, valid: readonly T[], fallback: T): T {
|
||||
const v = localStorage.getItem(key)
|
||||
return (valid as readonly string[]).includes(v ?? '') ? (v as T) : fallback
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
// A stored `eye` from before that mode was removed is not in the valid list,
|
||||
// so readStored falls back to `dark` rather than leaving the app in a mode
|
||||
// with no styles behind it.
|
||||
const [mode, setMode] = useState<Mode>(() =>
|
||||
readStored(MODE_KEY, ['light', 'dark'] as const, 'dark'),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement
|
||||
root.classList.toggle('dark', mode === 'dark')
|
||||
root.setAttribute('data-mode', mode)
|
||||
localStorage.setItem(MODE_KEY, mode)
|
||||
}, [mode])
|
||||
|
||||
return <ThemeContext.Provider value={{ mode, setMode }}>{children}</ThemeContext.Provider>
|
||||
}
|
||||
|
||||
export function useTheme(): ThemeContextValue {
|
||||
const ctx = useContext(ThemeContext)
|
||||
if (!ctx) throw new Error('useTheme must be used inside ThemeProvider')
|
||||
return ctx
|
||||
}
|
||||
|
||||
/** Two text pills in a pill, matching the mockup's header control. The icons
|
||||
* that used to sit beside each label are gone with the third mode: with only
|
||||
* Light and Dark left, the two words are already unambiguous and the glyphs
|
||||
* were competing with the Sign out button next to them.
|
||||
*
|
||||
* Colours here are literal white-on-black alphas rather than tokens, and have
|
||||
* to be: this control sits ON the brand gradient, which is the one saturated
|
||||
* surface in the product, and a token-coloured control there reads as a hole
|
||||
* punched through the band. See `.sb-themes` in index.css. */
|
||||
export function ThemeSwitcher() {
|
||||
const { mode, setMode } = useTheme()
|
||||
|
||||
return (
|
||||
<div className="sb-themes" role="radiogroup" aria-label="Appearance">
|
||||
{MODES.map((m) => (
|
||||
<button
|
||||
key={m.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={mode === m.value}
|
||||
aria-pressed={mode === m.value}
|
||||
title={m.hint}
|
||||
onClick={() => setMode(m.value)}
|
||||
>
|
||||
{m.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user