diff --git a/.agents/skills/common-library/SKILL.md b/.agents/skills/common-library/SKILL.md index c4cddc4..051a862 100644 --- a/.agents/skills/common-library/SKILL.md +++ b/.agents/skills/common-library/SKILL.md @@ -12,7 +12,7 @@ description: > ## 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. +La cartella `react/common/` contiene la libreria condivisa di componenti React/TypeScript usata da tutte le pagine custom. È basata su **Bootstrap Italia + HTML nativo** e implementa un sistema di form con campi dinamici. ## Architettura dei Componenti @@ -103,7 +103,7 @@ var myElixFormsReact = new ElixForms.ElixFormsReact({ ### Tipi di Campo Supportati -| Metodo Factory | Componente FluentUI | HTML Output | +| Metodo Factory | Componente Bootstrap Italia | HTML Output | |---|---|---| | `createTextInput` | `TextField` | Text input | | `createTextAreaInput` | `TextField` (multiline) | Textarea | diff --git a/.agents/skills/create-new-page/SKILL.md b/.agents/skills/create-new-page/SKILL.md index 1e35ce8..66b1489 100644 --- a/.agents/skills/create-new-page/SKILL.md +++ b/.agents/skills/create-new-page/SKILL.md @@ -50,7 +50,7 @@ npm install -D sass-embedded } ``` -> **Nota**: React, ReactDOM e FluentUI NON vanno nel package.json della pagina. +> **Nota**: React e ReactDOM 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 diff --git a/.agents/skills/css-design-system/SKILL.md b/.agents/skills/css-design-system/SKILL.md index cf7c478..94bdae3 100644 --- a/.agents/skills/css-design-system/SKILL.md +++ b/.agents/skills/css-design-system/SKILL.md @@ -130,7 +130,7 @@ Le pagine seguono un layout standard elixForms con classi CSS specifiche: | `iuTypeString` | Tipo di input stringa | | `iuClearContainer` | Clearfix interno | -### Classi Input FluentUI Custom +### Classi Input Bootstrap Italia Custom | Classe | Componente | |---|---| | `isiportalPartialAdminFormFieldSingleLineText` | TextField singola riga | @@ -143,20 +143,13 @@ Le pagine seguono un layout standard elixForms con classi CSS specifiche: `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. +> **Nota**: Gli stili usano classi Bootstrap Italia e CSS del design system UniPR, senza dipendenze da FluentUI. ## Logo e Risorse Esterne diff --git a/.agents/skills/dev-workflow/SKILL.md b/.agents/skills/dev-workflow/SKILL.md index a6d2bef..f5d650b 100644 --- a/.agents/skills/dev-workflow/SKILL.md +++ b/.agents/skills/dev-workflow/SKILL.md @@ -118,13 +118,13 @@ I valori verranno letti da `QueryParamHelper` e pre-popoleranno i campi del form ``` react/ ← npm install qui per dipendenze condivise -├── node_modules/ ← react, react-dom, @fluentui/react +├── node_modules/ ← react, react-dom ├── 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. +> **Nota**: Le dipendenze runtime (React, ReactDOM) 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 index d6638dd..2f506fa 100644 --- a/.agents/skills/project-architecture/SKILL.md +++ b/.agents/skills/project-architecture/SKILL.md @@ -12,7 +12,7 @@ description: > ## 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. +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, Bootstrap Italia e il codice custom, da pubblicare su un server remoto. ## Struttura del Workspace @@ -21,7 +21,7 @@ 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) + ├── package.json ← Dipendenze condivise (React, ReactDOM) ├── tsconfig.base.json ← Configurazione TypeScript base condivisa ├── .gitignore ├── common/ ← Libreria condivisa di componenti ElixForms @@ -58,7 +58,7 @@ Ogni pagina (es. `scelta-carriera/`) è un progetto Vite completamente autonomo 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/` +3. **Condivide le dipendenze runtime** (React, ReactDOM) 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 @@ -67,7 +67,6 @@ Ogni pagina (es. `scelta-carriera/`) è un progetto Vite completamente autonomo Le dipendenze runtime sono hoisted al livello `react/`: - `react` ^19.2.7 - `react-dom` ^19.2.5 -- `@fluentui/react` ^8.125.6 ## Configurazione TypeScript @@ -121,7 +120,7 @@ server.fs.allow: [".."] // Permette import da common/ 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 +- I componenti Bootstrap Italia usano classi HTML e CSS del design system UniPR - SCSS modules sono usati in `common/` per stili dei componenti condivisi - `sass-embedded` è installato come devDependency per supporto SCSS diff --git a/.agents/skills/typescript-react-conventions/SKILL.md b/.agents/skills/typescript-react-conventions/SKILL.md index 4a01605..4e1dc12 100644 --- a/.agents/skills/typescript-react-conventions/SKILL.md +++ b/.agents/skills/typescript-react-conventions/SKILL.md @@ -106,9 +106,6 @@ 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'; @@ -167,8 +164,8 @@ declare module "*.png"; - 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[]` +5. **Bootstrap Italia**: + - Usare markup HTML nativo con classi Bootstrap Italia e del design system UniPR + - Per i campi testuali usare `input`, `textarea`, `select` con classi `form-control`/`form-select` + - Per i checkbox/radio usare `input type="checkbox"`/`input type="radio"` con classi `form-check-input` + - Il layout dei campi deve rimanere compatibile con `ElixFormsElement` diff --git a/.agents/skills/vite-build-config/SKILL.md b/.agents/skills/vite-build-config/SKILL.md index 9141067..40ccfd3 100644 --- a/.agents/skills/vite-build-config/SKILL.md +++ b/.agents/skills/vite-build-config/SKILL.md @@ -105,7 +105,7 @@ npm run lint ``` dist/ - .js ← Bundle unico (React + FluentUI + CSS + codice) + .js ← Bundle unico (React + CSS + codice) .js.map ← Source map *.png, *.svg ← Asset referenziati nel codice ``` @@ -121,10 +121,10 @@ dist/ - **Fix**: Verificare `resolve.alias` e che la cartella `common/` esista ### Bundle troppo grande -- **Causa**: FluentUI include molti componenti +- **Causa**: l'uso di componenti UI pesanti o CSS non ottimizzati - **Fix possibili**: - - Tree shaking (import specifici: `import { TextField } from '@fluentui/react'`) - - Considerare `@fluentui/react-components` (v9) per bundle più piccoli + - Mantenere i CSS locali limitati e affidarsi ai CSS remoti del design system + - Evitare dipendenze UI pesanti per le pagine custom - Esternalizzare React se la pagina host lo fornisce già ### CSS non applicato in produzione diff --git a/react/common/package-lock.json b/react/common/package-lock.json index 0eae476..237ffcf 100644 --- a/react/common/package-lock.json +++ b/react/common/package-lock.json @@ -7,7 +7,27 @@ "": { "name": "@elixforms/common", "version": "0.0.1", - "license": "ISC" + "license": "ISC", + "devDependencies": { + "@types/react": "^19.2.17" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" } } } diff --git a/react/common/package.json b/react/common/package.json index b9631b8..17c16bc 100644 --- a/react/common/package.json +++ b/react/common/package.json @@ -5,5 +5,8 @@ "license": "ISC", "author": "", "type": "commonjs", - "main": "index.js" + "main": "index.js", + "devDependencies": { + "@types/react": "^19.2.17" + } } diff --git a/react/common/src/ElixFormsComponent.module.scss b/react/common/src/ElixFormsComponent.module.scss index 3caff90..41ceeff 100644 --- a/react/common/src/ElixFormsComponent.module.scss +++ b/react/common/src/ElixFormsComponent.module.scss @@ -1,34 +1,28 @@ -@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; - } } -.welcome { - text-align: center; +.autocompleteDropdown { + margin-top: 0.5rem; + border: 1px solid #c9d1d9; + border-radius: 0.5rem; + background: #fff; + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.08); } -.welcomeImage { - width: 100%; - max-width: 420px; +.autocompleteItem { + padding: 0.75rem 1rem; + cursor: pointer; } -.links { - a { - text-decoration: none; - color: "[theme:link, default:#03787c]"; - color: var(--link); // note: CSS Custom Properties support is limited to modern browsers only +.autocompleteItem:hover, +.autocompleteItem.isHighlighted { + background-color: #f5f7fa; +} - &:hover { - text-decoration: underline; - color: "[theme:linkHovered, default: #014446]"; - color: var(--linkHovered); // note: CSS Custom Properties support is limited to modern browsers only - } - } +.autocompleteStatus { + margin-top: 0.5rem; + font-size: 0.95rem; + color: #5c6f82; } \ No newline at end of file diff --git a/react/common/src/ElixFormsComponentAbstract.tsx b/react/common/src/ElixFormsComponentAbstract.tsx index c6a04ab..802be38 100644 --- a/react/common/src/ElixFormsComponentAbstract.tsx +++ b/react/common/src/ElixFormsComponentAbstract.tsx @@ -1,10 +1,9 @@ -import './App.css' -import * as FluentUI from '@fluentui/react'; +import './App.css'; import type { IElixFormsComponentFormState } from './IElixFormsComponentFormState'; import type { IElixFormsComponentProperties } from './IElixFormsComponentProperties'; import { ElixFormsElement } from './ElixFormsElement'; import { QueryParamHelper } from './QueryParamHelper'; -import { Component, type FormEvent, type JSX, useState } from 'react'; +import { Component, type JSX } from 'react'; import React from 'react'; import type { IElixFormsComponentCustomFormFieldFactory } from './IElixFormsComponentCustomFormFieldFactory'; import type { ElixFormsCheckboxOption, ElixFormsDropdownOption, ElixFormsRadioOption } from './ElixFormsTypes'; @@ -16,57 +15,26 @@ export default abstract class ElixFormsComponentAbstract extends Component (event: any, valueOrOption: any) => { //ChangeEvent | SyntheticEvent - // let value: any = null; - // // Fluent UI Dropdown passes (event, option) - // if (valueOrOption && valueOrOption.key !== undefined) { - // value = valueOrOption.key; - // } - // // Fluent UI TextField passes (event, newValue) - // else if (typeof valueOrOption === "string") { - // value = valueOrOption; - // } - // // Fluent UI Checkbox passes (event, checked) - // else if (typeof valueOrOption === "boolean") { - // value = valueOrOption; - // } - // // Native HTML inputs - // else if (event?.target) { - // value = event.target.type === "checkbox" - // ? event.target.checked - // : event.target.value; - // } - // setFormData(prev => ({ - // ...prev, - // [name]: value - // })); - // console.log(`Input ${name} changed to ${value}. Current form data:`, JSON.stringify(formData)); - // }; } private checkboxValues = new Map(); private mandatoryFormFieldNames: string[] = [ - "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" + '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' ]; private createMandatoryFormFields(queryParams: URLSearchParams): JSX.Element { const hiddenInputs: JSX.Element[] = []; - // Create a hidden input for each mandatory Elix parameter this.mandatoryFormFieldNames.forEach(paramName => { - const value = queryParams.get(paramName) ?? ""; - // Create the input element hiddenInputs.push(this.createHiddenInput(paramName)); }); @@ -74,7 +42,7 @@ export default abstract class ElixFormsComponentAbstract extends Component @@ -125,10 +93,7 @@ export default abstract class ElixFormsComponentAbstract extends Component { const { description, - isDarkTheme, environmentMessage, - hasTeamsContext, - userDisplayName, additionalFieldsJson, headerTitle, pageTitle, @@ -151,7 +116,10 @@ export default abstract class ElixFormsComponentAbstract extends Component

