Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 29 additions & 23 deletions src/components/BookmarkPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { motion } from 'motion/react';
import { useAuth } from '../contexts/AuthContext';
import { fetchBookmarks, removeBookmark, toggleBookmarkRead, type WebBookmark } from '../services/bookmarkService';
import GradientText from './GradientText';
import GlassIconButton from './GlassIconButton';

type ViewMode = 'cards' | 'compact';

Expand Down Expand Up @@ -124,35 +125,40 @@ export function BookmarkPanel({ selectedBookmarkId, selectedFolder, bookmarkFold
)}
</div>
<div className="bookmark-panel-actions">
<button
className={`bookmark-filter-btn ${filter === 'all' ? 'active' : ''}`}
<GlassIconButton
color="blue"
icon="✱"
title="Tous les bookmarks"
onClick={() => setFilter('all')}
>
Tous
</button>
<button
className={`bookmark-filter-btn ${filter === 'unread' ? 'active' : ''}`}
active={filter === 'all'}
/>
<GlassIconButton
color="indigo"
icon="○"
title="Non lus uniquement"
onClick={() => setFilter('unread')}
>
Non lus
</button>
<button
className={`bookmark-view-btn ${viewMode === 'cards' ? 'active' : ''}`}
active={filter === 'unread'}
/>
<GlassIconButton
color="orange"
icon="▦"
title="Vue cartes"
onClick={() => setViewMode('cards')}
>
</button>
<button
className={`bookmark-view-btn ${viewMode === 'compact' ? 'active' : ''}`}
active={viewMode === 'cards'}
/>
<GlassIconButton
color="green"
icon="☰"
title="Vue compacte"
onClick={() => setViewMode('compact')}
>
</button>
<button className="bookmark-refresh-btn" onClick={load} title="Actualiser">
</button>
active={viewMode === 'compact'}
/>
<GlassIconButton
color="purple"
icon="↻"
title="Actualiser"
onClick={load}
/>
</div>
</div>

Expand Down
70 changes: 38 additions & 32 deletions src/components/FeedPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { summarizeDigest } from '../services/llmService';
import { translateText, getTranslationConfig, saveTranslationConfig } from '../services/translationService';
import GradientText from './GradientText';
import SpotlightCard from './SpotlightCard';
import GlassIconButton from './GlassIconButton';

const ITEMS_PER_PAGE_NORMAL = 10;
const ITEMS_PER_PAGE_COMPACT = 20;
Expand Down Expand Up @@ -346,50 +347,55 @@ export function FeedPanel({ categories, items, selectedFeedId, selectedSource, s
)}
</div>
<div className="feed-panel-actions">
<button
className={`feed-action-btn ${digestState === 'loading' ? 'loading' : ''}`}
<GlassIconButton
color="purple"
icon={digestState === 'loading' ? <span className="btn-spinner" /> : isPro ? '✦' : '🔒'}
title={isPro ? "Résumer l'actualité" : "Résumer (Pro)"}
onClick={isPro ? handleDigest : showUpgradeModal}
disabled={digestState === 'loading' || items.length === 0}
>
{digestState === 'loading' ? <span className="btn-spinner" /> : isPro ? '✦' : '🔒'}
</button>
<button
className={`feed-action-btn ${translateActive ? 'active' : ''}`}
/>
<GlassIconButton
color="blue"
icon={translateLoading ? <span className="btn-spinner" /> : '🌐'}
title={translateActive ? 'Voir les originaux' : 'Traduire la liste'}
onClick={handleTranslateList}
disabled={translateLoading || items.length === 0}
>
{translateLoading ? <span className="btn-spinner" /> : '🌐'}
</button>
<button
className="feed-action-btn"
active={translateActive}
/>
<GlassIconButton
color="green"
icon={
<svg width="12" height="12" viewBox="0 0 12 12">
{unreadCount > 0 ? (
<circle cx="6" cy="6" r="4.5" fill="none" stroke="currentColor" strokeWidth="1.4" />
) : (
<circle cx="6" cy="6" r="4.5" fill="currentColor" stroke="currentColor" strokeWidth="1.4" />
)}
</svg>
}
title={unreadCount > 0 ? 'Tout marquer comme lu' : 'Tout marquer comme non lu'}
onClick={unreadCount > 0 ? onMarkAllAsRead : onMarkAllAsUnread}
>
<svg width="12" height="12" viewBox="0 0 12 12">
{unreadCount > 0 ? (
<circle cx="6" cy="6" r="4.5" fill="none" stroke="currentColor" strokeWidth="1.4" />
) : (
<circle cx="6" cy="6" r="4.5" fill="currentColor" stroke="currentColor" strokeWidth="1.4" />
)}
</svg>
</button>
<button
className={`feed-action-btn ${viewMode === 'cards' ? 'active' : ''}`}
/>
<GlassIconButton
color="orange"
icon="▦"
title="Vue cartes"
onClick={() => setViewMode(viewMode === 'cards' ? 'normal' : 'cards')}
>
</button>
<button
className={`feed-action-btn ${compact ? 'active' : ''}`}
active={viewMode === 'cards'}
/>
<GlassIconButton
color="indigo"
icon={compact ? '' : '≡'}
title={compact ? 'Vue normale' : 'Vue compacte'}
onClick={() => setViewMode(viewMode === 'compact' ? 'normal' : 'compact')}
>
{compact ? '☰' : '≡'}
</button>
<button className="panel-close-btn" title="Fermer le panneau (2)" onClick={onClose}>✕</button>
active={compact}
/>
<GlassIconButton
color="red"
icon="✕"
title="Fermer le panneau (2)"
onClick={onClose}
/>
</div>
</div>

