vegolas_obsidian-rpg-scene-.../styles.css
Mateusz Tomanek 65f478fdb1 Obsidian plugin: trigger scenes/events/sounds from notes (#66)
* Add Obsidian plugin to trigger scenes/events/sounds from notes (#65)

A small TypeScript Obsidian plugin under integrations/obsidian/ that turns
an inline token like `sm:scene:city` into a clickable button in a note —
tap it while reading to switch the table's lights + music, without leaving
the note or opening a browser tab.

- Inline syntax `sm:<kind>:<arg>[|label]` for scene/event/sound/music/lights,
  rendered as a chip in Reading view (markdown post-processor) and Live
  Preview (CM6 view plugin). Clicks fire in the background via requestUrl
  (no CORS, no navigation) with a Notice for ok/error.
- EditorSuggest autocomplete: `sm:` suggests kinds, `sm:scene:` suggests the
  scenes/events/sounds that actually exist on the server, showing name +
  tile art (fetched from /scenes/, /events/list, /sounds/list, /images).
- Emoji is split off the entity name like the panel's SceneNaming; art shows
  as a thumbnail with an emoji fallback. Unknown ids render with a dashed
  outline.
- Settings: server base URL + optional API key (kept in plugin data, never
  written into notes) + art toggle + a Test button. Works on desktop and
  mobile Obsidian.
- Uses the existing GET command endpoints (the same ones the Stream Deck
  integration relies on) — no server changes.

Build verified: `npm run build` (tsc + esbuild) is clean. Not yet smoke-
tested inside a live Obsidian instance — a manual check (especially the
Live Preview widget) is expected before relying on it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add banner render style + live active highlight to Obsidian plugin

Two follow-up options on top of the inline chips:

- Button style setting (chip | banner). Banner renders a full-width, larger
  bar with the entity's tile art as a darkened background; chip is the
  existing compact inline button. Global setting, not per-token.
- "Highlight what's live" setting: a StateTracker polls /scenes/active,
  /events/state and /sounds/state (every 4s, only while chips are on
  screen, throttled) and toggles an `is-active` class — a pulsing dot +
  accent ring — so a button lights up while its scene/event/sound is live,
  even when started from the panel or a Stream Deck. Firing a command
  refreshes the state right after. Disconnected chips are pruned so the
  poller idles when nothing is shown.

Build (tsc + esbuild) stays clean.

* Fix banner art jumping on hover

The `.sm-chip:hover` rule set the `background` shorthand, which resets the
background-size/background-position longhands. On a banner that carries art
(background-image set inline, size:cover/position:center from the base rule),
hovering let the higher-specificity :hover rule reset size/position to
auto / 0% 0% while the inline image survived — so the art resized and
jumped to the top-left. Use the background-color longhand instead, and give
banners a brightness-only hover so feedback doesn't move the background.

* Add embedded control-panel pane to the Obsidian plugin

Open the whole app inside an Obsidian tab — a ribbon dice icon and an
"Open control panel" command open a custom ItemView pointed at the
configured server address, so scenes can be driven from the same window as
the session notes (handy on a laptop). A small toolbar reloads it or pops
it out to the system browser.

Desktop uses an Electron <webview> (bypasses mixed-content / frame limits
for the LAN http server); mobile falls back to an <iframe> (documented as
unreliable there — use the PWA on iPad). Additive: an isolated view that
can't affect the inline-chip / autocomplete features.

Build (tsc + esbuild) stays clean.

* Fix embedded panel rendering at 150px height

The <webview>/<iframe> is a replaced element: when its height:100% can't
resolve against the flex-sized frame, it falls back to the CSS default 150px
intrinsic height (the "very short browser"). Position the embed absolutely
(inset:0) so it fills the frame's flex-computed height regardless, and add
min-height:0 to the view column.

* Force embedded panel to fill its pane via inline sizing

The absolute-positioning fix lived only in styles.css, so it did nothing
against a stale installed stylesheet (CSS-only commits leave main.js
unchanged, easy to forget re-copying). Set the panel's layout inline in
panelview.ts — root position:relative, an absolutely-placed toolbar, and
the <webview>/<iframe> absolutely filling the frame — so it sizes correctly
from main.js alone. styles.css is kept in sync as a secondary.

* Fix embedded webview collapsing to 150px (drop display:block)

The DOM showed the real cause: a <webview>'s shadow root stretches its
internal <iframe> via `:host { display: flex }`, but our embed styles set
`display: block` on the host, overriding that — so the inner iframe lost its
stretch and fell back to the 150px intrinsic replaced-element height. Drop
the `display` declaration (inline and in CSS); position:absolute already
block-ifies the element, and :host's flex now stretches the iframe to full
height.

* Embed control panel via <iframe> instead of <webview>

The <webview> sized correctly but rendered blank — Electron's webview is a
separate compositor layer over the element and is flaky after relayout.
Since the panel is served over http, a plain <iframe> is simpler and
reliable, and for a localhost / same-machine server it isn't mixed-content
blocked (localhost is a secure-context exception). Drop the webview + the
desktop/mobile branch entirely; a remote LAN-IP http server may be refused
by the browser (documented — use Open in browser or the PWA there).

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 10:01:36 +02:00

235 lines
4.7 KiB
CSS

/* Inline scene/event/sound trigger chips. Uses Obsidian theme variables so it fits light/dark
and whatever theme the user runs. */
.sm-chip {
display: inline-flex;
align-items: center;
gap: 0.4em;
padding: 0.05em 0.5em;
margin: 0 0.1em;
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
background: var(--background-secondary);
color: var(--text-normal);
font-size: 0.9em;
line-height: 1.5;
text-decoration: none;
vertical-align: baseline;
cursor: pointer;
white-space: nowrap;
}
.sm-chip:hover {
/* background-color (longhand), NOT the `background` shorthand — the shorthand resets
background-size/position, which would make a banner's art jump on hover. */
background-color: var(--background-modifier-hover);
border-color: var(--interactive-accent);
}
.sm-chip:active {
transform: translateY(1px);
}
.sm-chip:focus-visible {
outline: 2px solid var(--interactive-accent);
outline-offset: 1px;
}
.sm-chip-icon {
display: inline-flex;
align-items: center;
line-height: 1;
}
.sm-chip-art {
width: 1.3em;
height: 1.3em;
border-radius: 3px;
object-fit: cover;
display: block;
}
.sm-chip-label {
font-weight: 600;
}
/* Unknown id (not found in the server list): flag it so a typo is obvious. */
.sm-chip-missing {
border-style: dashed;
opacity: 0.75;
}
/* Banner render mode (global setting): full-width, larger, art as background. */
.sm-chip--banner {
display: flex;
box-sizing: border-box;
width: 100%;
min-height: 64px;
margin: 0.5em 0;
padding: 0.75em 1em;
gap: 0.6em;
border-radius: 10px;
background-size: cover;
background-position: center;
}
.sm-chip--banner .sm-chip-icon {
font-size: 1.5em;
}
.sm-chip--banner .sm-chip-label {
font-size: 1.2em;
font-weight: 700;
}
.sm-chip--banner .sm-chip-art {
width: 2.2em;
height: 2.2em;
border-radius: 6px;
}
/* When the banner carries art it becomes light-on-dark for legibility over the image. */
.sm-chip--banner.sm-has-art {
color: #fff;
border-color: transparent;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
}
/* Banner hover feedback that doesn't touch background geometry (so the art stays put). */
.sm-chip--banner:hover {
filter: brightness(1.08);
}
/* Live indicator: a pulsing dot + accent ring while the scene/event/sound is active. */
.sm-chip.is-active {
border-color: var(--interactive-accent);
box-shadow: inset 0 0 0 1px var(--interactive-accent);
}
.sm-chip.is-active::before {
content: "";
flex: 0 0 auto;
width: 0.5em;
height: 0.5em;
border-radius: 50%;
background: var(--color-green, #2ecf7a);
box-shadow: 0 0 6px var(--color-green, #2ecf7a);
animation: sm-pulse 1.6s ease-in-out infinite;
}
@keyframes sm-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.35;
}
}
@media (prefers-reduced-motion: reduce) {
.sm-chip.is-active::before {
animation: none;
}
}
/* Embedded control-panel pane. */
/* Layout is also set inline in panelview.ts (belt-and-suspenders so the pane fills even against a
stale styles.css). Keep these in sync with those inline styles. */
.sm-panel-view {
position: relative;
height: 100%;
padding: 0;
overflow: hidden;
}
.sm-panel-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 34px;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-bottom: 1px solid var(--background-modifier-border);
}
.sm-panel-title {
font-weight: 600;
font-size: 0.85em;
color: var(--text-muted);
}
.sm-panel-spacer {
flex: 1;
}
.sm-panel-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--text-muted);
cursor: pointer;
box-shadow: none;
}
.sm-panel-btn:hover {
background: var(--background-modifier-hover);
color: var(--text-normal);
}
.sm-panel-frame {
position: absolute;
top: 34px;
left: 0;
right: 0;
bottom: 0;
}
/* <webview>/<iframe> are replaced elements: a percentage height that can't resolve falls back
to their 150px intrinsic default. Fill the (flex-sized) frame by positioning absolutely. */
.sm-panel-embed {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
}
.sm-panel-empty {
padding: 1.5em;
color: var(--text-muted);
}
/* Autocomplete suggestion rows. */
.sm-suggestion {
display: flex;
align-items: center;
gap: 0.5em;
}
.sm-suggestion-art {
width: 2em;
height: 2em;
border-radius: 4px;
object-fit: cover;
flex: 0 0 auto;
}
.sm-suggestion-name {
display: inline-flex;
align-items: baseline;
gap: 0.5em;
}
.sm-suggestion-id {
color: var(--text-muted);
font-size: 0.8em;
}