dashui

DashUI — shared ipywidgets component library for the Dashlibs suite. Import from here in any dash-* package instead of duplicating widget code.

 1"""
 2DashUI — shared ipywidgets component library for the Dashlibs suite.
 3Import from here in any dash-* package instead of duplicating widget code.
 4"""
 5from dashui.components import (
 6    EditableTable,
 7    EnvSetupPanel,
 8    SourceSelector,
 9    action_button,
10    card,
11    editable_table,
12    env_setup_panel,
13    header,
14    html,
15    output_panel,
16    running_list,
17    section,
18    source_selector,
19    status_line,
20)
21from dashui.persistence import (
22    clear_config_dir,
23    config_path,
24    get_config_dir,
25    load_config,
26    save_config,
27    set_config_dir,
28)
29from dashui.schema import list_columns, list_columns_safe
30from dashui.theme import (
31    ACCENT_BG,
32    ACCENT_FG,
33    BORDER,
34    BORDER_STRONG,
35    CARD,
36    DANGER,
37    FONT_MONO,
38    FONT_SANS,
39    INFO,
40    MUTED,
41    PRIMARY,
42    SUCCESS,
43    WARNING,
44    accent,
45)
46
47__version__ = "0.3.5"
48__all__ = [
49    "SourceSelector",
50    "EditableTable",
51    "EnvSetupPanel",
52    "action_button",
53    "card",
54    "editable_table",
55    "env_setup_panel",
56    "header",
57    "html",
58    "output_panel",
59    "running_list",
60    "section",
61    "source_selector",
62    "status_line",
63    "list_columns",
64    "list_columns_safe",
65    "get_config_dir",
66    "set_config_dir",
67    "clear_config_dir",
68    "config_path",
69    "load_config",
70    "save_config",
71    "accent",
72    "PRIMARY",
73    "SUCCESS",
74    "DANGER",
75    "WARNING",
76    "INFO",
77    "BORDER",
78    "BORDER_STRONG",
79    "CARD",
80    "MUTED",
81    "ACCENT_BG",
82    "ACCENT_FG",
83    "FONT_SANS",
84    "FONT_MONO",
85]
@dataclass
class SourceSelector:
259@dataclass
260class SourceSelector:
261    """
262    The UC Table / DataFrame variable / SQL Query source picker.
263
264    Usage::
265        src = source_selector()
266        ui = card([src.toggle, src.box, ...])
267        kind, value = src.value()
268    """
269    toggle: object       # visible segmented-control container (place in card layout)
270    box: object          # visible input-area container (place in card layout)
271    table_input: object  # hidden Text — readable/settable for testing
272    df_input: object     # hidden Text
273    sql_input: object    # hidden Textarea
274    _mode_widget: object = field(default=None, repr=False)  # hidden Text tracking selected mode
275
276    def value(self) -> tuple[str, str]:
277        """Returns (kind, value) where kind is 'table' | 'dataframe' | 'sql'."""
278        mode = self._mode_widget.value if self._mode_widget else "UC Table"
279        if mode == "UC Table":
280            return "table", self.table_input.value.strip()
281        if mode == "DataFrame variable":
282            return "dataframe", self.df_input.value.strip()
283        return "sql", self.sql_input.value.strip()
284
285    def resolve_df(self):
286        """Resolve the selected source to a Spark DataFrame."""
287        kind, value = self.value()
288        if kind == "dataframe":
289            import IPython
290            shell = IPython.get_ipython()
291            df = shell.user_ns.get(value) if shell else None
292            if df is None:
293                raise ValueError(f"Variable '{value}' not found")
294            return df
295        from pyspark.sql import SparkSession
296        spark = SparkSession.getActiveSession()
297        if kind == "table":
298            return spark.table(value)
299        return spark.sql(value)

The UC Table / DataFrame variable / SQL Query source picker.

