diff --git a/package-lock.json b/package-lock.json index 9d1e600..62d1535 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python-doesnt-byte", - "version": "0.9.0", + "version": "0.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "python-doesnt-byte", - "version": "0.9.0", + "version": "0.11.0", "dependencies": { "@codemirror/commands": "^6.10.3", "@codemirror/lang-python": "^6.2.1", diff --git a/package.json b/package.json index ba81f34..28ee7ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "python-doesnt-byte", - "version": "0.9.0", + "version": "0.11.0", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/src/components/BentoFeatures/index.tsx b/src/components/BentoFeatures/index.tsx index 7d3615c..a3e0d43 100644 --- a/src/components/BentoFeatures/index.tsx +++ b/src/components/BentoFeatures/index.tsx @@ -2,6 +2,9 @@ import { useRef, useState, type ReactNode } from 'react'; import clsx from 'clsx'; import Heading from '@theme/Heading'; import PyRunner from '@site/src/theme/PyRunner'; +import SQLRunner from '@site/src/theme/SQLRunner'; +import Algorithm from '@site/src/components/Algorithm'; +import { OPEN_SEARCH_EVENT } from '@site/src/lib/events'; import styles from './styles.module.css'; interface Feature { @@ -12,65 +15,144 @@ interface Feature { demo: ReactNode; } -function PlayIcon() { +/* Icone delle tab: Font Awesome Pro 7.1.0 duotone (primario currentColor + + * secondario opacity .4). Il colore e l'animazione hover arrivano dalla + * classe passata da fuori (vedi .icon* in styles). */ +function PlayDuotone({ className }: { className?: string }) { return ( ); } -function GraphIcon() { +function TableDuotone({ className }: { className?: string }) { return ( ); } -function SearchIcon() { +function ChartDuotone({ className }: { className?: string }) { return ( + ); +} + +function SearchDuotone({ className }: { className?: string }) { + return ( + + ); +} + +/* Icone duotone (Font Awesome Pro 7.1.0) per il blocco «E molto altro ancora». + * Primario currentColor + secondario opacity .4: il colore deriva dal + * contenitore di ciascuna voce. */ +function RouteDuotone() { + return ( + + ); +} + +function WandDuotone() { + return ( + + ); +} + +function LinkSwapDuotone() { + return ( + ); } @@ -83,81 +165,104 @@ for i in range(1, righe + 1): print(spazi + stelle) `; -function ComingSoonPanel({ - icon, - title, - body, -}: { - icon: ReactNode; - title: string; - body: ReactNode; -}) { - return ( -
-
{icon}
-
{title}
-
{body}
- Prossimamente -
- ); -} +const SQL_DEMO = `-- Modifica la query e ripremi Run +SELECT a.nome, COUNT(*) AS prestiti +FROM prestiti p +JOIN libri l ON l.id = p.libro_id +JOIN autori a ON a.id = l.autore_id +GROUP BY a.nome +ORDER BY prestiti DESC +LIMIT 3; +`; const FEATURES: Feature[] = [ { kicker: '01', title: 'Esempi vivi', desc: 'Ogni snippet gira nella pagina. Modificalo, sperimenta, rompi qualcosa: il libro risponde.', - icon: , + icon: , demo: , }, { kicker: '02', - title: 'Algoritmi animati', - desc: 'Ordinamento, ricorsione, grafi: visualizzazioni interattive per vedere ogni passo.', - icon: , + title: 'Database dal vivo', + desc: 'Interroghi un vero SQLite nel browser. Zero installazioni: scrivi SQL e vedi le righe.', + icon: , demo: ( - } - title="Visualizzatore di algoritmi" - body={ - <> - Ogni algoritmo del libro avrà la sua animazione: step-by-step, - controlli play/pausa, evidenziazione delle variabili in gioco. - - } + ), }, { kicker: '03', + title: 'Algoritmi animati', + desc: 'Ordinamento, ricerca, ricorsione: visualizzazioni interattive per vedere ogni passo.', + icon: , + demo: , + }, + { + kicker: '04', title: 'Cerca, non sfogliare', desc: 'Trova subito un concetto, una definizione, un esempio. Senza indice analitico né rilegatura.', - icon: , + icon: , demo: ( - } - title="Ricerca full-text" - body={ - <> -
- - - ⌘K -
- Risultati istantanei con anteprima del contesto, scorciatoia da - tastiera, niente indice analitico da impaginare. - - } - /> +
+ +

+ Risultati istantanei con anteprima del contesto e scorciatoia da + tastiera. Niente indice analitico da impaginare. +

+
), }, ]; +interface MoreItem { + icon: ReactNode; + title: string; + desc: string; + /* Il colore vive nel CSS (classi more_*) con varianti light/dark. */ + accent: 'blue' | 'violet' | 'teal'; +} + +const MORE: MoreItem[] = [ + { + icon: , + title: 'Percorsi adattivi', + desc: 'Il libro si rimodella sul tuo indirizzo: Informatica, Liceo Scienze Applicate o ITS.', + accent: 'blue', + }, + { + icon: , + title: 'Spiegamelo facile', + desc: 'Copi un prompt pronto — o lo mandi al tuo LLM preferito — per farti rispiegare codice ed errori a parole tue.', + accent: 'violet', + }, + { + icon: , + title: 'Esercizi e teoria, collegati', + desc: 'Ogni esercizio sa da quale lezione nasce; ogni lezione sa come allenarti.', + accent: 'teal', + }, +]; + interface CardProps { feature: Feature; index: number; @@ -223,7 +328,7 @@ export default function BentoFeatures() { Pensato per imparare facendo.

- Tre cose che un libro di testo, sulla carta, non può fare. + Cose che un libro di testo, sulla carta, non può fare.

+ +
+
E molto altro ancora…
+
    + {MORE.map((m) => ( +
  • + {m.icon} +
    +
    {m.title}
    +

    {m.desc}

    +
    +
  • + ))} +
+
); } diff --git a/src/components/BentoFeatures/styles.module.css b/src/components/BentoFeatures/styles.module.css index 105bfc1..3e47c4f 100644 --- a/src/components/BentoFeatures/styles.module.css +++ b/src/components/BentoFeatures/styles.module.css @@ -30,11 +30,17 @@ .grid { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(4, 1fr); gap: 16px; } @media (max-width: 900px) { + .grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 560px) { .grid { grid-template-columns: 1fr; } @@ -85,8 +91,10 @@ transition: opacity 0.3s; pointer-events: none; } -.card:hover::before, -.cardActive::before { +/* Lo spotlight segue il cursore: visibile solo in hover (anche sulla card + * attiva, già distinta da bordo + ombra interna). Senza questo vincolo il + * glow resterebbe «appeso» all'ultima posizione del mouse sulla attiva. */ +.card:hover::before { opacity: 1; } @@ -117,6 +125,94 @@ z-index: 2; } +/* Colore (duotone via currentColor) di ciascuna icona-tab, con variante + * pastello in dark come per gli accent dei volumi. */ +.iconPlay { + color: var(--at-accent); +} +.iconData { + color: #0891b2; +} +.iconAlgo { + color: #7c3aed; + transform-origin: 50% 92%; +} +.iconSearch { + color: #a21caf; +} + +html[data-theme='dark'] .iconData { + color: #5ec8dd; +} +html[data-theme='dark'] .iconAlgo { + color: #a78bfa; +} +html[data-theme='dark'] .iconSearch { + color: #d98ce3; +} + +.iconBox svg { + transition: transform 250ms var(--ease-smooth); +} + +/* All'hover sulla card l'icona si anima con un gesto a tema: il play pulsa, + * la tabella ondeggia, le barre dell'algoritmo «riordinano», la lente scruta. */ +.card:hover .iconPlay { + animation: bento-icon-pulse 1.1s var(--ease-smooth) infinite; +} +.card:hover .iconData { + animation: bento-icon-nudge 1.1s var(--ease-smooth) infinite; +} +.card:hover .iconAlgo { + animation: bento-icon-bars 0.9s var(--ease-smooth) infinite; +} +.card:hover .iconSearch { + animation: bento-icon-scan 1.4s var(--ease-smooth) infinite; +} + +@keyframes bento-icon-pulse { + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.16); + } +} +@keyframes bento-icon-nudge { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-3px); + } +} +@keyframes bento-icon-bars { + 0%, + 100% { + transform: scaleY(1); + } + 35% { + transform: scaleY(0.76); + } + 70% { + transform: scaleY(1.14); + } +} +@keyframes bento-icon-scan { + 0%, + 100% { + transform: translate(0, 0) rotate(0); + } + 25% { + transform: translate(1.5px, -1px) rotate(-7deg); + } + 75% { + transform: translate(-1.5px, 1px) rotate(7deg); + } +} + .body { position: relative; z-index: 2; @@ -177,88 +273,158 @@ transform: none; transition: none; } + .card:hover .iconPlay, + .card:hover .iconData, + .card:hover .iconAlgo, + .card:hover .iconSearch { + animation: none; + } } -.comingWrap { - position: relative; +/* Box demo «Cerca»: un campo reale che apre la modale Pagefind (⌘K). */ +.searchPromo { border-radius: var(--radius-lg); - border: 1px dashed var(--at-border); + border: 1px solid var(--at-border); background: var(--at-bg-subtle); - padding: 36px 28px 28px; + padding: 40px 28px; text-align: center; - color: var(--at-muted); - font-size: 14px; - line-height: 1.6; } -.comingIcon { - width: 48px; - height: 48px; +.searchField { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + max-width: 460px; + margin: 0 auto 18px; + padding: 12px 14px; border-radius: var(--radius-sm); - background: var(--at-bg-chip); border: 1px solid var(--at-border); - display: inline-flex; - align-items: center; - justify-content: center; - margin-bottom: 14px; + background: var(--at-bg); + color: var(--at-fg); + font: inherit; + text-align: left; + cursor: text; + transition: + border-color 200ms var(--ease-smooth), + box-shadow 200ms var(--ease-smooth); } -.comingTitle { - font-family: var(--font-display); - font-size: 20px; - font-weight: 600; - color: var(--at-fg-strong); - margin-bottom: 10px; - letter-spacing: -0.01em; +.searchField:hover, +.searchField:focus-visible { + border-color: var(--at-accent); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--at-accent) 18%, transparent); + outline: none; +} + +.searchField svg { + width: 18px; + height: 18px; + flex-shrink: 0; + color: var(--at-muted); +} + +.searchFieldText { + flex: 1; + color: var(--at-muted); } -.comingBody { +.searchPromoNote { max-width: 460px; margin: 0 auto; + color: var(--at-muted); + font-size: 14px; + line-height: 1.6; } -.comingBadge { - position: absolute; - top: 14px; - right: 14px; +/* Blocco «E molto altro ancora»: peculiarità descrittive, non interattive. */ +.more { + margin-top: 56px; + padding-top: 36px; + border-top: 1px solid var(--at-border); +} + +.moreHead { + text-align: center; font-family: var(--font-mono-ui); - font-size: 10px; - letter-spacing: 0.14em; + font-size: 12px; + letter-spacing: 0.16em; text-transform: uppercase; - padding: 4px 8px; - border-radius: var(--radius-pill); - background: var(--at-bg-chip); - border: 1px solid var(--at-border); color: var(--at-muted); + margin-bottom: 28px; +} + +.moreGrid { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; +} + +@media (max-width: 760px) { + .moreGrid { + grid-template-columns: 1fr; + gap: 16px; + } } -.fakeSearch { +.moreItem { display: flex; - align-items: center; - gap: 10px; - padding: 10px 14px; - border-radius: var(--radius-sm); - border: 1px solid var(--at-border); - background: var(--at-bg); - max-width: 420px; - margin: 8px auto 18px; + align-items: flex-start; + gap: 14px; text-align: left; } -.fakeSearch svg { - width: 18px; - height: 18px; +/* Accenti delle voci, con variante pastello in dark (stesso schema degli + * accent dei volumi in ChapterIndex/VolumeCard). */ +.more_blue { + --more-color: #2563eb; +} +.more_violet { + --more-color: #7c3aed; +} +.more_teal { + --more-color: #0d9488; +} +html[data-theme='dark'] .more_blue { + --more-color: #75a3ed; +} +html[data-theme='dark'] .more_violet { + --more-color: #a78bfa; +} +html[data-theme='dark'] .more_teal { + --more-color: #5ecfc0; +} + +.moreIcon { flex-shrink: 0; + width: 40px; + height: 40px; + border-radius: var(--radius-sm); + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--more-color); + background: color-mix(in srgb, var(--more-color) 12%, transparent); + border: 1px solid color-mix(in srgb, var(--more-color) 28%, transparent); } -.fakeSearch input { - flex: 1; - border: 0; - background: transparent; - color: var(--at-fg); - font: inherit; - outline: none; - cursor: not-allowed; +.moreItemTitle { + font-family: var(--font-display); + font-size: 17px; + font-weight: 600; + color: var(--at-fg-strong); + letter-spacing: -0.01em; + margin-bottom: 4px; +} + +.moreItemDesc { + margin: 0; + color: var(--at-muted); + font-size: 14px; + line-height: 1.55; } .fakeKbd { diff --git a/src/components/ChapterIndex/index.tsx b/src/components/ChapterIndex/index.tsx index ab9ec47..dd57b96 100644 --- a/src/components/ChapterIndex/index.tsx +++ b/src/components/ChapterIndex/index.tsx @@ -1,75 +1,121 @@ -import { useRef, useState } from 'react'; +import { useCallback, useLayoutEffect, useRef, useState } from 'react'; import Link from '@docusaurus/Link'; import Heading from '@theme/Heading'; import clsx from 'clsx'; +import { + BracketsCurlyIcon, + CompassDraftingIcon, + DatabaseIcon, + BookOpenCoverIcon, +} from '@site/src/components/VolumeIcons'; import styles from './styles.module.css'; +type Accent = 'blue' | 'pink' | 'amber' | 'green'; + interface Chapter { title: string; - to: string; + lessons: number; + /** Rotta della pagina, solo per i capitoli già pubblicati. */ + to?: string; } interface Volume { - id: string; - short: string; + n: string; label: string; - accent: 'blue' | 'pink' | 'amber' | 'green'; + accent: Accent; + icon: (props: { size?: number }) => JSX.Element; chapters: Chapter[]; draft?: boolean; } +/* + * TOC pianificata: i capitoli senza `to` non esistono ancora come pagine e i + * conteggi in `lessons` sono di progetto, non misurati sul contenuto reale. + * Tenere allineata a mano man mano che i volumi prendono forma; a regime + * andrà derivata dai globalData delle istanze docs. + */ const VOLUMES: Volume[] = [ { - id: 'programmatore', - short: 'Programmatore', + n: '01', label: 'Manuale del Programmatore', accent: 'blue', + icon: BracketsCurlyIcon, + draft: true, chapters: [ - { title: 'Introduzione', to: '/programmatore/' }, - { title: 'Variabili', to: '/programmatore/variabili' }, + { title: 'Introduzione', lessons: 2, to: '/programmatore/' }, + { title: 'Fondamenti di Python', lessons: 4 }, + { title: 'Le basi del linguaggio', lessons: 5 }, + { title: 'Strutture di controllo', lessons: 4 }, + { title: 'Funzioni', lessons: 4 }, + { title: 'Strutture dati', lessons: 5 }, ], }, { - id: 'artefice', - short: 'Artefice', - label: "Manuale dell'Artefice", + n: '02', + label: 'Manuale dell’Artefice', accent: 'pink', + icon: CompassDraftingIcon, + draft: true, chapters: [ - { title: 'Introduzione', to: '/artefice/' }, - { title: 'Perché gli oggetti?', to: '/artefice/perche-gli-oggetti' }, - { title: 'Classi, istanze e metodi', to: '/artefice/classi-e-istanze' }, + { title: 'Introduzione', lessons: 2, to: '/artefice/' }, + { + title: 'Perché gli oggetti?', + lessons: 3, + to: '/artefice/perche-gli-oggetti', + }, + { + title: 'Classi, istanze e metodi', + lessons: 5, + to: '/artefice/classi-e-istanze', + }, { title: 'Metodi di classe e statici', + lessons: 3, to: '/artefice/metodi-di-classe-e-statici', }, - { title: 'Mostrare un oggetto', to: '/artefice/mostrare-un-oggetto' }, - { title: 'Incapsulamento', to: '/artefice/incapsulamento' }, + { + title: 'Mostrare un oggetto', + lessons: 2, + to: '/artefice/mostrare-un-oggetto', + }, + { title: 'Incapsulamento', lessons: 4, to: '/artefice/incapsulamento' }, ], - draft: true, }, { - id: 'archivista', - short: 'Archivista', - label: "Manuale dell'Archivista", + n: '03', + label: 'Manuale dell’Archivista', accent: 'amber', - chapters: [{ title: 'Introduzione', to: '/archivista/' }], + icon: DatabaseIcon, draft: true, + chapters: [ + { title: 'Introduzione', lessons: 2, to: '/archivista/' }, + { title: 'File e formati', lessons: 4 }, + { title: 'Database relazionali', lessons: 5 }, + { title: 'SQL essenziale', lessons: 6 }, + { title: 'Serializzazione', lessons: 3 }, + ], }, { - id: 'apprendista', - short: 'Apprendista', - label: "Biblioteca dell'Apprendista", + n: '04', + label: 'Biblioteca dell’Apprendista', accent: 'green', - chapters: [{ title: 'Introduzione', to: '/apprendista/' }], + icon: BookOpenCoverIcon, draft: true, + chapters: [ + { title: 'Introduzione', lessons: 1, to: '/apprendista/' }, + { title: 'Mini-progetti guidati', lessons: 5 }, + { title: 'Algoritmi visualizzati', lessons: 4 }, + { title: 'Sfide di codice', lessons: 6 }, + ], }, ]; function ArrowRight() { return ( (null); - function onMove(e: React.MouseEvent) { - const el = ref.current; - if (!el) return; - const r = el.getBoundingClientRect(); - el.style.setProperty('--at-mx', `${e.clientX - r.left}px`); - el.style.setProperty('--at-my', `${e.clientY - r.top}px`); - } - return ( - -
- {String(index + 1).padStart(2, '0')} - {chapter.title} - - - -
- - ); -} - export default function ChapterIndex() { const [active, setActive] = useState(0); const panelId = 'chapter-index-panel'; + const btnRefs = useRef<(HTMLButtonElement | null)[]>([]); + const indicatorRef = useRef(null); + const segRef = useRef(null); + const animatedRef = useRef(false); + const activeRef = useRef(active); + + // Tutte le dipendenze sono ref stabili → la funzione è creata una volta sola + // e gli effect qui sotto non la elencano come dep instabile. + const moveIndicator = useCallback((i: number, animate: boolean) => { + const ind = indicatorRef.current; + const btn = btnRefs.current[i]; + if (!ind || !btn) return; + if (!animate) ind.classList.add(styles.noAnim); + ind.style.transform = `translate(${btn.offsetLeft}px, ${btn.offsetTop}px)`; + ind.style.width = `${btn.offsetWidth}px`; + ind.style.height = `${btn.offsetHeight}px`; + if (!animate) { + // riattiva l'animazione dopo due frame (come nel template originale) + requestAnimationFrame(() => + requestAnimationFrame(() => ind.classList.remove(styles.noAnim)), + ); + } + }, []); + + useLayoutEffect(() => { + activeRef.current = active; + moveIndicator(active, animatedRef.current); + animatedRef.current = true; + }, [active, moveIndicator]); + + useLayoutEffect(() => { + const el = segRef.current; + if (!el) return; + const ro = new ResizeObserver(() => + moveIndicator(activeRef.current, false), + ); + ro.observe(el); + return () => ro.disconnect(); + }, [moveIndicator]); + + // Roving tabindex: il focus deve seguire il tab attivo, altrimenti resta + // su un bottone con tabIndex -1 e la navigazione da tastiera si rompe. + function selectTab(i: number) { + setActive(i); + btnRefs.current[i]?.focus(); + } + function onKeyDown(e: React.KeyboardEvent) { if (e.key === 'ArrowRight') { e.preventDefault(); - setActive((a) => (a + 1) % VOLUMES.length); + selectTab((active + 1) % VOLUMES.length); } else if (e.key === 'ArrowLeft') { e.preventDefault(); - setActive((a) => (a - 1 + VOLUMES.length) % VOLUMES.length); + selectTab((active - 1 + VOLUMES.length) % VOLUMES.length); } } const current = VOLUMES[active]; return ( -
+
Indice - {current.label} + {current.label}
+
- {VOLUMES.map((v, i) => ( - - ))} +
+
-
-
- {current.chapters.map((ch, i) => ( - - ))} -
- {current.draft && ( -
- Volume in stesura — nuovi capitoli in arrivo. -
- )} +
+ {current.chapters.map((ch, i) => { + const inner = ( + <> + + {String(i + 1).padStart(2, '0')} + + {ch.title} + + {ch.lessons} {ch.lessons === 1 ? 'lezione' : 'lezioni'} + + {ch.to && ( + + + + )} + + ); + // Freccia e hover solo sulle righe che portano davvero a una + // pagina; i capitoli pianificati restano voci statiche. + return ch.to ? ( + + {inner} + + ) : ( +
+ {inner} +
+ ); + })}
+ +
+
); } diff --git a/src/components/ChapterIndex/styles.module.css b/src/components/ChapterIndex/styles.module.css index 7e1c570..ef84bef 100644 --- a/src/components/ChapterIndex/styles.module.css +++ b/src/components/ChapterIndex/styles.module.css @@ -1,226 +1,362 @@ +/* + * Indice — segmented control con indicatore scorrevole + lista capitoli + * compatta. Adattato dal template Claude Design (hardcoded dark) ai token + * --at-* del sito, così funziona in entrambi i temi. L'accento del volume + * attivo (--vc) è impostato per-accent in fondo, con varianti light/dark. + */ + .wrap { + --vc: var(--at-accent); padding: 40px 32px 60px; - max-width: 780px; + max-width: 1140px; margin: 0 auto; position: relative; z-index: 1; } +/* ── Header ─────────────────────────────────────────── */ .head { display: flex; align-items: baseline; justify-content: space-between; + gap: 16px; margin-bottom: 24px; } .label { - font-size: 14px; - color: var(--at-muted); - letter-spacing: 0.12em; - text-transform: uppercase; - font-family: var(--font-mono-ui); margin: 0; + font-family: var(--font-mono-ui); + font-size: 13px; font-weight: 600; + letter-spacing: 0.3em; + text-transform: uppercase; + color: var(--at-muted); } -.count { - font-size: 12px; +.headVolume { + font-family: var(--font-display); + font-style: italic; + font-size: 19px; + color: var(--at-muted-soft); + text-align: right; + transition: color var(--dur-slow) var(--ease-smooth); +} + +/* ── Segmented control ──────────────────────────────── */ +.seg { + position: relative; + display: flex; + gap: 6px; + padding: 6px; + background: var(--at-bg-subtle); + border: 1px solid var(--at-border); + border-radius: var(--radius-lg); +} + +.indicator { + position: absolute; + left: 0; + top: 0; + z-index: 1; + border-radius: var(--radius-sm); + background: color-mix(in oklab, var(--vc) 14%, transparent); + box-shadow: + 0 0 0 1px color-mix(in oklab, var(--vc) 38%, transparent), + 0 10px 26px -12px color-mix(in oklab, var(--vc) 70%, transparent); + pointer-events: none; + transition: + transform 0.46s var(--ease-spring), + width 0.46s var(--ease-spring), + height 0.46s var(--ease-spring), + background var(--dur-slow) var(--ease-smooth), + box-shadow var(--dur-slow) var(--ease-smooth); +} +.indicator.noAnim { + transition: none; +} + +.segBtn { + position: relative; + z-index: 2; + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 13px; + padding: 12px 16px; + text-align: left; + background: none; + border: none; + cursor: pointer; +} +.segBtn:focus-visible { + outline: 2px solid var(--vc); + outline-offset: 2px; + border-radius: var(--radius-sm); +} +.segBtn:active { + transform: scale(0.98); +} + +.segIco { + flex-shrink: 0; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + background: var(--at-bg-chip); + border: 1px solid var(--at-border); color: var(--at-muted); - font-family: var(--font-mono-ui); + transition: + color var(--dur-slow) var(--ease-smooth), + background var(--dur-slow) var(--ease-smooth), + border-color var(--dur-slow) var(--ease-smooth); } -.list { +.segTxt { + min-width: 0; display: flex; flex-direction: column; - gap: 1px; - border-radius: var(--radius-md); - overflow: hidden; - border: 1px solid var(--at-border); + gap: 3px; } -.row { +.segKicker { + font-family: var(--font-mono-ui); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.14em; + color: var(--at-faint); + transition: color var(--dur-slow) var(--ease-smooth); +} + +.segName { + font-family: var(--font-display); + font-size: 13.5px; + font-weight: 500; + line-height: 1.2; + letter-spacing: -0.005em; + color: var(--at-muted); + text-wrap: balance; + transition: color var(--dur-slow) var(--ease-smooth); +} + +.segBtn:hover .segName, +.segBtn:hover .segIco { + color: var(--at-muted-soft); +} + +.segBtnActive .segIco { + color: var(--vc); + background: color-mix(in oklab, var(--vc) 16%, transparent); + border-color: color-mix(in oklab, var(--vc) 34%, transparent); +} +.segBtnActive .segKicker { + color: var(--vc); +} +.segBtnActive .segName { + color: var(--at-fg-strong); + font-weight: 600; +} + +/* ── Lista capitoli compatta ────────────────────────── */ +.panel { position: relative; +} + +.list { + margin-top: 20px; + border: 1px solid var(--at-border); + border-radius: var(--radius-md); overflow: hidden; background: var(--at-bg-subtle); - padding: 16px 22px; - text-decoration: none; - color: inherit; + animation: index-list-in 280ms var(--ease-out) both; +} + +@keyframes index-list-in { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: none; + } +} + +.row { + display: flex; + align-items: center; + gap: 18px; + padding: 13px 22px; border-bottom: 1px solid var(--at-border); + transition: background var(--dur-normal) var(--ease-smooth); } .row:last-child { border-bottom: none; } -.row:hover { + +/* Affordance (hover, freccia che scorre) solo sulle righe-link: le voci + * pianificate (.rowSoon) non portano da nessuna parte e non devono + * promettere un click. */ +.rowLink { text-decoration: none; color: inherit; } - -.row::before { - content: ''; - position: absolute; - inset: 0; - background: radial-gradient( - 400px circle at var(--at-mx, 50%) var(--at-my, 50%), - var(--at-spot-bg), - transparent 40% - ); - opacity: 0; - transition: opacity 0.3s; - pointer-events: none; +.rowLink:hover { + text-decoration: none; + color: inherit; + background: color-mix(in oklab, var(--vc) 7%, transparent); } -.row:hover::before { - opacity: 1; +.rowLink:focus-visible { + outline: 2px solid var(--vc); + outline-offset: -2px; } -.rowInner { - display: flex; - align-items: center; - gap: 18px; - position: relative; - z-index: 2; +.rowSoon .title { + color: var(--at-muted); } .num { - font-size: 12px; - color: var(--at-muted); - min-width: 28px; + min-width: 22px; font-family: var(--font-mono-ui); - letter-spacing: 0.05em; + font-size: 13px; + font-weight: 500; + color: var(--at-faint); + transition: color var(--dur-normal) var(--ease-smooth); +} +.rowLink:hover .num { + color: var(--vc); } .title { - font-size: 15px; - font-weight: 500; - color: var(--at-fg-strong); flex: 1; font-family: var(--font-display); - margin: 0; + font-size: 16px; + font-weight: 500; + line-height: 1.3; + letter-spacing: -0.01em; + color: var(--at-fg-strong); } -.sections { +.lessons { + font-family: var(--font-mono-ui); font-size: 12px; + letter-spacing: 0.02em; + white-space: nowrap; color: var(--at-muted); } .arrow { - color: var(--at-faint); display: flex; + color: var(--at-faint); + transition: + transform var(--dur-normal) var(--ease-smooth), + color var(--dur-normal) var(--ease-smooth); } - -.tabs { - display: flex; - gap: 6px; - margin-bottom: 18px; - overflow-x: auto; - scrollbar-width: none; - padding: 4px 2px 4px; +.rowLink:hover .arrow { + transform: translateX(4px); + color: var(--vc); } -.tabs::-webkit-scrollbar { - display: none; +.arrowSvg { + display: block; } -.tab { - position: relative; - flex-shrink: 0; - padding: 8px 14px; - border-radius: var(--radius-pill); - border: 1px solid var(--at-border); - background: var(--at-bg-subtle); - color: var(--at-muted); - font-family: var(--font-display); - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: - color 200ms var(--ease-smooth), - background 200ms var(--ease-smooth), - border-color 200ms var(--ease-smooth), - transform 200ms var(--ease-smooth); -} - -.tab:hover { - color: var(--at-fg-strong); - transform: translateY(-1px); +/* ── Footer (volume in stesura) ─────────────────────── */ +.foot { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + margin-top: 24px; + font-family: var(--font-mono-ui); + font-size: 12px; + letter-spacing: 0.03em; + color: var(--at-faint); } - -.tab:active { - transform: scale(0.98); - transition-duration: var(--dur-fast); +.footHidden { + display: none; } -.tab:focus-visible { - outline: 2px solid var(--at-accent); - outline-offset: 2px; +.footDot { + width: 6px; + height: 6px; + border-radius: var(--radius-circle); + background: var(--vc); + box-shadow: 0 0 9px var(--vc); + animation: index-pulse 2.4s var(--ease-smooth) infinite; } -.tabActive { - color: var(--at-fg-strong); - border-color: var(--vol-accent, var(--at-accent)); - box-shadow: 0 0 0 1px var(--vol-accent, var(--at-accent)) inset; - background: var(--at-bg); +@keyframes index-pulse { + 0%, + 100% { + opacity: 0.35; + } + 50% { + opacity: 1; + } } +/* ── Accenti per volume (colori delle card a inizio pagina) ── */ .accent_blue { - --vol-accent: #1d68e1; + --vc: #1d68e1; } .accent_pink { - --vol-accent: #d946a0; + --vc: #d946a0; } .accent_amber { - --vol-accent: #d97706; + --vc: #d97706; } .accent_green { - --vol-accent: #16a34a; + --vc: #16a34a; } html[data-theme='dark'] .accent_blue { - --vol-accent: #75a3ed; + --vc: #75a3ed; } html[data-theme='dark'] .accent_pink { - --vol-accent: #f0a8d4; + --vc: #f0a8d4; } html[data-theme='dark'] .accent_amber { - --vol-accent: #f5b860; + --vc: #f5b860; } html[data-theme='dark'] .accent_green { - --vol-accent: #6ed699; -} - -.panel { - position: relative; + --vc: #6ed699; } -.panelSlot { - animation: chapter-slot-in 280ms cubic-bezier(0.2, 0.7, 0.2, 1) both; +/* ── Responsive: il segmented control va in 2×2 ─────── */ +@media (max-width: 760px) { + .wrap { + padding: 32px 18px 48px; + } + .seg { + flex-wrap: wrap; + } + .segBtn { + flex: 1 1 calc(50% - 3px); + } + .headVolume { + font-size: 16px; + } } -@keyframes chapter-slot-in { - from { - opacity: 0; - transform: translateY(10px); +@media (max-width: 460px) { + .segName { + font-size: 12.5px; } - to { - opacity: 1; - transform: none; + .row { + gap: 12px; + padding: 13px 16px; } } @media (prefers-reduced-motion: reduce) { - .panelSlot { - animation: none; - } - .tab, - .tab:hover { - transform: none; + .indicator, + .list, + .arrow, + .footDot { transition: none; + animation: none; } } - -.draftNote { - margin-top: 14px; - text-align: center; - font-size: 13px; - color: var(--at-muted); - font-family: var(--font-mono-ui); - letter-spacing: 0.04em; -} diff --git a/src/components/VolumeIcons/index.tsx b/src/components/VolumeIcons/index.tsx new file mode 100644 index 0000000..651d979 --- /dev/null +++ b/src/components/VolumeIcons/index.tsx @@ -0,0 +1,80 @@ +/* + * Icone dei quattro volumi: Font Awesome Pro 7.1.0 — stile duotone. + * Path primario `currentColor` + path secondario `opacity .4`, com'è nel + * sorgente FA. Il colore (e quindi i due livelli) deriva da `currentColor`: + * la VolumeCard lo imposta via `--vol-icon-color` per accent, il segmented + * control dell'indice lo imposta per accent dal CSS. viewBox originale di + * ciascuna icona. + * + * Sorgente UNICA: usate sia dalle VolumeCard della homepage (size 22) sia dal + * segmented control di (size 20). Tenere i path qui evita che + * i due punti d'uso divergano a un futuro aggiornamento di Font Awesome. + */ + +interface VolumeIconProps { + size?: number; +} + +export function BracketsCurlyIcon({ size = 22 }: VolumeIconProps) { + return ( + + ); +} + +export function CompassDraftingIcon({ size = 22 }: VolumeIconProps) { + return ( + + ); +} + +export function DatabaseIcon({ size = 22 }: VolumeIconProps) { + return ( + + ); +} + +export function BookOpenCoverIcon({ size = 22 }: VolumeIconProps) { + return ( + + ); +} diff --git a/src/lib/events.ts b/src/lib/events.ts new file mode 100644 index 0000000..8edefd1 --- /dev/null +++ b/src/lib/events.ts @@ -0,0 +1,10 @@ +/** + * Eventi custom di window condivisi tra componenti disaccoppiati. + * + * Tenerli qui (non come stringhe letterali sparse) evita che un dispatcher e il + * suo listener divergano per un typo: una rinomina qui rompe in compilazione i + * punti d'uso, invece di trasformare il click in un no-op silenzioso. + */ + +/** Apre la modale di ricerca da fuori la navbar (es. il box «Cerca» della home). */ +export const OPEN_SEARCH_EVENT = 'pdb:open-search'; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index d1eed3e..2319df1 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -7,88 +7,15 @@ import Heading from '@theme/Heading'; import VolumeCard from '@site/src/components/VolumeCard'; import BentoFeatures from '@site/src/components/BentoFeatures'; import ChapterIndex from '@site/src/components/ChapterIndex'; +import { + BracketsCurlyIcon, + CompassDraftingIcon, + DatabaseIcon, + BookOpenCoverIcon, +} from '@site/src/components/VolumeIcons'; import styles from './index.module.css'; -function TerminalIcon() { - return ( - - ); -} - -function BoxIcon() { - return ( - - ); -} - -function DatabaseIcon() { - return ( - - ); -} - -function FlaskIcon() { - return ( - - ); -} - export default function Home(): JSX.Element { const { siteConfig } = useDocusaurusContext(); const logoUrl = useBaseUrl('/img/logo.svg'); @@ -145,7 +72,7 @@ export default function Home(): JSX.Element { kicker="Volume 1" title="Manuale del Programmatore" desc="Fondamenti del linguaggio: dati, controllo di flusso, funzioni." - icon={} + icon={} accent="blue" /> } + icon={} accent="pink" /> } + icon={} accent="green" /> diff --git a/src/theme/SearchBar/index.tsx b/src/theme/SearchBar/index.tsx index e8dba65..b55f6f6 100644 --- a/src/theme/SearchBar/index.tsx +++ b/src/theme/SearchBar/index.tsx @@ -24,6 +24,7 @@ import React, { import { createPortal } from 'react-dom'; import useBaseUrl from '@docusaurus/useBaseUrl'; +import { OPEN_SEARCH_EVENT } from '@site/src/lib/events'; import { LensIcon, CircleXmarkIcon } from './icons'; import styles from './styles.module.css'; @@ -385,6 +386,19 @@ export default function SearchBar(): ReactNode { return () => document.removeEventListener('keydown', onKey); }, []); + // Apertura programmatica da altrove nella pagina (es. il box «Cerca» della + // homepage). L'evento è dispatchato sincronamente nel gesto utente, così + // warm() e il primer della tastiera (iOS) restano dentro l'interazione. + useEffect(() => { + const onOpen = () => { + warm(); + openKeyboardPrimer(); + setOpen(true); + }; + window.addEventListener(OPEN_SEARCH_EVENT, onOpen); + return () => window.removeEventListener(OPEN_SEARCH_EVENT, onOpen); + }, [warm]); + return ( <>