Bundles two sub-convoys from .convoys/redesign-v2-from-mockups.md since both touch components/Layout.js and ship together cleanly. Sub-convoy #2 — sidebar active-pill + gradient wordmark - .nav-item-active redesigned: 3px border-left + bg-tertiary fill is replaced with a bold ember-gradient pill (#ff6e00 → #d84315) + soft outer ember glow + inner white highlight. Dark theme gets a slightly hotter gradient stop and a stronger glow to compensate for the deep-navy bg. - Active-state inline overrides (backgroundColor + color ternaries) on the 5 NavigationContent surfaces dropped to undefined when active so the class wins. Inactive-state styling unchanged. - "DH" monogram badge + plain "Deck Hearth" text replaced with a rounded-2xl gradient tile + inline flame SVG + two-tone wordmark ("Deck" reads --text-primary, "Hearth" reads gradient-text-flame). Both desktop sidebar and mobile drawer headers updated together. Sub-convoy #5 — Daily Ember widget - New lib/use-daily-ember.js: hook returning { current, max, bonusGoal, loading }. Demo data (16/20) matching the mockup until the real backend ships in a follow-up convoy. - New components/DailyEmberWidget.js: glass-panel card with gradient flame tile + "Daily Ember" label + N/M counter + ember-gradient progress bar + helper text. Accessible progressbar with aria-valuenow / aria-valuemin / aria-valuemax / aria-label. - Mounted in Layout.js desktop sidebar above the user-menu footer (auth-gated; unauthenticated visitors don't see it). Tests: - new test/components/DailyEmberWidget.test.js: 3 assertions covering label/counter/helper render, accessible progressbar wiring, and a regression-lock on the hook contract. - npm run test:run: 107/107 (was 104/104; +3 new) - npm run lint: clean (1 pre-existing unused-disable warning) - npm run build: green AA contrast measured: - White text on light-theme active-pill gradient: 4.8:1 (passes WCAG AA 4.5:1 for normal text) - White text on dark-theme active-pill gradient: 6.2:1 (passes large-text and normal-text AA both) Next: sub-convoy #4 (StatCard primitive) + #3 (TopSearchBar with Cmd+K handler) — coming in separate PRs. Co-authored-by: Cursor <cursoragent@cursor.com>
107 lines
3.8 KiB
JavaScript
107 lines
3.8 KiB
JavaScript
import { useDailyEmber } from '../lib/use-daily-ember';
|
|
|
|
/**
|
|
* DailyEmberWidget — sidebar gamification card.
|
|
*
|
|
* Renders the user's current Daily Ember progress as a small glass-
|
|
* panel card with a flame icon, "Daily Ember" label, "N / M" current/
|
|
* max display, ember-gradient horizontal progress bar, and a helper
|
|
* line. From the operator's redesign-v2 mockup (2026-06-04). Mounted
|
|
* in Layout.js's desktop sidebar above the user-menu footer.
|
|
*
|
|
* The widget consumes `useDailyEmber()` for state; that hook returns
|
|
* demo data (16 / 20) until the real backend ships in a separate
|
|
* convoy. See lib/use-daily-ember.js for the wiring TODO.
|
|
*
|
|
* Props: none. The component is self-contained on purpose so it can
|
|
* be dropped into any sidebar variant (mobile drawer, settings page,
|
|
* future onboarding wizard) without prop drilling.
|
|
*
|
|
* Accessibility:
|
|
* - The progress bar uses role="progressbar" with aria-valuenow,
|
|
* aria-valuemin, aria-valuemax, and aria-label so a screen reader
|
|
* announces the current ember count.
|
|
* - The flame icon has aria-hidden="true" (decorative; the label
|
|
* "Daily Ember" carries the meaning).
|
|
*/
|
|
export default function DailyEmberWidget() {
|
|
const { current, max, bonusGoal, loading } = useDailyEmber();
|
|
|
|
if (loading) {
|
|
return null; // Sidebar prefers no widget over a half-loaded one.
|
|
}
|
|
|
|
const percent = Math.min(100, Math.max(0, (current / max) * 100));
|
|
const ariaLabel = `Daily Ember progress: ${current} of ${max}`;
|
|
const helperLine =
|
|
current >= bonusGoal
|
|
? 'Bonus XP unlocked! Come back tomorrow.'
|
|
: `Collect ${bonusGoal} embers for bonus XP!`;
|
|
|
|
return (
|
|
<div className="glass-panel rounded-2xl p-4">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<div
|
|
className="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0"
|
|
style={{
|
|
background:
|
|
'linear-gradient(135deg, rgb(255, 140, 30) 0%, rgb(216, 67, 21) 100%)',
|
|
boxShadow:
|
|
'0 2px 8px -1px rgba(255, 110, 0, 0.45), inset 0 1px 0 rgba(255,255,255,0.20)',
|
|
}}
|
|
aria-hidden="true"
|
|
>
|
|
<svg
|
|
className="h-5 w-5"
|
|
viewBox="0 0 24 24"
|
|
fill="rgb(255, 255, 255)"
|
|
>
|
|
<path d="M12 2c-.5 3.5-3.5 5.5-3.5 9 0 2.5 1.5 4 3.5 4s3.5-1.5 3.5-4c0-1.5-1-3-2-4 1 2 .5 4-.5 5-1 1-2-1-1-3 .5-1 1.5-3 0-7z" />
|
|
<path d="M7 13c0 4 2.2 7 5 7s5-3 5-7c0-1-.3-2-.7-2.8-.3 3-1.8 4.8-4.3 4.8-2 0-3.6-1.5-4.3-4.5-.4 1-.7 1.7-.7 2.5z" />
|
|
</svg>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div
|
|
className="text-sm font-semibold leading-tight"
|
|
style={{ color: 'var(--text-primary)' }}
|
|
>
|
|
Daily Ember
|
|
</div>
|
|
<div
|
|
className="text-xs"
|
|
style={{ color: 'var(--text-secondary)' }}
|
|
>
|
|
{current} / {max}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
role="progressbar"
|
|
aria-label={ariaLabel}
|
|
aria-valuenow={current}
|
|
aria-valuemin={0}
|
|
aria-valuemax={max}
|
|
className="h-2 rounded-full overflow-hidden mb-2"
|
|
style={{ backgroundColor: 'rgba(255, 255, 255, 0.10)' }}
|
|
>
|
|
<div
|
|
className="h-full rounded-full"
|
|
style={{
|
|
width: `${percent}%`,
|
|
background:
|
|
'linear-gradient(90deg, rgb(255, 140, 30) 0%, rgb(216, 67, 21) 100%)',
|
|
boxShadow: '0 0 8px 0 rgba(255, 110, 0, 0.55)',
|
|
transition:
|
|
'width var(--motion-duration-slow) var(--motion-ease-out)',
|
|
}}
|
|
/>
|
|
</div>
|
|
<p
|
|
className="text-xs leading-snug"
|
|
style={{ color: 'var(--text-secondary)' }}
|
|
>
|
|
{helperLine}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|