Usage:: src = source_selector() ui = card([src.toggle, src.box, ...]) kind, value = src.value()

SourceSelector( toggle: object, box: object, table_input: object, df_input: object, sql_input: object, _mode_widget: object = None)
toggle: object
box: object
table_input: object
df_input: object
sql_input: object
def value(self) -> tuple[str, str]:
276    def value(self) -> tuple[str, str]:
277        """Returns (kind, value) where kind is 'table' | 'dataframe' | 'sql'."""
278        mode = self._mode_widget.value if self._mode_widget else "UC Table"
279        if mode == "UC Table":
280            return "table", self.table_input.value.strip()
281        if mode == "DataFrame variable":
282            return "dataframe", self.df_input.value.strip()
283        return "sql", self.sql_input.value.strip()

Returns (kind, value) where kind is 'table' | 'dataframe' | 'sql'.

def resolve_df(self):
285    def resolve_df(self):
286        """Resolve the selected source to a Spark DataFrame."""
287        kind, value = self.value()
288        if kind == "dataframe":
289            import IPython
290            shell = IPython.get_ipython()
291            df = shell.user_ns.get(value) if shell else None
292            if df is None:
293                raise ValueError(f"Variable '{value}' not found")
294            return df
295        from pyspark.sql import SparkSession
296        spark = SparkSession.getActiveSession()
297        if kind == "table":
298            return spark.table(value)
299        return spark.sql(value)

Resolve the selected source to a Spark DataFrame.

@dataclass
class EditableTable:
431@dataclass
432class EditableTable:
433    """
434    Add/remove-row key-value grid.  Row data is batch-synced to a hidden
435    Textarea as JSON; values() parses it — no per-cell widget needed.
436
437    Usage::
438        tbl = editable_table(["Key", "Value"], placeholders={"Key": "AWS_REGION"})
439        ui = card([tbl.widget, ...])
440        rows = tbl.values()   # [{"Key": "AWS_REGION", "Value": "us-east-1"}, ...]
441    """
442    widget: object
443    add_row: object   # Python-side no-op; rows are added by the JS "+ Add row" button
444    values: object    # callable() -> list[dict[str, str]]

Add/remove-row key-value grid. Row data is batch-synced to a hidden Textarea as JSON; values() parses it — no per-cell widget needed.

Usage:: tbl = editable_table(["Key", "Value"], placeholders={"Key": "AWS_REGION"}) ui = card([tbl.widget, ...]) rows = tbl.values() # [{"Key": "AWS_REGION", "Value": "us-east-1"}, ...]

EditableTable(widget: object, add_row: object, values: object)
widget: object
add_row: object
values: object
@dataclass
class EnvSetupPanel:
637@dataclass
638class EnvSetupPanel:
639    widget: object
640    values: object  # callable() -> dict
EnvSetupPanel(widget: object, values: object)
widget: object
values: object
def action_button(text: str, style: str = 'primary', emoji: str = '') -> object:
220def action_button(text: str, style: str = "primary", emoji: str = "") -> object:
221    """
222    Custom HTML button proxying to a hidden ipywidgets.Button for Python callbacks.
223
224    Returns an HBox widget with a forwarded .on_click attribute so call-sites
225    that do ``btn.on_click(handler)`` keep working unchanged.  The container
226    can be placed in any HBox/VBox layout.
227    """
228    w = _require_widgets()
229    label = f"{emoji} {text}".strip() if emoji else text
230    bg, fg, hover, border = _BUTTON_VARIANTS.get(style or "", _BUTTON_VARIANTS[""])
231
232    # Hidden ipywidgets.Button — carries the Comm channel to Python
233    hidden_btn = w.Button(description="_")
234    hidden_btn.add_class("dashui-hw")
235    uid = _uid(hidden_btn)
236    hidden_btn.add_class(f"dh{uid}")
237
238    # Visible custom HTML button — onclick finds the hidden DOM button and dispatches on it
239    visible = w.HTML(
240        f'<button'
241        f' style="background:{bg};color:{fg};border:1px solid {border};'
242        f'border-radius:{RADIUS_MD};font-weight:500;font-size:{FONT_SIZE_BASE};'
243        f'font-family:{FONT_SANS};height:32px;padding:0 14px;cursor:pointer;'
244        f'display:inline-flex;align-items:center;'
245        f'transition:background .12s,border-color .12s;white-space:nowrap;line-height:1;"'
246        f' onmouseenter="this.style.background=\'{hover}\';this.style.borderColor=\'{hover}\'"'
247        f' onmouseleave="this.style.background=\'{bg}\';this.style.borderColor=\'{border}\';this.style.opacity=\'1\'"'
248        f' onmousedown="this.style.opacity=\'.82\'"'
249        f' onmouseup="this.style.opacity=\'1\'"'
250        f' onclick="(function(){{var h=document.querySelector(\'.dh{uid} button\');if(h)h.click();}})();">'
251        f'{label}</button>'
252    )
253
254    container = w.HBox([visible, hidden_btn])
255    container.on_click = hidden_btn.on_click  # forward so btn.on_click(handler) still works
256    return container

