ucode-agent 1.26.2 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Light and dark, remembered, and right on the first paint.
3
+ *
4
+ * Two things a hand-written toggle misses: it ignores the setting the
5
+ * operating system already has, so a daylight user gets a dark page and has
6
+ * to fix it every visit; and it applies the saved choice after the stylesheet
7
+ * has painted, so the page flashes the wrong colours on every load.
8
+ *
9
+ * import { ThemeToggle, applyTheme } from './blocks/theme-toggle.js';
10
+ *
11
+ * applyTheme(); // first line of app.js
12
+ * header.append(ThemeToggle().el);
13
+ *
14
+ * The starter's stylesheet is the dark one. This adds the light values as an
15
+ * override on [data-theme="light"], so nothing already written has to change.
16
+ */
17
+
18
+ const CSS = `
19
+ :root[data-theme="light"] {
20
+ --bg: #f7f8fa;
21
+ --surface: #ffffff;
22
+ --line: #e3e6ec;
23
+ --text: #171a1f;
24
+ --muted: #626a78;
25
+ --accent: #0d9488;
26
+ --accent-ink: #ffffff;
27
+ --bad: #c2405a;
28
+ --bad-ink: #ffffff;
29
+ }
30
+ .tg {
31
+ display: inline-grid; place-items: center;
32
+ width: 40px; height: 40px;
33
+ font-size: 16px; line-height: 1;
34
+ color: var(--muted, #8b93a3); background: var(--surface, #14171c);
35
+ border: 1px solid var(--line, #232830); border-radius: 999px; cursor: pointer;
36
+ transition: color var(--fast, 160ms), border-color var(--fast, 160ms);
37
+ }
38
+ .tg:hover { color: var(--text, #e7eaf0); border-color: var(--accent, #2dd4bf); }
39
+ .tg:focus-visible { outline: 2px solid var(--accent, #2dd4bf); outline-offset: 2px; }
40
+ @media (prefers-reduced-motion: reduce) { .tg { transition: none; } }
41
+ `;
42
+
43
+ const KEY = 'theme';
44
+
45
+ let styled = false;
46
+ function styles() {
47
+ if (styled) return;
48
+ styled = true;
49
+ document.head.append(Object.assign(document.createElement('style'), { textContent: CSS }));
50
+ }
51
+
52
+ const saved = () => {
53
+ try { return localStorage.getItem(KEY); } catch { return null; }
54
+ };
55
+
56
+ const systemPrefersLight = () =>
57
+ typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: light)').matches;
58
+
59
+ /** What the page should be showing right now. */
60
+ export function currentTheme() {
61
+ return saved() ?? (systemPrefersLight() ? 'light' : 'dark');
62
+ }
63
+
64
+ /**
65
+ * Put the theme on <html>. Call this once, as early as possible — before the
66
+ * first render, not after it.
67
+ */
68
+ export function applyTheme(theme = currentTheme()) {
69
+ styles();
70
+ document.documentElement.dataset.theme = theme;
71
+ document.documentElement.style.colorScheme = theme;
72
+ return theme;
73
+ }
74
+
75
+ export function ThemeToggle({ onChange = () => {} } = {}) {
76
+ styles();
77
+ let theme = applyTheme();
78
+
79
+ const el = document.createElement('button');
80
+ el.type = 'button';
81
+ el.className = 'tg';
82
+
83
+ // Drawn rather than typed: a moon from the dingbat block is a different
84
+ // shape in every font, and missing altogether in some.
85
+ const SUN = '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M19.1 4.9l-1.4 1.4M6.3 17.7l-1.4 1.4"/>';
86
+ const MOON = '<path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5z"/>';
87
+
88
+ function paint() {
89
+ el.innerHTML =
90
+ `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" ` +
91
+ `stroke-width="1.7" stroke-linecap="round" aria-hidden="true">${theme === 'light' ? SUN : MOON}</svg>`;
92
+ el.setAttribute('aria-label', theme === 'light' ? 'Switch to dark' : 'Switch to light');
93
+ el.setAttribute('aria-pressed', String(theme === 'dark'));
94
+ }
95
+
96
+ el.addEventListener('click', () => {
97
+ theme = theme === 'light' ? 'dark' : 'light';
98
+ try { localStorage.setItem(KEY, theme); } catch { /* private mode: this visit only */ }
99
+ applyTheme(theme);
100
+ paint();
101
+ onChange(theme);
102
+ });
103
+
104
+ // Someone who has never chosen follows the system, including when it
105
+ // changes at sunset while the page is open.
106
+ if (typeof matchMedia === 'function') {
107
+ matchMedia('(prefers-color-scheme: light)').addEventListener?.('change', () => {
108
+ if (saved()) return;
109
+ theme = applyTheme(currentTheme());
110
+ paint();
111
+ });
112
+ }
113
+
114
+ paint();
115
+ return { el, get theme() { return theme; } };
116
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * A short message that appears, is read out, and goes away.
3
+ *
4
+ * The version everyone writes is a div that fades in and is invisible to a
5
+ * screen reader, so the one confirmation the app gives is the one thing a
6
+ * blind user never hears. This one lives in a polite live region, stacks,
7
+ * caps itself so a loop cannot bury the page, and holds still if the reader
8
+ * has asked for less motion.
9
+ *
10
+ * import { toast } from './blocks/toast.js';
11
+ *
12
+ * toast('Saved');
13
+ * toast.error('Could not reach the server');
14
+ * toast('Deleted', { action: { label: 'Undo', onClick: undo } });
15
+ */
16
+
17
+ const CSS = `
18
+ .tw {
19
+ position: fixed; inset: auto 0 0 0; z-index: 60;
20
+ display: flex; flex-direction: column-reverse; align-items: center; gap: var(--s1, 8px);
21
+ padding: var(--s3, 16px); pointer-events: none;
22
+ }
23
+ .tt {
24
+ display: flex; align-items: center; gap: var(--s2, 12px);
25
+ max-width: min(420px, calc(100vw - 32px));
26
+ padding: 12px var(--s3, 16px);
27
+ color: var(--text, #e7eaf0); background: var(--surface, #14171c);
28
+ border: 1px solid var(--line, #232830); border-radius: var(--r, 16px);
29
+ box-shadow: 0 12px 32px rgb(0 0 0 / .45);
30
+ pointer-events: auto;
31
+ animation: tt-in 180ms cubic-bezier(.2,0,.2,1);
32
+ }
33
+ .tt[data-tone="error"] { border-color: var(--bad, #ff8b95); }
34
+ .tt[data-tone="error"] .tt-dot { background: var(--bad, #ff8b95); }
35
+ .tt-dot { width: 8px; height: 8px; flex: none; border-radius: 999px; background: var(--accent, #2dd4bf); }
36
+ .tt-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
37
+ .tt-action {
38
+ flex: none; padding: 4px 10px;
39
+ font: 500 inherit; color: var(--accent, #2dd4bf);
40
+ background: none; border: 1px solid var(--line, #232830); border-radius: 999px; cursor: pointer;
41
+ }
42
+ .tt-action:hover { background: var(--bg, #0b0d10); }
43
+ .tt-action:focus-visible { outline: 2px solid var(--accent, #2dd4bf); outline-offset: 2px; }
44
+ @keyframes tt-in { from { opacity: 0; transform: translateY(8px); } }
45
+ @media (prefers-reduced-motion: reduce) { .tt { animation: none; } }
46
+ `;
47
+
48
+ const MAX = 3;
49
+ const LIFE = 4000;
50
+
51
+ let wrap = null;
52
+
53
+ function region() {
54
+ if (wrap) return wrap;
55
+ document.head.append(Object.assign(document.createElement('style'), { textContent: CSS }));
56
+ wrap = document.createElement('div');
57
+ wrap.className = 'tw';
58
+ // polite, not assertive: a confirmation should wait its turn rather than
59
+ // interrupt whatever the reader is in the middle of.
60
+ wrap.setAttribute('role', 'status');
61
+ wrap.setAttribute('aria-live', 'polite');
62
+ document.body.append(wrap);
63
+ return wrap;
64
+ }
65
+
66
+ function show(text, { tone = 'default', action = null, duration = LIFE } = {}) {
67
+ const host = region();
68
+
69
+ const el = document.createElement('div');
70
+ el.className = 'tt';
71
+ el.dataset.tone = tone;
72
+
73
+ const dot = document.createElement('span');
74
+ dot.className = 'tt-dot';
75
+
76
+ const label = document.createElement('span');
77
+ label.className = 'tt-text';
78
+ label.textContent = text;
79
+
80
+ el.append(dot, label);
81
+
82
+ let timer = null;
83
+ const dismiss = () => { clearTimeout(timer); el.remove(); };
84
+
85
+ if (action) {
86
+ const button = document.createElement('button');
87
+ button.type = 'button';
88
+ button.className = 'tt-action';
89
+ button.textContent = action.label;
90
+ button.addEventListener('click', () => { dismiss(); action.onClick?.(); });
91
+ el.append(button);
92
+ }
93
+
94
+ host.append(el);
95
+ while (host.children.length > MAX) host.firstElementChild.remove();
96
+
97
+ // A toast the pointer is resting on is a toast being read.
98
+ el.addEventListener('pointerenter', () => clearTimeout(timer));
99
+ el.addEventListener('pointerleave', () => { timer = setTimeout(dismiss, 1200); });
100
+ timer = setTimeout(dismiss, duration);
101
+
102
+ return dismiss;
103
+ }
104
+
105
+ export const toast = Object.assign(show, {
106
+ error: (text, options = {}) => show(text, { ...options, tone: 'error', duration: options.duration ?? 6000 }),
107
+ });
@@ -7,6 +7,10 @@
7
7
  --text: #e7eaf0;
8
8
  --muted: #8b93a3;
9
9
  --accent: #2dd4bf;
10
+ --accent-ink: #04110f;
11
+ --good: #4ade80;
12
+ --bad: #ff8b95;
13
+ --bad-ink: #2a0b0e;
10
14
 
11
15
  --r: 16px;
12
16
  --s1: 8px;