diff --git a/.agents/skills/common-library/SKILL.md b/.agents/skills/common-library/SKILL.md new file mode 100644 index 0000000..c4cddc4 --- /dev/null +++ b/.agents/skills/common-library/SKILL.md @@ -0,0 +1,168 @@ +--- +name: elixforms-common-library +description: > + Documentazione della libreria condivisa common/ del progetto elixForms. + Descrive i componenti ElixForms, il pattern di form field factory, + le interfacce TypeScript, i tipi, e l'helper per i query parameter. + Attiva questa skill quando lavori sui componenti condivisi, crei nuovi tipi di campo, + modifichi la gestione dello stato del form, o integri con la piattaforma elixForms. +--- + +# Libreria Common elixForms + +## Panoramica + +La cartella `react/common/` contiene la libreria condivisa di componenti React/TypeScript usata da tutte le pagine custom. È basata su **Fluent UI React v8** e implementa un sistema di form con campi dinamici. + +## Architettura dei Componenti + +``` +ElixFormsComponentAbstract (abstract class, extends React.Component) + └── ElixFormsComponent (classe concreta, exported as ElixFormsReact) + └── Usata nelle pagine come `new ElixForms.ElixFormsReact(props)` +``` + +### Gerarchia delle classi + +1. **`ElixFormsComponentAbstract`** (`ElixFormsComponentAbstract.tsx`) + - Classe astratta che estende `React.Component` + - Contiene tutta la logica di creazione dei campi form (factory methods) + - Gestisce lo state del form (`formData: { [key: string]: any }`) + - Implementa `render()` che genera `
` con action POST verso elixForms + +2. **`ElixFormsComponent`** (`ElixFormsComponent.tsx`) + - Classe concreta che estende `ElixFormsComponentAbstract` + - Esportata come `ElixFormsReact` per uso nelle pagine + - Attualmente vuota (la logica è nell'abstract) + +## Interfacce TypeScript + +### `IElixFormsComponentProperties` (Props) +```typescript +interface IElixFormsComponentProperties { + description: string; + isDarkTheme: boolean; + environmentMessage: string; + hasTeamsContext: boolean; + userDisplayName: string; + additionalFieldsJson?: string; // JSON schema per campi dinamici + customFormFieldsConfiguration?: ( // Callback con factory per campi custom + formFieldFactory: IElixFormsComponentCustomFormFieldFactory + ) => JSX.Element; +} +``` + +### `IElixFormsComponentFormState` (State) +```typescript +interface IElixFormsComponentFormState { + formData: { [key: string]: any }; +} +``` + +### `IElixFormsComponentCustomFormFieldFactory` (Form Field Factory) +```typescript +type IElixFormsComponentCustomFormFieldFactory = { + createBooleanInput(name: string, label: string, required?: boolean): JSX.Element; + createCheckboxInput(name: string, label: string, options: ElixFormsCheckboxOption[]): JSX.Element; + createDropdownInput(name: string, label: string, options: ElixFormsDropdownOption[], required?: boolean): JSX.Element; + createHiddenInput(name: string): JSX.Element; + createNumberInput(name: string, label: string, required?: boolean): JSX.Element; + createRadioInput(name: string, label: string, options: ElixFormsRadioOption[], required?: boolean): JSX.Element; + createTextInput(name: string, label: string, required?: boolean): JSX.Element; + createTextAreaInput(name: string, label: string, required?: boolean): JSX.Element; +}; +``` + +### Tipi per le Opzioni (`ElixFormsTypes.tsx`) +```typescript +type ElixFormsCheckboxOption = { value: number; label: string; required?: boolean }; +type ElixFormsDropdownOption = { value: number; label: string }; +type ElixFormsRadioOption = { value: number; label: string }; +``` + +## Form Field Factory Pattern + +Le pagine usano il pattern **Custom Form Field Factory** per dichiarare i campi del form: + +```jsx +// In App.jsx della pagina +var myElixFormsReact = new ElixForms.ElixFormsReact({ + // ...props... + customFormFieldsConfiguration: (formFieldFactory) => ( + <> + {formFieldFactory.createDropdownInput("COL0015", "Goals", [...options], true)} + {formFieldFactory.createTextInput("COL0002", "Campo STRING", true)} + {formFieldFactory.createTextAreaInput("COL0003", "Campo TEXTAREA", true)} + {formFieldFactory.createBooleanInput("COL0004", "Campo BOOLEAN", true)} + {formFieldFactory.createRadioInput("COL0005", "Campo RADIO", [...options], true)} + {formFieldFactory.createCheckboxInput("COL0006", "Campo CHECKBOX", [...options])} + + ) +}); +``` + +### Tipi di Campo Supportati + +| Metodo Factory | Componente FluentUI | HTML Output | +|---|---|---| +| `createTextInput` | `TextField` | Text input | +| `createTextAreaInput` | `TextField` (multiline) | Textarea | +| `createNumberInput` | `TextField` (type=number) | Number input | +| `createBooleanInput` | `ChoiceGroup` (Sì/No) | Radio buttons | +| `createRadioInput` | `ChoiceGroup` | Radio buttons | +| `createCheckboxInput` | `Checkbox` + hidden input | Checkboxes | +| `createDropdownInput` | `Dropdown` | Select dropdown | +| `createHiddenInput` | Native `` | Hidden field | + +## ElixFormsElement (Wrapper Layout) + +`ElixFormsElement` è una classe che incapsula label + input in un layout a due colonne compatibile con il design system elixForms: + +```tsx +class ElixFormsElement { + constructor(labelElement: ReactElement, inputElement: ReactElement, separator?: string); + render(): JSX.Element; +} +``` + +Genera markup con classi CSS specifiche di elixForms: +- `.iuFieldContainer` → container del campo +- `.attrDisplay_left`, `.attrDisplay_label` → colonna label +- `.attrDisplay_right`, `.attrDisplay_input` → colonna input + +## QueryParamHelper + +Utility statica per leggere parametri dalla URL corrente: + +```typescript +class QueryParamHelper { + static getCheckedFromQuery(paramName: string): number[]; // Per checkbox (valori separati da virgola) + static getOptionFromQuery(paramName: string): number | undefined; // Per radio/dropdown + static getBooleanFromQuery(paramName: string): boolean | undefined;// Per boolean + static getDecodedTextFromQuery(paramName: string): string | undefined; // Per testo (URI decoded) +} +``` + +Usato per pre-popolare i campi form con i valori passati via query string dalla piattaforma elixForms. + +## Campi Obbligatori elixForms + +Il componente abstract gestisce automaticamente questi parametri come hidden inputs: +- `RWE2_MODULE_ID` +- `RWE2_REQUEST_ID` +- `custom-workflow-back-url` +- `custom-workflow-generic-id` +- `custom-workflow-current-tabrel-genid` +- `custom-workflow-source-field` +- `crc` +- `MODULE_TESTMODE_KEY` +- `ELANG` + +## Note Importanti + +- Lo **state management** attuale usa `React.Component` class-based con `this.state` e `this.setState` +- I checkbox gestiscono un `Map` interno per tracciare i valori selezionati +- Il form fa POST a `https://procedure.unipr.it/rwe2/ComeBackToElixAndSave` +- L'encoding charset è `ISO-8859-1` per compatibilità con il backend +- La libreria usa **SCSS modules** per gli stili (`ElixFormsComponent.module.scss`) +- Il package `common` è di tipo `commonjs` (diverso dalle pagine che sono `module`) diff --git a/.agents/skills/create-new-page/SKILL.md b/.agents/skills/create-new-page/SKILL.md new file mode 100644 index 0000000..5e2502f --- /dev/null +++ b/.agents/skills/create-new-page/SKILL.md @@ -0,0 +1,229 @@ +--- +name: elixforms-create-new-page +description: > + Guida step-by-step per creare una nuova pagina custom nel progetto elixForms. + Copre la struttura dei file, la configurazione Vite per single-bundle, + il setup TypeScript, l'importazione dei componenti common, e le convenzioni. + Attiva questa skill quando devi creare una nuova pagina, duplicare una pagina esistente, + o scaffoldare un nuovo micro-progetto nel monorepo. +--- + +# Creare una Nuova Pagina elixForms Custom + +## Prerequisiti + +- Node.js installato +- Dipendenze root installate: `cd react && npm install` + +## Step 1: Scaffolding con Vite + +```powershell +cd react +npm create vite@latest -- --template react +cd +npm install vite-plugin-css-injected-by-js +npm install -D sass-embedded +``` + +## Step 2: Configurare package.json + +```json +{ + "name": "@elixforms/", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "vite-plugin-css-injected-by-js": "^5.0.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^6.0.1", + "sass-embedded": "^1.99.0", + "vite": "^8.0.10" + } +} +``` + +> **Nota**: React, ReactDOM e FluentUI NON vanno nel package.json della pagina. +> Sono nel `react/package.json` root e vengono risolti via hoisting di npm. + +## Step 3: Configurare vite.config.js + +```js +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; +import path from "path"; + +export default defineConfig({ + plugins: [ + react(), + cssInjectedByJsPlugin() + ], + build: { + outDir: 'dist', + cssCodeSplit: false, + rollupOptions: { + input: 'src/main.jsx', // o main.tsx se TypeScript + output: { + codeSplitting: false, + manualChunks: undefined, + entryFileNames: '.js', + assetFileNames: '[name].[ext]' + }, + }, + sourcemap: true, + }, + resolve: { + alias: { + "@common": path.resolve(__dirname, "../common"), + } + }, + server: { + fs: { + allow: [".."] + } + } +}) +``` + +## Step 4: Configurare tsconfig.json + +```json +{ + "extends": "../tsconfig.base.json", + "references": [ + { "path": "../common" } + ], + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "allowJs": true, + "checkJs": true + }, + "include": ["src/**/*"] +} +``` + +## Step 5: Creare il file di dichiarazioni globali + +`src/global.d.ts`: +```typescript +declare module "*.css"; +declare module "*.svg"; +declare module "*.png"; +``` + +## Step 6: Creare l'entry point + +`src/main.jsx` (o `main.tsx`): +```jsx +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Root element not found') + +createRoot(rootElement).render( + + + , +) +``` + +## Step 7: Creare il componente App + +`src/App.jsx` (o `App.tsx`): +```jsx +import './App.css' +import * as ElixForms from '@common/src/ElixFormsComponent'; + +function App() { + var myElixFormsReact = new ElixForms.ElixFormsReact({ + description: "Descrizione pagina", + isDarkTheme: false, + environmentMessage: "", + hasTeamsContext: false, + userDisplayName: "", + customFormFieldsConfiguration: (formFieldFactory) => ( + <> + {/* Aggiungi i tuoi campi qui usando formFieldFactory */} + + ) + }); + + var elixFormsReactRender = myElixFormsReact.render(); + + return <> +
+
+
+ {/* Header, contenuto e footer della pagina */} +
+
+
+
+

Titolo Pagina

+ {elixFormsReactRender} +
+
+
+
+
+
+
+ ; +} + +export default App +``` + +## Step 8: Configurare i CSS + +`src/App.css` — Importa i CSS remoti del design system elixForms: +```css +@import url('https://console-unipr.elixforms.it/elixFormsCustom/css/elixForms.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/bootstrap_adaptation.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/console_user.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/designitalia_adaptation.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/fonts.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/style-bs.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/style.css'); +/* ... altri CSS necessari */ +``` + +## Step 9: Configurare VS Code (opzionale) + +Aggiungere configurazione di launch e task in `.vscode/launch.json` e `.vscode/tasks.json` seguendo il pattern esistente per `scelta-carriera`. + +## Step 10: Build e Deploy + +```powershell +npm run build +``` + +Output in `dist/`: +- `.js` — Singolo bundle JS pronto per il deploy +- Eventuali asset (immagini) referenziati + +## Checklist Nuova Pagina + +- [ ] Cartella creata in `react//` +- [ ] `package.json` con `"name": "@elixforms/"` +- [ ] `vite.config.js` con `entryFileNames: '.js'` +- [ ] `tsconfig.json` estende `../tsconfig.base.json` +- [ ] Alias `@common` configurato in vite.config.js +- [ ] CSS remoti importati in App.css +- [ ] Entry point `main.jsx` con mount su `#root` +- [ ] `global.d.ts` con dichiarazioni per moduli CSS/SVG/PNG +- [ ] `npm install` eseguito nella cartella della pagina +- [ ] `npm run dev` funziona senza errori +- [ ] `npm run build` produce singolo bundle in `dist/` diff --git a/.agents/skills/css-design-system/SKILL.md b/.agents/skills/css-design-system/SKILL.md new file mode 100644 index 0000000..cb03ad5 --- /dev/null +++ b/.agents/skills/css-design-system/SKILL.md @@ -0,0 +1,171 @@ +--- +name: elixforms-css-design-system +description: > + Strategia CSS e design system del progetto elixForms. + Documenta come vengono importati i CSS remoti della piattaforma, + il layout a griglia elixForms, le classi CSS specifiche della piattaforma, + gli stili SCSS modules nella libreria common, e la struttura visiva delle pagine. + Attiva questa skill quando lavori sugli stili, sul layout delle pagine, + o devi capire come funzionano le classi CSS del design system elixForms. +--- + +# CSS e Design System elixForms + +## Strategia CSS + +Il progetto utilizza **tre livelli di stili**: + +1. **CSS remoti** — Design system dell'Università di Parma, caricati via `@import url(...)` da `console-unipr.elixforms.it` +2. **SCSS Modules** — Stili specifici dei componenti in `common/` (es. `ElixFormsComponent.module.scss`) +3. **CSS locali** — Stili specifici della pagina (es. `App.css`, `index.css`) + +## CSS Remoti (Design System UniPR/elixForms) + +Importati in `App.css` di ogni pagina: + +```css +/* CSS principali del design system */ +@import url('https://console-unipr.elixforms.it/elixFormsCustom/css/elixForms.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/bootstrap_adaptation.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/bootstrap_overrides.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/console_user.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/designitalia_adaptation.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/fonts.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/isipcss_overrides.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/iulib.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/print.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/responsive.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/style-bs.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/style.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/util.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/plugins/modaldisplay.css'); +@import url('https://console-unipr.elixforms.it/rwe2/css/themes/blu-italia.css'); +@import url('https://console-unipr.elixforms.it/rwe2/isipcss/css/grid_fluid.css'); +``` + +> **IMPORTANTE**: Questi CSS sono caricati a runtime dal CDN e **non vengono bundlati** nel JS. +> La pagina deve avere connessione internet per visualizzarsi correttamente. + +## Layout della Pagina (Struttura HTML) + +Le pagine seguono un layout standard elixForms con classi CSS specifiche: + +```html +
+
+
+ + +
+
+
+
+
+