Custom HTML button proxying to a hidden ipywidgets.Button for Python callbacks.

Returns an HBox widget with a forwarded .on_click attribute so call-sites that do btn.on_click(handler) keep working unchanged. The container can be placed in any HBox/VBox layout.

def card(children, padding: str = '16px'):
206def card(children, padding: str = "16px"):
207    """Bordered, shadowed VBox — the outer shell for every launch() UI."""
208    w = _require_widgets()
209    global _STYLE_INJECTED
210    body = [_global_style(), *children] if not _STYLE_INJECTED else list(children)
211    _STYLE_INJECTED = True
212    box = w.VBox(body, layout=w.Layout(padding=padding))
213    box.add_class("dashui-card")
214    box.add_class("dashui-root")
215    return box

Bordered, shadowed VBox — the outer shell for every launch() UI.

def editable_table( columns: list[str], placeholders: dict[str, str] | None = None, initial_rows: int = 1) -> EditableTable:
447def editable_table(
448    columns: list[str],
449    placeholders: dict[str, str] | None = None,
450    initial_rows: int = 1,
451) -> EditableTable:
452    w = _require_widgets()
453    placeholders = placeholders or {}
454
455    # Hidden Textarea — JS serialises all rows to JSON here on every change
456    hidden_ta = w.Textarea(value="[]")
457    hidden_ta.add_class("dashui-hw")
458    uid = _uid(hidden_ta)
459    hidden_ta.add_class(f"dh{uid}")
460
461    # Header cells
462    th_s = (
463        f"text-align:left;font-size:11px;font-weight:600;text-transform:uppercase;"
464        f"letter-spacing:.03em;color:{MUTED_FOREGROUND};background:{MUTED};"
465        f"padding:6px 8px;border-bottom:1px solid {BORDER};"
466    )
467    headers = "".join(f"<th style='{th_s}'>{c}</th>" for c in columns)
468    headers += f"<th style='{th_s}width:32px;'></th>"
469
470    cols_j = _json.dumps(columns)
471    phs_j  = _json.dumps(placeholders)
472
473    table_html = w.HTML(
474        f'<div style="border:1px solid {BORDER};border-radius:{RADIUS_MD};overflow:hidden;margin-bottom:6px">'
475        f'<table style="width:100%;border-collapse:collapse" id="du-et-{uid}">'
476        f'<thead><tr>{headers}</tr></thead>'
477        f'<tbody id="du-et-body-{uid}"></tbody>'
478        f'</table>'
479        f'</div>'
480        f'<button style="border:1px solid {BORDER_STRONG};border-radius:{RADIUS_SM};'
481        f'font-size:{FONT_SIZE_BASE};font-family:{FONT_SANS};background:{CARD};'
482        f'color:#1B3139;padding:4px 12px;height:28px;cursor:pointer;"'
483        f' onclick="window[\'duEtAdd_{uid}\']&&window[\'duEtAdd_{uid}\']()">+ Add row</button>'
484        f'<script>'
485        f'(function(){{'
486        f'  var uid=\'{uid}\',COLS={cols_j},PHS={phs_j};'
487        f'  function sync(){{'
488        f'    var rows=[];'
489        f'    document.querySelectorAll("#du-et-body-"+uid+" tr").forEach(function(tr){{'
490        f'      var row={{}};'
491        f'      tr.querySelectorAll("input[data-col]").forEach(function(inp){{row[inp.dataset.col]=inp.value;}});'
492        f'      rows.push(row);'
493        f'    }});'
494        f'    var ta=document.querySelector(".dh"+uid+" textarea");'
495        f'    if(ta){{ta.value=JSON.stringify(rows);ta.dispatchEvent(new Event("input",{{bubbles:true}}))}}'
496        f'  }}'
497        f'  function addRow(){{'
498        f'    var tbody=document.getElementById("du-et-body-"+uid);'
499        f'    if(!tbody)return;'
500        f'    var tr=document.createElement("tr");'
501        f'    tr.style.borderBottom="1px solid {BORDER}";'
502        f'    COLS.forEach(function(col){{'
503        f'      var td=document.createElement("td");td.style.padding="3px 4px";'
504        f'      var inp=document.createElement("input");'
505        f'      inp.type="text";inp.placeholder=PHS[col]||"";inp.dataset.col=col;'
506        f'      inp.style.border="1px solid {BORDER_STRONG}";'
507        f'      inp.style.borderRadius="{RADIUS_SM}";'
508        f'      inp.style.fontSize="{FONT_SIZE_BASE}";'
509        f'      inp.style.fontFamily="Inter,-apple-system,sans-serif";'
510        f'      inp.style.padding="3px 6px";'
511        f'      inp.style.background="{CARD}";'
512        f'      inp.style.color="#1B3139";'
513        f'      inp.style.width="100%";'
514        f'      inp.style.boxSizing="border-box";'
515        f'      inp.addEventListener("input",sync);'
516        f'      td.appendChild(inp);tr.appendChild(td);'
517        f'    }});'
518        f'    var tdR=document.createElement("td");tdR.style.padding="3px 4px";tdR.style.width="32px";'
519        f'    var rmv=document.createElement("button");'
520        f'    rmv.textContent="✕";'
521        f'    rmv.style.border="1px solid {BORDER_STRONG}";'
522        f'    rmv.style.borderRadius="{RADIUS_SM}";'
523        f'    rmv.style.fontSize="11px";'
524        f'    rmv.style.background="{CARD}";'
525        f'    rmv.style.color="#5A6872";'
526        f'    rmv.style.cursor="pointer";'
527        f'    rmv.style.width="28px";rmv.style.height="26px";'
528        f'    rmv.style.padding="0";rmv.style.lineHeight="1";'
529        f'    rmv.addEventListener("click",function(){{tr.remove();sync();}});'
530        f'    tdR.appendChild(rmv);tr.appendChild(tdR);tbody.appendChild(tr);sync();'
531        f'  }}'
532        f'  window["duEtAdd_{uid}"]=addRow;'
533        f'  for(var i=0;i<{initial_rows};i++)addRow();'
534        f'}})();'
535        f'</script>'
536    )
537
538    container = w.VBox([table_html, hidden_ta])
539
540    def values() -> list[dict]:
541        try:
542            data = _json.loads(hidden_ta.value or "[]")
543            return [
544                row for row in data
545                if any(str(v).strip() for v in row.values())
546            ]
547        except Exception:
548            return []
549
550    return EditableTable(widget=container, add_row=lambda _b=None: None, values=values)
def env_setup_panel(library: str, extra_fields: dict | None = None):
555def env_setup_panel(library: str, extra_fields: dict | None = None):
556    """
557    Ready-to-embed "Environment Setup" panel.
558
559    `extra_fields` is {label: placeholder} for library-specific settings
560    saved alongside the config directory.  All settings are returned by
561    the `values()` callable on the returned EnvSetupPanel.
562    """
563    from dashui.persistence import get_config_dir, load_config, save_config, set_config_dir
564
565    w = _require_widgets()
566    extra_fields = extra_fields or {}
567    saved = load_config(library, name="env")
568
569    dir_input = w.Text(
570        description="Config directory:",
571        value=saved.get("config_dir", get_config_dir(library)),
572        placeholder=get_config_dir(library),
573        layout=w.Layout(width="420px"),
574    )
575    extra_inputs = {
576        label: w.Text(
577            description=f"{label}:", value=saved.get(label, ""), placeholder=placeholder,
578        )
579        for label, placeholder in extra_fields.items()
580    }
581
582    save_btn   = action_button("Save",   style="primary")
583    reload_btn = action_button("Reload", style="info")
584    status = html(
585        f"<span style='font-size:12px;color:{MUTED_FOREGROUND}'>Currently using: "
586        f"<code>{get_config_dir(library)}</code></span>"
587    )
588
589    def _collect() -> dict:
590        return {
591            "config_dir": dir_input.value.strip() or get_config_dir(library),
592            **{label: field.value for label, field in extra_inputs.items()},
593        }
594
595    def _on_save(_b):
596        config = _collect()
597        set_config_dir(library, config["config_dir"])
598        path = save_config(library, config, name="env")
599        status.value = (
600            f"<span style='font-size:12px;color:{SUCCESS}'>Saved — settings will be read from "
601            f"<code>{path}</code> in future sessions.</span>"
602        )
603
604    def _on_reload(_b):
605        current = load_config(library, name="env")
606        dir_input.value = current.get("config_dir", get_config_dir(library))
607        for label, inp in extra_inputs.items():
608            inp.value = current.get(label, "")
609        status.value = (
610            f"<span style='font-size:12px;color:{MUTED_FOREGROUND}'>Reloaded from "
611            f"<code>{config_path_display(library)}</code>.</span>"
612        )
613
614    save_btn.on_click(_on_save)
615    reload_btn.on_click(_on_reload)
616
617    panel = w.VBox([
618        html(
619            f"<div style='font-size:12px;color:{MUTED_FOREGROUND};margin-bottom:4px'>"
620            "Where should this package's configs be read/written? Leave as-is to use "
621            "the notebook's current working directory — nothing here is required."
622            "</div>"
623        ),
624        dir_input,
625        *extra_inputs.values(),
626        w.HBox([save_btn, reload_btn]),
627        status,
628    ])
629    return EnvSetupPanel(widget=panel, values=_collect)

