mirror of
https://github.com/vegolas/obsidian-rpg-scene-maker.git
synced 2026-07-22 08:34:05 +00:00
* 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>
235 lines
4.7 KiB
CSS
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;
|
|
}
|