diff --git a/AGENTS.md b/AGENTS.md
index 812a8e5c8..5e159e662 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -61,6 +61,7 @@ On a machine that already has clones of Makepad, OctoScript or Octoscript-Makepa
9. **Keep docs honest and bilingual.** Every command in a doc was run; anything not run is marked **unverified**. User-facing docs are `README.md` plus `README.zh-CN.md`, linked by the single switcher line under the title (`English | [简体中文](README.zh-CN.md)`), and change together. `AGENTS.md` files are English only.
10. **Keep signing keys, keystores, tokens and personal paths out of the repository.** `rom/tests/test_no_local_paths.py` checks for local paths.
11. **App icon shapes belong to the shell style.** Use `octosense::style::AppIconDraw` on shell app-icon surfaces; never draw a bundle PNG/SVG directly. Keep new identities in the shared platform frame policy (`crates/shell/src/octosense/icon_frame.rs`), including App Hub and Assistant. Supply square artwork with its important content in the central safe area, without baked platform corners or shadows. Verify Android, macOS and iOS styles, including PNG/SVG and transparent artwork. Native Android package icons already have the system's mask and badges; do not mask them again. See [app icon guidelines](apps/README.md#launcher-icon-artwork).
+ New app artwork follows the [approved icon family](docs/design/icon-family/README.md#authoring-a-new-app-icon): a 64-point square master, foreground inside the central 66%, one dominant illustrated object, restrained ivory/ink materials and supporting accents. Give each new app its own identity; never ship the starter icon or copy another app's mark. Store apps ship their own declared bundle icon; only built-in replacements enter the local `icon_family.rs` overlay. Keep Makepad's theme files and runtime patches unchanged.
12. **Capability declarations describe usage.** `capabilities` and `network.hosts` do not grant or deny execution of public APIs. Apply this consistently to Splash, app-agent tools and components. Keep verified bundle and profile identity, device consent, OS permissions, account scope, trusted write review, storage jail/quota and inter-app tool sharing. `requires` and `host_api.required` remain compatibility checks. Capture intent is explicit (`audio`/`library` options), never inferred from API availability. See [capabilities and execution boundaries](docs/capabilities.md).
## Architecture documentation and code walkthroughs
diff --git a/README.md b/README.md
index bf5bdfe2d..23ebec492 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,12 @@ The script apps share [responsive, theme-aware interface styles](apps/README.md#
across desktop and phone. [Local native UX checks](tools/app-ux/README.md) cover
saved state and actual interaction; phone-size previews remain separate from device acceptance.
+The [icon design system](docs/design/icon-family/README.md) defines a shared
+visual language and provides 28 SVG replacements plus the original Rinx icon, with before/after and size
+reviews. The approved icons are connected to the shared shell and system
+bundles in source builds, including Browser's C5 Night rose compass. A local
+catalog overlay keeps Makepad's icon files untouched and preserves new upstream identities.
+
| Product | What it is | Where |
| --- | --- | --- |
| **OctoSense desktop** | The shell as one Makepad window on macOS, Windows or Linux; platform requirements and validation are recorded in the release notes | [`desktop/`](desktop/README.md) |
diff --git a/README.zh-CN.md b/README.zh-CN.md
index f84fa73ca..5ffa54590 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -22,6 +22,8 @@ OctoSense 是运行在普通操作系统之上的 Agent Shell。从屏幕上看
脚本应用在桌面和手机端共用[自适应主题与布局](apps/README.zh-CN.md#共用外观)。[本机原生 UX 检查](tools/app-ux/README.zh-CN.md)验证真实交互和保存状态;手机尺寸预览与真机验收分别记录。
+[图标设计规范](docs/design/icon-family/README.zh-CN.md)定义共用的视觉语言,并提供 28 个 SVG 替换图标与 Rinx 原始图标,附前后对比、尺寸检查图。批准的图标已接入源码构建的共享 Shell 及系统应用包,包括浏览器的 C5 夜色罗盘。本地图标覆盖层保持 Makepad 图标文件原样,并保留上游新增的应用身份。
+
| 产品 | 是什么 | 位置 |
| --- | --- | --- |
| **OctoSense 桌面端** | 在 macOS、Windows 或 Linux 上作为一个 Makepad 窗口运行的 Shell;各平台要求和验收范围见发行说明 | [`desktop/`](desktop/README.zh-CN.md) |
diff --git a/apps/README.md b/apps/README.md
index a895c14b2..ee09e650b 100644
--- a/apps/README.md
+++ b/apps/README.md
@@ -249,8 +249,10 @@ See [capabilities and execution boundaries](../docs/capabilities.md).
engine" and keep the draft; `writer/tests/ui.py` runs every screen there,
light and dark (see Writer's tests, below). Writer is desktop only, so
there is no phone run.
-- Camera and AI providers ship PNG launcher artwork; YouTube, Quick Deck and
- Writer ship SVG artwork. The shell frames bundle icons for the selected platform style.
+- Camera and AI providers ship PNG launcher artwork; Mail, Calendar, YouTube,
+ Quick Deck, PDF Tools and Writer ship SVG artwork. The shell frames bundle
+ icons for the selected platform style. Native and optional app identities
+ use the [approved icon family](../docs/design/icon-family/README.md).
## How the shells pack them
@@ -408,6 +410,20 @@ switching styles updates installed icons too. Review Android, macOS and iOS
at launcher and badge sizes, on light and dark backgrounds. The shell's
icon-frame/style tests run in the Phone workflow.
+OctoSense's [approved family](../docs/design/icon-family/README.md), including
+Browser's C5 Night rose compass, overlays the Makepad catalog by explicit app ID
+in `crates/shell/src/octosense/icon_family.rs`. These local replacements win over
+system bundle icons in every style. Other bundles and installed `hub:` apps keep
+their own artwork. Keep replacement assets under the shared shell's resources;
+updating Makepad must not require editing its theme icons or reapplying an icon patch.
+
+Every new app needs a distinct icon following the
+[new-icon authoring checklist](../docs/design/icon-family/README.md#authoring-a-new-app-icon).
+Use the same illustrated materials and safe area as the approved family; the
+template icon demonstrates the format and must be replaced before submission.
+Store apps declare their own bundle artwork in `listing.json`; they do not need
+an entry in the shell's built-in replacement list.
+
For a visual regression check, build the `icon_shapes` example from `phone/`
with `mobile-apps`. Run it with an isolated `OCTOSENSE_HOME`, hidden windows
and the Makepad remote control surface (see the root `AGENTS.md`). It shows
diff --git a/apps/README.zh-CN.md b/apps/README.zh-CN.md
index 57f923036..60a3cc28e 100644
--- a/apps/README.zh-CN.md
+++ b/apps/README.zh-CN.md
@@ -191,7 +191,7 @@ WebView 页面、图库导出、邮件、新闻、Glance 或单次模型调用
切换后输入的文字也被保留并保存。`card-host` 没有 `word` 服务,保存和预览在那里回答
“Writer can't … here: this device has no word engine”并保留草稿;`writer/tests/ui.py` 在那里以浅色和深色运行
每个界面(见下文的 Writer 的测试)。Writer 只在桌面端,所以没有手机上的运行。
-- Camera 和 AI providers 自带 PNG 启动器图案,YouTube、Quick Deck 和 Writer 自带 SVG 图案;Shell 按当前平台风格统一绘制外形。
+- Camera 和 AI providers 自带 PNG 启动器图案,Mail、Calendar、YouTube、Quick Deck、PDF Tools 和 Writer 自带 SVG 图案;Shell 按当前平台风格统一绘制外形。原生和可选应用使用[已批准的图标家族](../docs/design/icon-family/README.zh-CN.md)。
## Shell 如何打包它们
@@ -330,6 +330,10 @@ PNG/SVG 在绘制时使用同一外框策略,因此切换风格也会更新已
图标。检查 Android、macOS、iOS 风格下的启动器和小标识尺寸,以及明暗
背景。外框和风格回归测试随 Phone 工作流执行。
+OctoSense 的[已批准图标家族](../docs/design/icon-family/README.zh-CN.md)(包括浏览器的 C5 夜色罗盘)通过 `crates/shell/src/octosense/icon_family.rs` 中的明确应用 ID 覆盖 Makepad 图标目录。这些本地替换在所有样式下均优先于系统应用包图标;其他应用包及已安装的 `hub:` 应用保留自己的图案。替换资源放在共享 Shell 的资源目录中;更新 Makepad 不应需要修改其主题图标或重新应用图标补丁。
+
+每个新应用都需要独立的图标,并遵循[新图标创作清单](../docs/design/icon-family/README.zh-CN.md#为新应用创作图标)。沿用已批准家族的插画材质与安全区域;模板图标只演示格式,提交前必须替换。商店应用在 `listing.json` 中声明自己随包发布的图案,无需加入 Shell 的内置替换清单。
+
需要视觉回归检查时,在 `phone/` 中启用 `mobile-apps` 构建 `icon_shapes`
示例。使用隔离的 `OCTOSENSE_HOME`、隐藏窗口和 Makepad 远程控制接口运行
(参见根目录 `AGENTS.md`)。示例覆盖七种风格、内置和商店 PNG/SVG 图案、
diff --git a/apps/calendar/bundle/icon.svg b/apps/calendar/bundle/icon.svg
new file mode 100644
index 000000000..f970951ed
--- /dev/null
+++ b/apps/calendar/bundle/icon.svg
@@ -0,0 +1 @@
+
diff --git a/apps/calendar/bundle/manifest.json b/apps/calendar/bundle/manifest.json
index 0155da1a3..399d7996e 100644
--- a/apps/calendar/bundle/manifest.json
+++ b/apps/calendar/bundle/manifest.json
@@ -1,7 +1,7 @@
{
"schema": 1,
"id": "os.calendar",
- "version": "1.0.0",
+ "version": "1.0.1",
"name": "Calendar",
"integrity": {
"bundle_blake3": ""
diff --git a/apps/mail/bundle/icon.svg b/apps/mail/bundle/icon.svg
new file mode 100644
index 000000000..2ef0835b8
--- /dev/null
+++ b/apps/mail/bundle/icon.svg
@@ -0,0 +1 @@
+
diff --git a/apps/mail/bundle/manifest.json b/apps/mail/bundle/manifest.json
index e66085aa9..65d8e77b8 100644
--- a/apps/mail/bundle/manifest.json
+++ b/apps/mail/bundle/manifest.json
@@ -1,7 +1,7 @@
{
"schema": 1,
"id": "os.mail",
- "version": "1.0.0",
+ "version": "1.0.1",
"name": "Mail",
"integrity": {
"bundle_blake3": ""
diff --git a/crates/shell/examples/icon_shapes.rs b/crates/shell/examples/icon_shapes.rs
index dc9cd5fbb..1d4636ee1 100644
--- a/crates/shell/examples/icon_shapes.rs
+++ b/crates/shell/examples/icon_shapes.rs
@@ -1,5 +1,6 @@
//! Visual regression sheet using the production shell renderer.
//! Run from phone/ with mobile-apps so Camera is in the bundled catalog.
+//! --family-page=0 through 3 reviews the approved family through this same renderer.
pub use makepad_widgets;
use makepad_widgets::*;
use octosense_shell::octosense::style::{AppIconDraw, DesktopStyle};
@@ -41,7 +42,7 @@ impl AppMain for App {
let root = std::env::var_os("OCTOSENSE_HOME").expect("set an isolated OCTOSENSE_HOME for this preview");
let root = std::path::PathBuf::from(root).join("icon-fixtures");
for (id, icon) in [("shape-svg", "icon.svg"), ("shape-png", "icon.png")] {
- let bundle = root.join(id).join("bundle");
+ let bundle = octosense_app_hub::installed_bundle_dir(&root, id);
std::fs::create_dir_all(&bundle).unwrap();
std::fs::write(bundle.join("listing.json"), serde_json::json!({
"schema":1, "description":"Icon shape regression fixture", "category":"utilities",
@@ -83,7 +84,7 @@ impl Widget for IconSheet {
fn draw_walk(&mut self, cx: &mut Cx2d, _: &mut Scope, walk: Walk) -> DrawStep {
let canvas = cx.walk_turtle(walk);
self.background.draw_abs(cx, canvas);
- let names = ["photos", "apphub", "assistant", "camera", "ai-providers", "youtube", "hub:shape-svg", "hub:shape-png"];
+ let names = preview_names();
for (row, style) in [DesktopStyle::Android, DesktopStyle::Macos, DesktopStyle::Ios,
DesktopStyle::Windows, DesktopStyle::NextStep, DesktopStyle::Omarchy, DesktopStyle::Windows2000].into_iter().enumerate() {
let y = canvas.pos.y + 32. + row as f64 * 120.;
@@ -93,10 +94,26 @@ impl Widget for IconSheet {
if row == 0 { self.text.draw_abs(cx, dvec2(x - 8., canvas.pos.y + 8.), name.strip_prefix("hub:").unwrap_or(name)); }
for (size, dx, dy) in [(64., 0., 0.), (24., 0., 74.), (24., 34., 74.)] {
self.icons.draw(cx, name, style, Rect { pos: dvec2(x + dx, y + dy), size: dvec2(size, size) },
- if dx > 0. {0.5} else {1.}, vec4(0.9, 0.94, 1., 1.));
+ if dx > 0. {0.5} else {1.},
+ if dy > 0. {vec4(0.14, 0.19, 0.27, 1.)} else {vec4(0.9, 0.94, 1., 1.)});
}
}
}
DrawStep::done()
}
}
+
+fn preview_names() -> [&'static str; 8] {
+ static NAMES: std::sync::OnceLock<[&'static str; 8]> = std::sync::OnceLock::new();
+ *NAMES.get_or_init(|| {
+ let pages = [
+ ["mail", "calendar", "calculator", "clock", "notes", "reminders", "weather", "sheets"],
+ ["settings", "updater", "terminal", "octoscode", "rinx", "reference", "appcard", "browser"],
+ ["files", "task", "finance", "route", "image", "pdf", "mixer", "fabric"],
+ ["score", "video", "vj", "fab", "studio", "photos", "apphub", "assistant"],
+ ];
+ let page = std::env::args().find_map(|arg| arg.strip_prefix("--family-page=").and_then(|n| n.parse::().ok()));
+ page.map(|n| *pages.get(n).expect("family page must be 0 through 3"))
+ .unwrap_or(["photos", "apphub", "assistant", "camera", "ai-providers", "youtube", "hub:shape-svg", "hub:shape-png"])
+ })
+}
diff --git a/crates/shell/resources/icons/apps/family/appcard.svg b/crates/shell/resources/icons/apps/family/appcard.svg
new file mode 100644
index 000000000..3fef290bf
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/appcard.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/browser.svg b/crates/shell/resources/icons/apps/family/browser.svg
new file mode 100644
index 000000000..c628da844
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/browser.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/calculator.svg b/crates/shell/resources/icons/apps/family/calculator.svg
new file mode 100644
index 000000000..738b97964
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/calculator.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/calendar.svg b/crates/shell/resources/icons/apps/family/calendar.svg
new file mode 100644
index 000000000..f970951ed
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/calendar.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/clock.svg b/crates/shell/resources/icons/apps/family/clock.svg
new file mode 100644
index 000000000..6762addb2
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/clock.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/fab.svg b/crates/shell/resources/icons/apps/family/fab.svg
new file mode 100644
index 000000000..f9419d533
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/fab.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/fabric.svg b/crates/shell/resources/icons/apps/family/fabric.svg
new file mode 100644
index 000000000..9bd71b469
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/fabric.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/files.svg b/crates/shell/resources/icons/apps/family/files.svg
new file mode 100644
index 000000000..05f0172ba
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/files.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/finance.svg b/crates/shell/resources/icons/apps/family/finance.svg
new file mode 100644
index 000000000..dd2216e77
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/finance.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/image.svg b/crates/shell/resources/icons/apps/family/image.svg
new file mode 100644
index 000000000..a893d8490
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/image.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/mail.svg b/crates/shell/resources/icons/apps/family/mail.svg
new file mode 100644
index 000000000..2ef0835b8
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/mail.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/mixer.svg b/crates/shell/resources/icons/apps/family/mixer.svg
new file mode 100644
index 000000000..bbd670b30
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/mixer.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/notes.svg b/crates/shell/resources/icons/apps/family/notes.svg
new file mode 100644
index 000000000..35b29f765
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/notes.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/octoscode.svg b/crates/shell/resources/icons/apps/family/octoscode.svg
new file mode 100644
index 000000000..fe9df68c3
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/octoscode.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/appcard.svg b/crates/shell/resources/icons/apps/family/omarchy/appcard.svg
new file mode 100644
index 000000000..8828d67ac
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/appcard.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/browser.svg b/crates/shell/resources/icons/apps/family/omarchy/browser.svg
new file mode 100644
index 000000000..d15fb73ac
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/browser.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/calculator.svg b/crates/shell/resources/icons/apps/family/omarchy/calculator.svg
new file mode 100644
index 000000000..c7bf2bc0a
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/calculator.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/calendar.svg b/crates/shell/resources/icons/apps/family/omarchy/calendar.svg
new file mode 100644
index 000000000..e70f61d2c
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/calendar.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/clock.svg b/crates/shell/resources/icons/apps/family/omarchy/clock.svg
new file mode 100644
index 000000000..c6afc6868
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/clock.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/fab.svg b/crates/shell/resources/icons/apps/family/omarchy/fab.svg
new file mode 100644
index 000000000..0b5377b67
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/fab.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/fabric.svg b/crates/shell/resources/icons/apps/family/omarchy/fabric.svg
new file mode 100644
index 000000000..9f370810a
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/fabric.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/files.svg b/crates/shell/resources/icons/apps/family/omarchy/files.svg
new file mode 100644
index 000000000..e3858f9ca
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/files.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/finance.svg b/crates/shell/resources/icons/apps/family/omarchy/finance.svg
new file mode 100644
index 000000000..20eb8e20d
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/finance.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/image.svg b/crates/shell/resources/icons/apps/family/omarchy/image.svg
new file mode 100644
index 000000000..4f7768f0f
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/image.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/mail.svg b/crates/shell/resources/icons/apps/family/omarchy/mail.svg
new file mode 100644
index 000000000..a888b7c9f
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/mail.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/mixer.svg b/crates/shell/resources/icons/apps/family/omarchy/mixer.svg
new file mode 100644
index 000000000..1ab61ede2
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/mixer.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/notes.svg b/crates/shell/resources/icons/apps/family/omarchy/notes.svg
new file mode 100644
index 000000000..e5a409d8f
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/notes.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/octoscode.svg b/crates/shell/resources/icons/apps/family/omarchy/octoscode.svg
new file mode 100644
index 000000000..49b68451e
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/octoscode.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/pdf.svg b/crates/shell/resources/icons/apps/family/omarchy/pdf.svg
new file mode 100644
index 000000000..96089f7de
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/pdf.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/reference.svg b/crates/shell/resources/icons/apps/family/omarchy/reference.svg
new file mode 100644
index 000000000..dada4f487
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/reference.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/reminders.svg b/crates/shell/resources/icons/apps/family/omarchy/reminders.svg
new file mode 100644
index 000000000..b744fe70d
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/reminders.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/route.svg b/crates/shell/resources/icons/apps/family/omarchy/route.svg
new file mode 100644
index 000000000..e20141b17
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/route.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/score.svg b/crates/shell/resources/icons/apps/family/omarchy/score.svg
new file mode 100644
index 000000000..611122dc7
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/score.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/settings.svg b/crates/shell/resources/icons/apps/family/omarchy/settings.svg
new file mode 100644
index 000000000..3e3a60e46
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/settings.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/sheets.svg b/crates/shell/resources/icons/apps/family/omarchy/sheets.svg
new file mode 100644
index 000000000..3ed67e7a4
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/sheets.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/studio.svg b/crates/shell/resources/icons/apps/family/omarchy/studio.svg
new file mode 100644
index 000000000..d624c3733
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/studio.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/task.svg b/crates/shell/resources/icons/apps/family/omarchy/task.svg
new file mode 100644
index 000000000..c520fcf64
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/task.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/terminal.svg b/crates/shell/resources/icons/apps/family/omarchy/terminal.svg
new file mode 100644
index 000000000..03adc196f
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/terminal.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/updater.svg b/crates/shell/resources/icons/apps/family/omarchy/updater.svg
new file mode 100644
index 000000000..957fe55b7
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/updater.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/video.svg b/crates/shell/resources/icons/apps/family/omarchy/video.svg
new file mode 100644
index 000000000..f64370dcc
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/video.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/vj.svg b/crates/shell/resources/icons/apps/family/omarchy/vj.svg
new file mode 100644
index 000000000..bf04f1bd8
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/vj.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/omarchy/weather.svg b/crates/shell/resources/icons/apps/family/omarchy/weather.svg
new file mode 100644
index 000000000..9f0394645
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/omarchy/weather.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/pdf.svg b/crates/shell/resources/icons/apps/family/pdf.svg
new file mode 100644
index 000000000..3ba3ae7fa
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/pdf.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/reference.svg b/crates/shell/resources/icons/apps/family/reference.svg
new file mode 100644
index 000000000..0b06f1277
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/reference.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/reminders.svg b/crates/shell/resources/icons/apps/family/reminders.svg
new file mode 100644
index 000000000..ac8ea4481
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/reminders.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/rinx-attribution/LICENSE b/crates/shell/resources/icons/apps/family/rinx-attribution/LICENSE
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/rinx-attribution/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/crates/shell/resources/icons/apps/family/rinx-attribution/LICENSE-MIT b/crates/shell/resources/icons/apps/family/rinx-attribution/LICENSE-MIT
new file mode 100644
index 000000000..7f77de13d
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/rinx-attribution/LICENSE-MIT
@@ -0,0 +1,9 @@
+The MIT License (MIT)
+
+Copyright (c) 2023-2026 Project Robius Developers
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/crates/shell/resources/icons/apps/family/rinx-attribution/NOTICE b/crates/shell/resources/icons/apps/family/rinx-attribution/NOTICE
new file mode 100644
index 000000000..ea0489236
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/rinx-attribution/NOTICE
@@ -0,0 +1,24 @@
+Rinx
+Copyright 2026 Rinx Contributors
+https://github.com/upstreamlabs/Rinx
+
+Rinx is an independent derivative of Robrix and Robrix2, extracted from
+OctoSense-org/robrix2's wechat-ui branch at commit
+16913e0c6f0397b4ed1272b7dcd421935521d5f3.
+
+With gratitude to Kevin Boos, Project Robius, the Robrix2 contributors,
+and all upstream contributors for their work on native Rust Matrix clients.
+
+Robrix: https://github.com/project-robius/robrix
+Robrix2: https://github.com/Project-Robius-China/robrix2
+Copyright (c) 2023-2026 Project Robius Developers
+The inherited MIT copyright and permission notice is preserved in LICENSE-MIT.
+
+Rinx is distributed under the Apache License, Version 2.0 (LICENSE).
+Dependencies, copied components, fonts, artwork, and other third-party assets
+retain their respective licenses and attribution requirements. See licenses/
+and crates/article-makepad/MAKEPAD-LICENSE.txt for retained notices.
+
+Rinx modifications include the WeChat-style UI, English/Chinese localization,
+Matrix Moments, scoped mini-app and article-editor integration, native
+makepad-html/Blitz previews, and the independent Rinx application identity.
diff --git a/crates/shell/resources/icons/apps/family/rinx-attribution/provenance.json b/crates/shell/resources/icons/apps/family/rinx-attribution/provenance.json
new file mode 100644
index 000000000..0e4fde5c6
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/rinx-attribution/provenance.json
@@ -0,0 +1,12 @@
+{
+ "repository": "https://github.com/hagency-org/Rinx.git",
+ "revision": "4b89097d8791a7190d01de1c576979c93df0013d",
+ "treatment": "Original upstream PNGs copied byte-for-byte; no recoloring or redraw.",
+ "files": [
+ {
+ "source": "resources/icon_1024.png",
+ "local": "../rinx.png",
+ "sha256": "a5db71934b2fd2e8177637527db2d925c01bbe9bb046aecf0d2eddc5bef43329"
+ }
+ ]
+}
diff --git a/crates/shell/resources/icons/apps/family/rinx.png b/crates/shell/resources/icons/apps/family/rinx.png
new file mode 100644
index 000000000..8d9ea6bf8
Binary files /dev/null and b/crates/shell/resources/icons/apps/family/rinx.png differ
diff --git a/crates/shell/resources/icons/apps/family/route.svg b/crates/shell/resources/icons/apps/family/route.svg
new file mode 100644
index 000000000..1893e23e4
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/route.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/score.svg b/crates/shell/resources/icons/apps/family/score.svg
new file mode 100644
index 000000000..72e55b66f
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/score.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/settings.svg b/crates/shell/resources/icons/apps/family/settings.svg
new file mode 100644
index 000000000..36c3c56e9
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/settings.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/sheets.svg b/crates/shell/resources/icons/apps/family/sheets.svg
new file mode 100644
index 000000000..6df194ae1
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/sheets.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/studio.svg b/crates/shell/resources/icons/apps/family/studio.svg
new file mode 100644
index 000000000..1a005c079
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/studio.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/task.svg b/crates/shell/resources/icons/apps/family/task.svg
new file mode 100644
index 000000000..c2cddb570
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/task.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/terminal.svg b/crates/shell/resources/icons/apps/family/terminal.svg
new file mode 100644
index 000000000..111bfea6b
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/terminal.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/updater.svg b/crates/shell/resources/icons/apps/family/updater.svg
new file mode 100644
index 000000000..19770f5a4
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/updater.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/video.svg b/crates/shell/resources/icons/apps/family/video.svg
new file mode 100644
index 000000000..767eb9eee
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/video.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/vj.svg b/crates/shell/resources/icons/apps/family/vj.svg
new file mode 100644
index 000000000..ee0fe30be
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/vj.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/resources/icons/apps/family/weather.svg b/crates/shell/resources/icons/apps/family/weather.svg
new file mode 100644
index 000000000..0a772dad6
--- /dev/null
+++ b/crates/shell/resources/icons/apps/family/weather.svg
@@ -0,0 +1 @@
+
diff --git a/crates/shell/src/apps.rs b/crates/shell/src/apps.rs
index 5a25f64af..76fcc946d 100644
--- a/crates/shell/src/apps.rs
+++ b/crates/shell/src/apps.rs
@@ -1256,6 +1256,13 @@ mod tests {
if system_app_ids().contains(&"camera") {
assert!(octosense_app_hub_app::system_icon("camera").is_some(), "Camera ships its own icon");
}
+ for (id, title) in [("mail", "Mail"), ("calendar", "Calendar")] {
+ if !system_app_ids().contains(&id) { continue; }
+ let Some(octosense_app_hub_app::icons::IconData::Svg(svg)) = octosense_app_hub_app::system_icon(id) else {
+ panic!("{id} must ship its approved bundle SVG");
+ };
+ assert!(svg.contains(&format!("{title}")));
+ }
}
/// native-apps.json is the one declaration: every native module this
diff --git a/crates/shell/src/octosense/icon_family.rs b/crates/shell/src/octosense/icon_family.rs
new file mode 100644
index 000000000..9f982479e
--- /dev/null
+++ b/crates/shell/src/octosense/icon_family.rs
@@ -0,0 +1,81 @@
+//! Approved OctoSense identities, shared by desktop and Home.
+//! Regenerate resource copies with `python3 tools/build_icon_family.py --install`.
+use makepad_widgets::{app_icon, desktop_style::DesktopStyle};
+use super::icon_frame;
+
+macro_rules! icon {
+ ($name:literal) => {
+ ($name,
+ include_str!(concat!("../../resources/icons/apps/family/", $name, ".svg")),
+ include_str!(concat!("../../resources/icons/apps/family/omarchy/", $name, ".svg")))
+ };
+}
+
+pub(super) const ICONS: &[(&str, &str, &str)] = &[
+ icon!("mail"), icon!("calendar"), icon!("calculator"), icon!("clock"),
+ icon!("notes"), icon!("reminders"), icon!("weather"), icon!("sheets"),
+ icon!("settings"), icon!("updater"), icon!("terminal"), icon!("octoscode"),
+ icon!("reference"), icon!("appcard"), icon!("browser"), icon!("files"),
+ icon!("task"), icon!("finance"), icon!("route"), icon!("image"),
+ icon!("pdf"), icon!("mixer"), icon!("fabric"), icon!("score"),
+ icon!("video"), icon!("vj"), icon!("fab"), icon!("studio"),
+];
+
+fn artwork(style: DesktopStyle) -> impl Iterator {
+ ICONS.iter().map(move |&(name, source, mono)| {
+ let svg = if style == DesktopStyle::Omarchy {
+ mono.to_string()
+ } else {
+ icon_frame::styled_svg(source, style)
+ };
+ (name, svg)
+ })
+}
+
+/// Overlay the explicit local identities onto an otherwise untouched catalog.
+/// Upstream can change these images or add new IDs without an asset merge here.
+pub(super) fn apply(assets: &mut Vec, style: DesktopStyle) {
+ for (name, svg) in artwork(style) {
+ match assets.iter_mut().find(|asset| asset.name == name) {
+ Some(asset) => asset.svg = svg,
+ None => assets.push(app_icon::IconAsset { name: name.into(), svg }),
+ }
+ }
+}
+
+/// Explicit built-in replacements win over system bundle art in every style.
+/// Installed `hub:` names and identities outside this list keep their own art.
+pub(super) fn owns_svg(name: &str) -> bool {
+ ICONS.iter().any(|&(id, _, _)| id == app_icon::canonical_name(name))
+}
+
+/// Native identity only: never intercept an installed app's `hub:` namespace.
+pub(super) fn png(name: &str) -> Option<&'static [u8]> {
+ (makepad_widgets::app_icon::canonical_name(name) == "rinx")
+ .then_some(include_bytes!("../../resources/icons/apps/family/rinx.png").as_slice())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn upstream_changes_and_new_identities_survive_the_local_overlay() {
+ for style in DesktopStyle::ALL {
+ let mut assets = vec![
+ app_icon::IconAsset { name: "browser".into(), svg: "changed upstream Browser".into() },
+ app_icon::IconAsset { name: "photos".into(), svg: "upstream Photos".into() },
+ app_icon::IconAsset { name: "future-app".into(), svg: "new upstream identity".into() },
+ ];
+ apply(&mut assets, style);
+ let source = |name| &assets.iter().find(|asset| asset.name == name).unwrap().svg;
+ assert_eq!(source("photos"), "upstream Photos");
+ assert_eq!(source("future-app"), "new upstream identity");
+ assert_eq!(source("browser"), &artwork(style).find(|(name, _)| *name == "browser").unwrap().1);
+ assert_eq!(assets.len(), ICONS.len() + 2);
+ let original = assets.clone();
+ apply(&mut assets, style);
+ assert_eq!(assets, original, "reapplying must not duplicate or change identities");
+ }
+ }
+}
diff --git a/crates/shell/src/octosense/icon_frame.rs b/crates/shell/src/octosense/icon_frame.rs
index a836dbebb..04ca563c5 100644
--- a/crates/shell/src/octosense/icon_frame.rs
+++ b/crates/shell/src/octosense/icon_frame.rs
@@ -46,12 +46,14 @@ impl IconFrame {
// These shell-owned SVGs share an explicit tile element. Keep foreground
// identity and gradients intact; don't try to rewrite arbitrary store SVGs.
const SOURCE_TILE: &str = r#""#;
+const SOURCE_SQUARE_TILE: &str = r#""#;
pub(super) const SHELL_ART_MARKER: &str = "";
pub(super) fn styled_svg(source: &str, style: DesktopStyle) -> String {
- debug_assert!(source.contains(SOURCE_TILE), "shell icon needs the shared tile element");
+ let source_tile = if source.contains(SOURCE_SQUARE_TILE) { SOURCE_SQUARE_TILE } else { SOURCE_TILE };
+ debug_assert!(source.contains(source_tile), "shell icon needs the shared tile element");
let tile = IconFrame::for_style(style).map_or_else(String::new, IconFrame::svg);
- source.replacen(SOURCE_TILE, &format!("{SHELL_ART_MARKER}{tile}"), 1)
+ source.replacen(source_tile, &format!("{SHELL_ART_MARKER}{tile}"), 1)
}
script_mod! {
@@ -178,6 +180,19 @@ pub(super) fn placement(style: DesktopStyle, canvas: Rect) -> (Rect, f32, Vec4f)
mod tests {
use super::*;
+ #[test]
+ fn approved_square_sources_use_the_shared_frame() {
+ let source = r##""##;
+ for style in DesktopStyle::ALL {
+ let framed = styled_svg(source, style);
+ assert!(!framed.contains(SOURCE_SQUARE_TILE));
+ assert!(framed.contains("r=\"10\" fill=\"#fffdf7\""), "the foreground identity must be preserved");
+ if style == DesktopStyle::Android {
+ assert!(framed.contains("r=\"31\" fill=\"url(#tile)\""));
+ }
+ }
+ }
+
#[test]
fn frames_match_the_platform_catalog_and_keep_freeform_themes() {
let canvas = Rect { pos: dvec2(10., 20.), size: dvec2(64., 64.) };
diff --git a/crates/shell/src/octosense/mod.rs b/crates/shell/src/octosense/mod.rs
index 4da63f2e6..98312f6e2 100644
--- a/crates/shell/src/octosense/mod.rs
+++ b/crates/shell/src/octosense/mod.rs
@@ -2,6 +2,7 @@ pub mod catalog;
pub mod makepad_source;
pub mod style;
pub(crate) mod icon_frame;
+mod icon_family;
pub mod paths;
pub mod policy;
pub mod retired_passes;
diff --git a/crates/shell/src/octosense/style.rs b/crates/shell/src/octosense/style.rs
index 7cac403fe..3ddb01553 100644
--- a/crates/shell/src/octosense/style.rs
+++ b/crates/shell/src/octosense/style.rs
@@ -1,8 +1,9 @@
//! OctoSense's additional desktop style, layered on the upstream widget API.
use makepad_widgets::{app_icon, desktop_style::{DesktopStyle as UpstreamStyle, StyleSheet}, *};
-use super::icon_frame;
+use super::{icon_family, icon_frame};
+use super::icon_frame::DrawIconImage;
#[cfg(any(feature = "app-hub", native_mobile))]
-use super::icon_frame::{DrawIconImage, SvgIconTexture};
+use super::icon_frame::SvgIconTexture;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DesktopStyle {
@@ -86,8 +87,7 @@ pub fn load_sheet(style: DesktopStyle, dark: bool) -> StyleSheet {
}
}
-/// The framework's artwork for a style with this shell's own laid over it:
-/// News as redrawn here, and OctosMap, which the framework has no art for.
+/// The framework's artwork with approved OctoSense identities laid over it.
pub fn icon_assets(style: UpstreamStyle) -> Vec {
fn wear(assets: &mut Vec, name: &str, svg: String) {
match assets.iter_mut().find(|asset| asset.name == name) {
@@ -110,6 +110,7 @@ pub fn icon_assets(style: UpstreamStyle) -> Vec {
}
}
}
+ icon_family::apply(&mut assets, style);
wear(&mut assets, "apphub", icon_frame::styled_svg(apphub_svg(), style));
// The system chat's dock entry and home chip (#143): its own art, so it
// never reads as the AI pane's app.
@@ -157,6 +158,7 @@ fn own_artwork(style: UpstreamStyle) -> Option<[&'static str; 2]> {
#[derive(Default)]
pub struct AppIconDraw {
draw: app_icon::AppIconDraw,
+ original_png: Option<(DrawIconImage, Texture)>,
#[cfg(any(feature = "app-hub", native_mobile))]
library: InstalledIcons,
/// The styles whose catalog this drawer has seen to, by discriminant.
@@ -165,7 +167,25 @@ pub struct AppIconDraw {
impl AppIconDraw {
pub fn draw(&mut self, cx: &mut Cx2d, name: &str, style: DesktopStyle, rect: Rect, opacity: f32, ink: Vec4f) {
#[cfg(any(feature = "app-hub", native_mobile))]
- if self.library.draw(cx, name, style.framework(), rect, opacity) { return; }
+ if !icon_family::owns_svg(name)
+ && self.library.draw(cx, name, style.framework(), rect, opacity) { return; }
+ if let Some(bytes) = icon_family::png(name) {
+ let (draw, texture) = self.original_png.get_or_insert_with(|| {
+ let buffer = image_cache::ImageBuffer::from_png(bytes).expect("bundled Rinx icon is valid PNG");
+ let size = vec2(buffer.width as f32, buffer.height as f32);
+ let texture = buffer.into_new_texture(cx);
+ let mut draw = cx.with_vm(|vm| DrawIconImage::script_new_with_default(vm));
+ draw.source_size = size;
+ (draw, texture)
+ });
+ let (rect, radius, backing) = icon_frame::placement(style.framework(), rect);
+ draw.draw_vars.set_texture(0, texture);
+ draw.radius = radius;
+ draw.backing = backing;
+ draw.opacity = opacity;
+ draw.draw_abs(cx, rect);
+ return;
+ }
let style = style.framework();
// A style can be drawn before its sheet is applied (a crossfade's
// target, the first frame); the framework would then fall back to
@@ -256,6 +276,57 @@ mod tests {
&assets.iter().find(|asset| asset.name == name).unwrap_or_else(|| panic!("no {name} icon")).svg
}
+ #[test]
+ fn approved_family_replaces_generic_catalog_artwork() {
+ for style in UpstreamStyle::ALL {
+ let assets = icon_assets(style);
+ for (name, title) in [
+ ("clock", "Clock"), ("sheets", "Sheets"), ("settings", "Settings"),
+ ("updater", "OctoSense Updates"), ("octoscode", "OctosCode"),
+ ("reference", "Reference"), ("appcard", "AppCard"),
+ ("mail", "Mail"), ("calendar", "Calendar"),
+ ] {
+ assert!(svg_of(&assets, name).contains(&format!("{title}")),
+ "{} {name} must use the approved family", style.id());
+ }
+ assert_eq!(svg_of(&assets, "photos"), svg_of(&app_icon::load_assets(style), "photos"));
+ }
+ }
+
+ #[test]
+ fn approved_family_is_complete_and_parses_in_the_production_renderer() {
+ assert_eq!(icon_family::ICONS.len(), 28);
+ for style in UpstreamStyle::ALL {
+ let assets = icon_assets(style);
+ for &(name, _, _) in icon_family::ICONS {
+ let svg = svg_of(&assets, name);
+ let doc = makepad_widgets::makepad_draw::svg::parse_svg(svg);
+ assert_eq!(doc.logical_size(), (64., 64.), "{} {name}", style.id());
+ assert!(!doc.root.is_empty(), "{} {name}", style.id());
+ for unsupported in [" = framework.iter().map(|asset| asset.name.as_str()).collect();
+ expected.extend(icon_family::ICONS.iter().map(|&(name, _, _)| name));
+ expected.extend(["maps", "apphub", "assistant"]);
+ assert_eq!(assets.iter().map(|asset| asset.name.as_str()).collect::>(), expected, "{}", style.id());
+ assert_eq!(assets.len(), expected.len(), "identities must not be duplicated");
assert!(svg_of(&assets, "assistant").contains("OctoSense #143"), "{}", style.id());
assert!(assets.windows(2).all(|pair| pair[0].name < pair[1].name), "{}", style.id());
for asset in &framework {
- if asset.name != "news" {
+ if asset.name != "news" && !icon_family::ICONS.iter().any(|&(name, _, _)| name == asset.name) {
assert_eq!(svg_of(&assets, &asset.name), asset.svg, "{} {}", style.id(), asset.name);
}
}
diff --git a/docs/design/icon-family/README.md b/docs/design/icon-family/README.md
new file mode 100644
index 000000000..0bf9a0eaf
--- /dev/null
+++ b/docs/design/icon-family/README.md
@@ -0,0 +1,276 @@
+# OctoSense icon family
+
+English | [简体中文](README.zh-CN.md)
+
+**Approved family, 2026-10-15.** This package contains 28 original SVG replacement
+icons, the original Rinx PNG, and an icon design system derived from the existing News, Photos, App Hub,
+Maps and OctoSense artwork. The approved family is now connected to the shared
+shell catalog and the Mail/Calendar system bundles. It takes effect in builds
+from this source; existing installed releases are not updated by a source edit.
+**Browser updated, 2026-10-21:** C5 Night rose is the approved replacement,
+with an ink dial, mint rim, four-spoke rose and ivory northeast point.
+
+
+
+## How many icons are needed?
+
+**14 product identities need new artwork**, including feature-selectable native
+apps; **Rinx reuses its original icon**. **14 additional identities** appear in the optional checkout catalogs.
+Together, these are **28 new icons and one restored original**. This is a count of app identities across
+both products, not a claim that every build exposes all 29.
+
+The audit covers the union of `desktop/system-apps.json` (10 bundles),
+`phone/system-apps.json` (8 bundles; 11 unique bundles across both), active or
+feature-selectable entries in `native-apps.json`, Home's Settings, the shell's
+Assistant entry, and both `config/apps.json` catalogs. An entry with a binary
+in a checkout catalog need not be installed or visible on a given machine.
+
+Before this change, six product identities had no matching catalog artwork and resolved to the
+same default `app.svg`: **Settings, Updates, Rinx, OctosCode, Reference and
+AppCard**. The other replacements use Makepad's existing illustrations; they
+are app-specific, but have no local OctoSense identity.
+
+Rinx does have original artwork in its own repository. The shell now restores
+that icon instead of redesigning it. Its 1024 px PNG is copied byte-for-byte from
+`resources/icon_1024.png` at the revision pinned in `native-apps.json`.
+See [provenance and checksums](references/rinx/provenance.json); upstream license
+and attribution notices are retained alongside it.
+
+| Product app | Existing artwork | Proposed identity |
+| --- | --- | --- |
+| Mail | Makepad envelope | Folded ivory envelope, mint letter, coral seal |
+| Calendar | Makepad calendar | Coral flip calendar with physical binding rings |
+| Calculator | Makepad calculator | Ivory calculator, sage display, coral equals key |
+| Clock | Makepad dial | Two-color ivory dial and dark hands; no rim, feet or second hand |
+| Notes | Makepad notepad | Pocket notebook with a mint pencil |
+| Reminders | Makepad checklist | Ivory clipboard with coral completion marker |
+| Weather | Makepad sun/cloud | Butter sun, ivory cloud and two rain strokes |
+| Sheets | Makepad workbook | Ivory workbook with a green formula tab; every cell uses the same color |
+| Settings | Shared default app | Ivory plate with three tactile sliders |
+| OctoSense Updates | Shared default app | Upward install arrow between mint cycle arcs |
+| Terminal | Makepad prompt | Ink command window, mint prompt, apricot cursor |
+| OctosCode | Shared default app | Ivory code window, violet brackets, amber tab |
+| Rinx | Shared default app | Restore the original Rinx cube artwork |
+| Reference | Shared default app | Ivory reference book with apricot bookmark |
+| AppCard | Shared default app | Layered ivory cards with an inset chat panel |
+
+Reference, Sheets and AppCard have feature-dependent availability; Terminal is
+desktop-only. Settings belongs to Home. The artwork can be shared whenever the
+same identity appears on either product.
+
+The **14 optional catalog apps** are Browser, Files, Task Manager, Finance,
+Route, Image Viewer, PDF Viewer, Mixer, Fabric, Score, Video Player, VJ, Fab and
+Director (`studio`). Task's native module is disabled in both products, but its
+process entry remains in the checkout catalogs.
+
+
+
+## Existing identities to retain
+
+| Identity | Current source | Treatment |
+| --- | --- | --- |
+| News | `crates/shell/resources/icons/apps/