Ready-to-embed "Environment Setup" panel.

extra_fields is {label: placeholder} for library-specific settings saved alongside the config directory. All settings are returned by the values() callable on the returned EnvSetupPanel.

def html(text: str):
69def html(text: str):
70    w = _require_widgets()
71    return w.HTML(text)
def output_panel():
400def output_panel():
401    """Standard scrollable output area for run/profile results."""
402    w = _require_widgets()
403    out = w.Output(layout=w.Layout(padding="12px"))
404    out.add_class("dashui-output")
405    return out

Standard scrollable output area for run/profile results.

def running_list(formatter):
408def running_list(formatter):
409    """
410    Live-updating numbered list — pattern for 'added entities' accumulators.
411
412    Usage::
413        out, render = running_list(lambda i, item: f"{i}. {item['name']}")
414        render(items)  # call again to refresh
415    """
416    w = _require_widgets()
417    out = w.Output(layout=w.Layout(padding="8px 12px"))
418    out.add_class("dashui-output")
419
420    def render(items: list):
421        with out:
422            out.clear_output()
423            for i, item in enumerate(items, 1):
424                print(formatter(i, item))
425
426    return out, render

Live-updating numbered list — pattern for 'added entities' accumulators.

Usage:: out, render = running_list(lambda i, item: f"{i}. {item['name']}") render(items) # call again to refresh

