- name
- ui-styling
- description
- Guide for the visual style, structure, and shared building blocks used by Semantic Link Labs interactive UI tools (HTML widgets and anywidget-based widgets). Use this when adding a new interactive UI, modifying an existing one, or adding shared visual components.
# UI Styling for Interactive Tools
This skill describes the styling conventions, shared building blocks, and architectural patterns used by all interactive UI tools in Semantic Link Labs so that every tool has a consistent, elegant design.
## When to Use This Skill
Use this skill when you need to:
- Add a **new interactive UI** function to the library.
- Modify an existing interactive UI (e.g. `vertipaq_analyzer`, `delta_analyzer`, `perspective_editor`).
- Add new **shared visual components** (icons, theme variables, headers, etc.) that should be reused across tools.
- Decide between a **static-HTML** widget and an **anywidget**-based widget.
---
## The Two UI Patterns
Semantic Link Labs has exactly two supported patterns for interactive UI tools. Always pick one based on whether the UI needs to call back into Python after the initial render.
| Pattern | When to use | Reference implementations |
|---------|-------------|---------------------------|
| **Static-HTML widget** (the *Vertipaq style*) | The UI is fully driven by the data computed in Python *before* render. All interactivity (filtering, sorting, tab switching, theme toggle, column resizing, etc.) is done in **pure browser-side JavaScript**. No Python code runs after `display(HTML(...))`. | `sempy_labs.semantic_model._vertipaq_analyzer.vertipaq_analyzer`, `sempy_labs._delta_analyzer.delta_analyzer` |
| **anywidget widget** (the *Perspective Editor style*) | The UI must run Python code in response to user actions (e.g. write back to a semantic model, refresh data from a REST API, perform long-running operations). State is synced between the JS frontend and the Python backend via `traitlets`. | `sempy_labs.semantic_model._perspective_editor.perspective_editor` |
> **Rule of thumb:** if the only thing the user does is view, sort, filter, or switch between pre-computed data, use the **static-HTML** pattern. If they can *change* something that must persist (model edits, refresh triggers, server calls), use **anywidget**.
---
## Shared Building Blocks: `sempy_labs._ui_components`
`src/sempy_labs/_ui_components.py` is the single source of truth for everything visual that should be consistent across tools. **Both patterns must source their visual primitives from this module.** If you need a new shared visual component (a new icon, a new helper, a new themed control), add it here so every tool can pick it up.
### What lives there today
| Export | Purpose |
|--------|---------|
| `ICONS` | Dict of monochrome SVG icons. All use `stroke="currentColor"` / `fill="currentColor"` so they adapt to light and dark themes automatically. Keys include tabular-object icons (`table`, `calculation_group`, `column`, `column_chunk`, `measure`, `hierarchy`, `calculation_item`, `partition`, `relationship`), tree/navigation icons (`caret_right`, `folder`, `level`), and UI/action icons (`sun`, `moon`, `search`, `plus`, `play`, `stop`, `refresh`, `swap`, `sort_asc`, `sort_desc`, `panel_collapse`, `panel_expand`, `builder`, `close`, `fullscreen`, `fullscreen_exit`). |
| `LIGHT_THEME_VARS`, `DARK_THEME_VARS` | CSS custom-property blocks defining the Apple-inspired light and dark palettes. Always reference colors via these `--ui-*` tokens, never hard-coded hex values. Includes semantic tokens for hover backgrounds (`--ui-bg-hover`), on-accent text (`--ui-on-accent`), and destructive/error states (`--ui-danger*`). |
| `SYNTAX_HIGHLIGHT_VARS` | Theme-independent `--ui-syntax-*` token block for colorizing DAX/code in an editor. Inject once into the widget's base scope (it is the same in light and dark). |
| `HEADER_CSS`, `scoped_header_css(root_selector)` | Standard widget header styles (title + dataset/workspace subtitle) **and the four standard header controls** (`.sl-theme-btn`, `.sl-change-btn`, `.sl-reload-btn`). `scoped_header_css` prefixes every rule with the root selector so the styles win against notebook host CSS (e.g. Jupyter's `.jp-RenderedHTMLCommon button`). Every tool must inject this, even when it builds its own header markup. |
| `render_header_html(title, dataset_name, workspace_name, theme_btn_id, dark_mode, fullscreen_btn_id)` | Renders the standard header markup. Pass `fullscreen_btn_id` to include a full-screen toggle button next to the theme toggle. |
| `theme_toggle_script(btn_id, root_selector, dark_class)` | Returns a `<script>` block that wires the theme toggle button to flip a `dark_class` on the root element and swap the sun/moon icon. |
| `fullscreen_css(root_selector, fullscreen_class, container_selector=None, bg_var)` | Returns the CSS for a widget's full-screen state (covers both the native `:fullscreen` pseudo-class and the `fullscreen_class` CSS-overlay fallback). Pass `container_selector` when an inner element carries the card styling; pass `bg_var` to match the widget's background token. |
| `fullscreen_toggle_script(btn_id, root_selector, fullscreen_class)` | Returns a `<script>` block that wires a full-screen toggle button for **static-HTML** widgets. Pair with `render_header_html(..., fullscreen_btn_id=...)` and `fullscreen_css`. |
| `fullscreen_setup_js(func_name="sllsSetupFullscreen")` | Returns a JS function definition that wires a full-screen toggle button for **anywidget** widgets. Embed once at the top of the ESM module, then call `func_name(root, btn, fullscreenClass, enterSvg, exitSvg)` after creating the button. |
| `display_html_widget(html, fallback=True)` | Renders a self-contained HTML string (styles + markup + inline `<script>`) via a lightweight anywidget so it lives in the notebook **webview's light DOM** instead of the sandboxed `srcdoc` iframe used for raw `display(HTML)`. Use this for static-HTML widgets so the full-screen toggle gets the native Fullscreen API (matching the anywidget tools). Falls back to `display(HTML(html))` when `anywidget` is unavailable. |
| `ATTRIBUTION_CSS`, `scoped_attribution_css(root_selector)`, `render_attribution_html(extra_links=None)` | "Powered by Semantic Link Labs" attribution shown at the bottom of every widget, with an optional list of extra `(label, url)` links. |
| `SEARCH_SELECT_CSS`, `SEARCH_SELECT_JS` | The standard **searchable single-select** picker (`createSearchSelect({ placeholder, searchPlaceholder, ariaLabel, emptyLabel, onChange })`). Every workspace / semantic model / item picker must use this control — never a plain `<select>` — so long lists can always be filtered by typing. Inject the CSS into the widget stylesheet and the JS into the widget's ESM module, then drive the returned controller with `setOptions(items, value)`, `setEmptyLabel(text)` and `setDisabled(flag)`. Reference implementations: `semantic_model._find_unused_objects`, `semantic_model._bpa`. |
### When to extend `_ui_components`
Add a new export to `_ui_components.py` whenever the same visual element appears (or *should* appear) in more than one tool. Typical candidates: a new icon, a shared button style, a status-pill style, a confirmation-dialog component, a toast/notification helper. Do **not** copy-paste CSS or SVGs between widgets — promote them to `_ui_components` instead.
---
## Standard Header Controls (mandatory)
Four controls recur in nearly every tool. They must look and behave identically
everywhere, so both their icon and their chrome come from `_ui_components` — a
tool never defines its own size, radius, border or icon for them. The
`sempy_labs.semantic_model.test` (DAX Perf Optimizer) widget is the reference
implementation.
| Control | Class | Icon | Footprint |
|---------|-------|------|-----------|
| Light/dark mode | `sl-theme-btn` | `ICONS["sun"]` / `ICONS["moon"]` | 32×32 circle, 18px icon |
| Full screen | `sl-theme-btn` | `ICONS["fullscreen"]` / `ICONS["fullscreen_exit"]` | 32×32 circle, 18px icon |
| Change model / workspace | `sl-change-btn` | `ICONS["swap"]` | 32×32, 8px radius, 18px icon |
| Reload (workspaces, models, lists) | `sl-reload-btn` | `ICONS["refresh"]` | 32×32 circle, 14px icon |
Rules:
- Inject `scoped_header_css(root_selector)` into the widget stylesheet, and use
the class names above. Do **not** re-declare `.sl-theme-btn`,
`.sl-change-btn` or `.sl-reload-btn` in a tool — `tests/test_ui_header_controls.py`
fails if a tool restyles them.
- While a reload is in flight, add `sl-spinning` to the reload button; the
shared CSS animates the icon.
- If the tool uses `--slls-*` (or other) palette names, alias the `--ui-*`
tokens the shared CSS reads: `--ui-surface`, `--ui-surface-2`,
`--ui-border-strong`, `--ui-text`, `--ui-text-secondary`,
`--ui-text-tertiary`, `--ui-accent`, `--ui-accent-soft`.
- Tools rendering their header with `render_header_html` get these for free via
`theme_btn_id`, `fullscreen_btn_id` and `picker_btn_id`. For an extra button
that changes the model/workspace, pass `"base": "sl-change-btn"` in
`extra_buttons`.
- Buttons that are *not* one of these four (e.g. expand/collapse, delete,
tool-specific actions) keep their own tool-scoped classes.
### Workspace / semantic model pickers
Always resolve the workspace with `resolve_workspace_name_and_id(workspace)`
**even when the caller passed nothing**, and seed the picker with the result, so
the workspace dropdown opens pre-selected on the current workspace. Passing
`None` resolves to the attached lakehouse's workspace, or the notebook's.
---
## Design Tokens (the visual language)
All interactive UIs share one visual language. Stick to these tokens — do not introduce one-off colors, fonts, radii, or shadows.
### Typography
- Font stack: `-apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", Helvetica, Arial, sans-serif`.
- Enable font smoothing: `-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;`.
- Title (22px / 600 / `-0.01em` letter-spacing) and subtitle (≈12.5px / `--ui-text-secondary`) are produced by `render_header_html` — do not re-implement them.
- Use `font-variant-numeric: tabular-nums` for any numeric column or large numeric value.
### Color tokens (from `LIGHT_THEME_VARS` / `DARK_THEME_VARS`)
| Token | Use for |
|-------|---------|
| `--ui-bg`, `--ui-bg-secondary`, `--ui-bg-tertiary`, `--ui-bg-solid` | Surfaces, in increasing emphasis levels |
| `--ui-bg-hover` | Solid hover background for controls/buttons whose base is `--ui-bg` |
| `--ui-surface`, `--ui-surface-2` | Translucent overlays / hover backgrounds |
| `--ui-border`, `--ui-border-strong` | Subtle and strong borders |
| `--ui-text`, `--ui-text-secondary`, `--ui-text-tertiary` | Primary, secondary, tertiary text |
| `--ui-accent`, `--ui-accent-hover`, `--ui-accent-soft` | Brand accent (links, focus rings, active tab indicator, primary buttons, data bars) |
| `--ui-on-accent` | Text/icon color placed on top of an `--ui-accent` (or `--ui-danger`) fill — i.e. "white" |
| `--ui-danger`, `--ui-danger-hover` | Destructive button fill + hover (e.g. a Stop button) |
| `--ui-danger-bg`, `--ui-danger-border`, `--ui-danger-text` | Inline error/alert box background, border, and text (themed for light + dark) |
| `--ui-shadow-sm`, `--ui-shadow-md`, `--ui-shadow-lg` | Elevation |
Never hard-code `#fff`, red error colors, or any other literal — always go through a token. If a destructive action, error banner, or white-on-accent label is needed, use the danger / on-accent tokens above rather than inlining hex values.
### Code / DAX syntax-highlight palette (`SYNTAX_HIGHLIGHT_VARS`)
For colorizing DAX (or similar code) inside an editor/highlighter, inject the
theme-independent `SYNTAX_HIGHLIGHT_VARS` block once into the widget's **base**
scope (it intentionally renders the same in light and dark, so do not override
it in the dark block). Reference these `--ui-syntax-*` tokens — never the raw
hex values.
| Token | Token class it colors |
|-------|-----------------------|
| `--ui-syntax-keyword`, `--ui-syntax-function` | Keywords and function names |
| `--ui-syntax-variable` | Variables |
| `--ui-syntax-number` | Numeric literals |
| `--ui-syntax-virtual-column` | Virtual / measure-ref columns |
| `--ui-syntax-string` | String literals |
| `--ui-syntax-operator`, `--ui-syntax-punctuation` | Operators and punctuation |
See `sempy_labs.semantic_model._test_dax.test` for the reference usage (it
injects `SYNTAX_HIGHLIGHT_VARS` into its `.dtx` base block alongside
`LIGHT_THEME_VARS`).
If a tool needs its own derived names (e.g. `--vpx-text`), alias them to the `--ui-*` tokens inside the tool's scoped block — see `_vertipaq_analyzer.py` for the pattern. This keeps the palette centralized while letting tools use short, local names.
### Shape and motion
- Radii: `12px` (containers), `8px` (controls/cards) — exposed as `--vpx-radius` / `--vpx-radius-sm` style aliases when needed.
- Transitions: `0.25s cubic-bezier(0.4, 0, 0.2, 1)` for color/border/background; `120ms ease` for small button state changes; `80ms` for press scale.
GitHub에서 보기