refactor(design): site-wide sweep — broken Tailwind tokens, rounded corners, SearchBar primitive (#117)

Comprehensive design sweep across the rest of the app following the
shipped Liquid Glass + corner-border-light system (#116).

## Three classes of finding

### 1. Broken Tailwind token classes (HIGH — pages were unstyled)

The decks / deck-builder / deck-detail cluster relied on Tailwind
classes that don't exist in `tailwind.config.js` (no `bg-bg-*`,
`text-text-*`, `border-border`, `bg-accent-ember`,
`focus:ring-accent-ember`, `hover:bg-accent-ember-dark`). Those classes
produced ZERO CSS — backgrounds were transparent, borders invisible,
hover states absent.

Rewrote with inline `style={{ ... CSS vars ... }}` + the `<Button>` /
`<SearchBar>` primitives + `glass-panel` surfaces:

- `pages/decks.js` (full page)
- `pages/deck/[id].js` (header, stats sidebar, group-by controls,
  card list)
- `pages/deck-builder.js` (loading spinner)
- `components/DeckBuilderView.js` (toolbar + main panel)
- `components/DeckBuilderCardBrowser.js` (full rewrite; integrated
  `<SearchBar>` for the card-picker input)
- `components/DeckBuilderDeckList.js` (full rewrite)
- `components/DeckBuilderStatsBar.js`
- `components/ManaSymbolSettings.js`
- `components/ManaSymbols.js` (single `text-text-secondary`)
- `pages/admin/card-editor.js` cluster was already clean

### 2. Duplicative / stale page searches

Replaced raw `<input>` search controls with the `<SearchBar>` primitive
(adds clear button, ember focus ring, system-consistent rounded
corners). Kept page-specific filter searches (they filter the visible
list — distinct from the global TopSearchBar command palette):

- `pages/my-cards.js`
- `pages/community/collections.js`
- `components/CardsPageView.js`
- `components/CollectionPageView.js`
- `components/DeckBuilderCardBrowser.js`

`pages/my-cards.js` filter wrapper also lifted into a `glass-panel`
chip instead of a solid `var(--bg-primary)` band.

### 3. Square corners + stale palette in shared views

- `components/CollectionPageView.js`: 10 action buttons (`rounded-lg`
  + `hover:bg-gray-50`) → `rounded-xl` + `nav-item-hover`; 4 filter
  selects (`focus:ring-purple-500 rounded-lg`) → `.input-field`;
  view-mode toggle (`bg-white text-gray-900` — invisible in dark mode)
  → tokenised; SYSTEM badge gradient (`from-blue-500 to-purple-600`)
  → ember↔flame; tooltip (`bg-gray-900`) → `glass-panel-strong`;
  search-results dropdown (`bg-white border-gray-200` — invisible in
  dark mode) → `glass-panel-strong`; Activity / game-count /
  TCG-game badges palette-aligned.
- `components/CardsPageView.js`: "Load More Cards" button
  (`bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg`) →
  `<Button variant="primary" size="lg">`.
- `components/CollectionsPageView.js`: matching SYSTEM badge +
  tooltip cleanup.
- `components/ShareModal.js`: user-search dropdown
  (`border-gray-200 hover:bg-gray-50`) and email-invite card moved
  onto `glass-panel` + `nav-item-hover`; social-share buttons
  `rounded-lg hover:bg-gray-50` → `rounded-xl nav-item-hover`.
- `components/Layout.js`: profile-menu dropdown row
  (`hover:bg-gray-50 dark:hover:bg-gray-700`) → `nav-item-hover`.
- `components/CardItem.js`: bulk-select checkbox
  `focus:ring-purple-500` → ember.

### 4. `dark:` modifier classes (broken with `[data-theme]` theming)

This app uses `[data-theme="dark"]` CSS selector theming, not
Tailwind's `class` strategy, so `dark:bg-green-900/20` etc. produced
no CSS in dark mode. Affected alerts on `pages/settings.js` and
`pages/profile.js` — replaced with `glass-panel` + semantic border
colour (flame for success, #dc2626 for error).

`pages/settings.js` sidebar nav also moved off its hardcoded full-ember
fill onto the system `nav-item` / `nav-item-active` / `nav-item-hover`
pattern for consistency with the global sidebar.

## Verification

- `npm run build` — green (Next 16 + Turbopack)
- `npm run lint` — 0 errors, 1 unrelated pre-existing warning
- `npm run test:run` — 113/113 pass (no test changes needed)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Randall Stillwell 2026-06-04 12:55:29 -05:00
parent c57b5d0406
commit 1769d4576a
19 changed files with 1397 additions and 1188 deletions

View file

@ -194,7 +194,11 @@ export default function CardItem({
type="checkbox" type="checkbox"
checked={isSelected} checked={isSelected}
onChange={handleSelectChange} onChange={handleSelectChange}
className="w-4 h-4 text-purple-600 bg-gray-100 border-gray-300 rounded focus:ring-purple-500 focus:ring-2" className="w-4 h-4 rounded focus:ring-2"
style={{
accentColor: 'var(--accent-ember)',
'--tw-ring-color': 'var(--accent-ember)',
}}
/> />
</div> </div>

View file

@ -1,6 +1,7 @@
import CardItem from './CardItem'; import CardItem from './CardItem';
import BulkSelectionToolbar from './BulkSelectionToolbar'; import BulkSelectionToolbar from './BulkSelectionToolbar';
import CollectionSelectionModal from './CollectionSelectionModal'; import CollectionSelectionModal from './CollectionSelectionModal';
import { Button, SearchBar } from './ui';
export default function CardsPageView(props) { export default function CardsPageView(props) {
const { const {
@ -146,14 +147,18 @@ export default function CardsPageView(props) {
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Search Search
</label> </label>
<div className="relative"> <SearchBar
<input
type="text"
placeholder="Search cards..."
className="search-bar w-full pr-12"
value={searchQuery} value={searchQuery}
onChange={(e) => handleSearchChange(e.target.value)} onChange={(e) => handleSearchChange(e.target.value)}
onKeyPress={(e) => { onClear={() => {
handleSearchChange('');
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
setHasMore(true);
hasMoreRef.current = true;
fetchCards(false);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
setPagination(prev => ({ ...prev, page: 1 })); setPagination(prev => ({ ...prev, page: 1 }));
setCards([]); setCards([]);
@ -162,23 +167,8 @@ export default function CardsPageView(props) {
fetchCards(false); fetchCards(false);
} }
}} }}
placeholder="Search cards…"
/> />
<button
onClick={() => {
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
setHasMore(true);
hasMoreRef.current = true;
fetchCards(false);
}}
className="absolute right-2 top-1/2 transform -translate-y-1/2 p-2 rounded-lg hover:bg-opacity-20 transition-all duration-200"
style={{ backgroundColor: 'var(--bg-tertiary)' }}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
</div>
</div> </div>
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
@ -358,12 +348,9 @@ export default function CardsPageView(props) {
{/* Load More Button */} {/* Load More Button */}
{hasMore && !loadingMore && ( {hasMore && !loadingMore && (
<div className="flex justify-center py-8"> <div className="flex justify-center py-8">
<button <Button variant="primary" size="lg" onClick={loadMoreCards}>
onClick={loadMoreCards}
className="px-6 py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-lg hover:from-blue-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl"
>
Load More Cards Load More Cards
</button> </Button>
</div> </div>
)} )}

View file

@ -10,6 +10,7 @@ import ManaSymbolSettings from './ManaSymbolSettings';
import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js'; import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js';
import CollectionEditModal from './CollectionEditModal'; import CollectionEditModal from './CollectionEditModal';
import CollectionDeleteModal from './CollectionDeleteModal'; import CollectionDeleteModal from './CollectionDeleteModal';
import { Button, SearchBar } from './ui';
export default function CollectionPageView(props) { export default function CollectionPageView(props) {
const { const {
cards, cards,
@ -99,7 +100,7 @@ export default function CollectionPageView(props) {
<> <>
<button <button
onClick={() => setShowEditModal(true)} onClick={() => setShowEditModal(true)}
className="px-4 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50 flex items-center space-x-2" className="px-4 py-2 text-sm font-medium border rounded-xl nav-item-hover flex items-center space-x-2"
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }} style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -109,7 +110,7 @@ export default function CollectionPageView(props) {
</button> </button>
<button <button
onClick={() => setShowDeleteModal(true)} onClick={() => setShowDeleteModal(true)}
className="px-4 py-2 text-sm font-medium border rounded-lg hover:bg-red-50 hover:border-red-200 hover:text-red-600 flex items-center space-x-2" className="px-4 py-2 text-sm font-medium border rounded-xl nav-item-hover flex items-center space-x-2"
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }} style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -122,12 +123,15 @@ export default function CollectionPageView(props) {
<button <button
onClick={() => setShowUploadModal(true)} onClick={() => setShowUploadModal(true)}
className="px-4 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50" className="px-4 py-2 text-sm font-medium border rounded-xl nav-item-hover"
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }} style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
> >
Upload Image Upload Image
</button> </button>
<button className="px-4 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50" style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}> <button
className="px-4 py-2 text-sm font-medium border rounded-xl nav-item-hover"
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
>
🪄 Generate AI Image 🪄 Generate AI Image
</button> </button>
</div> </div>
@ -142,16 +146,31 @@ export default function CollectionPageView(props) {
{/* System collection indicator */} {/* System collection indicator */}
{collection.isSystemCollection && ( {collection.isSystemCollection && (
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<span className="px-2.5 py-1 text-xs font-bold rounded-full bg-gradient-to-r from-blue-500 to-purple-600 text-white shadow-sm border-2 border-blue-200"> <span
className="px-2.5 py-1 text-xs font-bold rounded-full text-white shadow-sm border-2"
style={{
background:
'linear-gradient(135deg, var(--accent-ember) 0%, var(--accent-flame) 100%)',
borderColor: 'var(--accent-flame)',
}}
>
🔒 SYSTEM 🔒 SYSTEM
</span> </span>
<div className="group relative"> <div className="group relative">
<svg className="h-4 w-4 text-blue-500 cursor-help" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
className="h-4 w-4 cursor-help"
style={{ color: 'var(--accent-ember)' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 text-xs bg-gray-900 text-white rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap"> <div
className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 text-xs rounded-xl shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap glass-panel-strong"
style={{ color: 'var(--text-primary)' }}
>
{VOCAB.SYSTEM_COLLECTION_SYNC_HINT} {VOCAB.SYSTEM_COLLECTION_SYNC_HINT}
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
</div> </div>
</div> </div>
</div> </div>
@ -167,7 +186,11 @@ export default function CollectionPageView(props) {
count > 0 ? ( count > 0 ? (
<span <span
key={game} key={game}
className="px-3 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800" className="px-3 py-1 text-xs font-medium rounded-full"
style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-primary)',
}}
> >
{game} {game}
</span> </span>
@ -202,7 +225,7 @@ export default function CollectionPageView(props) {
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<button <button
onClick={() => setShowShareModal(true)} onClick={() => setShowShareModal(true)}
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50" className="flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-xl nav-item-hover"
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }} style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -213,10 +236,18 @@ export default function CollectionPageView(props) {
<button <button
onClick={toggleFavorite} onClick={toggleFavorite}
className={`flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-lg ${ className={`flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-xl ${
isFavorited ? 'bg-red-50 border-red-200 text-red-600' : 'hover:bg-gray-50' isFavorited ? '' : 'nav-item-hover'
}`} }`}
style={!isFavorited ? { borderColor: 'var(--border)', color: 'var(--text-primary)' } : {}} style={
isFavorited
? {
borderColor: 'var(--accent-ember)',
color: 'var(--accent-ember)',
backgroundColor: 'transparent',
}
: { borderColor: 'var(--border)', color: 'var(--text-primary)' }
}
> >
<svg className="w-4 h-4" fill={isFavorited ? 'currentColor' : 'none'} stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill={isFavorited ? 'currentColor' : 'none'} stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
@ -226,7 +257,7 @@ export default function CollectionPageView(props) {
<button <button
onClick={handleDownloadCSV} onClick={handleDownloadCSV}
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50" className="flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-xl nav-item-hover"
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }} style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -238,7 +269,15 @@ export default function CollectionPageView(props) {
<div className="flex items-center space-x-1 text-sm" style={{ color: 'var(--text-secondary)' }}> <div className="flex items-center space-x-1 text-sm" style={{ color: 'var(--text-secondary)' }}>
<span>Activity</span> <span>Activity</span>
<span className="px-2 py-1 bg-purple-100 text-purple-800 rounded-full text-xs font-medium">123</span> <span
className="px-2 py-1 rounded-full text-xs font-medium"
style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-primary)',
}}
>
123
</span>
</div> </div>
</div> </div>
</div> </div>
@ -248,28 +287,25 @@ export default function CollectionPageView(props) {
{/* Search and Filters */} {/* Search and Filters */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="relative"> <div className="w-64">
<input <SearchBar
type="text"
placeholder="Search cards..."
value={searchCards} value={searchCards}
onChange={(e) => { onChange={(e) => {
setSearchCards(e.target.value); setSearchCards(e.target.value);
handleSearchCards(e.target.value); handleSearchCards(e.target.value);
}} }}
className="w-64 px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" onClear={() => {
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }} setSearchCards('');
handleSearchCards('');
}}
placeholder="Search cards…"
/> />
<svg className="absolute right-3 top-2.5 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ color: 'var(--text-secondary)' }}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div> </div>
<select <select
value={selectedRarity} value={selectedRarity}
onChange={(e) => setSelectedRarity(e.target.value)} onChange={(e) => setSelectedRarity(e.target.value)}
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500" className="input-field"
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
> >
<option>All Rarities</option> <option>All Rarities</option>
<option>Common</option> <option>Common</option>
@ -282,8 +318,7 @@ export default function CollectionPageView(props) {
<select <select
value={selectedType} value={selectedType}
onChange={(e) => setSelectedType(e.target.value)} onChange={(e) => setSelectedType(e.target.value)}
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500" className="input-field"
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
> >
<option>All Types</option> <option>All Types</option>
<option>Creature</option> <option>Creature</option>
@ -296,8 +331,7 @@ export default function CollectionPageView(props) {
<select <select
value={groupBy} value={groupBy}
onChange={(e) => setGroupBy(e.target.value)} onChange={(e) => setGroupBy(e.target.value)}
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500" className="input-field"
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
> >
<option>Group by Game</option> <option>Group by Game</option>
<option>Group by Rarity</option> <option>Group by Rarity</option>
@ -307,8 +341,7 @@ export default function CollectionPageView(props) {
<select <select
value={sortBy} value={sortBy}
onChange={(e) => setSortBy(e.target.value)} onChange={(e) => setSortBy(e.target.value)}
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500" className="input-field"
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
> >
<option>Sort by Name</option> <option>Sort by Name</option>
<option>Sort by Price</option> <option>Sort by Price</option>
@ -318,11 +351,22 @@ export default function CollectionPageView(props) {
</div> </div>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="flex rounded-lg border" style={{ borderColor: 'var(--border)' }}> <div
className="flex rounded-xl border"
style={{ borderColor: 'var(--border)' }}
>
<button <button
onClick={() => setViewMode('grid')} onClick={() => setViewMode('grid')}
className={`p-2 rounded-l-lg ${viewMode === 'grid' ? 'bg-white text-gray-900 shadow-sm' : 'bg-transparent hover:bg-gray-50'}`} className="p-2 rounded-l-xl transition-colors"
style={viewMode !== 'grid' ? { color: 'var(--text-secondary)' } : {}} style={{
backgroundColor:
viewMode === 'grid' ? 'var(--bg-tertiary)' : 'transparent',
color:
viewMode === 'grid'
? 'var(--text-primary)'
: 'var(--text-secondary)',
}}
aria-label="Grid view"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
@ -330,8 +374,16 @@ export default function CollectionPageView(props) {
</button> </button>
<button <button
onClick={() => setViewMode('list')} onClick={() => setViewMode('list')}
className={`p-2 rounded-r-lg ${viewMode === 'list' ? 'bg-white text-gray-900 shadow-sm' : 'bg-transparent hover:bg-gray-50'}`} className="p-2 rounded-r-xl transition-colors"
style={viewMode !== 'list' ? { color: 'var(--text-secondary)' } : {}} style={{
backgroundColor:
viewMode === 'list' ? 'var(--bg-tertiary)' : 'transparent',
color:
viewMode === 'list'
? 'var(--text-primary)'
: 'var(--text-secondary)',
}}
aria-label="List view"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
@ -339,18 +391,22 @@ export default function CollectionPageView(props) {
</button> </button>
</div> </div>
<button <Button
variant="primary"
onClick={() => router.push('/cards')} onClick={() => router.push('/cards')}
className="px-4 py-2 text-white rounded-lg flex items-center space-x-2" leadingIcon={
style={{ backgroundColor: 'var(--accent-ember)' }}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg> </svg>
<span>Add Cards</span> }
</button> >
Add Cards
</Button>
<button className="px-4 py-2 border rounded-lg hover:bg-gray-50 flex items-center space-x-2" style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}> <button
className="px-4 py-2 border rounded-xl nav-item-hover flex items-center space-x-2"
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
@ -366,7 +422,13 @@ export default function CollectionPageView(props) {
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold flex items-center space-x-3" style={{ color: 'var(--text-primary)' }}> <h2 className="text-xl font-bold flex items-center space-x-3" style={{ color: 'var(--text-primary)' }}>
<span>{game === 'MTG' ? 'Magic The Gathering' : game}</span> <span>{game === 'MTG' ? 'Magic The Gathering' : game}</span>
<span className="px-2 py-1 bg-gray-100 text-gray-600 rounded text-sm font-medium"> <span
className="px-2 py-1 rounded-xl text-sm font-medium"
style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-secondary)',
}}
>
{gameCards.length} {gameCards.length}
</span> </span>
</h2> </h2>
@ -399,21 +461,23 @@ export default function CollectionPageView(props) {
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}> <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
Add cards to get started with your collection Add cards to get started with your collection
</p> </p>
<button className="px-6 py-3 text-white rounded-lg" style={{ backgroundColor: 'var(--accent-ember)' }}> <Button variant="primary" onClick={() => router.push('/cards')}>
Browse Cards to Add Browse Cards to Add
</button> </Button>
</div> </div>
)} )}
</div> </div>
{/* Search Results Dropdown */} {/* Search Results Dropdown */}
{showSearchResults && searchResults.length > 0 && ( {showSearchResults && searchResults.length > 0 && (
<div className="absolute top-full left-0 right-0 bg-white border border-gray-200 rounded-lg shadow-lg max-h-64 overflow-y-auto z-50"> <div
className="absolute top-full left-0 right-0 glass-panel-strong rounded-xl max-h-64 overflow-y-auto z-50"
>
{searchResults.map(card => ( {searchResults.map(card => (
<div <div
key={card.id} key={card.id}
onClick={() => handleAddCard(card)} onClick={() => handleAddCard(card)}
className="flex items-center p-3 hover:bg-gray-50 cursor-pointer" className="flex items-center p-3 cursor-pointer nav-item-hover"
> >
<img <img
src={card.image_url} src={card.image_url}
@ -424,8 +488,12 @@ export default function CollectionPageView(props) {
}} }}
/> />
<div> <div>
<div className="font-medium">{card.name}</div> <div className="font-medium" style={{ color: 'var(--text-primary)' }}>
<div className="text-sm text-gray-500">{card.set_name} ${card.market_price}</div> {card.name}
</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{card.set_name} ${card.market_price}
</div>
</div> </div>
</div> </div>
))} ))}

View file

@ -140,16 +140,31 @@ export default function CollectionsPageView(props) {
{/* System collection indicator */} {/* System collection indicator */}
{collection.isSystemCollection && ( {collection.isSystemCollection && (
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<span className="px-2.5 py-1 text-xs font-bold rounded-full bg-gradient-to-r from-blue-500 to-purple-600 text-white shadow-sm border-2 border-blue-200"> <span
className="px-2.5 py-1 text-xs font-bold rounded-full text-white shadow-sm border-2"
style={{
background:
'linear-gradient(135deg, var(--accent-ember) 0%, var(--accent-flame) 100%)',
borderColor: 'var(--accent-flame)',
}}
>
🔒 SYSTEM 🔒 SYSTEM
</span> </span>
<div className="group relative"> <div className="group relative">
<svg className="h-4 w-4 text-blue-500 cursor-help" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
className="h-4 w-4 cursor-help"
style={{ color: 'var(--accent-ember)' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 text-xs bg-gray-900 text-white rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap"> <div
className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 text-xs rounded-xl shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap glass-panel-strong"
style={{ color: 'var(--text-primary)' }}
>
{VOCAB.SYSTEM_COLLECTION_SYNC_HINT} {VOCAB.SYSTEM_COLLECTION_SYNC_HINT}
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
</div> </div>
</div> </div>
</div> </div>

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,27 @@
/* eslint-disable @next/next/no-img-element -- External card image URLs; next/image migration is out of scope. */ /* eslint-disable @next/next/no-img-element -- External card image URLs; next/image migration is out of scope. */
import { ManaCost } from './ManaSymbols'; import { ManaCost } from './ManaSymbols';
import { Button } from './ui';
/* 2026-06-04 design-sweep pass: same broken Tailwind tokens
(bg-bg-*, text-text-*, bg-accent-ember, hover:bg-accent-ember-dark)
that the other deck builder files had none of those classes are
defined in tailwind.config.js, so this list rendered with no
visible card surfaces or hover affordances. Sweep replaces with
inline CSS variables + Button primitive. Rarity badges reuse the
neutral palette from DeckBuilderCardBrowser. */
const rarityBadgeStyle = (rarity) => {
switch (rarity) {
case 'mythic':
return { backgroundColor: 'var(--accent-ember)', color: '#ffffff' };
case 'rare':
return { backgroundColor: 'var(--accent-flame)', color: '#ffffff' };
case 'uncommon':
return { backgroundColor: 'var(--bg-tertiary)', color: 'var(--text-primary)' };
default:
return { backgroundColor: 'var(--bg-tertiary)', color: 'var(--text-secondary)' };
}
};
export default function DeckBuilderDeckList({ export default function DeckBuilderDeckList({
deckCards, deckCards,
@ -14,22 +36,30 @@ export default function DeckBuilderDeckList({
{deckCards.length === 0 ? ( {deckCards.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<div className="text-6xl mb-4">🃏</div> <div className="text-6xl mb-4">🃏</div>
<h3 className="text-xl font-semibold text-text-primary mb-2">Empty Deck</h3> <h3
<p className="text-text-secondary mb-4">Start building your deck by searching for cards</p> className="text-xl font-semibold mb-2"
{!sidebarOpen && ( style={{ color: 'var(--text-primary)' }}
<button
onClick={onOpenSidebar}
className="bg-accent-ember text-white px-4 py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
> >
Empty Deck
</h3>
<p className="mb-4" style={{ color: 'var(--text-secondary)' }}>
Start building your deck by searching for cards
</p>
{!sidebarOpen && (
<Button variant="primary" onClick={onOpenSidebar}>
Open Card Browser Open Card Browser
</button> </Button>
)} )}
</div> </div>
) : ( ) : (
deckCards deckCards
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
.map((card) => ( .map((card) => (
<div key={`${card.card_id}-${card.id}`} className="flex items-center justify-between p-4 bg-bg-primary rounded-lg hover:bg-bg-tertiary transition-colors"> <div
key={`${card.card_id}-${card.id}`}
className="flex items-center justify-between p-4 rounded-xl transition-colors nav-item-hover"
style={{ backgroundColor: 'var(--bg-primary)' }}
>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
{card.image_url && ( {card.image_url && (
<img <img
@ -39,21 +69,36 @@ export default function DeckBuilderDeckList({
/> />
)} )}
<div> <div>
<h4 className="font-semibold text-text-primary text-lg">{card.name}</h4> <h4
<p className="text-text-secondary text-sm">{card.set_name}</p> className="font-semibold text-lg"
style={{ color: 'var(--text-primary)' }}
>
{card.name}
</h4>
<p
className="text-sm"
style={{ color: 'var(--text-secondary)' }}
>
{card.set_name}
</p>
<div className="flex items-center space-x-3 mt-1"> <div className="flex items-center space-x-3 mt-1">
{card.mana_cost && ( {card.mana_cost && (
<div className="bg-bg-secondary px-2 py-1 rounded"> <div
<ManaCost cost={card.mana_cost} size="sm" useSVG={manaSymbolSettings.useSVG} /> className="px-2 py-1 rounded"
style={{ backgroundColor: 'var(--bg-secondary)' }}
>
<ManaCost
cost={card.mana_cost}
size="sm"
useSVG={manaSymbolSettings.useSVG}
/>
</div> </div>
)} )}
{card.rarity && ( {card.rarity && (
<span className={`text-xs px-2 py-1 rounded capitalize ${ <span
card.rarity === 'mythic' ? 'bg-orange-100 text-orange-800' : className="text-xs px-2 py-1 rounded capitalize"
card.rarity === 'rare' ? 'bg-yellow-100 text-yellow-800' : style={rarityBadgeStyle(card.rarity)}
card.rarity === 'uncommon' ? 'bg-gray-100 text-gray-800' : >
'bg-green-100 text-green-800'
}`}>
{card.rarity} {card.rarity}
</span> </span>
)} )}
@ -61,17 +106,30 @@ export default function DeckBuilderDeckList({
</div> </div>
</div> </div>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<span className="text-text-primary font-bold text-lg">{card.quantity}x</span> <span
className="font-bold text-lg"
style={{ color: 'var(--text-primary)' }}
>
{card.quantity}x
</span>
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<button <button
onClick={() => onRemoveCard(card.card_id, 1)} onClick={() => onRemoveCard(card.card_id, 1)}
className="w-8 h-8 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors flex items-center justify-center font-bold" className="w-8 h-8 rounded-full transition-colors flex items-center justify-center font-bold text-white"
style={{ backgroundColor: '#dc2626' }}
aria-label="Remove one"
> >
</button> </button>
<button <button
onClick={() => onAddCard(card, 1)} onClick={() => onAddCard(card, 1)}
className="w-8 h-8 bg-accent-ember text-white rounded-full hover:bg-accent-ember-dark transition-colors flex items-center justify-center font-bold" className="w-8 h-8 rounded-full transition-transform hover:scale-105 active:scale-95 flex items-center justify-center font-bold text-white"
style={{
background:
'linear-gradient(135deg, var(--accent-ember) 0%, var(--accent-flame) 100%)',
boxShadow: 'var(--rim-light-inner), var(--ember-rim-subtle)',
}}
aria-label="Add one"
> >
+ +
</button> </button>

View file

@ -4,26 +4,41 @@
*/ */
export default function DeckBuilderStatsBar({ stats }) { export default function DeckBuilderStatsBar({ stats }) {
return ( return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6 p-4 bg-bg-primary rounded-lg"> <div
className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6 p-4 rounded-xl"
style={{ backgroundColor: 'var(--bg-primary)' }}
>
<div className="text-center"> <div className="text-center">
<div className="text-lg font-bold text-text-primary">{stats.totalCards}/100</div> <div className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>
<div className="text-text-secondary text-sm">Cards</div> {stats.totalCards}/100
</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Cards
</div>
</div> </div>
<div className="text-center"> <div className="text-center">
<div className="text-lg font-bold text-text-primary">{stats.avgCmc}</div> <div className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>
<div className="text-text-secondary text-sm">Avg CMC</div> {stats.avgCmc}
</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Avg CMC
</div>
</div> </div>
<div className="text-center"> <div className="text-center">
<div className="text-lg font-bold text-text-primary"> <div className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>
{Object.keys(stats.colorCounts).length} {Object.keys(stats.colorCounts).length}
</div> </div>
<div className="text-text-secondary text-sm">Colors</div> <div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Colors
</div>
</div> </div>
<div className="text-center"> <div className="text-center">
<div className="text-lg font-bold text-text-primary"> <div className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>
{Object.keys(stats.typeCounts).length} {Object.keys(stats.typeCounts).length}
</div> </div>
<div className="text-text-secondary text-sm">Types</div> <div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Types
</div>
</div> </div>
</div> </div>
); );

View file

@ -2,8 +2,16 @@ import Link from 'next/link';
import DeckBuilderCardBrowser from './DeckBuilderCardBrowser'; import DeckBuilderCardBrowser from './DeckBuilderCardBrowser';
import DeckBuilderDeckList from './DeckBuilderDeckList'; import DeckBuilderDeckList from './DeckBuilderDeckList';
import DeckBuilderStatsBar from './DeckBuilderStatsBar'; import DeckBuilderStatsBar from './DeckBuilderStatsBar';
import { Button } from './ui';
import { computeDeckStats } from '../lib/deck-builder-stats'; import { computeDeckStats } from '../lib/deck-builder-stats';
/* 2026-06-04 design-sweep pass: replaced broken Tailwind token
classes (bg-bg-secondary, text-text-primary, bg-accent-ember, etc.
none of which are defined in tailwind.config.js and produced
zero CSS) with inline style={{ ... CSS vars ... }} + the Button
primitive + `glass-panel` + `rounded-2xl` so this view actually
renders with the Liquid Glass design system. */
export default function DeckBuilderView({ export default function DeckBuilderView({
addCardToDeck, addCardToDeck,
clearFilters, clearFilters,
@ -37,46 +45,46 @@ export default function DeckBuilderView({
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<div> <div>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<Link href="/decks" className="text-accent-ember hover:underline"> <Link
href="/decks"
className="hover:underline"
style={{ color: 'var(--accent-ember)' }}
>
Back to Decks Back to Decks
</Link> </Link>
</div> </div>
<h1 className="text-3xl font-bold text-text-primary mt-2">{deck.name}</h1> <h1 className="text-3xl font-bold mt-2" style={{ color: 'var(--text-primary)' }}>
<p className="text-text-secondary"> {deck.name}
</h1>
<p style={{ color: 'var(--text-secondary)' }}>
{deck.format} {stats.totalCards}/100 cards {deck.format} {stats.totalCards}/100 cards
</p> </p>
</div> </div>
<div className="flex space-x-3"> <div className="flex space-x-3">
<button <Button type="button" variant="secondary">
type="button"
className="bg-bg-secondary text-text-primary px-4 py-2 rounded-lg hover:bg-bg-tertiary transition-colors"
>
Save Deck Save Deck
</button> </Button>
<Link <Link href={`/deck/${deck.id}`}>
href={`/deck/${deck.id}`} <Button variant="primary">View Deck</Button>
className="bg-accent-ember text-white px-4 py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
>
View Deck
</Link> </Link>
</div> </div>
</div> </div>
<div className="flex gap-6 h-[calc(100vh-12rem)]"> <div className="flex gap-6 h-[calc(100vh-12rem)]">
<div className="flex-1 transition-all duration-300"> <div className="flex-1 transition-all duration-300">
<div className="bg-bg-secondary rounded-lg p-6 h-full flex flex-col"> <div className="glass-panel rounded-2xl p-6 h-full flex flex-col">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold text-text-primary"> <h2 className="text-xl font-semibold" style={{ color: 'var(--text-primary)' }}>
Deck Cards ({stats.totalCards}) Deck Cards ({stats.totalCards})
</h2> </h2>
<button <Button
type="button" type="button"
variant="primary"
onClick={() => setSidebarOpen(!sidebarOpen)} onClick={() => setSidebarOpen(!sidebarOpen)}
className="bg-accent-ember text-white px-4 py-2 rounded-lg hover:bg-accent-ember-dark transition-colors flex items-center space-x-2" trailingIcon={<span aria-hidden="true">{sidebarOpen ? '→' : '←'}</span>}
> >
<span>{sidebarOpen ? 'Hide' : 'Show'} Browser</span> {sidebarOpen ? 'Hide' : 'Show'} Browser
<span>{sidebarOpen ? '→' : '←'}</span> </Button>
</button>
</div> </div>
<DeckBuilderStatsBar stats={stats} /> <DeckBuilderStatsBar stats={stats} />

View file

@ -99,11 +99,7 @@ function UserProfileDropdown({ user, onMobileMenuClose }) {
{profileMenuItems.map((item) => ( {profileMenuItems.map((item) => (
<Link key={item.name} href={item.href}> <Link key={item.name} href={item.href}>
<div <div
className={` className="flex items-center px-4 py-3 text-sm cursor-pointer nav-item-hover"
flex items-center px-4 py-3 text-sm transition-all duration-200 cursor-pointer
hover:bg-gray-50 dark:hover:bg-gray-700
${item.isLogout ? 'text-red-600 dark:text-red-400' : ''}
`}
style={{ style={{
color: item.isLogout ? 'var(--accent-ember)' : 'var(--text-primary)' color: item.isLogout ? 'var(--accent-ember)' : 'var(--text-primary)'
}} }}

View file

@ -31,18 +31,27 @@ export default function ManaSymbolSettings({ onSettingsChange }) {
}; };
return ( return (
<div className="flex items-center space-x-3 p-3 bg-bg-secondary rounded-lg"> <div
className="flex items-center space-x-3 p-3 rounded-xl"
style={{ backgroundColor: 'var(--bg-secondary)' }}
>
<div className="flex-1"> <div className="flex-1">
<h4 className="text-sm font-medium text-text-primary">Mana Symbol Style</h4> <h4
<p className="text-xs text-text-secondary"> className="text-sm font-medium"
style={{ color: 'var(--text-primary)' }}
>
Mana Symbol Style
</h4>
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
{useSVG ? 'Using official Scryfall SVG symbols' : 'Using custom circular symbols'} {useSVG ? 'Using official Scryfall SVG symbols' : 'Using custom circular symbols'}
</p> </p>
</div> </div>
<button <button
onClick={handleToggle} onClick={handleToggle}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${ className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
useSVG ? 'bg-accent-ember' : 'bg-gray-300' style={{
}`} backgroundColor: useSVG ? 'var(--accent-ember)' : 'var(--bg-tertiary)',
}}
> >
<span <span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${ className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${

View file

@ -212,7 +212,7 @@ export function AdvancedManaCost({ cost, showAnalysis = false, useSVG = false })
<div className="space-y-2"> <div className="space-y-2">
<ManaCost cost={analysis.cost} useSVG={useSVG} /> <ManaCost cost={analysis.cost} useSVG={useSVG} />
{showAnalysis && ( {showAnalysis && (
<div className="text-xs text-text-secondary space-y-1"> <div className="text-xs space-y-1" style={{ color: 'var(--text-secondary)' }}>
<div>CMC: {analysis.cmc}</div> <div>CMC: {analysis.cmc}</div>
{analysis.colors.length > 0 && ( {analysis.colors.length > 0 && (
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">

View file

@ -184,21 +184,33 @@ export default function ShareModal({
{/* Search Results */} {/* Search Results */}
{searchResults.length > 0 && ( {searchResults.length > 0 && (
<div className="mt-2 border border-gray-200 rounded-lg max-h-40 overflow-y-auto"> <div
className="mt-2 rounded-xl max-h-40 overflow-y-auto glass-panel"
>
{searchResults.map((user) => ( {searchResults.map((user) => (
<div <div
key={user.id} key={user.id}
onClick={() => handleInvite(user)} onClick={() => handleInvite(user)}
className="flex items-center p-3 hover:bg-gray-50 cursor-pointer" className="flex items-center p-3 cursor-pointer nav-item-hover"
>
<div
className="w-8 h-8 rounded-full flex items-center justify-center mr-3"
style={{
background:
'linear-gradient(135deg, var(--accent-ember) 0%, var(--accent-flame) 100%)',
}}
> >
<div className="w-8 h-8 bg-purple-600 rounded-full flex items-center justify-center mr-3">
<span className="text-white text-sm font-bold"> <span className="text-white text-sm font-bold">
{user.email.charAt(0).toUpperCase()} {user.email.charAt(0).toUpperCase()}
</span> </span>
</div> </div>
<div> <div>
<div className="font-medium text-gray-900">{user.email}</div> <div className="font-medium" style={{ color: 'var(--text-primary)' }}>
<div className="text-sm text-gray-500">Click to invite as viewer</div> {user.email}
</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Click to invite as viewer
</div>
</div> </div>
</div> </div>
))} ))}
@ -207,19 +219,32 @@ export default function ShareModal({
{/* Email invite option */} {/* Email invite option */}
{searchQuery && isValidEmail(searchQuery) && !searchResults.some(u => u.email === searchQuery) && ( {searchQuery && isValidEmail(searchQuery) && !searchResults.some(u => u.email === searchQuery) && (
<div className="mt-2 border border-gray-200 rounded-lg"> <div className="mt-2 rounded-xl glass-panel">
<div <div
onClick={() => handleInvite(searchQuery)} onClick={() => handleInvite(searchQuery)}
className="flex items-center p-3 hover:bg-gray-50 cursor-pointer" className="flex items-center p-3 cursor-pointer nav-item-hover"
>
<div
className="w-8 h-8 rounded-full flex items-center justify-center mr-3"
style={{ backgroundColor: 'var(--bg-tertiary)' }}
>
<svg
className="w-4 h-4"
style={{ color: 'var(--text-primary)' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
> >
<div className="w-8 h-8 bg-gray-400 rounded-full flex items-center justify-center mr-3">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
</svg> </svg>
</div> </div>
<div> <div>
<div className="font-medium text-gray-900">Invite {searchQuery}</div> <div className="font-medium" style={{ color: 'var(--text-primary)' }}>
<div className="text-sm text-gray-500">Send email invitation as viewer</div> Invite {searchQuery}
</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Send email invitation as viewer
</div>
</div> </div>
</div> </div>
</div> </div>
@ -310,7 +335,8 @@ export default function ShareModal({
<button <button
key={social.platform} key={social.platform}
onClick={() => handleSocialShare(social.platform)} onClick={() => handleSocialShare(social.platform)}
className="flex flex-col items-center p-3 rounded-lg border hover:bg-gray-50 transition-colors" className="flex flex-col items-center p-3 rounded-xl border nav-item-hover transition-colors"
style={{ borderColor: 'var(--border)' }}
> >
<div className="w-8 h-8 mb-2 text-gray-600"> <div className="w-8 h-8 mb-2 text-gray-600">
{social.icon === 'twitter' && ( {social.icon === 'twitter' && (

View file

@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import Layout from '../../components/Layout'; import Layout from '../../components/Layout';
import PermissionIndicator from '../../components/PermissionIndicator'; import PermissionIndicator from '../../components/PermissionIndicator';
import { Button } from '../../components/ui'; import { Button, SearchBar } from '../../components/ui';
import { useAuth } from '../../lib/use-auth'; import { useAuth } from '../../lib/use-auth';
import { VOCAB } from '../../lib/collection-vocabulary.js'; import { VOCAB } from '../../lib/collection-vocabulary.js';
@ -213,12 +213,11 @@ export default function CommunityCollections() {
{/* Search and Sort */} {/* Search and Sort */}
<div className="flex items-center justify-between mt-6"> <div className="flex items-center justify-between mt-6">
<div className="flex-1 max-w-md"> <div className="flex-1 max-w-md">
<input <SearchBar
type="text"
placeholder="Search lists..."
className="input-field w-full"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
onClear={() => setSearchQuery('')}
placeholder="Search lists…"
/> />
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
@ -311,10 +310,10 @@ export default function CommunityCollections() {
{collection.tags.slice(0, 2).map((tag, index) => ( {collection.tags.slice(0, 2).map((tag, index) => (
<span <span
key={index} key={index}
className="px-2 py-1 rounded-md text-xs font-medium" className="px-2 py-1 rounded-xl text-xs font-medium"
style={{ style={{
backgroundColor: 'var(--bg-tertiary)', backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-secondary)' color: 'var(--text-secondary)',
}} }}
> >
{tag} {tag}
@ -322,10 +321,10 @@ export default function CommunityCollections() {
))} ))}
{collection.tags.length > 2 && ( {collection.tags.length > 2 && (
<span <span
className="px-2 py-1 rounded-md text-xs font-medium" className="px-2 py-1 rounded-xl text-xs font-medium"
style={{ style={{
backgroundColor: 'var(--bg-tertiary)', backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-secondary)' color: 'var(--text-secondary)',
}} }}
> >
+{collection.tags.length - 2} +{collection.tags.length - 2}

View file

@ -27,7 +27,10 @@ export default function DeckBuilder() {
return ( return (
<Layout user={user}> <Layout user={user}>
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-accent-ember" /> <div
className="animate-spin rounded-full h-32 w-32 border-b-2"
style={{ borderColor: 'var(--accent-ember)' }}
/>
</div> </div>
</Layout> </Layout>
); );

View file

@ -4,9 +4,18 @@ import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import Layout from '../../components/Layout'; import Layout from '../../components/Layout';
import { ManaCost, ColorIdentity } from '../../components/ManaSymbols'; import { ManaCost, ColorIdentity } from '../../components/ManaSymbols';
import { Button } from '../../components/ui';
import { useAuth } from '../../lib/use-auth'; import { useAuth } from '../../lib/use-auth';
import { getColorIdentity } from '../../lib/mana-symbols'; import { getColorIdentity } from '../../lib/mana-symbols';
/* 2026-06-04 design-sweep pass: this page used the broken Tailwind
token classes (bg-bg-*, text-text-*, bg-accent-ember,
hover:bg-accent-ember-dark, border-accent-ember) that don't
resolve in tailwind.config.js. Sweep replaces them with inline
CSS variables + Button primitive + glass-panel surface + the
nav-item-active / nav-item-hover utilities so the page renders
with the Liquid Glass design system. */
export default function DeckDetail() { export default function DeckDetail() {
const { user } = useAuth(); const { user } = useAuth();
const router = useRouter(); const router = useRouter();
@ -136,7 +145,10 @@ export default function DeckDetail() {
return ( return (
<Layout user={user}> <Layout user={user}>
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-accent-ember"></div> <div
className="animate-spin rounded-full h-32 w-32 border-b-2"
style={{ borderColor: 'var(--accent-ember)' }}
/>
</div> </div>
</Layout> </Layout>
); );
@ -147,8 +159,17 @@ export default function DeckDetail() {
<Layout user={user}> <Layout user={user}>
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
<div className="text-center"> <div className="text-center">
<h1 className="text-2xl font-bold mb-4">Deck not found</h1> <h1
<Link href="/decks" className="text-accent-ember hover:underline"> className="text-2xl font-bold mb-4"
style={{ color: 'var(--text-primary)' }}
>
Deck not found
</h1>
<Link
href="/decks"
className="hover:underline"
style={{ color: 'var(--accent-ember)' }}
>
Back to Decks Back to Decks
</Link> </Link>
</div> </div>
@ -168,34 +189,51 @@ export default function DeckDetail() {
<div className="flex justify-between items-start mb-8"> <div className="flex justify-between items-start mb-8">
<div> <div>
<div className="flex items-center space-x-3 mb-2"> <div className="flex items-center space-x-3 mb-2">
<Link href="/decks" className="text-accent-ember hover:underline"> <Link
href="/decks"
className="hover:underline"
style={{ color: 'var(--accent-ember)' }}
>
Back to Decks Back to Decks
</Link> </Link>
</div> </div>
<div className="flex items-center space-x-3 mb-2"> <div className="flex items-center space-x-3 mb-2">
<span className="text-3xl">{getFormatIcon(deck.format)}</span> <span className="text-3xl">{getFormatIcon(deck.format)}</span>
<h1 className="text-3xl font-bold text-text-primary">{deck.name}</h1> <h1
className="text-3xl font-bold"
style={{ color: 'var(--text-primary)' }}
>
{deck.name}
</h1>
{deck.is_public && ( {deck.is_public && (
<span className="bg-green-100 text-green-800 px-2 py-1 rounded-full text-xs font-medium"> <span
className="px-2 py-1 rounded-full text-xs font-medium"
style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--accent-ember)',
}}
>
Public Public
</span> </span>
)} )}
</div> </div>
<p className="text-text-secondary mb-2"> <p className="mb-2" style={{ color: 'var(--text-secondary)' }}>
by {deck.creator_username} {deck.format} {stats.totalCards} cards by {deck.creator_username} {deck.format} {stats.totalCards} cards
</p> </p>
{deck.description && ( {deck.description && (
<p className="text-text-secondary max-w-2xl">{deck.description}</p> <p
className="max-w-2xl"
style={{ color: 'var(--text-secondary)' }}
>
{deck.description}
</p>
)} )}
</div> </div>
{isOwner && ( {isOwner && (
<div className="flex space-x-3"> <div className="flex space-x-3">
<Link <Link href={`/deck-builder?deck=${deck.id}`}>
href={`/deck-builder?deck=${deck.id}`} <Button variant="primary">Edit Deck</Button>
className="bg-accent-ember text-white px-4 py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
>
Edit Deck
</Link> </Link>
</div> </div>
)} )}
@ -204,28 +242,44 @@ export default function DeckDetail() {
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Stats Sidebar */} {/* Stats Sidebar */}
<div className="lg:col-span-1"> <div className="lg:col-span-1">
<div className="bg-bg-secondary rounded-lg p-6 mb-6"> <div className="glass-panel rounded-2xl p-6 mb-6">
<h3 className="text-lg font-semibold text-text-primary mb-4">Statistics</h3> <h3
className="text-lg font-semibold mb-4"
style={{ color: 'var(--text-primary)' }}
>
Statistics
</h3>
<div className="space-y-3"> <div className="space-y-3">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-text-secondary">Total Cards:</span> <span style={{ color: 'var(--text-secondary)' }}>Total Cards:</span>
<span className="text-text-primary font-medium">{stats.totalCards}</span> <span className="font-medium" style={{ color: 'var(--text-primary)' }}>
{stats.totalCards}
</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-text-secondary">Avg. CMC:</span> <span style={{ color: 'var(--text-secondary)' }}>Avg. CMC:</span>
<span className="text-text-primary font-medium">{stats.avgCmc}</span> <span className="font-medium" style={{ color: 'var(--text-primary)' }}>
{stats.avgCmc}
</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-text-secondary">Format:</span> <span style={{ color: 'var(--text-secondary)' }}>Format:</span>
<span className="text-text-primary font-medium">{deck.format}</span> <span className="font-medium" style={{ color: 'var(--text-primary)' }}>
{deck.format}
</span>
</div> </div>
</div> </div>
{/* Color Distribution */} {/* Color Distribution */}
{Object.keys(stats.colorCounts).length > 0 && ( {Object.keys(stats.colorCounts).length > 0 && (
<div className="mt-6"> <div className="mt-6">
<h4 className="text-text-secondary text-sm font-medium mb-3">Color Distribution</h4> <h4
className="text-sm font-medium mb-3"
style={{ color: 'var(--text-secondary)' }}
>
Color Distribution
</h4>
<div className="space-y-2"> <div className="space-y-2">
{Object.entries(stats.colorCounts) {Object.entries(stats.colorCounts)
.sort(([, a], [, b]) => b - a) .sort(([, a], [, b]) => b - a)
@ -233,9 +287,19 @@ export default function DeckDetail() {
<div key={color} className="flex justify-between items-center"> <div key={color} className="flex justify-between items-center">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span className="text-lg">{color}</span> <span className="text-lg">{color}</span>
<span className="text-text-secondary text-sm">{color}</span> <span
className="text-sm"
style={{ color: 'var(--text-secondary)' }}
>
{color}
</span>
</div> </div>
<span className="text-text-primary font-medium">{count}</span> <span
className="font-medium"
style={{ color: 'var(--text-primary)' }}
>
{count}
</span>
</div> </div>
))} ))}
</div> </div>
@ -245,15 +309,30 @@ export default function DeckDetail() {
{/* Type Distribution */} {/* Type Distribution */}
{Object.keys(stats.typeCounts).length > 0 && ( {Object.keys(stats.typeCounts).length > 0 && (
<div className="mt-6"> <div className="mt-6">
<h4 className="text-text-secondary text-sm font-medium mb-3">Card Types</h4> <h4
className="text-sm font-medium mb-3"
style={{ color: 'var(--text-secondary)' }}
>
Card Types
</h4>
<div className="space-y-2"> <div className="space-y-2">
{Object.entries(stats.typeCounts) {Object.entries(stats.typeCounts)
.sort(([, a], [, b]) => b - a) .sort(([, a], [, b]) => b - a)
.slice(0, 8) .slice(0, 8)
.map(([type, count]) => ( .map(([type, count]) => (
<div key={type} className="flex justify-between"> <div key={type} className="flex justify-between">
<span className="text-text-secondary text-sm">{type}</span> <span
<span className="text-text-primary font-medium">{count}</span> className="text-sm"
style={{ color: 'var(--text-secondary)' }}
>
{type}
</span>
<span
className="font-medium"
style={{ color: 'var(--text-primary)' }}
>
{count}
</span>
</div> </div>
))} ))}
</div> </div>
@ -262,22 +341,25 @@ export default function DeckDetail() {
</div> </div>
{/* Group By Controls */} {/* Group By Controls */}
<div className="bg-bg-secondary rounded-lg p-6"> <div className="glass-panel rounded-2xl p-6">
<h3 className="text-lg font-semibold text-text-primary mb-4">Group Cards By</h3> <h3
className="text-lg font-semibold mb-4"
style={{ color: 'var(--text-primary)' }}
>
Group Cards By
</h3>
<div className="space-y-2"> <div className="space-y-2">
{[ {[
{ value: 'type', label: 'Card Type' }, { value: 'type', label: 'Card Type' },
{ value: 'cmc', label: 'Mana Cost' }, { value: 'cmc', label: 'Mana Cost' },
{ value: 'color', label: 'Color' }, { value: 'color', label: 'Color' },
{ value: 'rarity', label: 'Rarity' } { value: 'rarity', label: 'Rarity' },
].map(option => ( ].map(option => (
<button <button
key={option.value} key={option.value}
onClick={() => setGroupBy(option.value)} onClick={() => setGroupBy(option.value)}
className={`w-full text-left px-3 py-2 rounded-lg transition-colors ${ className={`nav-item w-full text-left px-3 py-2 ${
groupBy === option.value groupBy === option.value ? 'nav-item-active' : 'nav-item-hover'
? 'bg-accent-ember text-white'
: 'text-text-secondary hover:bg-bg-tertiary'
}`} }`}
> >
{option.label} {option.label}
@ -289,12 +371,19 @@ export default function DeckDetail() {
{/* Card List */} {/* Card List */}
<div className="lg:col-span-3"> <div className="lg:col-span-3">
<div className="bg-bg-secondary rounded-lg p-6"> <div className="glass-panel rounded-2xl p-6">
{deck.cards && deck.cards.length === 0 ? ( {deck.cards && deck.cards.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<div className="text-6xl mb-4">🃏</div> <div className="text-6xl mb-4">🃏</div>
<h3 className="text-xl font-semibold text-text-primary mb-2">Empty Deck</h3> <h3
<p className="text-text-secondary">This deck doesn&apos;t have any cards yet</p> className="text-xl font-semibold mb-2"
style={{ color: 'var(--text-primary)' }}
>
Empty Deck
</h3>
<p style={{ color: 'var(--text-secondary)' }}>
This deck doesn&apos;t have any cards yet
</p>
</div> </div>
) : ( ) : (
<div className="space-y-6"> <div className="space-y-6">
@ -302,14 +391,24 @@ export default function DeckDetail() {
.sort(([a], [b]) => a.localeCompare(b)) .sort(([a], [b]) => a.localeCompare(b))
.map(([group, cards]) => ( .map(([group, cards]) => (
<div key={group}> <div key={group}>
<h3 className="text-lg font-semibold text-text-primary mb-3 border-b border-border pb-2"> <h3
className="text-lg font-semibold mb-3 border-b pb-2"
style={{
color: 'var(--text-primary)',
borderColor: 'var(--border)',
}}
>
{group} ({cards.reduce((sum, card) => sum + card.quantity, 0)}) {group} ({cards.reduce((sum, card) => sum + card.quantity, 0)})
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{cards {cards
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
.map((card) => ( .map((card) => (
<div key={`${card.card_id}-${card.id}`} className="flex items-center space-x-3 p-3 bg-bg-primary rounded-lg hover:bg-bg-tertiary transition-colors"> <div
key={`${card.card_id}-${card.id}`}
className="flex items-center space-x-3 p-3 rounded-xl transition-colors nav-item-hover"
style={{ backgroundColor: 'var(--bg-primary)' }}
>
{card.image_url && ( {card.image_url && (
<img <img
src={card.image_url} src={card.image_url}
@ -319,15 +418,35 @@ export default function DeckDetail() {
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="font-medium text-text-primary truncate">{card.name}</h4> <h4
<span className="text-text-primary font-medium ml-2">{card.quantity}x</span> className="font-medium truncate"
style={{ color: 'var(--text-primary)' }}
>
{card.name}
</h4>
<span
className="font-medium ml-2"
style={{ color: 'var(--text-primary)' }}
>
{card.quantity}x
</span>
</div> </div>
<p className="text-text-secondary text-sm">{card.set_name}</p> <p
<div className="flex items-center space-x-2 text-xs text-text-secondary"> className="text-sm"
style={{ color: 'var(--text-secondary)' }}
>
{card.set_name}
</p>
<div
className="flex items-center space-x-2 text-xs"
style={{ color: 'var(--text-secondary)' }}
>
{card.mana_cost && ( {card.mana_cost && (
<ManaCost cost={card.mana_cost} size="xs" /> <ManaCost cost={card.mana_cost} size="xs" />
)} )}
{card.rarity && <span className="capitalize">{card.rarity}</span>} {card.rarity && (
<span className="capitalize">{card.rarity}</span>
)}
</div> </div>
</div> </div>
</div> </div>

View file

@ -5,6 +5,17 @@ import Layout from '../components/Layout';
import { Modal, Input, Button } from '../components/ui'; import { Modal, Input, Button } from '../components/ui';
import { useAuth } from '../lib/use-auth'; import { useAuth } from '../lib/use-auth';
/* 2026-06-04 design-sweep pass: the Tailwind token classes that this
page relied on (bg-bg-secondary, text-text-primary, border-border,
bg-accent-ember, hover:bg-accent-ember-dark, focus:ring-accent-ember)
are NOT defined in tailwind.config.js they produced zero CSS,
leaving the page visually unstyled (transparent backgrounds, no
borders, no hover states). Sweep replaces those with inline
style={{ ... CSS vars ... }} and the Button / SearchBar primitives
so the page actually renders with the Liquid Glass + corner-border
design system. `rounded-lg` `rounded-xl` across the board to match
the system standard. */
export default function Decks() { export default function Decks() {
const { user } = useAuth(); const { user } = useAuth();
const router = useRouter(); const router = useRouter();
@ -16,7 +27,7 @@ export default function Decks() {
name: '', name: '',
description: '', description: '',
format: 'Commander', format: 'Commander',
is_public: false is_public: false,
}); });
const fetchDecks = async () => { const fetchDecks = async () => {
@ -24,8 +35,8 @@ export default function Decks() {
const token = localStorage.getItem('auth_token'); const token = localStorage.getItem('auth_token');
const response = await fetch('/api/decks', { const response = await fetch('/api/decks', {
headers: { headers: {
'Authorization': `Bearer ${token}` Authorization: `Bearer ${token}`,
} },
}); });
if (response.ok) { if (response.ok) {
@ -39,7 +50,7 @@ export default function Decks() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
} };
useEffect(() => { useEffect(() => {
if (user) { if (user) {
@ -48,20 +59,17 @@ export default function Decks() {
} }
}, [user]); }, [user]);
;
const handleCreateDeck = async (e) => { const handleCreateDeck = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
const token = localStorage.getItem('auth_token'); const token = localStorage.getItem('auth_token');
const response = await fetch('/api/decks', { const response = await fetch('/api/decks', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify(newDeck) body: JSON.stringify(newDeck),
}); });
if (response.ok) { if (response.ok) {
@ -69,8 +77,6 @@ export default function Decks() {
setDecks([createdDeck, ...decks]); setDecks([createdDeck, ...decks]);
setShowCreateModal(false); setShowCreateModal(false);
setNewDeck({ name: '', description: '', format: 'Commander', is_public: false }); setNewDeck({ name: '', description: '', format: 'Commander', is_public: false });
// Navigate to deck builder for the new deck
router.push(`/deck-builder?deck=${createdDeck.id}`); router.push(`/deck-builder?deck=${createdDeck.id}`);
} else { } else {
console.error('Failed to create deck'); console.error('Failed to create deck');
@ -82,21 +88,20 @@ export default function Decks() {
const handleEditDeck = async (e) => { const handleEditDeck = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
const token = localStorage.getItem('auth_token'); const token = localStorage.getItem('auth_token');
const response = await fetch(`/api/decks/${editingDeck.id}`, { const response = await fetch(`/api/decks/${editingDeck.id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify(editingDeck) body: JSON.stringify(editingDeck),
}); });
if (response.ok) { if (response.ok) {
const updatedDeck = await response.json(); const updatedDeck = await response.json();
setDecks(decks.map(deck => deck.id === updatedDeck.id ? updatedDeck : deck)); setDecks(decks.map((deck) => (deck.id === updatedDeck.id ? updatedDeck : deck)));
setEditingDeck(null); setEditingDeck(null);
} else { } else {
console.error('Failed to update deck'); console.error('Failed to update deck');
@ -110,18 +115,15 @@ export default function Decks() {
if (!confirm('Are you sure you want to delete this deck? This action cannot be undone.')) { if (!confirm('Are you sure you want to delete this deck? This action cannot be undone.')) {
return; return;
} }
try { try {
const token = localStorage.getItem('auth_token'); const token = localStorage.getItem('auth_token');
const response = await fetch(`/api/decks/${deckId}`, { const response = await fetch(`/api/decks/${deckId}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: { Authorization: `Bearer ${token}` },
'Authorization': `Bearer ${token}`
}
}); });
if (response.ok) { if (response.ok) {
setDecks(decks.filter(deck => deck.id !== deckId)); setDecks(decks.filter((deck) => deck.id !== deckId));
} else { } else {
console.error('Failed to delete deck'); console.error('Failed to delete deck');
} }
@ -132,41 +134,46 @@ export default function Decks() {
const getFormatIcon = (format) => { const getFormatIcon = (format) => {
switch (format) { switch (format) {
case 'Commander': case 'Commander': return '⚔️';
return '⚔️'; case 'Standard': return '🏆';
case 'Standard': case 'Modern': return '🔥';
return '🏆'; case 'Legacy': return '💎';
case 'Modern': default: return '🃏';
return '🔥';
case 'Legacy':
return '💎';
default:
return '🃏';
} }
}; };
const getFormatColor = (format) => { // Single neutral chip style for format badges — the prior
switch (format) { // getFormatColor() returned 5 stale Tailwind colour pairs
case 'Commander': // (bg-purple-100, bg-blue-100, etc.) that don't fit the
return 'bg-purple-100 text-purple-800'; // Deck Hearth palette and don't render in dark theme anyway.
case 'Standard': const formatBadgeStyle = {
return 'bg-blue-100 text-blue-800'; backgroundColor: 'var(--bg-tertiary)',
case 'Modern': color: 'var(--text-primary)',
return 'bg-red-100 text-red-800';
case 'Legacy':
return 'bg-yellow-100 text-yellow-800';
default:
return 'bg-gray-100 text-gray-800';
}
}; };
// Reusable inline-style for the .card surfaces used as stat
// cards + deck cards. We can't use the .glass-panel class +
// rounded-xl directly because we want the same gradient-border
// catch-light effect that the chrome chips have, and inline
// styles for that pattern are verbose. Easiest path: opt into
// the existing .glass-panel class for the multi-layer bg, then
// override border-radius via className. (.glass-panel doesn't
// set border-radius so we control it via Tailwind.)
const chipBgStyle = {};
// textarea / select use .input-field which is the Deck-Hearth
// standard input class (rounded-2xl + ember focus ring).
const inputFieldClass = 'input-field w-full';
if (!user) { if (!user) {
return ( return (
<Layout user={user}> <Layout user={user}>
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
<div className="text-center"> <div className="text-center">
<h1 className="text-2xl font-bold mb-4">Please log in to view your decks</h1> <h1 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
<Link href="/login" className="text-accent-ember hover:underline"> Please log in to view your decks
</h1>
<Link href="/login" style={{ color: 'var(--accent-ember)' }} className="hover:underline">
Go to Login Go to Login
</Link> </Link>
</div> </div>
@ -179,7 +186,10 @@ export default function Decks() {
return ( return (
<Layout user={user}> <Layout user={user}>
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-accent-ember"></div> <div
className="animate-spin rounded-full h-32 w-32 border-b-2"
style={{ borderColor: 'var(--accent-ember)' }}
/>
</div> </div>
</Layout> </Layout>
); );
@ -191,42 +201,43 @@ export default function Decks() {
{/* Header */} {/* Header */}
<div className="flex justify-between items-center mb-8"> <div className="flex justify-between items-center mb-8">
<div> <div>
<h1 className="text-3xl font-bold text-text-primary">My Decks</h1> <h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}>
<p className="text-text-secondary mt-2"> My Decks
</h1>
<p className="mt-2" style={{ color: 'var(--text-secondary)' }}>
Build and manage your MTG decks Build and manage your MTG decks
</p> </p>
</div> </div>
<button <Button variant="primary" onClick={() => setShowCreateModal(true)}>
onClick={() => setShowCreateModal(true)}
className="bg-accent-ember text-white px-6 py-3 rounded-lg hover:bg-accent-ember-dark transition-colors"
>
Create New Deck Create New Deck
</button> </Button>
</div> </div>
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8"> <div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-bg-secondary rounded-lg p-6"> <div className="glass-panel rounded-2xl p-6" style={chipBgStyle}>
<div className="text-2xl font-bold text-text-primary">{decks.length}</div> <div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
<div className="text-text-secondary">Total Decks</div> {decks.length}
</div> </div>
<div className="bg-bg-secondary rounded-lg p-6"> <div style={{ color: 'var(--text-secondary)' }}>Total Decks</div>
<div className="text-2xl font-bold text-text-primary">
{decks.filter(d => d.format === 'Commander').length}
</div> </div>
<div className="text-text-secondary">Commander</div> <div className="glass-panel rounded-2xl p-6">
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
{decks.filter((d) => d.format === 'Commander').length}
</div> </div>
<div className="bg-bg-secondary rounded-lg p-6"> <div style={{ color: 'var(--text-secondary)' }}>Commander</div>
<div className="text-2xl font-bold text-text-primary">
{decks.filter(d => d.is_public).length}
</div> </div>
<div className="text-text-secondary">Public</div> <div className="glass-panel rounded-2xl p-6">
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
{decks.filter((d) => d.is_public).length}
</div> </div>
<div className="bg-bg-secondary rounded-lg p-6"> <div style={{ color: 'var(--text-secondary)' }}>Public</div>
<div className="text-2xl font-bold text-text-primary"> </div>
<div className="glass-panel rounded-2xl p-6">
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
{decks.reduce((sum, deck) => sum + (deck.card_count || 0), 0)} {decks.reduce((sum, deck) => sum + (deck.card_count || 0), 0)}
</div> </div>
<div className="text-text-secondary">Total Cards</div> <div style={{ color: 'var(--text-secondary)' }}>Total Cards</div>
</div> </div>
</div> </div>
@ -234,65 +245,83 @@ export default function Decks() {
{decks.length === 0 ? ( {decks.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<div className="text-6xl mb-4">🃏</div> <div className="text-6xl mb-4">🃏</div>
<h3 className="text-xl font-semibold text-text-primary mb-2">No decks yet</h3> <h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
<p className="text-text-secondary mb-6">Create your first deck to get started</p> No decks yet
<button </h3>
onClick={() => setShowCreateModal(true)} <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
className="bg-accent-ember text-white px-6 py-3 rounded-lg hover:bg-accent-ember-dark transition-colors" Create your first deck to get started
> </p>
<Button variant="primary" onClick={() => setShowCreateModal(true)}>
Create Your First Deck Create Your First Deck
</button> </Button>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{decks.map((deck) => ( {decks.map((deck) => (
<div key={deck.id} className="bg-bg-secondary rounded-lg p-6 hover:shadow-lg transition-shadow"> <div key={deck.id} className="glass-panel rounded-2xl p-6 transition-shadow">
<div className="flex justify-between items-start mb-4"> <div className="flex justify-between items-start mb-4">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span className="text-2xl">{getFormatIcon(deck.format)}</span> <span className="text-2xl">{getFormatIcon(deck.format)}</span>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getFormatColor(deck.format)}`}> <span
className="px-2 py-1 rounded-full text-xs font-medium"
style={formatBadgeStyle}
>
{deck.format} {deck.format}
</span> </span>
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
<button <button
onClick={() => setEditingDeck({ ...deck })} onClick={() => setEditingDeck({ ...deck })}
className="text-text-secondary hover:text-accent-ember transition-colors" className="transition-colors"
style={{ color: 'var(--text-secondary)' }}
aria-label="Edit deck"
> >
</button> </button>
<button <button
onClick={() => handleDeleteDeck(deck.id)} onClick={() => handleDeleteDeck(deck.id)}
className="text-text-secondary hover:text-red-500 transition-colors" className="transition-colors"
style={{ color: 'var(--text-secondary)' }}
aria-label="Delete deck"
> >
🗑 🗑
</button> </button>
</div> </div>
</div> </div>
<h3 className="text-xl font-bold text-text-primary mb-2">{deck.name}</h3> <h3 className="text-xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
{deck.name}
</h3>
{deck.description && ( {deck.description && (
<p className="text-text-secondary text-sm mb-4 line-clamp-2">{deck.description}</p> <p
className="text-sm mb-4 line-clamp-2"
style={{ color: 'var(--text-secondary)' }}
>
{deck.description}
</p>
)} )}
<div className="flex justify-between items-center text-sm text-text-secondary mb-4"> <div
className="flex justify-between items-center text-sm mb-4"
style={{ color: 'var(--text-secondary)' }}
>
<span>{deck.card_count || 0} cards</span> <span>{deck.card_count || 0} cards</span>
{deck.is_public && <span className="text-green-600">Public</span>} {deck.is_public && (
<span style={{ color: 'var(--accent-ember)' }}>Public</span>
)}
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
<Link <Link href={`/deck-builder?deck=${deck.id}`} className="flex-1">
href={`/deck-builder?deck=${deck.id}`} <Button variant="primary" size="md" className="w-full">
className="flex-1 bg-accent-ember text-white text-center py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
>
Edit Deck Edit Deck
</Button>
</Link> </Link>
<Link <Link href={`/deck/${deck.id}`} className="flex-1">
href={`/deck/${deck.id}`} <Button variant="secondary" size="md" className="w-full">
className="flex-1 bg-bg-tertiary text-text-primary text-center py-2 rounded-lg hover:bg-bg-primary transition-colors"
>
View View
</Button>
</Link> </Link>
</div> </div>
</div> </div>
@ -326,7 +355,7 @@ export default function Decks() {
id="create-deck-format" id="create-deck-format"
value={newDeck.format} value={newDeck.format}
onChange={(e) => setNewDeck({ ...newDeck, format: e.target.value })} onChange={(e) => setNewDeck({ ...newDeck, format: e.target.value })}
className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary" className={inputFieldClass}
> >
<option value="Commander">Commander</option> <option value="Commander">Commander</option>
<option value="Standard">Standard</option> <option value="Standard">Standard</option>
@ -346,7 +375,7 @@ export default function Decks() {
id="create-deck-description" id="create-deck-description"
value={newDeck.description} value={newDeck.description}
onChange={(e) => setNewDeck({ ...newDeck, description: e.target.value })} onChange={(e) => setNewDeck({ ...newDeck, description: e.target.value })}
className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary" className={inputFieldClass}
rows="3" rows="3"
placeholder="Describe your deck strategy..." placeholder="Describe your deck strategy..."
/> />
@ -391,9 +420,7 @@ export default function Decks() {
label="Deck Name *" label="Deck Name *"
required required
value={editingDeck.name} value={editingDeck.name}
onChange={(e) => onChange={(e) => setEditingDeck({ ...editingDeck, name: e.target.value })}
setEditingDeck({ ...editingDeck, name: e.target.value })
}
/> />
<div> <div>
<label <label
@ -406,10 +433,8 @@ export default function Decks() {
<select <select
id="edit-deck-format" id="edit-deck-format"
value={editingDeck.format} value={editingDeck.format}
onChange={(e) => onChange={(e) => setEditingDeck({ ...editingDeck, format: e.target.value })}
setEditingDeck({ ...editingDeck, format: e.target.value }) className={inputFieldClass}
}
className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary"
> >
<option value="Commander">Commander</option> <option value="Commander">Commander</option>
<option value="Standard">Standard</option> <option value="Standard">Standard</option>
@ -428,10 +453,8 @@ export default function Decks() {
<textarea <textarea
id="edit-deck-description" id="edit-deck-description"
value={editingDeck.description || ''} value={editingDeck.description || ''}
onChange={(e) => onChange={(e) => setEditingDeck({ ...editingDeck, description: e.target.value })}
setEditingDeck({ ...editingDeck, description: e.target.value }) className={inputFieldClass}
}
className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary"
rows="3" rows="3"
/> />
</div> </div>
@ -439,9 +462,7 @@ export default function Decks() {
<input <input
type="checkbox" type="checkbox"
checked={editingDeck.is_public} checked={editingDeck.is_public}
onChange={(e) => onChange={(e) => setEditingDeck({ ...editingDeck, is_public: e.target.checked })}
setEditingDeck({ ...editingDeck, is_public: e.target.checked })
}
className="mr-2" className="mr-2"
style={{ accentColor: 'var(--accent-ember)' }} style={{ accentColor: 'var(--accent-ember)' }}
/> />

View file

@ -7,7 +7,7 @@ import BulkSelectionToolbar from '../components/BulkSelectionToolbar';
import CollectionSelectionModal from '../components/CollectionSelectionModal'; import CollectionSelectionModal from '../components/CollectionSelectionModal';
import { ManaCost, ColorFilterSymbol } from '../components/ManaSymbols'; import { ManaCost, ColorFilterSymbol } from '../components/ManaSymbols';
import ManaSymbolSettings from '../components/ManaSymbolSettings'; import ManaSymbolSettings from '../components/ManaSymbolSettings';
import { Button } from '../components/ui'; import { Button, SearchBar } from '../components/ui';
import { useAuth } from '../lib/use-auth'; import { useAuth } from '../lib/use-auth';
import { VOCAB } from '../lib/collection-vocabulary.js'; import { VOCAB } from '../lib/collection-vocabulary.js';
@ -310,30 +310,22 @@ export default function MyCards() {
</div> </div>
{/* Filters */} {/* Filters */}
<div className="p-4 sm:p-6" style={{ backgroundColor: 'var(--bg-primary)' }}> <div className="p-4 sm:p-6">
<div className="flex flex-wrap gap-4 mb-6"> <div className="glass-panel rounded-2xl p-4 mb-6">
<input <div className="flex flex-wrap gap-4">
type="text" <div className="flex-1 min-w-[200px]">
placeholder="Search your cards..." <SearchBar
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="flex-1 min-w-[200px] px-4 py-2 rounded-xl border" onClear={() => setSearchQuery('')}
style={{ placeholder="Search your cards…"
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
/> />
</div>
<select <select
value={selectedTCG} value={selectedTCG}
onChange={(e) => setSelectedTCG(e.target.value)} onChange={(e) => setSelectedTCG(e.target.value)}
className="px-4 py-2 rounded-xl border" className="input-field"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
> >
<option value="all">All Games</option> <option value="all">All Games</option>
{filters.games.map(game => ( {filters.games.map(game => (
@ -344,12 +336,7 @@ export default function MyCards() {
<select <select
value={selectedRarity} value={selectedRarity}
onChange={(e) => setSelectedRarity(e.target.value)} onChange={(e) => setSelectedRarity(e.target.value)}
className="px-4 py-2 rounded-xl border" className="input-field"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
> >
<option value="all">All Rarities</option> <option value="all">All Rarities</option>
{filters.rarities.map(rarity => ( {filters.rarities.map(rarity => (
@ -357,6 +344,7 @@ export default function MyCards() {
))} ))}
</select> </select>
</div> </div>
</div>
{/* Results Summary */} {/* Results Summary */}
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">

View file

@ -275,11 +275,15 @@ export default function Profile() {
{/* Message */} {/* Message */}
{message.text && ( {message.text && (
<div className={`mb-6 p-4 rounded-xl ${ <div
message.type === 'success' className="mb-6 p-4 rounded-xl glass-panel"
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-800' style={{
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-800' color:
}`}> message.type === 'success' ? 'var(--accent-flame)' : '#dc2626',
borderColor:
message.type === 'success' ? 'var(--accent-flame)' : '#dc2626',
}}
>
{message.text} {message.text}
</div> </div>
)} )}

View file

@ -251,11 +251,15 @@ export default function Settings() {
{/* Message */} {/* Message */}
{message.text && ( {message.text && (
<div className={`mb-6 p-4 rounded-xl ${ <div
message.type === 'success' className="mb-6 p-4 rounded-xl glass-panel"
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-800' style={{
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-800' color:
}`}> message.type === 'success' ? 'var(--accent-flame)' : '#dc2626',
borderColor:
message.type === 'success' ? 'var(--accent-flame)' : '#dc2626',
}}
>
{message.text} {message.text}
</div> </div>
)} )}
@ -269,13 +273,9 @@ export default function Settings() {
<button <button
key={section.id} key={section.id}
onClick={() => setActiveSection(section.id)} onClick={() => setActiveSection(section.id)}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl font-medium transition-all duration-200 text-left ${ className={`nav-item w-full flex items-center gap-3 px-4 py-3 font-medium text-left ${
activeSection === section.id ? 'shadow-lg' : 'hover:shadow-md' activeSection === section.id ? 'nav-item-active' : 'nav-item-hover'
}`} }`}
style={{
backgroundColor: activeSection === section.id ? 'var(--accent-ember)' : 'transparent',
color: activeSection === section.id ? 'white' : 'var(--text-primary)'
}}
> >
<span>{section.icon}</span> <span>{section.icon}</span>
{section.name} {section.name}