refactor(design): site-wide sweep — broken Tailwind tokens, rounded corners, SearchBar primitive (#117) (#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:
varutasu 2026-06-04 14:06:22 -05:00 committed by GitHub
parent c57b5d0406
commit 22364de8c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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,38 +242,64 @@ 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)
.map(([color, count]) => ( .map(([color, count]) => (
<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}