Expand Down
37 changes: 37 additions & 0 deletions src/components/GlassIconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ReactNode, ButtonHTMLAttributes } from 'react';

const gradientMapping: Record<string, string> = {
blue: 'linear-gradient(hsl(223, 90%, 50%), hsl(208, 90%, 50%))',
purple: 'linear-gradient(hsl(283, 90%, 50%), hsl(268, 90%, 50%))',
red: 'linear-gradient(hsl(3, 90%, 50%), hsl(348, 90%, 50%))',
indigo: 'linear-gradient(hsl(253, 90%, 50%), hsl(238, 90%, 50%))',
orange: 'linear-gradient(hsl(43, 90%, 50%), hsl(28, 90%, 50%))',
green: 'linear-gradient(hsl(123, 90%, 40%), hsl(108, 90%, 40%))',
};

interface GlassIconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
color: string;
icon: ReactNode;
active?: boolean;
}

export default function GlassIconButton({ color, icon, active, className, ...props }: GlassIconButtonProps) {
const bg = gradientMapping[color] || color;

return (
<button
type="button"
{...props}
className={`glass-icon-btn group ${active ? 'active' : ''} ${className || ''}`}
>
{/* Colored background — tilted */}
<span className="glass-icon-bg" style={{ background: bg }} />
{/* Frosted glass overlay */}
<span className="glass-icon-front">
<span className="glass-icon-inner" aria-hidden="true">
{icon}
</span>
</span>
</button>
);
}
71 changes: 71 additions & 0 deletions src/components/GlassIcons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';

export interface GlassIconsItem {
icon: React.ReactElement;
color: string;
label: string;
customClass?: string;
}

export interface GlassIconsProps {
items: GlassIconsItem[];
className?: string;
}

const gradientMapping: Record<string, string> = {
blue: 'linear-gradient(hsl(223, 90%, 50%), hsl(208, 90%, 50%))',
purple: 'linear-gradient(hsl(283, 90%, 50%), hsl(268, 90%, 50%))',
red: 'linear-gradient(hsl(3, 90%, 50%), hsl(348, 90%, 50%))',
indigo: 'linear-gradient(hsl(253, 90%, 50%), hsl(238, 90%, 50%))',
orange: 'linear-gradient(hsl(43, 90%, 50%), hsl(28, 90%, 50%))',
green: 'linear-gradient(hsl(123, 90%, 40%), hsl(108, 90%, 40%))'
};

const GlassIcons: React.FC<GlassIconsProps> = ({ items, className }) => {
const getBackgroundStyle = (color: string): React.CSSProperties => {
if (gradientMapping[color]) {
return { background: gradientMapping[color] };
}
return { background: color };
};

return (
<div className={`grid gap-[5em] grid-cols-2 md:grid-cols-3 mx-auto py-[3em] overflow-visible ${className || ''}`}>
{items.map((item, index) => (
<button
key={index}
type="button"
aria-label={item.label}
className={`relative bg-transparent outline-none border-none cursor-pointer w-[4.5em] h-[4.5em] [perspective:24em] [transform-style:preserve-3d] [-webkit-tap-highlight-color:transparent] group ${
item.customClass || ''
}`}
>
<span
className="absolute top-0 left-0 w-full h-full rounded-[1.25em] block transition-[opacity,transform] duration-300 ease-[cubic-bezier(0.83,0,0.17,1)] origin-[100%_100%] rotate-[15deg] [will-change:transform] group-hover:[transform:rotate(25deg)_translate3d(-0.5em,-0.5em,0.5em)]"
style={{
...getBackgroundStyle(item.color),
boxShadow: '0.5em -0.5em 0.75em hsla(223, 10%, 10%, 0.15)'
}}
></span>

<span
className="absolute top-0 left-0 w-full h-full rounded-[1.25em] bg-[hsla(0,0%,100%,0.15)] transition-[opacity,transform] duration-300 ease-[cubic-bezier(0.83,0,0.17,1)] origin-[80%_50%] flex backdrop-blur-[0.75em] [-webkit-backdrop-filter:blur(0.75em)] [-moz-backdrop-filter:blur(0.75em)] [will-change:transform] transform group-hover:[transform:translate3d(0,0,2em)]"
style={{
boxShadow: '0 0 0 0.1em hsla(0, 0%, 100%, 0.3) inset'
}}
>
<span className="m-auto w-[1.5em] h-[1.5em] flex items-center justify-center" aria-hidden="true">
{item.icon}
</span>
</span>

<span className="absolute top-full left-0 right-0 text-center whitespace-nowrap leading-[2] text-base opacity-0 transition-[opacity,transform] duration-300 ease-[cubic-bezier(0.83,0,0.17,1)] translate-y-0 group-hover:opacity-100 group-hover:[transform:translateY(20%)]">
{item.label}
</span>
</button>
))}
</div>
);
};

