-
-
- {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}
+
+ );
+ })}
+
+
+
+ Volume in stesura · nuovi capitoli in arrivo
+
);
}
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 (
<>