def section(title: str):
187def section(title: str):
188    """Step/section divider."""
189    return html(f"<div class='dashui-section'>{title}</div>")

Step/section divider.

def source_selector(label: str = 'Source:') -> SourceSelector:
302def source_selector(label: str = "Source:") -> SourceSelector:
303    w = _require_widgets()
304
305    # Hidden data-channel widgets — JS syncs user input into these
306    hidden_mode  = w.Text(value="UC Table")
307    hidden_table = w.Text(placeholder="catalog.schema.table")
308    hidden_df    = w.Text(placeholder="df")
309    hidden_sql   = w.Textarea(placeholder="SELECT * FROM ...")
310
311    mid = _uid(hidden_mode)
312    tid = _uid(hidden_table)
313    did = _uid(hidden_df)
314    sid = _uid(hidden_sql)
315
316    for hw, uid in [(hidden_mode, mid), (hidden_table, tid),
317                    (hidden_df, did), (hidden_sql, sid)]:
318        hw.add_class("dashui-hw")
319        hw.add_class(f"dh{uid}")
320
321    # ── Segmented control ──────────────────────────────────────────────────
322    base_s = (
323        f"border:1px solid {BORDER_STRONG};background:{CARD};color:#1B3139;"
324        f"font-size:{FONT_SIZE_BASE};font-family:{FONT_SANS};"
325        f"padding:5px 12px;cursor:pointer;transition:background .12s;"
326    )
327    act_s = f"background:{ACCENT_BG};border-color:{PRIMARY};color:{ACCENT_FG};font-weight:600;"
328
329    toggle_html = w.HTML(
330        f'<div style="margin-bottom:4px">'
331        f'<button id="du-uc-{mid}" style="{base_s}border-radius:{RADIUS_SM} 0 0 {RADIUS_SM};{act_s}"'
332        f' onclick="duSrcSel(\'{mid}\',\'{tid}\',\'{did}\',\'{sid}\',\'UC Table\',this)">'
333        f'UC Table</button>'
334        f'<button id="du-df-{mid}" style="{base_s}border-radius:0;border-left:none;"'
335        f' onclick="duSrcSel(\'{mid}\',\'{tid}\',\'{did}\',\'{sid}\',\'DataFrame variable\',this)">'
336        f'DataFrame variable</button>'
337        f'<button id="du-sql-{mid}" style="{base_s}border-radius:0 {RADIUS_SM} {RADIUS_SM} 0;border-left:none;"'
338        f' onclick="duSrcSel(\'{mid}\',\'{tid}\',\'{did}\',\'{sid}\',\'SQL Query\',this)">'
339        f'SQL Query</button>'
340        f'</div>'
341        f'<script>'
342        # duSrcSel and duSrcSync are shared helpers — defined once via the || idiom
343        f'window.duSrcSel=window.duSrcSel||function(mid,tid,did,sid,mode,btn){{'
344        f'  [["uc","{CARD}","{BORDER_STRONG}","#1B3139","500"],'
345        f'   ["df","{CARD}","{BORDER_STRONG}","#1B3139","500"],'
346        f'   ["sql","{CARD}","{BORDER_STRONG}","#1B3139","500"]'
347        f'  ].forEach(function(x){{var b=document.getElementById("du-"+x[0]+"-"+mid);'
348        f'    if(b){{b.style.background=x[1];b.style.borderColor=x[2];'
349        f'           b.style.color=x[3];b.style.fontWeight=x[4];}}}});'
350        f'  btn.style.background="{ACCENT_BG}";btn.style.borderColor="{PRIMARY}";'
351        f'  btn.style.color="{ACCENT_FG}";btn.style.fontWeight="600";'
352        f'  document.getElementById("du-uc-panel-"+mid).style.display=mode==="UC Table"?"":"none";'
353        f'  document.getElementById("du-df-panel-"+mid).style.display=mode==="DataFrame variable"?"":"none";'
354        f'  document.getElementById("du-sql-panel-"+mid).style.display=mode==="SQL Query"?"":"none";'
355        f'  var m=document.querySelector(".dh"+mid+" input");'
356        f'  if(m){{m.value=mode;m.dispatchEvent(new Event("input",{{bubbles:true}}))}}'
357        f'}};'
358        f'window.duSrcSync=window.duSrcSync||function(cls,tag,val){{'
359        f'  var el=document.querySelector(cls+" "+tag);'
360        f'  if(el){{el.value=val;el.dispatchEvent(new Event("input",{{bubbles:true}}))}}'
361        f'}};'
362        f'</script>'
363    )
364
365    # ── Input panels ───────────────────────────────────────────────────────
366    inp_s = (
367        f"width:100%;border:1px solid {BORDER_STRONG};border-radius:{RADIUS_SM};"
368        f"font-size:{FONT_SIZE_BASE};font-family:{FONT_SANS};padding:4px 8px;"
369        f"box-sizing:border-box;background:{CARD};color:#1B3139;"
370    )
371
372    input_html = w.HTML(
373        f'<div id="du-uc-panel-{mid}">'
374        f'<input type="text" placeholder="catalog.schema.table" style="{inp_s}"'
375        f' oninput="window.duSrcSync&&window.duSrcSync(\'.dh{tid}\',\'input\',this.value)">'
376        f'</div>'
377        f'<div id="du-df-panel-{mid}" style="display:none">'
378        f'<input type="text" placeholder="df" style="{inp_s}"'
379        f' oninput="window.duSrcSync&&window.duSrcSync(\'.dh{did}\',\'input\',this.value)">'
380        f'</div>'
381        f'<div id="du-sql-panel-{mid}" style="display:none">'
382        f'<textarea placeholder="SELECT * FROM ..." rows="3" style="{inp_s}resize:vertical;"'
383        f' oninput="window.duSrcSync&&window.duSrcSync(\'.dh{sid}\',\'textarea\',this.value)">'
384        f'</textarea>'
385        f'</div>'
386    )
387
388    toggle = w.VBox([toggle_html, hidden_mode])
389    box    = w.VBox([input_html, hidden_table, hidden_df, hidden_sql])
390
391    return SourceSelector(
392        toggle=toggle, box=box,
393        table_input=hidden_table, df_input=hidden_df, sql_input=hidden_sql,
394        _mode_widget=hidden_mode,
395    )
def status_line(text: str, kind: str = 'info'):
192def status_line(text: str, kind: str = "info"):
193    """One-line status. kind in success|error|warning|info — color carried by a small dot."""
194    color = {
195        "success": SUCCESS, "error": DANGER, "warning": WARNING, "info": MUTED_FOREGROUND,
196    }.get(kind, MUTED_FOREGROUND)
197    return html(
198        f"<span style='font-family:{FONT_SANS};color:#1B3139'>"
199        f"<span style='display:inline-block;width:6px;height:6px;border-radius:50%;"
200        f"background:{color};margin-right:7px'></span>{text}</span>"
201    )

