HASH 509eb35a41d0
DATE 2026-05-06
SUBJECT web: add HelpHint component
FILES 2 CHANGED
HASH 509eb35a41d0
DATE 2026-05-06
SUBJECT web: add HelpHint component
FILES 2 CHANGED
┌─ web/src/components/HelpHint/HelpHint.astro ───────────────────────────────┐
│ diff --git a/web/src/components/HelpHint/HelpHint.astro b/web/src/components/HelpH │
│ int/HelpHint.astro │
│ new file mode 100644 │
│ index 0000000..a7a4b4d │
│ --- /dev/null │
│ +++ b/web/src/components/HelpHint/HelpHint.astro │
│ @@ -0,0 +1,36 @@ │
│ +--- │
│ +import "./HelpHint.scss"; │
│ + │
│ +// "(?)" icon with a CSS-only tooltip; explanation goes in the slot │
│ + │
│ +interface Props { │
│ + title?: string; │
│ + glyph?: string; │
│ + align?: "left" | "right"; │
│ + // default "below" since most usages live in bordered cards where above │
│ + // leaks outside the card boundary │
│ + placement?: "above" | "below"; │
│ +} │
│ + │
│ +const { │
│ + title, │
│ + glyph = "?", │
│ + align = "right", │
│ + placement = "below", │
│ +} = Astro.props; │
│ +--- │
│ + │
│ +<span │
│ + class:list={["help-hint", `help-hint--${align}`, `help-hint--${placement}`]} │
│ +> │
│ + <button │
│ + type="button" │
│ + class="help-hint__trigger" │
│ + aria-label={title ?? "More info"} │
│ + tabindex="0">{glyph}</button │
│ + > │
│ + <span class="help-hint__bubble" role="tooltip"> │
│ + {title && <strong class="help-hint__title">{title}</strong>} │
│ + <span class="help-hint__body"><slot /></span> │
│ + </span> │
│ +</span> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...nents/HelpHint/HelpHint.astro ───┐
│ diff --git a/web/src/components/HelpHint/Hel │
│ pHint.astro b/web/src/components/HelpHint/He │
│ lpHint.astro │
│ new file mode 100644 │
│ index 0000000..a7a4b4d │
│ --- /dev/null │
│ +++ b/web/src/components/HelpHint/HelpHint.a │
│ stro │
│ @@ -0,0 +1,36 @@ │
│ +--- │
│ +import "./HelpHint.scss"; │
│ + │
│ +// "(?)" icon with a CSS-only tooltip; expl │
│ anation goes in the slot │
│ + │
│ +interface Props { │
│ + title?: string; │
│ + glyph?: string; │
│ + align?: "left" | "right"; │
│ + // default "below" since most usages live │
│ in bordered cards where above │
│ + // leaks outside the card boundary │
│ + placement?: "above" | "below"; │
│ +} │
│ + │
│ +const { │
│ + title, │
│ + glyph = "?", │
│ + align = "right", │
│ + placement = "below", │
│ +} = Astro.props; │
│ +--- │
│ + │
│ +<span │
│ + class:list={["help-hint", `help-hint--${a │
│ lign}`, `help-hint--${placement}`]} │
│ +> │
│ + <button │
│ + type="button" │
│ + class="help-hint__trigger" │
│ + aria-label={title ?? "More info"} │
│ + tabindex="0">{glyph}</button │
│ + > │
│ + <span class="help-hint__bubble" role="too │
│ ltip"> │
│ + {title && <strong class="help-hint__tit │
│ le">{title}</strong>} │
│ + <span class="help-hint__body"><slot />< │
│ /span> │
│ + </span> │
│ +</span> │
└──────────────────────────────────────────────┘
┌─ web/src/components/HelpHint/HelpHint.scss ────────────────────────────────┐
│ diff --git a/web/src/components/HelpHint/HelpHint.scss b/web/src/components/HelpHi │
│ nt/HelpHint.scss │
│ new file mode 100644 │
│ index 0000000..6ae89ca │
│ --- /dev/null │
│ +++ b/web/src/components/HelpHint/HelpHint.scss │
│ @@ -0,0 +1,118 @@ │
│ +@use "../../styles/core/tokens" as *; │
│ + │
│ +.help-hint { │
│ + position: relative; │
│ + display: inline-flex; │
│ + align-items: center; │
│ + vertical-align: middle; │
│ + margin-left: 4px; │
│ + │
│ + &__trigger { │
│ + width: 16px; │
│ + height: 16px; │
│ + border-radius: 50%; │
│ + border: 1px solid var(--border-color); │
│ + background: var(--bg-secondary); │
│ + color: var(--text-tertiary); │
│ + font-size: 11px; │
│ + font-weight: $font-weight-bold; │
│ + line-height: 1; │
│ + cursor: help; │
│ + padding: 0; │
│ + display: inline-flex; │
│ + align-items: center; │
│ + justify-content: center; │
│ + transition: │
│ + color 0.12s ease, │
│ + border-color 0.12s ease, │
│ + background 0.12s ease; │
│ + │
│ + &:hover, │
│ + &:focus-visible { │
│ + color: var(--text-primary); │
│ + border-color: var(--highlight-color); │
│ + background: var(--bg-primary); │
│ + outline: none; │
│ + } │
│ + } │
│ + │
│ + &__bubble { │
│ + position: absolute; │
│ + z-index: 50; │
│ + width: max-content; │
│ + max-width: 320px; │
│ + padding: 10px 12px; │
│ + background: var(--bg-secondary); │
│ + border: 1px solid var(--border-color); │
│ + border-radius: $radius-md; │
│ + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45); │
│ + color: var(--text-primary); │
│ + // Use absolute pixel sizes so the bubble looks the same regardless of the │
│ + // parent's em-cascade. (Inside chart cards, the parent font-size is already │
│ + // small and a relative size compounded to ~10px, which read as unstyled.) │
│ + font-size: 13px; │
│ + line-height: 1.5; │
│ + font-weight: $font-weight-medium; │
│ + opacity: 0; │
│ + pointer-events: none; │
│ + transition: │
│ + opacity 0.1s ease, │
│ + transform 0.1s ease; │
│ + // Reset inherited typography. `.filter-group label` sets `white-space: │
│ + // nowrap` (and `text-transform: uppercase`); both inherit down to here │
│ + // and break body text inside chart-card hints. Lock to sensible defaults. │
│ + white-space: normal; │
│ + text-transform: none; │
│ + letter-spacing: 0; │
│ + } │
│ + │
│ + // Vertical placement: above stacks the bubble over the trigger; below │
│ + // is the default and slides it under the trigger so it stays inside the │
│ + // surrounding card. │
│ + &--above &__bubble { │
│ + bottom: calc(100% + 8px); │
│ + transform: translateY(2px); │
│ + } │
│ + &--below &__bubble { │
│ + top: calc(100% + 8px); │
│ + transform: translateY(-2px); │
│ + } │
│ + │
│ + // Horizontal alignment. │
│ + &--right &__bubble { │
│ + left: 0; │
│ + } │
│ + &--left &__bubble { │
│ + right: 0; │
│ + } │
│ + │
│ + &__trigger:hover + &__bubble, │
│ + &__trigger:focus + &__bubble { │
│ + opacity: 1; │
│ + transform: translateY(0); │
│ + } │
│ + │
│ + &__title { │
│ + display: block; │
│ + color: var(--highlight-color); │
│ + margin-bottom: 6px; │
│ + font-weight: $font-weight-bold; │
│ + font-size: 13px; │
│ + } │
│ + │
│ + &__body { │
│ + display: block; │
│ + color: var(--text-secondary); │
│ + font-size: 13px; │
│ + │
│ + p { │
│ + margin: 0 0 8px; │
│ + &:last-child { margin-bottom: 0; } │
│ + } │
│ + │
│ + strong { │
│ + color: var(--text-primary); │
│ + font-weight: $font-weight-semibold; │
│ + } │
│ + } │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...onents/HelpHint/HelpHint.scss ───┐
│ diff --git a/web/src/components/HelpHint/Hel │
│ pHint.scss b/web/src/components/HelpHint/Hel │
│ pHint.scss │
│ new file mode 100644 │
│ index 0000000..6ae89ca │
│ --- /dev/null │
│ +++ b/web/src/components/HelpHint/HelpHint.s │
│ css │
│ @@ -0,0 +1,118 @@ │
│ +@use "../../styles/core/tokens" as *; │
│ + │
│ +.help-hint { │
│ + position: relative; │
│ + display: inline-flex; │
│ + align-items: center; │
│ + vertical-align: middle; │
│ + margin-left: 4px; │
│ + │
│ + &__trigger { │
│ + width: 16px; │
│ + height: 16px; │
│ + border-radius: 50%; │
│ + border: 1px solid var(--border-color); │
│ + background: var(--bg-secondary); │
│ + color: var(--text-tertiary); │
│ + font-size: 11px; │
│ + font-weight: $font-weight-bold; │
│ + line-height: 1; │
│ + cursor: help; │
│ + padding: 0; │
│ + display: inline-flex; │
│ + align-items: center; │
│ + justify-content: center; │
│ + transition: │
│ + color 0.12s ease, │
│ + border-color 0.12s ease, │
│ + background 0.12s ease; │
│ + │
│ + &:hover, │
│ + &:focus-visible { │
│ + color: var(--text-primary); │
│ + border-color: var(--highlight-color); │
│ + background: var(--bg-primary); │
│ + outline: none; │
│ + } │
│ + } │
│ + │
│ + &__bubble { │
│ + position: absolute; │
│ + z-index: 50; │
│ + width: max-content; │
│ + max-width: 320px; │
│ + padding: 10px 12px; │
│ + background: var(--bg-secondary); │
│ + border: 1px solid var(--border-color); │
│ + border-radius: $radius-md; │
│ + box-shadow: 0 6px 18px rgba(0, 0, 0, 0. │
│ 45); │
│ + color: var(--text-primary); │
│ + // Use absolute pixel sizes so the bubb │
│ le looks the same regardless of the │
│ + // parent's em-cascade. (Inside chart c │
│ ards, the parent font-size is already │
│ + // small and a relative size compounded │
│ to ~10px, which read as unstyled.) │
│ + font-size: 13px; │
│ + line-height: 1.5; │
│ + font-weight: $font-weight-medium; │
│ + opacity: 0; │
│ + pointer-events: none; │
│ + transition: │
│ + opacity 0.1s ease, │
│ + transform 0.1s ease; │
│ + // Reset inherited typography. `.filter │
│ -group label` sets `white-space: │
│ + // nowrap` (and `text-transform: upperc │
│ ase`); both inherit down to here │
│ + // and break body text inside chart-car │
│ d hints. Lock to sensible defaults. │
│ + white-space: normal; │
│ + text-transform: none; │
│ + letter-spacing: 0; │
│ + } │
│ + │
│ + // Vertical placement: above stacks the b │
│ ubble over the trigger; below │
│ + // is the default and slides it under the │
│ trigger so it stays inside the │
│ + // surrounding card. │
│ + &--above &__bubble { │
│ + bottom: calc(100% + 8px); │
│ + transform: translateY(2px); │
│ + } │
│ + &--below &__bubble { │
│ + top: calc(100% + 8px); │
│ + transform: translateY(-2px); │
│ + } │
│ + │
│ + // Horizontal alignment. │
│ + &--right &__bubble { │
│ + left: 0; │
│ + } │
│ + &--left &__bubble { │
│ + right: 0; │
│ + } │
│ + │
│ + &__trigger:hover + &__bubble, │
│ + &__trigger:focus + &__bubble { │
│ + opacity: 1; │
│ + transform: translateY(0); │
│ + } │
│ + │
│ + &__title { │
│ + display: block; │
│ + color: var(--highlight-color); │
│ + margin-bottom: 6px; │
│ + font-weight: $font-weight-bold; │
│ + font-size: 13px; │
│ + } │
│ + │
│ + &__body { │
│ + display: block; │
│ + color: var(--text-secondary); │
│ + font-size: 13px; │
│ + │
│ + p { │
│ + margin: 0 0 8px; │
│ + &:last-child { margin-bottom: 0; } │
│ + } │
│ + │
│ + strong { │
│ + color: var(--text-primary); │
│ + font-weight: $font-weight-semibold; │
│ + } │
│ + } │
│ +} │
└──────────────────────────────────────────────┘
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET