From 2fb5cb6164aacd9f12d9aef30e0ace68815a2b1c Mon Sep 17 00:00:00 2001 From: Marco Farina Date: Sun, 24 May 2026 18:37:30 +0200 Subject: [PATCH 01/15] feat(volumes): scaffold three-volume infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three new plugin-content-docs instances (Manuale del Programmatore, dell'Artefice, dell'Archivista) alongside the existing default docs, each with its own content folder under docs//, its own routeBasePath, and a sidebars/.ts that exports one sidebar per "percorso" (IT / Liceo / ITS). Sidebars currently reference only the placeholder intro.mdx for each volume — real chapters come once we have UI to switch percorso. PathContext (src/contexts/PathContext.tsx) persists per-volume the user's chosen path in localStorage under pdb:path:, with cross-tab sync via the storage event. The provider is wired through a Root swizzle so any component (future navbar selector, sidebar resolver, off-path banner) can read or update it. Each volume has the same three placeholder paths for now; per- volume customization happens when we wire the navbar selector. Co-Authored-By: Claude Opus 4.7 --- docusaurus.config.ts | 33 ++++++++++ sidebars/archivista.ts | 10 +++ sidebars/artefice.ts | 10 +++ sidebars/programmatore.ts | 12 ++++ src/contexts/PathContext.tsx | 110 ++++++++++++++++++++++++++++++++ src/theme/Root.tsx | 11 ++++ volumes/archivista/intro.mdx | 8 +++ volumes/artefice/intro.mdx | 8 +++ volumes/programmatore/intro.mdx | 8 +++ 9 files changed, 210 insertions(+) create mode 100644 sidebars/archivista.ts create mode 100644 sidebars/artefice.ts create mode 100644 sidebars/programmatore.ts create mode 100644 src/contexts/PathContext.tsx create mode 100644 src/theme/Root.tsx create mode 100644 volumes/archivista/intro.mdx create mode 100644 volumes/artefice/intro.mdx create mode 100644 volumes/programmatore/intro.mdx diff --git a/docusaurus.config.ts b/docusaurus.config.ts index b2ea482..1a245eb 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -77,6 +77,39 @@ const config: Config = { clientModules: ['./src/fonts.ts'], + plugins: [ + [ + '@docusaurus/plugin-content-docs', + { + id: 'programmatore', + path: 'volumes/programmatore', + routeBasePath: 'programmatore', + sidebarPath: './sidebars/programmatore.ts', + editUrl: 'https://github.com/marcofarina/python-doesnt-byte', + }, + ], + [ + '@docusaurus/plugin-content-docs', + { + id: 'artefice', + path: 'volumes/artefice', + routeBasePath: 'artefice', + sidebarPath: './sidebars/artefice.ts', + editUrl: 'https://github.com/marcofarina/python-doesnt-byte', + }, + ], + [ + '@docusaurus/plugin-content-docs', + { + id: 'archivista', + path: 'volumes/archivista', + routeBasePath: 'archivista', + sidebarPath: './sidebars/archivista.ts', + editUrl: 'https://github.com/marcofarina/python-doesnt-byte', + }, + ], + ], + themeConfig: { // Replace with your project's social card image: 'img/docusaurus-social-card.jpg', diff --git a/sidebars/archivista.ts b/sidebars/archivista.ts new file mode 100644 index 0000000..86c6c44 --- /dev/null +++ b/sidebars/archivista.ts @@ -0,0 +1,10 @@ +import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; + +// Manuale dell'Archivista (Volume 3 — 5a). +const sidebars: SidebarsConfig = { + it: ['intro'], + liceo: ['intro'], + its: ['intro'], +}; + +export default sidebars; diff --git a/sidebars/artefice.ts b/sidebars/artefice.ts new file mode 100644 index 0000000..1305acf --- /dev/null +++ b/sidebars/artefice.ts @@ -0,0 +1,10 @@ +import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; + +// Manuale dell'Artefice (Volume 2 — 4a). +const sidebars: SidebarsConfig = { + it: ['intro'], + liceo: ['intro'], + its: ['intro'], +}; + +export default sidebars; diff --git a/sidebars/programmatore.ts b/sidebars/programmatore.ts new file mode 100644 index 0000000..1804032 --- /dev/null +++ b/sidebars/programmatore.ts @@ -0,0 +1,12 @@ +import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; + +// Manuale del Programmatore (Volume 1 — 3a). +// Una sidebar per percorso. Lo stesso doc può comparire in più percorsi +// con label/categoria diverse: l'URL del doc resta unico. +const sidebars: SidebarsConfig = { + it: ['intro'], + liceo: ['intro'], + its: ['intro'], +}; + +export default sidebars; diff --git a/src/contexts/PathContext.tsx b/src/contexts/PathContext.tsx new file mode 100644 index 0000000..d3a214f --- /dev/null +++ b/src/contexts/PathContext.tsx @@ -0,0 +1,110 @@ +/** + * PathContext — persistenza per-volume del percorso scelto dall'utente + * (es. IT / Liceo / ITS). Salvato in localStorage con chiave + * `pdb:path:`. Ogni volume sceglie autonomamente il suo set + * di percorsi disponibili (vedi sidebars/.ts). + */ +import React, { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from 'react'; + +export type VolumeId = 'programmatore' | 'artefice' | 'archivista'; + +const STORAGE_PREFIX = 'pdb:path'; +const storageKey = (volume: VolumeId) => `${STORAGE_PREFIX}:${volume}`; + +type PathState = Partial>; + +type PathContextValue = { + /** Percorso correntemente attivo per il volume specificato (o null se mai scelto). */ + getPath: (volume: VolumeId) => string | null; + /** Imposta il percorso per un volume; persiste in localStorage. */ + setPath: (volume: VolumeId, path: string) => void; + /** Cancella la scelta per un volume (torna al default della sidebar). */ + clearPath: (volume: VolumeId) => void; +}; + +const PathContext = createContext(null); + +function readInitialState(): PathState { + if (typeof window === 'undefined') return {}; + const out: PathState = {}; + for (const v of ['programmatore', 'artefice', 'archivista'] as VolumeId[]) { + try { + const raw = window.localStorage.getItem(storageKey(v)); + if (raw) out[v] = raw; + } catch { + // localStorage disabilitato → silently ignore + } + } + return out; +} + +export function PathProvider({children}: {children: ReactNode}) { + const [state, setState] = useState(() => readInitialState()); + + // Hydration: il primo render server-side restituisce {}, sincronizziamo al mount. + useEffect(() => { + setState(readInitialState()); + }, []); + + // Cross-tab sync: se l'utente cambia la scelta in un'altra tab, aggiorniamo qui. + useEffect(() => { + if (typeof window === 'undefined') return; + const onStorage = (e: StorageEvent) => { + if (!e.key?.startsWith(STORAGE_PREFIX + ':')) return; + const v = e.key.slice(STORAGE_PREFIX.length + 1) as VolumeId; + setState((prev) => ({...prev, [v]: e.newValue ?? undefined})); + }; + window.addEventListener('storage', onStorage); + return () => window.removeEventListener('storage', onStorage); + }, []); + + const getPath = useCallback( + (volume: VolumeId) => state[volume] ?? null, + [state], + ); + + const setPath = useCallback((volume: VolumeId, path: string) => { + setState((prev) => ({...prev, [volume]: path})); + try { + window.localStorage.setItem(storageKey(volume), path); + } catch { + /* ignore */ + } + }, []); + + const clearPath = useCallback((volume: VolumeId) => { + setState((prev) => { + const next = {...prev}; + delete next[volume]; + return next; + }); + try { + window.localStorage.removeItem(storageKey(volume)); + } catch { + /* ignore */ + } + }, []); + + const value = useMemo( + () => ({getPath, setPath, clearPath}), + [getPath, setPath, clearPath], + ); + + return {children}; +} + +export function usePathContext(): PathContextValue { + const ctx = useContext(PathContext); + if (!ctx) { + throw new Error('usePathContext deve essere usato dentro '); + } + return ctx; +} diff --git a/src/theme/Root.tsx b/src/theme/Root.tsx new file mode 100644 index 0000000..1f29a95 --- /dev/null +++ b/src/theme/Root.tsx @@ -0,0 +1,11 @@ +/** + * Root — Docusaurus envelope around the app. Used here to inject our + * PathProvider so any component (sidebar, banner, navbar selector) can + * read/write the user's chosen path per volume. + */ +import React, {type ReactNode} from 'react'; +import {PathProvider} from '@site/src/contexts/PathContext'; + +export default function Root({children}: {children: ReactNode}) { + return {children}; +} diff --git a/volumes/archivista/intro.mdx b/volumes/archivista/intro.mdx new file mode 100644 index 0000000..ee3936a --- /dev/null +++ b/volumes/archivista/intro.mdx @@ -0,0 +1,8 @@ +--- +slug: / +sidebar_position: 1 +--- + +# Manuale dell'Archivista + +Volume 3 — dati, persistenza, SQLite. Placeholder. diff --git a/volumes/artefice/intro.mdx b/volumes/artefice/intro.mdx new file mode 100644 index 0000000..bb54de3 --- /dev/null +++ b/volumes/artefice/intro.mdx @@ -0,0 +1,8 @@ +--- +slug: / +sidebar_position: 1 +--- + +# Manuale dell'Artefice + +Volume 2 — programmazione ad oggetti. Placeholder. diff --git a/volumes/programmatore/intro.mdx b/volumes/programmatore/intro.mdx new file mode 100644 index 0000000..f718a5d --- /dev/null +++ b/volumes/programmatore/intro.mdx @@ -0,0 +1,8 @@ +--- +slug: / +sidebar_position: 1 +--- + +# Manuale del Programmatore + +Volume 1 — primo anno di Python. Placeholder. From 7e3a4a0f6cb0e26969e8d4499c22a8efb55d5f68 Mon Sep 17 00:00:00 2001 From: Marco Farina Date: Sun, 24 May 2026 19:25:55 +0200 Subject: [PATCH 02/15] feat(volumes): per-volume path selector + runtime sidebar swap PathSelector (src/components/PathSelector) reads the active plugin-content-docs instance via useActivePlugin, fetches that instance's full sidebar set via useAllDocsData (each sidebar key = a percorso: it/liceo/its by current convention), and renders a pill-shaped segmented control in the navbar center. Clicking writes to PathContext + localStorage; the component hides itself on non-volume routes (so the navbar center stays empty on the homepage and elsewhere). To slot it into the navbar I swizzled Navbar/Content (otherwise the only public extension point is themeConfig.navbar.items, which can't render arbitrary React). The original layout had left/right slots; I added a center slot between them. The actual sidebar swap happens in a DocRoot swizzle: useResolvedSidebar reads useDocsVersion().docsSidebars (the full set for the current volume) and replaces sidebarName/sidebarItems in DocsSidebarProvider with the user's pick when one exists and is valid. Falls back to the default Docusaurus selection for any volume not in the registered set and for users who haven't picked a path yet. Verified end-to-end with a second placeholder doc (volumes/programmatore/variabili.mdx) listed under it+liceo but not under its: switching the selector now visibly adds/removes the lesson from the sidebar without a reload, and the choice persists across refreshes. Co-Authored-By: Claude Opus 4.7 --- sidebars/programmatore.ts | 4 +- src/components/PathSelector/index.tsx | 75 +++++++++++ src/components/PathSelector/styles.module.css | 43 +++++++ src/theme/DocRoot/index.tsx | 86 +++++++++++++ src/theme/Navbar/Content/index.tsx | 116 ++++++++++++++++++ src/theme/Navbar/Content/styles.module.css | 23 ++++ volumes/programmatore/variabili.mdx | 7 ++ 7 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 src/components/PathSelector/index.tsx create mode 100644 src/components/PathSelector/styles.module.css create mode 100644 src/theme/DocRoot/index.tsx create mode 100644 src/theme/Navbar/Content/index.tsx create mode 100644 src/theme/Navbar/Content/styles.module.css create mode 100644 volumes/programmatore/variabili.mdx diff --git a/sidebars/programmatore.ts b/sidebars/programmatore.ts index 1804032..8a084f4 100644 --- a/sidebars/programmatore.ts +++ b/sidebars/programmatore.ts @@ -4,8 +4,8 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; // Una sidebar per percorso. Lo stesso doc può comparire in più percorsi // con label/categoria diverse: l'URL del doc resta unico. const sidebars: SidebarsConfig = { - it: ['intro'], - liceo: ['intro'], + it: ['intro', 'variabili'], + liceo: ['intro', 'variabili'], its: ['intro'], }; diff --git a/src/components/PathSelector/index.tsx b/src/components/PathSelector/index.tsx new file mode 100644 index 0000000..08ee8ba --- /dev/null +++ b/src/components/PathSelector/index.tsx @@ -0,0 +1,75 @@ +/** + * PathSelector — segmented control nella navbar per scegliere il percorso + * (IT / Liceo / ITS ecc.) del volume correntemente visitato. Visibile solo + * quando l'utente è dentro un volume; nascosto altrove. + * + * I percorsi disponibili sono dedotti dalle sidebar dichiarate per il + * plugin-content-docs instance del volume attivo: ogni "sidebar name" + * = un percorso. + */ +import React, {type ReactNode} from 'react'; +import { + useActivePlugin, + useAllDocsData, +} from '@docusaurus/plugin-content-docs/client'; +import {usePathContext, type VolumeId} from '@site/src/contexts/PathContext'; + +import styles from './styles.module.css'; + +const VOLUMES: ReadonlySet = new Set([ + 'programmatore', + 'artefice', + 'archivista', +]); + +// Etichetta umana del percorso, mostrata nel toggle. +const PATH_LABEL: Record = { + it: 'IT', + liceo: 'Liceo', + its: 'ITS', +}; + +function labelFor(pathId: string): string { + return PATH_LABEL[pathId] ?? pathId; +} + +export default function PathSelector(): ReactNode { + const activePlugin = useActivePlugin(); + const allData = useAllDocsData(); + const {getPath, setPath} = usePathContext(); + + if (!activePlugin || !VOLUMES.has(activePlugin.pluginId)) { + return null; + } + const volumeId = activePlugin.pluginId as VolumeId; + + // Ricavo i nomi delle sidebar (= percorsi) dichiarate per questa istanza + // leggendo i metadata della versione "current". + const pluginData = allData[volumeId]; + const version = pluginData?.versions?.find((v) => v.name === 'current'); + if (!version) return null; + const pathIds = Object.keys(version.sidebars ?? {}); + if (pathIds.length <= 1) return null; + + // Default: primo percorso dichiarato per il volume. Se l'utente non ha + // ancora scelto nulla, non scriviamo nulla in localStorage — solo evidenza + // visiva. + const stored = getPath(volumeId); + const active = stored ?? pathIds[0]; + + return ( +
+ {pathIds.map((id) => ( + + ))} +
+ ); +} + diff --git a/src/components/PathSelector/styles.module.css b/src/components/PathSelector/styles.module.css new file mode 100644 index 0000000..37829ea --- /dev/null +++ b/src/components/PathSelector/styles.module.css @@ -0,0 +1,43 @@ +.wrap { + display: inline-flex; + align-items: center; + gap: 2px; + padding: 2px; + background: var(--at-bg-subtle); + border: 1px solid var(--at-border); + border-radius: 999px; + margin: 0 8px; +} + +.btn { + appearance: none; + background: transparent; + border: none; + border-radius: 999px; + cursor: pointer; + font-family: var(--font-mono-ui); + font-size: 12px; + font-weight: 500; + letter-spacing: 0.05em; + padding: 4px 10px; + color: var(--at-muted); + transition: + background 0.15s ease, + color 0.15s ease; +} + +.btn:hover { + color: var(--at-fg-strong); +} + +.btn.active { + background: var(--at-accent-bg); + color: var(--at-accent-soft); + font-weight: 600; + box-shadow: inset 0 0 0 1px var(--at-accent-chip-border); +} + +.btn:focus-visible { + outline: 2px solid var(--at-accent); + outline-offset: 1px; +} diff --git a/src/theme/DocRoot/index.tsx b/src/theme/DocRoot/index.tsx new file mode 100644 index 0000000..8e07a01 --- /dev/null +++ b/src/theme/DocRoot/index.tsx @@ -0,0 +1,86 @@ +/** + * Swizzle DocRoot — sostituisce la sidebar default scelta da Docusaurus + * con quella corrispondente al "percorso" attualmente scelto dall'utente + * per il volume corrente, leggendo da PathContext. + * + * Se l'utente non ha scelto nulla, oppure la sidebar selezionata non esiste + * per questo volume, ricade sul default (`sidebarName` ricavato dal doc). + * + * Quando la lezione corrente NON è dentro la sidebar attiva (es. utente sul + * percorso "Liceo" ma è arrivato a una lezione che vive solo in "IT"), + * mostriamo comunque la sidebar attiva: la lezione si rende a tutto schermo + * senza essere evidenziata in sidebar (banner off-path: prossimo step). + */ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import {HtmlClassNameProvider, ThemeClassNames} from '@docusaurus/theme-common'; +import { + DocsSidebarProvider, + useDocRootMetadata, + useDocsVersion, +} from '@docusaurus/plugin-content-docs/client'; +import DocRootLayout from '@theme/DocRoot/Layout'; +import NotFoundContent from '@theme/NotFound/Content'; +import type {Props} from '@theme/DocRoot'; +import type {PropSidebar} from '@docusaurus/plugin-content-docs'; + +import {usePathContext, type VolumeId} from '@site/src/contexts/PathContext'; + +const VOLUMES: ReadonlySet = new Set([ + 'programmatore', + 'artefice', + 'archivista', +]); + +function useResolvedSidebar(defaultName: string | undefined) { + const version = useDocsVersion(); + const {getPath} = usePathContext(); + const pluginId = version.pluginId; + + if (!VOLUMES.has(pluginId)) { + // Pagine docs non-volume: comportamento standard. + return null; + } + const chosen = getPath(pluginId as VolumeId); + if (!chosen) return null; + const sidebars = version.docsSidebars; + if (!sidebars || !(chosen in sidebars)) return null; + if (chosen === defaultName) return null; + return {name: chosen, items: sidebars[chosen]!}; +} + +export default function DocRoot(props: Props): ReactNode { + const currentDocRouteMetadata = useDocRootMetadata(props); + if (!currentDocRouteMetadata) { + return ; + } + const {docElement, sidebarName, sidebarItems} = currentDocRouteMetadata; + return ( + + + + ); +} + +function DocRootInner({ + defaultName, + defaultItems, + docElement, +}: { + defaultName: string | undefined; + defaultItems: PropSidebar | undefined; + docElement: ReactNode; +}) { + const resolved = useResolvedSidebar(defaultName); + const name = resolved?.name ?? defaultName; + const items = resolved?.items ?? defaultItems; + return ( + + {docElement} + + ); +} diff --git a/src/theme/Navbar/Content/index.tsx b/src/theme/Navbar/Content/index.tsx new file mode 100644 index 0000000..cec3b28 --- /dev/null +++ b/src/theme/Navbar/Content/index.tsx @@ -0,0 +1,116 @@ +/** + * Swizzle Navbar/Content — copia dell'originale theme-classic con l'aggiunta + * del nostro al centro fra gli item di sinistra e quelli + * di destra. Il componente PathSelector si nasconde da sé quando l'utente + * non è dentro un volume, quindi sulle altre pagine la navbar resta vuota + * al centro. + */ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import { + useThemeConfig, + ErrorCauseBoundary, + ThemeClassNames, +} from '@docusaurus/theme-common'; +import { + splitNavbarItems, + useNavbarMobileSidebar, +} from '@docusaurus/theme-common/internal'; +import NavbarItem, {type Props as NavbarItemConfig} from '@theme/NavbarItem'; +import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle'; +import SearchBar from '@theme/SearchBar'; +import NavbarMobileSidebarToggle from '@theme/Navbar/MobileSidebar/Toggle'; +import NavbarLogo from '@theme/Navbar/Logo'; +import NavbarSearch from '@theme/Navbar/Search'; + +import PathSelector from '@site/src/components/PathSelector'; + +import styles from './styles.module.css'; + +function useNavbarItems() { + return useThemeConfig().navbar.items as NavbarItemConfig[]; +} + +function NavbarItems({items}: {items: NavbarItemConfig[]}): ReactNode { + return ( + <> + {items.map((item, i) => ( + + new Error( + `A theme navbar item failed to render. +Please double-check the following navbar item (themeConfig.navbar.items) of your Docusaurus config: +${JSON.stringify(item, null, 2)}`, + {cause: error}, + ) + }> + + + ))} + + ); +} + +function NavbarContentLayout({ + left, + center, + right, +}: { + left: ReactNode; + center: ReactNode; + right: ReactNode; +}) { + return ( +
+
+ {left} +
+
{center}
+
+ {right} +
+
+ ); +} + +export default function NavbarContent(): ReactNode { + const mobileSidebar = useNavbarMobileSidebar(); + + const items = useNavbarItems(); + const [leftItems, rightItems] = splitNavbarItems(items); + + const searchBarItem = items.find((item) => item.type === 'search'); + + return ( + + {!mobileSidebar.disabled && } + + + + } + center={} + right={ + <> + + + {!searchBarItem && ( + + + + )} + + } + /> + ); +} diff --git a/src/theme/Navbar/Content/styles.module.css b/src/theme/Navbar/Content/styles.module.css new file mode 100644 index 0000000..eee2127 --- /dev/null +++ b/src/theme/Navbar/Content/styles.module.css @@ -0,0 +1,23 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* +Hide color mode toggle in small viewports + */ +@media (max-width: 996px) { + .colorModeToggle { + display: none; + } +} + +/* +Restore some Infima style that broke with CSS Cascade Layers +See https://github.com/facebook/docusaurus/pull/11142 + */ +:global(.navbar__items--right) > :last-child { + padding-right: 0; +} diff --git a/volumes/programmatore/variabili.mdx b/volumes/programmatore/variabili.mdx new file mode 100644 index 0000000..bfc70a3 --- /dev/null +++ b/volumes/programmatore/variabili.mdx @@ -0,0 +1,7 @@ +--- +sidebar_position: 2 +--- + +# Le variabili + +Placeholder — questa lezione è presente nei percorsi IT e Liceo, NON nell'ITS. From c0403bf2ad33fc97ae6058ef90bb4a3c8a4b7e6f Mon Sep 17 00:00:00 2001 From: Marco Farina Date: Sun, 24 May 2026 20:08:52 +0200 Subject: [PATCH 03/15] feat(volumes): off-path lesson banner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the user opens a lesson whose docId isn't present in their chosen path's sidebar (e.g. lands on /programmatore/variabili while on ITS, but variabili lives only in IT+Liceo), an inline banner appears above the chapter kicker. The banner lists the *other* paths in the same volume that DO contain the lesson; each is rendered as an inline button that calls setPath(volumeId, otherPath). Switching is in-place — no navigation — so the URL stays the same, the sidebar reshuffles to include the lesson, and the banner self-removes. Detection lives in useOffPathInfo (src/components/OffPathBanner) which walks each sidebar's items recursively for a docId match (categories included). Returns null when: - the active plugin isn't one of the three volumes - the user has never picked a path (we don't surprise them) - the lesson IS in the active path's sidebar Path labels (Istituto Tecnico / Liceo / ITS) extracted to src/contexts/pathLabels.ts and reused by both PathSelector (short labels for the navbar pill) and the banner (full labels for prose). Co-Authored-By: Claude Opus 4.7 --- src/components/OffPathBanner/index.tsx | 106 ++++++++++++++++++ .../OffPathBanner/styles.module.css | 80 +++++++++++++ src/components/PathSelector/index.tsx | 14 +-- src/contexts/pathLabels.ts | 22 ++++ src/theme/DocItem/Content/index.tsx | 3 + 5 files changed, 213 insertions(+), 12 deletions(-) create mode 100644 src/components/OffPathBanner/index.tsx create mode 100644 src/components/OffPathBanner/styles.module.css create mode 100644 src/contexts/pathLabels.ts diff --git a/src/components/OffPathBanner/index.tsx b/src/components/OffPathBanner/index.tsx new file mode 100644 index 0000000..992aa16 --- /dev/null +++ b/src/components/OffPathBanner/index.tsx @@ -0,0 +1,106 @@ +/** + * OffPathBanner — avviso mostrato in cima alla pagina di una lezione quando + * la lezione NON fa parte del percorso correntemente scelto dall'utente. + * Elenca in quali altri percorsi del volume la lezione è presente e offre + * un bottone per switchare il percorso (la sidebar si ricostruisce in + * place senza navigare). + */ +import React, {type ReactNode} from 'react'; +import {useDoc, useDocsVersion} from '@docusaurus/plugin-content-docs/client'; +import type {PropSidebar, PropSidebarItem} from '@docusaurus/plugin-content-docs'; +import {usePathContext, type VolumeId} from '@site/src/contexts/PathContext'; +import {pathLabel} from '@site/src/contexts/pathLabels'; + +import styles from './styles.module.css'; + +const VOLUMES: ReadonlySet = new Set([ + 'programmatore', + 'artefice', + 'archivista', +]); + +function sidebarContainsDoc(items: PropSidebar, docId: string): boolean { + for (const item of items as PropSidebarItem[]) { + if (item.type === 'link' && item.docId === docId) return true; + if (item.type === 'category' && sidebarContainsDoc(item.items, docId)) + return true; + } + return false; +} + +function useOffPathInfo() { + const {metadata} = useDoc(); + const version = useDocsVersion(); + const {getPath} = usePathContext(); + + if (!VOLUMES.has(version.pluginId)) return null; + const volumeId = version.pluginId as VolumeId; + const chosen = getPath(volumeId); + if (!chosen) return null; // utente non ha mai scelto → niente banner + + const sidebars = version.docsSidebars ?? {}; + const activeSidebar = sidebars[chosen]; + if (!activeSidebar) return null; + + if (sidebarContainsDoc(activeSidebar as PropSidebar, metadata.id)) { + return null; // la lezione è nel percorso attivo → tutto ok + } + + const availableIn = Object.entries(sidebars) + .filter( + ([name, sb]) => + name !== chosen && + sidebarContainsDoc(sb as PropSidebar, metadata.id), + ) + .map(([name]) => name); + + return {volumeId, chosen, availableIn}; +} + +export default function OffPathBanner(): ReactNode { + const info = useOffPathInfo(); + const {setPath} = usePathContext(); + if (!info) return null; + const {volumeId, chosen, availableIn} = info; + + return ( + + ); +} diff --git a/src/components/OffPathBanner/styles.module.css b/src/components/OffPathBanner/styles.module.css new file mode 100644 index 0000000..32ccef1 --- /dev/null +++ b/src/components/OffPathBanner/styles.module.css @@ -0,0 +1,80 @@ +.banner { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 14px 16px; + margin: 0 0 28px; + border-radius: 10px; + background: rgba(245, 158, 11, 0.06); + border: 1px solid rgba(245, 158, 11, 0.3); + color: var(--at-fg-body); + font-family: var(--font-body); +} + +html[data-theme='dark'] .banner { + background: rgba(251, 191, 36, 0.06); + border-color: rgba(251, 191, 36, 0.28); +} + +.icon { + flex-shrink: 0; + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: #d97706; + background: rgba(245, 158, 11, 0.12); +} + +html[data-theme='dark'] .icon { + color: #fbbf24; + background: rgba(251, 191, 36, 0.12); +} + +.body { + flex: 1; + min-width: 0; +} + +.title { + font-size: 0.95rem; + font-weight: 500; + line-height: 1.45; + margin: 0 0 4px; + color: var(--at-fg-strong); +} + +.title strong { + font-family: var(--font-mono-ui); + font-weight: 600; + letter-spacing: 0.03em; +} + +.detail { + font-size: 0.875rem; + line-height: 1.5; + margin: 0; + color: var(--at-muted-soft); +} + +.switch { + appearance: none; + background: none; + border: none; + padding: 0; + margin: 0; + font: inherit; + font-family: var(--font-mono-ui); + font-size: 0.85em; + letter-spacing: 0.02em; + color: var(--at-accent); + cursor: pointer; + text-decoration: underline; + text-underline-offset: 2px; +} + +.switch:hover { + color: var(--at-accent-soft); +} diff --git a/src/components/PathSelector/index.tsx b/src/components/PathSelector/index.tsx index 08ee8ba..7d597ca 100644 --- a/src/components/PathSelector/index.tsx +++ b/src/components/PathSelector/index.tsx @@ -13,6 +13,7 @@ import { useAllDocsData, } from '@docusaurus/plugin-content-docs/client'; import {usePathContext, type VolumeId} from '@site/src/contexts/PathContext'; +import {pathLabel} from '@site/src/contexts/pathLabels'; import styles from './styles.module.css'; @@ -22,17 +23,6 @@ const VOLUMES: ReadonlySet = new Set([ 'archivista', ]); -// Etichetta umana del percorso, mostrata nel toggle. -const PATH_LABEL: Record = { - it: 'IT', - liceo: 'Liceo', - its: 'ITS', -}; - -function labelFor(pathId: string): string { - return PATH_LABEL[pathId] ?? pathId; -} - export default function PathSelector(): ReactNode { const activePlugin = useActivePlugin(); const allData = useAllDocsData(); @@ -66,7 +56,7 @@ export default function PathSelector(): ReactNode { className={`${styles.btn} ${id === active ? styles.active : ''}`} aria-pressed={id === active} onClick={() => setPath(volumeId, id)}> - {labelFor(id)} + {pathLabel(id, true)} ))} diff --git a/src/contexts/pathLabels.ts b/src/contexts/pathLabels.ts new file mode 100644 index 0000000..f330785 --- /dev/null +++ b/src/contexts/pathLabels.ts @@ -0,0 +1,22 @@ +/** + * Etichette umane dei percorsi didattici, condivise fra PathSelector, + * OffPathBanner e qualsiasi altro componente che debba mostrarle. + * Aggiungi qui se un volume introduce un nuovo percorso. + */ +export const PATH_LABEL: Record = { + it: 'Istituto Tecnico', + liceo: 'Liceo', + its: 'ITS', +}; + +/** Etichetta breve da usare nei controlli compatti (toggle in navbar). */ +export const PATH_LABEL_SHORT: Record = { + it: 'IT', + liceo: 'Liceo', + its: 'ITS', +}; + +export function pathLabel(id: string, short = false): string { + const map = short ? PATH_LABEL_SHORT : PATH_LABEL; + return map[id] ?? id; +} diff --git a/src/theme/DocItem/Content/index.tsx b/src/theme/DocItem/Content/index.tsx index 2748bbd..d053d57 100644 --- a/src/theme/DocItem/Content/index.tsx +++ b/src/theme/DocItem/Content/index.tsx @@ -10,6 +10,8 @@ import type { PropSidebarItem, } from '@docusaurus/plugin-content-docs'; +import OffPathBanner from '@site/src/components/OffPathBanner'; + function useSyntheticTitle(): string | null { const {metadata, frontMatter, contentTitle} = useDoc(); const shouldRender = @@ -62,6 +64,7 @@ export default function DocItemContent({children}: Props): ReactNode { 'markdown', kicker && 'doc-has-chapter-kicker', )}> + {kicker &&