One-line status. kind in success|error|warning|info — color carried by a small dot.

def list_columns(table: str) -> list[str]:
 6def list_columns(table: str) -> list[str]:
 7    """Return column names for a UC table, without loading any data."""
 8    from pyspark.sql import SparkSession
 9    spark = SparkSession.getActiveSession()
10    return [f.name for f in spark.table(table).schema.fields]

Return column names for a UC table, without loading any data.

def list_columns_safe(table: str) -> list[str]:
13def list_columns_safe(table: str) -> list[str]:
14    """Like list_columns, but returns [] instead of raising — for UI dropdowns."""
15    try:
16        return list_columns(table)
17    except Exception:
18        return []

Like list_columns, but returns [] instead of raising — for UI dropdowns.

def get_config_dir(library: str) -> str:
29def get_config_dir(library: str) -> str:
30    """The directory configs for `library` should be read/written from:
31    the env_setup()-configured directory if one was set, else cwd."""
32    pointer = _pointer_path(library)
33    if pointer.exists():
34        try:
35            configured = json.loads(pointer.read_text()).get("config_dir")
36            if configured:
37                return configured
38        except Exception:
39            pass
40    return os.environ.get("DASHLIBS_CONFIG_DIR", os.getcwd())

The directory configs for library should be read/written from: the env_setup()-configured directory if one was set, else cwd.

