Spaces:
Running
Running
Download src/core/theme.py from klydekushy/Vortex-Flux: direct link, hf CLI and curl.
- Browser
- Download file 18.1 kB
-
https://huggingface.co/spaces/klydekushy/Vortex-Flux/resolve/main/src/core/theme.py
- Command line
-
hf download hf://spaces/klydekushy/Vortex-Flux/src/core/theme.py
-
curl -L -o theme.py https://huggingface.co/spaces/klydekushy/Vortex-Flux/resolve/main/src/core/theme.py
18.1 kB
| """ | |
| THEMES VORTEX β NOIR (defaut, Gotham / poste de commandement) et BLEU | |
| ====================================================================== | |
| Le theme BLEU est celui de .streamlit/config.toml et de toutes les couleurs ecrites dans le code. | |
| Le theme NOIR n'est PAS une seconde copie du style : c'est une table de correspondance | |
| BLEU -> NOIR appliquee automatiquement, au moment du rendu, a : | |
| - tout HTML/CSS injecte (st.markdown / st.html / components.html, y compris le grand bloc CSS | |
| de streamlit_app.py, les bandeaux, l'overlay, le graphe Ontology...) ; | |
| - toutes les figures Plotly (st.plotly_chart). | |
| Les composants natifs Streamlit (fond, barre laterale, champs, listes, onglets, boutons, | |
| alertes, toasts...) recoivent leurs couleurs du bloc CSS_CHROME, ecrit en couleurs BLEU puis | |
| converti de la meme facon : une seule source de style pour les deux themes. | |
| Le choix est memorise par navigateur (cookie vortex_theme, 1 an). | |
| Limite : l'interieur des tableaux st.dataframe est dessine sur un canvas avec les couleurs de | |
| config.toml (fond #0a0e12, quasi noir) ; seul leur cadre suit le theme. | |
| """ | |
| import json | |
| import re | |
| import streamlit as st | |
| THEMES = ("BLEU", "NOIR") | |
| DEFAUT = "NOIR" | |
| NOM_COOKIE = "vortex_theme" | |
| CLE_SESSION = "vx_theme" | |
| # ββ Correspondance des couleurs BLEU -> NOIR ββββββββββββββββββββββββββββββββ | |
| # Fonds bleu-nuit -> noirs ; gris bleutes -> gris neutres ; accent bleu -> ambre (selection, | |
| # element actif, comme les postes de commandement) ; etats (vert / orange / rouge) desatures. | |
| HEX_NOIR = { | |
| # fonds | |
| "#0a0e12": "#000000", "#0d1117": "#030303", "#0f141a": "#080808", "#161b22": "#0e0e0e", | |
| "#1a2a4a": "#161616", "#21262d": "#141414", "#30363d": "#262626", | |
| # gris / textes | |
| "#2a3a4a": "#2b2b2b", "#3a4a5a": "#3a3a3a", "#4a5a6a": "#4c4c4c", "#5a6a7a": "#6a6a6a", | |
| "#6a7a8a": "#777777", "#7a8a9a": "#878787", "#8a9aaa": "#999999", "#8b949e": "#8f8f8f", | |
| "#a8b8c8": "#c4c4c4", "#c9d1d9": "#dedede", "#e6edf3": "#f0f0f0", | |
| # accent | |
| "#58a6ff": "#f2c94c", "#388bfd": "#d9ad2f", "#1e88e5": "#e0b43c", "#79c0ff": "#f6d77a", | |
| # etats | |
| "#54bd4b": "#6fcf97", "#2ecc71": "#6fcf97", "#3fb950": "#6fcf97", | |
| "#f39c12": "#f2994a", "#e67e22": "#e8893a", "#f0b90b": "#f2c94c", | |
| "#c0392b": "#d9534f", "#e74c3c": "#e05a4f", "#f85149": "#e05a4f", | |
| # grilles et axes Plotly (modeles par defaut) : bleutes -> neutres | |
| "#283442": "#1c1c1c", "#506784": "#3c3c3c", "#2a3f5f": "#cfcfcf", "#ebf0f8": "#1c1c1c", "#e5ecf6": "#0a0a0a", | |
| } | |
| RGB_NOIR = { | |
| (10, 14, 18): (0, 0, 0), (13, 17, 23): (3, 3, 3), (15, 20, 26): (8, 8, 8), (22, 27, 34): (14, 14, 14), | |
| (80, 100, 120): (90, 90, 90), (100, 120, 140): (110, 110, 110), (60, 80, 100): (70, 70, 70), | |
| (50, 65, 80): (55, 55, 55), (168, 184, 200): (196, 196, 196), (48, 54, 61): (38, 38, 38), | |
| (88, 166, 255): (242, 201, 76), (30, 136, 229): (224, 180, 60), | |
| (243, 156, 18): (242, 153, 74), (192, 57, 43): (217, 83, 79), (231, 76, 60): (224, 90, 79), | |
| (84, 189, 75): (111, 207, 151), (46, 204, 113): (111, 207, 151), | |
| } | |
| _RE_HEX = re.compile(r"#[0-9a-fA-F]{6}\b") | |
| _RE_RGB = re.compile(r"rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(,\s*[\d.]+\s*)?\)", re.IGNORECASE) | |
| def actif() -> str: | |
| try: | |
| valeur = st.session_state.get(CLE_SESSION) | |
| except Exception: | |
| valeur = None | |
| return valeur if valeur in THEMES else DEFAUT | |
| def est_noir() -> bool: | |
| return actif() == "NOIR" | |
| def convertir(texte, theme=None) -> str: | |
| """Texte (HTML, CSS, JSON) avec les couleurs du theme. BLEU : inchange.""" | |
| if (theme or actif()) != "NOIR" or not isinstance(texte, str) or not texte: | |
| return texte | |
| def _hex(m): | |
| return HEX_NOIR.get(m.group(0).lower(), m.group(0)) | |
| def _rgb(m): | |
| cle = tuple(int(x) for x in m.groups()[:3]) | |
| if cle not in RGB_NOIR: | |
| return m.group(0) | |
| r, g, b = RGB_NOIR[cle] | |
| alpha = m.group(4) | |
| return f"rgba({r}, {g}, {b}, {alpha.strip(' ,')})" if alpha else f"rgb({r}, {g}, {b})" | |
| return _RE_RGB.sub(_rgb, _RE_HEX.sub(_hex, texte)) | |
| def convertir_figure(fig, theme=None): | |
| """Figure Plotly aux couleurs du theme (copie ; l'original n'est pas modifie).""" | |
| if (theme or actif()) != "NOIR" or fig is None or not hasattr(fig, "to_json"): | |
| return fig | |
| try: | |
| import plotly.io as pio | |
| return pio.from_json(convertir(fig.to_json(), "NOIR")) | |
| except Exception: | |
| return fig | |
| # ββ Installation : conversion automatique au rendu ββββββββββββββββββββββββββ | |
| def installer(): | |
| """Branche la conversion sur markdown / html / components.html / plotly_chart. Idempotent.""" | |
| from streamlit.delta_generator import DeltaGenerator | |
| import streamlit.components.v1 as components_v1 | |
| if getattr(DeltaGenerator, "_vortex_theme_installe", False): | |
| return | |
| md, ht, pc = DeltaGenerator.markdown, DeltaGenerator.html, DeltaGenerator.plotly_chart | |
| def _markdown(self, body, *args, **kwargs): | |
| if kwargs.get("unsafe_allow_html") or (len(args) > 0 and args[0] is True): | |
| body = convertir(body) | |
| return md(self, body, *args, **kwargs) | |
| def _html(self, body, *args, **kwargs): | |
| return ht(self, convertir(body) if isinstance(body, str) else body, *args, **kwargs) | |
| def _plotly(self, figure_or_data, *args, **kwargs): | |
| return pc(self, convertir_figure(figure_or_data), *args, **kwargs) | |
| DeltaGenerator.markdown, DeltaGenerator.html, DeltaGenerator.plotly_chart = _markdown, _html, _plotly | |
| st.markdown = lambda body, *a, **k: DeltaGenerator.markdown(st._main, body, *a, **k) | |
| st.html = lambda body, *a, **k: DeltaGenerator.html(st._main, body, *a, **k) | |
| st.plotly_chart = lambda fig, *a, **k: DeltaGenerator.plotly_chart(st._main, fig, *a, **k) | |
| composant = components_v1.html | |
| def _composant(html_texte, *args, **kwargs): | |
| return composant(convertir(html_texte), *args, **kwargs) | |
| components_v1.html = _composant | |
| DeltaGenerator._vortex_theme_installe = True | |
| def initialiser(): | |
| """Au debut de chaque execution : theme de la session, sinon celui du cookie du navigateur.""" | |
| if st.session_state.get(CLE_SESSION) in THEMES: | |
| return | |
| try: | |
| cookie = st.context.cookies.get(NOM_COOKIE) | |
| except Exception: | |
| cookie = None | |
| st.session_state[CLE_SESSION] = cookie if cookie in THEMES else DEFAUT | |
| def selecteur(zone=None): | |
| """Interrupteur BLEU / NOIR (barre laterale). Le choix est ecrit dans un cookie (1 an).""" | |
| zone = zone or st.sidebar | |
| # Cookie ecrit a l'execution SUIVANT le changement : un script rendu juste avant st.rerun() | |
| # n'atteint jamais le navigateur. | |
| a_poser = st.session_state.pop("_vx_cookie_a_poser", None) | |
| if a_poser in THEMES: | |
| valeur = json.dumps(f"{NOM_COOKIE}={a_poser}; Path=/; Max-Age=31536000; Secure; SameSite=Lax") | |
| import streamlit.components.v1 as components_v1 | |
| components_v1.html(f"<script>try {{ window.parent.document.cookie = {valeur}; }} catch (e) {{}}</script>", | |
| height=0) | |
| noir = zone.toggle("MODE NOIR Β· GOTHAM", value=est_noir(), key="vx_theme_toggle", | |
| help="Thème noir poste de commandement. Désactivé : thème bleu habituel.") | |
| choix = "NOIR" if noir else "BLEU" | |
| if choix != st.session_state.get(CLE_SESSION): | |
| st.session_state[CLE_SESSION] = choix | |
| st.session_state["_vx_cookie_a_poser"] = choix | |
| st.rerun() | |
| # ββ Style des composants natifs (ecrit en couleurs BLEU, converti pour NOIR) β | |
| CSS_CHROME = """ | |
| <style> | |
| :root { | |
| --vx-bg: #0a0e12; --vx-panel: #0f141a; --vx-panel-2: #161b22; --vx-line: rgba(80, 100, 120, 0.3); | |
| --vx-text: #c9d1d9; --vx-muted: #8b949e; --vx-accent: #58a6ff; | |
| --vx-ok: #54bd4b; --vx-info: #58a6ff; --vx-warn: #f39c12; --vx-err: #e74c3c; | |
| --vx-mono: 'JetBrains Mono', 'Courier New', monospace; | |
| } | |
| /* ββ Fonds βββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .stApp, [data-testid="stAppViewContainer"], [data-testid="stMain"], [data-testid="stHeader"], | |
| [data-testid="stBottom"] > div, [data-testid="stBottomBlockContainer"] { background-color: var(--vx-bg) !important; } | |
| [data-testid="stSidebar"], [data-testid="stSidebar"] > div:first-child { | |
| background-color: var(--vx-panel) !important; border-right: 1px solid var(--vx-line) !important; } | |
| [data-testid="stHeader"] { border-bottom: 1px solid var(--vx-line) !important; } | |
| /* ββ Champs de saisie, listes, dates, zone de chat βββββββ */ | |
| [data-testid="stTextInputRootElement"], [data-testid="stTextAreaRootElement"], [data-testid="stNumberInputContainer"], | |
| [data-testid="stSelectbox"] [role="group"], [data-testid="stMultiSelect"] [role="group"], | |
| [data-testid="stDateInput"] [role="group"], [data-testid="stDateInputField"], [data-testid="stChatInput"], | |
| [data-testid="stChatInputTextArea"], [data-testid="stFileUploaderDropzone"], [data-testid="stChatInput"] > div { | |
| background-color: var(--vx-bg) !important; border-color: var(--vx-line) !important; border-radius: 0 !important; color: var(--vx-text) !important; } | |
| [data-testid="stTextInputField"], [data-testid="stNumberInputField"], textarea, input { color: var(--vx-text) !important; } | |
| [data-testid="stTextInputRootElement"]:focus-within, [data-testid="stTextAreaRootElement"]:focus-within, | |
| [data-testid="stNumberInputContainer"]:focus-within, [data-testid="stSelectbox"] [role="group"]:focus-within, | |
| [data-testid="stMultiSelect"] [role="group"]:focus-within, [data-testid="stChatInput"]:focus-within { | |
| border-color: var(--vx-accent) !important; box-shadow: none !important; } | |
| [role="listbox"], [role="dialog"], [role="menu"], [data-testid="stPopoverBody"], [data-testid="stTooltipContent"] { | |
| background-color: var(--vx-panel) !important; border: 1px solid var(--vx-line) !important; border-radius: 0 !important; color: var(--vx-text) !important; } | |
| [role="option"][aria-selected="true"], [role="option"]:hover, [role="option"][data-focused="true"] { | |
| background-color: rgba(88, 166, 255, 0.12) !important; color: var(--vx-accent) !important; } | |
| [data-testid="stMultiSelect"] [data-tag] { background-color: rgba(88, 166, 255, 0.12) !important; | |
| color: var(--vx-accent) !important; border: 1px solid rgba(88, 166, 255, 0.4) !important; border-radius: 0 !important; } | |
| /* ββ Boutons ββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| [data-testid^="stBaseButton-secondary"], [data-testid="stBaseButton-secondaryFormSubmit"], [data-testid="stDownloadButton"] button { | |
| background-color: var(--vx-panel) !important; color: var(--vx-text) !important; border: 1px solid var(--vx-line) !important; | |
| border-radius: 0 !important; font-family: var(--vx-mono) !important; text-transform: uppercase; letter-spacing: 1.5px; } | |
| [data-testid^="stBaseButton-secondary"]:hover, [data-testid="stDownloadButton"] button:hover { | |
| border-color: var(--vx-accent) !important; color: var(--vx-accent) !important; } | |
| [data-testid^="stBaseButton-primary"] { | |
| background-color: rgba(88, 166, 255, 0.12) !important; color: var(--vx-accent) !important; border: 1px solid var(--vx-accent) !important; | |
| border-radius: 0 !important; font-family: var(--vx-mono) !important; text-transform: uppercase; letter-spacing: 1.5px; font-weight: 700 !important; } | |
| [data-testid^="stBaseButton-primary"]:hover { background-color: var(--vx-accent) !important; color: var(--vx-bg) !important; } | |
| [data-testid^="stBaseButton-primary"] p, [data-testid^="stBaseButton-secondary"] p { color: inherit !important; } | |
| /* ββ Onglets, expanders, conteneurs, metriques ββββββββββββ */ | |
| [data-testid="stTab"] p { font-family: var(--vx-mono) !important; text-transform: uppercase; letter-spacing: 1.5px; font-size: 0.78rem !important; color: var(--vx-muted) !important; } | |
| [data-testid="stTab"][data-selected="true"] p { color: var(--vx-accent) !important; } | |
| [data-testid="stTab"][data-selected="true"] { box-shadow: inset 0 -2px 0 var(--vx-accent) !important; } | |
| [data-testid="stExpander"] details { background-color: var(--vx-panel) !important; border: 1px solid var(--vx-line) !important; border-radius: 0 !important; } | |
| [data-testid="stExpander"] summary p { font-family: var(--vx-mono) !important; text-transform: uppercase; letter-spacing: 1.5px; font-size: 0.75rem !important; } | |
| [data-testid="stExpander"] summary:hover p, [data-testid="stExpander"] summary:hover [data-testid="stIconMaterial"] { color: var(--vx-accent) !important; } | |
| [data-testid="stVerticalBlockBorderWrapper"], [data-testid="stForm"], [data-testid="stVerticalBlock"] { border-color: var(--vx-line) !important; border-radius: 0 !important; } | |
| [data-testid="stMetric"] { background-color: var(--vx-panel); border: 1px solid var(--vx-line); border-left: 2px solid var(--vx-accent); padding: 10px 14px; } | |
| [data-testid="stMetricLabel"] p { font-family: var(--vx-mono) !important; text-transform: uppercase; letter-spacing: 1.5px; font-size: 0.68rem !important; color: var(--vx-muted) !important; } | |
| [data-testid="stMetricValue"] { font-family: var(--vx-mono) !important; color: var(--vx-text) !important; } | |
| [data-testid="stDataFrame"], [data-testid="stTable"] { border: 1px solid var(--vx-line) !important; } | |
| [data-testid="stCode"], [data-testid="stCode"] pre { background-color: var(--vx-bg) !important; border: 1px solid var(--vx-line) !important; border-radius: 0 !important; } | |
| [data-testid="stCode"] code { font-family: var(--vx-mono) !important; color: var(--vx-text) !important; } | |
| [data-testid="stProgressBarTrack"] { background-color: var(--vx-panel-2) !important; border-radius: 0 !important; } | |
| [data-testid="stProgressBarTrack"] > div { background-color: var(--vx-accent) !important; border-radius: 0 !important; } | |
| [data-testid="stChatMessage"] { background-color: var(--vx-panel) !important; border: 1px solid var(--vx-line); border-radius: 0 !important; } | |
| hr { border-color: var(--vx-line) !important; } | |
| [data-testid="stCaptionContainer"], [data-testid="stCaptionContainer"] p { color: var(--vx-muted) !important; } | |
| /* ββ Alertes : st.success / info / warning / error / exception βββββββββββββ | |
| Bandeau tactique : panneau sombre, liserΓ© gauche de la couleur de l'etat, etiquette. */ | |
| [data-testid="stAlertContainer"] { | |
| --vx-k: var(--vx-info); background-color: var(--vx-panel) !important; color: var(--vx-text) !important; | |
| border: 1px solid var(--vx-line) !important; border-left: 3px solid var(--vx-k) !important; border-radius: 0 !important; | |
| padding: 10px 14px !important; font-family: var(--vx-mono) !important; } | |
| [data-testid="stAlertContainer"]:has([data-testid="stAlertContentSuccess"]) { --vx-k: var(--vx-ok); } | |
| [data-testid="stAlertContainer"]:has([data-testid="stAlertContentWarning"]) { --vx-k: var(--vx-warn); } | |
| [data-testid="stAlertContainer"]:has([data-testid="stAlertContentError"]) { --vx-k: var(--vx-err); } | |
| [data-testid^="stAlertContent"] p, [data-testid^="stAlertContent"] li { color: var(--vx-text) !important; font-family: var(--vx-mono) !important; font-size: 0.8rem !important; } | |
| [data-testid^="stAlertContent"]::before { | |
| display: block; margin-bottom: 4px; font-family: var(--vx-mono); font-size: 0.62rem; font-weight: 800; | |
| letter-spacing: 2.5px; text-transform: uppercase; color: var(--vx-k); } | |
| [data-testid="stAlertContentSuccess"]::before { content: "β CONFIRMATION"; } | |
| [data-testid="stAlertContentInfo"]::before { content: "β INFORMATION"; } | |
| [data-testid="stAlertContentWarning"]::before { content: "β² AVERTISSEMENT"; } | |
| [data-testid="stAlertContentError"]::before { content: "[ X ] ALERTE"; } | |
| [data-testid="stException"] [data-testid="stAlertContentError"]::before { content: "[ X ] EXCEPTION SYSTEME"; } | |
| [data-testid="stExceptionMessage"], [data-testid="stExceptionTraceRow"] { font-family: var(--vx-mono) !important; color: var(--vx-text) !important; } | |
| [data-testid="stAlertContainer"] svg, [data-testid="stAlertContainer"] [data-testid="stIconMaterial"] { color: var(--vx-k) !important; } | |
| /* ββ Toasts βββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| [data-testid="stToast"] { background-color: var(--vx-panel) !important; color: var(--vx-text) !important; | |
| border: 1px solid var(--vx-line) !important; border-left: 3px solid var(--vx-accent) !important; border-radius: 0 !important; | |
| font-family: var(--vx-mono) !important; } | |
| [data-testid="stToast"] p { color: var(--vx-text) !important; font-family: var(--vx-mono) !important; } | |
| /* ββ Barres de defilement βββββββββββββββββββββββββββββββββ */ | |
| ::-webkit-scrollbar { width: 8px; height: 8px; } | |
| ::-webkit-scrollbar-track { background: var(--vx-bg); } | |
| ::-webkit-scrollbar-thumb { background: var(--vx-panel-2); border: 1px solid var(--vx-line); } | |
| ::-webkit-scrollbar-thumb:hover { background: var(--vx-accent); } | |
| </style> | |
| """ | |
| # Uniquement en NOIR : typographie "mission suite" (titres fins, grandes capitales espacees). | |
| CSS_NOIR = """ | |
| <style> | |
| :root { --vx-info: #7fa7c9; } /* info : bleu acier, distinct de l'accent ambre */ | |
| .stApp h1, .stApp h2, .stApp h3 { | |
| font-family: 'Space Grotesk', 'Helvetica Neue', Arial, sans-serif !important; font-weight: 300 !important; | |
| text-transform: uppercase; letter-spacing: 0.22em !important; color: #f0f0f0 !important; } | |
| .stApp h1 strong, .stApp h2 strong, .stApp h3 strong { font-weight: 700 !important; } | |
| [data-testid="stSidebar"] { background-color: #050505 !important; } | |
| [data-testid="stHeader"] { background-color: #000000 !important; } | |
| </style> | |
| """ | |
| def css() -> str: | |
| """Feuille de style du theme actif (a injecter une fois par execution, apres le CSS principal).""" | |
| return convertir(CSS_CHROME) + (CSS_NOIR if est_noir() else "") | |