From 3b2545cc43cb7f4295584ae722d85d37403a4f6e Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Sat, 19 Sep 2026 10:14:36 -0400 Subject: [PATCH] Docs: add the interface icon set to the design system --- public/js/admin-app-form.js | 12 +- public/js/admin-auth.js | 4 +- public/js/admin-color-control.js | 21 +- public/js/admin-drag.js | 2 +- public/js/admin-list.js | 26 +- public/js/admin-settings.js | 4 +- public/js/admin-shared.js | 6 +- public/js/admin-widget-form.js | 22 +- public/js/admin.js | 33 +-- public/js/dashboard.js | 12 +- public/js/icon-set.js | 258 ++++++++++++++++++ public/js/listbox.js | 6 +- public/js/settings-icon.js | 2 +- public/js/spotlight.js | 2 +- public/js/ui.js | 2 +- public/js/utils.js | 4 +- public/js/widget-config-form.js | 8 +- public/js/widget-glyphs.js | 47 ++-- public/widgets/system-summary/index.html | 5 +- src/components/IconSet.astro | 74 +++++ .../docs/docs/contributing/design-system.mdx | 39 ++- .../docs/docs/create-a-widget/manifest.md | 18 +- 22 files changed, 473 insertions(+), 134 deletions(-) create mode 100644 public/js/icon-set.js create mode 100644 src/components/IconSet.astro diff --git a/public/js/admin-app-form.js b/public/js/admin-app-form.js index a16f367..311ae52 100644 --- a/public/js/admin-app-form.js +++ b/public/js/admin-app-form.js @@ -1,4 +1,4 @@ -import { clr as rc, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=5d2b6f16'; +import { clr as rc, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=045df327'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { loadLocalIcons, resolveIcon, iconChain, cdnIconRef, splitIconRef } from '/js/icons.js?v=9c8c550c'; import { state } from '/js/admin-state.js?v=5a5d655f'; @@ -21,12 +21,13 @@ import { reveal, setTogDisabled, swapContent, -} from '/js/admin-shared.js?v=69f5c7f9'; -import { createListbox } from '/js/listbox.js?v=2b633751'; +} from '/js/admin-shared.js?v=dc0e02b6'; +import { createListbox } from '/js/listbox.js?v=96066369'; import { MAX_LABELS } from '/js/badge-logic.js?v=b3c8b6c2'; -import { renderColorControl, BADGE_SWATCHES, BADGE_DEFAULT } from '/js/admin-color-control.js?v=d0c89032'; +import { renderColorControl, BADGE_SWATCHES, BADGE_DEFAULT } from '/js/admin-color-control.js?v=a48fbc58'; import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=10f3cdb1'; import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86'; +import { iconSvg } from '/js/icon-set.js?v=606a68c6'; export function buildFolderForm(body, item) { const children = item?.children || []; @@ -437,8 +438,7 @@ function renderActLabels(host) { const _optRow = label => html`${label} ${t('app.optional')}`; -const GRIP_SVG = - ''; +const GRIP_SVG = iconSvg('grip', 14); function addActLabel() { captureActLabels(); diff --git a/public/js/admin-auth.js b/public/js/admin-auth.js index 6add72f..214d378 100644 --- a/public/js/admin-auth.js +++ b/public/js/admin-auth.js @@ -1,7 +1,7 @@ -import { ag, ap } from '/js/admin-shared.js?v=69f5c7f9'; +import { ag, ap } from '/js/admin-shared.js?v=dc0e02b6'; import { t } from '/js/i18n.js?v=1f1ea9c1'; import { pwStrength } from '/js/password-strength.js?v=42f45ac7'; -import { el, inp as inpById, qa } from '/js/utils.js?v=5d2b6f16'; +import { el, inp as inpById, qa } from '/js/utils.js?v=045df327'; export async function checkAuth(onLogin) { try { diff --git a/public/js/admin-color-control.js b/public/js/admin-color-control.js index 26ba049..d21159b 100644 --- a/public/js/admin-color-control.js +++ b/public/js/admin-color-control.js @@ -1,22 +1,19 @@ -import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=69f5c7f9'; +import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=dc0e02b6'; import { t } from '/js/i18n.js?v=1f1ea9c1'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { qa, q } from '/js/utils.js?v=5d2b6f16'; +import { qa, q } from '/js/utils.js?v=045df327'; +import { iconSvg } from '/js/icon-set.js?v=606a68c6'; const CC_SWATCHES = ['#1c1c1e', '#8e8e93', '#f2f2f7', '#ff393c', '#ffcd00', '#35c759', '#0289ff', '#cb30df']; export const BADGE_DEFAULT = '#1e6ef4'; export const BADGE_SWATCHES = ['#1c1c1e', '#8e8e93', '#f2f2f7', '#ff393c', '#ffcd00', '#35c759', '#1e6ef4', '#cb30df']; const _ccIco = { - hueLo: - '', - hueHi: - '', - satLo: - '', - satHi: - '', - brLo: '', - brHi: '', + hueLo: iconSvg('hue-lo', 16), + hueHi: iconSvg('hue-hi', 16), + satLo: iconSvg('sat-lo', 16), + satHi: iconSvg('sat-hi', 16), + brLo: iconSvg('bright-lo', 16), + brHi: iconSvg('bright-hi', 16), }; /* Normalise the hue before computing `x`. A negative hue otherwise mixes a wrapped sextant with an unwrapped `x` and yields a negative channel. diff --git a/public/js/admin-drag.js b/public/js/admin-drag.js index bc9d2d1..f8cf603 100644 --- a/public/js/admin-drag.js +++ b/public/js/admin-drag.js @@ -12,7 +12,7 @@ import { applyDrop, canJoinFolder, folderRowZone } from '/js/admin-drag-logic.js?v=6b767e76'; import { state } from '/js/admin-state.js?v=5a5d655f'; import { snapshotItems } from '/js/admin-save-logic.js?v=4f71ef6c'; -import { qa } from '/js/utils.js?v=5d2b6f16'; +import { qa } from '/js/utils.js?v=045df327'; /** @type {(before: unknown) => void} */ let _save = () => {}; diff --git a/public/js/admin-list.js b/public/js/admin-list.js index 3c08fe1..d2248c6 100644 --- a/public/js/admin-list.js +++ b/public/js/admin-list.js @@ -14,13 +14,14 @@ import { collapsedFolders, filter, state } from '/js/admin-state.js?v=5a5d655f'; import { snapshotItems } from '/js/admin-save-logic.js?v=4f71ef6c'; import { reorderItems } from '/js/admin-logic.js?v=e3673bd7'; -import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=dbf4e7e6'; -import { paintIcon } from '/js/admin-shared.js?v=69f5c7f9'; -import { clr as rc, el, qa, setUserText } from '/js/utils.js?v=5d2b6f16'; +import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=227e3153'; +import { paintIcon } from '/js/admin-shared.js?v=dc0e02b6'; +import { clr as rc, el, qa, setUserText } from '/js/utils.js?v=045df327'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { t } from '/js/i18n.js?v=1f1ea9c1'; -import { sizeLabel } from '/js/admin-widget-form.js?v=c4b11d2a'; -import { widgetGlyph } from '/js/widget-glyphs.js?v=12b0a947'; +import { sizeLabel } from '/js/admin-widget-form.js?v=3237dd3e'; +import { widgetGlyph } from '/js/widget-glyphs.js?v=3adb57ad'; +import { iconSvg } from '/js/icon-set.js?v=606a68c6'; /** @type {{ openModal: (idx: number|null) => void, openFolderPicker: (appId: string|null, targetFolderId?: string|null) => void, @@ -39,17 +40,12 @@ export function initList(page) { } /* Constant markup only. No user data reaches these. */ -const FOLDER_ICON = - ''; +const FOLDER_ICON = iconSvg('folder', 26); const SIZE_ICONS = { - small: - '', - medium: - '', - large: - '', - xlarge: - '', + small: iconSvg('small', 26), + medium: iconSvg('medium', 26), + large: iconSvg('large', 26), + xlarge: iconSvg('xlarge', 26), }; function svgNode(markup) { const t = document.createElement('template'); diff --git a/public/js/admin-settings.js b/public/js/admin-settings.js index 708635d..c797fb6 100644 --- a/public/js/admin-settings.js +++ b/public/js/admin-settings.js @@ -1,4 +1,4 @@ -import { toast, ag, ap, reveal, swapContent } from '/js/admin-shared.js?v=69f5c7f9'; +import { toast, ag, ap, reveal, swapContent } from '/js/admin-shared.js?v=dc0e02b6'; import { pwStrength } from '/js/password-strength.js?v=42f45ac7'; import { t } from '/js/i18n.js?v=1f1ea9c1'; import { @@ -9,7 +9,7 @@ import { BLOCK, } from '/js/admin-logic.js?v=e3673bd7'; import { confirmText } from '/js/modal.js?v=11fa1eff'; -import { el, inp, setUserText } from '/js/utils.js?v=5d2b6f16'; +import { el, inp, setUserText } from '/js/utils.js?v=045df327'; /* Mirrors the server's rule: auth cannot be switched on with no password. */ let _passwordSet = false; diff --git a/public/js/admin-shared.js b/public/js/admin-shared.js index 571ab88..bf59245 100644 --- a/public/js/admin-shared.js +++ b/public/js/admin-shared.js @@ -1,8 +1,9 @@ /* Stateless helpers shared by the admin modules. Mutable state stays out. */ import { recoversSession, toastHoldMs } from '/js/admin-logic.js?v=e3673bd7'; -import { el, q } from '/js/utils.js?v=5d2b6f16'; +import { el, q } from '/js/utils.js?v=045df327'; import { t } from '/js/i18n.js?v=1f1ea9c1'; import { iconChain } from '/js/icons.js?v=9c8c550c'; +import { iconSvg } from '/js/icon-set.js?v=606a68c6'; export const API = ''; @@ -199,8 +200,7 @@ export function swapContent(node, apply) { timer = setTimeout(end, 600); } -export const PE_SVG = - ''; +export const PE_SVG = iconSvg('edit', 17); /* `root` lets a caller wire a subtree that is not in the document yet. */ /** A function placeholder is resolved on use, not at wiring time: this runs diff --git a/public/js/admin-widget-form.js b/public/js/admin-widget-form.js index 1b6d1f8..c44ded2 100644 --- a/public/js/admin-widget-form.js +++ b/public/js/admin-widget-form.js @@ -1,22 +1,14 @@ import { state } from '/js/admin-state.js?v=5a5d655f'; -import { PE_SVG, initInlineEdit, swapContent } from '/js/admin-shared.js?v=69f5c7f9'; -import { createListbox } from '/js/listbox.js?v=2b633751'; -import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=253fcdcd'; +import { PE_SVG, initInlineEdit, swapContent } from '/js/admin-shared.js?v=dc0e02b6'; +import { createListbox } from '/js/listbox.js?v=96066369'; +import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=e998ed94'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { sizesForView, widgetConfigMode, rejectionLines, carriesTypedValues } from '/js/admin-logic.js?v=e3673bd7'; import { t } from '/js/i18n.js?v=1f1ea9c1'; -import { q, qi, qa } from '/js/utils.js?v=5d2b6f16'; +import { q, qi, qa } from '/js/utils.js?v=045df327'; +import { iconSvg } from '/js/icon-set.js?v=606a68c6'; -const SIZE_ICONS = { - small: - '', - medium: - '', - large: - '', - xlarge: - '', -}; +const SIZES_WITH_ICONS = new Set(['small', 'medium', 'large', 'xlarge']); const CUSTOM_SIZES = ['small', 'medium', 'large', 'xlarge']; function widgetSizes(type) { @@ -108,7 +100,7 @@ function _renderWidgetForm(body) { scard.className = 'grp'; setHtml( scard, - html`
${_sizeOpts.map(s => html``)}
`, + html`
${_sizeOpts.map(s => html``)}
`, ); body.appendChild(scard); qa('.tile-opt', scard).forEach(b => diff --git a/public/js/admin.js b/public/js/admin.js index ac0c351..7490754 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -1,6 +1,6 @@ -import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=7da1ff10'; -import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=e2646986'; -import { initList, render, syncFilterUI } from '/js/admin-list.js?v=c5d8bff8'; +import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=0b617803'; +import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=9833ce5d'; +import { initList, render, syncFilterUI } from '/js/admin-list.js?v=1c65c8ad'; import { resolveAdminSection } from '/js/admin-logic.js?v=e3673bd7'; import { buildAppItem, @@ -10,16 +10,17 @@ import { snapshotItems, upsertItem, } from '/js/admin-save-logic.js?v=4f71ef6c'; -import { loadSettings, settingsDirty, showBgFields, showWallpaperFile } from '/js/admin-settings.js?v=4d5e9919'; -import { ag, ap, initInlineEdit, paintIcon, reveal, setReauthHandler, toast } from '/js/admin-shared.js?v=69f5c7f9'; +import { loadSettings, settingsDirty, showBgFields, showWallpaperFile } from '/js/admin-settings.js?v=002dde46'; +import { ag, ap, initInlineEdit, paintIcon, reveal, setReauthHandler, toast } from '/js/admin-shared.js?v=dc0e02b6'; import { collapsedFolders, filter, state } from '/js/admin-state.js?v=5a5d655f'; -import { buildWidgetForm } from '/js/admin-widget-form.js?v=c4b11d2a'; +import { buildWidgetForm } from '/js/admin-widget-form.js?v=3237dd3e'; import { initFluidHover } from '/js/fluid-hover.js?v=cb886e86'; import { initGlideSelect, syncGlideSelect } from '/js/glide-select.js?v=8b39e9d0'; -import { createListbox } from '/js/listbox.js?v=2b633751'; +import { createListbox } from '/js/listbox.js?v=96066369'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { initI18n, LANGUAGES, t } from '/js/i18n.js?v=1f1ea9c1'; import { loadLocalIcons } from '/js/icons.js?v=9c8c550c'; +import { ensureSprite, iconSvg } from '/js/icon-set.js?v=606a68c6'; import { clearSkipTls, convert, @@ -32,11 +33,13 @@ import { import { isMobileLayout, onLayoutChange } from '/js/layout.js?v=e9f4b607'; import { confirmModal, confirmText, openModal as openDialog, promptModal } from '/js/modal.js?v=11fa1eff'; import { readMode, watchSystemTheme, writeMode } from '/js/theme.js?v=787bfdff'; -import { el, inp, q, qa, clr as rc, sanitizeCssUrl, setUserText, tgt } from '/js/utils.js?v=5d2b6f16'; -import { normalizeColorInput } from '/js/admin-color-control.js?v=d0c89032'; +import { el, inp, q, qa, clr as rc, sanitizeCssUrl, setUserText, tgt } from '/js/utils.js?v=045df327'; +import { normalizeColorInput } from '/js/admin-color-control.js?v=a48fbc58'; import { parseYamlTolerant, YamlLiteError } from '/js/yaml-lite.js?v=6ebb564c'; import { loadWallpaper, saveWallpaper } from '/js/wallpaper-cache.js?v=c5f8a3e6'; +ensureSprite(); + /* A class rather than a bare media query. Some phones report a wider CSS viewport than they have. The rule lives in layout.js, shared with the dashboard, so the two screens cannot disagree about what mobile means. */ @@ -190,13 +193,6 @@ function showEditView() { q('.cp')?.scrollTo?.(0, 0); } -const TYPE_ICONS = { - app: '', - widget: - '', - folder: - '', -}; /* Read at draw time. The catalog is not loaded when this module evaluates. */ const typeLabels = () => ({ app: t('type.app'), widget: t('type.widget'), folder: t('type.folder') }); @@ -216,10 +212,7 @@ function buildAddNewCard() { b.dataset.ctype = kind; b.setAttribute('aria-pressed', String(kind === state.ctype)); b.setAttribute('aria-label', t('type.addNew') + ': ' + label); - setHtml( - b, - html`${label}`, - ); + setHtml(b, html`${raw(iconSvg(kind, 38))}${label}`); b.onclick = () => { if (state.ctype === kind) return; state.ctype = kind; diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 00ba92b..463a913 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -23,9 +23,9 @@ import { setUserText, teardownWidgets, titleWhenTruncated, -} from '/js/utils.js?v=5d2b6f16'; +} from '/js/utils.js?v=045df327'; import { initFluidHover } from '/js/fluid-hover.js?v=cb886e86'; -import { initSpotlight } from '/js/spotlight.js?v=50bd3af5'; +import { initSpotlight } from '/js/spotlight.js?v=38fe61b4'; import { html, setHtml, raw } from '/js/html.js?v=c71f8903'; import { initI18n, t, currentLang } from '/js/i18n.js?v=1f1ea9c1'; import { pwStrength, passwordMismatch } from '/js/password-strength.js?v=42f45ac7'; @@ -39,7 +39,7 @@ import { buildMobile, resetMobileChrome, mkFolderGlyph, -} from '/js/ui.js?v=9317d147'; +} from '/js/ui.js?v=e28d57cd'; import { badgeMinimum, badgeSignature, computeBadgeVisual, readBadgeUpdate } from '/js/badge-logic.js?v=b3c8b6c2'; import { formatNumber } from '/js/format-number.js?v=4a5ccef4'; import { closeBadgePopover, wireBadgePopover } from '/js/badge-popover.js?v=aa52b1a3'; @@ -56,6 +56,9 @@ import { jitter } from '/js/jitter.js?v=4eeef4c9'; import { isMobileLayout, onLayoutChange } from '/js/layout.js?v=e9f4b607'; import { startWakeLock } from '/js/wake-lock.js?v=6b9591cf'; import { applyLabelTones, loadSamplingImage, sampleImage, toneForColor } from '/js/label-contrast.js?v=c1ac6fb8'; +import { ensureSprite, iconSvg } from '/js/icon-set.js?v=606a68c6'; + +ensureSprite(); /* Recomputed, never stored: the window can cross the breakpoint after load. */ let MOB = isMobileLayout(); @@ -665,8 +668,7 @@ async function pollHealth() { } } -const EYE = - ''; +const EYE = iconSvg('eye', 18); function showSetupPrompt() { return new Promise(resolve => { diff --git a/public/js/icon-set.js b/public/js/icon-set.js new file mode 100644 index 0000000..172afc7 --- /dev/null +++ b/public/js/icon-set.js @@ -0,0 +1,258 @@ +const T = { SW: 2, G: 2, R_LG: 4, R_MD: 3, R_SM: 2, BAR: 2, DOT: 2, NODE: 3, PAD: 3 }; + +const ROLE = { ink: 'i', faint: 'i3', accent: 'a', cut: 'c' }; +const rl = r => ROLE[r] || r; + +export const rect = (x, y, w, h, r, role = 'ink', o = {}) => ({ + k: 'area', + t: 'rect', + x, + y, + w, + h, + r, + role: rl(role), + ...o, +}); +export const detail = (x, y, w, h, r, role = 'ink', o = {}) => ({ + k: 'solid', + t: 'rect', + x, + y, + w, + h, + r, + role: rl(role), + ...o, +}); +export const bar = (x, y, w, role = 'cut') => detail(x, y, w, T.BAR, T.BAR / 2, role); +export const circle = (cx, cy, r, role = 'ink') => ({ k: 'area', t: 'circle', cx, cy, r, role: rl(role) }); +export const dot = (cx, cy, r, role = 'ink') => ({ k: 'solid', t: 'circle', cx, cy, r, role: rl(role) }); +export const shape = (d, role = 'ink', o = {}) => ({ k: 'area', t: 'path', d, role: rl(role), ...o }); +export const line = (d, role = 'ink') => ({ k: 'line', t: 'path', d, role: rl(role) }); +export const ring = (cx, cy, r, role = 'ink') => ({ k: 'line', t: 'circle', cx, cy, r, role: rl(role) }); + +export const STYLES = { + stroke: { line: true, accent: 'ink' }, + twotone: { line: true, accent: 'accent' }, + duotone: { line: true, accent: 'accent', accentFill: true }, + solid: { line: false, accent: 'accent' }, + bulk: { line: false, accent: 'ink', inkFaint: true }, +}; + +function colour(role, st) { + if (role === 'a') return st.accent === 'ink' ? 'i' : 'a'; + if (role === 'i3') return 'i3'; + return st.inkFaint ? 'i3' : 'i'; +} +function strokeAttrs(sharp, w = T.SW) { + return ` stroke-width="${w}" stroke-linecap="${sharp ? 'square' : 'round'}" stroke-linejoin="${sharp ? 'miter' : 'round'}"`; +} +function geom(p, sharp, inset) { + const tr = p.transform ? ` transform="${p.transform}"` : ''; + if (p.t === 'rect') { + const d = inset ? 1 : 0; + const r = sharp ? 0 : Math.max(0, p.r - d); + return ``; + if (p.rj) return `${geom(p, sharp, false)} class="sy-${cls} sy-${cls}s"${strokeAttrs(sharp)}/>`; + return `${geom(p, sharp, false)} class="sy-${cls}"/>`; +} +function hole(p, sharp, grow) { + if (p.k === 'line') + return `${geom(p, sharp, false)} fill="none" stroke="#000"${strokeAttrs(sharp, T.SW + 2 * grow)}/>`; + return `${geom(p, sharp, false)} fill="#000"${grow ? ` stroke="#000"${strokeAttrs(sharp, 2 * grow)}` : ''}/>`; +} +function mask(id, content) { + return `${content}`; +} + +export function symbol(id, ic, styleId = 'solid', sharp = false) { + const st = STYLES[styleId]; + const gap = ic.gap ?? T.G; + const sid = `sy-${styleId}${sharp ? '-sharp' : ''}-${id}`; + const shapes = ic.shapes.map(p => (st.line && p.lineAlt ? { ...p, ...p.lineAlt } : p)); + const ink = shapes.filter(p => p.role !== 'a' && (st.line || p.role !== 'c')); + const acc = shapes.filter(p => p.role === 'a'); + let holes = st.line + ? '' + : shapes + .filter(p => p.role === 'c') + .map(p => hole(p, sharp, 0)) + .join(''); + if (ic.inkMask !== false) holes += acc.map(p => hole(p, sharp, gap)).join(''); + let defs = ''; + let inkAttr = ''; + let accAttr = ''; + if (holes) { + defs += mask(`${sid}-m`, holes); + inkAttr = ` mask="url(#${sid}-m)"`; + } + if (ic.accentCut) { + defs += mask(`${sid}-am`, ic.accentCut.map(p => hole(p, sharp, p.grow ?? 0)).join('')); + accAttr = ` mask="url(#${sid}-am)"`; + } + const body = `${ink.map(p => draw(p, st, sharp)).join('')}${acc.map(p => draw(p, st, sharp)).join('')}`; + return { defs, sym: `${body}` }; +} + +const knob = (x, y) => detail(x, y, 10, 4, 2, 'ink', { lineAlt: { x: x + 1, y: y + 1, w: 8, h: 2, r: 1 } }); +const TOGGLES = { + inkMask: false, + shapes: [rect(2, 3, 20, 8, 4, 'accent'), knob(10, 5), rect(2, 13, 20, 8, 4, 'faint'), knob(4, 15)], + accentCut: [detail(10, 5, 10, 4, 2)], +}; +const CLOUD = 'M6 20h10a4 4 0 0 0 0-8 5 5 0 0 0-9.6 1.2A3.5 3.5 0 0 0 6 20z'; + +function sizeIcon(k) { + const [x, y, w, h] = { small: [5, 5, 14, 14], medium: [2, 7, 20, 10], large: [3, 3, 18, 18], xlarge: [5, 2, 14, 20] }[ + k + ]; + const ix = x + T.PAD; + const iw = w - 2 * T.PAD; + const shapes = [rect(x, y, w, h, T.R_LG)]; + if (k === 'medium') { + const cy = y + (h - T.BAR) / 2; + shapes.push(bar(ix, cy, 6, 'accent'), bar(ix + 6 + T.G, cy, iw - 6 - T.G)); + return { gap: 0, shapes }; + } + const rows = Math.floor((h - 2 * T.PAD + T.G) / (T.BAR + T.G)); + shapes.push(bar(ix, y + T.PAD, Math.min(8, iw - 2), 'accent')); + for (let i = 1; i < rows; i++) + shapes.push(bar(ix, y + T.PAD + i * (T.BAR + T.G), i === rows - 1 && rows > 2 ? iw - 4 : iw)); + return { gap: 0, shapes }; +} + +export const ICONS = { + general: TOGGLES, + appearance: { + inkMask: false, + shapes: [shape('M11 2.05A10 10 0 0 0 11 21.95z'), shape('M13 2.05A10 10 0 0 1 13 21.95z', 'accent')], + }, + dashboard: { + inkMask: false, + shapes: [rect(3, 3, 18, 8, T.R_MD, 'accent'), rect(3, 13, 8, 8, T.R_MD), rect(13, 13, 8, 8, T.R_MD)], + }, + + app: { shapes: [rect(3, 5, 16, 16, T.R_LG), dot(19, 5, T.NODE, 'accent')] }, + folder: { + gap: 0, + shapes: [ + rect(3, 3, 18, 18, T.R_LG), + detail(6, 6, 5, 5, T.R_SM, 'accent'), + detail(13, 6, 5, 5, T.R_SM, 'cut'), + detail(6, 13, 5, 5, T.R_SM, 'cut'), + detail(13, 13, 5, 5, T.R_SM, 'cut'), + ], + }, + widget: { gap: 0, shapes: [rect(2, 4, 20, 16, T.R_LG), bar(5, 7, 8, 'accent'), bar(5, 11, 14), bar(5, 15, 10)] }, + + small: sizeIcon('small'), + medium: sizeIcon('medium'), + large: sizeIcon('large'), + xlarge: sizeIcon('xlarge'), + + backup: { shapes: [line('M5 12a7 7 0 1 0 2-5L5 9'), line('M12 8v4l3 2'), line('M5 4v5h5', 'accent')] }, + books: { + inkMask: false, + shapes: [ + rect(3, 6, 4, 15, T.R_SM), + rect(9, 4, 4, 17, T.R_SM, 'accent'), + rect(15, 6, 4, 15, T.R_SM, 'ink', { transform: 'rotate(-15 15 21)' }), + ], + }, + clock: { + inkMask: false, + shapes: [ring(12, 12, 9), line('M12 12l4 2'), line('M12 12V7', 'accent'), dot(12, 12, T.DOT, 'accent')], + }, + connections: { + shapes: [ + line('M12 5 5 19M12 5l7 14M5 19h14'), + dot(5, 19, T.NODE), + dot(19, 19, T.NODE), + dot(12, 5, T.NODE, 'accent'), + ], + }, + switcher: { + inkMask: false, + shapes: [detail(2, 7, 3, 10, 1, 'faint'), detail(19, 7, 3, 10, 1, 'faint'), rect(7, 4, 10, 16, T.R_MD, 'accent')], + }, + disk: { inkMask: false, shapes: [shape('M8 4h8l4 8H4z'), rect(2, 14, 20, 6, T.R_SM), dot(17, 17, T.DOT, 'accent')] }, + dns: { + inkMask: false, + shapes: [detail(2, 5, 4, 14, T.R_SM), line('M9 12c5 0 6-6 12-6M9 12h12'), line('M9 12c5 0 6 6 12 6', 'accent')], + }, + github: { + inkMask: false, + shapes: [ + line('M7 5v14'), + dot(7, 5, T.NODE), + dot(7, 19, T.NODE), + line('M7 10c0 4 4 4 10 4', 'accent'), + dot(17, 14, T.NODE, 'accent'), + ], + }, + nowplaying: { shapes: [rect(3, 3, 18, 18, T.R_LG), shape('M10 8v8l6-4z', 'accent', { rj: true })] }, + system: { shapes: [line('M3 16 7 12 11 15 16 8'), dot(19, 6, T.NODE, 'accent')] }, + weather: { + inkMask: false, + shapes: [shape(CLOUD), circle(17, 8, 4, 'accent')], + accentCut: [{ ...shape(CLOUD), grow: T.G }], + }, + + edit: { shapes: [shape('M14 5l5 5L9 20H4v-5z'), line('M12 7l5 5', 'cut')] }, + chevrons: { shapes: [line('M8 9l4-4 4 4'), line('M8 15l4 4 4-4')] }, + back: { shapes: [line('M15 5l-7 7 7 7')] }, + up: { shapes: [line('M6 15l6-6 6 6')] }, + 'hue-lo': { shapes: [line('M12 3c3 4 6 7.5 6 11a6 6 0 0 1-12 0c0-3.5 3-7 6-11z')] }, + 'hue-hi': { shapes: [shape('M12 3c3 4 6 7.5 6 11a6 6 0 0 1-12 0c0-3.5 3-7 6-11z')] }, + 'sat-lo': { shapes: [ring(12, 12, 8)] }, + 'sat-hi': { shapes: [ring(12, 12, 8), shape('M12 4a8 8 0 0 1 0 16z')] }, + 'bright-lo': { shapes: [dot(12, 12, 3), line('M12 5v1M12 18v1M5 12h1M18 12h1')] }, + 'bright-hi': { shapes: [dot(12, 12, 4), line('M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1 1M18 18l1 1M5 19l1-1M18 6l1-1')] }, + external: { shapes: [line('M14 4h6v6M20 4l-9 9'), line('M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4')] }, + docs: { shapes: [rect(4, 2, 16, 20, T.R_MD), bar(7, 7, 6), bar(7, 11, 10), bar(7, 15, 8)] }, + issue: { shapes: [circle(12, 12, 10), line('M12 7v6', 'cut'), detail(11, 16, 2, 2, 1, 'cut')] }, + info: { shapes: [circle(12, 12, 10), line('M12 11v6', 'cut'), detail(11, 6, 2, 2, 1, 'cut')] }, + heart: { shapes: [shape('M12 21l-8-8a5 5 0 0 1 8-6 5 5 0 0 1 8 6z')] }, + grip: { + shapes: [ + dot(9, 6, T.DOT), + dot(15, 6, T.DOT), + dot(9, 12, T.DOT), + dot(15, 12, T.DOT), + dot(9, 18, T.DOT), + dot(15, 18, T.DOT), + ], + }, + eye: { shapes: [line('M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7z'), dot(12, 12, 3)] }, + search: { shapes: [ring(10, 10, 7), line('M15 15l6 6')] }, + 'arrow-down': { shapes: [line('M12 4v15M6 13l6 6 6-6')] }, + 'arrow-up': { shapes: [line('M12 20V5M6 11l6-6 6 6')] }, +}; + +export function iconSvg(id, px = 24, variant = 'solid', cls = '') { + return ``; +} + +export function ensureSprite(doc = document, variant = 'solid') { + if (doc.getElementById('sy-sprite')) return; + let defs = ''; + let syms = ''; + for (const [id, ic] of Object.entries(ICONS)) { + const s = symbol(id, ic, variant); + defs += s.defs; + syms += s.sym; + } + const src = ``; + const svg = new DOMParser().parseFromString(src, 'image/svg+xml').documentElement; + doc.body.prepend(doc.importNode(svg, true)); +} diff --git a/public/js/listbox.js b/public/js/listbox.js index 3dfe163..aeaa4a8 100644 --- a/public/js/listbox.js +++ b/public/js/listbox.js @@ -9,10 +9,10 @@ import { nextActiveIndex } from '/js/admin-logic.js?v=e3673bd7'; import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { qa } from '/js/utils.js?v=5d2b6f16'; +import { qa } from '/js/utils.js?v=045df327'; +import { iconSvg } from '/js/icon-set.js?v=606a68c6'; -const CHEV = - ''; +const CHEV = iconSvg('chevrons', 22, 'solid', 'dd-chev'); const TYPEAHEAD_MS = 800; diff --git a/public/js/settings-icon.js b/public/js/settings-icon.js index f49f776..f5c5d11 100644 --- a/public/js/settings-icon.js +++ b/public/js/settings-icon.js @@ -3,4 +3,4 @@ the same name left there would shadow the bundled artwork. */ export const SETTINGS_ICON = - 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAyNHB0IiBoZWlnaHQ9IjEwMjRwdCIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsPSIjZjJmMmY3IiBzdHJva2U9IiNmMmYyZjciIHN0cm9rZS13aWR0aD0iMC4wOTM3NSIgb3BhY2l0eT0iMS4wMCIgZD0iIE0gMzU1LjU1IDM5OC43NyBDIDM2NC45OSAzOTcuNjIgMzc0LjUxIDM5OC4xMCAzODQuMDAgMzk4LjAwIEMgNDc0LjY3IDM5OC4wMCA1NjUuMzQgMzk4LjAxIDY1Ni4wMSAzOTcuOTkgQyA2NzcuMTEgMzk3Ljk1IDY5OC4yMiA0MDMuODMgNzE2LjE1IDQxNC45OSBDIDc0Ni42MiA0MzMuNjAgNzY3LjMwIDQ2Ny4zMiA3NjkuNjYgNTAzLjAwIEMgNzcyLjYwIDUzOC4xNiA3NTcuNzIgNTc0LjA5IDczMS4wMCA1OTcuMDcgQyA3MTQuMjIgNjExLjcyIDY5My4wNCA2MjEuMjcgNjcwLjk0IDYyNC4wNSBDIDY2MS42NyA2MjUuMzQgNjUyLjMwIDYyNC45NCA2NDIuOTggNjI1LjAwIEMgNTU3LjMxIDYyNS4wMCA0NzEuNjQgNjI1LjAwIDM4NS45NyA2MjUuMDAgQyAzNzMuMzAgNjI0LjkyIDM2MC41MCA2MjUuNjAgMzQ3Ljk5IDYyMy4xMCBDIDMxNy4yMiA2MTcuNjAgMjg5LjIzIDU5OC42MiAyNzIuNjEgNTcyLjE2IEMgMjU5Ljc3IDU1Mi4wMyAyNTMuNjAgNTI3Ljc1IDI1NS4yNiA1MDMuOTMgQyAyNTYuNzAgNDgxLjI4IDI2NS4yNCA0NTkuMTcgMjc5LjMyIDQ0MS4zNyBDIDI5Ny43MyA0MTcuNzkgMzI1LjgwIDQwMi4wNiAzNTUuNTUgMzk4Ljc3IE0gNDY5LjMxIDQxMC40NyBDIDQ1Mi43OSA0MTIuMjIgNDM2LjYwIDQxNy41OCA0MjIuNzEgNDI2Ljc4IEMgNDAwLjE2IDQ0MS40MCAzODQuMDUgNDY1LjUyIDM3OS4xMCA0OTEuOTMgQyAzNzQuNjQgNTExLjQwIDM3Ny42NCA1MzEuODcgMzg0Ljc1IDU1MC4zMiBDIDM4OC4wNiA1NTcuNDAgMzkxLjMzIDU2NC41OSAzOTYuMjYgNTcwLjczIEMgNDE0Ljg1IDU5Ny42MyA0NDcuNTcgNjEzLjMwIDQ4MC4wNSA2MTMuMDEgQyA1MzguNzIgNjEzLjAwIDU5Ny4zOCA2MTMuMDQgNjU2LjA1IDYxMi45NyBDIDY4NC44OSA2MTIuMjggNzEzLjU2IDU5OS4zNyA3MzIuMDAgNTc2Ljk3IEMgNzM2LjY3IDU3MC44MyA3NDEuNTkgNTY0Ljc4IDc0NC43NCA1NTcuNjggQyA3NTAuMTEgNTQ3LjkxIDc1Mi44NSA1MzYuOTYgNzU0Ljg0IDUyNi4wNyBDIDc1Ny45MiA1MDguMzMgNzU0Ljc2IDQ5MC4wNiA3NDguNTMgNDczLjM1IEMgNzQ1LjE5IDQ2NS43NSA3NDEuNDcgNDU4LjI2IDczNi4zMiA0NTEuNzEgQyA3MjQuOTYgNDM1LjQxIDcwOC4xMyA0MjMuMzYgNjg5LjY5IDQxNi4zMSBDIDY3OC4xNiA0MTIuNTkgNjY2LjEzIDQwOS44NyA2NTMuOTYgNDEwLjAzIEMgNTk3LjI5IDQwOS45NSA1NDAuNjEgNDEwLjAzIDQ4My45NCA0MTAuMDAgQyA0NzkuMDYgNDA5Ljk4IDQ3NC4xNyA0MDkuOTQgNDY5LjMxIDQxMC40NyBaIiAvPgo8cGF0aCBmaWxsPSIjZGZkZmU0IiBzdHJva2U9IiNkZmRmZTQiIHN0cm9rZS13aWR0aD0iMC4wOTM3NSIgb3BhY2l0eT0iMS4wMCIgZD0iIE0gNDY5LjMxIDQxMC40NyBDIDQ3NC4xNyA0MDkuOTQgNDc5LjA2IDQwOS45OCA0ODMuOTQgNDEwLjAwIEMgNTQwLjYxIDQxMC4wMyA1OTcuMjkgNDA5Ljk1IDY1My45NiA0MTAuMDMgQyA2NjYuMTMgNDA5Ljg3IDY3OC4xNiA0MTIuNTkgNjg5LjY5IDQxNi4zMSBDIDcwOC4xMyA0MjMuMzYgNzI0Ljk2IDQzNS40MSA3MzYuMzIgNDUxLjcxIEMgNzQxLjQ3IDQ1OC4yNiA3NDUuMTkgNDY1Ljc1IDc0OC41MyA0NzMuMzUgQyA3NTQuNzYgNDkwLjA2IDc1Ny45MiA1MDguMzMgNzU0Ljg0IDUyNi4wNyBDIDc1Mi44NSA1MzYuOTYgNzUwLjExIDU0Ny45MSA3NDQuNzQgNTU3LjY4IEMgNzQxLjU5IDU2NC43OCA3MzYuNjcgNTcwLjgzIDczMi4wMCA1NzYuOTcgQyA3MTMuNTYgNTk5LjM3IDY4NC44OSA2MTIuMjggNjU2LjA1IDYxMi45NyBDIDU5Ny4zOCA2MTMuMDQgNTM4LjcyIDYxMy4wMCA0ODAuMDUgNjEzLjAxIEMgNDQ3LjU3IDYxMy4zMCA0MTQuODUgNTk3LjYzIDM5Ni4yNiA1NzAuNzMgQyAzOTEuMzMgNTY0LjU5IDM4OC4wNiA1NTcuNDAgMzg0Ljc1IDU1MC4zMiBDIDM3Ny42NCA1MzEuODcgMzc0LjY0IDUxMS40MCAzNzkuMTAgNDkxLjkzIEMgMzg0LjA1IDQ2NS41MiA0MDAuMTYgNDQxLjQwIDQyMi43MSA0MjYuNzggQyA0MzYuNjAgNDE3LjU4IDQ1Mi43OSA0MTIuMjIgNDY5LjMxIDQxMC40NyBNIDQ2Ny40OCA0MjIuNjYgQyA0MzYuNjggNDI2LjEzIDQwOC43NCA0NDcuMDggMzk2LjUwIDQ3NS41MSBDIDM4Ny43NyA0OTUuMjcgMzg2LjU4IDUxOC4yMSAzOTMuMjEgNTM4Ljc2IEMgMzk4LjcyIDU1Ni4xNiA0MDkuNzUgNTcxLjc1IDQyNC4zMCA1ODIuNzYgQyA0MzkuMDQgNTk0LjA3IDQ1Ny4zOSA2MDAuNTEgNDc1Ljk1IDYwMS4wMCBDIDUzMi4zMiA2MDEuMDAgNTg4LjY5IDYwMS4wMCA2NDUuMDYgNjAxLjAwIEMgNjUyLjE5IDYwMC45NCA2NTkuMzcgNjAxLjMzIDY2Ni40NiA2MDAuMjIgQyA2ODMuMTYgNTk4LjA4IDY5OS4xNCA1OTAuOTUgNzEyLjAyIDU4MC4xMSBDIDcyOC41OCA1NjYuMzIgNzM5Ljg5IDU0Ni4zMiA3NDMuMDAgNTI0Ljk4IEMgNzQ1Ljg2IDUwNi42NSA3NDIuNzYgNDg3LjQ2IDczNC4zNCA0NzAuOTQgQyA3MjYuNzQgNDU1Ljk3IDcxNC44NCA0NDMuMjMgNzAwLjQxIDQzNC42MyBDIDY4NS45NCA0MjUuODggNjY4LjkyIDQyMS41NCA2NTIuMDMgNDIxLjk5IEMgNTk1LjY2IDQyMi4wMSA1MzkuMjkgNDIyLjAwIDQ4Mi45MSA0MjIuMDAgQyA0NzcuNzYgNDIxLjk0IDQ3Mi41OSA0MjEuOTQgNDY3LjQ4IDQyMi42NiBaIiAvPgo8cGF0aCBmaWxsPSIjMzg3Zjk1IiBzdHJva2U9IiMzODdmOTUiIHN0cm9rZS13aWR0aD0iMC4wOTM3NSIgb3BhY2l0eT0iMS4wMCIgZD0iIE0gNDY3LjQ4IDQyMi42NiBDIDQ3Mi41OSA0MjEuOTQgNDc3Ljc2IDQyMS45NCA0ODIuOTEgNDIyLjAwIEMgNTM5LjI5IDQyMi4wMCA1OTUuNjYgNDIyLjAxIDY1Mi4wMyA0MjEuOTkgQyA2NjguOTIgNDIxLjU0IDY4NS45NCA0MjUuODggNzAwLjQxIDQzNC42MyBDIDcxNC44NCA0NDMuMjMgNzI2Ljc0IDQ1NS45NyA3MzQuMzQgNDcwLjk0IEMgNzQyLjc2IDQ4Ny40NiA3NDUuODYgNTA2LjY1IDc0My4wMCA1MjQuOTggQyA3MzkuODkgNTQ2LjMyIDcyOC41OCA1NjYuMzIgNzEyLjAyIDU4MC4xMSBDIDY5OS4xNCA1OTAuOTUgNjgzLjE2IDU5OC4wOCA2NjYuNDYgNjAwLjIyIEMgNjU5LjM3IDYwMS4zMyA2NTIuMTkgNjAwLjk0IDY0NS4wNiA2MDEuMDAgQyA1ODguNjkgNjAxLjAwIDUzMi4zMiA2MDEuMDAgNDc1Ljk1IDYwMS4wMCBDIDQ1Ny4zOSA2MDAuNTEgNDM5LjA0IDU5NC4wNyA0MjQuMzAgNTgyLjc2IEMgNDA5Ljc1IDU3MS43NSAzOTguNzIgNTU2LjE2IDM5My4yMSA1MzguNzYgQyAzODYuNTggNTE4LjIxIDM4Ny43NyA0OTUuMjcgMzk2LjUwIDQ3NS41MSBDIDQwOC43NCA0NDcuMDggNDM2LjY4IDQyNi4xMyA0NjcuNDggNDIyLjY2IFoiIC8+Cjwvc3ZnPg=='; + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHJlY3QgeD0iMiIgeT0iMyIgd2lkdGg9IjIwIiBoZWlnaHQ9IjgiIHJ4PSI0IiBmaWxsPSIjMDBEMkUwIi8+PHJlY3QgeD0iMTAiIHk9IjUiIHdpZHRoPSIxMCIgaGVpZ2h0PSI0IiByeD0iMiIgZmlsbD0iI0YyRjJGNyIvPjxyZWN0IHg9IjIiIHk9IjEzIiB3aWR0aD0iMjAiIGhlaWdodD0iOCIgcng9IjQiIGZpbGw9IiNGMkYyRjciIGZpbGwtb3BhY2l0eT0iLjQiLz48cmVjdCB4PSI0IiB5PSIxNSIgd2lkdGg9IjEwIiBoZWlnaHQ9IjQiIHJ4PSIyIiBmaWxsPSIjRjJGMkY3Ii8+PC9zdmc+'; diff --git a/public/js/spotlight.js b/public/js/spotlight.js index 3a843bb..1e8fdd5 100644 --- a/public/js/spotlight.js +++ b/public/js/spotlight.js @@ -1,5 +1,5 @@ import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86'; -import { mk, clr, el, inp as inpById, q, qa, setUserText } from '/js/utils.js?v=5d2b6f16'; +import { mk, clr, el, inp as inpById, q, qa, setUserText } from '/js/utils.js?v=045df327'; import { t } from '/js/i18n.js?v=1f1ea9c1'; /* Attached to the window so a re-open can undo the previous one. */ diff --git a/public/js/ui.js b/public/js/ui.js index e388cb3..6993bd7 100644 --- a/public/js/ui.js +++ b/public/js/ui.js @@ -13,7 +13,7 @@ import { q, qa, setUserText, -} from '/js/utils.js?v=5d2b6f16'; +} from '/js/utils.js?v=045df327'; import { t, currentLang } from '/js/i18n.js?v=1f1ea9c1'; import { toneForColor } from '/js/label-contrast.js?v=c1ac6fb8'; import { mobileMetrics, gridColumnWidth, gridCellCount } from '/js/mobile-metrics.js?v=ab5fe77e'; diff --git a/public/js/utils.js b/public/js/utils.js index 848f370..16569a8 100644 --- a/public/js/utils.js +++ b/public/js/utils.js @@ -1,6 +1,6 @@ import { iconChain } from '/js/icons.js?v=9c8c550c'; import { toneForColor } from '/js/label-contrast.js?v=c1ac6fb8'; -import { SETTINGS_ICON } from '/js/settings-icon.js?v=b96e5b13'; +import { SETTINGS_ICON } from '/js/settings-icon.js?v=fe53a1db'; import { BRAND_MARK } from '/js/brand-mark.js?v=1dcbf1ac'; import { t } from '/js/i18n.js?v=1f1ea9c1'; @@ -112,7 +112,7 @@ export const tgt = e => /** @type {HTMLInputElement} */ (e.target); export function mkWrap(item, sz, r, isz, cls, breg) { const w = mk('div'); w.className = cls ? `plate ${cls}` : 'plate'; - const wrapBg = item.system === 'settings' ? '#027eae' : clr(item.color); + const wrapBg = item.system === 'settings' ? '#0D1117' : clr(item.color); w.style.cssText = `--pw:${sz}px;--tc:${wrapBg};width:${sz}px;height:${sz}px;border-radius:${r}px;background-color:${wrapBg};position:relative;flex-shrink:0;overflow:visible;display:flex;align-items:center;justify-content:center;`; const rawIcon = item.iconUrl || ''; if (item.system === 'settings') { diff --git a/public/js/widget-config-form.js b/public/js/widget-config-form.js index ffe2573..75c0f15 100644 --- a/public/js/widget-config-form.js +++ b/public/js/widget-config-form.js @@ -3,9 +3,9 @@ import { t } from '/js/i18n.js?v=1f1ea9c1'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { reveal, wireInlineEdit } from '/js/admin-shared.js?v=69f5c7f9'; -import { createListbox } from '/js/listbox.js?v=2b633751'; -import { renderColorControl } from '/js/admin-color-control.js?v=d0c89032'; +import { reveal, wireInlineEdit } from '/js/admin-shared.js?v=dc0e02b6'; +import { createListbox } from '/js/listbox.js?v=96066369'; +import { renderColorControl } from '/js/admin-color-control.js?v=a48fbc58'; import { seedCarried, applyOptionSet, @@ -15,7 +15,7 @@ import { visibleFieldFlags, } from '/js/admin-logic.js?v=e3673bd7'; import { optionsErrorAdvice, TONE } from '/js/admin-error.js?v=10f3cdb1'; -import { qi } from '/js/utils.js?v=5d2b6f16'; +import { qi } from '/js/utils.js?v=045df327'; const PE = ''; diff --git a/public/js/widget-glyphs.js b/public/js/widget-glyphs.js index 2f7a86b..9025b7f 100644 --- a/public/js/widget-glyphs.js +++ b/public/js/widget-glyphs.js @@ -1,41 +1,28 @@ // @ts-check -/* Drawn on the same 24-unit grid as the other item-list icons, and stroked - with currentColor so they follow the theme. Do not replace with an . */ +import { iconSvg } from '/js/icon-set.js?v=606a68c6'; +/* Keys are the glyph names in widget.json files. Renaming one breaks that widget. */ /** @type {Record} */ -const PATHS = { - clock: '', - weather: - '', - gauge: - '', - shield: - '', - drive: - '', - archive: - '', - shelf: - '', - play: '', - network: - '', - merge: - '', - panels: - '', +const GLYPH_ICON = { + clock: 'clock', + weather: 'weather', + gauge: 'system', + shield: 'dns', + drive: 'disk', + archive: 'backup', + shelf: 'books', + play: 'nowplaying', + network: 'connections', + merge: 'github', + panels: 'switcher', }; /** The glyph names a manifest may declare. */ -export const GLYPH_NAMES = Object.freeze(Object.keys(PATHS)); +export const GLYPH_NAMES = Object.freeze(Object.keys(GLYPH_ICON)); /** One glyph as SVG markup, or null when the name is not one of ours. @param {unknown} name @returns {string|null} */ export function widgetGlyph(name) { - const d = typeof name === 'string' ? PATHS[name] : undefined; - if (!d) return null; - return ( - '` - ); + const id = typeof name === 'string' ? GLYPH_ICON[name] : undefined; + return id ? iconSvg(id, 26) : null; } diff --git a/public/widgets/system-summary/index.html b/public/widgets/system-summary/index.html index 04068e2..5c10abe 100644 --- a/public/widgets/system-summary/index.html +++ b/public/widgets/system-summary/index.html @@ -76,6 +76,7 @@ /* Network slot — text only, always one line */ .net-row { display: flex; gap: 6px; flex-wrap: nowrap; align-items: center; white-space: nowrap; } + .net-ico { vertical-align: -0.125em; } .net-item { font-size: clamp(10px, 2.6cqw, 13px); font-weight: 500; color: rgba(255,255,255,0.65); white-space: nowrap; } @media (prefers-reduced-motion: reduce){ @@ -265,8 +266,8 @@ return html`
Uptime —
`; } return html`
-
↓ —
-
↑ —
+
—
+
—
`; } diff --git a/src/components/IconSet.astro b/src/components/IconSet.astro new file mode 100644 index 0000000..05048eb --- /dev/null +++ b/src/components/IconSet.astro @@ -0,0 +1,74 @@ +--- +import { readRepoFile } from '../lib/repo'; + +const source = readRepoFile('ui/js/icon-set.js'); +let sprite = ''; +let ids: string[] = []; +if (source) { + const mod = await import(/* @vite-ignore */ `data:text/javascript;base64,${Buffer.from(source).toString('base64')}`); + let defs = ''; + let syms = ''; + ids = Object.keys(mod.ICONS); + for (const id of ids) { + const s = mod.symbol(id, mod.ICONS[id]); + defs += s.defs; + syms += s.sym; + } + sprite = ``; +} +--- + +{ + source ? ( +
+ + {ids.map(id => ( +
+ +
{id}
+
+ ))} +
+ ) : ( +

+ See ui/js/icon-set.js. +

+ ) +} + + diff --git a/src/content/docs/docs/contributing/design-system.mdx b/src/content/docs/docs/contributing/design-system.mdx index c1dcaa1..33f20ab 100644 --- a/src/content/docs/docs/contributing/design-system.mdx +++ b/src/content/docs/docs/contributing/design-system.mdx @@ -1,8 +1,10 @@ --- title: Stackyard design system -description: The Stackyard colours, logo and the few design rules that go beyond Apple's Human Interface Guidelines. +description: The Stackyard colours, logo, icons and the few design rules that go beyond Apple's Human Interface Guidelines. --- +import IconSet from '../../../../components/IconSet.astro'; + Stackyard follows Apple's [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines) for type, spacing, controls and motion. This page covers only what is specific to Stackyard. Tokens live in `ui/css/tokens.css`. ## Colour @@ -58,3 +60,38 @@ Use the files as they are. Do not recolour, stretch or redraw them.
Mark, light
+ +## Icons + +The interface draws one icon set, defined in [`ui/js/icon-set.js`](https://github.com/SandObserver/stackyard/blob/main/ui/js/icon-set.js). This list is built from that file, so it always matches the app. + + + +Each icon is a short list of shapes on a 24-unit grid. The file turns the list into an SVG sprite. Icons follow the text colour, and one part per icon takes the accent. + +| Rule | Value | +| --- | --- | +| Grid | 24 by 24. Draw inside 2 to 22. | +| Coordinates | Whole numbers on every straight edge. | +| Stroke and gap | 2 and 2. | +| Corners | 4 for shapes 14 or larger, 3 medium, 2 small. | +| Bars and dots | Bars 2 high, rows every 4. Dots radius 2, nodes radius 3. | +| Accent | At most one part: the thing the icon is about. | + +### Adding an icon for a widget + +1. Add an entry to `ICONS` in `ui/js/icon-set.js`. Draw what the widget shows, in as few shapes as read at 16 pixels. + + ```js + system: { + shapes: [ + line('M3 16 7 12 11 15 16 8'), // the graph + dot(19, 6, 3, 'accent'), // the latest reading + ], + }, + ``` + +2. Map a glyph name to it in `ui/js/widget-glyphs.js`, and add the same name to `VALID_GLYPHS` in `api/src/widgets.js`. +3. Set `"glyph"` in the widget's `widget.json`. See [List icon](/docs/create-a-widget/manifest/#list-icon). + +`rect`, `circle` and `shape` draw bodies. `detail`, `dot` and `bar` draw small parts. `line` and `ring` draw lines. A test rejects a glyph that points at no icon. diff --git a/src/content/docs/docs/create-a-widget/manifest.md b/src/content/docs/docs/create-a-widget/manifest.md index 0d42697..31ecd19 100644 --- a/src/content/docs/docs/create-a-widget/manifest.md +++ b/src/content/docs/docs/create-a-widget/manifest.md @@ -60,17 +60,19 @@ Settings lists every item with an icon. A widget that names a `glyph` shows it. | `glyph` | What it depicts | | --- | --- | -| `clock` | A dial and hands | +| `clock` | A dial with hands | | `weather` | A sun behind a cloud | -| `gauge` | A dial with a needle, for a measured figure | -| `shield` | A shield with record lines, for a name server | -| `drive` | A drive with a trace across it | -| `archive` | A store with an arrow into it | -| `shelf` | Book spines | +| `gauge` | A rising line, for live system figures | +| `shield` | Queries flowing out, one blocked | +| `drive` | A drive with a status light | +| `archive` | A circular arrow around a clock | +| `shelf` | Book spines, one leaning | | `play` | A play mark in a frame | | `network` | Three linked nodes | -| `merge` | Two branches joining one | -| `panels` | Two panels, one handing over to the other | +| `merge` | A branch leaving a line | +| `panels` | One card between its neighbours | + +The icons come from the shared [icon set](/docs/contributing/design-system/#icons). Two widgets may not name the same glyph. A test refuses it. A name not on this list is rejected at startup.