def set_config_dir(library: str, path: str) -> None:
43def set_config_dir(library: str, path: str) -> None:
44    """Remember `path` as this library's config directory for future sessions."""
45    pointer = _pointer_path(library)
46    pointer.parent.mkdir(parents=True, exist_ok=True)
47    pointer.write_text(json.dumps({"config_dir": path}, indent=2))

Remember path as this library's config directory for future sessions.

def clear_config_dir(library: str) -> None:
50def clear_config_dir(library: str) -> None:
51    """Forget the configured directory — future calls fall back to cwd again."""
52    pointer = _pointer_path(library)
53    if pointer.exists():
54        pointer.unlink()

Forget the configured directory — future calls fall back to cwd again.

def config_path(library: str, name: str = 'config') -> str:
57def config_path(library: str, name: str = "config") -> str:
58    return os.path.join(get_config_dir(library), f"{library}_{name}.json")
def load_config(library: str, name: str = 'config', defaults: dict | None = None) -> dict:
61def load_config(library: str, name: str = "config", defaults: dict | None = None) -> dict:
62    """Read `<config_dir>/<library>_<name>.json`, merged over `defaults`.
63    Returns `defaults` (or {}) unchanged if the file doesn't exist or is invalid."""
64    path = config_path(library, name)
65    if os.path.exists(path):
66        try:
67            with open(path) as f:
68                return {**(defaults or {}), **json.load(f)}
69        except Exception:
70            pass
71    return dict(defaults or {})

