Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ Issues and pull requests are welcome. Before opening one:
`docker/t3-client/setup-bridge.js`, and prints a pairing link.
`scripts/setup-bridge-audit.js` checks those pages in a browser, through the
same router.
It also checks full native appearance synchronization, including custom colors,
contrast, fonts, size, legacy fallback and unsaved-draft preservation.
`node scripts/appearance-ui-audit.js URL KEY` checks standalone role parity
against the pinned native reference, offline Inter delivery and forced-color
focus at phone, tablet and desktop widths. See the
[design system](docker/setup/design/README.md) for source mapping and sync rules.
It covers grouped Settings navigation, its compact phone rail, native search,
the older sidebar sheet, and opening/closing Setup with focus restored.
Set `T3_BRIDGE_UI_OUTPUT=/tmp/t3-bridge-ui` to save desktop and phone
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ COPY --from=t3-build-node /usr/local/bin/node /usr/local/bin/node
COPY docker/runtime/ /src/docker/runtime/
COPY scripts/build-t3-runtime.mjs /src/scripts/build-t3-runtime.mjs
# The one bundled version pin; scripts/bump-versions.sh refreshes it.
ARG T3_VERSION=0.1.3
ARG T3_VERSION=0.1.5
ARG TARGETARCH
ENV npm_config_store_dir=/var/cache/pnpm
RUN --mount=type=cache,target=/var/cache/pnpm set -eux; \
Expand Down
18 changes: 16 additions & 2 deletions docker/setup/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1593,8 +1593,9 @@
const checking = check.checking || ui.runtimeChecking;
const failures = !checking ? check.failures : [];
const lastChecked = s.updates && s.updates.checkedAt ? M.relTime(s.updates.checkedAt, n) : null;
const idleCheckLabel = html`${icon('refresh-cw')}Check for updates`;
const headActions = html`
<button class="tc-btn tc-btn--sm tc-update-check tc-update-check--label" type="button" data-cmd="updates.check" data-key="updates-check" aria-label="${checking ? 'Checking for updates' : 'Check for updates'}"${check.checking && !check.manual ? raw(' title="Checking in the background. Press to check again now."') : ''} aria-disabled="${String(Boolean(check.manual || ui.runtimeChecking))}">${checking ? html`<span class="tc-spinner" aria-hidden="true"></span>Checking…` : html`${icon('refresh-cw')}Check for updates`}</button>
<button class="tc-btn tc-btn--sm tc-update-check tc-update-check--label" type="button" data-cmd="updates.check" data-key="updates-check" aria-label="${checking ? 'Checking for updates' : 'Check for updates'}"${check.checking && !check.manual ? raw(' title="Checking in the background. Press to check again now."') : ''} aria-disabled="${String(Boolean(check.manual || ui.runtimeChecking))}"><span class="tc-update-check-size" aria-hidden="true">${idleCheckLabel}</span><span class="tc-update-check-content" aria-hidden="true">${checking ? html`<span class="tc-spinner"></span>Checking…` : idleCheckLabel}</span></button>
${view.due.length ? html`<button class="tc-btn tc-btn--primary tc-btn--sm" type="button" data-cmd="updates.all" data-key="updates-all">${icon('circle-arrow-up')}Update all · ${view.due.length}</button>` : ''}`;
return html`
${pageHead('updates', { actions: headActions })}
Expand Down Expand Up @@ -1902,6 +1903,16 @@
const s = state.status;
const n = now();
document.body.dataset.consoleRoute = state.route;
const pageInfo = T3Routes.page(state.route);
const title = pageInfo?.title || titleOf(state.route);
if ($('current-page').textContent !== title) $('current-page').textContent = title;
const guide = $('page-guide');
guide.hidden = !pageInfo?.doc;
if (pageInfo?.doc) {
const href = T3Routes.docUrl(pageInfo.doc, s?.image?.version);
if (guide.getAttribute('href') !== href) guide.setAttribute('href', href);
guide.setAttribute('aria-label', pageInfo.title + ' guide');
}
if (s) renderShell(s, n);
const route = state.route;
const el = $('page-' + route);
Expand Down Expand Up @@ -4053,7 +4064,10 @@
window.addEventListener('message', (e) => {
const m = e.data;
if (e.source !== window.parent || e.origin !== location.origin || !m || m.source !== 't3-code') return;
if (m.type === 'theme' && (m.theme === 'light' || m.theme === 'dark') && m.theme !== themeMode()) setTheme(m.theme);
if (m.type === 'theme' && (m.theme === 'light' || m.theme === 'dark')) {
if (m.theme !== themeMode()) setTheme(m.theme);
if (!T3Appearance.apply(document.documentElement, m.appearance)) T3Appearance.clear(document.documentElement);
}
});
// Escape closes the dialog once nothing on this page wants it: an open
// menu, sheet or palette takes it first (Kit stops it there).
Expand Down
60 changes: 60 additions & 0 deletions docker/setup/client/appearance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Native T3 appearance messages carry semantic colors, never arbitrary CSS.
// Keep local component names here so the bridge does not depend on Kit internals.
const T3Appearance = (() => {
'use strict';
const COLORS = Object.freeze({
'--bg': 'background', '--surface': 'card', '--surface-raised': 'popover',
'--surface-sunken': 'muted', '--fg': 'foreground', '--fg-muted': 'muted-foreground',
'--fg-subtle': 'placeholder', '--border': 'border', '--border-strong': 'input',
'--accent': 'primary', '--on-accent': 'primary-foreground', '--accent-text': 'update-foreground',
'--focus': 'ring', '--hover': 'accent', '--pressed': 'accent',
'--sidebar': 'sidebar', '--sidebar-fg': 'sidebar-foreground',
'--sidebar-fg-muted': 'sidebar-muted-foreground', '--sidebar-border': 'sidebar-border',
'--sidebar-row-hover': 'sidebar-row-hover', '--sidebar-row-active': 'sidebar-row-active',
'--sidebar-row-selected': 'sidebar-row-selected', '--sidebar-control': 'sidebar-control-surface',
'--warn': 'warning', '--warn-text': 'warning-foreground', '--warn-soft': 'warning-surface',
'--danger': 'destructive', '--danger-text': 'destructive-foreground', '--danger-soft': 'error-surface',
'--ok': 'success', '--ok-text': 'success-foreground', '--info': 'info', '--info-text': 'info-foreground',
});
const FONTS = Object.freeze({ '--font-sans': 'sans', '--font-mono': 'mono' });
const OWNED = [...Object.keys(COLORS), ...Object.keys(FONTS), '--control-radius', '--font-smoothing'];
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
const literal = (value, property, supports) => typeof value === 'string' && value.length > 0 && value.length <= 512
&& !/[;{}<>]/.test(value) && !/\b(?:url|var|env)\s*\(/i.test(value) && supports(property, value);

const normalize = (value, supports) => {
if (!record(value) || value.version !== 1 || !record(value.colors) || typeof supports !== 'function') return null;
const properties = {};
for (const [token, role] of Object.entries(COLORS)) {
const color = Object.hasOwn(value.colors, role) ? value.colors[role] : null;
if (literal(color, 'color', supports)) properties[token] = color;
}
if (record(value.fonts)) for (const [token, role] of Object.entries(FONTS)) {
const font = Object.hasOwn(value.fonts, role) ? value.fonts[role] : null;
if (literal(font, 'font-family', supports)) properties[token] = font;
}
if (record(value.fonts) && ['auto', 'antialiased', 'subpixel-antialiased'].includes(value.fonts.smoothing)) properties['--font-smoothing'] = value.fonts.smoothing;
if (typeof value.radius === 'string' && /^(?:0|(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem))$/.test(value.radius)
&& value.radius.length <= 16 && parseFloat(value.radius) <= (value.radius.endsWith('rem') ? 2 : 32)
&& supports('border-radius', value.radius)) properties['--control-radius'] = value.radius;
const size = typeof value.fontSize === 'string' && /^(?:\d+(?:\.\d+)?)px$/.test(value.fontSize) ? parseFloat(value.fontSize) : null;
return { properties, fontSize: size !== null && size >= 12 && size <= 20 ? `${size}px` : null };
};

const clear = (root) => {
for (const token of OWNED) root.style.removeProperty(token);
root.style.removeProperty('font-size');
};
const apply = (root, value) => {
const result = normalize(value, (property, text) => CSS.supports(property, text));
if (!result) return false;
// A palette can lose a role when switching to an older host. Remove its
// previous override so the current light/dark default takes over cleanly.
clear(root);
for (const [token, color] of Object.entries(result.properties)) root.style.setProperty(token, color);
if (result.fontSize) root.style.fontSize = result.fontSize;
else root.style.removeProperty('font-size');
return true;
};
return { COLORS, FONTS, normalize, apply, clear };
})();
Loading
Loading