elixForms

+ +
+
...
+
+
+
+
+ + +
+
+
+

Titolo Modulo

+
+
+
+ +
+
+
+
+ + +
+ +
+ +
+
+
+``` + +## Classi CSS del Design System + +### Layout a Griglia +| Classe | Scopo | +|---|---| +| `container_12` | Container principale a 12 colonne | +| `grid_12` | Occupa tutte le 12 colonne | +| `clear` | Clearfix | + +### Struttura Pagina +| Classe | Scopo | +|---|---| +| `fe-behaviour` | Container principale della console utente | +| `console_header` | Header della console | +| `console_main` | Area contenuto principale | +| `fe-module-header-container` | Header del modulo/form | +| `receipt` | Container del contenuto del form | + +### Layout Campi Form (ElixFormsElement) +| Classe | Scopo | +|---|---| +| `iuFieldContainer` | Container di un campo form (label + input) | +| `attrDisplay_left` | Colonna sinistra (label) | +| `attrDisplay_right` | Colonna destra (input) | +| `attrDisplay_label` | Marcatore per la label | +| `attrDisplay_input` | Marcatore per l'input | +| `iuLabelContainer` | Container interno della label | +| `iuInputContainer` | Container interno dell'input | +| `iuTypeString` | Tipo di input stringa | +| `iuClearContainer` | Clearfix interno | + +### Classi Input FluentUI Custom +| Classe | Componente | +|---|---| +| `isiportalPartialAdminFormFieldSingleLineText` | TextField singola riga | +| `isiportalPartialAdminFormFieldMultiLineText` | TextField multiriga | +| `isiportalPartialAdminFormFieldRadio` | ChoiceGroup radio | +| `isiportalPartialAdminFormFieldSelect` | Dropdown select | +| `isiportalPartialAdminCheckboxFieldItem` | Checkbox singolo | + +## SCSS Modules (common/) + +`ElixFormsComponent.module.scss`: +```scss +@import '~@fluentui/react/dist/sass/References.scss'; + +.elixFormsComponent { + overflow: hidden; + padding: 1em; + color: "[theme:bodyText, default: #323130]"; + color: var(--bodyText); + &.teams { + font-family: $ms-font-family-fallbacks; + } +} +``` + +> **Nota**: Gli stili usano il **theming di FluentUI** con variabili CSS e placeholder di tema SharePoint. + +## Logo e Risorse Esterne + +- **Logo UniPR**: `https://console-unipr.elixforms.it/elixFormsCustom/images/logo.png` +- Le immagini locali vanno in `src/assets/` e importate come moduli ES +- Le immagini vengono copiate nella `dist/` durante la build + +## Note Importanti + +1. Il CSS di `index.css` nella pagina `scelta-carriera` è **attualmente tutto commentato** — si usano solo i CSS remoti +2. Il CSS injection plugin (`vite-plugin-css-injected-by-js`) gestisce solo i CSS locali e SCSS, non gli `@import url(...)` che restano come riferimenti esterni +3. Per funzionare correttamente, la pagina deployata deve poter raggiungere `console-unipr.elixforms.it` diff --git a/.agents/skills/dev-workflow/SKILL.md b/.agents/skills/dev-workflow/SKILL.md new file mode 100644 index 0000000..a6d2bef --- /dev/null +++ b/.agents/skills/dev-workflow/SKILL.md @@ -0,0 +1,130 @@ +--- +name: elixforms-dev-workflow +description: > + Workflow di sviluppo, debug e deploy del progetto elixForms Custom Pages. + Copre i comandi per avviare il dev server, buildare, debuggare con VS Code, + e le configurazioni di launch/tasks. + Attiva questa skill quando devi eseguire comandi di sviluppo, + configurare il debug, o gestire il ciclo dev/build/deploy. +--- + +# Workflow di Sviluppo + +## Comandi Principali + +Tutti i comandi vanno eseguiti **dalla cartella della pagina** (es. `react/scelta-carriera/`): + +```powershell +# Installare le dipendenze (prima volta o dopo modifiche a package.json) +cd react +npm install +cd scelta-carriera +npm install + +# Avviare il dev server con HMR +npm run dev + +# Build per produzione (singolo bundle JS) +npm run build + +# Preview della build di produzione +npm run preview + +# Lint del codice +npm run lint +``` + +## Configurazione VS Code + +### Launch Configurations (`.vscode/launch.json`) + +#### 1. "Scelta Carriere (BROWSER)" — Dev + Chrome Debug +- Avvia `npm run dev` nella cartella della pagina +- Apre automaticamente Chrome quando il server è pronto +- Pattern di rilevamento: `Local:\s+http://localhost:([0-9]+)/` +- WebRoot per sourcemaps: `${workspaceFolder}/react/scelta-carriera/src` + +#### 2. "Scelta Carriere (DEBUG ONLY)" — Solo Dev Server +- Avvia `npm run dev` senza aprire il browser +- Utile per debug da terminale o browser già aperto + +### Variabili d'Ambiente +- `NO_COLOR: "1"` — Disabilita colori ANSI nell'output (migliora leggibilità nel debug console) + +### Skip Files nel Debug +```json +"skipFiles": [ + "/**", + "**/node_modules/**", + "**/@vite/client/**" +] +``` + +### Tasks (`.vscode/tasks.json`) + +#### "Start scelta-carriera (Vite App)" +- Task background che avvia `npm run dev` +- Problem matcher configurato per output Vite: + - `beginsPattern`: "VITE" + - `endsPattern`: "ready in" + +## Aggiungere una Nuova Pagina al Debug + +Per aggiungere una nuova configurazione di debug per una pagina: + +```json +// In .vscode/launch.json, aggiungere in "configurations": +{ + "name": " (BROWSER)", + "cwd": "${workspaceFolder}/react/", + "env": { "NO_COLOR": "1" }, + "outputCapture": "console", + "type": "node", + "request": "launch", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "skipFiles": [ + "/**", + "**/node_modules/**", + "**/@vite/client/**" + ], + "serverReadyAction": { + "action": "debugWithChrome", + "pattern": "Local:\\s+http://localhost:([0-9]+)/", + "uriFormat": "http://localhost:%s", + "webRoot": "${workspaceFolder}/react//src" + } +} +``` + +## Flusso di Deploy + +1. `npm run build` nella cartella della pagina +2. Output: `dist/.js` (+ sourcemap e asset) +3. Caricare il file JS sul server remoto `console-unipr.elixforms.it` +4. La pagina viene servita dalla piattaforma elixForms con i parametri query appropriati + +## Testing Locale con Query Parameters + +Per testare localmente con i parametri elixForms, aggiungere query params all'URL del dev server: + +``` +http://localhost:5173/?RWE2_MODULE_ID=123&RWE2_REQUEST_ID=456&COL0002=test&COL0015=3 +``` + +I valori verranno letti da `QueryParamHelper` e pre-popoleranno i campi del form. + +## Struttura delle Dipendenze + +``` +react/ ← npm install qui per dipendenze condivise +├── node_modules/ ← react, react-dom, @fluentui/react +├── package.json +└── scelta-carriera/ ← npm install qui per devDependencies + ├── node_modules/ ← vite, eslint, plugin, sass-embedded + └── package.json +``` + +> **Nota**: Le dipendenze runtime (React, FluentUI) sono nel `package.json` root. +> Le dipendenze di sviluppo (Vite, ESLint) sono nel `package.json` della pagina. +> npm risolve le dipendenze risalendo la gerarchia delle cartelle (hoisting). diff --git a/.agents/skills/project-architecture/SKILL.md b/.agents/skills/project-architecture/SKILL.md new file mode 100644 index 0000000..d6638dd --- /dev/null +++ b/.agents/skills/project-architecture/SKILL.md @@ -0,0 +1,150 @@ +--- +name: elixforms-project-architecture +description: > + Architettura e struttura del progetto elixForms Custom Pages. + Descrive il monorepo, la libreria common condivisa, il pattern delle pagine autonome + e tutte le convenzioni di naming, configurazione e build. + Attiva questa skill quando lavori su qualsiasi parte del progetto, + crei nuove pagine, modifichi l'architettura o devi capire come è organizzato il codice. +--- + +# Architettura del Progetto elixForms Custom Pages + +## Panoramica + +Questo progetto è un **monorepo** per la creazione di pagine web React custom destinate alla piattaforma **elixForms** dell'Università di Parma (UniPR). Ogni pagina è un micro-progetto indipendente che produce un **singolo file JS bundle** contenente React, Fluent UI, CSS e il codice custom, da pubblicare su un server remoto. + +## Struttura del Workspace + +``` +elixforms-custom-pages/ ← Root del workspace (VS Code) +├── .vscode/ ← Configurazioni VS Code (launch, tasks) +├── docs/ ← Documentazione (PDF manuali) +└── react/ ← Root del monorepo React + ├── package.json ← Dipendenze condivise (React, ReactDOM, FluentUI) + ├── tsconfig.base.json ← Configurazione TypeScript base condivisa + ├── .gitignore + ├── common/ ← Libreria condivisa di componenti ElixForms + │ ├── package.json + │ ├── tsconfig.json ← Estende tsconfig.base, composite: true + │ └── src/ + │ ├── ElixFormsComponent.tsx + │ ├── ElixFormsComponentAbstract.tsx + │ ├── ElixFormsElement.tsx + │ ├── ElixFormsTypes.tsx + │ ├── ElixFormsComponent.module.scss + │ ├── IElixFormsComponentProperties.ts + │ ├── IElixFormsComponentFormState.tsx + │ ├── IElixFormsComponentCustomFormFieldFactory.ts + │ └── QueryParamHelper.tsx + └── scelta-carriera/ ← Esempio di pagina custom (micro-progetto) + ├── package.json + ├── tsconfig.json ← Estende tsconfig.base + ├── vite.config.js + ├── eslint.config.js + ├── index.html + └── src/ + ├── main.jsx ← Entry point React + ├── App.jsx ← Componente principale della pagina + ├── App.css ← CSS (importa CSS remoti di elixForms) + ├── index.css ← CSS globale (attualmente commentato) + ├── global.d.ts ← Dichiarazioni di tipo per moduli + └── assets/ ← Immagini e risorse statiche +``` + +## Pattern Architetturale: Micro-Progetto per Pagina + +Ogni pagina (es. `scelta-carriera/`) è un progetto Vite completamente autonomo che: + +1. **Importa dalla libreria `common/`** tramite alias `@common` configurato in `vite.config.js` +2. **Ha le proprie dipendenze** nel suo `package.json` (solo devDependencies e plugin Vite) +3. **Condivide le dipendenze runtime** (React, ReactDOM, FluentUI) dal `package.json` root di `react/` +4. **Produce un singolo file JS** tramite la configurazione Rollup in `vite.config.js` +5. **Estende il tsconfig base** per avere configurazione TypeScript coerente + +## Dipendenze Condivise (react/package.json) + +Le dipendenze runtime sono hoisted al livello `react/`: +- `react` ^19.2.7 +- `react-dom` ^19.2.5 +- `@fluentui/react` ^8.125.6 + +## Configurazione TypeScript + +### Base (tsconfig.base.json) +- `module`: ESNext +- `target`: ES2022 +- `moduleResolution`: bundler +- `jsx`: react-jsx +- `strict`: true +- Path alias: `@common/*` → `./common/*` + +### Common (common/tsconfig.json) +- `composite`: true (per project references) +- `declaration`: true, `declarationMap`: true + +### Pagine (scelta-carriera/tsconfig.json) +- `extends`: `../tsconfig.base.json` +- `references`: `../common` +- `allowJs`: true, `checkJs`: true (supporto misto JSX/TSX) + +## Configurazione Build (Vite) + +Ogni pagina usa questa configurazione per generare un **singolo bundle JS**: + +```js +// Plugin chiave +react() // Supporto React JSX +cssInjectedByJsPlugin() // CSS iniettato nel JS (no file .css separati) + +// Build options +cssCodeSplit: false +rollupOptions.input: 'src/main.jsx' +rollupOptions.output: + codeSplitting: false + manualChunks: undefined + entryFileNames: '.js' // Nome descrittivo (es. 'scelta-carriera.js') + assetFileNames: '[name].[ext]' +sourcemap: true +``` + +## Resolve Alias + +```js +resolve.alias: { + "@common": path.resolve(__dirname, "../common") +} +server.fs.allow: [".."] // Permette import da common/ +``` + +## CSS Strategy + +Le pagine caricano CSS dal server remoto elixForms tramite `@import url(...)` in `App.css`: +- CSS da `console-unipr.elixforms.it` per il design system dell'università +- I componenti FluentUI usano stili inline/theme nativi +- SCSS modules sono usati in `common/` per stili dei componenti condivisi +- `sass-embedded` è installato come devDependency per supporto SCSS + +## Integrazione con elixForms + +La pagina custom interagisce con la piattaforma elixForms tramite: +1. **Query parameters**: Parametri obbligatori passati nell'URL (es. `RWE2_MODULE_ID`, `RWE2_REQUEST_ID`, `crc`, etc.) +2. **Form POST**: Il form fa submit a `https://procedure.unipr.it/rwe2/ComeBackToElixAndSave` +3. **Hidden inputs**: I parametri query vengono inseriti come campi hidden nel form +4. **Encoding**: `acceptCharset="ISO-8859-1"` per compatibilità con il backend + +## Convenzioni di Naming + +- **Cartelle pagina**: kebab-case (es. `scelta-carriera`) +- **Package name**: `@elixforms/` (es. `@elixforms/scelta-carriera`) +- **Bundle output**: `.js` (es. `scelta-carriera.js`) +- **Componenti React**: PascalCase +- **File sorgente**: I file correnti sono `.jsx` ma il progetto ha pieno supporto TypeScript (`.tsx`) +- **Tipo moduli**: `"type": "module"` nelle pagine + +## Ambiente di Sviluppo + +- **Dev server**: `npm run dev` (Vite HMR) +- **Build**: `npm run build` → output in `dist/` +- **Debug VS Code**: Configurazioni in `.vscode/launch.json` per avviare Vite + Chrome +- **Lint**: ESLint con plugin react-hooks e react-refresh diff --git a/.agents/skills/typescript-react-conventions/SKILL.md b/.agents/skills/typescript-react-conventions/SKILL.md new file mode 100644 index 0000000..4a01605 --- /dev/null +++ b/.agents/skills/typescript-react-conventions/SKILL.md @@ -0,0 +1,174 @@ +--- +name: elixforms-typescript-conventions +description: > + Convenzioni TypeScript e React per il progetto elixForms. + Copre la configurazione TypeScript, i pattern di codice, la gestione dei tipi, + l'uso di JSDoc per type annotations nei file .jsx, e le best practices. + Attiva questa skill quando scrivi codice TypeScript o React, + crei nuovi componenti, definisci interfacce, o gestisci i tipi nel progetto. +--- + +# Convenzioni TypeScript e React + +## Configurazione TypeScript del Progetto + +### Opzioni Compiler Principali +- **target**: ES2022 +- **module**: ESNext +- **moduleResolution**: bundler +- **jsx**: react-jsx (trasformazione automatica JSX senza import React) +- **strict**: true +- **isolatedModules**: true +- **verbatimModuleSyntax**: true (usa `import type` per import solo di tipi) + +### File Misti JSX/TSX + +Il progetto supporta **sia file `.jsx` che `.tsx`** grazie a: +- `allowJs: true` e `checkJs: true` nel tsconfig delle pagine +- Type annotations via JSDoc nei file `.jsx` + +#### Pattern JSDoc per Tipi nei File .jsx + +```jsx +/** @type {import('@common/src/ElixFormsComponent').ElixFormsReact} */ +var myElixFormsReact = new ElixForms.ElixFormsReact({...}); + +// Per i parametri di callback +customFormFieldsConfiguration: ( + /** @type {import('../../common/src/IElixFormsComponentCustomFormFieldFactory').IElixFormsComponentCustomFormFieldFactory} */ + formFieldFactory) => (...) +``` + +## Pattern React Correnti + +### Class Components (common/) + +La libreria `common/` usa **React Class Components** (non hooks): + +```tsx +abstract class ElixFormsComponentAbstract + extends Component { + + constructor(props: IElixFormsComponentProperties) { + super(props); + this.state = { formData: {} }; + } + + // State gestito con this.setState() + this.setState({ formData: { ...this.state.formData, [name]: value } }); +} +``` + +### Functional Components (pagine) + +Le pagine usano **Functional Components** con hooks: + +```jsx +import { useState } from 'react' + +function App() { + const [count, setCount] = useState(0) + // ... +} +``` + +### Istanziazione del Componente ElixForms + +Il componente ElixForms viene istanziato come classe (non come JSX): + +```jsx +// ✅ Corretto - istanziazione diretta +var myElixFormsReact = new ElixForms.ElixFormsReact({...props...}); +var elixFormsReactRender = myElixFormsReact.render(); + +// Poi nel JSX +return <>{elixFormsReactRender}; + +// ❌ NON usare come componente JSX +return ; +``` + +## Import Conventions + +### Import Type-Only (verbatimModuleSyntax) + +Con `verbatimModuleSyntax: true`, usare sempre `import type` per i tipi: + +```tsx +// ✅ Corretto +import type { IElixFormsComponentProperties } from './IElixFormsComponentProperties'; +import type { JSX } from 'react'; + +// ❌ Errato - causerà errore con verbatimModuleSyntax +import { IElixFormsComponentProperties } from './IElixFormsComponentProperties'; +``` + +### Import di Moduli + +```tsx +// Fluent UI - import tutto il namespace +import * as FluentUI from '@fluentui/react'; + +// React - import specifici +import { Component, type FormEvent, type JSX, useState } from 'react'; +import React from 'react'; + +// Componenti common - import namespace +import * as ElixForms from '@common/src/ElixFormsComponent'; + +// CSS +import './App.css' +``` + +## Tipi Custom del Progetto + +```typescript +// Opzioni per campi form +type ElixFormsCheckboxOption = { value: number; label: string; required?: boolean }; +type ElixFormsDropdownOption = { value: number; label: string }; +type ElixFormsRadioOption = { value: number; label: string }; + +// State del form +interface IElixFormsComponentFormState { + formData: { [key: string]: any }; +} +``` + +## Dichiarazioni di Modulo + +`global.d.ts` nella cartella `src/` di ogni pagina: + +```typescript +declare module "*.css"; +declare module "*.svg"; +declare module "*.png"; +``` + +## Best Practices per il Progetto + +1. **Naming**: + - Interfacce: prefisso `I` (es. `IElixFormsComponentProperties`) + - Tipi: PascalCase (es. `ElixFormsCheckboxOption`) + - File interfacce: nome dell'interfaccia (es. `IElixFormsComponentProperties.ts`) + - File componenti: nome del componente (es. `ElixFormsComponent.tsx`) + +2. **Estensioni file**: + - `.tsx` per componenti React con TypeScript (common/) + - `.ts` per moduli TypeScript puri (interfacce, tipi) + - `.jsx` per componenti React con JavaScript (pagine attuali) + - `.scss` per stili con moduli SCSS + +3. **Export**: + - Named export per tipi e interfacce + - Default export per classi componente + - Re-export con alias: `export { ElixFormsComponent as ElixFormsReact }` + +4. **State management**: + - Usare `this.setState` con spread operator per update immutabili + - I checkbox mantengono un `Map` separato + +5. **Fluent UI**: + - Usare Fluent UI React v8 (`@fluentui/react`) + - Componenti: `TextField`, `Checkbox`, `ChoiceGroup`, `Dropdown`, `Label`, `Stack` + - Le opzioni radio usano `IChoiceGroupOption[]` + - Le opzioni dropdown usano `IDropdownOption[]` diff --git a/.agents/skills/vite-build-config/SKILL.md b/.agents/skills/vite-build-config/SKILL.md new file mode 100644 index 0000000..9141067 --- /dev/null +++ b/.agents/skills/vite-build-config/SKILL.md @@ -0,0 +1,136 @@ +--- +name: elixforms-vite-build-config +description: > + Configurazione Vite e Rollup per il progetto elixForms. + Copre la build single-bundle, i plugin utilizzati, gli alias, + il sourcemap, e la strategia CSS (injection nel JS). + Attiva questa skill quando devi modificare la build configuration, + risolvere problemi di bundling, aggiungere plugin Vite, + o ottimizzare l'output di produzione. +--- + +# Configurazione Vite e Build + +## Stack di Build + +- **Vite** v8.x — Dev server e bundler +- **@vitejs/plugin-react** v6.x — Supporto React/JSX +- **vite-plugin-css-injected-by-js** v5.x — Inietta CSS nel bundle JS +- **sass-embedded** v1.99.x — Compilazione SCSS (per common/) +- **Rollup** — Bundler interno di Vite (configurato via `rollupOptions`) + +## Configurazione Vite Completa di Riferimento + +```js +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; +import path from "path"; + +export default defineConfig({ + plugins: [ + react(), + cssInjectedByJsPlugin() + ], + build: { + outDir: 'dist', + cssCodeSplit: false, + rollupOptions: { + input: 'src/main.jsx', + output: { + codeSplitting: false, + manualChunks: undefined, + entryFileNames: '.js', + assetFileNames: '[name].[ext]' + }, + }, + sourcemap: true, + }, + resolve: { + alias: { + "@common": path.resolve(__dirname, "../common"), + } + }, + server: { + fs: { + allow: [".."] + } + } +}) +``` + +## Dettaglio Opzioni Critiche + +### Single Bundle (Rollup) +| Opzione | Valore | Scopo | +|---|---|---| +| `codeSplitting` | `false` | Disabilita il code splitting | +| `manualChunks` | `undefined` | Impedisce separazione vendor/app | +| `entryFileNames` | `'.js'` | Nome deterministico senza hash | +| `assetFileNames` | `'[name].[ext]'` | Asset senza hash nei nomi | + +### CSS Strategy +| Opzione | Valore | Scopo | +|---|---|---| +| `cssCodeSplit` | `false` | No file CSS separati | +| `cssInjectedByJsPlugin()` | plugin | CSS iniettato runtime nel DOM via JS | + +### Alias di Import +| Alias | Path | Scopo | +|---|---|---| +| `@common` | `../common` | Accesso alla libreria condivisa | + +### Dev Server +| Opzione | Valore | Scopo | +|---|---|---| +| `server.fs.allow` | `[".."]` | Permette import fuori dalla root del progetto | + +## Comandi + +```powershell +# Dev con HMR +npm run dev + +# Build produzione +npm run build + +# Preview build produzione +npm run preview + +# Lint +npm run lint +``` + +## Output della Build + +``` +dist/ + .js ← Bundle unico (React + FluentUI + CSS + codice) + .js.map ← Source map + *.png, *.svg ← Asset referenziati nel codice +``` + +## Risoluzione Problemi Comuni + +### Errore: "The request url is outside of Vite serving allow list" +- **Causa**: Import di file dalla cartella `common/` o altre cartelle esterne +- **Fix**: Aggiungere `server.fs.allow: [".."]` nella configurazione Vite + +### Errore: "Cannot find module '@common/...'" +- **Causa**: Alias non configurato o path errato +- **Fix**: Verificare `resolve.alias` e che la cartella `common/` esista + +### Bundle troppo grande +- **Causa**: FluentUI include molti componenti +- **Fix possibili**: + - Tree shaking (import specifici: `import { TextField } from '@fluentui/react'`) + - Considerare `@fluentui/react-components` (v9) per bundle più piccoli + - Esternalizzare React se la pagina host lo fornisce già + +### CSS non applicato in produzione +- **Causa**: `cssInjectedByJsPlugin` non installato o non configurato +- **Fix**: Verificare plugin in `vite.config.js` e `npm install vite-plugin-css-injected-by-js` + +### SCSS non compilato +- **Causa**: `sass-embedded` mancante +- **Fix**: `npm install -D sass-embedded` diff --git a/docs/Manuale_Custom_Workflow_v2.pdf b/docs/Manuale_Custom_Workflow_v2.pdf new file mode 100644 index 0000000..0429c2e Binary files /dev/null and b/docs/Manuale_Custom_Workflow_v2.pdf differ diff --git a/react/package-lock.json b/react/package-lock.json index 696253b..2105e84 100644 --- a/react/package-lock.json +++ b/react/package-lock.json @@ -262,14 +262,14 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -279,6 +279,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, diff --git a/react/scelta-carriera/package-lock.json b/react/scelta-carriera/package-lock.json index 86d5505..1a8c0d7 100644 --- a/react/scelta-carriera/package-lock.json +++ b/react/scelta-carriera/package-lock.json @@ -54,6 +54,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -270,31 +271,10 @@ "devOptional": true, "license": "(Apache-2.0 AND BSD-3-Clause)" }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "license": "MIT", "optional": true, "dependencies": { @@ -1078,6 +1058,37 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.0-rc.17", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", @@ -1154,6 +1165,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1200,6 +1212,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1290,6 +1303,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1447,6 +1461,7 @@ "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -2487,6 +2502,7 @@ "integrity": "sha512-gF/juR1aX02lZHkvwxdF80SapkQeg2fetoDF6gIQkNbSw5YEUFspMkyGTjPjgZSgIHuZpy+Wz4PlebKnLXMjdg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@bufbuild/protobuf": "^2.5.0", "colorjs.io": "^0.5.0", @@ -2981,6 +2997,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -3114,6 +3131,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }