From 9996e6fbf792875f47f501140fc5824e5f6f12fa Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 13:38:15 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20add=20plataforma=20m=C3=A9dica=20React?= =?UTF-8?q?=20(Vite)=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plataforma completa com três papéis (paciente, médico, admin), design system iOS 27 Liquid Glass, fluxo de cadastro/triagem/pagamento/prontuário e chat com assistente. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01GEEehTx7xBj7f7MhSXprpi --- plataforma-medica/.gitignore | 24 + plataforma-medica/.oxlintrc.json | 8 + plataforma-medica/README.md | 16 + plataforma-medica/index.html | 13 + plataforma-medica/package.json | 23 + plataforma-medica/public/favicon.svg | 1 + plataforma-medica/public/icons.svg | 24 + plataforma-medica/src/App.css | 184 + plataforma-medica/src/App.jsx | 4453 ++++++++++++++++++++++++ plataforma-medica/src/assets/hero.png | Bin 0 -> 13057 bytes plataforma-medica/src/assets/react.svg | 1 + plataforma-medica/src/assets/vite.svg | 1 + plataforma-medica/src/index.css | 3 + plataforma-medica/src/main.jsx | 10 + plataforma-medica/vite.config.js | 7 + 15 files changed, 4768 insertions(+) create mode 100644 plataforma-medica/.gitignore create mode 100644 plataforma-medica/.oxlintrc.json create mode 100644 plataforma-medica/README.md create mode 100644 plataforma-medica/index.html create mode 100644 plataforma-medica/package.json create mode 100644 plataforma-medica/public/favicon.svg create mode 100644 plataforma-medica/public/icons.svg create mode 100644 plataforma-medica/src/App.css create mode 100644 plataforma-medica/src/App.jsx create mode 100644 plataforma-medica/src/assets/hero.png create mode 100644 plataforma-medica/src/assets/react.svg create mode 100644 plataforma-medica/src/assets/vite.svg create mode 100644 plataforma-medica/src/index.css create mode 100644 plataforma-medica/src/main.jsx create mode 100644 plataforma-medica/vite.config.js diff --git a/plataforma-medica/.gitignore b/plataforma-medica/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/plataforma-medica/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/plataforma-medica/.oxlintrc.json b/plataforma-medica/.oxlintrc.json new file mode 100644 index 000000000..125507823 --- /dev/null +++ b/plataforma-medica/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/plataforma-medica/README.md b/plataforma-medica/README.md new file mode 100644 index 000000000..d937833bd --- /dev/null +++ b/plataforma-medica/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the Oxlint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and Oxlint's TypeScript related rules in your project. diff --git a/plataforma-medica/index.html b/plataforma-medica/index.html new file mode 100644 index 000000000..90178820c --- /dev/null +++ b/plataforma-medica/index.html @@ -0,0 +1,13 @@ + + + + + + + plataforma-medica + + +
+ + + diff --git a/plataforma-medica/package.json b/plataforma-medica/package.json new file mode 100644 index 000000000..102b498ea --- /dev/null +++ b/plataforma-medica/package.json @@ -0,0 +1,23 @@ +{ + "name": "plataforma-medica", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.7", + "react-dom": "^19.2.7" + }, + "devDependencies": { + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.2", + "oxlint": "^1.69.0", + "vite": "^8.1.0" + } +} diff --git a/plataforma-medica/public/favicon.svg b/plataforma-medica/public/favicon.svg new file mode 100644 index 000000000..6893eb132 --- /dev/null +++ b/plataforma-medica/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plataforma-medica/public/icons.svg b/plataforma-medica/public/icons.svg new file mode 100644 index 000000000..e9522193d --- /dev/null +++ b/plataforma-medica/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plataforma-medica/src/App.css b/plataforma-medica/src/App.css new file mode 100644 index 000000000..f90339d8f --- /dev/null +++ b/plataforma-medica/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/plataforma-medica/src/App.jsx b/plataforma-medica/src/App.jsx new file mode 100644 index 000000000..6fd7c254d --- /dev/null +++ b/plataforma-medica/src/App.jsx @@ -0,0 +1,4453 @@ +import React, { useState, useRef, useCallback, useEffect } from "react"; + +/* ============================================================ + DESIGN SYSTEM — iOS 27 / iPadOS 27 / macOS 27 Golden Gate + Liquid Glass refinado · SF Pro · HIG 2026 + ============================================================ */ +const SF = `-apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Segoe UI", Helvetica, Arial, sans-serif`; +const SF_MONO = `"SF Mono", ui-monospace, "JetBrains Mono", Menlo, monospace`; + +/* ── Paleta ── */ +const C = { + /* Backgrounds */ + bg: "#F2F2F7", // systemBackground iOS 27 + bgAlt: "#E5E5EA", // secondarySystemBackground + bgTertiary: "#FFFFFF", // tertiarySystemBackground + panel: "#FFFFFF", + panelGlass: "rgba(255,255,255,0.80)", + + /* Labels */ + ink: "#000000", // label + inkSoft: "#3C3C43", // secondaryLabel (opacity .6 over white) + inkTertiary: "#787880", // tertiaryLabel + inkQuaternary: "#C7C7CC", // quaternaryLabel + + /* Separators */ + line: "rgba(60,60,67,0.12)", // separator + lineSoft: "rgba(60,60,67,0.06)", // opaqueSeparator + + /* Accent — SystemMint (mantido da identidade Sua Logo) */ + primary: "#00BA84", + primarySoft: "rgba(0,186,132,0.12)", + primaryDark: "#009E70", + primaryGlass: "rgba(0,186,132,0.18)", + + /* System colours */ + blue: "#007AFF", + blueSoft: "rgba(0,122,255,0.10)", + amber: "#FF9F0A", + amberSoft: "rgba(255,159,10,0.10)", + red: "#FF3B30", + redSoft: "rgba(255,59,48,0.10)", + sage: "#34C759", + sageSoft: "rgba(52,199,89,0.12)", + purple: "#AF52DE", + purpleSoft: "rgba(175,82,222,0.10)", + + /* Fill (iOS 27 fill colours) */ + fill: "rgba(120,120,128,0.20)", + fillSecondary: "rgba(120,120,128,0.16)", + fillTertiary: "rgba(120,120,128,0.12)", +}; + +/* ── Raios — iOS 27 cantos mais arredondados e consistentes ── */ +const RADIUS = { + xs: 8, + sm: 12, + md: 14, // inputs, chips + lg: 20, // cards + xl: 28, // modais, sheets + pill: 9999, +}; + +/* ── Hit target mínimo Apple HIG ── */ +const HIT_MIN = 44; + +/* ── Liquid Glass refinado (iOS/macOS 27) + Contraste melhorado, bordas com gradiente, sombra com profundidade ── */ +const glass = { + background: "rgba(255,255,255,0.72)", + WebkitBackdropFilter: "blur(40px) saturate(200%) brightness(1.05)", + backdropFilter: "blur(40px) saturate(200%) brightness(1.05)", + border: "1px solid rgba(255,255,255,0.55)", + boxShadow: [ + "0 2px 0 0 rgba(255,255,255,0.60) inset", /* highlight topo */ + "0 -1px 0 0 rgba(0,0,0,0.04) inset", /* borda base */ + "0 8px 32px -8px rgba(0,0,0,0.10)", /* sombra suave */ + "0 1px 3px 0 rgba(0,0,0,0.06)", /* sombra próxima */ + ].join(", "), +}; + +/* Glass mais opaco para cards de conteúdo */ +const glassPanel = { + background: "rgba(255,255,255,0.82)", + WebkitBackdropFilter: "blur(40px) saturate(180%)", + backdropFilter: "blur(40px) saturate(180%)", + border: "1px solid rgba(255,255,255,0.60)", + boxShadow: [ + "0 2px 0 0 rgba(255,255,255,0.80) inset", + "0 1px 0 0 rgba(0,0,0,0.04) inset", + "0 4px 24px -6px rgba(0,0,0,0.08)", + "0 1px 4px 0 rgba(0,0,0,0.05)", + ].join(", "), +}; + +/* ── Escala tipográfica SF Pro (HIG 2026) ── */ +const TYPE = { + largeTitle: { fontSize: 34, fontWeight: 700, letterSpacing: "-0.4px", lineHeight: "41px" }, + title1: { fontSize: 28, fontWeight: 700, letterSpacing: "-0.3px", lineHeight: "34px" }, + title2: { fontSize: 22, fontWeight: 700, letterSpacing: "-0.2px", lineHeight: "28px" }, + title3: { fontSize: 20, fontWeight: 600, letterSpacing: "-0.1px", lineHeight: "25px" }, + headline: { fontSize: 17, fontWeight: 600, letterSpacing: "0px", lineHeight: "22px" }, + body: { fontSize: 17, fontWeight: 400, letterSpacing: "0px", lineHeight: "22px" }, + callout: { fontSize: 16, fontWeight: 400, letterSpacing: "0px", lineHeight: "21px" }, + subhead: { fontSize: 15, fontWeight: 400, letterSpacing: "0px", lineHeight: "20px" }, + footnote: { fontSize: 13, fontWeight: 400, letterSpacing: "0px", lineHeight: "18px" }, + caption1: { fontSize: 12, fontWeight: 400, letterSpacing: "0px", lineHeight: "16px" }, + caption2: { fontSize: 11, fontWeight: 400, letterSpacing: "0.07px",lineHeight: "13px" }, +}; + +/* ── Sombras iOS 27 (mais refinadas e com profundidade) ── */ +const SHADOW = { + xs: "0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)", + sm: "0 2px 8px -2px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.06)", + md: "0 4px 16px -4px rgba(0,0,0,0.10), 0 2px 6px rgba(0,0,0,0.06)", + lg: "0 8px 28px -6px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06)", + xl: "0 16px 48px -8px rgba(0,0,0,0.16), 0 4px 12px rgba(0,0,0,0.08)", +}; + +/* ============================================================ + TOAST SYSTEM + ============================================================ */ +const ToastCtx = React.createContext(null); + +function ToastProvider({ children }) { + const [toasts, setToasts] = useState([]); + const idRef = useRef(0); + + const push = useCallback((message, variant = "info") => { + const id = ++idRef.current; + setToasts((t) => [...t, { id, message, variant }]); + setTimeout(() => { + setToasts((t) => t.filter((x) => x.id !== id)); + }, 4800); + }, []); + + const dismiss = (id) => setToasts((t) => t.filter((x) => x.id !== id)); + + return ( + + {children} +
+ {toasts.map((t) => ( + dismiss(t.id)} /> + ))} +
+
+ ); +} + +function useToast() { + return React.useContext(ToastCtx); +} + +function Toast({ toast, onClose }) { + const variants = { + error: { icon: "✕", iconBg: C.red }, + warn: { icon: "!", iconBg: C.amber }, + success: { icon: "✓", iconBg: C.sage }, + info: { icon: "i", iconBg: C.primary }, + }; + const v = variants[toast.variant] || variants.info; + return ( +
+
{v.icon}
+
+ {toast.message} +
+ +
+ ); +} + +/* ============================================================ + VALIDATION HELPERS + ============================================================ */ +function validCPF(cpf) { + const digits = (cpf || "").replace(/\D/g, ""); + if (digits.length !== 11 || /^(\d)\1{10}$/.test(digits)) return false; + let sum = 0; + for (let i = 0; i < 9; i++) sum += parseInt(digits[i]) * (10 - i); + let rev = (sum * 10) % 11; + if (rev === 10 || rev === 11) rev = 0; + if (rev !== parseInt(digits[9])) return false; + sum = 0; + for (let i = 0; i < 10; i++) sum += parseInt(digits[i]) * (11 - i); + rev = (sum * 10) % 11; + if (rev === 10 || rev === 11) rev = 0; + return rev === parseInt(digits[10]); +} + +function maskCPF(v) { + return v.replace(/\D/g, "").slice(0, 11) + .replace(/(\d{3})(\d)/, "$1.$2") + .replace(/(\d{3})(\d)/, "$1.$2") + .replace(/(\d{3})(\d{1,2})$/, "$1-$2"); +} + +function maskPhone(v) { + const d = v.replace(/\D/g, "").slice(0, 11); + if (d.length <= 2) return d.replace(/(\d{0,2})/, "($1"); + if (d.length <= 7) return d.replace(/(\d{2})(\d{0,5})/, "($1) $2"); + return d.replace(/(\d{2})(\d{5})(\d{0,4})/, "($1) $2-$3"); +} + +function maskDate(v) { + return v.replace(/\D/g, "").slice(0, 8) + .replace(/(\d{2})(\d)/, "$1/$2") + .replace(/(\d{2})(\d)/, "$1/$2"); +} + +function isAdult(dateStr) { + const m = dateStr.match(/^(\d{2})\/(\d{2})\/(\d{4})$/); + if (!m) return false; + const [, dd, mm, yyyy] = m; + const birth = new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd)); + if (birth.getMonth() !== parseInt(mm) - 1) return false; + const today = new Date(); + let age = today.getFullYear() - birth.getFullYear(); + const md = today.getMonth() - birth.getMonth(); + if (md < 0 || (md === 0 && today.getDate() < birth.getDate())) age--; + return age >= 18 && age < 130; +} + +function validEmail(v) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); +} + +function countWords(v) { + return v.trim().split(/\s+/).filter(Boolean).length; +} + +/* ============================================================ + SHARED UI PRIMITIVES + ============================================================ */ +function Field({ label, hint, error, children, required, style }) { + return ( +
+ + {children} + {hint && !error && ( +
{hint}
+ )} + {error && ( +
{error}
+ )} +
+ ); +} + +const inputBase = { + width: "100%", + minHeight: HIT_MIN, + padding: "12px 16px", + fontSize: 17, // Body — Apple HIG + fontFamily: SF, + fontWeight: 400, + border: `1.5px solid ${C.line}`, + borderRadius: RADIUS.md, + outline: "none", + background: C.fillTertiary, /* iOS 27: fills em vez de branco puro */ + color: C.ink, + boxSizing: "border-box", + transition: "border-color 0.15s, box-shadow 0.15s, background 0.15s", + WebkitAppearance: "none", + appearance: "none", +}; + +function TextInput(props) { + const { error, style, ...rest } = props; + const [focus, setFocus] = useState(false); + return ( + { setFocus(true); rest.onFocus?.(e); }} + onBlur={(e) => { setFocus(false); rest.onBlur?.(e); }} + style={{ + ...inputBase, + background: focus ? "#fff" : C.fillTertiary, + borderColor: error ? C.red : focus ? C.primary : C.line, + boxShadow: focus + ? `0 0 0 3.5px ${error ? "rgba(255,59,48,0.18)" : C.primaryGlass}` + : error ? `0 0 0 3.5px rgba(255,59,48,0.10)` : "none", + ...style, + }} + /> + ); +} + +function TextArea(props) { + const { error, style, ...rest } = props; + const [focus, setFocus] = useState(false); + return ( +