| author | |
| committer | |
| log | c0cad74bfaddf153c3adaaf4f78af2be9260c4c1 |
| tree | 16117d97826252c0c67ffb7415b44285d868e838 |
| parent | 12f5604442564d3e8c1cb23135b27b37ab4af1cf |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>10 files changed, 211 insertions(+), 22 deletions(-)
apps/mobile/app.config.ts+12-3| ... | @@ -18,8 +18,8 @@ const personalTeamBundleIdentifier = repoEnv.T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID? | ... | @@ -18,8 +18,8 @@ const personalTeamBundleIdentifier = repoEnv.T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID? |
| 18 | const IOS_BUNDLE_IDENTIFIER_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/; | 18 | const IOS_BUNDLE_IDENTIFIER_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/; |
| 19 | 19 | ||
| 20 | const fromRepoRoot = (relativePath: string) => `../../${relativePath}`; | 20 | const fromRepoRoot = (relativePath: string) => `../../${relativePath}`; |
| 21 | // Universal exports already contain their own rounded-square silhouette. Using one as an adaptive | 21 | // Android layers are rendered by scripts/export-android-icons.ts from the Icon Composer sources. |
| 22 | // foreground makes Android draw an icon shape inside the launcher's mask. | 22 | // The wordmark sits inside the adaptive safe zone; the variant artwork is a full-bleed background. |
| 23 | const androidAdaptiveForeground = "./assets/android-icon-foreground.png"; | 23 | const androidAdaptiveForeground = "./assets/android-icon-foreground.png"; |
| 24 | 24 | ||
| 25 | if ( | 25 | if ( |
| ... | @@ -37,7 +37,8 @@ const DEVELOPMENT_ASSETS = { | ... | @@ -37,7 +37,8 @@ const DEVELOPMENT_ASSETS = { |
| 37 | iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIconComposerProject), | 37 | iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIconComposerProject), |
| 38 | splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIosIconPng), | 38 | splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIosIconPng), |
| 39 | androidAdaptiveForeground, | 39 | androidAdaptiveForeground, |
| 40 | androidAdaptiveBackgroundColor: "#00639B", | 40 | androidAdaptiveBackgroundColor: "#347FF8", |
| 41 | androidAdaptiveBackgroundImage: "./assets/android-icon-background-dev.png", | ||
| 41 | androidMonochromeIcon: "./assets/android-icon-mark.png", | 42 | androidMonochromeIcon: "./assets/android-icon-mark.png", |
| 42 | androidNotificationIcon: "./assets/android-notification-icon.png", | 43 | androidNotificationIcon: "./assets/android-notification-icon.png", |
| 43 | androidNotificationColor: "#00639B", | 44 | androidNotificationColor: "#00639B", |
| ... | @@ -49,6 +50,7 @@ const PREVIEW_ASSETS = { | ... | @@ -49,6 +50,7 @@ const PREVIEW_ASSETS = { |
| 49 | splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIosIconPng), | 50 | splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIosIconPng), |
| 50 | androidAdaptiveForeground, | 51 | androidAdaptiveForeground, |
| 51 | androidAdaptiveBackgroundColor: "#111533", | 52 | androidAdaptiveBackgroundColor: "#111533", |
| 53 | androidAdaptiveBackgroundImage: "./assets/android-icon-background-nightly.png", | ||
| 52 | androidMonochromeIcon: "./assets/android-icon-mark.png", | 54 | androidMonochromeIcon: "./assets/android-icon-mark.png", |
| 53 | androidNotificationIcon: "./assets/android-notification-icon.png", | 55 | androidNotificationIcon: "./assets/android-notification-icon.png", |
| 54 | androidNotificationColor: "#7565C7", | 56 | androidNotificationColor: "#7565C7", |
| ... | @@ -60,6 +62,7 @@ const RELEASE_ASSETS = { | ... | @@ -60,6 +62,7 @@ const RELEASE_ASSETS = { |
| 60 | splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIosIconPng), | 62 | splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIosIconPng), |
| 61 | androidAdaptiveForeground, | 63 | androidAdaptiveForeground, |
| 62 | androidAdaptiveBackgroundColor: "#000000", | 64 | androidAdaptiveBackgroundColor: "#000000", |
| 65 | androidAdaptiveBackgroundImage: undefined, | ||
| 63 | androidMonochromeIcon: "./assets/android-icon-mark.png", | 66 | androidMonochromeIcon: "./assets/android-icon-mark.png", |
| 64 | androidNotificationIcon: "./assets/android-notification-icon.png", | 67 | androidNotificationIcon: "./assets/android-notification-icon.png", |
| 65 | androidNotificationColor: "#FFFFFF", | 68 | androidNotificationColor: "#FFFFFF", |
| ... | @@ -230,6 +233,9 @@ const config: ExpoConfig = { | ... | @@ -230,6 +233,9 @@ const config: ExpoConfig = { |
| 230 | package: variant.androidPackage, | 233 | package: variant.androidPackage, |
| 231 | adaptiveIcon: { | 234 | adaptiveIcon: { |
| 232 | backgroundColor: variant.assets.androidAdaptiveBackgroundColor, | 235 | backgroundColor: variant.assets.androidAdaptiveBackgroundColor, |
| 236 | ...(variant.assets.androidAdaptiveBackgroundImage | ||
| 237 | ? { backgroundImage: variant.assets.androidAdaptiveBackgroundImage } | ||
| 238 | : {}), | ||
| 233 | foregroundImage: variant.assets.androidAdaptiveForeground, | 239 | foregroundImage: variant.assets.androidAdaptiveForeground, |
| 234 | monochromeImage: variant.assets.androidMonochromeIcon, | 240 | monochromeImage: variant.assets.androidMonochromeIcon, |
| 235 | }, | 241 | }, |
| ... | @@ -293,6 +299,9 @@ const config: ExpoConfig = { | ... | @@ -293,6 +299,9 @@ const config: ExpoConfig = { |
| 293 | shortcut_icon: { | 299 | shortcut_icon: { |
| 294 | foregroundImage: variant.assets.androidAdaptiveForeground, | 300 | foregroundImage: variant.assets.androidAdaptiveForeground, |
| 295 | backgroundColor: variant.assets.androidAdaptiveBackgroundColor, | 301 | backgroundColor: variant.assets.androidAdaptiveBackgroundColor, |
| 302 | ...(variant.assets.androidAdaptiveBackgroundImage | ||
| 303 | ? { backgroundImage: variant.assets.androidAdaptiveBackgroundImage } | ||
| 304 | : {}), | ||
| 296 | }, | 305 | }, |
| 297 | }, | 306 | }, |
| 298 | }, | 307 | }, |
apps/mobile/assets/android-icon-background-dev.png created| Binary files /dev/null and b/apps/mobile/assets/android-icon-background-dev.png differ | |||
apps/mobile/assets/android-icon-background-nightly.png created| Binary files /dev/null and b/apps/mobile/assets/android-icon-background-nightly.png differ | |||
apps/mobile/assets/android-icon-foreground.png| Binary files a/apps/mobile/assets/android-icon-foreground.png and b/apps/mobile/assets/android-icon-foreground.png differ | |||
apps/mobile/assets/android-icon-foreground.svg deleted-8| ... | @@ -1,8 +0,0 @@ | ||
| 1 | <svg width="432" height="432" viewBox="0 0 432 432" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| 2 | <g transform="translate(116.3 155.8) scale(2.6)"> | ||
| 3 | <path | ||
| 4 | d="M14.56 45.5V8.58H0V0H39.65V8.58H25.09V45.5H14.56ZM57.8454 46.28C54.6821 46.28 51.5404 45.8683 48.4204 45.045C45.3004 44.1783 42.6571 42.965 40.4904 41.405L44.5854 33.345C46.3188 34.6017 48.3338 35.5983 50.6304 36.335C52.9271 37.0717 55.2454 37.44 57.5854 37.44C60.2288 37.44 62.3088 36.92 63.8254 35.88C65.3421 34.84 66.1004 33.41 66.1004 31.59C66.1004 29.8567 65.4288 28.4917 64.0854 27.495C62.7421 26.4983 60.5754 26 57.5854 26H52.7754V19.045L65.4504 4.68L66.6204 8.45H42.7654V0H74.6154V6.825L62.0054 21.19L56.6754 18.135H59.7304C65.3204 18.135 69.5454 19.3917 72.4054 21.905C75.2654 24.4183 76.6954 27.6467 76.6954 31.59C76.6954 34.1467 76.0238 36.5517 74.6804 38.805C73.3371 41.015 71.2788 42.8133 68.5054 44.2C65.7321 45.5867 62.1788 46.28 57.8454 46.28Z" | ||
| 5 | fill="#FFFFFF" | ||
| 6 | /> | ||
| 7 | </g> | ||
| 8 | </svg> | ||
assets/README.md+11-10| ... | @@ -49,16 +49,17 @@ Verify every result is 1024×1024 and has the classic macOS safe area: an 824×8 | ... | @@ -49,16 +49,17 @@ Verify every result is 1024×1024 and has the classic macOS safe area: an 824×8 |
| 49 | 49 | ||
| 50 | Do not edit the generated PNG or ICO files directly. | 50 | Do not edit the generated PNG or ICO files directly. |
| 51 | 51 | ||
| 52 | ## Android adaptive foreground | 52 | ## Android adaptive icon layers |
| 53 | 53 | ||
| 54 | `apps/mobile/assets/android-icon-foreground.svg` is the source of truth for the foreground used by | 54 | Android masks the central 72dp of a 108dp adaptive canvas, so the Icon Composer exports cannot be |
| 55 | the normal Android adaptive launcher icon. Export its paired PNG after changing it: | 55 | used directly: their rounded-square silhouette gets framed again and the wordmark is cropped. The |
| 56 | Android layers are instead rendered from the same Icon Composer SVG sources by | ||
| 57 | `vp run icons:export:android`: | ||
| 56 | 58 | ||
| 57 | ```sh | 59 | - `apps/mobile/assets/android-icon-foreground.png`: the shared transparent wordmark, sized to stay |
| 58 | rsvg-convert -w 432 -h 432 \ | 60 | inside the safe zone |
| 59 | -o apps/mobile/assets/android-icon-foreground.png \ | 61 | - `apps/mobile/assets/android-icon-background-dev.png` and `-nightly.png`: full-bleed variant |
| 60 | apps/mobile/assets/android-icon-foreground.svg | 62 | artwork (blueprint grid and annotations; night sky and clouds). Production uses a solid color. |
| 61 | ``` | ||
| 62 | 63 | ||
| 63 | The foreground must remain transparent and keep the T3 mark inside Android's adaptive-icon safe | 64 | Rerun the export after changing a layer SVG. `android-icon-mark.png` remains a flat silhouette for |
| 64 | zone. `android-icon-mark.png` remains a flat silhouette for Android's monochrome themed icon. | 65 | Android's monochrome themed icon. |
package.json+1| ... | @@ -18,6 +18,7 @@ | ... | @@ -18,6 +18,7 @@ |
| 18 | "screenshots:mobile": "node scripts/mobile-showcase.ts", | 18 | "screenshots:mobile": "node scripts/mobile-showcase.ts", |
| 19 | "icons:export": "node scripts/export-brand-icons.ts", | 19 | "icons:export": "node scripts/export-brand-icons.ts", |
| 20 | "icons:check": "node scripts/export-brand-icons.ts --check", | 20 | "icons:check": "node scripts/export-brand-icons.ts --check", |
| 21 | "icons:export:android": "node scripts/export-android-icons.ts", | ||
| 21 | "build": "vp run --filter './apps/*' build", | 22 | "build": "vp run --filter './apps/*' build", |
| 22 | "build:marketing": "vp run --filter @t3tools/marketing build", | 23 | "build:marketing": "vp run --filter @t3tools/marketing build", |
| 23 | "build:desktop": "vp run --filter @t3tools/desktop --filter t3 build", | 24 | "build:desktop": "vp run --filter @t3tools/desktop --filter t3 build", |
pnpm-lock.yaml+3| ... | @@ -990,6 +990,9 @@ importers: | ... | @@ -990,6 +990,9 @@ importers: |
| 990 | pngjs: | 990 | pngjs: |
| 991 | specifier: 7.0.0 | 991 | specifier: 7.0.0 |
| 992 | version: 7.0.0 | 992 | version: 7.0.0 |
| 993 | sharp: | ||
| 994 | specifier: 0.34.5 | ||
| 995 | version: 0.34.5 | ||
| 993 | devDependencies: | 996 | devDependencies: |
| 994 | '@effect/vitest': | 997 | '@effect/vitest': |
| 995 | specifier: 4.0.0-beta.103 | 998 | specifier: 4.0.0-beta.103 |
scripts/export-android-icons.ts created+182| ... | @@ -0,0 +1,182 @@ | ||
| 1 | #!/usr/bin/env node | ||
| 2 | |||
| 3 | // Renders the Android adaptive-icon layers from the Icon Composer SVG sources. | ||
| 4 | // | ||
| 5 | // Icon Composer exports already contain a rounded-square silhouette, and Android masks | ||
| 6 | // the central 72dp of a 108dp adaptive canvas, so exporting them as a foreground produces | ||
| 7 | // a double-framed icon with the letters cropped by the mask. Instead, each variant gets a | ||
| 8 | // full-bleed background layer (the artwork behind the wordmark) and a shared transparent | ||
| 9 | // foreground that keeps the wordmark inside the safe zone. | ||
| 10 | |||
| 11 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; | ||
| 12 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 13 | import * as Console from "effect/Console"; | ||
| 14 | import * as Effect from "effect/Effect"; | ||
| 15 | import * as FileSystem from "effect/FileSystem"; | ||
| 16 | import * as Path from "effect/Path"; | ||
| 17 | import * as Schema from "effect/Schema"; | ||
| 18 | import sharp from "sharp"; | ||
| 19 | |||
| 20 | type IconVariant = "dev" | "nightly" | "prod"; | ||
| 21 | |||
| 22 | // 108dp at xxxhdpi. Expo's prebuild derives every density bucket from this. | ||
| 23 | const CANVAS = 432; | ||
| 24 | // Icon Composer's layer sources use a 128pt viewBox; the wordmark path spans this box. | ||
| 25 | const TEXT = { x: 15.53, y: 37, width: 94.5, height: 57 }; | ||
| 26 | // Wordmark width as a fraction of the 108dp canvas. The visible area is 72dp (66dp | ||
| 27 | // guaranteed), so 0.48 leaves the letters at ~72% of the mask with room for the | ||
| 28 | // launcher's own zoom effects. | ||
| 29 | const WORDMARK_FRACTION = 0.48; | ||
| 30 | // Icon Composer positions layers on a 1024pt canvas, with translation relative to center. | ||
| 31 | const COMPOSER_CANVAS_PT = 1024; | ||
| 32 | const SVG_DENSITY = 300; | ||
| 33 | const OUTPUT_DIRECTORY = "apps/mobile/assets"; | ||
| 34 | |||
| 35 | export class AndroidIconRenderError extends Schema.TaggedErrorClass<AndroidIconRenderError>()( | ||
| 36 | "AndroidIconRenderError", | ||
| 37 | { layer: Schema.String, cause: Schema.Defect() }, | ||
| 38 | ) {} | ||
| 39 | |||
| 40 | const wordmarkTransform = () => { | ||
| 41 | const scale = (CANVAS * WORDMARK_FRACTION) / TEXT.width; | ||
| 42 | const tx = (CANVAS - TEXT.width * scale) / 2 - TEXT.x * scale; | ||
| 43 | const ty = (CANVAS - TEXT.height * scale) / 2 - TEXT.y * scale; | ||
| 44 | return `translate(${tx.toFixed(3)} ${ty.toFixed(3)}) scale(${scale.toFixed(4)})`; | ||
| 45 | }; | ||
| 46 | |||
| 47 | const canvasSvg = (inner: string) => | ||
| 48 | `<svg xmlns="http://www.w3.org/2000/svg" width="${CANVAS}" height="${CANVAS}" viewBox="0 0 ${CANVAS} ${CANVAS}" fill="none">${inner}</svg>`; | ||
| 49 | |||
| 50 | // The layer sources clip to a 10pt rounded rectangle for the iOS silhouette. Android | ||
| 51 | // applies its own mask, so the layer must bleed to the canvas edge. | ||
| 52 | const fullBleed = (svg: string) => | ||
| 53 | svg.replace(/<rect width="128" height="128" rx="10"\/>/, '<rect width="128" height="128"/>'); | ||
| 54 | |||
| 55 | const rasterize = (layer: string, svg: string, width: number, height = width) => | ||
| 56 | Effect.tryPromise({ | ||
| 57 | try: () => | ||
| 58 | sharp(Buffer.from(svg), { density: SVG_DENSITY }).resize(width, height).png().toBuffer(), | ||
| 59 | catch: (cause) => new AndroidIconRenderError({ layer, cause }), | ||
| 60 | }); | ||
| 61 | |||
| 62 | const composite = ( | ||
| 63 | layer: string, | ||
| 64 | base: Buffer, | ||
| 65 | overlays: ReadonlyArray<{ input: Buffer; left?: number; top?: number }>, | ||
| 66 | ) => | ||
| 67 | Effect.tryPromise({ | ||
| 68 | try: () => | ||
| 69 | sharp(base) | ||
| 70 | .composite([...overlays]) | ||
| 71 | .png() | ||
| 72 | .toBuffer(), | ||
| 73 | catch: (cause) => new AndroidIconRenderError({ layer, cause }), | ||
| 74 | }); | ||
| 75 | |||
| 76 | const readLayerSource = Effect.fn("androidIcons.readLayerSource")(function* ( | ||
| 77 | repositoryRoot: string, | ||
| 78 | variant: IconVariant, | ||
| 79 | file: string, | ||
| 80 | ) { | ||
| 81 | const fs = yield* FileSystem.FileSystem; | ||
| 82 | const path = yield* Path.Path; | ||
| 83 | return yield* fs.readFileString( | ||
| 84 | path.join(repositoryRoot, "assets", variant, "app-icon.icon", "Assets", file), | ||
| 85 | ); | ||
| 86 | }); | ||
| 87 | |||
| 88 | const renderForeground = Effect.fn("androidIcons.renderForeground")(function* ( | ||
| 89 | repositoryRoot: string, | ||
| 90 | ) { | ||
| 91 | const text = yield* readLayerSource(repositoryRoot, "prod", "text.svg"); | ||
| 92 | const paths = text.match(/<path[^>]*\/>/g) ?? []; | ||
| 93 | return yield* rasterize( | ||
| 94 | "foreground", | ||
| 95 | canvasSvg(`<g transform="${wordmarkTransform()}">${paths.join("")}</g>`), | ||
| 96 | CANVAS, | ||
| 97 | ); | ||
| 98 | }); | ||
| 99 | |||
| 100 | const renderDevelopmentBackground = Effect.fn("androidIcons.renderDevelopmentBackground")( | ||
| 101 | function* (repositoryRoot: string) { | ||
| 102 | // The annotation layer shares the wordmark's coordinate space, so it is scaled and | ||
| 103 | // centered the same way to keep the dimension lines around the letters. | ||
| 104 | const annotations = yield* readLayerSource(repositoryRoot, "dev", "annotations.svg"); | ||
| 105 | const defs = annotations.match(/<defs>[\s\S]*?<\/defs>/)?.[0] ?? ""; | ||
| 106 | const body = annotations.replace(/^[\s\S]*?<\/defs>/, "").replace(/<\/svg>\s*$/, ""); | ||
| 107 | const paper = yield* readLayerSource(repositoryRoot, "dev", "background.svg"); | ||
| 108 | const background = yield* rasterize("dev-background", fullBleed(paper), CANVAS); | ||
| 109 | const overlay = yield* rasterize( | ||
| 110 | "dev-annotations", | ||
| 111 | canvasSvg(`${defs}<g transform="${wordmarkTransform()}">${body}</g>`), | ||
| 112 | CANVAS, | ||
| 113 | ); | ||
| 114 | return yield* composite("dev-background", background, [{ input: overlay }]); | ||
| 115 | }, | ||
| 116 | ); | ||
| 117 | |||
| 118 | const renderNightlyBackground = Effect.fn("androidIcons.renderNightlyBackground")(function* ( | ||
| 119 | repositoryRoot: string, | ||
| 120 | ) { | ||
| 121 | // Positions mirror assets/nightly/app-icon.icon/icon.json. The SVG blur filter is | ||
| 122 | // dropped because Icon Composer ignores it and it smears at this raster size. | ||
| 123 | const clouds = [ | ||
| 124 | { file: "cloud-lower-left.svg", scale: 25, translation: [-309.6375, 268.66077693836917] }, | ||
| 125 | { | ||
| 126 | file: "cloud-upper-right.svg", | ||
| 127 | scale: 15, | ||
| 128 | translation: [387.9605131881942, -134.30064713259117], | ||
| 129 | }, | ||
| 130 | ] as const; | ||
| 131 | const k = CANVAS / COMPOSER_CANVAS_PT; | ||
| 132 | const overlays = yield* Effect.forEach(clouds, (cloud) => | ||
| 133 | Effect.gen(function* () { | ||
| 134 | const width = Math.round(64 * cloud.scale * k); | ||
| 135 | const height = Math.round(32 * cloud.scale * k); | ||
| 136 | const left = Math.round((COMPOSER_CANVAS_PT / 2 + cloud.translation[0]) * k - width / 2); | ||
| 137 | const top = Math.round((COMPOSER_CANVAS_PT / 2 + cloud.translation[1]) * k - height / 2); | ||
| 138 | const source = yield* readLayerSource(repositoryRoot, "nightly", cloud.file); | ||
| 139 | const png = yield* rasterize( | ||
| 140 | cloud.file, | ||
| 141 | source.replace(/ filter="url\(#soft\)"/, ""), | ||
| 142 | width, | ||
| 143 | height, | ||
| 144 | ); | ||
| 145 | const x0 = Math.max(0, left); | ||
| 146 | const y0 = Math.max(0, top); | ||
| 147 | const x1 = Math.min(CANVAS, left + width); | ||
| 148 | const y1 = Math.min(CANVAS, top + height); | ||
| 149 | const clipped = yield* Effect.tryPromise({ | ||
| 150 | try: () => | ||
| 151 | sharp(png) | ||
| 152 | .extract({ left: x0 - left, top: y0 - top, width: x1 - x0, height: y1 - y0 }) | ||
| 153 | .png() | ||
| 154 | .toBuffer(), | ||
| 155 | catch: (cause) => new AndroidIconRenderError({ layer: cloud.file, cause }), | ||
| 156 | }); | ||
| 157 | return { input: clipped, left: x0, top: y0 }; | ||
| 158 | }), | ||
| 159 | ); | ||
| 160 | const sky = yield* readLayerSource(repositoryRoot, "nightly", "background.svg"); | ||
| 161 | const background = yield* rasterize("nightly-background", fullBleed(sky), CANVAS); | ||
| 162 | return yield* composite("nightly-background", background, overlays); | ||
| 163 | }); | ||
| 164 | |||
| 165 | const exportAndroidIcons = Effect.gen(function* () { | ||
| 166 | const fs = yield* FileSystem.FileSystem; | ||
| 167 | const path = yield* Path.Path; | ||
| 168 | const repositoryRoot = path.resolve(import.meta.dirname, ".."); | ||
| 169 | const outputs = [ | ||
| 170 | ["android-icon-foreground.png", yield* renderForeground(repositoryRoot)], | ||
| 171 | ["android-icon-background-dev.png", yield* renderDevelopmentBackground(repositoryRoot)], | ||
| 172 | ["android-icon-background-nightly.png", yield* renderNightlyBackground(repositoryRoot)], | ||
| 173 | ] as const; | ||
| 174 | for (const [name, contents] of outputs) { | ||
| 175 | yield* fs.writeFile(path.join(repositoryRoot, OUTPUT_DIRECTORY, name), contents); | ||
| 176 | yield* Console.log(`wrote ${OUTPUT_DIRECTORY}/${name}`); | ||
| 177 | } | ||
| 178 | }); | ||
| 179 | |||
| 180 | if (import.meta.main) { | ||
| 181 | exportAndroidIcons.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain); | ||
| 182 | } | ||
scripts/package.json+2-1| ... | @@ -13,7 +13,8 @@ | ... | @@ -13,7 +13,8 @@ |
| 13 | "@t3tools/shared": "workspace:*", | 13 | "@t3tools/shared": "workspace:*", |
| 14 | "@t3tools/tailscale": "workspace:*", | 14 | "@t3tools/tailscale": "workspace:*", |
| 15 | "effect": "catalog:", | 15 | "effect": "catalog:", |
| 16 | "pngjs": "7.0.0" | 16 | "pngjs": "7.0.0", |
| 17 | "sharp": "0.34.5" | ||
| 17 | }, | 18 | }, |
| 18 | "devDependencies": { | 19 | "devDependencies": { |
| 19 | "@effect/vitest": "catalog:", | 20 | "@effect/vitest": "catalog:", |