Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
276 changes: 276 additions & 0 deletions mitosheet/css/mito.css
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,282 @@
width: 100%;
display: flex;
flex-direction: column;
position: relative;
}

.mito-selection-card {
position: absolute;
top: 0;
left: 0;
min-width: 220px;
max-width: 380px;
max-height: 55%;
overflow: auto;
z-index: 5;
background: linear-gradient(
180deg,
var(--mito-background-highlight, #faf8ff) 0%,
var(--mito-background, white) 48px
);
border: 1px solid var(--mito-highlight-light, #d4c4ff);
border-radius: 10px;
box-shadow: 0 6px 20px rgba(80, 40, 140, 0.14), 0 1px 3px rgba(0, 0, 0, 0.06);
padding: 14px 16px;
}

.mito-selection-card-loading {
font-size: 13px;
color: var(--mito-text-secondary, #888);
}

.mito-selection-card-explore {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--mito-border, #e0e0e0);
}

.mito-selection-card-explore-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--mito-text-secondary, #888);
margin: 0 0 6px 0;
}

.mito-selection-card-explore-links {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
}

.mito-selection-card-explore-link {
background: none;
border: none;
padding: 0;
text-align: left;
font-size: 13px;
line-height: 1.35;
color: var(--mito-highlight, #9d6cff);
cursor: pointer;
text-decoration: underline;
}

.mito-selection-card-explore-link:hover:not(:disabled) {
color: var(--mito-text, #333);
}

.mito-selection-card-explore-link:disabled {
opacity: 0.6;
cursor: wait;
}

.mito-selection-card-blocks {
display: flex;
flex-direction: column;
gap: 10px;
}

.mito-card-header {
font-size: 17px;
font-weight: 700;
line-height: 1.25;
letter-spacing: -0.02em;
color: var(--mito-text, #1a1a1a);
word-break: break-word;
margin-bottom: -2px;
}

.mito-card-header + .mito-card-caption {
margin-top: -4px;
}

.mito-card-text {
font-size: 13px;
line-height: 1.4;
color: var(--mito-text, #333);
white-space: pre-wrap;
word-break: break-word;
}

.mito-card-divider {
border: none;
border-top: 1px solid var(--mito-border, #e8e4f0);
margin: 4px 0;
}

.mito-card-metrics-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}

.mito-card-metric {
flex: 1 1 0;
min-width: 88px;
background: var(--mito-background, #fff);
border: 1px solid var(--mito-border, #ebe8f2);
border-radius: 8px;
padding: 10px 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}

.mito-card-metric-label {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--mito-text-secondary, #6b6b6b);
margin-bottom: 4px;
}

.mito-card-metric-value {
font-size: 20px;
font-weight: 700;
line-height: 1.15;
color: var(--mito-text, #111);
word-break: break-word;
font-variant-numeric: tabular-nums;
}

.mito-card-metric-delta {
font-size: 12px;
font-weight: 500;
color: var(--mito-text-secondary, #6b6b6b);
margin-top: 4px;
font-variant-numeric: tabular-nums;
}

.mito-card-metric-delta-up {
color: #15803d;
}

.mito-card-metric-delta-down {
color: #b91c1c;
}

.mito-card-metric-delta-neutral {
color: var(--mito-text-secondary, #6b6b6b);
}

.mito-card-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 13px;
background: var(--mito-background, #fff);
border: 1px solid var(--mito-border, #ebe8f2);
border-radius: 8px;
overflow: hidden;
}

.mito-card-table tr:not(:last-child) td {
border-bottom: 1px solid var(--mito-border, #f0eef5);
}

.mito-card-table-label {
padding: 8px 10px;
color: var(--mito-text-secondary, #6b6b6b);
vertical-align: top;
white-space: nowrap;
font-weight: 500;
width: 42%;
}

.mito-card-table-value {
padding: 8px 10px;
color: var(--mito-text, #222);
text-align: right;
word-break: break-word;
font-variant-numeric: tabular-nums;
}

.mito-card-caption {
font-size: 12px;
color: var(--mito-text-secondary, #6b6b6b);
line-height: 1.35;
}

.mito-card-code {
font-size: 11px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
background: var(--mito-background-secondary, #f6f6f6);
border-radius: 4px;
padding: 8px;
margin: 0;
white-space: pre-wrap;
word-break: break-word;
overflow-x: auto;
}

.mito-card-alert {
font-size: 13px;
line-height: 1.45;
padding: 10px 12px;
border-radius: 8px;
word-break: break-word;
border: 1px solid transparent;
}

.mito-card-alert-info {
background: #eff6ff;
color: #1e3a5f;
border-color: #bfdbfe;
}

.mito-card-alert-success {
background: #ecfdf3;
color: #14532d;
border-color: #bbf7d0;
}

.mito-card-alert-warning {
background: #fffbeb;
color: #713f12;
border-color: #fde68a;
}

.mito-card-alert-error {
background: #fef2f2;
color: #7f1d1d;
border-color: #fecaca;
}

.mito-card-dataframe {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 12px;
border: 1px solid var(--mito-border, #ebe8f2);
border-radius: 8px;
overflow: hidden;
}

.mito-card-dataframe th {
text-align: left;
font-weight: 600;
padding: 6px 8px;
border-bottom: 1px solid var(--mito-border, #ddd);
color: var(--mito-text-secondary, #555);
background: var(--mito-background-highlight, #f8f7fb);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.03em;
}

.mito-card-dataframe td {
padding: 6px 8px;
border-bottom: 1px solid var(--mito-border, #f0eef5);
color: var(--mito-text, #222);
word-break: break-word;
font-variant-numeric: tabular-nums;
}

.mito-card-dataframe tr:last-child td {
border-bottom: none;
}

.mito-sheet-and-formula-bar-container-wide-taskpane-open {
Expand Down
6 changes: 6 additions & 0 deletions mitosheet/mitosheet/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from mitosheet.api.get_ai_completion import get_ai_completion
from mitosheet.api.get_chart_suggestions import get_chart_suggestions
from mitosheet.api.get_column_suggestions import get_column_suggestions
from mitosheet.api.get_card_template import get_card_template
from mitosheet.api.get_card_content import get_card_content
from mitosheet.api.get_data_alerts import get_data_alerts
from mitosheet.api.get_available_snowflake_options_and_defaults import \
get_available_snowflake_options_and_defaults
Expand Down Expand Up @@ -240,6 +242,10 @@ def handle_api_event(
result = get_chart_suggestions(params, steps_manager)
elif event["type"] == "get_column_suggestions":
result = get_column_suggestions(params, steps_manager)
elif event["type"] == "get_card_template":
result = get_card_template(params, steps_manager)
elif event["type"] == "get_card_content":
result = get_card_content(params, steps_manager)
elif event["type"] == "get_data_alerts":
result = get_data_alerts(params, steps_manager)
elif event["type"] == "get_parameterizable_params":
Expand Down
83 changes: 83 additions & 0 deletions mitosheet/mitosheet/api/card_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env python
# coding: utf-8

# Copyright (c) Saga Inc.
# Distributed under the terms of the GNU Affero General Public License v3.0 License.

"""
Parse and serialize column card definitions (Streamlit code + explore links).
"""

from __future__ import annotations

import json
import re
from typing import Any, Dict, List, Tuple

import pandas as pd

_PLACEHOLDER_RE = re.compile(r"\{([^}]+)\}")


def parse_card_definition(stored: str) -> Tuple[str, List[Dict[str, Any]]]:
stripped = stored.strip()
if not stripped:
return "", []
try:
parsed = json.loads(stripped)
if isinstance(parsed, dict) and isinstance(parsed.get("code"), str):
explore = parsed.get("explore", [])
if not isinstance(explore, list):
explore = []
return parsed["code"], explore
except json.JSONDecodeError:
pass
return stripped, []


def serialize_card_definition(code: str, explore: List[Dict[str, Any]]) -> str:
if len(explore) == 0:
return code
return json.dumps({"code": code, "explore": explore})


def _format_cell_value(value: Any) -> str:
if value is None or (isinstance(value, float) and pd.isna(value)):
return ""
if isinstance(value, (int, float)) and not isinstance(value, bool):
f = float(value)
if f == 0:
return "0"
af = abs(f)
if af >= 1_000_000:
return f"{f:,.0f}"
if af >= 10_000:
return f"{f:,.2f}".rstrip("0").rstrip(".")
if af >= 1:
rounded = round(f, 2)
return str(int(rounded)) if rounded == int(rounded) else str(rounded)
return str(round(f, 2))
return str(value)


def render_explore_labels(
explore: List[Dict[str, Any]], row: pd.Series
) -> List[Dict[str, str]]:
"""Fill {Column Name} placeholders in explore link labels from the selected row."""
out: List[Dict[str, str]] = []
for item in explore:
if not isinstance(item, dict):
continue
label = item.get("label")
view_code = item.get("view_code")
if not isinstance(label, str) or not isinstance(view_code, str):
continue
rendered_label = label
for match in _PLACEHOLDER_RE.finditer(label):
col_name = match.group(1)
if col_name in row.index:
rendered_label = rendered_label.replace(
match.group(0), _format_cell_value(row[col_name])
)
out.append({"label": rendered_label, "view_code": view_code})
return out
Loading
Loading