export default GlassIcons;
21 changes: 11 additions & 10 deletions src/components/NotePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useCallback } from 'react';
import { motion } from 'motion/react';
import Markdown from 'react-markdown';
import GradientText from './GradientText';
import GlassIconButton from './GlassIconButton';
import { NoteStickyBoard } from './NoteStickyBoard';

export interface Note {
Expand Down Expand Up @@ -61,20 +62,20 @@ export function NotePanel({ notes, selectedNoteId, onSelectNote, onAddNote, onDe
)}
</div>
<div className="note-panel-actions">
<button
className={`feed-action-btn ${viewMode === 'cards' ? 'active' : ''}`}
<GlassIconButton
color="orange"
icon="▦"
title="Vue cartes"
onClick={() => setViewMode('cards')}
>
</button>
<button
className={`feed-action-btn ${viewMode === 'board' ? 'active' : ''}`}
active={viewMode === 'cards'}
/>
<GlassIconButton
color="indigo"
icon="▤"
title="Vue post-its"
onClick={() => setViewMode('board')}
>
</button>
active={viewMode === 'board'}
/>
</div>
</div>

Expand Down
23 changes: 12 additions & 11 deletions src/components/SourcePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { Note } from "./NotePanel";
import { PalettePicker } from "./PalettePicker";
import { getStoredPaletteId, getPaletteById } from "../themes/palettes";
import ShinyText from "./ShinyText";
import GlassIconButton from "./GlassIconButton";

interface SourcePanelProps {
categories: FeedCategory[];
Expand Down Expand Up @@ -883,25 +884,25 @@ export function SourcePanel({
{onBrandSwitch && (
<div className="mode-tab-bar">
{([
{ mode: 'flux' as const, icon: '◈', label: 'Flux', shortcut: '1', pro: false },
{ mode: 'bookmark' as const, icon: '🔖', label: 'Signets', shortcut: '2', pro: false },
{ mode: 'note' as const, icon: '📝', label: 'Notes', shortcut: '3', pro: false },
{ mode: 'editor' as const, icon: '✏️', label: 'Éditeur', shortcut: '4', pro: true },
{ mode: 'draw' as const, icon: '🎨', label: 'Dessin', shortcut: '5', pro: true },
{ mode: 'flux' as const, icon: '◈', label: 'Flux', shortcut: '1', pro: false, color: 'blue' },
{ mode: 'bookmark' as const, icon: '🔖', label: 'Signets', shortcut: '2', pro: false, color: 'green' },
{ mode: 'note' as const, icon: '📝', label: 'Notes', shortcut: '3', pro: false, color: 'orange' },
{ mode: 'editor' as const, icon: '✏️', label: 'Éditeur', shortcut: '4', pro: true, color: 'purple' },
{ mode: 'draw' as const, icon: '🎨', label: 'Dessin', shortcut: '5', pro: true, color: 'indigo' },
]).map(tab => {
const locked = tab.pro && !isPro;
return (
<button
<GlassIconButton
key={tab.mode}
className={`mode-tab ${brandMode === tab.mode ? 'mode-tab--active' : ''} ${locked ? 'mode-tab--locked' : ''}`}
color={locked ? 'red' : tab.color}
icon={locked ? '🔒' : tab.icon}
active={brandMode === tab.mode}
onClick={() => onBrandSwitch(tab.mode)}
title={locked ? `${tab.label} (Pro)` : `${tab.label} (Ctrl+${tab.shortcut})`}
>
<span className="mode-tab-icon">{locked ? '🔒' : tab.icon}</span>
</button>
className={locked ? 'mode-tab--locked' : ''}
/>
);
})}
<span className="mode-tab-kbd" title="Recherche / Commandes">Ctrl+K</span>
</div>
)}

Expand Down
Loading