Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4680e95
Add HighlightTarget and enrich Highlight model
rich-iannone May 26, 2026
eafd9aa
Refactor highlight serialization
rich-iannone May 26, 2026
733a88a
Add timed highlights layer for SVG transcripts
rich-iannone May 26, 2026
9f7e24b
Add highlights overlay styles to termshow.css
rich-iannone May 26, 2026
0fba6e1
Use HighlightTarget in test highlights
rich-iannone May 26, 2026
0b1db3c
Add tests for terminal highlight system
rich-iannone May 26, 2026
759d8c3
Add highlight style demos and gallery
rich-iannone May 26, 2026
4004dc1
Add highlights track to term player editor
rich-iannone May 26, 2026
6e2e26f
Add highlights overlay, grid/nums & wide-char support
rich-iannone May 27, 2026
d7c1a63
Add fill color support for highlight boxes
rich-iannone May 27, 2026
f2bd9c1
Separate highlight underlay; add styles & glow
rich-iannone May 27, 2026
397df39
Add nudge support for highlight edges
rich-iannone May 27, 2026
9a83e76
Add highlight animation controls and clamp numeric inputs
rich-iannone May 27, 2026
fa6e07b
Set crosshair cursor for #track-annotations
rich-iannone May 27, 2026
bd72de9
Add in-editor player preview (Quarto render)
rich-iannone May 27, 2026
6a3ec52
Add multi-device preview tabs & CSS overrides
rich-iannone May 27, 2026
e19f4f3
Add highlight UI, keyboard hints, and Ctrl+S
rich-iannone May 27, 2026
bdb55e6
Make default durations 2s; keep panel open
rich-iannone May 27, 2026
b76c31c
Document highlights feature and UI changes
rich-iannone May 27, 2026
028af0e
Update overlay UI icons and inspector behavior
rich-iannone May 27, 2026
94ef112
Update termshow-editor-diagram.png
rich-iannone May 27, 2026
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
Binary file modified assets/termshow-editor-diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,900 changes: 1,852 additions & 48 deletions great_docs/_term_player/editor.py

Large diffs are not rendered by default.

