Shell: tooltips at once, one or two words; text-size buttons are drawn − and +
Every header and top-bar button's title becomes a tip the shell shows the moment the pointer arrives (the browser's own waits a second and looks different everywhere); it moves from `title` to data-tip so the native one never appears, and a module that sets `title` again is picked up on the next hover. The shell's own labels are "Settings" and "Remove"; the top bar's are "Fullscreen", "Add tile", "Layouts", "Admin"; Slack, PCO Plan, ProdCom and Timers shorten theirs to match. A− / A+ are a drawn − and + in Slack, PCO Plan and ProdCom, sized like every other header icon. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
1930fb142d
commit
818def822d
@@ -330,16 +330,17 @@ bar, next to the shell's gear/close buttons:
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
const btn = moduleApi.header.addButton({
|
const btn = moduleApi.header.addButton({
|
||||||
icon: '<svg …>…</svg>', // inline SVG string, and/or
|
icon: '<svg …>…</svg>', // inline SVG string (13 px), and/or
|
||||||
label: 'A+', // short text
|
label: 'Timers ▾', // short text
|
||||||
title: 'Larger text', // hover tooltip
|
title: 'Larger text', // tooltip — one or two words; the shell shows
|
||||||
|
// it the moment the pointer arrives
|
||||||
onClick() { … },
|
onClick() { … },
|
||||||
});
|
});
|
||||||
btn.classList.add('active'); // toggle styling for on/off buttons
|
btn.classList.add('active'); // toggle styling for on/off buttons
|
||||||
|
|
||||||
const menu = moduleApi.header.addMenu({
|
const menu = moduleApi.header.addMenu({
|
||||||
label: 'Channels ▾',
|
label: 'Channels ▾',
|
||||||
title: 'Choose visible channels',
|
title: 'Channels',
|
||||||
build(menuEl) {
|
build(menuEl) {
|
||||||
// called on every open with an emptied menu element — fill it with
|
// called on every open with an emptied menu element — fill it with
|
||||||
// buttons (give them your scoped classes; the container is styled and
|
// buttons (give them your scoped classes; the container is styled and
|
||||||
@@ -351,7 +352,10 @@ menu.setLabel('Channels (2/4) ▾'); // update the button text any time
|
|||||||
|
|
||||||
Everything you add is removed automatically when your instance stops, so a
|
Everything you add is removed automatically when your instance stops, so a
|
||||||
remount never duplicates controls. Keep them compact (icon buttons, short
|
remount never duplicates controls. Keep them compact (icon buttons, short
|
||||||
labels): the title bar is 32 px tall and shared with the tile's name.
|
labels): the title bar is 32 px tall and shared with the tile's name. Draw
|
||||||
|
icons as inline SVG rather than characters — a glyph such as ✕ or + takes
|
||||||
|
its size from whatever font each browser picks; text-size buttons are a
|
||||||
|
drawn − and +.
|
||||||
|
|
||||||
**One control per action.** Whatever has a dedicated header button is not
|
**One control per action.** Whatever has a dedicated header button is not
|
||||||
offered again in the gear popover: text size is A− / A+ up here (persisted
|
offered again in the gear popover: text size is A− / A+ up here (persisted
|
||||||
|
|||||||
@@ -10,6 +10,10 @@
|
|||||||
|
|
||||||
const REFRESH_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg>';
|
const REFRESH_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg>';
|
||||||
|
|
||||||
|
/* the text-size buttons: a drawn − and + at the icon size, like every other header icon */
|
||||||
|
const MINUS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M5 12h14"/></svg>';
|
||||||
|
const PLUS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>';
|
||||||
|
|
||||||
export default function create({ root, moduleApi }) {
|
export default function create({ root, moduleApi }) {
|
||||||
let state = null;
|
let state = null;
|
||||||
let skew = 0; // server clock − local clock, ms
|
let skew = 0; // server clock − local clock, ms
|
||||||
@@ -55,7 +59,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
|
|
||||||
const refreshBtn = moduleApi.header.addButton({
|
const refreshBtn = moduleApi.header.addButton({
|
||||||
icon: REFRESH_SVG,
|
icon: REFRESH_SVG,
|
||||||
title: 'Re-read the plan from Planning Center now',
|
title: 'Refresh',
|
||||||
onClick() {
|
onClick() {
|
||||||
refreshBtn.classList.add('active');
|
refreshBtn.classList.add('active');
|
||||||
moduleApi.fetch('/refresh', { method: 'POST' })
|
moduleApi.fetch('/refresh', { method: 'POST' })
|
||||||
@@ -69,8 +73,8 @@ export default function create({ root, moduleApi }) {
|
|||||||
moduleApi.saveInstanceSettings({ textSize: Math.min(40, Math.max(9, size + delta)) });
|
moduleApi.saveInstanceSettings({ textSize: Math.min(40, Math.max(9, size + delta)) });
|
||||||
applyPrefs();
|
applyPrefs();
|
||||||
}
|
}
|
||||||
moduleApi.header.addButton({ label: 'A−', title: 'Smaller text', onClick: () => bumpTextSize(-1) });
|
moduleApi.header.addButton({ icon: MINUS_SVG, title: 'Smaller text', onClick: () => bumpTextSize(-1) });
|
||||||
moduleApi.header.addButton({ label: 'A+', title: 'Larger text', onClick: () => bumpTextSize(1) });
|
moduleApi.header.addButton({ icon: PLUS_SVG, title: 'Larger text', onClick: () => bumpTextSize(1) });
|
||||||
|
|
||||||
/* ── formatting ─────────────────────────────────────────────────── */
|
/* ── formatting ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
persists in instance settings, and all network traffic goes through
|
persists in instance settings, and all network traffic goes through
|
||||||
moduleApi (the module's server proxy at /prodcom/*). */
|
moduleApi (the module's server proxy at /prodcom/*). */
|
||||||
|
|
||||||
|
/* the text-size buttons: a drawn − and + at the icon size, like every other header icon */
|
||||||
|
const MINUS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M5 12h14"/></svg>';
|
||||||
|
const PLUS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>';
|
||||||
|
|
||||||
export default function create({ root, moduleApi }) {
|
export default function create({ root, moduleApi }) {
|
||||||
/* ── per-instance state ─────────────────────────────────────────── */
|
/* ── per-instance state ─────────────────────────────────────────── */
|
||||||
|
|
||||||
@@ -48,7 +52,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
let channelMenuEl = null; // the open menu's element (filled on each open)
|
let channelMenuEl = null; // the open menu's element (filled on each open)
|
||||||
const channelsMenu = moduleApi.header.addMenu({
|
const channelsMenu = moduleApi.header.addMenu({
|
||||||
icon: CHANNELS_SVG,
|
icon: CHANNELS_SVG,
|
||||||
title: 'Choose visible channels',
|
title: 'Channels',
|
||||||
build(menu) {
|
build(menu) {
|
||||||
menu.classList.add('pt-menu');
|
menu.classList.add('pt-menu');
|
||||||
channelMenuEl = menu;
|
channelMenuEl = menu;
|
||||||
@@ -65,7 +69,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
let timeBtn = null;
|
let timeBtn = null;
|
||||||
timeBtn = moduleApi.header.addButton({
|
timeBtn = moduleApi.header.addButton({
|
||||||
icon: CLOCK_SVG,
|
icon: CLOCK_SVG,
|
||||||
title: 'Show / hide timestamps',
|
title: 'Timestamps',
|
||||||
onClick() {
|
onClick() {
|
||||||
moduleApi.saveInstanceSettings({ showTimes: !moduleApi.instanceSettings.showTimes });
|
moduleApi.saveInstanceSettings({ showTimes: !moduleApi.instanceSettings.showTimes });
|
||||||
applyInstanceSettings();
|
applyInstanceSettings();
|
||||||
@@ -77,8 +81,8 @@ export default function create({ root, moduleApi }) {
|
|||||||
moduleApi.saveInstanceSettings({ textSize: Math.min(48, Math.max(10, current + delta)) });
|
moduleApi.saveInstanceSettings({ textSize: Math.min(48, Math.max(10, current + delta)) });
|
||||||
applyInstanceSettings();
|
applyInstanceSettings();
|
||||||
}
|
}
|
||||||
moduleApi.header.addButton({ label: 'A−', title: 'Smaller text', onClick: () => bumpTextSize(-2) });
|
moduleApi.header.addButton({ icon: MINUS_SVG, title: 'Smaller text', onClick: () => bumpTextSize(-2) });
|
||||||
moduleApi.header.addButton({ label: 'A+', title: 'Larger text', onClick: () => bumpTextSize(2) });
|
moduleApi.header.addButton({ icon: PLUS_SVG, title: 'Larger text', onClick: () => bumpTextSize(2) });
|
||||||
|
|
||||||
// icon and tooltip track the current direction — applyInstanceSettings sets both
|
// icon and tooltip track the current direction — applyInstanceSettings sets both
|
||||||
const flowBtn = moduleApi.header.addButton({
|
const flowBtn = moduleApi.header.addButton({
|
||||||
@@ -93,7 +97,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
|
|
||||||
moduleApi.header.addButton({
|
moduleApi.header.addButton({
|
||||||
icon: JUMP_SVG,
|
icon: JUMP_SVG,
|
||||||
title: 'Jump to latest',
|
title: 'Latest',
|
||||||
onClick() {
|
onClick() {
|
||||||
pinnedToLatest = true;
|
pinnedToLatest = true;
|
||||||
jumpBtn.hidden = true;
|
jumpBtn.hidden = true;
|
||||||
@@ -103,7 +107,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
|
|
||||||
const clearBtn = moduleApi.header.addButton({
|
const clearBtn = moduleApi.header.addButton({
|
||||||
icon: CLEAR_SVG,
|
icon: CLEAR_SVG,
|
||||||
title: 'Clear this tile (does not affect ProdCom)',
|
title: 'Clear',
|
||||||
onClick: clearView,
|
onClick: clearView,
|
||||||
});
|
});
|
||||||
clearBtn.classList.add('pt-clear');
|
clearBtn.classList.add('pt-clear');
|
||||||
@@ -120,9 +124,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
wrap.classList.toggle('newest-first', up);
|
wrap.classList.toggle('newest-first', up);
|
||||||
flowBtn.innerHTML = up ? FLOW_UP_SVG : FLOW_DOWN_SVG;
|
flowBtn.innerHTML = up ? FLOW_UP_SVG : FLOW_DOWN_SVG;
|
||||||
flowBtn.classList.toggle('active', up);
|
flowBtn.classList.toggle('active', up);
|
||||||
flowBtn.title = up
|
flowBtn.title = up ? 'Newest first' : 'Oldest first';
|
||||||
? 'Newest at top — click for newest at bottom'
|
|
||||||
: 'Newest at bottom — click for newest at top';
|
|
||||||
jumpBtn.textContent = (up ? '↑' : '↓') + ' New messages';
|
jumpBtn.textContent = (up ? '↑' : '↓') + ' New messages';
|
||||||
}
|
}
|
||||||
applyInstanceSettings();
|
applyInstanceSettings();
|
||||||
@@ -174,9 +176,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
channelCountEl.textContent = filtering ? `${visibleCount}/${channels.size}` : '';
|
channelCountEl.textContent = filtering ? `${visibleCount}/${channels.size}` : '';
|
||||||
channelCountEl.hidden = !filtering;
|
channelCountEl.hidden = !filtering;
|
||||||
channelsMenu.button.classList.toggle('active', filtering);
|
channelsMenu.button.classList.toggle('active', filtering);
|
||||||
channelsMenu.button.title = filtering
|
channelsMenu.button.title = filtering ? `Channels ${visibleCount}/${channels.size}` : 'Channels';
|
||||||
? `Channels — showing ${visibleCount} of ${channels.size}`
|
|
||||||
: 'Choose visible channels';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function rebuildChannelMenu() {
|
function rebuildChannelMenu() {
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
let menuEl = null; // the open menu's element (filled on each open)
|
let menuEl = null; // the open menu's element (filled on each open)
|
||||||
const itemsMenu = solo ? null : moduleApi.header.addMenu({
|
const itemsMenu = solo ? null : moduleApi.header.addMenu({
|
||||||
label: 'Timers ▾',
|
label: 'Timers ▾',
|
||||||
title: 'Choose which timers this tile shows',
|
title: 'Choose timers',
|
||||||
build(menu) {
|
build(menu) {
|
||||||
// the shell hands over an emptied element on every open
|
// the shell hands over an emptied element on every open
|
||||||
menu.classList.add('tm-menu');
|
menu.classList.add('tm-menu');
|
||||||
|
|||||||
@@ -635,6 +635,10 @@ const emojiMenu = (() => {
|
|||||||
|
|
||||||
/* ── the factory ────────────────────────────────────────────────────── */
|
/* ── the factory ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* the text-size buttons: a drawn − and + at the icon size, like every other header icon */
|
||||||
|
const MINUS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M5 12h14"/></svg>';
|
||||||
|
const PLUS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>';
|
||||||
|
|
||||||
export default function create({ root, moduleApi }) {
|
export default function create({ root, moduleApi }) {
|
||||||
const variant = String(moduleApi.variant || '');
|
const variant = String(moduleApi.variant || '');
|
||||||
// The entry's preset `view` is authoritative; a tile added under 1.0.0 has
|
// The entry's preset `view` is authoritative; a tile added under 1.0.0 has
|
||||||
@@ -680,11 +684,11 @@ function createTranscriptTile({ root, moduleApi }) {
|
|||||||
moduleApi.saveInstanceSettings({ textSize: Math.min(48, Math.max(10, textSizePx(moduleApi) + delta)) });
|
moduleApi.saveInstanceSettings({ textSize: Math.min(48, Math.max(10, textSizePx(moduleApi) + delta)) });
|
||||||
applyInstanceSettings();
|
applyInstanceSettings();
|
||||||
}
|
}
|
||||||
moduleApi.header.addButton({ label: 'A−', title: 'Smaller text', onClick: () => bumpTextSize(-1) });
|
moduleApi.header.addButton({ icon: MINUS_SVG, title: 'Smaller text', onClick: () => bumpTextSize(-1) });
|
||||||
moduleApi.header.addButton({ label: 'A+', title: 'Larger text', onClick: () => bumpTextSize(1) });
|
moduleApi.header.addButton({ icon: PLUS_SVG, title: 'Larger text', onClick: () => bumpTextSize(1) });
|
||||||
moduleApi.header.addButton({
|
moduleApi.header.addButton({
|
||||||
icon: JUMP_SVG,
|
icon: JUMP_SVG,
|
||||||
title: 'Jump to latest',
|
title: 'Latest',
|
||||||
onClick() {
|
onClick() {
|
||||||
pinnedToLatest = true;
|
pinnedToLatest = true;
|
||||||
jumpBtn.hidden = true;
|
jumpBtn.hidden = true;
|
||||||
@@ -1056,7 +1060,7 @@ function createSendTile({ root, moduleApi, mode }) {
|
|||||||
const gate = root.querySelector('.sl-gate');
|
const gate = root.querySelector('.sl-gate');
|
||||||
const compose = root.querySelector('.sl-compose');
|
const compose = root.querySelector('.sl-compose');
|
||||||
|
|
||||||
const lockBtn = moduleApi.header.addButton({ icon: LOCK_SVG, title: 'Lock now', onClick: () => lock('Locked') });
|
const lockBtn = moduleApi.header.addButton({ icon: LOCK_SVG, title: 'Lock', onClick: () => lock('Locked') });
|
||||||
lockBtn.style.display = 'none';
|
lockBtn.style.display = 'none';
|
||||||
|
|
||||||
function applyInstanceSettings() {
|
function applyInstanceSettings() {
|
||||||
|
|||||||
+4
-4
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
|
|
||||||
<button class="tb-btn icon" id="fs-btn" title="Fullscreen — auto-fits the layout to the screen" aria-label="Fullscreen">
|
<button class="tb-btn icon" id="fs-btn" title="Fullscreen" aria-label="Fullscreen">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
<path d="M4 9V5a1 1 0 0 1 1-1h4"/><path d="M20 9V5a1 1 0 0 0-1-1h-4"/>
|
<path d="M4 9V5a1 1 0 0 1 1-1h4"/><path d="M20 9V5a1 1 0 0 0-1-1h-4"/>
|
||||||
<path d="M4 15v4a1 1 0 0 0 1 1h4"/><path d="M20 15v4a1 1 0 0 1-1 1h-4"/>
|
<path d="M4 15v4a1 1 0 0 0 1 1h4"/><path d="M20 15v4a1 1 0 0 1-1 1h-4"/>
|
||||||
@@ -27,14 +27,14 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="dropdown" id="add-dd">
|
<div class="dropdown" id="add-dd">
|
||||||
<button class="tb-btn icon" id="add-btn" title="Add a module tile" aria-label="Add tile">
|
<button class="tb-btn icon" id="add-btn" title="Add tile" aria-label="Add tile">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="menu" id="add-menu" hidden></div>
|
<div class="menu" id="add-menu" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dropdown" id="layout-dd">
|
<div class="dropdown" id="layout-dd">
|
||||||
<button class="tb-btn icon" id="layout-btn" title="Save, load or reset layouts" aria-label="Layouts">
|
<button class="tb-btn icon" id="layout-btn" title="Layouts" aria-label="Layouts">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
<rect x="3" y="3" width="8" height="8" rx="1.5"/>
|
<rect x="3" y="3" width="8" height="8" rx="1.5"/>
|
||||||
<rect x="3" y="13" width="8" height="8" rx="1.5"/>
|
<rect x="3" y="13" width="8" height="8" rx="1.5"/>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<div class="menu" id="layout-menu" hidden></div>
|
<div class="menu" id="layout-menu" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a class="tb-btn icon" id="admin-link" href="/admin/" title="Admin — configure modules (server-wide)" aria-label="Admin">
|
<a class="tb-btn icon" id="admin-link" href="/admin/" title="Admin" aria-label="Admin">
|
||||||
<svg width="14" height="14" viewBox="0 0 90 90" fill="currentColor" aria-hidden="true"><path d="M 82.94 32.538 c -2.685 0 -5.093 -1.545 -6.327 -3.929 c -0.245 -0.476 -0.504 -0.948 -0.774 -1.416 c -0.27 -0.468 -0.55 -0.928 -0.84 -1.378 c -1.448 -2.26 -1.582 -5.117 -0.239 -7.443 c 1.951 -3.378 0.792 -7.696 -2.584 -9.645 l -9.352 -5.4 c -3.378 -1.951 -7.696 -0.792 -9.645 2.584 l -0.002 0.003 c -1.322 2.291 -3.815 3.641 -6.456 3.512 c -1.149 -0.056 -2.301 -0.057 -3.455 0 c -2.638 0.128 -5.124 -1.226 -6.444 -3.513 l -0.002 -0.002 c -1.951 -3.378 -6.269 -4.535 -9.645 -2.584 l -9.352 5.4 c -3.378 1.951 -4.535 6.269 -2.584 9.645 l 0.002 0.002 c 1.321 2.286 1.25 5.117 -0.18 7.336 c -0.625 0.972 -1.201 1.97 -1.726 2.993 c -1.209 2.353 -3.625 3.836 -6.269 3.836 H 7.063 c -3.9 0 -7.061 3.162 -7.061 7.061 v 10.801 c 0 3.9 3.162 7.061 7.061 7.061 l 0 0 c 2.685 0 5.093 1.545 6.327 3.929 c 0.245 0.475 0.504 0.948 0.774 1.416 c 0.27 0.468 0.55 0.928 0.84 1.378 c 1.448 2.26 1.582 5.117 0.239 7.443 c -1.951 3.378 -0.792 7.696 2.584 9.645 l 9.352 5.4 c 3.378 1.951 7.696 0.792 9.645 -2.584 l 0.002 -0.003 c 1.322 -2.291 3.815 -3.641 6.456 -3.512 c 1.149 0.056 2.301 0.057 3.455 0 c 2.638 -0.128 5.124 1.226 6.444 3.513 l 0.002 0.002 c 1.951 3.378 6.268 4.535 9.645 2.584 l 9.352 -5.4 c 3.378 -1.949 4.535 -6.268 2.584 -9.645 l -0.002 -0.002 c -1.321 -2.286 -1.25 -5.117 0.18 -7.336 c 0.625 -0.972 1.201 -1.97 1.726 -2.993 c 1.209 -2.353 3.625 -3.836 6.269 -3.836 h 0.003 c 3.9 0 7.061 -3.162 7.061 -7.061 V 39.599 C 90.001 35.7 86.84 32.538 82.94 32.538 z M 54.998 62.318 c -9.565 5.523 -21.796 2.245 -27.317 -7.32 c -5.523 -9.565 -2.245 -21.796 7.32 -27.317 s 21.796 -2.245 27.317 7.32 C 67.841 44.566 64.563 56.797 54.998 62.318 z"/></svg>
|
<svg width="14" height="14" viewBox="0 0 90 90" fill="currentColor" aria-hidden="true"><path d="M 82.94 32.538 c -2.685 0 -5.093 -1.545 -6.327 -3.929 c -0.245 -0.476 -0.504 -0.948 -0.774 -1.416 c -0.27 -0.468 -0.55 -0.928 -0.84 -1.378 c -1.448 -2.26 -1.582 -5.117 -0.239 -7.443 c 1.951 -3.378 0.792 -7.696 -2.584 -9.645 l -9.352 -5.4 c -3.378 -1.951 -7.696 -0.792 -9.645 2.584 l -0.002 0.003 c -1.322 2.291 -3.815 3.641 -6.456 3.512 c -1.149 -0.056 -2.301 -0.057 -3.455 0 c -2.638 0.128 -5.124 -1.226 -6.444 -3.513 l -0.002 -0.002 c -1.951 -3.378 -6.269 -4.535 -9.645 -2.584 l -9.352 5.4 c -3.378 1.951 -4.535 6.269 -2.584 9.645 l 0.002 0.002 c 1.321 2.286 1.25 5.117 -0.18 7.336 c -0.625 0.972 -1.201 1.97 -1.726 2.993 c -1.209 2.353 -3.625 3.836 -6.269 3.836 H 7.063 c -3.9 0 -7.061 3.162 -7.061 7.061 v 10.801 c 0 3.9 3.162 7.061 7.061 7.061 l 0 0 c 2.685 0 5.093 1.545 6.327 3.929 c 0.245 0.475 0.504 0.948 0.774 1.416 c 0.27 0.468 0.55 0.928 0.84 1.378 c 1.448 2.26 1.582 5.117 0.239 7.443 c -1.951 3.378 -0.792 7.696 2.584 9.645 l 9.352 5.4 c 3.378 1.951 7.696 0.792 9.645 -2.584 l 0.002 -0.003 c 1.322 -2.291 3.815 -3.641 6.456 -3.512 c 1.149 0.056 2.301 0.057 3.455 0 c 2.638 -0.128 5.124 1.226 6.444 3.513 l 0.002 0.002 c 1.951 3.378 6.268 4.535 9.645 2.584 l 9.352 -5.4 c 3.378 -1.949 4.535 -6.268 2.584 -9.645 l -0.002 -0.002 c -1.321 -2.286 -1.25 -5.117 0.18 -7.336 c 0.625 -0.972 1.201 -1.97 1.726 -2.993 c 1.209 -2.353 3.625 -3.836 6.269 -3.836 h 0.003 c 3.9 0 7.061 -3.162 7.061 -7.061 V 39.599 C 90.001 35.7 86.84 32.538 82.94 32.538 z M 54.998 62.318 c -9.565 5.523 -21.796 2.245 -27.317 -7.32 c -5.523 -9.565 -2.245 -21.796 7.32 -27.317 s 21.796 -2.245 27.317 7.32 C 67.841 44.566 64.563 56.797 54.998 62.318 z"/></svg>
|
||||||
</a>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
+68
-2
@@ -283,7 +283,7 @@ function buildTile(tile) {
|
|||||||
if (hasSettings) {
|
if (hasSettings) {
|
||||||
const gear = document.createElement('button');
|
const gear = document.createElement('button');
|
||||||
gear.className = 'tile-btn';
|
gear.className = 'tile-btn';
|
||||||
gear.title = 'Tile settings (this tile only)';
|
gear.title = 'Settings';
|
||||||
gear.innerHTML = GEAR_SVG;
|
gear.innerHTML = GEAR_SVG;
|
||||||
gear.addEventListener('click', (e) => {
|
gear.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -294,7 +294,7 @@ function buildTile(tile) {
|
|||||||
|
|
||||||
const close = document.createElement('button');
|
const close = document.createElement('button');
|
||||||
close.className = 'tile-btn close';
|
close.className = 'tile-btn close';
|
||||||
close.title = 'Remove this tile';
|
close.title = 'Remove';
|
||||||
close.innerHTML = CLOSE_SVG; // a drawn cross, the same size in every browser (the ✕ glyph varies)
|
close.innerHTML = CLOSE_SVG; // a drawn cross, the same size in every browser (the ✕ glyph varies)
|
||||||
close.addEventListener('click', (e) => {
|
close.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -1106,6 +1106,72 @@ function tileSchema(tile, man) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── tooltips: at once on hover, short, one for the whole page ──────────
|
||||||
|
Every header and top-bar button's `title` becomes a tip the shell shows
|
||||||
|
the moment the pointer arrives — the browser's own waits about a second
|
||||||
|
and looks different in each browser. The text moves from `title` to
|
||||||
|
data-tip so the native one never appears; a module that sets `title`
|
||||||
|
again later is picked up on the next hover. */
|
||||||
|
let tipEl = null;
|
||||||
|
const TIP_TARGETS = '.tile-btn, .tb-btn, [data-tip]';
|
||||||
|
|
||||||
|
function tipTextFor(el) {
|
||||||
|
if (el.title) {
|
||||||
|
el.dataset.tip = el.title;
|
||||||
|
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', el.title);
|
||||||
|
el.removeAttribute('title');
|
||||||
|
}
|
||||||
|
return el.dataset.tip || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTip(el) {
|
||||||
|
const text = tipTextFor(el);
|
||||||
|
if (!text) return;
|
||||||
|
if (!tipEl) {
|
||||||
|
tipEl = document.createElement('div');
|
||||||
|
tipEl.id = 'tip';
|
||||||
|
tipEl.setAttribute('role', 'tooltip');
|
||||||
|
document.body.appendChild(tipEl);
|
||||||
|
}
|
||||||
|
tipEl.textContent = text;
|
||||||
|
tipEl.hidden = false;
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
const w = tipEl.offsetWidth;
|
||||||
|
const h = tipEl.offsetHeight;
|
||||||
|
const left = Math.max(6, Math.min(r.left + r.width / 2 - w / 2, window.innerWidth - w - 6));
|
||||||
|
let top = r.bottom + 6;
|
||||||
|
if (top + h > window.innerHeight - 6) top = r.top - h - 6;
|
||||||
|
tipEl.style.left = `${Math.round(left)}px`;
|
||||||
|
tipEl.style.top = `${Math.round(top)}px`;
|
||||||
|
tipEl._for = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideTip(el = null) {
|
||||||
|
if (!tipEl || tipEl.hidden) return;
|
||||||
|
if (el && tipEl._for !== el) return;
|
||||||
|
tipEl.hidden = true;
|
||||||
|
tipEl._for = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('pointerover', (e) => {
|
||||||
|
const el = e.target?.closest?.(TIP_TARGETS);
|
||||||
|
if (!el || (tipEl && tipEl._for === el && !tipEl.hidden)) return;
|
||||||
|
showTip(el);
|
||||||
|
});
|
||||||
|
document.addEventListener('pointerout', (e) => {
|
||||||
|
const el = e.target?.closest?.(TIP_TARGETS);
|
||||||
|
if (!el || (e.relatedTarget && el.contains(e.relatedTarget))) return;
|
||||||
|
hideTip(el);
|
||||||
|
});
|
||||||
|
document.addEventListener('pointerdown', () => hideTip(), true);
|
||||||
|
document.addEventListener('focusin', (e) => {
|
||||||
|
const el = e.target?.closest?.(TIP_TARGETS);
|
||||||
|
if (el) showTip(el);
|
||||||
|
});
|
||||||
|
document.addEventListener('focusout', () => hideTip());
|
||||||
|
window.addEventListener('scroll', () => hideTip(), true);
|
||||||
|
window.addEventListener('resize', () => hideTip());
|
||||||
|
|
||||||
function closeSettingsPopovers() {
|
function closeSettingsPopovers() {
|
||||||
document.querySelectorAll('.tile-settings').forEach((p) => {
|
document.querySelectorAll('.tile-settings').forEach((p) => {
|
||||||
p._cleanup?.();
|
p._cleanup?.();
|
||||||
|
|||||||
@@ -308,6 +308,24 @@ body.menu-hidden #topbar {
|
|||||||
.tile-btn.active { color: var(--accent); }
|
.tile-btn.active { color: var(--accent); }
|
||||||
.tile-btn.is-open,
|
.tile-btn.is-open,
|
||||||
.tile-btn.is-open:hover { background: var(--accent); color: var(--accent-text); }
|
.tile-btn.is-open:hover { background: var(--accent); color: var(--accent-text); }
|
||||||
|
|
||||||
|
/* the shell's tooltip: shown at once under the button the pointer is on */
|
||||||
|
#tip {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 200;
|
||||||
|
pointer-events: none;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
#tip[hidden] { display: none; }
|
||||||
.tile-btn svg { width: 13px; height: 13px; display: block; }
|
.tile-btn svg { width: 13px; height: 13px; display: block; }
|
||||||
|
|
||||||
/* controls a module adds to its own title bar (moduleApi.header) */
|
/* controls a module adds to its own title bar (moduleApi.header) */
|
||||||
|
|||||||
Reference in New Issue
Block a user