5 Commits

Author SHA1 Message Date
Marcos Paulo d5fb5136f7 chore: release 0.6.2
Release / release (push) Has been cancelled
Windows ARM64 / windows-arm64 (push) Has been cancelled
Publish Flatpak repository to Pages / Build Flatpak repository (release) Has been cancelled
Publish Flatpak repository to Pages / Deploy Pages (release) Has been cancelled
2026-10-12 21:41:27 -03:00
Marcos Paulo f162240fcf build: refresh Cargo.lock for the masi/craft-libs fork rev 2026-10-12 21:08:19 -03:00
Marcos Paulo 8033af3f03 build: point craft-ui/craft-print at the masi/craft-libs fork (rebased debrand) 2026-10-12 20:53:55 -03:00
Marcos Paulo 14b6964db0 feat(ui): Catppuccin themes
Adds Latte, Frappé, Macchiato and Mocha to the Display theme choices (ribbon,
File > Options, native menu, view.theme). The craft-ui dependency comes from the
masi/craft-libs fork. ThemeMode is the one typed list the menus and command share.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 20:53:23 -03:00
Marcos Paulo 61201a1173 chore: debrand ArtCraft and Discord references
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 20:53:01 -03:00
34 changed files with 1939 additions and 271 deletions
+13
View File
@@ -0,0 +1,13 @@
# Strip upstream (ArtCraft / Discord) branding. Run: perl tools/craft-skills/skills/debrand/debrand.pl . (--check in CI)
delete docs/brand
# MIT/Apache texts stay verbatim. The copyright holder line stays everywhere (legal requirement).
keep ^(LICENSE-(MIT|APACHE)|\.debrand)$
dropline ^ATTRIBUTION\.md$ :: \|\s*`docs/brand/
sub (?s)\nThe ArtCraft name, wordmark and logos in docs/brand/.*?LICENSE-brand\.txt\.\n =>
sub (?s)\nThe ArtCraft name, wordmark and logos in \[`docs/brand/`\].*?must remove them\.\n =>
sub The one exception is the ArtCraft brand files in `docs/brand/`: ArtCraft trademarks, not open source, used under `docs/brand/LICENSE-brand\.txt`\. =>
sub \| \[Website\]\(https://getartcraft\.com/apps/\w+\) (?=\|) =>
sub https://getartcraft\.com/apps/gridcraft => https://github.com/storytold/gridcraft
sub https://discord\.gg/artcraft => https://github.com/storytold/gridcraft/issues
forbid (?i)artcraft(?! Team and the GridCraft contributors)
forbid (?i)discord
+3
View File
@@ -0,0 +1,3 @@
[submodule "tools/craft-skills"]
path = tools/craft-skills
url = https://git.marcospaulo.dev.br/storytold/craft-skills.git
-1
View File
@@ -17,7 +17,6 @@ Standards and learnings shared across the crafting apps live in `../../craftrule
- Prefer art generated in code: the UI icon set (`crates/ui-egui/src/icons.rs`), table and cell style palettes, themes and sample workbooks are drawn/defined in code and original.
- Never copy Office's icons, ribbon artwork, theme XML, table-style definitions, templates, clip art or wording beyond feature names. Excel feature *names* (menu labels, function names) are fine.
- Screenshots of Microsoft software are never committed (they live only in `plan/excel/screenshots/`).
- The ArtCraft brand files in `docs/brand/` are ArtCraft trademarks under `docs/brand/LICENSE-brand.txt`, the one exception.
## Never crash
People trust GridCraft with their numbers; a crash loses their work. **This outranks feature work.** Standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md).
+1 -10
View File
@@ -2,21 +2,12 @@
Every non-code asset in this repository (images, icons, fonts, example files, presets) is listed here with its author, source and licence. `cargo xtask assets` (part of `cargo xtask ci`) fails if an asset file is missing from this table.
**Policy (mandatory):** GridCraft contains **no Microsoft, Adobe, Avid or Autodesk iconography, images, artwork, fonts, templates, themes, table styles or sample files**. Every asset is original work by GridCraft contributors or third-party material under an open licence (OSI open source, public domain / CC0, or Creative Commons that allows redistribution). Screenshots of Microsoft software are never committed. Font files are not added here: shared fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), an optional build input (`CRAFT_FONTS_DIR`). The one exception is the ArtCraft brand files in `docs/brand/`: ArtCraft trademarks, not open source, used under `docs/brand/LICENSE-brand.txt`.
**Policy (mandatory):** GridCraft contains **no Microsoft, Adobe, Avid or Autodesk iconography, images, artwork, fonts, templates, themes, table styles or sample files**. Every asset is original work by GridCraft contributors or third-party material under an open licence (OSI open source, public domain / CC0, or Creative Commons that allows redistribution). Screenshots of Microsoft software are never committed. Font files are not added here: shared fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), an optional build input (`CRAFT_FONTS_DIR`).
Generated-in-code art is original and has no file to list: the UI icon set (`crates/ui-egui/src/icons.rs`), themes (`crates/engine/src/cmd/view.rs`), cell styles (`crates/engine/src/cmd/format.rs`), table styles (`crates/engine/src/tables.rs`), chart palettes and the sample workbooks (`crates/engine/src/sample.rs`).
| Asset | Author | Source | Licence | Notes |
|---|---|---|---|---|
| `docs/brand/artcraft-logo.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms (`docs/brand/LICENSE-brand.txt`) | Trademark, not open source |
| `docs/brand/artcraft-logo.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
| `docs/brand/artcraft-logo-white.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
| `docs/brand/artcraft-logo-white.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
| `docs/brand/artcraft-mark.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
| `docs/brand/artcraft-mark.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
| `docs/brand/artcraft-mark-black.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
| `docs/brand/artcraft-mark-black.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
| `docs/brand/LICENSE-brand.txt` | (licence text) | craftrules | — | |
| `assets/app-icon/gridcraft.svg` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | PLACEHOLDER app icon (ledger page); replaced by the owner's creature art later |
| `packaging/macos/dmg/background.svg` | @XusBadia | original (hand-written SVG, derived from the GridCraft app icon `assets/app-icon/gridcraft.svg`, referenced via `<image>`, not copied; text is outlined glyph paths; `packaging/macos/dmg/generate.py` renders it to `background.tiff`) | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | GridCraft macOS DMG window background for the Finder window; added 2026-10-08 |
| `packaging/macos/dmg/background.tiff` | @XusBadia | Rendered from `packaging/macos/dmg/background.svg` by `packaging/macos/dmg/generate.py` | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | 1x + 2x HiDPI TIFF for the macOS DMG window; includes the app icon (see its row) and LittleCMS's built-in sRGB profile ("No copyright, use freely") |
-1
View File
@@ -17,7 +17,6 @@ Standards and learnings shared across the crafting apps live in `../../craftrule
- Prefer art generated in code: the UI icon set (`crates/ui-egui/src/icons.rs`), table and cell style palettes, themes and sample workbooks are drawn/defined in code and original.
- Never copy Office's icons, ribbon artwork, theme XML, table-style definitions, templates, clip art or wording beyond feature names. Excel feature *names* (menu labels, function names) are fine.
- Screenshots of Microsoft software are never committed (they live only in `plan/excel/screenshots/`).
- The ArtCraft brand files in `docs/brand/` are ArtCraft trademarks under `docs/brand/LICENSE-brand.txt`, the one exception.
## Never crash
People trust GridCraft with their numbers; a crash loses their work. **This outranks feature work.** Standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md).
Generated
+1681 -39
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
default-members = ["crates/*", "apps/gridcraft", "apps/gridcraft-cli", "xtask"]
[workspace.package]
version = "0.6.1"
version = "0.6.2"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.90"
@@ -41,6 +41,7 @@ sha1 = { version = "0.11", default-features = false }
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
# ui
craft-ui = { version = "0.1.0", git = "https://git.marcospaulo.dev.br/masi/craft-libs", rev = "7fcee66b8eef8eac3c7644fd024e1f1b051f5a3e", features = ["serde"] }
egui = "0.36"
egui_extras = { version = "0.36", default-features = false }
eframe = { version = "0.36", default-features = false }
-4
View File
@@ -3,10 +3,6 @@ Copyright (c) 2026 ArtCraft Team and the GridCraft contributors.
Licensed under the MIT License or the Apache License, Version 2.0, at your option
(LICENSE-MIT, LICENSE-APACHE).
The ArtCraft name, wordmark and logos in docs/brand/ are trademarks of the ArtCraft Team. They are
not open source and are not covered by the license above; their terms are in
docs/brand/LICENSE-brand.txt.
Third-party material
This product bundles third-party material under its own licenses, listed below. The per-file
+12 -73
View File
@@ -1,12 +1,3 @@
<p align="center">
<a href="https://getartcraft.com/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/brand/artcraft-logo-white.svg">
<img alt="ArtCraft" src="docs/brand/artcraft-logo.svg" width="200">
</picture>
</a>
</p>
<h1 align="center">GridCraft</h1>
<p align="center">
@@ -15,8 +6,7 @@
<p align="center">
A fast, open-source, clean-room take on the Excel workflow. It runs natively on macOS,
Windows, Linux and BSD, and in the browser via WebAssembly.<br>
<i>By the ArtCraft team.</i>
Windows, Linux and BSD, and in the browser via WebAssembly.
</p>
<p align="center">
@@ -26,16 +16,6 @@
<img alt="Agent-drivable over MCP" src="https://img.shields.io/badge/agents-MCP-1f9d55?style=flat-square">
</p>
<p align="center">
<a href="https://discord.gg/artcraft"><img alt="Join the ArtCraft community on Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
</p>
<p align="center">
<a href="https://getartcraft.com/apps/gridcraft"><b>GridCraft on getartcraft.com</b></a> ·
<a href="https://getartcraft.com/">ArtCraft</a> ·
<a href="https://getartcraft.com/apps">All Crafting Apps</a>
</p>
<br>
<p align="center">
@@ -43,11 +23,6 @@
<br><sub><b>Quarterly Sales Dashboard</b>: a table with a totals row, conditional formatting, sparklines and two live charts — built entirely from GridCraft commands (<code>File → New → Sample</code>).</sub>
</p>
> [!NOTE]
> **ArtCraft is a community of artists from all walks of life.** Painters, photographers,
> filmmakers, illustrators, designers, animators, hobbyists, and people who picked up a pencil
> last week. If you make things, you're one of us. **[Come say hi on Discord](https://discord.gg/artcraft).**
<p align="center">
<a href="#a-look-around">A look around</a> ·
<a href="#why-gridcraft">Why GridCraft</a> ·
@@ -279,43 +254,19 @@ handling again for troubleshooting.
## The Crafting Apps
GridCraft is one of the **Crafting Apps**: free, open-source creative tools from the
[ArtCraft](https://getartcraft.com/) team, each written from scratch in Rust and each able to
storytold team, each written from scratch in Rust and each able to
stand on its own.
| | App | What it's for | Code | Learn more |
|:-:|---|---|---|---|
| <img src="https://raw.githubusercontent.com/storytold/photocraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.photocraft.png" alt="" width="32" height="32"> | **PhotoCraft** | Image editing: layers, masks, type and real PSD files | [GitHub](https://github.com/storytold/photocraft) | [Website](https://getartcraft.com/apps/photocraft) |
| <img src="https://raw.githubusercontent.com/storytold/vectorcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.vectorcraft.png" alt="" width="32" height="32"> | **VectorCraft** | Vector illustration | [GitHub](https://github.com/storytold/vectorcraft) | [Website](https://getartcraft.com/apps/vectorcraft) |
| <img src="https://raw.githubusercontent.com/storytold/filmcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.filmcraft.png" alt="" width="32" height="32"> | **FilmCraft** | Video editing, color and sound | [GitHub](https://github.com/storytold/filmcraft) | [Website](https://getartcraft.com/apps/filmcraft) |
| <img src="https://raw.githubusercontent.com/storytold/lightcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.lightcraft.png" alt="" width="32" height="32"> | **LightCraft** | Photo library and raw development | [GitHub](https://github.com/storytold/lightcraft) | [Website](https://getartcraft.com/apps/lightcraft) |
| <img src="https://raw.githubusercontent.com/storytold/pdfcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.pdfcraft.png" alt="" width="32" height="32"> | **PdfCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/pdfcraft) | [Website](https://getartcraft.com/apps/pdfcraft) |
| <img src="https://raw.githubusercontent.com/storytold/effectcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.effectcraft.png" alt="" width="32" height="32"> | **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) |
| <img src="https://raw.githubusercontent.com/storytold/designcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.designcraft.png" alt="" width="32" height="32"> | **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) |
| <img src="https://raw.githubusercontent.com/storytold/gridcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.gridcraft.png" alt="" width="32" height="32"> | **GridCraft** | **Spreadsheets and data · you are here** | [GitHub](https://github.com/storytold/gridcraft) | [Website](https://getartcraft.com/apps/gridcraft) |
And [**ArtCraft**](https://getartcraft.com/) itself, our AI image and video studio for artists who want real control.
<br>
<p align="center">
<a href="https://discord.gg/artcraft"><img alt="Join the ArtCraft community on Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
</p>
<h3 align="center">Come make things with us</h3>
<p align="center">
Our Discord is where artists of every kind hang out: people who paint, shoot, draw, cut film,
set type, and people still figuring out what they like to make. Share what you're working on,
ask for help, tell us what's broken, or tell us what you wish these tools could do.
Whatever your medium and however long you've been at it, you're welcome here.
</p>
<p align="center">
<a href="https://discord.gg/artcraft"><b>discord.gg/artcraft</b></a> ·
<a href="https://getartcraft.com/">getartcraft.com</a> ·
<a href="https://getartcraft.com/apps">The Crafting Apps</a> ·
<a href="https://getartcraft.com/apps/gridcraft">GridCraft</a>
</p>
| | App | What it's for | Code |
|:-:|---|---|---|
| <img src="https://raw.githubusercontent.com/storytold/photocraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.photocraft.png" alt="" width="32" height="32"> | **PhotoCraft** | Image editing: layers, masks, type and real PSD files | [GitHub](https://github.com/storytold/photocraft) |
| <img src="https://raw.githubusercontent.com/storytold/vectorcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.vectorcraft.png" alt="" width="32" height="32"> | **VectorCraft** | Vector illustration | [GitHub](https://github.com/storytold/vectorcraft) |
| <img src="https://raw.githubusercontent.com/storytold/filmcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.filmcraft.png" alt="" width="32" height="32"> | **FilmCraft** | Video editing, color and sound | [GitHub](https://github.com/storytold/filmcraft) |
| <img src="https://raw.githubusercontent.com/storytold/lightcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.lightcraft.png" alt="" width="32" height="32"> | **LightCraft** | Photo library and raw development | [GitHub](https://github.com/storytold/lightcraft) |
| <img src="https://raw.githubusercontent.com/storytold/pdfcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.pdfcraft.png" alt="" width="32" height="32"> | **PdfCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/pdfcraft) |
| <img src="https://raw.githubusercontent.com/storytold/effectcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.effectcraft.png" alt="" width="32" height="32"> | **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) |
| <img src="https://raw.githubusercontent.com/storytold/designcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.designcraft.png" alt="" width="32" height="32"> | **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) |
| <img src="https://raw.githubusercontent.com/storytold/gridcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.gridcraft.png" alt="" width="32" height="32"> | **GridCraft** | **Spreadsheets and data · you are here** | [GitHub](https://github.com/storytold/gridcraft) |
## License and credits
@@ -329,16 +280,4 @@ All UI icons are drawn in code and are original; the sample workbooks and every
this README were made with GridCraft itself. GridCraft bundles no fonts: it uses the fonts
installed on your system.
The ArtCraft name, wordmark and logos in [`docs/brand/`](docs/brand/) are trademarks of the
ArtCraft Team and are not covered by this license. They may be used only unmodified, and only as
part of this repository and GridCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt).
Forks and modified versions must remove them.
<sub>Microsoft, Excel and Microsoft 365 are trademarks or registered trademarks of Microsoft Corporation in the United States and/or other countries. GridCraft is an independent, open-source project and is not affiliated with, sponsored by or endorsed by Microsoft Corporation; these names are used only to describe the workflows it is compatible with.</sub>
<br>
<p align="center">
<a href="https://getartcraft.com/"><img alt="ArtCraft" src="docs/brand/artcraft-mark.svg" width="28"></a><br>
<sub>Made by the <a href="https://getartcraft.com/">ArtCraft</a> team and community.</sub>
</p>
+1 -1
View File
@@ -49,7 +49,7 @@ pub fn start() {
) {
app.ui.language = browser;
}
SheetApp::setup_context_for_language(&cc.egui_ctx, false, app.ui.language);
SheetApp::setup_context_for_language(&cc.egui_ctx, false, None, app.ui.language);
Ok(Box::new(WebShell { app, inbox }))
}),
)
+1 -1
View File
@@ -24,7 +24,7 @@ image = { workspace = true }
winresource = "0.1"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
craft-print = { git = "https://github.com/storytold/craft-libs", rev = "d27646a5f7c8b4fc85d9dd3213b8c88b58e0088d" }
craft-print = { git = "https://git.marcospaulo.dev.br/masi/craft-libs", rev = "7fcee66b8eef8eac3c7644fd024e1f1b051f5a3e" }
rfd = { workspace = true }
arboard = { workspace = true }
+1 -1
View File
@@ -336,7 +336,7 @@ fn main() -> eframe::Result<()> {
let Some(mut app) = pending.take() else {
return Err("the app was already created".into());
};
SheetApp::setup_context_for_language(&cc.egui_ctx, app.ui.dark, app.ui.language);
SheetApp::setup_context_for_language(&cc.egui_ctx, app.ui.dark, app.ui.flavor, app.ui.language);
if let Some(port) = control_port {
match control_server::start(port, cc.egui_ctx.clone()) {
Ok(rx) => app.control_rx = Some(rx),
+10 -15
View File
@@ -5,6 +5,7 @@ use std::str::FromStr;
use std::sync::mpsc::{Receiver, channel};
use gridcraft_ui_egui::SheetApp;
use gridcraft_ui_egui::theme::ThemeMode;
use muda::accelerator::Accelerator;
use muda::{CheckMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
use serde_json::{Value, json};
@@ -173,8 +174,7 @@ fn tree() -> Vec<(&'static str, Vec<Entry>)> {
vec![
("Agent Control (MCP)…", "dialog:agents", None),
("Contributors…", "dialog:about:Contributors", None),
("GridCraft on getartcraft.com", "url:https://getartcraft.com/apps/gridcraft", None),
("Join the ArtCraft Discord…", "url:https://discord.gg/artcraft", None),
("GridCraft on GitHub", "url:https://github.com/storytold/gridcraft", None),
],
),
]
@@ -183,17 +183,12 @@ fn tree() -> Vec<(&'static str, Vec<Entry>)> {
pub struct NativeMenu {
_menu: Menu,
events: Receiver<MenuEvent>,
theme_items: Vec<(&'static str, CheckMenuItem)>,
theme_items: Vec<(ThemeMode, CheckMenuItem)>,
}
fn select_theme(app: &mut SheetApp, id: &str) -> bool {
let mode = match id {
"theme:system" => "system",
"theme:light" => "light",
"theme:dark" => "dark",
_ => return false,
};
app.run_or_alert("view.theme", json!({"mode": mode}));
let Some(mode) = id.strip_prefix("theme:").and_then(ThemeMode::parse) else { return false };
app.run_or_alert("view.theme", json!({"mode": mode.id()}));
true
}
@@ -224,8 +219,8 @@ impl NativeMenu {
}
if id == "view.theme" {
let themes = Submenu::new(label, true);
for (mode, label) in [("system", "System"), ("light", "Light"), ("dark", "Dark")] {
let item = CheckMenuItem::with_id(format!("theme:{mode}"), label, true, false, None);
for mode in ThemeMode::ALL {
let item = CheckMenuItem::with_id(format!("theme:{}", mode.id()), mode.label(), true, false, None);
let _ = themes.append(&item);
theme_items.push((mode, item));
}
@@ -318,12 +313,12 @@ mod tests {
#[test]
fn theme_choices_use_the_shared_preference_command() {
let mut app = SheetApp::new(gridcraft_engine::Session::new(), Default::default());
for mode in ["dark", "system", "light"] {
assert!(select_theme(&mut app, &format!("theme:{mode}")));
for mode in ThemeMode::ALL.into_iter().rev() {
assert!(select_theme(&mut app, &format!("theme:{}", mode.id())));
assert_eq!(app.ui.theme_mode(), mode);
assert!(app.message.is_none());
}
assert!(!select_theme(&mut app, "view.formulaBar"));
assert_eq!(app.ui.theme_mode(), "light");
assert_eq!(app.ui.theme_mode(), ThemeMode::System);
}
}
+1 -1
View File
@@ -9,9 +9,9 @@ description = "GridCraft egui frontend (Excel-style UI); swappable — nothing b
workspace = true
[dependencies]
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "d27646a5f7c8b4fc85d9dd3213b8c88b58e0088d" }
gridcraft-engine = { workspace = true }
gridcraft-chart = { workspace = true }
craft-ui = { workspace = true }
egui = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
+9 -1
View File
@@ -1,7 +1,7 @@
//! Renders the whole GridCraft window offscreen (wgpu, no window) to a PNG.
//!
//! `cargo run --release -p gridcraft-ui-egui --example snapshot -- [--sample sales] [--in file.xlsx]
//! [--size 1440x900] [--scale 2] [--cmd 'id={json}']... [--tab Insert] [--dark] [--language en|zh|ja|ko|ru|pt|cs]
//! [--size 1440x900] [--scale 2] [--cmd 'id={json}']... [--tab Insert] [--dark] [--theme catppuccinMocha|…] [--language en|zh|ja|ko|ru|pt|cs]
//! [--dialog 'name={json}'] [--hf-edit header|footer] out.png`
use gridcraft_engine::Session;
@@ -17,6 +17,7 @@ fn main() {
let mut cmds: Vec<String> = vec![];
let mut tab = None;
let mut dark = false;
let mut theme = None;
let mut pane: Option<String> = None;
let mut chart: Option<u32> = None;
let mut dialog: Option<String> = None;
@@ -71,6 +72,10 @@ fn main() {
i += 1;
hf_edit = args.get(i).cloned();
}
"--theme" => {
i += 1;
theme = args.get(i).and_then(|s| gridcraft_ui_egui::theme::ThemeMode::parse(s));
}
"--language" => {
i += 1;
language = args.get(i).cloned();
@@ -103,6 +108,9 @@ fn main() {
{
let mut app = SheetApp::new(session, Default::default());
app.ui.dark = dark;
if let Some(mode) = theme {
app.ui.set_theme_mode(mode);
}
if let Some(code) = &language
&& let Some(l) = gridcraft_ui_egui::i18n::Language::parse(code)
{
+5 -10
View File
@@ -13,7 +13,7 @@ use serde_json::{Value as Json, json};
use crate::SheetApp;
use crate::i18n::{self, Language};
use crate::icons::{self, Icon};
use crate::theme::{self, Tokens};
use crate::theme::{self, ThemeMode, Tokens};
/// Width of the navigation column.
const NAV_W: f32 = 196.0;
@@ -795,15 +795,10 @@ fn options(app: &mut SheetApp, ui: &mut Ui) {
egui::Grid::new("backstage_options_general").num_columns(2).spacing(vec2(18.0, 10.0)).show(ui, |ui| {
ui.label(lang.tr("Display theme"));
let mode = app.ui.theme_mode();
let current = match mode {
"system" => "System",
"dark" => "Dark",
_ => "Light",
};
egui::ComboBox::from_id_salt("backstage_theme").width(160.0).selected_text(lang.tr(current)).show_ui(ui, |ui| {
for (value, label) in [("system", "System"), ("light", "Light"), ("dark", "Dark")] {
if ui.selectable_label(mode == value, lang.tr(label)).clicked() {
app.run_or_alert("view.theme", json!({"mode": value}));
egui::ComboBox::from_id_salt("backstage_theme").width(160.0).selected_text(lang.tr(mode.label())).show_ui(ui, |ui| {
for m in ThemeMode::ALL {
if ui.selectable_label(mode == m, lang.tr(m.label())).clicked() {
app.run_or_alert("view.theme", json!({"mode": m.id()}));
}
}
});
+1 -2
View File
@@ -212,8 +212,7 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) -
}
"ui.set" => {
if let Some(v) = p.get("dark").and_then(Json::as_bool) {
app.ui.dark = v;
app.ui.system_theme = false;
app.ui.set_theme_mode(if v { crate::theme::ThemeMode::Dark } else { crate::theme::ThemeMode::Light });
}
if let Some(v) = p.get("formulaBar").and_then(Json::as_bool) {
let _ = app.run("view.formulaBar", json!({"on": v}));
+2 -3
View File
@@ -1326,9 +1326,8 @@ fn about(ui: &mut egui::Ui, d: &mut Dialog) {
ui.heading("GridCraft");
ui.label(format!("Version {}", env!("CARGO_PKG_VERSION")));
ui.add_space(4.0);
ui.label("A clean-room, open-source, Rust-native spreadsheet. Part of ArtCraft.");
ui.hyperlink_to("getartcraft.com/apps/gridcraft", "https://getartcraft.com/apps/gridcraft");
ui.hyperlink_to("Join the ArtCraft Discord", "https://discord.gg/artcraft");
ui.label("A clean-room, open-source, Rust-native spreadsheet.");
ui.hyperlink_to("github.com/storytold/gridcraft", "https://github.com/storytold/gridcraft");
}
}
}
+45 -21
View File
@@ -38,6 +38,7 @@ use std::collections::HashMap;
use gridcraft_engine::core::{CellRef, RangeRef};
use gridcraft_engine::{Session, UiRequest};
use craft_ui::theme::catppuccin::Flavor;
use serde::{Deserialize, Serialize};
use serde_json::{Value as Json, json};
@@ -51,6 +52,9 @@ pub struct UiState {
/// The manual preference, retained when following the system appearance.
pub dark: bool,
pub system_theme: bool,
/// The Catppuccin flavour in use, if any (`dark` then follows the flavour).
#[serde(deserialize_with = "lenient_flavor")]
pub flavor: Option<Flavor>,
pub ribbon_tab: String,
pub ribbon_collapsed: bool,
pub formula_bar_expanded: bool,
@@ -84,6 +88,10 @@ pub fn clamp_interface_scale(percent: f64) -> Option<u16> {
percent.is_finite().then(|| percent.round().clamp(INTERFACE_SCALE_MIN as f64, INTERFACE_SCALE_MAX as f64) as u16)
}
fn lenient_flavor<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<Flavor>, D::Error> {
Ok(serde_json::from_value(Json::deserialize(d)?).unwrap_or_default())
}
fn lenient_interface_scale<'de, D: serde::Deserializer<'de>>(d: D) -> Result<u16, D::Error> {
let v = Json::deserialize(d)?;
Ok(v.as_f64().and_then(clamp_interface_scale).unwrap_or(100))
@@ -94,6 +102,7 @@ impl Default for UiState {
UiState {
dark: false,
system_theme: false,
flavor: None,
ribbon_tab: "Home".into(),
ribbon_collapsed: false,
formula_bar_expanded: false,
@@ -123,13 +132,28 @@ impl UiState {
clamp_interface_scale(self.interface_scale as f64).unwrap_or(100) as f32 / 100.0
}
pub fn theme_mode(&self) -> &'static str {
if self.system_theme {
"system"
} else if self.dark {
"dark"
} else {
"light"
pub fn theme_mode(&self) -> theme::ThemeMode {
use theme::ThemeMode;
match (self.flavor, self.system_theme, self.dark) {
(Some(f), ..) => ThemeMode::Catppuccin(f),
(None, true, _) => ThemeMode::System,
(None, false, true) => ThemeMode::Dark,
(None, false, false) => ThemeMode::Light,
}
}
/// Chooses a display theme. System keeps the manual light/dark preference.
pub fn set_theme_mode(&mut self, mode: theme::ThemeMode) {
use theme::ThemeMode;
self.flavor = None;
self.system_theme = mode == ThemeMode::System;
match mode {
ThemeMode::System => {}
ThemeMode::Light | ThemeMode::Dark => self.dark = mode == ThemeMode::Dark,
ThemeMode::Catppuccin(f) => {
self.flavor = Some(f);
self.dark = f.is_dark();
}
}
}
}
@@ -297,13 +321,13 @@ impl SheetApp {
/// [`logic`]: SheetApp::logic
pub fn setup_context(ctx: &egui::Context, dark: bool) {
fonts::start_scan();
Self::setup_context_for_language(ctx, dark, i18n::Language::En);
Self::setup_context_for_language(ctx, dark, None, i18n::Language::En);
}
/// Sets up fonts and visuals for a specific persisted interface language.
pub fn setup_context_for_language(ctx: &egui::Context, dark: bool, language: i18n::Language) {
pub fn setup_context_for_language(ctx: &egui::Context, dark: bool, flavor: Option<Flavor>, language: i18n::Language) {
ctx.set_fonts(theme::font_definitions_for_language(language));
theme::apply(ctx, dark);
theme::apply(ctx, dark, flavor);
}
/// The font names the active workbook uses: every style's font plus the theme's heading/body
@@ -479,19 +503,15 @@ impl SheetApp {
"view.darkMode" => {
let current = if self.ui.system_theme { self.effective_dark } else { self.ui.dark };
self.ui.dark = p.get("on").and_then(Json::as_bool).unwrap_or(!current);
self.ui.system_theme = false;
self.ui.set_theme_mode(if self.ui.dark { theme::ThemeMode::Dark } else { theme::ThemeMode::Light });
Ok(json!({"on": self.ui.dark}))
}
"view.theme" => {
match p.get("mode").and_then(Json::as_str) {
Some("system") => self.ui.system_theme = true,
Some(mode @ ("light" | "dark")) => {
self.ui.dark = mode == "dark";
self.ui.system_theme = false;
}
_ => return Some(Err("theme mode must be system, light, or dark".into())),
}
Ok(json!({"mode": self.ui.theme_mode()}))
let Some(mode) = p.get("mode").and_then(Json::as_str).and_then(theme::ThemeMode::parse) else {
return Some(Err(format!("theme mode must be one of {}", theme::ThemeMode::id_list())));
};
self.ui.set_theme_mode(mode);
Ok(json!({"mode": mode.id()}))
}
"view.animations" => {
self.ui.animations = p.get("on").and_then(Json::as_bool).unwrap_or(!self.ui.animations);
@@ -891,7 +911,7 @@ impl SheetApp {
if self.fonts_set {
self.fonts_ready = true;
} else {
SheetApp::setup_context_for_language(ctx, self.ui.dark, self.ui.language);
SheetApp::setup_context_for_language(ctx, self.ui.dark, self.ui.flavor, self.ui.language);
self.fonts_set = true;
// The rebuilt set has no workbook fonts yet: bind them again once it is active.
self.fonts_bound.clear();
@@ -921,6 +941,10 @@ impl SheetApp {
if let Some(answer) = self.desktop_theme.take_change() {
self.desktop_dark = Some(answer);
}
if theme::flavor_of(ctx) != self.ui.flavor {
theme::apply(ctx, self.ui.dark, self.ui.flavor);
ctx.request_repaint();
}
let preference = match (
self.ui.system_theme,
ctx.system_theme().or_else(|| self.desktop_dark.map(|d| if d { egui::Theme::Dark } else { egui::Theme::Light })),
+8 -13
View File
@@ -262,13 +262,13 @@ fn file_menu(app: &mut SheetApp, ui: &mut Ui) {
}
}
/// System / Light / Dark, as in the View tab's Display theme box (`view.theme`).
/// System / Light / Dark / Catppuccin, as in the View tab's Display theme box (`view.theme`).
fn theme_choices(app: &mut SheetApp, ui: &mut Ui) {
let lang = app.ui.language;
let mode = app.ui.theme_mode();
for (value, label) in [("system", "System"), ("light", "Light"), ("dark", "Dark")] {
if ui.radio(mode == value, lang.tr(label)).clicked() {
act(app, "view.theme", json!({"mode": value}));
for m in theme::ThemeMode::ALL {
if ui.radio(mode == m, lang.tr(m.label())).clicked() {
act(app, "view.theme", json!({"mode": m.id()}));
ui.close();
}
}
@@ -2172,15 +2172,10 @@ fn view(app: &mut SheetApp, ui: &mut Ui) {
ui.vertical(|ui| {
ui.label(lang.tr("Display theme"));
let mode = app.ui.theme_mode();
let label = match mode {
"system" => "System",
"dark" => "Dark",
_ => "Light",
};
egui::ComboBox::from_id_salt("display_theme").selected_text(lang.tr(label)).width(88.0).show_ui(ui, |ui| {
for (value, label) in [("system", "System"), ("light", "Light"), ("dark", "Dark")] {
if ui.selectable_label(mode == value, lang.tr(label)).clicked() {
act(app, "view.theme", json!({"mode": value}));
egui::ComboBox::from_id_salt("display_theme").selected_text(lang.tr(mode.label())).width(88.0).show_ui(ui, |ui| {
for m in theme::ThemeMode::ALL {
if ui.selectable_label(mode == m, lang.tr(m.label())).clicked() {
act(app, "view.theme", json!({"mode": m.id()}));
}
}
});
+136 -3
View File
@@ -7,8 +7,65 @@
use std::sync::Arc;
use craft_ui::theme::catppuccin::Flavor;
use egui::{Color32, FontData, FontDefinitions, FontFamily, FontId, Stroke, Visuals};
/// The Display theme choices: follow the OS, fixed light or dark, or a Catppuccin flavour.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ThemeMode {
System,
Light,
Dark,
Catppuccin(Flavor),
}
impl ThemeMode {
pub const ALL: [ThemeMode; 7] = [
ThemeMode::System,
ThemeMode::Light,
ThemeMode::Dark,
ThemeMode::Catppuccin(Flavor::Latte),
ThemeMode::Catppuccin(Flavor::Frappe),
ThemeMode::Catppuccin(Flavor::Macchiato),
ThemeMode::Catppuccin(Flavor::Mocha),
];
/// The `view.theme` `mode` value.
pub const fn id(self) -> &'static str {
match self {
ThemeMode::System => "system",
ThemeMode::Light => "light",
ThemeMode::Dark => "dark",
ThemeMode::Catppuccin(Flavor::Latte) => "catppuccinLatte",
ThemeMode::Catppuccin(Flavor::Frappe) => "catppuccinFrappe",
ThemeMode::Catppuccin(Flavor::Macchiato) => "catppuccinMacchiato",
ThemeMode::Catppuccin(Flavor::Mocha) => "catppuccinMocha",
}
}
/// The English label, the i18n key.
pub const fn label(self) -> &'static str {
match self {
ThemeMode::System => "System",
ThemeMode::Light => "Light",
ThemeMode::Dark => "Dark",
ThemeMode::Catppuccin(Flavor::Latte) => "Catppuccin Latte",
ThemeMode::Catppuccin(Flavor::Frappe) => "Catppuccin Frappé",
ThemeMode::Catppuccin(Flavor::Macchiato) => "Catppuccin Macchiato",
ThemeMode::Catppuccin(Flavor::Mocha) => "Catppuccin Mocha",
}
}
pub fn parse(id: &str) -> Option<Self> {
Self::ALL.into_iter().find(|m| m.id() == id)
}
/// Every id, `|`-separated, for error messages.
pub fn id_list() -> String {
Self::ALL.map(ThemeMode::id).join("|")
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Tokens {
pub dark: bool,
@@ -137,8 +194,60 @@ impl Tokens {
}
}
/// A Catppuccin flavour on the same layout: base/mantle/crust surfaces, surface0-2 borders and
/// hover, mauve accent, blue selection. The grid keeps its white cells.
pub fn catppuccin(f: Flavor) -> Tokens {
let p = f.palette();
let dark = f.is_dark();
let base = if dark { Tokens::dark() } else { Tokens::light() };
Tokens {
dark,
window: p.base,
ribbon: p.mantle,
ribbon_border: p.surface0,
text: p.text,
text_dim: p.subtext0,
text_disabled: p.overlay0,
accent: p.mauve,
accent_dark: p.mauve,
accent_soft: p.surface1,
hover: p.surface0,
pressed: p.surface1,
separator: p.surface0,
desk: p.crust,
header_bg: p.mantle,
header_text: p.subtext1,
header_line: p.surface0,
header_sel_bg: p.surface1,
header_sel_text: p.mauve,
header_all_bg: p.surface1,
sel_fill: Color32::from_rgba_unmultiplied(p.blue.r(), p.blue.g(), p.blue.b(), 0x28),
sel_border: p.blue,
input_bg: if dark { p.crust } else { p.base },
input_border: p.surface1,
tab_bar: p.mantle,
tab_active: p.base,
status_bar: p.mantle,
menu_bg: p.mantle,
danger: p.red,
notice_warn_bg: p.surface0,
notice_warn_border: p.yellow,
notice_info_bg: p.surface0,
notice_info_border: p.blue,
..base
}
}
fn for_slot(dark: bool, flavor: Option<Flavor>) -> Tokens {
match flavor {
Some(f) if f.is_dark() == dark => Tokens::catppuccin(f),
_ if dark => Tokens::dark(),
_ => Tokens::light(),
}
}
pub fn get(ctx: &egui::Context) -> Tokens {
if ctx.global_style().visuals.dark_mode { Tokens::dark() } else { Tokens::light() }
Tokens::for_slot(ctx.global_style().visuals.dark_mode, flavor_of(ctx))
}
}
@@ -675,11 +784,17 @@ pub fn symbol_face_installed() -> bool {
}
}
pub fn apply(ctx: &egui::Context, dark: bool) {
/// The Catppuccin flavour [`apply`] last installed on `ctx`.
pub fn flavor_of(ctx: &egui::Context) -> Option<Flavor> {
ctx.data(|d| d.get_temp::<Option<Flavor>>(egui::Id::new("catppuccin_flavor"))).flatten()
}
pub fn apply(ctx: &egui::Context, dark: bool, flavor: Option<Flavor>) {
ctx.data_mut(|d| d.insert_temp(egui::Id::new("catppuccin_flavor"), flavor));
// Install both palettes before System can switch between the style slots.
for theme in [egui::Theme::Light, egui::Theme::Dark] {
let dark = theme == egui::Theme::Dark;
let t = if dark { Tokens::dark() } else { Tokens::light() };
let t = Tokens::for_slot(dark, flavor);
let mut v = if dark { Visuals::dark() } else { Visuals::light() };
v.panel_fill = t.window;
v.window_fill = t.menu_bg;
@@ -935,3 +1050,21 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
}
#[cfg(test)]
mod theme_mode_tests {
use super::*;
#[test]
fn catppuccin_themes_use_the_flavour_palette() {
for f in Flavor::ALL {
let t = Tokens::catppuccin(f);
assert_eq!((t.window, t.dark), (f.palette().base, f.is_dark()), "{}", f.name());
assert_eq!(ThemeMode::parse(ThemeMode::Catppuccin(f).id()), Some(ThemeMode::Catppuccin(f)));
}
assert_eq!(ThemeMode::id_list().split('|').count(), ThemeMode::ALL.len());
let ctx = egui::Context::default();
apply(&ctx, true, Some(Flavor::Mocha));
assert_eq!(Tokens::get(&ctx).window, Flavor::Mocha.palette().base);
}
}
-42
View File
@@ -1,42 +0,0 @@
ArtCraft brand assets: license
The ArtCraft name, wordmark and mark (the "ArtCraft Marks"), including every file in this
directory in every color, size and format, are owned by the ArtCraft Team. They are trademarks
and are NOT open source. They are not covered by GridCraft's MIT OR Apache-2.0 license, and nothing in
that license, in NOTICE or in any other file of this repository grants any right to them.
Permission
The ArtCraft Team grants a limited, non-exclusive, non-transferable, revocable, royalty-free
permission to use the ArtCraft Marks only:
1. as part of copies of this repository; and
2. within GridCraft itself, its documentation and its official builds, to identify GridCraft as made
by the ArtCraft team and community and to link to getartcraft.com and its community.
Conditions
The permission holds only while every one of these is true:
- The marks are used exactly as supplied in this directory. Do not redraw, recolor, crop,
stretch, rotate, animate, outline or add effects to them, extract parts of them, or combine
them with any other name, mark or artwork.
- They are used only in the context of GridCraft, and only to say that the ArtCraft team makes it.
They must never suggest that the ArtCraft Team made, sponsors, endorses, certifies or supports
any other product, service, fork, modified version, plug-in, organization or person.
- Neither the marks nor the ArtCraft name appear in the name, logo, icon, domain, social media
handle, app-store listing, advertising or marketing of anything other than GridCraft.
Forks and modified versions
If you distribute or publish a modified version of GridCraft, or any work derived from it, you must
remove the ArtCraft Marks from it (or replace them with your own) and must not call it ArtCraft or
present it as an ArtCraft product. You may say, in plain text, that your work is based on GridCraft by
the ArtCraft team. A fork kept only to propose changes back to this repository may keep the marks
unchanged.
Everything else
Any use not expressly permitted here needs prior written permission from the ArtCraft Team
(https://getartcraft.com/). The permission ends automatically for anyone who breaks these terms.
All other rights are reserved.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

-4
View File
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -19.42 136.34 136.34" role="img" aria-label="ArtCraft">
<title>ArtCraft</title>
<path fill="#0b0b0c" d="M104.28,49.49L81.55,0h-31.23l-3.17,4.76L14.75,53.63,0,75.85l21.55,21.55,63.79-36.94,16.99,37.04,14.01-21.74-12.06-26.27ZM32.89,65.66l32.42-48.87,10.91,23.77-43.32,25.09Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

-4
View File
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -19.42 136.34 136.34" role="img" aria-label="ArtCraft">
<title>ArtCraft</title>
<path fill="#4e7bfb" d="M104.28,49.49L81.55,0h-31.23l-3.17,4.76L14.75,53.63,0,75.85l21.55,21.55,63.79-36.94,16.99,37.04,14.01-21.74-12.06-26.27ZM32.89,65.66l32.42-48.87,10.91,23.77-43.32,25.09Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 341 B

@@ -24,10 +24,10 @@
<launchable type="desktop-id">ai.storyteller.gridcraft.desktop</launchable>
<icon type="stock">ai.storyteller.gridcraft</icon>
<!-- Only url types known to appstream 0.15 (Ubuntu 22.04, where CI validates): no vcs-browser. -->
<url type="homepage">https://getartcraft.com/apps/gridcraft</url>
<url type="homepage">https://github.com/storytold/gridcraft</url>
<url type="bugtracker">https://github.com/storytold/gridcraft/issues</url>
<url type="help">https://github.com/storytold/gridcraft</url>
<url type="contact">https://discord.gg/artcraft</url>
<url type="contact">https://github.com/storytold/gridcraft/issues</url>
<categories>
<category>Office</category>
<category>Spreadsheet</category>
+1 -1
View File
@@ -14,7 +14,7 @@ section: misc
priority: optional
maintainer: ${GRIDCRAFT_MAINTAINER}
vendor: Learning Machines LLC
homepage: https://getartcraft.com/apps/gridcraft
homepage: https://github.com/storytold/gridcraft
license: MIT OR Apache-2.0
description: |-
Open-source, native spreadsheet that works the way Excel users expect.
+3 -3
View File
@@ -30,9 +30,9 @@
<Icon Id="GridcraftIcon.ico" SourceFile="$(var.IconPath)" />
<Property Id="ARPPRODUCTICON" Value="GridcraftIcon.ico" />
<Property Id="ARPURLINFOABOUT" Value="https://getartcraft.com/apps/gridcraft" />
<Property Id="ARPURLUPDATEINFO" Value="https://getartcraft.com/apps/gridcraft" />
<Property Id="ARPHELPLINK" Value="https://discord.gg/artcraft" />
<Property Id="ARPURLINFOABOUT" Value="https://github.com/storytold/gridcraft" />
<Property Id="ARPURLUPDATEINFO" Value="https://github.com/storytold/gridcraft" />
<Property Id="ARPHELPLINK" Value="https://github.com/storytold/gridcraft/issues" />
<!-- An explicit command-line value wins. Otherwise retain the previous installation's
choice, defaulting to off for a fresh install. Run only in the first sequence so the
Submodule tools/craft-skills added at 26c1c201a7