diff --git a/.fork/customizations.yaml b/.fork/customizations.yaml
index aed9065f3ab6..29c6b065426c 100644
--- a/.fork/customizations.yaml
+++ b/.fork/customizations.yaml
@@ -181,12 +181,12 @@
database. legacyUserDataDirName is redirected too, so a fork build never
adopts the legacy directory upstream migrates from.
The fork presents as "no3y Code": bundle id com.t3tools.t3code.fork,
- installs as "no3y Code.app", ships no3y-Code-* artifacts, packages
- placeholder "N" lettermark art from assets/fork on all three platforms,
- and keeps all filesystem state under ~/.t3-fork. The bundle id and the
- base directory are the stable identities and must never change across
- display renames; display strings are safe to rename precisely because
- those two are held stable.
+ installs as "no3y Code.app", ships no3y-Code-* artifacts, packages the
+ green-grid mark from assets/fork on all three platforms (plus web splash
+ and favicon), and keeps all filesystem state under ~/.t3-fork. The bundle
+ id and the base directory are the stable identities and must never change
+ across display renames; display strings are safe to rename precisely
+ because those two are held stable.
Isolation is forked at the base directory, not the "userdata" leaf,
because the packaged app is two processes: the desktop resolves its own
state directory and hands only baseDir (as bootstrap t3Home) to the
@@ -210,22 +210,41 @@
The rendered app carries the name too: the sidebar brand reads
APP_BASE_NAME and drops the T3 wordmark, and branding.ts's bridge-less
fallback points at the fork's own constant instead of upstream's
- "T3 Code". Deliberately shared or deferred: the t3code:// URL scheme stays
- shared (it is the app's own internal origin, not just a deep-link
- handler — two installed apps contend for it, untidy but not destructive);
- splash, favicon and resolveDesktopWebAssetBrand still carry upstream's T3
- art (images rather than strings; separate work). Known carve-out: the WSL
- backend bootstrap deliberately omits t3Home, so WSL-side state is not
- isolated by this customization.
+ "T3 Code". Splash and favicon also ship from assets/fork (copied into
+ apps/web/public for local/dev and into the packaged desktop client at
+ build time via FORK_WEB_ICON_ASSETS + the shared applyWebIconOverrides
+ path). assets/fork/app-icon.svg is the Figma source of truth for the
+ green-grid mark (file t3-fork, node 157:4036); raster exports are
+ derived from that family. Filenames still say n3-* for path stability.
+ Deliberately shared: the t3code:// URL scheme stays shared (it is the
+ app's own internal origin, not just a deep-link handler — two installed
+ apps contend for it, untidy but not destructive).
+ Known carve-out: the WSL backend bootstrap deliberately omits t3Home, so
+ WSL-side state is not isolated by this customization.
+ Known carve-out: hosted web (`apps/web/vercel.ts`) still runs
+ apply-web-brand-assets.ts with the Vercel channel and stamps upstream
+ production/nightly icons over the fork public/ set at deploy time.
+ Known carve-out: `npx t3` publish (`apps/server/scripts/cli.ts`
+ preparePublishIcons) still resolves upstream icons via
+ resolveWebAssetBrandForPackageVersion into dist/client.
+ Known conflict: `pnpm icons:export` writes blueprint development art
+ into apps/web/public via DEVELOPMENT_PUBLIC_ICON_OVERRIDES; after a
+ brand export the fork public icons must be re-copied from assets/fork
+ (or icons:check will report them stale / the byte-equality guard fails).
History — the v0.1.1 leaf-rename incident, the N3→no3y rename, and the
pre-a17cbc3b4 dev-state claim — is in
.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md.
tier: 4
files:
- apps/web/src/custom/forkBranding.ts
+ - assets/fork/app-icon.svg
- assets/fork/n3-macos-1024.png
- assets/fork/n3-universal-1024.png
- assets/fork/n3-windows.ico
+ - assets/fork/n3-web-apple-touch-180.png
+ - assets/fork/n3-web-favicon-16x16.png
+ - assets/fork/n3-web-favicon-32x32.png
+ - assets/fork/n3-web-favicon.ico
shadows: []
watch:
- apps/desktop/src/main.ts
@@ -234,14 +253,33 @@
- apps/desktop/src/app/DesktopEnvironment.ts
- apps/desktop/src/app/DesktopEnvironment.test.ts
- apps/desktop/src/backend/DesktopBackendConfiguration.ts
+ - apps/desktop/scripts/electron-launcher.mjs
- apps/server/src/config.ts
- apps/web/src/branding.ts
- apps/web/src/branding.test.ts
- apps/web/src/components/sidebar/SidebarChrome.tsx
+ - apps/web/public/apple-touch-icon.png
+ - apps/web/public/favicon-16x16.png
+ - apps/web/public/favicon-32x32.png
+ - apps/web/public/favicon.ico
+ - apps/desktop/resources/icon.png
+ - apps/desktop/resources/icon.ico
+ - apps/desktop/resources/icon.icns
- packages/shared/src/devHome.ts
- packages/shared/src/hostProcess.ts
- scripts/build-desktop-artifact.ts
- scripts/build-desktop-artifact.test.ts
+ # Shared web-icon override builder + apply path packaging reuses for
+ # FORK_WEB_ICON_ASSETS (no twin copy pipeline in the packaging script).
+ - scripts/lib/brand-assets.ts
+ - scripts/apply-web-brand-assets.ts
+ # icons:export can overwrite apps/web/public with blueprint art — see
+ # Known conflict in intent.
+ - scripts/export-brand-icons.ts
+ # Hosted-web / npx-t3 surfaces still stamp upstream channel icons — see
+ # Known carve-out lines in intent.
+ - apps/web/vercel.ts
+ - apps/server/scripts/cli.ts
- scripts/dev-runner.ts
- vite.config.ts
- .vscode/settings.json
@@ -1262,7 +1300,12 @@
worktree, PR, and branch chips sit above the surface as filled chips
sharing one selected-surface fill with no hairline, 8px apart (matching
the context row's pb-2 under the pills — including the nested PR+branch
- pair that upstream keeps at gap-1), on the Auto chip's container at
+ pair that upstream keeps at gap-1). Upstream's strip children use flex-1
+ and the branch's justify-end / md:ml-auto, which opens a large empty gap
+ between checkout and branch at narrower widths while the checkout label
+ truncates; the fork forces those direct flex children to flex 0 1 auto,
+ justify-content flex-start, and margin-inline-start 0 so the chips stay a
+ packed leading cluster. Chips use the Auto chip's container at
4px/6px inline padding (24px tall, 6px radius — extra 2px on the end so
the trailing chevron clears the curve; branch combobox takes 6px start,
workspace select 0 for its roomier folder glyph) at 12px type matching
@@ -1304,16 +1347,33 @@
mode chip are 12px medium on 16px with 12px glyphs. The prompt wrapper
owns the 12.5rem scroll cap so a one-line Lexical editor never paints a
phantom scrollbar, and the placeholder/spacer overflow containment remains
- in place. Attachment and pending-state content may add height above the
+ in place. Attachment and pending-state content may add height above the
base row, but ordinary composer height changes only when prompt content
wraps. Pill styling stays narrowly scoped so pending-state primary actions
retain their upstream geometry.
+
+ Compact ⋯ collapse for the below-surface mode row uses fork-lowered
+ form-width thresholds (400px normal / 520px with wide primary actions),
+ not upstream's 620/780. Upstream's numbers pack denser in-box footers;
+ here mode chips sit opposite the model picker on a dedicated row, so
+ those thresholds hide Full access / Build / Plan behind ⋯ while a large
+ empty gap remains. The wide-actions threshold must stay under the form's
+ max-w-3xl (768px) or the expanded row can never appear in that state.
+ Mode-row collapse and model-picker compaction are deliberately split:
+ the override drives only ⋯ vs ComposerFooterModeControls; the model
+ trigger keeps upstream's 620/780 via composerModelSlotCompact.ts so the
+ right slot does not grow in the newly expanded 400–620 band. Traits stay
+ gated with the mode row (inline when expanded, CompactComposerControlsMenu
+ when collapsed) so they are never stranded between the two thresholds.
tier: 4
files:
- apps/web/src/custom/ComposerShell.tsx
- apps/web/src/custom/ComposerShell.css
+ - apps/web/src/custom/composerModelSlotCompact.ts
- apps/web/src/theme.custom.css
- shadows: []
+ - apps/web/src/overrides/components/composerFooterLayout.ts
+ shadows:
+ - apps/web/src/components/composerFooterLayout.ts
watch:
# Carries the base row, the context/control rows, and contextStrip prop.
- apps/web/src/components/chat/ChatComposer.tsx
diff --git a/.fork/notes/pr-46/README.md b/.fork/notes/pr-46/README.md
new file mode 100644
index 000000000000..80a922783335
--- /dev/null
+++ b/.fork/notes/pr-46/README.md
@@ -0,0 +1,20 @@
+# PR #46 composer + favicon screenshots
+
+Before/after captures for the visual checklist on
+[`NoahHendrickson/t3code#46`](https://github.com/NoahHendrickson/t3code/pull/46).
+Captured from the t3code draft thread (`Current checkout` + `#46` +
+`fork/composer-chrome-and-favicon` context strip) with the composer form's
+measured `clientWidth` pinned to each target, dark mode, 2x scale.
+
+- `composer-before-*.png` — `custom@a3c19aa70`: mode row collapses to `⋯` below
+ 620, checkout chip stretches on upstream `flex-1`, branch chip truncates while
+ empty space remains.
+- `composer-after-*.png` — this branch: Full access / Build stay expanded down
+ to 400, chips pack left, the 32-char branch label fits at 460+.
+- `composer-after-380.png` — below the new 400 threshold the `⋯` compact menu
+ and model-slot compaction still engage.
+- The long-label context-strip case is the same captures: the branch chip
+ carries the 32-char `fork/composer-chrome-and-favicon` label, truncated in
+ before-460/620 and fully visible in after-460+.
+- `apple-touch-icon-before/after.png` — dev-served web icon: upstream T3
+ blueprint vs the Figma green-grid mark (`t3-fork` node `157:4036`).
diff --git a/.fork/notes/pr-46/apple-touch-icon-after.png b/.fork/notes/pr-46/apple-touch-icon-after.png
new file mode 100644
index 000000000000..7ff1fbcbcb1c
Binary files /dev/null and b/.fork/notes/pr-46/apple-touch-icon-after.png differ
diff --git a/.fork/notes/pr-46/apple-touch-icon-before.png b/.fork/notes/pr-46/apple-touch-icon-before.png
new file mode 100644
index 000000000000..3eed25ea6b78
Binary files /dev/null and b/.fork/notes/pr-46/apple-touch-icon-before.png differ
diff --git a/.fork/notes/pr-46/composer-after-380.png b/.fork/notes/pr-46/composer-after-380.png
new file mode 100644
index 000000000000..f30a47a94b57
Binary files /dev/null and b/.fork/notes/pr-46/composer-after-380.png differ
diff --git a/.fork/notes/pr-46/composer-after-400.png b/.fork/notes/pr-46/composer-after-400.png
new file mode 100644
index 000000000000..f05fbdae2c36
Binary files /dev/null and b/.fork/notes/pr-46/composer-after-400.png differ
diff --git a/.fork/notes/pr-46/composer-after-460.png b/.fork/notes/pr-46/composer-after-460.png
new file mode 100644
index 000000000000..9f4aabb3ff39
Binary files /dev/null and b/.fork/notes/pr-46/composer-after-460.png differ
diff --git a/.fork/notes/pr-46/composer-after-520.png b/.fork/notes/pr-46/composer-after-520.png
new file mode 100644
index 000000000000..da831c1793f4
Binary files /dev/null and b/.fork/notes/pr-46/composer-after-520.png differ
diff --git a/.fork/notes/pr-46/composer-after-620.png b/.fork/notes/pr-46/composer-after-620.png
new file mode 100644
index 000000000000..e72fd1413085
Binary files /dev/null and b/.fork/notes/pr-46/composer-after-620.png differ
diff --git a/.fork/notes/pr-46/composer-after-768.png b/.fork/notes/pr-46/composer-after-768.png
new file mode 100644
index 000000000000..275328cf545d
Binary files /dev/null and b/.fork/notes/pr-46/composer-after-768.png differ
diff --git a/.fork/notes/pr-46/composer-before-400.png b/.fork/notes/pr-46/composer-before-400.png
new file mode 100644
index 000000000000..25cc4df30616
Binary files /dev/null and b/.fork/notes/pr-46/composer-before-400.png differ
diff --git a/.fork/notes/pr-46/composer-before-460.png b/.fork/notes/pr-46/composer-before-460.png
new file mode 100644
index 000000000000..697bfb1fae57
Binary files /dev/null and b/.fork/notes/pr-46/composer-before-460.png differ
diff --git a/.fork/notes/pr-46/composer-before-520.png b/.fork/notes/pr-46/composer-before-520.png
new file mode 100644
index 000000000000..f2f8ad11592e
Binary files /dev/null and b/.fork/notes/pr-46/composer-before-520.png differ
diff --git a/.fork/notes/pr-46/composer-before-620.png b/.fork/notes/pr-46/composer-before-620.png
new file mode 100644
index 000000000000..486a2b48a08c
Binary files /dev/null and b/.fork/notes/pr-46/composer-before-620.png differ
diff --git a/.fork/notes/pr-46/composer-before-768.png b/.fork/notes/pr-46/composer-before-768.png
new file mode 100644
index 000000000000..275328cf545d
Binary files /dev/null and b/.fork/notes/pr-46/composer-before-768.png differ
diff --git a/apps/desktop/resources/icon.icns b/apps/desktop/resources/icon.icns
index da16d12a0c7c..785e9ba9f552 100644
Binary files a/apps/desktop/resources/icon.icns and b/apps/desktop/resources/icon.icns differ
diff --git a/apps/desktop/resources/icon.ico b/apps/desktop/resources/icon.ico
index 8298f70d8b36..f968d6bb13df 100644
Binary files a/apps/desktop/resources/icon.ico and b/apps/desktop/resources/icon.ico differ
diff --git a/apps/desktop/resources/icon.png b/apps/desktop/resources/icon.png
index 37f3f756a553..589a73155f60 100644
Binary files a/apps/desktop/resources/icon.png and b/apps/desktop/resources/icon.png differ
diff --git a/apps/desktop/scripts/electron-launcher.mjs b/apps/desktop/scripts/electron-launcher.mjs
index 929afeeabe9c..293b94a62e94 100644
--- a/apps/desktop/scripts/electron-launcher.mjs
+++ b/apps/desktop/scripts/electron-launcher.mjs
@@ -22,12 +22,9 @@ export const APP_BUNDLE_ID = isDevelopment
const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];
const LAUNCHER_VERSION = 14;
const defaultIconPath = NodePath.join(desktopDir, "resources", "icon.icns");
-const developmentMacIconPngPath = NodePath.join(
- repoRoot,
- "assets",
- "dev",
- "blueprint-macos-1024.png",
-);
+// fork:begin fork-app-identity — see .fork/customizations.yaml#fork-app-identity
+const developmentMacIconPngPath = NodePath.join(repoRoot, "assets", "fork", "n3-macos-1024.png");
+// fork:end fork-app-identity
// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone launcher script has no Effect runtime.
const hostPlatform = NodeOS.platform();
diff --git a/apps/web/public/apple-touch-icon.png b/apps/web/public/apple-touch-icon.png
index 3eed25ea6b78..7ff1fbcbcb1c 100644
Binary files a/apps/web/public/apple-touch-icon.png and b/apps/web/public/apple-touch-icon.png differ
diff --git a/apps/web/public/favicon-16x16.png b/apps/web/public/favicon-16x16.png
index a3431b8c6dfe..db3f5ed2e99f 100644
Binary files a/apps/web/public/favicon-16x16.png and b/apps/web/public/favicon-16x16.png differ
diff --git a/apps/web/public/favicon-32x32.png b/apps/web/public/favicon-32x32.png
index 862f7629971f..943a285ba056 100644
Binary files a/apps/web/public/favicon-32x32.png and b/apps/web/public/favicon-32x32.png differ
diff --git a/apps/web/public/favicon.ico b/apps/web/public/favicon.ico
index 750da22602ee..3a8f7938a8f7 100644
Binary files a/apps/web/public/favicon.ico and b/apps/web/public/favicon.ico differ
diff --git a/apps/web/src/__fork_guards__/forkAppIdentity.test.ts b/apps/web/src/__fork_guards__/forkAppIdentity.test.ts
index 5a9c69970349..45dba5d7e86a 100644
--- a/apps/web/src/__fork_guards__/forkAppIdentity.test.ts
+++ b/apps/web/src/__fork_guards__/forkAppIdentity.test.ts
@@ -99,13 +99,38 @@ describe("fork guard: fork-app-identity", () => {
it("packages with the fork's own artwork", () => {
// Upstream art would make a fork build indistinguishable from the real
// app in the Dock and /Applications, exactly where the two must be
- // tell-apart-able. All three platform icons come from assets/fork.
+ // tell-apart-able. Desktop icons and bundled splash/favicon come from
+ // assets/fork; packaging must not fall back to upstream channel art.
const script = read(BUILD_SCRIPT);
expect(script).toContain('"assets/fork/n3-macos-1024.png"');
expect(script).toContain('"assets/fork/n3-universal-1024.png"');
expect(script).toContain('"assets/fork/n3-windows.ico"');
+ expect(script).toContain('"assets/fork/n3-web-favicon.ico"');
+ expect(script).toContain("FORK_WEB_ICON_ASSETS");
+ expect(script).toContain("applyWebIconOverrides");
+ expect(script).toContain("resolveWebIconOverridesFromSources");
expect(script).not.toContain("BRAND_ASSET_PATHS.productionMacIconPng");
expect(script).not.toContain("BRAND_ASSET_PATHS.nightlyMacIconPng");
+ expect(script).not.toContain("applyWebBrandAssets(");
+ expect(script).not.toContain("applyForkWebBrandAssets");
+ expect(script).not.toContain("resolveForkWebIconOverrides");
+
+ const launcher = read("apps/desktop/scripts/electron-launcher.mjs");
+ expect(launcher).toContain('"assets", "fork", "n3-macos-1024.png"');
+ expect(launcher).not.toContain("blueprint-macos-1024.png");
+
+ // Dev (`vp run dev`) serves apps/web/public — keep it byte-identical to
+ // the fork sources so local tabs don't silently fall back to T3 blueprint.
+ for (const [source, target] of [
+ ["assets/fork/n3-web-favicon.ico", "apps/web/public/favicon.ico"],
+ ["assets/fork/n3-web-favicon-16x16.png", "apps/web/public/favicon-16x16.png"],
+ ["assets/fork/n3-web-favicon-32x32.png", "apps/web/public/favicon-32x32.png"],
+ ["assets/fork/n3-web-apple-touch-180.png", "apps/web/public/apple-touch-icon.png"],
+ ] as const) {
+ expect(NodeFS.readFileSync(NodePath.join(repoRoot, target))).toEqual(
+ NodeFS.readFileSync(NodePath.join(repoRoot, source)),
+ );
+ }
});
it("keeps packaged state out of the shared ~/.t3 base directory", () => {
diff --git a/apps/web/src/__fork_guards__/forkComposerShell.test.ts b/apps/web/src/__fork_guards__/forkComposerShell.test.ts
index bfb8bb473f80..d3180518a807 100644
--- a/apps/web/src/__fork_guards__/forkComposerShell.test.ts
+++ b/apps/web/src/__fork_guards__/forkComposerShell.test.ts
@@ -7,6 +7,16 @@ import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vite-plus/test";
+import {
+ COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX,
+ COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX,
+ shouldUseCompactComposerFooter,
+} from "../components/composerFooterLayout";
+import {
+ COMPOSER_MODEL_SLOT_COMPACT_BREAKPOINT_PX,
+ COMPOSER_MODEL_SLOT_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX,
+ shouldUseCompactComposerModelSlot,
+} from "../custom/composerModelSlotCompact";
import {
ComposerPromptRow,
ComposerShell,
@@ -16,6 +26,9 @@ import {
import { FORK_MARKER_ATTRIBUTE, FORK_MARKER_VALUE } from "../custom/forkMarker";
import { cssRules } from "./cssRules";
+/** Form ceiling from ChatComposer's `max-w-3xl` — keep compact thresholds below it. */
+const COMPOSER_FORM_MAX_WIDTH_PX = 768;
+
function readSibling(relativePath: string): string {
return NodeFS.readFileSync(NodeURL.fileURLToPath(new URL(relativePath, import.meta.url)), "utf8");
}
@@ -307,6 +320,43 @@ describe("fork guard: fork-composer-shell", () => {
}
});
+ it("keeps mode-row ⋯ collapse below the denser fork control-row widths", () => {
+ // Upstream is 620/780; those fire with a large empty gap on the fork row.
+ // Mode-row only — model-picker compaction stays at upstream 620/780.
+ expect(COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX).toBe(400);
+ expect(COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX).toBe(520);
+ expect(COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX).toBeLessThan(
+ COMPOSER_FORM_MAX_WIDTH_PX,
+ );
+ expect(shouldUseCompactComposerFooter(560)).toBe(false);
+ expect(
+ shouldUseCompactComposerFooter(560, {
+ hasWideActions: true,
+ }),
+ ).toBe(false);
+ // Wide-actions branch: expanded at/above 520, compact below.
+ expect(
+ shouldUseCompactComposerFooter(519, {
+ hasWideActions: true,
+ }),
+ ).toBe(true);
+ expect(
+ shouldUseCompactComposerFooter(520, {
+ hasWideActions: true,
+ }),
+ ).toBe(false);
+ expect(shouldUseCompactComposerFooter(399)).toBe(true);
+ expect(shouldUseCompactComposerFooter(400)).toBe(false);
+
+ // Model trigger stays on upstream widths; ChatComposer must wire both.
+ expect(COMPOSER_MODEL_SLOT_COMPACT_BREAKPOINT_PX).toBe(620);
+ expect(COMPOSER_MODEL_SLOT_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX).toBe(780);
+ expect(shouldUseCompactComposerModelSlot(560)).toBe(true);
+ expect(shouldUseCompactComposerModelSlot(620)).toBe(false);
+ expect(chatComposer).toContain("shouldUseCompactComposerModelSlot");
+ expect(chatComposer).toContain("compact={isComposerModelSlotCompact}");
+ });
+
it("keeps primary actions out of ghost sizing", () => {
expect(primaryActions).toContain('data-fork-composer-action="send"');
expect(primaryActions).toContain('data-fork-composer-action="stop"');
@@ -342,7 +392,8 @@ describe("fork guard: fork-composer-shell", () => {
expect(strip?.body).toMatch(/margin:\s*0/u);
expect(strip?.body).toMatch(/gap:\s*8px/u);
// Nested PR+branch (and env+checkout) wrappers keep upstream gap-1; the
- // fork re-gaps them to 8px so checkout→PR→branch reads evenly.
+ // fork re-gaps them to 8px so checkout→PR→branch reads evenly, and drops
+ // flex-1 / justify-end / ml-auto so the cluster stays packed when narrow.
const nested = rules.find(
(rule) =>
rule.selector.includes("[data-fork-composer-context-row]") &&
@@ -350,5 +401,9 @@ describe("fork guard: fork-composer-shell", () => {
(rule.selector.includes(">.flex") || rule.selector.includes("> .flex")),
);
expect(nested?.body).toMatch(/gap:\s*8px/u);
+ expect(nested?.body).toMatch(/flex:\s*0 1 auto/u);
+ expect(nested?.body).toMatch(/justify-content:\s*flex-start/u);
+ expect(nested?.body).toMatch(/margin-inline-start:\s*0/u);
+ expect(nested?.body).toMatch(/max-width:\s*100%/u);
});
});
diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx
index 2d577231e607..2b4d617abdf1 100644
--- a/apps/web/src/components/chat/ChatComposer.tsx
+++ b/apps/web/src/components/chat/ChatComposer.tsx
@@ -86,6 +86,7 @@ import {
} from "../composerFooterLayout";
import { type ComposerPromptEditorHandle, ComposerPromptEditor } from "../ComposerPromptEditor";
/* fork:begin fork-composer-shell — see .fork/customizations.yaml#fork-composer-shell */
+import { shouldUseCompactComposerModelSlot } from "../../custom/composerModelSlotCompact";
import {
ComposerPromptRow,
ComposerRuntimeModeTrigger,
@@ -983,6 +984,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
);
const [isDragOverComposer, setIsDragOverComposer] = useState(false);
const [isComposerFooterCompact, setIsComposerFooterCompact] = useState(false);
+ /* fork:begin fork-composer-shell — see .fork/customizations.yaml#fork-composer-shell */
+ // Mode-row ⋯ uses the fork footer override; model trigger keeps upstream widths.
+ const [isComposerModelSlotCompact, setIsComposerModelSlotCompact] = useState(false);
+ /* fork:end fork-composer-shell */
const [isComposerPrimaryActionsCompact, setIsComposerPrimaryActionsCompact] = useState(false);
const [isComposerModelPickerOpen, setIsComposerModelPickerOpen] = useState(false);
const [isComposerFocused, setIsComposerFocused] = useState(false);
@@ -1442,6 +1447,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
const footerCompact = shouldUseCompactComposerFooter(composerFormWidth, {
hasWideActions: composerFooterHasWideActions,
});
+ /* fork:begin fork-composer-shell — see .fork/customizations.yaml#fork-composer-shell */
+ const modelSlotCompact = shouldUseCompactComposerModelSlot(composerFormWidth, {
+ hasWideActions: composerFooterHasWideActions,
+ });
+ /* fork:end fork-composer-shell */
const primaryActionsCompact =
footerCompact &&
shouldUseCompactComposerPrimaryActions(composerFormWidth, {
@@ -1450,12 +1460,18 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
return {
primaryActionsCompact,
footerCompact,
+ /* fork:begin fork-composer-shell — see .fork/customizations.yaml#fork-composer-shell */
+ modelSlotCompact,
+ /* fork:end fork-composer-shell */
};
};
const initialCompactness = measureFooterCompactness();
setIsComposerPrimaryActionsCompact(initialCompactness.primaryActionsCompact);
setIsComposerFooterCompact(initialCompactness.footerCompact);
+ /* fork:begin fork-composer-shell — see .fork/customizations.yaml#fork-composer-shell */
+ setIsComposerModelSlotCompact(initialCompactness.modelSlotCompact);
+ /* fork:end fork-composer-shell */
if (typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(() => {
@@ -1468,6 +1484,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
setIsComposerFooterCompact((previous) =>
previous === nextCompactness.footerCompact ? previous : nextCompactness.footerCompact,
);
+ /* fork:begin fork-composer-shell — see .fork/customizations.yaml#fork-composer-shell */
+ setIsComposerModelSlotCompact((previous) =>
+ previous === nextCompactness.modelSlotCompact ? previous : nextCompactness.modelSlotCompact,
+ );
+ /* fork:end fork-composer-shell */
});
observer.observe(composerForm);
@@ -2674,10 +2695,12 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
/* fork:begin fork-composer-shell — see .fork/customizations.yaml#fork-composer-shell */
// The footer's three clusters, split by where the designs put them.
+ // Model compact uses upstream widths; traits stay with the mode-row ⋯ gate
+ // so they are never stranded when modes expand before the model slot does.
const composerModelControls = noProviderAvailable ? null : (
<>
- {/* At compact widths the traits fold into CompactComposerControlsMenu. */}
+ {/* At compact mode-row widths the traits fold into CompactComposerControlsMenu. */}
{!isComposerFooterCompact && providerTraitsPicker ? providerTraitsPicker : null}
>
);
diff --git a/apps/web/src/custom/composerModelSlotCompact.ts b/apps/web/src/custom/composerModelSlotCompact.ts
new file mode 100644
index 000000000000..4b768e901759
--- /dev/null
+++ b/apps/web/src/custom/composerModelSlotCompact.ts
@@ -0,0 +1,21 @@
+/**
+ * Model-picker / traits-adjacent compact threshold for the fork composer.
+ *
+ * Mode-row ⋯ collapse lives in the `composerFooterLayout` override (400/520).
+ * The model trigger still uses upstream's 620/780 so expanding modes early
+ * does not also grow the right-hand model control in the 400–620 band.
+ * See `.fork/customizations.yaml#fork-composer-shell`.
+ */
+
+export const COMPOSER_MODEL_SLOT_COMPACT_BREAKPOINT_PX = 620;
+export const COMPOSER_MODEL_SLOT_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX = 780;
+
+export function shouldUseCompactComposerModelSlot(
+ width: number | null,
+ options?: { hasWideActions?: boolean },
+): boolean {
+ const breakpoint = options?.hasWideActions
+ ? COMPOSER_MODEL_SLOT_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX
+ : COMPOSER_MODEL_SLOT_COMPACT_BREAKPOINT_PX;
+ return width !== null && width < breakpoint;
+}
diff --git a/apps/web/src/overrides/components/composerFooterLayout.ts b/apps/web/src/overrides/components/composerFooterLayout.ts
new file mode 100644
index 000000000000..29e29691a5ad
--- /dev/null
+++ b/apps/web/src/overrides/components/composerFooterLayout.ts
@@ -0,0 +1,36 @@
+// Fork shadow of upstream composerFooterLayout — see
+// `.fork/customizations.yaml#fork-composer-shell`.
+//
+// These constants drive mode-row ⋯ collapse only (ComposerFooterModeControls
+// vs CompactComposerControlsMenu). Upstream's 620/780 packed mode + model into
+// one in-box footer; the fork's denser below-surface mode chips still fit well
+// below those widths. Model-picker compaction stays on upstream's numbers via
+// `apps/web/src/custom/composerModelSlotCompact.ts` so one visual complaint
+// does not widen the right slot at the same time. Keep the wide-actions
+// threshold under the form's max-w-3xl (768px) so the expanded mode row remains
+// reachable at the form's natural ceiling.
+
+export const COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX = 400;
+export const COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX = 520;
+export const COMPOSER_PRIMARY_ACTIONS_COMPACT_BREAKPOINT_PX =
+ COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX;
+
+export function shouldUseCompactComposerFooter(
+ width: number | null,
+ options?: { hasWideActions?: boolean },
+): boolean {
+ const breakpoint = options?.hasWideActions
+ ? COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX
+ : COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX;
+ return width !== null && width < breakpoint;
+}
+
+export function shouldUseCompactComposerPrimaryActions(
+ width: number | null,
+ options?: { hasWideActions?: boolean },
+): boolean {
+ if (!options?.hasWideActions) {
+ return false;
+ }
+ return width !== null && width < COMPOSER_PRIMARY_ACTIONS_COMPACT_BREAKPOINT_PX;
+}
diff --git a/apps/web/src/theme.custom.css b/apps/web/src/theme.custom.css
index 244839f59c76..58675e551009 100644
--- a/apps/web/src/theme.custom.css
+++ b/apps/web/src/theme.custom.css
@@ -592,11 +592,21 @@
/* Upstream nests the PR pill with the branch trigger at gap-1 (4px), while the
strip itself is 8px between checkout and that pair — so checkout→PR reads
loose and PR→branch reads tight. Same 8px on every direct flex child keeps
- the three chips (and env→checkout when both show) evenly spaced. */
+ the three chips (and env→checkout when both show) evenly spaced.
+
+ Upstream also gives the checkout group and branch wrapper `flex-1`, and the
+ branch `justify-end` below `md` (plus `md:ml-auto`). That splits the strip
+ into halves and parks the branch at the end of its half — a large empty gap
+ while "Current checkout" truncates. The fork's chips sit in one packed
+ leading cluster; neutralize the grow/end alignment so gap stays 8px. */
:root[data-fork="noahhendrickson-t3code"]
[data-fork-composer-context-row]
.chat-composer-context-strip
> .flex {
+ flex: 0 1 auto;
+ justify-content: flex-start;
+ margin-inline-start: 0;
+ max-width: 100%;
gap: 8px;
}
diff --git a/assets/fork/app-icon.svg b/assets/fork/app-icon.svg
new file mode 100644
index 000000000000..36bc38d2fdb1
--- /dev/null
+++ b/assets/fork/app-icon.svg
@@ -0,0 +1,15 @@
+
diff --git a/assets/fork/n3-macos-1024.png b/assets/fork/n3-macos-1024.png
index 89b22a6f07c9..48efb37a2940 100644
Binary files a/assets/fork/n3-macos-1024.png and b/assets/fork/n3-macos-1024.png differ
diff --git a/assets/fork/n3-universal-1024.png b/assets/fork/n3-universal-1024.png
index 86a76ced2588..29c8fd301f2a 100644
Binary files a/assets/fork/n3-universal-1024.png and b/assets/fork/n3-universal-1024.png differ
diff --git a/assets/fork/n3-web-apple-touch-180.png b/assets/fork/n3-web-apple-touch-180.png
new file mode 100644
index 000000000000..7ff1fbcbcb1c
Binary files /dev/null and b/assets/fork/n3-web-apple-touch-180.png differ
diff --git a/assets/fork/n3-web-favicon-16x16.png b/assets/fork/n3-web-favicon-16x16.png
new file mode 100644
index 000000000000..db3f5ed2e99f
Binary files /dev/null and b/assets/fork/n3-web-favicon-16x16.png differ
diff --git a/assets/fork/n3-web-favicon-32x32.png b/assets/fork/n3-web-favicon-32x32.png
new file mode 100644
index 000000000000..943a285ba056
Binary files /dev/null and b/assets/fork/n3-web-favicon-32x32.png differ
diff --git a/assets/fork/n3-web-favicon.ico b/assets/fork/n3-web-favicon.ico
new file mode 100644
index 000000000000..3a8f7938a8f7
Binary files /dev/null and b/assets/fork/n3-web-favicon.ico differ
diff --git a/assets/fork/n3-windows.ico b/assets/fork/n3-windows.ico
index ed0db214a19a..f968d6bb13df 100644
Binary files a/assets/fork/n3-windows.ico and b/assets/fork/n3-windows.ico differ
diff --git a/scripts/apply-web-brand-assets.ts b/scripts/apply-web-brand-assets.ts
index 30e6bc325b6c..7fe72b2082a5 100644
--- a/scripts/apply-web-brand-assets.ts
+++ b/scripts/apply-web-brand-assets.ts
@@ -11,6 +11,7 @@ import {
resolveWebAssetBrandForChannel,
resolveWebIconOverrides,
WEB_ASSET_CHANNELS,
+ type IconOverride,
type WebAssetBrand,
} from "./lib/brand-assets.ts";
@@ -20,16 +21,16 @@ const WEB_ASSET_BRANDS = [
"production",
] as const satisfies ReadonlyArray;
-export const applyWebBrandAssets = Effect.fn("applyWebBrandAssets")(function* (
- brand: WebAssetBrand,
- targetDirectory: string,
+/** Copy an explicit list of web icon overrides into the repo-relative targets. */
+export const applyWebIconOverrides = Effect.fn("applyWebIconOverrides")(function* (
+ overrides: ReadonlyArray,
) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const repoRoot = yield* path.fromFileUrl(new URL("..", import.meta.url));
yield* Effect.forEach(
- resolveWebIconOverrides(brand, targetDirectory),
+ overrides,
(override) =>
fs.copyFile(
path.join(repoRoot, override.sourceRelativePath),
@@ -39,6 +40,13 @@ export const applyWebBrandAssets = Effect.fn("applyWebBrandAssets")(function* (
);
});
+export const applyWebBrandAssets = Effect.fn("applyWebBrandAssets")(function* (
+ brand: WebAssetBrand,
+ targetDirectory: string,
+) {
+ yield* applyWebIconOverrides(resolveWebIconOverrides(brand, targetDirectory));
+});
+
export const applyWebBrandAssetsCommand = Command.make(
"apply-web-brand-assets",
{
diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts
index d965e34408fa..aab295749c69 100644
--- a/scripts/build-desktop-artifact.test.ts
+++ b/scripts/build-desktop-artifact.test.ts
@@ -30,10 +30,10 @@ import {
resolveDesktopRuntimeDependencies,
resolveFffNativeDependencies,
resolveBuildOptions,
+ FORK_WEB_ICON_ASSETS,
resolveDesktopBuildIconAssets,
resolveDesktopProductName,
resolveDesktopUpdateChannel,
- resolveDesktopWebAssetBrand,
resolveResourceMonitorRustTargets,
resourceMonitorExecutableName,
resolveGitHubPublishConfig,
@@ -44,6 +44,7 @@ import {
STAGE_INSTALL_ARGS,
WINDOWS_ASAR_UNPACK,
} from "./build-desktop-artifact.ts";
+import { resolveWebIconOverridesFromSources } from "./lib/brand-assets.ts";
import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
function mockProcess(exitCode: number) {
@@ -96,7 +97,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
// fork:end fork-app-identity
});
- it("uses the fork's placeholder artwork for every channel", () => {
+ it("uses the fork's green-grid artwork for every channel", () => {
// fork:begin fork-app-identity — see .fork/customizations.yaml#fork-app-identity
const forkIconAssets = {
macIconPng: "assets/fork/n3-macos-1024.png",
@@ -111,9 +112,38 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
// fork:end fork-app-identity
});
- it("switches the bundled splash and favicon branding for nightly versions", () => {
- assert.equal(resolveDesktopWebAssetBrand("0.0.17"), "production");
- assert.equal(resolveDesktopWebAssetBrand("0.0.17-nightly.20260413.42"), "nightly");
+ it("bundles the fork splash and favicon instead of upstream channel art", () => {
+ // fork:begin fork-app-identity — see .fork/customizations.yaml#fork-app-identity
+ // Packaging always copies FORK_WEB_ICON_ASSETS through the shared brand
+ // override builder — no per-channel web brand selection.
+ assert.deepStrictEqual(FORK_WEB_ICON_ASSETS, {
+ faviconIco: "assets/fork/n3-web-favicon.ico",
+ favicon16Png: "assets/fork/n3-web-favicon-16x16.png",
+ favicon32Png: "assets/fork/n3-web-favicon-32x32.png",
+ appleTouchIconPng: "assets/fork/n3-web-apple-touch-180.png",
+ });
+ assert.deepStrictEqual(
+ resolveWebIconOverridesFromSources(FORK_WEB_ICON_ASSETS, "apps/server/dist/client"),
+ [
+ {
+ sourceRelativePath: "assets/fork/n3-web-favicon.ico",
+ targetRelativePath: "apps/server/dist/client/favicon.ico",
+ },
+ {
+ sourceRelativePath: "assets/fork/n3-web-favicon-16x16.png",
+ targetRelativePath: "apps/server/dist/client/favicon-16x16.png",
+ },
+ {
+ sourceRelativePath: "assets/fork/n3-web-favicon-32x32.png",
+ targetRelativePath: "apps/server/dist/client/favicon-32x32.png",
+ },
+ {
+ sourceRelativePath: "assets/fork/n3-web-apple-touch-180.png",
+ targetRelativePath: "apps/server/dist/client/apple-touch-icon.png",
+ },
+ ],
+ );
+ // fork:end fork-app-identity
});
it.effect("resolves GitHub desktop publish config from Effect config", () =>
diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts
index ff328ce772b0..275e4b686a3f 100644
--- a/scripts/build-desktop-artifact.ts
+++ b/scripts/build-desktop-artifact.ts
@@ -10,12 +10,13 @@ import rootPackageJson from "../package.json" with { type: "json" };
import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" };
import serverPackageJson from "../apps/server/package.json" with { type: "json" };
-import { applyWebBrandAssets } from "./apply-web-brand-assets.ts";
// fork:begin fork-app-identity — see .fork/customizations.yaml#fork-app-identity
-// BRAND_ASSET_PATHS is no longer imported: the fork's desktop icons come from
-// FORK_DESKTOP_ICON_ASSETS below instead of upstream's per-channel art.
+// Packaged splash/favicon use FORK_WEB_ICON_ASSETS through the shared
+// applyWebIconOverrides / resolveWebIconOverridesFromSources path — no twin
+// copy pipeline in this file.
+import { applyWebIconOverrides } from "./apply-web-brand-assets.ts";
+import { resolveWebIconOverridesFromSources } from "./lib/brand-assets.ts";
// fork:end fork-app-identity
-import { resolveWebAssetBrandForChannel, type WebAssetBrand } from "./lib/brand-assets.ts";
import { getDefaultBuildArch } from "./lib/build-target-arch.ts";
import { loadRepoEnv } from "./lib/public-config.ts";
import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts";
@@ -39,13 +40,19 @@ const LINUX_ICON_SIZES = [16, 22, 24, 32, 48, 64, 128, 256, 512] as const;
// Distinct bundle id so macOS treats a fork build as a different application
// from an installed upstream release rather than a replacement for it.
const DESKTOP_APP_ID = "com.t3tools.t3code.fork";
-// Fork-owned placeholder art (an "N" lettermark) generated into assets/fork,
-// consumed by resolveDesktopBuildIconAssets below.
+// Fork-owned green grid mark from Figma (t3-fork 157:4036 for web favicon;
+// desktop PNGs/ICO share the same mark family), kept in assets/fork.
const FORK_DESKTOP_ICON_ASSETS = {
macIconPng: "assets/fork/n3-macos-1024.png",
linuxIconPng: "assets/fork/n3-universal-1024.png",
windowsIconIco: "assets/fork/n3-windows.ico",
} as const;
+export const FORK_WEB_ICON_ASSETS = {
+ faviconIco: "assets/fork/n3-web-favicon.ico",
+ favicon16Png: "assets/fork/n3-web-favicon-16x16.png",
+ favicon32Png: "assets/fork/n3-web-favicon-32x32.png",
+ appleTouchIconPng: "assets/fork/n3-web-apple-touch-180.png",
+} as const;
// fork:end fork-app-identity
const APPLE_TEAM_ID_PATTERN = /^[A-Z0-9]{10}$/u;
@@ -1489,16 +1496,13 @@ export function resolveDesktopUpdateChannel(version: string): "latest" | "nightl
return /-nightly\.\d{8}\.\d+$/.test(version) ? "nightly" : "latest";
}
-export function resolveDesktopWebAssetBrand(version: string): WebAssetBrand {
- return resolveWebAssetBrandForChannel(resolveDesktopUpdateChannel(version));
-}
-
// fork:begin fork-app-identity — see .fork/customizations.yaml#fork-app-identity
-// Placeholder fork art: an "N" lettermark on every channel for now, so a fork
-// build is visually distinct from upstream in the Dock and /Applications.
-// Upstream's per-channel art stays untouched in assets/prod and assets/nightly
-// for clean merges; the version argument keeps upstream's call shape and goes
-// unused until the fork ships channel-specific art.
+// Fork green-grid art on every channel so a fork build is visually distinct
+// from upstream in the Dock and /Applications. Upstream's per-channel art
+// stays untouched in assets/prod and assets/nightly for clean merges; the
+// version argument keeps upstream's call shape and goes unused until the fork
+// ships channel-specific art. resolveDesktopWebAssetBrand was deleted — it
+// had no production caller after packaging switched to FORK_WEB_ICON_ASSETS.
export function resolveDesktopBuildIconAssets(_version: string): DesktopBuildIconAssets {
return FORK_DESKTOP_ICON_ASSETS;
}
@@ -1849,9 +1853,13 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
});
}
- const webAssetBrand = resolveDesktopWebAssetBrand(appVersion);
- yield* applyWebBrandAssets(webAssetBrand, "apps/server/dist/client");
- yield* Effect.log(`[desktop-artifact] Applied ${webAssetBrand} web client branding.`);
+ // fork:begin fork-app-identity — see .fork/customizations.yaml#fork-app-identity
+ // Splash + favicon ship the fork mark via the shared brand-assets copy path.
+ yield* applyWebIconOverrides(
+ resolveWebIconOverridesFromSources(FORK_WEB_ICON_ASSETS, "apps/server/dist/client"),
+ );
+ yield* Effect.log("[desktop-artifact] Applied fork web client branding.");
+ // fork:end fork-app-identity
yield* validateBundledClientAssets(path.dirname(bundledClientEntry));
yield* fs.makeDirectory(path.join(stageAppDir, "apps/desktop"), { recursive: true });
diff --git a/scripts/lib/brand-assets.test.ts b/scripts/lib/brand-assets.test.ts
index 8265ad681802..35e230460cc2 100644
--- a/scripts/lib/brand-assets.test.ts
+++ b/scripts/lib/brand-assets.test.ts
@@ -7,6 +7,7 @@ import {
resolveWebAssetBrandForChannel,
resolveWebAssetBrandForPackageVersion,
resolveWebIconOverrides,
+ resolveWebIconOverridesFromSources,
} from "./brand-assets.ts";
describe("brand-assets", () => {
@@ -31,6 +32,37 @@ describe("brand-assets", () => {
]);
});
+ it("builds the same override list from an explicit source map", () => {
+ expect(
+ resolveWebIconOverridesFromSources(
+ {
+ faviconIco: "assets/fork/n3-web-favicon.ico",
+ favicon16Png: "assets/fork/n3-web-favicon-16x16.png",
+ favicon32Png: "assets/fork/n3-web-favicon-32x32.png",
+ appleTouchIconPng: "assets/fork/n3-web-apple-touch-180.png",
+ },
+ "apps/server/dist/client",
+ ),
+ ).toEqual([
+ {
+ sourceRelativePath: "assets/fork/n3-web-favicon.ico",
+ targetRelativePath: "apps/server/dist/client/favicon.ico",
+ },
+ {
+ sourceRelativePath: "assets/fork/n3-web-favicon-16x16.png",
+ targetRelativePath: "apps/server/dist/client/favicon-16x16.png",
+ },
+ {
+ sourceRelativePath: "assets/fork/n3-web-favicon-32x32.png",
+ targetRelativePath: "apps/server/dist/client/favicon-32x32.png",
+ },
+ {
+ sourceRelativePath: "assets/fork/n3-web-apple-touch-180.png",
+ targetRelativePath: "apps/server/dist/client/apple-touch-icon.png",
+ },
+ ]);
+ });
+
it("maps server build web assets to development icons", () => {
expect(DEVELOPMENT_ICON_OVERRIDES[0]).toEqual({
sourceRelativePath: BRAND_ASSET_PATHS.developmentWebFaviconIco,
diff --git a/scripts/lib/brand-assets.ts b/scripts/lib/brand-assets.ts
index 2dcc6ccd6ce9..7de52dfc5a0e 100644
--- a/scripts/lib/brand-assets.ts
+++ b/scripts/lib/brand-assets.ts
@@ -50,13 +50,17 @@ export interface IconOverride {
readonly targetRelativePath: string;
}
-const WEB_ICON_TARGET_FILENAMES = {
+export const WEB_ICON_TARGET_FILENAMES = {
faviconIco: "favicon.ico",
favicon16Png: "favicon-16x16.png",
favicon32Png: "favicon-32x32.png",
appleTouchIconPng: "apple-touch-icon.png",
} as const;
+export type WebIconSourcePaths = {
+ readonly [K in keyof typeof WEB_ICON_TARGET_FILENAMES]: string;
+};
+
const WEB_ICON_SOURCE_PATHS_BY_BRAND = {
development: {
faviconIco: BRAND_ASSET_PATHS.developmentWebFaviconIco,
@@ -76,13 +80,13 @@ const WEB_ICON_SOURCE_PATHS_BY_BRAND = {
favicon32Png: BRAND_ASSET_PATHS.productionWebFavicon32Png,
appleTouchIconPng: BRAND_ASSET_PATHS.productionWebAppleTouchIconPng,
},
-} as const satisfies Record>;
+} as const satisfies Record;
-export function resolveWebIconOverrides(
- brand: WebAssetBrand,
+/** Build favicon/splash copy pairs from an arbitrary source map into `targetDirectory`. */
+export function resolveWebIconOverridesFromSources(
+ sourcePaths: WebIconSourcePaths,
targetDirectory: string,
): ReadonlyArray {
- const sourcePaths = WEB_ICON_SOURCE_PATHS_BY_BRAND[brand];
return [
{
sourceRelativePath: sourcePaths.faviconIco,
@@ -103,6 +107,13 @@ export function resolveWebIconOverrides(
];
}
+export function resolveWebIconOverrides(
+ brand: WebAssetBrand,
+ targetDirectory: string,
+): ReadonlyArray {
+ return resolveWebIconOverridesFromSources(WEB_ICON_SOURCE_PATHS_BY_BRAND[brand], targetDirectory);
+}
+
export const DEVELOPMENT_ICON_OVERRIDES = resolveWebIconOverrides("development", "dist/client");
export const DEVELOPMENT_PUBLIC_ICON_OVERRIDES = resolveWebIconOverrides(