Read <config_dir>/<library>_<name>.json, merged over defaults. Returns defaults (or {}) unchanged if the file doesn't exist or is invalid.

def save_config(library: str, config: dict, name: str = 'config') -> str:
74def save_config(library: str, config: dict, name: str = "config") -> str:
75    """Write `config` to `<config_dir>/<library>_<name>.json`. Returns the path written."""
76    path = config_path(library, name)
77    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
78    with open(path, "w") as f:
79        json.dump(config, f, indent=2)
80    return path

Write config to <config_dir>/<library>_<name>.json. Returns the path written.

def accent(library: str) -> str:
72def accent(library: str) -> str:
73    """Look up the accent color for a Dashlibs package name (e.g. 'dashsynthetic')."""
74    return ACCENTS.get(library, ACCENTS["default"])

Look up the accent color for a Dashlibs package name (e.g. 'dashsynthetic').

PRIMARY = '#FF3621'
SUCCESS = '#2E7D32'
DANGER = '#C62828'
WARNING = '#B36B00'
INFO = '#0E6BA8'
BORDER = '#DCE0E2'
BORDER_STRONG = '#C7CCD1'
CARD = '#FFFFFF'
MUTED = '#F3F4F5'
ACCENT_BG = '#FFF1EC'
ACCENT_FG = '#B33B1E'
FONT_SANS = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
FONT_MONO = "'Roboto Mono', 'SFMono-Regular', Consolas, monospace"