diff --git a/OPEN-CORE.md b/OPEN-CORE.md index d8835e273..1f4b9bbd4 100644 --- a/OPEN-CORE.md +++ b/OPEN-CORE.md @@ -118,6 +118,17 @@ Consulted in `handleConnection` when an unknown peer connects: if the auth provi Default (no provider) keeps the whitelist + approval flow byte-identical. `authorize` is a synchronous lookup against state the plugin maintains from its own async login. +**Automatic admissions are never silent (fix-live F-5).** Whenever `decide` answers +`'admit'` (or `authorize` returns true), core shows the HOST a toast and a +notification-history entry that keeps its buttons until answered: **Remove** (closes the +link, drops the roster row, refuses that peer's re-dials while the tab stays open) and, +when the provider offers it, a switch-off. The optional provider member +`admitNotice(peerId, cloudMeta) → {text?, turnOff?, turnOffLabel?} | null` supplies the +line (≤ 200 chars) and the switch-off; absent or null = a generic line, no switch-off. +`api.setConnectBadge({text, title?, onClick?} | null)` puts a small badge on the Connect +pill (`#connect-setting-badge`, ≤ 32 chars) for a standing setting such as "Public · +guests join"; a press runs `onClick` or opens the drawer's Rooms tab. + ### 3. UI mount points Roles replicate over the **plugin message channel**: an admin broadcasts the roles @@ -248,6 +259,9 @@ static|embed>`. Core never talks to a server about it. | `loadRemoteScene({…, keepGameId?})` | `keepGameId: true` = the OWNER opening their own published scene (no fork); otherwise a community load is a remix | | `onExportBuilt(fn)` → off | after an export zip is built: `fn({gameId, buildId, preset, title, template, parentGameId, appVersion})` | | `onTemplateOpen(fn)` → off | a Games-tab template was started: `fn(slug)` (one "template open") | +| `toast(message, actions?, {keep?})` | fix-live F-2: with `keep: true` an action toast's buttons also stay on its notification-history entry until one is pressed (a role offer the user sees later) | +| `setPeerMenuItems(items)` | fix-live: `[{id, label, run(peerId, {name})}]` (≤ 8, labels ≤ 40) appended to the Users peer menu (right-click a peer) under core's own entries — the cloud's "Report…"; `[]` clears | +| `setConnectBadge(badge \| null)` | fix-live F-5: `{text, title?, onClick?}` — a small badge on the Connect pill for a standing setting (e.g. "Public · guests join"); `null` hides it. Probe with `typeof` | | `setSceneHeart(info \| null)` | the heart on the play-link start card (`#embed-heart`) and a game's pause menu (`#game-shell-heart`): `{count, liked, toggle() → Promise<{liked, count} \| null>}`; `null` from `toggle` rolls the optimistic flip back | The community provider gains three optional entry fields — `liked` (boolean, the viewer's own diff --git a/src/components/menu/Connect.svelte b/src/components/menu/Connect.svelte index 712cca483..533f8b5be 100644 --- a/src/components/menu/Connect.svelte +++ b/src/components/menu/Connect.svelte @@ -14,7 +14,7 @@ import { signalingRetry, formatElapsed, approvalStartedAt, approvalRemaining, APPROVAL_WINDOW_MS, joinRefusal, clearJoinRefusal, HARD_PEER_CAP } from '$lib/connectionState'; import { cancelOutboundRequest, requestConnect } from '$lib/peerApproval'; import { sessionHost } from '$lib/connectionState'; - import { connectSlot, drawerSlot } from '$lib/cloudHooks'; + import { connectSlot, drawerSlot, connectBadge } from '$lib/cloudHooks'; // 41 G21: the status is a DOT (grey / yellow pulsing / green / red); its words ride title + aria-label import { connectionStatus } from '$lib/connectionStatus'; import CloudSlot from '../CloudSlot.svelte'; @@ -659,6 +659,19 @@ > {/if} + {#if $connectBadge && !squeezedView} + + + {/if} + {#if $drawerSlot && $showRoomsButton && !squeezedView} @@ -864,6 +877,21 @@ background: color-mix(in srgb, var(--ink-bad) 16%, transparent); cursor: pointer; } + .cx-setting-badge { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + height: 20px; + padding: 0 8px; + border: 0; + border-radius: var(--radius-pill); + font-size: var(--fs-badge); + font-weight: 600; + white-space: nowrap; + color: var(--ink-warn); + background: color-mix(in srgb, var(--ink-warn) 16%, transparent); + cursor: pointer; + } .cx-refused[data-result='full'] { color: var(--ink-warn); background: color-mix(in srgb, var(--ink-warn) 16%, transparent); diff --git a/src/components/menu/NotificationCenter.svelte b/src/components/menu/NotificationCenter.svelte index 18f3de554..1c0656e57 100644 --- a/src/components/menu/NotificationCenter.svelte +++ b/src/components/menu/NotificationCenter.svelte @@ -7,7 +7,7 @@ // missed (or dismissed while a modal was open) is still recoverable. Placed in the // top-right chrome next to the peers/profile cluster (Users.svelte), mirroring the // peers popover pattern (absolute dropdown, click-catcher backdrop). - import { notifications, notificationsUnread, notificationCenterOpen } from '../../stores/appStore.js'; + import { notifications, notificationsUnread, notificationCenterOpen, notificationActions } from '../../stores/appStore.js'; function toggle() { const willOpen = !$notificationCenterOpen; @@ -18,6 +18,16 @@ function clearAll() { notifications.set([]); notificationsUnread.set(0); + notificationActions.set({}); + } + + /** F-5: the newest entry of a group that still offers buttons @param {any[]} items */ + function liveActionsOf(items) { + for (let i = items.length - 1; i >= 0; i--) { + const acts = $notificationActions[items[i].id]; + if (acts?.length) return acts; + } + return null; } // NOTES-38 #13 (user): a message that repeats ("Cannot reach the peer server. Retrying…" @@ -95,6 +105,13 @@ {g.text} {ago(g.latest.ts)} + {#if liveActionsOf(g.items)} + + {#each liveActionsOf(g.items) ?? [] as a (a.label)} + + {/each} + + {/if} {#if g.items.length > 1 && open[g.text]}