49 changes: 37 additions & 12 deletions great_docs/_term_player/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,7 @@ def to_json(self) -> str:
}
for a in self.annotations
],
"highlights": [
{
"time": round(h.time, 3),
"duration": round(h.duration, 3),
"row": h.row,
"col": h.col,
"width": h.width,
"height": h.height,
"style": h.style,
}
for h in self.highlights
],
"highlights": [self._serialize_highlight(h) for h in self.highlights],
"snippets": [
{
"time": round(c.time, 3),
Expand All @@ -114,6 +103,42 @@ def to_json(self) -> str:
data["window_chrome"] = self.window_chrome
return json.dumps(data, indent=2)

@staticmethod
def _serialize_highlight(h: Highlight) -> dict:
"""Serialize a Highlight to a manifest-compatible dict."""
target: dict = {}
if h.target.region:
target["region"] = h.target.region
if h.target.match:
target["match"] = h.target.match
if h.target.group:
target["group"] = h.target.group
if h.target.lines:
target["lines"] = h.target.lines
if h.target.track_scroll:
target["track_scroll"] = True

result: dict = {
"time": round(h.time, 3),
"duration": round(h.duration, 3),
"target": target,
"style": h.style,
"color": h.color,
}
# Optional badge fields
if h.badge_text:
result["badge_text"] = h.badge_text
if h.badge_icon:
result["badge_icon"] = h.badge_icon
# Animation
if h.fade_in != 0.3:
result["fade_in"] = round(h.fade_in, 3)
if h.fade_out != 0.3:
result["fade_out"] = round(h.fade_out, 3)
if h.pulse:
result["pulse"] = True
return result


def generate_manifest(
recording: Recording,
Expand Down
93 changes: 80 additions & 13 deletions great_docs/_term_player/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,66 @@ class Snippet:
label: str = ""


@dataclass
class HighlightTarget:
"""Targeting information for a highlight."""

# Region-based targeting
region: dict[str, int] | None = None # {row, col, width, height}
# Pattern-based targeting
match: str | None = None
group: int = 0
# Line-based targeting
lines: list[int] | None = None
# Scroll tracking
track_scroll: bool = False


@dataclass
class Highlight:
"""A highlighted region of the terminal."""
"""A highlighted region of the terminal with rich adornment styles.

Styles: outline, underline, underline-wavy, background, spotlight,
glow, box, badge-before, badge-after, bracket
"""

time: float
duration: float
row: int
col: int
width: int
height: int
style: str = "box" # "box", "underline", "glow"
target: HighlightTarget
style: str = "outline"
color: str = "#f1fa8c"
# Badge options (only for badge-before / badge-after)
badge_text: str = ""
badge_icon: str = ""
# Animation
fade_in: float = 0.3
fade_out: float = 0.3
pulse: bool = False

# Legacy compatibility properties for region-based access
@property
def row(self) -> int:
if self.target.region:
return self.target.region.get("row", 0)
return 0

@property
def col(self) -> int:
if self.target.region:
return self.target.region.get("col", 0)
return 0

@property
def width(self) -> int:
if self.target.region:
return self.target.region.get("width", 10)
return 0

@property
def height(self) -> int:
if self.target.region:
return self.target.region.get("height", 1)
return 1


@dataclass
Expand Down Expand Up @@ -192,17 +241,35 @@ def _parse_script_data(data: dict[str, Any]) -> Script:

# Highlights
for hl in data.get("highlights", []):
if isinstance(hl, dict) and "at" in hl and "region" in hl:
region = hl["region"]
if isinstance(hl, dict) and "at" in hl:
target_data = hl.get("target", {})
if not isinstance(target_data, dict):
target_data = {}

# Legacy format: region at top level
if "region" in hl and "target" not in hl:
target_data = {"region": hl["region"]}

target = HighlightTarget(
region=target_data.get("region"),
match=target_data.get("match"),
group=int(target_data.get("group", 0)),
lines=target_data.get("lines"),
track_scroll=bool(target_data.get("track_scroll", False)),
)

script.highlights.append(
Highlight(
time=float(hl["at"]),
duration=float(hl.get("duration", 2.0)),
row=int(region.get("row", 0)),
col=int(region.get("col", 0)),
width=int(region.get("width", 10)),
height=int(region.get("height", 1)),
style=hl.get("style", "box"),
target=target,
style=hl.get("style", "outline"),
color=hl.get("color", "#f1fa8c"),
badge_text=hl.get("badge_text", ""),
badge_icon=hl.get("badge_icon", ""),
fade_in=float(hl.get("fade_in", 0.3)),
fade_out=float(hl.get("fade_out", 0.3)),
pulse=bool(hl.get("pulse", False)),
)
)

Expand Down
109 changes: 109 additions & 0 deletions great_docs/assets/termshow.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
width: 100%;
overflow: hidden;
outline: none;
container-type: inline-size;
}

.gd-tp-viewport:focus-visible {
Expand Down Expand Up @@ -93,6 +94,114 @@
contain: layout style;
}

/* Highlights overlay — top: -2px matches the SVG's margin-top: -2px */
.gd-tp-highlights {
position: absolute;
top: -2px;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
contain: layout style;
z-index: 1;
}

.gd-tp-highlight {
--hl-color: #f1fa8c;
transition: opacity 0.15s ease;
box-sizing: border-box;
border-radius: max(1px, 0.426cqi);
}

/* Styles */
.gd-tp-hl-outline {
box-shadow: 0 0 0 max(1px, 0.284cqi) var(--hl-color);
}

.gd-tp-hl-underline {
border-bottom: max(1px, 0.284cqi) solid var(--hl-color);
border-radius: 0;
}

.gd-tp-hl-underline-wavy {
border-radius: 0;
}
.gd-tp-hl-underline-wavy::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: max(2px, 0.568cqi);
background-color: var(--hl-color);
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='4' viewBox='0 0 8 4'%3E%3Cpath d='M0 2Q2 0 4 2Q6 4 8 2' fill='none' stroke='black' stroke-width='1.5'/%3E%3C/svg%3E");
-webkit-mask-repeat: repeat-x;
-webkit-mask-size: max(4px, 1.136cqi) max(2px, 0.568cqi);
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='4' viewBox='0 0 8 4'%3E%3Cpath d='M0 2Q2 0 4 2Q6 4 8 2' fill='none' stroke='black' stroke-width='1.5'/%3E%3C/svg%3E");
mask-repeat: repeat-x;
mask-size: max(4px, 1.136cqi) max(2px, 0.568cqi);
}

.gd-tp-hl-background {
background: color-mix(in srgb, var(--hl-color) 20%, transparent);
}

.gd-tp-hl-spotlight {
background: color-mix(in srgb, var(--hl-color) 10%, transparent);
box-shadow: 0 0 max(8px, 2.84cqi) color-mix(in srgb, var(--hl-color) 40%, transparent);
}

.gd-tp-hl-glow {
box-shadow: 0 0 max(4px, 1.136cqi) max(1px, 0.284cqi) var(--hl-color), inset 0 0 max(2px, 0.568cqi) max(1px, 0.142cqi) color-mix(in srgb, var(--hl-color) 30%, transparent);
}

.gd-tp-hl-box {
border: max(1px, 0.284cqi) solid var(--hl-color);
background: color-mix(in srgb, var(--hl-color) 10%, transparent);
}

.gd-tp-hl-bracket {
border-left: max(2px, 0.426cqi) solid var(--hl-color);
border-radius: 0;
}

.gd-tp-hl-badge-before,
.gd-tp-hl-badge-after {
border: max(1px, 0.213cqi) solid var(--hl-color);
}

.gd-tp-hl-badge {
position: absolute;
background: var(--hl-color);
color: #1e1e2e;
font-size: max(7px, 1.42cqi);
font-weight: 600;
padding: max(1px, 0.142cqi) max(3px, 0.71cqi);
border-radius: max(1px, 0.426cqi);
white-space: nowrap;
line-height: 1.3;
}

.gd-tp-hl-badge-before .gd-tp-hl-badge {
top: max(-14px, -1.989cqi);
left: 0;
}

.gd-tp-hl-badge-after .gd-tp-hl-badge {
bottom: max(-14px, -1.989cqi);
left: 0;
}

/* Pulse animation */
@keyframes gd-tp-hl-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}

.gd-tp-hl-pulse {
animation: gd-tp-hl-pulse 1.5s ease-in-out infinite;
}

.gd-tp-annotation {
position: absolute;
padding: clamp(4px, 1.2cqi, 10px) clamp(6px, 1.8cqi, 14px);
Expand Down
Loading
Loading