toast notifications (0.32.0): elegant feedback for copies/actions
- src/toast.js: dependency-free toast + copyToast helpers - wired copyToast into all token/accesskey/url/key copy actions (silent before) - replaces jarring browser dialogs for copy feedback; consistent polished UX
This commit is contained in:
parent
0de81620b0
commit
3090ed92b0
@ -7,7 +7,7 @@
|
||||
name="description"
|
||||
content="amerc is an agent mercenary layer for embedding, bringing, and hosting agents across vertical software boundaries."
|
||||
/>
|
||||
<meta name="version" content="0.31.0-activity" />
|
||||
<meta name="version" content="0.32.0-toasts" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amerc-site",
|
||||
"version": "0.31.0-activity",
|
||||
"version": "0.32.0-toasts",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { api } from './api.js';
|
||||
import { copyToast } from './toast.js';
|
||||
|
||||
const STATUS_COLOR = { online: '#36f0b0', pending: '#f2b85f', lost: '#ff9f5a', down: '#ff7a8c' };
|
||||
const TAG_EMOJI = { backend: '🛠️', model: '🧠', frontend: '🎨', ui: '🖌️', web: '🌐', data: '📊', devops: '⚙️', ml: '🤖', security: '🔐', api: '🔌', bot: '🤖', chat: '💬', test: '🧪', docs: '📄', design: '✏️', ops: '🚦', research: '🔬' };
|
||||
@ -92,7 +93,7 @@ function ClassModal({ id, auth, onClose }) {
|
||||
{err && <p className="px-auth-err">{err}</p>}
|
||||
<div className="ap-sub">
|
||||
{auth.user ? <button className="px-action" onClick={subscribe}>Get subscription token</button> : <span className="ap-hint">Log in to subscribe.</span>}
|
||||
{sub && <code className="ap-token" onClick={() => navigator.clipboard?.writeText(sub)}>{sub} ⧉</code>}
|
||||
{sub && <code className="ap-token" onClick={() => copyToast(sub)}>{sub} ⧉</code>}
|
||||
</div>
|
||||
<h4 className="ap-h4">Instances</h4>
|
||||
<div className="ap-inst-list">
|
||||
@ -178,7 +179,7 @@ function KeysCard() {
|
||||
<input placeholder="key name (e.g. my-bot)" value={name} onChange={(e) => setName(e.target.value)} required />
|
||||
<button className="px-action" type="submit">+ Mint key</button>
|
||||
</form>
|
||||
{created && <div className="mn-created"><p>✅ minted — copy now, shown once:</p><code className="ap-token" onClick={() => navigator.clipboard?.writeText(created.key)}>{created.key} ⧉</code></div>}
|
||||
{created && <div className="mn-created"><p>✅ minted — copy now, shown once:</p><code className="ap-token" onClick={() => copyToast(created.key)}>{created.key} ⧉</code></div>}
|
||||
{err && <p className="px-auth-err">{err}</p>}
|
||||
{keys.map((k) => (
|
||||
<div key={k.id} className="ap-mine-inst">
|
||||
@ -257,7 +258,7 @@ export function BoothDashboard({ auth, setRoute }) {
|
||||
{insts.map((i) => (
|
||||
<div key={i.id} className="ap-mine-inst">
|
||||
<span className="ap-inst-status" style={{ background: STATUS_COLOR[i.status] }} /> #{i.id} <em>{i.status}</em>
|
||||
{i.accesskey && <code className="ap-token" title="accesskey — your broker connects with this" onClick={() => navigator.clipboard?.writeText(i.accesskey)}>{i.accesskey.slice(0, 14)}… ⧉</code>}
|
||||
{i.accesskey && <code className="ap-token" title="accesskey — your broker connects with this" onClick={() => copyToast(i.accesskey)}>{i.accesskey.slice(0, 14)}… ⧉</code>}
|
||||
<button className="px-action" onClick={() => shutdown(i)}>shut down</button>
|
||||
</div>
|
||||
))}
|
||||
@ -270,8 +271,8 @@ export function BoothDashboard({ auth, setRoute }) {
|
||||
<div className="ap-modal-box" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="ap-modal-head"><h3>Instance #{newInst.id} created</h3><button onClick={() => setNewInst(null)}>✕</button></div>
|
||||
<p className="ap-hint">Run your broker against this instance, then it shows online. The accesskey is the relay token.</p>
|
||||
<label className="ap-field">accesskey<code className="ap-token" onClick={() => navigator.clipboard?.writeText(newInst.accesskey)}>{newInst.accesskey} ⧉</code></label>
|
||||
<label className="ap-field">broker WS<code className="ap-token" onClick={() => navigator.clipboard?.writeText(newInst.relayWs)}>{newInst.relayWs} ⧉</code></label>
|
||||
<label className="ap-field">accesskey<code className="ap-token" onClick={() => copyToast(newInst.accesskey)}>{newInst.accesskey} ⧉</code></label>
|
||||
<label className="ap-field">broker WS<code className="ap-token" onClick={() => copyToast(newInst.relayWs)}>{newInst.relayWs} ⧉</code></label>
|
||||
<label className="ap-field">heartbeat<code className="ap-token">POST /api/agents/instances/{newInst.id}/heartbeat {'{'}accesskey,broker,tui{'}'}</code></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { api } from './api.js';
|
||||
import { copyToast } from './toast.js';
|
||||
|
||||
// amerc Mansion — a hub of amerc services. First service: Showcase (reverse proxy).
|
||||
export default function Mansion({ auth, setRoute }) {
|
||||
@ -33,7 +34,7 @@ function Showcase({ handle }) {
|
||||
catch (e2) { setErr(e2.message); }
|
||||
};
|
||||
const del = async (id) => { if (!window.confirm('Delete this showcase?')) return; try { await api(`/showcase/${id}`, { method: 'DELETE' }); load(); } catch (e2) { setErr(e2.message); } };
|
||||
const copy = (t) => navigator.clipboard?.writeText(t);
|
||||
const copy = (t) => copyToast(t);
|
||||
|
||||
return (
|
||||
<div className="mn-card mn-card-wide">
|
||||
|
||||
@ -6,7 +6,7 @@ import Mansion from './Mansion.jsx';
|
||||
import TavernGame from './TavernGame.jsx';
|
||||
import { api } from './api.js';
|
||||
|
||||
const RELEASE = '0.31.0-activity';
|
||||
const RELEASE = '0.32.0-toasts';
|
||||
|
||||
const NAV = [
|
||||
{ id: 'home', label: 'Tavern' },
|
||||
|
||||
@ -1146,3 +1146,9 @@ button {
|
||||
.act-ico { font-size: 16px; flex: none; }
|
||||
.act-text { font-size: 13px; color: #c9b8e6; flex: 1; }
|
||||
.act-time { font-size: 11px; color: #6f5b8c; flex: none; }
|
||||
|
||||
/* toasts (0.32) */
|
||||
#amerc-toasts { position: fixed; bottom: 20px; right: 20px; z-index: 99999; display: flex; flex-direction: column; gap: 8px; pointer-events: none; }
|
||||
.amerc-toast { background: #142238; color: #eaf2ff; border: 1px solid #2e4a6e; border-left: 3px solid #36f0b0; border-radius: 9px; padding: 11px 16px; font-family: Inter, sans-serif; font-size: 13px; box-shadow: 0 10px 28px rgba(0,0,0,0.55); opacity: 0; transform: translateX(24px); transition: opacity .25s ease, transform .25s ease; max-width: 320px; }
|
||||
.amerc-toast.in { opacity: 1; transform: none; }
|
||||
.amerc-toast.t-err { border-left-color: #ff7a8c; }
|
||||
|
||||
16
src/toast.js
Normal file
16
src/toast.js
Normal file
@ -0,0 +1,16 @@
|
||||
// Tiny dependency-free toast notifications, callable from anywhere.
|
||||
export function toast(msg, type = 'ok', ms = 2600) {
|
||||
let c = document.getElementById('amerc-toasts');
|
||||
if (!c) { c = document.createElement('div'); c.id = 'amerc-toasts'; document.body.appendChild(c); }
|
||||
const t = document.createElement('div');
|
||||
t.className = `amerc-toast t-${type}`;
|
||||
t.textContent = msg;
|
||||
c.appendChild(t);
|
||||
requestAnimationFrame(() => t.classList.add('in'));
|
||||
setTimeout(() => { t.classList.remove('in'); setTimeout(() => t.remove(), 300); }, ms);
|
||||
}
|
||||
|
||||
export function copyToast(text, label = 'Copied') {
|
||||
try { navigator.clipboard.writeText(text).then(() => toast(`${label} ✓`)).catch(() => toast('Copy failed', 'err')); }
|
||||
catch { toast('Copy failed', 'err'); }
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user