From 352e01d564197ae469a6a6364a7356433e1b7690 Mon Sep 17 00:00:00 2001 From: Marco Farina Date: Mon, 22 Jun 2026 13:11:52 +0200 Subject: [PATCH 1/9] feat(apprendista): template esercizi Volume 4 (componenti + demo + sidebar) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Infrastruttura per gli esercizi della Biblioteca dell'Apprendista (Vol. 4): - : card-link lato lezione per gli «Esercizi assegnati». - //: pagina-esercizio con provenienza, esercizi numerati e soluzioni in PyRunner readonly. Card di provenienza con lo stile del redesign Claude Design (ancora icona+numero, breadcrumb, teoria), riadattata a token/font/icone del progetto. Niente bordo-accento sinistro. - Modello ibrido: lezione = pagina singola se ha solo esercizi rapidi, categoria se ha anche esercizi dedicati/laboratori. Badge tipo pagina (rapidi/esercizio/laboratorio) con icone duotone (dumbbell/pen/flask). - Sidebar apprendista ristrutturata (volume-sorgente aperto, resto chiuso). - Pagine demo segnaposto + _esercizio-template.mdx (escluso dal build). - Linking solo on-site (doc id/permalink), mai paideia_id. Co-Authored-By: Claude Opus 4.8 --- sidebars/apprendista.ts | 75 ++++- src/components/Exercise/index.tsx | 189 ++++++++++++ src/components/Exercise/styles.module.css | 289 ++++++++++++++++++ src/components/ExerciseLink/index.tsx | 73 +++++ src/components/ExerciseLink/styles.module.css | 117 +++++++ src/components/Icon/icons.ts | 29 +- src/theme/MDXComponents.tsx | 6 + volumes/apprendista/_esercizio-template.mdx | 132 ++++++++ .../i-tipi-di-dato/esercizi-rapidi.mdx | 59 ++++ .../i-tipi-di-dato/esercizio-convertitore.mdx | 38 +++ .../laboratorio-convertitore.mdx | 41 +++ .../programmatore/le-basi/le-variabili.mdx | 70 +++++ 12 files changed, 1112 insertions(+), 6 deletions(-) create mode 100644 src/components/Exercise/index.tsx create mode 100644 src/components/Exercise/styles.module.css create mode 100644 src/components/ExerciseLink/index.tsx create mode 100644 src/components/ExerciseLink/styles.module.css create mode 100644 volumes/apprendista/_esercizio-template.mdx create mode 100644 volumes/apprendista/programmatore/le-basi/i-tipi-di-dato/esercizi-rapidi.mdx create mode 100644 volumes/apprendista/programmatore/le-basi/i-tipi-di-dato/esercizio-convertitore.mdx create mode 100644 volumes/apprendista/programmatore/le-basi/i-tipi-di-dato/laboratorio-convertitore.mdx create mode 100644 volumes/apprendista/programmatore/le-basi/le-variabili.mdx diff --git a/sidebars/apprendista.ts b/sidebars/apprendista.ts index a6830f3..0146468 100644 --- a/sidebars/apprendista.ts +++ b/sidebars/apprendista.ts @@ -1,11 +1,76 @@ -import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; +import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; // Biblioteca dell'Apprendista (Volume 4 — esercizi e laboratori). -// Sidebar per percorso, identica struttura agli altri volumi. +// +// Gli esercizi sono raggruppati per volume di provenienza: +// volume-sorgente > capitolo > lezione +// +// Modello IBRIDO al livello "lezione": +// - lezione con SOLO esercizi rapidi → è una PAGINA singola (un doc) +// - lezione con anche esercizi dedicati / laboratori → è una CATEGORIA, con +// sotto una pagina per tipo (Esercizi rapidi / Esercizio: … / Laboratorio: …) +// Gli esercizietti di una pagina-batch sono sezioni