elixForms

-
Logo Università: di Parma +
@@ -176,29 +144,28 @@ export default abstract class ElixFormsComponentAbstract extends Component )}
-
- {pageTitle &&

{pageTitle}

} - {heroImageSrc && Hero Image} - {pageDescription &&

{pageDescription}

} +
+ {pageTitle &&

{pageTitle}

} + {heroImageSrc && Hero Image} + {pageDescription &&

{pageDescription}

} - {environmentMessage &&
{environmentMessage}
} - {description &&
{description}
} + {environmentMessage &&
{environmentMessage}
} + {description &&
{description}
} {hasAuthenticationError ? ( - +
Accesso non autorizzato. Parametri obbligatori mancanti ({missingMandatoryParams.join(', ')}). - +
) : ( <> {this.renderExtraContentPre()} -
+ {this.createMandatoryFormFields(queryParams)} {this.createAdditionalFormFields()} {this.renderCustomFormFields()} -
-
- +
+
@@ -240,158 +207,180 @@ export default abstract class ElixFormsComponentAbstract extends Component { - const id = `${paramName}_${entryIndex}`; - checkboxes.push( - { - const currentCheckboxValues = this.checkboxValues.get(paramName)!; - const checkboxIndex = currentCheckboxValues.indexOf(entryIndex); - if (checked && checkboxIndex === -1) { - currentCheckboxValues.push(entryIndex); - } else if (!checked && checkboxIndex !== -1) { - currentCheckboxValues.splice(checkboxIndex, 1); - } - const newValue = currentCheckboxValues.join(',').trim(); - this.setState({ formData: { ...this.state.formData, [paramName]: newValue } }, () => { - console.log(`Checkbox ${id} changed to ${checked}. Current values for ${paramName}: ${this.state.formData[paramName]}`); - }); - }} - /> - ); - }); - const currentValue = this.state.formData[paramName] !== undefined ? this.state.formData[paramName] : (this.checkboxValues.get(paramName)?.join(',').trim() ?? ''); + const selectedValues = (currentValue || '').split(',').filter(Boolean).map((value: string) => Number(value)); + return new ElixFormsElement( - <>{label}, - <> - {checkboxes} - - + , + <> +
+ {values.map((entry, entryIndex) => { + const id = `${paramName}_${entryIndex}`; + const isChecked = selectedValues.includes(entryIndex); + + return ( +
+ { + const currentCheckboxValues = this.checkboxValues.get(paramName) ?? []; + const checkboxIndex = currentCheckboxValues.indexOf(entryIndex); + const nextValues = [...currentCheckboxValues]; + + if (event.target.checked && checkboxIndex === -1) { + nextValues.push(entryIndex); + } else if (!event.target.checked && checkboxIndex !== -1) { + nextValues.splice(checkboxIndex, 1); + } + + this.checkboxValues.set(paramName, nextValues); + const newValue = nextValues.join(',').trim(); + this.setState({ formData: { ...this.state.formData, [paramName]: newValue } }); + }} + /> + +
+ ); + })} +
+ ).render(); } private createRadioInput(paramName: string, label: string, values: Array, required: boolean = false): JSX.Element { - const radioValue = QueryParamHelper.getOptionFromQuery(paramName); - const radios: FluentUI.IChoiceGroupOption[] = []; - - values.forEach((entry, _) => { - const id = `${paramName}_${entry.value}`; - radios.push( - { - key: id, - text: entry.label, - id: id, - name: paramName, - value: entry.value, - className: 'isiportalPartialAdminFormFieldRadio' - } - ); - }); + const currentValue = this.state.formData[paramName] !== undefined + ? String(this.state.formData[paramName]) + : String(QueryParamHelper.getOptionFromQuery(paramName) ?? ''); return new ElixFormsElement( - <>, - { - const value = option?.value ?? ''; - this.setState({ formData: { ...this.state.formData, [paramName]: value } }, () => - console.log(`Radio ${option?.id} changed to ${value}. Current value for ${paramName}: ${this.state.formData[paramName]}`) + {label}, +
+ {values.map((entry) => { + const id = `${paramName}_${entry.value}`; + return ( +
+ { + this.setState({ formData: { ...this.state.formData, [paramName]: String(entry.value) } }); + }} + required={required} + /> + +
); - }} - /> + })} +
).render(); } private createBooleanInput(paramName: string, label: string, required: boolean = false): JSX.Element { - const boolValue = QueryParamHelper.getOptionFromQuery(paramName); - const radios: FluentUI.IChoiceGroupOption[] = []; - - [{ "value": "true", "label": "Sì" }, { "value": "false", "label": "No" }].forEach((val) => { - const id = `${paramName}_${val.value}`; - radios.push( - { - key: id, - text: val.label, - id: id, - name: paramName, - value: val.value, - className: 'isiportalPartialAdminFormFieldRadio' - } - ); - }); + const currentValue = this.state.formData[paramName] !== undefined + ? String(this.state.formData[paramName]) + : String(QueryParamHelper.getOptionFromQuery(paramName) ?? ''); return new ElixFormsElement( - <>, - { - const value = option?.value ?? ''; - this.setState({ formData: { ...this.state.formData, [paramName]: value } }, () => - console.log(`Boolean ${option?.id} changed to ${value}. Current value for ${paramName}: ${this.state.formData[paramName]}`) + {label}, +
+ {[{ value: 'true', label: 'Sì' }, { value: 'false', label: 'No' }].map((val) => { + const id = `${paramName}_${val.value}`; + return ( +
+ { + this.setState({ formData: { ...this.state.formData, [paramName]: val.value } }); + }} + required={required} + /> + +
); - }} - /> + })} +
).render(); } private createTextAreaInput(paramName: string, label: string, required: boolean = false): JSX.Element { - const textareaValue = QueryParamHelper.getDecodedTextFromQuery(paramName) ?? ""; + const currentValue = this.state.formData[paramName] !== undefined + ? String(this.state.formData[paramName]) + : (QueryParamHelper.getDecodedTextFromQuery(paramName) ?? ''); + return new ElixFormsElement( - {label}, - { - this.setState({ formData: { ...this.state.formData, [paramName]: value } }, () => - console.log(`TextArea ${paramName} changed to ${value}. Current value for ${paramName}: ${this.state.formData[paramName]}`) - ); + , +