{kicker}

} {syntheticTitle && (
From f725e0d56b75c6df8a13baccc2c0e107ff2285b8 Mon Sep 17 00:00:00 2001 From: Marco Farina Date: Sun, 24 May 2026 22:23:13 +0200 Subject: [PATCH 04/15] polish(off-path-banner): single-line layout + FA icon + vertical alignment fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Banner collapsed from a two-paragraph stack (~120px tall, lots of empty space) into a single horizontal line (~46px). Title and detail are now one continuous sentence: ⓘ Lezione fuori dal percorso [ITS] · Disponibile in [IT] · [Liceo] - Replaced the inline SVG with FontAwesome circle-info (consistent with our ``/`` convention) - "passa a X" prefix dropped — under "Disponibile in:" the bare path name is enough and reads better - Capitalized "Disponibile" and "Non inclusa" for sentence-case consistency after the · separator - align-items: center on the flex container, but the message

wasn't centering because .theme-doc-markdown p (specificity 0,1,1) was adding margin-bottom 1.15em that inflated the banner content area and anchored the

to the top of the line. Bumped specificity with .banner > .message (0,2,0) to wipe the margin. Now icon-cy = msg-cy = banner-cy. Co-Authored-By: Claude Opus 4.7 --- src/components/OffPathBanner/index.tsx | 49 +++++++------ .../OffPathBanner/styles.module.css | 72 ++++++++++--------- 2 files changed, 66 insertions(+), 55 deletions(-) diff --git a/src/components/OffPathBanner/index.tsx b/src/components/OffPathBanner/index.tsx index 992aa16..477a2c7 100644 --- a/src/components/OffPathBanner/index.tsx +++ b/src/components/OffPathBanner/index.tsx @@ -8,6 +8,7 @@ import React, {type ReactNode} from 'react'; import {useDoc, useDocsVersion} from '@docusaurus/plugin-content-docs/client'; import type {PropSidebar, PropSidebarItem} from '@docusaurus/plugin-content-docs'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {usePathContext, type VolumeId} from '@site/src/contexts/PathContext'; import {pathLabel} from '@site/src/contexts/pathLabels'; @@ -68,39 +69,45 @@ export default function OffPathBanner(): ReactNode { className={styles.banner} role="note" aria-label="Avviso sul percorso"> - -

-

- Questa lezione non fa parte del percorso{' '} - {pathLabel(chosen)}. -

+
+

); } diff --git a/src/components/OffPathBanner/styles.module.css b/src/components/OffPathBanner/styles.module.css index 32ccef1..ebffdd6 100644 --- a/src/components/OffPathBanner/styles.module.css +++ b/src/components/OffPathBanner/styles.module.css @@ -1,62 +1,66 @@ .banner { display: flex; - align-items: flex-start; - gap: 12px; - padding: 14px 16px; - margin: 0 0 28px; - border-radius: 10px; + align-items: center; + gap: 10px; + padding: 10px 14px; + margin: 0 0 24px; + border-radius: 8px; background: rgba(245, 158, 11, 0.06); - border: 1px solid rgba(245, 158, 11, 0.3); + border: 1px solid rgba(245, 158, 11, 0.28); + border-left-width: 3px; color: var(--at-fg-body); font-family: var(--font-body); + font-size: 1rem; + line-height: 1.5; } html[data-theme='dark'] .banner { - background: rgba(251, 191, 36, 0.06); + background: rgba(251, 191, 36, 0.05); border-color: rgba(251, 191, 36, 0.28); } .icon { - flex-shrink: 0; - width: 28px; - height: 28px; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 50%; color: #d97706; - background: rgba(245, 158, 11, 0.12); + font-size: 15px; + flex-shrink: 0; } html[data-theme='dark'] .icon { color: #fbbf24; - background: rgba(251, 191, 36, 0.12); } -.body { +.banner > .message { flex: 1; - min-width: 0; -} - -.title { - font-size: 0.95rem; - font-weight: 500; - line-height: 1.45; - margin: 0 0 4px; - color: var(--at-fg-strong); + margin: 0; + font-size: 1rem; + line-height: 1.5; + color: var(--at-fg); } -.title strong { +.tag { font-family: var(--font-mono-ui); + font-size: 0.82em; font-weight: 600; - letter-spacing: 0.03em; + letter-spacing: 0.04em; + padding: 1px 7px; + border-radius: 4px; + background: rgba(245, 158, 11, 0.14); + color: #b45309; } -.detail { - font-size: 0.875rem; - line-height: 1.5; - margin: 0; - color: var(--at-muted-soft); +html[data-theme='dark'] .tag { + background: rgba(251, 191, 36, 0.14); + color: #fbbf24; +} + +.sep { + display: inline-block; + margin: 0 8px; + color: var(--at-faint); +} + +.minisep { + color: var(--at-faint); } .switch { @@ -67,7 +71,7 @@ html[data-theme='dark'] .icon { margin: 0; font: inherit; font-family: var(--font-mono-ui); - font-size: 0.85em; + font-size: 0.88em; letter-spacing: 0.02em; color: var(--at-accent); cursor: pointer; From 8667e680faa183f83545bfb1947dd190782c5f82 Mon Sep 17 00:00:00 2001 From: Marco Farina Date: Sun, 24 May 2026 22:24:32 +0200 Subject: [PATCH 05/15] feat(navbar): Libri dropdown with the three volumes Replaces the single "Libro" entry with a dropdown listing the three volumes (Programmatore, Artefice, Archivista) plus a "Versione precedente" entry pointing at the legacy /docs/intro content until we migrate it into Programmatore. Each volume entry uses type: 'doc' with docsPluginId so Docusaurus highlights the right item when the user is inside that plugin instance. Co-Authored-By: Claude Opus 4.7 --- docusaurus.config.ts | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 1a245eb..0c2d141 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -126,10 +126,35 @@ const config: Config = { }, items: [ { - type: 'docSidebar', - sidebarId: 'docs', + type: 'dropdown', + label: 'Libri', position: 'left', - label: 'Libro', + items: [ + { + type: 'doc', + docId: 'intro', + docsPluginId: 'programmatore', + label: 'Manuale del Programmatore', + }, + { + type: 'doc', + docId: 'intro', + docsPluginId: 'artefice', + label: 'Manuale dell\'Artefice', + }, + { + type: 'doc', + docId: 'intro', + docsPluginId: 'archivista', + label: 'Manuale dell\'Archivista', + }, + // Vecchia versione, sarà rimossa dopo la migrazione dei + // contenuti attuali dentro Programmatore. + { + to: '/docs/intro', + label: 'Versione precedente', + }, + ], }, /* { to: '/blog', From 1bf28004c9833e9986f19ebe082f7e910556f833 Mon Sep 17 00:00:00 2001 From: Marco Farina Date: Sun, 24 May 2026 22:38:58 +0200 Subject: [PATCH 06/15] feat(volumes): fourth volume "Biblioteca dell'Apprendista" + 2x2 home grid Adds a fourth plugin-content-docs instance (apprendista) for exercises and lab projects, alongside the three theory volumes. Bookkeeping spread across: VolumeId union in PathContext, the three VOLUMES sets in PathSelector / OffPathBanner / DocRoot, sidebars/apprendista.ts placeholder, navbar "Libri" dropdown. Homepage hero cards go from 2 to 4 in a natural 2x2 grid (existing CSS already used grid-template-columns: 1fr 1fr): Programmatore (blue), Artefice (pink), Archivista (amber), Apprendista (green). Added two new VolumeCard accent palettes (amber + green) plus two inline SVG icons (database, flask). Card targets switch from legacy /docs/ URLs to the new volume roots. Naming note: "Biblioteca" not "Manuale" because the fourth volume is a curated collection of practice material, not a linear didactic narrative like the other three. Co-Authored-By: Claude Opus 4.7 --- docusaurus.config.ts | 16 +++++++++ sidebars/apprendista.ts | 11 ++++++ src/components/OffPathBanner/index.tsx | 1 + src/components/PathSelector/index.tsx | 1 + src/components/VolumeCard/index.tsx | 28 ++++++++++++++- src/contexts/PathContext.tsx | 13 +++++-- src/pages/index.tsx | 48 ++++++++++++++++++++++---- src/theme/DocRoot/index.tsx | 1 + volumes/apprendista/intro.mdx | 8 +++++ 9 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 sidebars/apprendista.ts create mode 100644 volumes/apprendista/intro.mdx diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 0c2d141..4d891b3 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -108,6 +108,16 @@ const config: Config = { editUrl: 'https://github.com/marcofarina/python-doesnt-byte', }, ], + [ + '@docusaurus/plugin-content-docs', + { + id: 'apprendista', + path: 'volumes/apprendista', + routeBasePath: 'apprendista', + sidebarPath: './sidebars/apprendista.ts', + editUrl: 'https://github.com/marcofarina/python-doesnt-byte', + }, + ], ], themeConfig: { @@ -148,6 +158,12 @@ const config: Config = { docsPluginId: 'archivista', label: 'Manuale dell\'Archivista', }, + { + type: 'doc', + docId: 'intro', + docsPluginId: 'apprendista', + label: 'Biblioteca dell\'Apprendista', + }, // Vecchia versione, sarà rimossa dopo la migrazione dei // contenuti attuali dentro Programmatore. { diff --git a/sidebars/apprendista.ts b/sidebars/apprendista.ts new file mode 100644 index 0000000..a6830f3 --- /dev/null +++ b/sidebars/apprendista.ts @@ -0,0 +1,11 @@ +import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; + +// Biblioteca dell'Apprendista (Volume 4 — esercizi e laboratori). +// Sidebar per percorso, identica struttura agli altri volumi. +const sidebars: SidebarsConfig = { + it: ['intro'], + liceo: ['intro'], + its: ['intro'], +}; + +export default sidebars; diff --git a/src/components/OffPathBanner/index.tsx b/src/components/OffPathBanner/index.tsx index 477a2c7..99d3360 100644 --- a/src/components/OffPathBanner/index.tsx +++ b/src/components/OffPathBanner/index.tsx @@ -18,6 +18,7 @@ const VOLUMES: ReadonlySet = new Set([ 'programmatore', 'artefice', 'archivista', + 'apprendista', ]); function sidebarContainsDoc(items: PropSidebar, docId: string): boolean { diff --git a/src/components/PathSelector/index.tsx b/src/components/PathSelector/index.tsx index 7d597ca..2a3deba 100644 --- a/src/components/PathSelector/index.tsx +++ b/src/components/PathSelector/index.tsx @@ -21,6 +21,7 @@ const VOLUMES: ReadonlySet = new Set([ 'programmatore', 'artefice', 'archivista', + 'apprendista', ]); export default function PathSelector(): ReactNode { diff --git a/src/components/VolumeCard/index.tsx b/src/components/VolumeCard/index.tsx index 0267a20..a3f19ae 100644 --- a/src/components/VolumeCard/index.tsx +++ b/src/components/VolumeCard/index.tsx @@ -2,7 +2,7 @@ import { useRef, type CSSProperties, type ReactNode } from 'react'; import Link from '@docusaurus/Link'; import styles from './styles.module.css'; -type Accent = 'blue' | 'pink'; +type Accent = 'blue' | 'pink' | 'amber' | 'green'; interface VolumeCardProps { to: string; @@ -40,6 +40,32 @@ const PALETTES: Record = { ['--vol-icon-color' as string]: '#a21caf', ['--vol-kicker-color' as string]: '#a21caf', }, + amber: { + ['--vol-glow' as string]: 'rgba(245,158,11,0.28)', + ['--vol-shadow' as string]: 'rgba(245,158,11,0.4)', + ['--vol-ring' as string]: 'rgba(245,158,11,0.4)', + ['--vol-beam-1' as string]: '#f59e0b', + ['--vol-beam-2' as string]: '#fb923c', + ['--vol-beam-3' as string]: '#ef4444', + ['--vol-icon-bg' as string]: + 'linear-gradient(135deg, rgba(245,158,11,0.15), rgba(251,146,60,0.15))', + ['--vol-icon-border' as string]: 'rgba(245,158,11,0.3)', + ['--vol-icon-color' as string]: '#b45309', + ['--vol-kicker-color' as string]: '#b45309', + }, + green: { + ['--vol-glow' as string]: 'rgba(34,197,94,0.28)', + ['--vol-shadow' as string]: 'rgba(34,197,94,0.4)', + ['--vol-ring' as string]: 'rgba(34,197,94,0.4)', + ['--vol-beam-1' as string]: '#10b981', + ['--vol-beam-2' as string]: '#14b8a6', + ['--vol-beam-3' as string]: '#06b6d4', + ['--vol-icon-bg' as string]: + 'linear-gradient(135deg, rgba(34,197,94,0.15), rgba(20,184,166,0.15))', + ['--vol-icon-border' as string]: 'rgba(34,197,94,0.3)', + ['--vol-icon-color' as string]: '#15803d', + ['--vol-kicker-color' as string]: '#15803d', + }, }; function ArrowRight() { diff --git a/src/contexts/PathContext.tsx b/src/contexts/PathContext.tsx index d3a214f..90f74c3 100644 --- a/src/contexts/PathContext.tsx +++ b/src/contexts/PathContext.tsx @@ -14,7 +14,11 @@ import React, { type ReactNode, } from 'react'; -export type VolumeId = 'programmatore' | 'artefice' | 'archivista'; +export type VolumeId = + | 'programmatore' + | 'artefice' + | 'archivista' + | 'apprendista'; const STORAGE_PREFIX = 'pdb:path'; const storageKey = (volume: VolumeId) => `${STORAGE_PREFIX}:${volume}`; @@ -35,7 +39,12 @@ const PathContext = createContext(null); function readInitialState(): PathState { if (typeof window === 'undefined') return {}; const out: PathState = {}; - for (const v of ['programmatore', 'artefice', 'archivista'] as VolumeId[]) { + for (const v of [ + 'programmatore', + 'artefice', + 'archivista', + 'apprendista', + ] as VolumeId[]) { try { const raw = window.localStorage.getItem(storageKey(v)); if (raw) out[v] = raw; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 47e1173..0f66693 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -28,6 +28,26 @@ function BoxIcon() { ); } +function DatabaseIcon() { + return ( + + ); +} + +function FlaskIcon() { + return ( + + ); +} + const HERO_CODE = `# Il tuo primo programma nome = input("Come ti chiami? ") print(f"Ciao, {nome}!") @@ -88,21 +108,37 @@ export default function Home(): JSX.Element { style={{ animationDelay: '0.4s' }} > } accent="blue" /> } accent="pink" /> + } + accent="amber" + /> + } + accent="green" + /> diff --git a/src/theme/DocRoot/index.tsx b/src/theme/DocRoot/index.tsx index 8e07a01..afbffed 100644 --- a/src/theme/DocRoot/index.tsx +++ b/src/theme/DocRoot/index.tsx @@ -30,6 +30,7 @@ const VOLUMES: ReadonlySet = new Set([ 'programmatore', 'artefice', 'archivista', + 'apprendista', ]); function useResolvedSidebar(defaultName: string | undefined) { diff --git a/volumes/apprendista/intro.mdx b/volumes/apprendista/intro.mdx new file mode 100644 index 0000000..b6171d4 --- /dev/null +++ b/volumes/apprendista/intro.mdx @@ -0,0 +1,8 @@ +--- +slug: / +sidebar_position: 1 +--- + +# Biblioteca dell'Apprendista + +Volume 4 — esercizi e laboratori. Placeholder. From 30cb06eaf396a3d105e55f547bae5a64366bf43c Mon Sep 17 00:00:00 2001 From: Marco Farina Date: Sun, 24 May 2026 22:53:57 +0200 Subject: [PATCH 07/15] polish(navbar): two-tone title + coffee icon + cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Title: "Python doesn't byte" rendered as two spans via a Navbar/Logo swizzle. "Python" is fg-strong, "doesn't byte" is muted italic in Crimson Pro, matching the design's navbar treatment (which inspired the homepage hero word split). Icons: both .github-link and .sponsorship-link now use a mask-image + background-color: currentColor pattern instead of hardcoded SVG fills. This means the icon color follows the link color (and any :hover/light/dark variant) without needing a separate SVG per state. GitHub previously turned green on hover because the hover SVG had a baked-in #25c2a0 fill; now both icons fade to var(--at-accent) on hover via a plain CSS color transition. Replaced the heart icon (Support sponsorship link) with a coffee-mug glyph (Lucide style) and renamed the navbar entry "Support" → "Offrimi un caffè". Removed the Rainbow Bits navbar entry and the unused .rainbowbits-link + .designedBy/.heart CSS leftovers from the Docusaurus template. Removed the "Versione precedente" entry from the Libri dropdown — the legacy /docs/ content stays reachable by direct URL and via the footer until we migrate it into Programmatore. Co-Authored-By: Claude Opus 4.7 --- docusaurus.config.ts | 15 +-- src/css/custom.css | 129 +++++------------------- src/theme/Navbar/Logo/index.tsx | 40 ++++++++ src/theme/Navbar/Logo/styles.module.css | 19 ++++ 4 files changed, 87 insertions(+), 116 deletions(-) create mode 100644 src/theme/Navbar/Logo/index.tsx create mode 100644 src/theme/Navbar/Logo/styles.module.css diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 4d891b3..ffed8bb 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -164,12 +164,6 @@ const config: Config = { docsPluginId: 'apprendista', label: 'Biblioteca dell\'Apprendista', }, - // Vecchia versione, sarà rimossa dopo la migrazione dei - // contenuti attuali dentro Programmatore. - { - to: '/docs/intro', - label: 'Versione precedente', - }, ], }, /* { @@ -178,7 +172,7 @@ const config: Config = { position: 'left'},*/ { to: '/support/', - label: 'Support', + label: 'Offrimi un caffè', position: 'right', className: 'sponsorship-link', }, @@ -190,13 +184,6 @@ const config: Config = { className: 'github-link', 'aria-label': 'GitHub repository', }, - { - to: 'https://www.rainbowbits.cloud', - label: 'Rainbow Bits', - position: 'right', - target: '_blank', - className: 'rainbowbits-link', - }, ], }, footer: { diff --git a/src/css/custom.css b/src/css/custom.css index b5ae94b..8909607 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -245,119 +245,44 @@ html[data-theme='dark'] .hero { margin-bottom: 10px; }*/ -/* Header Icons */ +/* Header icons — line-style, follow `color` via mask + currentColor */ -html[data-theme='dark'] .github-link { +.github-link, +.sponsorship-link { align-items: center; display: flex; - - &:before { - align-self: center; - background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij48cGF0aCBmaWxsPSIjZTNlM2UzIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik04IDBDMy41OCAwIDAgMy41OCAwIDhjMCAzLjU0IDIuMjkgNi41MyA1LjQ3IDcuNTkuNC4wNy41NS0uMTcuNTUtLjM4IDAtLjE5LS4wMS0uODItLjAxLTEuNDktMi4wMS4zNy0yLjUzLS40OS0yLjY5LS45NC0uMDktLjIzLS40OC0uOTQtLjgyLTEuMTMtLjI4LS4xNS0uNjgtLjUyLS4wMS0uNTMuNjMtLjAxIDEuMDguNTggMS4yMy44Mi43MiAxLjIxIDEuODcuODcgMi4zMy42Ni4wNy0uNTIuMjgtLjg3LjUxLTEuMDctMS43OC0uMi0zLjY0LS44OS0zLjY0LTMuOTUgMC0uODcuMzEtMS41OS44Mi0yLjE1LS4wOC0uMi0uMzYtMS4wMi4wOC0yLjEyIDAgMCAuNjctLjIxIDIuMi44Mi42NC0uMTggMS4zMi0uMjcgMi0uMjcuNjggMCAxLjM2LjA5IDIgLjI3IDEuNTMtMS4wNCAyLjItLjgyIDIuMi0uODIuNDQgMS4xLjE2IDEuOTIuMDggMi4xMi41MS41Ni44MiAxLjI3LjgyIDIuMTUgMCAzLjA3LTEuODcgMy43NS0zLjY1IDMuOTUuMjkuMjUuNTQuNzMuNTQgMS40OCAwIDEuMDctLjAxIDEuOTMtLjAxIDIuMiAwIC4yMS4xNS40Ni41NS4zOEE4LjAxMyA4LjAxMyAwIDAwMTYgOGMwLTQuNDItMy41OC04LTgtOHoiLz48L3N2Zz4=') 0 0 / contain; - content: ''; - display: inline-flex; - height: 24px; - width: 24px; - margin-right: 0.5rem; - color: var(--ifm-navbar-link-color); - } - - &:hover { - &:before { - background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij48cGF0aCBmaWxsPSIjMjVjMmEwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik04IDBDMy41OCAwIDAgMy41OCAwIDhjMCAzLjU0IDIuMjkgNi41MyA1LjQ3IDcuNTkuNC4wNy41NS0uMTcuNTUtLjM4IDAtLjE5LS4wMS0uODItLjAxLTEuNDktMi4wMS4zNy0yLjUzLS40OS0yLjY5LS45NC0uMDktLjIzLS40OC0uOTQtLjgyLTEuMTMtLjI4LS4xNS0uNjgtLjUyLS4wMS0uNTMuNjMtLjAxIDEuMDguNTggMS4yMy44Mi43MiAxLjIxIDEuODcuODcgMi4zMy42Ni4wNy0uNTIuMjgtLjg3LjUxLTEuMDctMS43OC0uMi0zLjY0LS44OS0zLjY0LTMuOTUgMC0uODcuMzEtMS41OS44Mi0yLjE1LS4wOC0uMi0uMzYtMS4wMi4wOC0yLjEyIDAgMCAuNjctLjIxIDIuMi44Mi42NC0uMTggMS4zMi0uMjcgMi0uMjcuNjggMCAxLjM2LjA5IDIgLjI3IDEuNTMtMS4wNCAyLjItLjgyIDIuMi0uODIuNDQgMS4xLjE2IDEuOTIuMDggMi4xMi41MS41Ni44MiAxLjI3LjgyIDIuMTUgMCAzLjA3LTEuODcgMy43NS0zLjY1IDMuOTUuMjkuMjUuNTQuNzMuNTQgMS40OCAwIDEuMDctLjAxIDEuOTMtLjAxIDIuMiAwIC4yMS4xNS40Ni41NS4zOEE4LjAxMyA4LjAxMyAwIDAwMTYgOGMwLTQuNDItMy41OC04LTgtOHoiLz48L3N2Zz4=') 0 0 / contain; - } - } + transition: color 0.15s ease; } -html[data-theme='light'] .github-link { - align-items: center; - display: flex; - - &:before { - align-self: center; - background: url('data:image/svg+xml;base64,PHN2ZyAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgd2lkdGg9IjI0IiAgaGVpZ2h0PSIyNCIgIHZpZXdCb3g9IjAgMCAyNCAyNCIgIGZpbGw9Im5vbmUiICBzdHJva2U9ImN1cnJlbnRDb2xvciIgIHN0cm9rZS13aWR0aD0iMiIgIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgIHN0cm9rZS1saW5lam9pbj0icm91bmQiICBjbGFzcz0iaWNvbiBpY29uLXRhYmxlciBpY29ucy10YWJsZXItb3V0bGluZSBpY29uLXRhYmxlci1icmFuZC1naXRodWIiPjxwYXRoIHN0cm9rZT0ibm9uZSIgZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik05IDE5Yy00LjMgMS40IC00LjMgLTIuNSAtNiAtM20xMiA1di0zLjVjMCAtMSAuMSAtMS40IC0uNSAtMmMyLjggLS4zIDUuNSAtMS40IDUuNSAtNmE0LjYgNC42IDAgMCAwIC0xLjMgLTMuMmE0LjIgNC4yIDAgMCAwIC0uMSAtMy4ycy0xLjEgLS4zIC0zLjUgMS4zYTEyLjMgMTIuMyAwIDAgMCAtNi4yIDBjLTIuNCAtMS42IC0zLjUgLTEuMyAtMy41IC0xLjNhNC4yIDQuMiAwIDAgMCAtLjEgMy4yYTQuNiA0LjYgMCAwIDAgLTEuMyAzLjJjMCA0LjYgMi43IDUuNyA1LjUgNmMtLjYgLjYgLS42IDEuMiAtLjUgMnYzLjUiIC8+PC9zdmc+ ') 0 0 / contain; - content: ''; - display: inline-flex; - height: 24px; - width: 24px; - margin-right: 0.5rem; - color: var(--ifm-navbar-link-color); - } - - &:hover { - &:before { - background: url('data:image/svg+xml;base64,PHN2ZyAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgd2lkdGg9IjI0IiAgaGVpZ2h0PSIyNCIgIHZpZXdCb3g9IjAgMCAyNCAyNCIgIGZpbGw9Im5vbmUiICBzdHJva2U9ImN1cnJlbnRDb2xvciIgIHN0cm9rZS13aWR0aD0iMiIgIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgIHN0cm9rZS1saW5lam9pbj0icm91bmQiICBjbGFzcz0iaWNvbiBpY29uLXRhYmxlciBpY29ucy10YWJsZXItb3V0bGluZSBpY29uLXRhYmxlci1icmFuZC1naXRodWIiPjxwYXRoIHN0cm9rZT0ibm9uZSIgZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik05IDE5Yy00LjMgMS40IC00LjMgLTIuNSAtNiAtM20xMiA1di0zLjVjMCAtMSAuMSAtMS40IC0uNSAtMmMyLjggLS4zIDUuNSAtMS40IDUuNSAtNmE0LjYgNC42IDAgMCAwIC0xLjMgLTMuMmE0LjIgNC4yIDAgMCAwIC0uMSAtMy4ycy0xLjEgLS4zIC0zLjUgMS4zYTEyLjMgMTIuMyAwIDAgMCAtNi4yIDBjLTIuNCAtMS42IC0zLjUgLTEuMyAtMy41IC0xLjNhNC4yIDQuMiAwIDAgMCAtLjEgMy4yYTQuNiA0LjYgMCAwIDAgLTEuMyAzLjJjMCA0LjYgMi43IDUuNyA1LjUgNmMtLjYgLjYgLS42IDEuMiAtLjUgMnYzLjUiIC8+PC9zdmc+') 0 0 / contain; - } - } +.github-link::before, +.sponsorship-link::before { + content: ''; + display: inline-block; + width: 18px; + height: 18px; + margin-right: 0.45rem; + background-color: currentColor; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; } -@keyframes heart { - - 0%, - 40%, - 80%, - 100% { - transform: scale(1); - } - - 20%, - 60% { - transform: scale(1.15); - } +.github-link::before { + -webkit-mask-image: url("data:image/svg+xml;utf8,"); + mask-image: url("data:image/svg+xml;utf8,"); } -.sponsorship-link { - align-items: center; - display: flex; - - &:before { - align-self: center; - background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjYzk2MTk4IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik02LjczNiA0QzQuNjU3IDQgMi41IDUuODggMi41IDguNTE0YzAgMy4xMDcgMi4zMjQgNS45NiA0Ljg2MSA4LjEyYTI5LjY2IDI5LjY2IDAgMDA0LjU2NiAzLjE3NWwuMDczLjA0MS4wNzMtLjA0Yy4yNzEtLjE1My42NjEtLjM4IDEuMTMtLjY3NC45NC0uNTg4IDIuMTktMS40NDEgMy40MzYtMi41MDIgMi41MzctMi4xNiA0Ljg2MS01LjAxMyA0Ljg2MS04LjEyQzIxLjUgNS44OCAxOS4zNDMgNCAxNy4yNjQgNGMtMi4xMDYgMC0zLjgwMSAxLjM4OS00LjU1MyAzLjY0M2EuNzUuNzUgMCAwMS0xLjQyMiAwQzEwLjUzNyA1LjM4OSA4Ljg0MSA0IDYuNzM2IDR6TTEyIDIwLjcwM2wuMzQzLjY2N2EuNzUuNzUgMCAwMS0uNjg2IDBsLjM0My0uNjY3ek0xIDguNTEzQzEgNS4wNTMgMy44MjkgMi41IDYuNzM2IDIuNSA5LjAzIDIuNSAxMC44ODEgMy43MjYgMTIgNS42MDUgMTMuMTIgMy43MjYgMTQuOTcgMi41IDE3LjI2NCAyLjUgMjAuMTcgMi41IDIzIDUuMDUyIDIzIDguNTE0YzAgMy44MTgtMi44MDEgNy4wNi01LjM4OSA5LjI2MmEzMS4xNDYgMzEuMTQ2IDAgMDEtNS4yMzMgMy41NzZsLS4wMjUuMDEzLS4wMDcuMDAzLS4wMDIuMDAxLS4zNDQtLjY2Ni0uMzQzLjY2Ny0uMDAzLS4wMDItLjAwNy0uMDAzLS4wMjUtLjAxM0EyOS4zMDggMjkuMzA4IDAgMDExMCAyMC40MDhhMzEuMTQ3IDMxLjE0NyAwIDAxLTMuNjExLTIuNjMyQzMuOCAxNS41NzMgMSAxMi4zMzIgMSA4LjUxNHoiLz48L3N2Zz4=') 0 0 / contain; - content: ''; - display: inline-flex; - height: 24px; - width: 24px; - margin-right: 0.5rem; - color: var(--ifm-navbar-link-color); - } - - &:hover { - color: #c96198; - - &:before { - animation: heart 2000ms infinite; - } - } -} - -.rainbowbits-link { - align-items: center; - display: flex; - - &:before { - align-self: center; - background: url('/static/img/icons/rb-icon.svg') 0 0 / contain; - content: ''; - display: inline-flex; - height: 24px; - width: 24px; - margin-right: 0.5rem; - } +.sponsorship-link::before { + -webkit-mask-image: url("data:image/svg+xml;utf8,"); + mask-image: url("data:image/svg+xml;utf8,"); } -.designedBy { - display: inline-flex; - align-items: center; - - .heart { - margin: 0.25rem; - animation: heart 1500ms infinite; - fill: red; - } - - a { - margin-inline-start: 0.25rem; - } +.github-link:hover, +.sponsorship-link:hover { + color: var(--at-accent); } /* Font */ diff --git a/src/theme/Navbar/Logo/index.tsx b/src/theme/Navbar/Logo/index.tsx new file mode 100644 index 0000000..b54f04f --- /dev/null +++ b/src/theme/Navbar/Logo/index.tsx @@ -0,0 +1,40 @@ +/** + * Swizzle Navbar/Logo — title two-tone ispirato al design Atmospheric: + * "Python" in colore fg-strong + "doesn't byte" in muted italic. + * Lascia il link e l'immagine del logo come da configurazione standard. + */ +import React, {type ReactNode} from 'react'; +import Link from '@docusaurus/Link'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import {useThemeConfig} from '@docusaurus/theme-common'; +import ThemedImage from '@theme/ThemedImage'; + +import styles from './styles.module.css'; + +export default function NavbarLogo(): ReactNode { + const { + navbar: {logo}, + } = useThemeConfig(); + const logoLink = useBaseUrl(logo?.href ?? '/'); + const logoSrc = useBaseUrl(logo?.src); + const logoDarkSrc = useBaseUrl(logo?.srcDark ?? logo?.src); + + return ( + + {logo && ( +
+ +
+ )} + + Python{' '} + doesn't byte + + + ); +} diff --git a/src/theme/Navbar/Logo/styles.module.css b/src/theme/Navbar/Logo/styles.module.css new file mode 100644 index 0000000..4cadb7f --- /dev/null +++ b/src/theme/Navbar/Logo/styles.module.css @@ -0,0 +1,19 @@ +.title { + font-family: var(--font-display); + font-size: 16px; + font-weight: 600; + letter-spacing: -0.01em; + display: inline-flex; + align-items: baseline; + gap: 0.28em; +} + +.brand { + color: var(--at-fg-strong); +} + +.tagline { + color: var(--at-muted); + font-style: italic; + font-weight: 400; +} From 6de090ee725c27ab27c3b7ab335f7ad9d02ba6d8 Mon Sep 17 00:00:00 2001 From: Marco Farina Date: Sun, 24 May 2026 23:02:15 +0200 Subject: [PATCH 08/15] polish(navbar): icon-only round buttons with neon tooltips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub and "Offrimi un caffè" navbar links rebuilt to match the ColorModeToggle visually: 2rem round buttons with just the icon glyph, label collapsed via font-size: 0 (kept in DOM + aria-label for SR). Hover paints a soft bg and tints the icon accent. On hover/focus the link's ::after expands into a neon popup tooltip with the human label — the same Lumos/Nox treatment: GitHub → cyan (matches our default accent) Offrimi un caffè → warm amber/brown (#c8845c border, #f0b072 text, brown box-shadow) Specificity quirk: .navbar__link sits later in the file and was overriding .github-link's font-size: 0. Bumped to .navbar__link.github-link (0,2,0) so the icon-only collapse actually wins. Added aria-label to the sponsorship navbar item too (GitHub already had one) so screen readers still announce both even with visible text hidden. Co-Authored-By: Claude Opus 4.7 --- docusaurus.config.ts | 1 + src/css/custom.css | 103 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 95 insertions(+), 9 deletions(-) diff --git a/docusaurus.config.ts b/docusaurus.config.ts index ffed8bb..9ded4f1 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -175,6 +175,7 @@ const config: Config = { label: 'Offrimi un caffè', position: 'right', className: 'sponsorship-link', + 'aria-label': 'Offrimi un caffè', }, { to: 'https://github.com/marcofarina/python-doesnt-byte', diff --git a/src/css/custom.css b/src/css/custom.css index 8909607..f378cfd 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -245,13 +245,42 @@ html[data-theme='dark'] .hero { margin-bottom: 10px; }*/ -/* Header icons — line-style, follow `color` via mask + currentColor */ +/* Header icons — circular buttons (stile ColorModeToggle) with neon + popup tooltip on hover. The visible link text is squashed to 0 so + we keep accessibility (aria-label + DOM text) while the user sees + only the icon glyph; the human label lives in the ::after popup. */ + +@keyframes pdb-iconPopupPulse { + 0%, + 100% { + opacity: 0.55; + } + 50% { + opacity: 1; + } +} -.github-link, -.sponsorship-link { +.navbar__link.github-link, +.navbar__link.sponsorship-link { + position: relative; + width: 2rem; + height: 2rem; + padding: 0 !important; + margin: 0 2px; + border-radius: 50%; + display: inline-flex; align-items: center; - display: flex; - transition: color 0.15s ease; + justify-content: center; + font-size: 0; + transition: + background var(--ifm-transition-fast), + color 0.15s ease; +} + +.navbar__link.github-link:hover, +.navbar__link.sponsorship-link:hover { + background: var(--at-bg-subtle); + color: var(--at-accent); } .github-link::before, @@ -260,7 +289,7 @@ html[data-theme='dark'] .hero { display: inline-block; width: 18px; height: 18px; - margin-right: 0.45rem; + margin: 0; background-color: currentColor; -webkit-mask-size: contain; mask-size: contain; @@ -280,9 +309,65 @@ html[data-theme='dark'] .hero { mask-image: url("data:image/svg+xml;utf8,"); } -.github-link:hover, -.sponsorship-link:hover { - color: var(--at-accent); +/* ── Neon popup tooltips for navbar icon-buttons ───────────────── */ + +.github-link::after, +.sponsorship-link::after { + position: absolute; + top: calc(100% + 12px); + right: 0; + transform: translate(0, 4px); + padding: 6px 12px; + border-radius: 6px; + background: rgba(7, 9, 13, 0.92); + font-family: var(--font-mono-ui); + font-size: 12px; + font-weight: 600; + letter-spacing: 0.06em; + white-space: nowrap; + pointer-events: none; + opacity: 0; + transition: + opacity 0.18s ease, + transform 0.18s ease; + z-index: 100; + border: 1px solid; +} + +.github-link::after { + content: 'GitHub'; + color: var(--at-accent-soft); + border-color: var(--at-accent); + text-shadow: + 0 0 8px var(--at-accent), + 0 0 16px rgba(56, 189, 248, 0.4); + box-shadow: + 0 0 0 1px rgba(56, 189, 248, 0.2), + 0 0 18px -2px var(--at-accent), + 0 0 32px -8px var(--at-accent-soft), + inset 0 0 8px rgba(56, 189, 248, 0.08); +} + +.sponsorship-link::after { + content: 'Offrimi un caffè'; + color: #f0b072; + border-color: #c8845c; + text-shadow: + 0 0 8px #c8845c, + 0 0 16px rgba(180, 83, 9, 0.4); + box-shadow: + 0 0 0 1px rgba(200, 132, 92, 0.25), + 0 0 18px -2px #c8845c, + 0 0 32px -8px #b45309, + inset 0 0 8px rgba(180, 83, 9, 0.1); +} + +.github-link:hover::after, +.github-link:focus-visible::after, +.sponsorship-link:hover::after, +.sponsorship-link:focus-visible::after { + opacity: 1; + transform: translate(0, 0); } /* Font */ From 9847b67dffcd5ceb60e6f499c66594989338494f Mon Sep 17 00:00:00 2001 From: Marco Farina Date: Mon, 25 May 2026 00:29:17 +0200 Subject: [PATCH 09/15] refactor(navbar): unified icon buttons with FA icons + popup carets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub and "Offrimi un caffè" rebuilt as a shared React component (NavbarIconButton) instead of CSS-only ::before/::after on config-driven navbar items. This gives them the same DOM shape as ColorModeToggle — round 2rem button + FontAwesomeIcon + a proper tooltip with two pseudo-element carets (border color + bg color stacked) pointing at the icon. Tooltips now have the same upward caret as Lumos/Nox; the previous CSS-only ::after approach couldn't draw an arrow outside its own box without ugly tricks. Two accent variants: cyan (`--at-accent` family) for GitHub, amber/brown for the coffee button (#c8845c border, #f0b072 text, warm box-shadow). ColorModeToggle switched from Docusaurus' IconLightMode / IconDarkMode / IconSystemColorMode to FontAwesome sun / moon / circle-half-stroke at 18px — same family + sizing as the new buttons, so all four icons in the navbar right rail look unified. mug-hot is rendered at 20px because the steam puffs push it visually smaller otherwise. FontAwesome library.add(fab, fas) moved from MDXComponents.tsx (which is only loaded on MDX-rendered routes) to Root.tsx so non-MDX pages like the homepage get the icons registered. GitHub and sponsorship links removed from themeConfig.navbar.items — they're rendered directly by the Navbar/Content swizzle now. Old per-class CSS in custom.css (mask SVGs + ::after tooltips) deleted. Co-Authored-By: Claude Opus 4.7 --- docusaurus.config.ts | 18 +-- src/components/NavbarIconButton/index.tsx | 57 +++++++ .../NavbarIconButton/styles.module.css | 153 ++++++++++++++++++ src/css/custom.css | 127 +-------------- src/theme/ColorModeToggle/index.tsx | 13 +- src/theme/ColorModeToggle/styles.module.css | 1 + src/theme/Navbar/Content/index.tsx | 19 +++ src/theme/Root.tsx | 15 +- 8 files changed, 255 insertions(+), 148 deletions(-) create mode 100644 src/components/NavbarIconButton/index.tsx create mode 100644 src/components/NavbarIconButton/styles.module.css diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 9ded4f1..e502fd3 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -170,21 +170,9 @@ const config: Config = { to: '/blog', label: 'Blog', position: 'left'},*/ - { - to: '/support/', - label: 'Offrimi un caffè', - position: 'right', - className: 'sponsorship-link', - 'aria-label': 'Offrimi un caffè', - }, - { - to: 'https://github.com/marcofarina/python-doesnt-byte', - label: 'GitHub', - position: 'right', - target: '_blank', - className: 'github-link', - 'aria-label': 'GitHub repository', - }, + // GitHub e "Offrimi un caffè" sono renderizzati come icone+popup + // (NavbarIconButton) dal swizzle src/theme/Navbar/Content, non + // tramite navbar items standard. ], }, footer: { diff --git a/src/components/NavbarIconButton/index.tsx b/src/components/NavbarIconButton/index.tsx new file mode 100644 index 0000000..0414252 --- /dev/null +++ b/src/components/NavbarIconButton/index.tsx @@ -0,0 +1,57 @@ +/** + * NavbarIconButton — un pulsante icona-solo per la navbar, visualmente + * gemello del ColorModeToggle: 2rem rotondo, FA icon, popup neon al hover + * con caret che punta al pulsante. Il colore del popup è parametrizzato + * (es. cyan per link funzionali tipo GitHub, ambra per il caffè). + */ +import React, {type ReactNode} from 'react'; +import Link from '@docusaurus/Link'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import type {IconProp} from '@fortawesome/fontawesome-svg-core'; + +import styles from './styles.module.css'; + +export type IconButtonAccent = 'cyan' | 'amber'; + +interface Props { + to: string; + ariaLabel: string; + tooltip: string; + /** FontAwesome icon, es. `['fab', 'github']`. */ + icon: IconProp; + /** Una taglia extra-larga è utile per icone con elementi sopra (es. mug-hot). */ + iconSize?: number; + accent?: IconButtonAccent; + target?: string; + rel?: string; +} + +export default function NavbarIconButton({ + to, + ariaLabel, + tooltip, + icon, + iconSize = 18, + accent = 'cyan', + target, + rel, +}: Props): ReactNode { + return ( + + +