(vanno nel TOC della +// pagina), non voci di sidebar. +// +// Regola di collapse (per evitare la "lista infinita"): +// - categorie volume-sorgente: collapsed: false → aperte, mostrano i capitoli +// - categorie capitolo / lezione: collapsed: true → chiuse di default +// Così a colpo d'occhio si vedono i volumi-sorgente e l'elenco dei capitoli; +// il resto resta nascosto finché non si espande. +// +// NB: `themeConfig.docs.sidebar.autoCollapseCategories` è GLOBALE ed è a `true`: +// aprendo un capitolo, i fratelli si richiudono. Per questa sidebar sarebbe +// preferibile `false` (vedi nota in docusaurus.config.ts). + +// Albero condiviso dai tre percorsi (it / liceo / its). Quando i percorsi +// divergeranno, duplicare e personalizzare per chiave. +const tree: SidebarsConfig[string] = [ + 'intro', + { + type: 'category', + label: 'Manuale del Programmatore', + collapsed: false, // volume-sorgente: aperto + items: [ + { + type: 'category', + label: 'Le basi', + collapsed: true, // capitolo: chiuso + items: [ + // lezione con solo esercizi rapidi → pagina singola + 'programmatore/le-basi/le-variabili', + // lezione con più tipi di pagina → categoria + { + type: 'category', + label: 'I tipi di dato', + collapsed: true, // lezione: chiusa + items: [ + 'programmatore/le-basi/i-tipi-di-dato/esercizi-rapidi', + 'programmatore/le-basi/i-tipi-di-dato/esercizio-convertitore', + 'programmatore/le-basi/i-tipi-di-dato/laboratorio-convertitore', + ], + }, + ], + }, + ], + }, + // { + // type: 'category', + // label: 'Manuale dell'Artefice', + // collapsed: false, + // items: [ /* capitoli > lezioni > esercizi */ ], + // }, + // { + // type: 'category', + // label: 'Manuale dell'Archivista', + // collapsed: false, + // items: [ /* capitoli > lezioni > esercizi */ ], + // }, +]; + const sidebars: SidebarsConfig = { - it: ['intro'], - liceo: ['intro'], - its: ['intro'], + it: tree, + liceo: tree, + its: tree, }; export default sidebars; diff --git a/src/components/Exercise/index.tsx b/src/components/Exercise/index.tsx new file mode 100644 index 0000000..adb0fe1 --- /dev/null +++ b/src/components/Exercise/index.tsx @@ -0,0 +1,189 @@ +/** + * Componenti per le pagine-esercizio del Volume 4 (Biblioteca dell'Apprendista). + * + * Modello IBRIDO: una lezione con soli esercizi rapidi è una pagina singola; + * se ha anche esercizi dedicati / laboratori diventa una cartella con una pagina + * per tipo. Ogni pagina dichiara la sua provenienza con . + * + * card compatta (badge tipo + provenienza + teoria) + * un esercizietto (nelle pagine batch) + * ```py live readonly … ``` soluzione (PyRunner non editabile) + * + * Numerazione: id interno volume.capitolo.lezione.esercizio (es. 1.3.10.2). + * A video l'esercizio mostra la forma corta lezione.esercizio (es. 10.2). + * Tutti i riferimenti sono identificatori on-site (path/permalink), mai esterni. + */ +import { type ReactNode } from 'react'; +import Link from '@docusaurus/Link'; +import Heading from '@theme/Heading'; +import Icon, { type IconName } from '@site/src/components/Icon'; + +import styles from './styles.module.css'; + +// ─── LessonMeta ────────────────────────────────────────────────────────────── + +interface TheoryRef { + /** path on-site della lezione */ + to: string; + label: string; +} + +/** + * Tipo di pagina-esercizio: + * rapidi → batch di esercizietti «a batteria» (più in pagina) + * esercizio → un esercizio completo, pagina dedicata + * laboratorio → laboratorio/progetto, pagina dedicata + */ +type ExerciseKind = 'rapidi' | 'esercizio' | 'laboratorio'; + +const KIND: Record = { + rapidi: { label: 'Esercizi rapidi', icon: 'dumbbell' }, + esercizio: { label: 'Esercizio', icon: 'pen' }, + laboratorio: { label: 'Laboratorio', icon: 'flask' }, +}; + +interface LessonMetaProps { + /** Tipo di pagina: mostra un badge con icona. */ + kind?: ExerciseKind; + /** + * Numero dell'esercizio (forma corta lezione.esercizio, es. «11.3»), per le + * pagine a esercizio singolo / laboratorio. Sui batch si usa . + */ + n?: string; + /** Path on-site della lezione che assegna gli esercizi. */ + lessonTo: string; + /** Nome della lezione (link alla teoria). */ + lessonLabel: string; + /** Volume di provenienza, es. «Manuale del Programmatore». */ + volume: string; + /** Capitolo di provenienza, es. «Le basi». */ + chapter: string; + /** Id interno della lezione, es. «1.3.10». Mostrato come badge discreto. */ + lessonId?: string; + /** Lezioni che danno teoria supplementare. */ + theory?: TheoryRef[]; +} + +export function LessonMeta({ + kind = 'rapidi', + n, + lessonTo, + lessonLabel, + volume, + chapter, + lessonId, + theory = [], +}: LessonMetaProps): ReactNode { + const k = KIND[kind]; + const section = n ?? lessonId; + return ( + + ); +} + +// ─── Exercise ──────────────────────────────────────────────────────────────── + +interface ExerciseProps { + /** Numero a video, forma corta lezione.esercizio, es. «10.2». */ + n: string; + /** Titolo dell'esercizio, es. «Calcola la media». */ + title: string; + children: ReactNode; +} + +export function Exercise({ n, title, children }: ExerciseProps): ReactNode { + const id = `esercizio-${n.replace(/\./g, '-')}`; + return ( +
+ + {n} + {title} + + {children} +
+ ); +} + +// ─── Solution ──────────────────────────────────────────────────────────────── + +interface SolutionProps { + /** Etichetta del disclosure. Default: «Mostra la soluzione». */ + label?: string; + children: ReactNode; +} + +export function Solution({ + label = 'Mostra la soluzione', + children, +}: SolutionProps): ReactNode { + return ( +
+ + + {label} + +
{children}
+
+ ); +} + +export default Exercise; diff --git a/src/components/Exercise/styles.module.css b/src/components/Exercise/styles.module.css new file mode 100644 index 0000000..e32ce9a --- /dev/null +++ b/src/components/Exercise/styles.module.css @@ -0,0 +1,289 @@ +/* LessonMeta — card di riferimento esercizio. + Stile ripreso dal redesign Claude Design (progetto "Rainbow bits", + ExerciseCard), riadattato ai token, font e icone del progetto. + Niente bordo-accento sinistro: l'accento sta nel pannello "ancora". */ + +.card { + /* mappa la palette del design sui nostri token (che gestiscono già dark/light) */ + --c-mono: var(--font-mono-ui); + --c-serif: var(--font-body); + --c-bg-from: var(--at-bg-panel); + --c-bg-to: var(--at-bg-subtle); + --c-border: var(--at-border); + --c-border-soft: color-mix(in srgb, var(--at-border) 55%, transparent); + --c-fg-strong: var(--at-fg-strong); + --c-fg-body: var(--at-fg-body); + --c-muted: var(--at-muted); + --c-faint: var(--at-faint); + --c-accent: var(--at-accent); + --c-accent-soft: var(--at-accent-soft); + --c-accent-dim: var(--at-accent-soft); + --c-accent-chip: var(--at-accent-chip); + --c-accent-chip-bd: var(--at-accent-chip-border); + --c-anchor-bg: color-mix(in srgb, var(--at-accent) 4%, transparent); + + position: relative; + background: linear-gradient(180deg, var(--c-bg-from), var(--c-bg-to)); + border: 1px solid var(--c-border); + border-radius: var(--radius-lg); + overflow: hidden; + margin: 0 0 32px; +} + +.inner { + display: flex; + align-items: stretch; +} + +/* ── ancora (sinistra): tipo + numero ───────────────────── */ +.anchor { + display: flex; + flex-direction: row; + align-items: stretch; + gap: 15px; + padding: 18px 22px; + border-right: 1px solid var(--c-border); + background: var(--c-anchor-bg); + min-width: 180px; +} + +.iconTile { + align-self: stretch; + width: 54px; + flex-shrink: 0; + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + background: var(--c-accent-chip); + border: 1px solid var(--c-accent-chip-bd); + color: var(--c-accent); +} + +.anchorText { + display: flex; + flex-direction: column; + justify-content: center; + gap: 5px; + min-width: 0; +} + +.typeLabel { + font-family: var(--c-mono); + font-size: 12.5px; + font-weight: 600; + line-height: 1.2; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--c-accent-soft); +} + +.section { + font-family: var(--c-mono); + font-size: 25px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; + color: var(--c-fg-strong); +} + +/* ── meta (destra): assegnato in / teoria ───────────────── */ +.meta { + flex: 1; + min-width: 0; + padding: 16px 26px; + display: flex; + flex-direction: column; + justify-content: center; + gap: 12px; +} + +.row { + display: flex; + align-items: center; + gap: 14px; +} + +.label { + width: 124px; + flex-shrink: 0; + display: inline-flex; + align-items: center; + gap: 7px; + font-family: var(--c-mono); + font-size: 11px; + font-weight: 600; + line-height: 1; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--c-faint); + white-space: nowrap; +} + +/* l'icona duotone ha il viewBox quadrato: la centriamo otticamente sulla + x-height del testo maiuscoletto */ +.label :global(svg) { + display: block; + margin-top: -1px; +} + +.divider { + height: 1px; + background: var(--c-border-soft); +} + +/* ── breadcrumb ─────────────────────────────────────────── */ +.crumbs { + font-family: var(--c-serif); + font-size: 16px; + line-height: 1.4; + color: var(--c-muted); +} + +.chevron { + color: var(--c-faint); + font-family: var(--c-mono); + font-size: 0.82em; + padding: 0 4px; + user-select: none; +} + +.crumbLeaf { + color: var(--c-accent-soft); + font-weight: 600; + text-decoration: none; + border-bottom: 1px solid var(--c-accent-chip-bd); + padding-bottom: 1px; +} + +.crumbLeaf:hover { + color: var(--c-accent); + border-bottom-color: var(--c-accent); + text-decoration: none; +} + +/* ── link teoria ────────────────────────────────────────── */ +.theory { + display: inline-flex; + flex-wrap: wrap; + gap: 4px 16px; + align-items: baseline; +} + +.theoryLink { + font-family: var(--c-serif); + font-size: 16px; + color: var(--c-fg-body); + text-decoration: none; + border-bottom: 1px solid var(--c-accent-dim); + padding-bottom: 1px; + transition: color var(--dur-fast) var(--ease-out); +} + +.theoryLink:hover { + color: var(--c-accent-soft); + text-decoration: none; +} + +/* ── responsive: su schermi stretti impila ancora + meta ── */ +@media (max-width: 600px) { + .inner { + flex-direction: column; + } + .anchor { + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 16px; + border-right: none; + border-bottom: 1px solid var(--c-border); + min-width: 0; + } + .label { + width: auto; + } + .row { + flex-wrap: wrap; + gap: 6px 12px; + } +} + +/* ─── Exercise ─────────────────────────────────────────────────────────────── */ + +.exercise { + margin: 40px 0; + scroll-margin-top: 80px; +} + +.exHeading { + display: flex; + align-items: baseline; + gap: 12px; + margin-bottom: 16px; +} + +.exNum { + flex-shrink: 0; + font-family: var(--font-mono-ui); + font-size: 0.78em; + font-weight: 700; + letter-spacing: 0.02em; + color: var(--at-accent); + background: var(--at-accent-bg); + border: 1px solid var(--at-accent-chip-border); + border-radius: var(--radius-sm); + padding: 2px 10px; + line-height: 1.4; +} + +.exTitle { + min-width: 0; +} + +/* ─── Solution ─────────────────────────────────────────────────────────────── */ + +.solution { + margin: 16px 0 0; + border: 1px solid var(--at-border); + border-radius: var(--radius-md); + background: var(--at-bg-subtle); + overflow: hidden; +} + +.solutionSummary { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + cursor: pointer; + list-style: none; + font-family: var(--font-mono-ui); + font-size: 0.85rem; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--at-fg-strong); + user-select: none; + transition: background var(--dur-fast) var(--ease-out); +} + +.solutionSummary::-webkit-details-marker { + display: none; +} + +.solutionSummary:hover { + background: var(--at-bg-chip); +} + +.solutionIcon { + color: #16a34a; + flex-shrink: 0; +} + +html[data-theme='dark'] .solutionIcon { + color: #4ade80; +} + +.solutionBody { + padding: 4px 16px 14px; + border-top: 1px solid var(--at-border); +} diff --git a/src/components/ExerciseLink/index.tsx b/src/components/ExerciseLink/index.tsx new file mode 100644 index 0000000..59323ca --- /dev/null +++ b/src/components/ExerciseLink/index.tsx @@ -0,0 +1,73 @@ +/** + * ExerciseLink — card-link da usare nelle lezioni (volumi 1–3) per elencare gli + * esercizi assegnati, che vivono nel Volume 4 (Biblioteca dell'Apprendista). + * + * Il riferimento è SEMPRE un identificatore on-site: `to` è il path del sito + * (senza baseUrl, lo aggiunge Docusaurus), es. + * /apprendista/programmatore/le-basi/le-variabili/calcola-la-media + * Mai riferimenti interni esterni al sito (niente paideia_id o simili). + */ +import React, { type ReactNode } from 'react'; +import Link from '@docusaurus/Link'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import styles from './styles.module.css'; + +type Difficulty = 'facile' | 'medio' | 'difficile'; + +interface ExerciseLinkProps { + /** Path on-site dell'esercizio (senza baseUrl). Es. `/apprendista/...`. */ + to: string; + /** Difficoltà, opzionale: mostra un chip colorato. */ + difficulty?: Difficulty; + /** Tempo stimato, opzionale. Es. `20 min`. */ + time?: string; + /** Titolo dell'esercizio. */ + children: ReactNode; +} + +const DIFFICULTY_LABEL: Record = { + facile: 'Facile', + medio: 'Medio', + difficile: 'Difficile', +}; + +export default function ExerciseLink({ + to, + difficulty, + time, + children, +}: ExerciseLinkProps): ReactNode { + return ( + +