Spaces:
Running
Running
| """Gradio theme and CSS built from the Bit design system. | |
| The design system ships six global stylesheets, listed in its own manifest as | |
| `globalCssPaths`, and they are loaded here **in that order**: | |
| tokens/fonts.css tokens/colors.css tokens/typography.css | |
| tokens/spacing.css base.css styles.css | |
| `base.css` is the important one and was previously vendored but never loaded. | |
| It carries the design's global reset: square corners everywhere | |
| (`border-radius:0 !important`), zeroed body margins, heading defaults, link and | |
| focus treatment, the 8px pixel-font rule, and the scrollbar styling. Without it | |
| the app inherited Gradio's rounded, roomy defaults and only *approximated* the | |
| design. | |
| Three things fight the design if left alone, and all three are handled here: | |
| 1. **Gradio's spacing scale.** Its defaults are `layout_gap: *spacing_xxl` and | |
| `block_padding: *spacing_xl` — far looser than the design, which uses 12px | |
| panel padding and 6–10px gaps. These are configured on the theme rather than | |
| fought selector-by-selector. | |
| 2. **Gradio's type scale.** Its `body_text_size` is `*text_md` (16px). The | |
| design's workhorse size is `--text-xs` (10px) with `--text-2xs` (8px) for | |
| micro labels; `--text-base` (12px) appears exactly once in the whole design. | |
| 3. **Rounded corners.** Gradio rounds everything; the design is square. | |
| """ | |
| from __future__ import annotations | |
| from pathlib import Path | |
| import gradio as gr | |
| ASSETS = Path(__file__).resolve().parent.parent.parent / "assets" | |
| FONT_DIR = ASSETS / "fonts" | |
| TOKENS = ASSETS / "tokens" | |
| # The design system's own manifest order. styles.css is only @import glue for | |
| # the others, so it is resolved here rather than fetched. | |
| GLOBAL_CSS = ("colors.css", "typography.css", "spacing.css", "base.css") | |
| def _font_face_css() -> str: | |
| """@font-face rules pointing at Gradio's static file route.""" | |
| faces = [ | |
| ("Styrene A", "StyreneA-Thin.otf", 100, "normal"), | |
| ("Styrene A", "StyreneA-Light.otf", 300, "normal"), | |
| ("Styrene A", "StyreneA-Regular.otf", 400, "normal"), | |
| ("Styrene A", "StyreneA-Medium.otf", 500, "normal"), | |
| ("Mac Minecraft", "MacMinecraft.ttf", 400, "normal"), | |
| ] | |
| out = [] | |
| for family, filename, weight, style in faces: | |
| path = FONT_DIR / filename | |
| if not path.exists(): | |
| continue | |
| fmt = "opentype" if filename.endswith(".otf") else "truetype" | |
| out.append( | |
| f"@font-face{{font-family:'{family}';" | |
| f"src:url('/gradio_api/file={path}') format('{fmt}');" | |
| f"font-weight:{weight};font-style:{style};font-display:swap}}" | |
| ) | |
| return "\n".join(out) | |
| def _global_css() -> str: | |
| """Inline the design system's global stylesheets, verbatim and in order.""" | |
| parts = [] | |
| for name in GLOBAL_CSS: | |
| p = TOKENS / name | |
| if p.exists(): | |
| parts.append(f"/* ---- design system: {name} ---- */\n{p.read_text()}") | |
| return "\n".join(parts) | |
| # base.css sets `user-select:none` on everything and re-enables it only for | |
| # inputs, code and `.bit-selectable`. That suits a trading dashboard but not a | |
| # data tool: people need to select numbers out of the tables. This is the one | |
| # deliberate departure from base.css, scoped to data surfaces only. | |
| SELECTION_FIX = """ | |
| table, table *, .bit-table, .bit-table *, .prose, .prose *, | |
| .bit-stat-value, .bit-stat-sub, .bit-sig-row, .bit-sig-row *, | |
| .bit-podium, .bit-podium *, .bit-note, .bit-note *, | |
| .markdown, .markdown * { user-select: text; } | |
| """ | |
| # -------------------------------------------------------------------------- | |
| # Gradio neutralisation: strip its layout spacing back to the design's scale. | |
| # -------------------------------------------------------------------------- | |
| GRADIO_RESET = """ | |
| /* Gradio wraps every component in padded, gapped containers. Collapse them so | |
| the design's own spacing is the only spacing on the page. */ | |
| .gradio-container{ max-width:100% !important; padding:0 !important; } | |
| .gradio-container .block, | |
| .gradio-container .form, | |
| .gradio-container .styler, | |
| .gradio-container .container, | |
| .gradio-container .panel{ | |
| background:transparent !important; border:none !important; | |
| box-shadow:none !important; padding:0 !important; | |
| } | |
| .gradio-container .gap{ gap:8px !important; } | |
| .gradio-container .form > *{ margin:0 !important; } | |
| .gradio-container .block > .wrap{ padding:0 !important; } | |
| .gradio-container .tabitem{ padding:10px 0 0 !important; border:none !important; } | |
| .gradio-container .column, .gradio-container .row{ gap:8px !important; } | |
| .gradio-container .hide-container{ margin:0 !important; padding:0 !important; } | |
| .gradio-container span.svelte-1gfkn6j, .gradio-container .block-title{ margin:0 !important; } | |
| .gradio-container .wrap.svelte-1sk0pyu{ padding:0 !important; } | |
| .gradio-container .icon-button-wrapper{ display:none !important; } | |
| .gradio-container .block > label{ margin-bottom:2px !important; } | |
| .gradio-container .prose :is(h1,h2,h3,h4){ margin:6px 0 4px !important; } | |
| .gradio-container .prose p, .gradio-container .prose li{ | |
| margin:2px 0 !important; font-size:var(--text-xs) !important; | |
| } | |
| .gradio-container .prose ul{ margin:2px 0 !important; padding-left:16px !important; } | |
| footer, .gradio-container footer{ display:none !important; } | |
| """ | |
| SHELL_CSS = """ | |
| /* ========================================================================== | |
| Atlas shell. | |
| The design expresses almost everything as inline styles on its own | |
| elements, so there is very little component CSS to write here. What is | |
| left is the handful of things inline styles cannot express: | |
| * hover states -- the design writes them as a `style-hover` attribute, | |
| which is a design-tool convention with no HTML equivalent, so each one | |
| is translated into a real rule below | |
| * the hidden bridge elements | |
| * scrollbars, which base.css styles for the page but not for the | |
| design's own scroll containers | |
| ========================================================================== */ | |
| /* The bridge's transport. `visible=False` would remove these from the DOM | |
| entirely and the delegated listener would write into nothing, so they are | |
| rendered and hidden here instead. */ | |
| #bit-action, #bit-trigger{ | |
| position:absolute !important; width:1px !important; height:1px !important; | |
| padding:0 !important; margin:-1px !important; overflow:hidden !important; | |
| clip:rect(0,0,0,0) !important; white-space:nowrap !important; border:0 !important; | |
| } | |
| /* Gradio centres its app in a padded container that eats ~200px of a wide | |
| viewport. The Atlas is a full-bleed dashboard. */ | |
| .gradio-container{ width:100% !important; margin:0 !important; } | |
| .gradio-container > .main, .gradio-container .contain{ padding:0 !important; gap:0 !important; } | |
| /* ---------------- hover states (design `style-hover`) ---------------- */ | |
| .bit-hover-amber:hover{ | |
| border-color:var(--accent-amber) !important; | |
| color:var(--accent-amber-strong) !important; | |
| } | |
| .bit-hover-row:hover{ background:var(--bg-raised) !important; } | |
| .bit-hover-text:hover{ color:var(--text-primary) !important; } | |
| .bit-hover-amber-text:hover{ color:var(--accent-amber-strong) !important; } | |
| .bit-hover-strong:hover{ | |
| border-color:var(--border-strong) !important; color:var(--text-primary) !important; | |
| } | |
| .bit-hover-red:hover{ | |
| border-color:var(--mute-red) !important; color:var(--text-primary) !important; | |
| } | |
| .bit-hover-nav:hover{ | |
| background:var(--bg-raised) !important; color:var(--text-primary) !important; | |
| text-decoration:none !important; | |
| } | |
| .bit-hover-cta:hover{ background:var(--accent-amber-strong) !important; } | |
| .bit-hover-connect:hover{ | |
| background:var(--accent-amber) !important; border-color:var(--accent-amber) !important; | |
| color:var(--stone-950) !important; text-decoration:none !important; | |
| } | |
| /* ---------------- scroll containers ---------------- */ | |
| .bit-scroll{ scrollbar-width:thin; scrollbar-color:var(--border-strong) transparent; } | |
| .bit-scroll::-webkit-scrollbar{ width:8px; height:8px; } | |
| .bit-scroll::-webkit-scrollbar-track{ background:transparent; } | |
| .bit-scroll::-webkit-scrollbar-thumb{ background:var(--border-default); } | |
| .bit-scroll::-webkit-scrollbar-thumb:hover{ background:var(--border-strong); } | |
| /* ---------------- animations the design declares ---------------- */ | |
| @keyframes bitTape{ from{transform:translateX(0)} to{transform:translateX(-50%)} } | |
| @keyframes bitPulse{ 0%,100%{opacity:1} 50%{opacity:0.25} } | |
| @keyframes bitSlide{ from{transform:translateX(24px);opacity:0} to{transform:translateX(0);opacity:1} } | |
| @keyframes bitFade{ from{opacity:0} to{opacity:1} } | |
| /* Respect a reduced-motion preference: the ticker tape is a 70s infinite | |
| marquee, which is exactly the kind of thing that setting exists for. */ | |
| @media (prefers-reduced-motion: reduce){ | |
| .bit-tape, [style*="bitTape"], [style*="bitPulse"]{ animation:none !important; } | |
| } | |
| /* Data surfaces must stay selectable -- people copy model ids out of the | |
| table. base.css disables selection globally for the dashboard feel. */ | |
| .bit-atlas, .bit-atlas *{ user-select:text; } | |
| .bit-atlas button, .bit-atlas a{ user-select:none; } | |
| /* The table scrolls horizontally inside its own panel rather than making the | |
| page scroll sideways. */ | |
| .bit-table-scroll{ overflow-x:auto; } | |
| /* Focus visibility: the design hands us bare <button>s, and a keyboard user | |
| still needs to see where they are. */ | |
| .bit-atlas :is(button,a,input):focus-visible{ | |
| outline:2px solid var(--accent-amber); outline-offset:1px; | |
| } | |
| """ | |
| def full_css() -> str: | |
| """Fonts, the design system's globals, the Gradio reset, then the shell.""" | |
| return "\n".join([_font_face_css(), _global_css(), SELECTION_FIX, | |
| GRADIO_RESET, SHELL_CSS]) | |
| def bit_theme() -> gr.Theme: | |
| """Gradio theme configured to the design's scale. | |
| Setting Gradio's own spacing/radius/text variables is what stops its | |
| defaults from injecting padding and gaps the design never asked for. Doing | |
| it here rather than with `!important` overrides means the generated CSS is | |
| already right instead of being corrected afterwards. | |
| """ | |
| return gr.themes.Base( | |
| primary_hue=gr.themes.colors.yellow, | |
| secondary_hue=gr.themes.colors.lime, | |
| neutral_hue=gr.themes.colors.stone, | |
| font=["Styrene A", "system-ui", "sans-serif"], | |
| font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"], | |
| spacing_size=gr.themes.sizes.spacing_sm, | |
| radius_size=gr.themes.sizes.radius_none, | |
| text_size=gr.themes.sizes.text_sm, | |
| ).set( | |
| # Palette | |
| body_background_fill="#161512", | |
| body_text_color="#f7f4ec", | |
| background_fill_primary="#1d1c18", | |
| background_fill_secondary="#24221d", | |
| block_background_fill="transparent", | |
| block_border_color="#3d3a32", | |
| block_border_width="0px", | |
| block_label_text_color="#6f6a56", | |
| border_color_primary="#3d3a32", | |
| button_primary_background_fill="#af9209", | |
| button_primary_text_color="#161512", | |
| button_secondary_background_fill="transparent", | |
| button_secondary_text_color="#b6b09a", | |
| input_background_fill="#000000", | |
| input_border_color="#3d3a32", | |
| # Spacing -- the design uses 12px panel padding and 6-10px gaps. | |
| layout_gap="8px", | |
| block_padding="0px", | |
| form_gap_width="0px", | |
| block_label_margin="0px", | |
| block_title_padding="0px", | |
| input_padding="5px 8px", | |
| checkbox_label_padding="3px 8px", | |
| checkbox_label_gap="6px", | |
| button_small_padding="5px 10px", | |
| button_large_padding="7px 12px", | |
| # Type -- 10px workhorse, matching the design's dominant size. | |
| body_text_size="10px", | |
| input_text_size="10px", | |
| block_label_text_size="8px", | |
| block_title_text_size="10px", | |
| button_small_text_size="8px", | |
| button_large_text_size="10px", | |
| checkbox_label_text_size="8px", | |
| prose_text_size="10px", | |
| # Square, everywhere. | |
| block_radius="0px", | |
| input_radius="0px", | |
| button_large_radius="0px", | |
| button_small_radius="0px", | |
| checkbox_border_radius="0px", | |
| container_radius="0px", | |
| ) | |
| def static_paths() -> list[str]: | |
| """Directories Gradio may serve (fonts, tokens, mark).""" | |
| return [str(ASSETS)] | |