diff --git a/README.md b/README.md index 1eb95e9..0ab25c2 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ requests. GitHub Pages publishes it from `main` through `.github/workflows/pages | `index.html` | the page | | `assets/css/site.css` | the styles: colour tokens for the light and dark themes, Fluent surfaces, the layout | | `assets/js/config.js` | the version the page shows and the release flag (below) | -| `assets/js/site.js` | versions in the snippets, code highlighting and copy buttons, the theme switch, tabs, the crate filter, Reveal | +| `assets/js/site.js` | versions in the snippets, code highlighting and copy buttons, the theme switch, tabs, the `cargo generate` commands of the templates, the crate filter, Reveal | | `assets/img/` | the logo and the screenshots (WebP, each with a 480 or 640 pixel variant) | | `scripts/update-crates.py` | writes the crate table from the README of FerroUI | | `scripts/check-site.py` | the checks of CI: local references, versions, names | diff --git a/assets/css/site.css b/assets/css/site.css index d4581b9..4cbe72a 100644 --- a/assets/css/site.css +++ b/assets/css/site.css @@ -254,7 +254,7 @@ p { margin: 0; } box-shadow: 0 0 0 1px var(--focus-inner); border-radius: var(--r-control); } -.btn:focus-visible, .chip:focus-visible, .tab:focus-visible, .icon-btn:focus-visible, .copy:focus-visible, .field input:focus-visible { +.btn:focus-visible, .chip:focus-visible, .tab:focus-visible, .tpl-tab:focus-visible, .icon-btn:focus-visible, .copy:focus-visible, .field input:focus-visible, .opt input:focus-visible, .opt select:focus-visible { outline: 2px solid var(--rust); outline-offset: 2px; box-shadow: 0 0 0 1px var(--focus-inner); @@ -818,6 +818,72 @@ table.data td:first-child { color: var(--text); } .template .section-head { margin-bottom: 20px; } .template h3 { font-size: 22px; font-weight: 600; margin-bottom: 8px; } +/* Templates */ + +.tpl-about { display: flex; flex-direction: column; gap: 12px; color: var(--text-2); font-size: 15px; } +.tpl-about .notice { margin-bottom: 0; } +.tpl { display: grid; gap: 16px; grid-template-columns: 1fr; margin-top: 14px; } +@media (min-width: 960px) { .tpl { grid-template-columns: 280px minmax(0, 1fr); align-items: start; } } +.tpl-list { display: grid; gap: 2px; padding: 6px; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } +@media (min-width: 960px) { .tpl-list { grid-template-columns: 1fr; position: sticky; top: calc(var(--header-h) + 16px); } } +.tpl-tab { + position: relative; + display: grid; gap: 1px; + padding: 8px 12px 8px 16px; + border-radius: var(--r-control); + border: 1px solid transparent; + background: transparent; + color: var(--text); + font: inherit; text-align: left; + cursor: pointer; + transition: background var(--dur) var(--ease); +} +.tpl-tab:hover { background: var(--layer-alt); } +.tpl-tab[aria-selected="true"] { background: var(--card-solid); border-color: var(--stroke-card); box-shadow: var(--shadow-2); } +.tpl-tab[aria-selected="true"]::before { + content: ""; position: absolute; left: 4px; top: 50%; + width: 3px; height: 16px; margin-top: -8px; border-radius: 2px; + background: var(--rust-fill); +} +.tpl-name { font-family: var(--font-mono); font-size: 13.5px; font-weight: 600; overflow-wrap: anywhere; } +.tpl-kind { font-size: 12.5px; color: var(--text-2); } +@media (max-width: 959px) { .tpl-kind { display: none; } } +.tpl-panels { min-width: 0; } +.tpl-panel { padding: 20px; } +.tpl-panel:hover { background: var(--card); box-shadow: var(--shadow-2); } +.tpl-panel h3 { font-family: var(--font-mono); font-size: 18px; font-weight: 600; margin-bottom: 8px; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; } +.tpl-panel h3 .badge { font-family: var(--font-text); } +.tpl-files { display: flex; flex-wrap: wrap; gap: 6px; list-style: none; margin: 0 0 16px; padding: 0; } +.tpl-files li { + font-family: var(--font-mono); font-size: 12px; + padding: 3px 8px; + border-radius: var(--r-control); + border: 1px solid var(--stroke); + background: var(--layer-alt); + color: var(--text-2); +} +.tpl-options { display: flex; flex-wrap: wrap; gap: 12px 16px; align-items: flex-end; margin: 0 0 16px; } +.no-js .tpl-options { display: none; } +.opt { display: grid; gap: 4px; margin: 0; padding: 0; border: 0; font-size: 12px; font-weight: 600; color: var(--text-2); min-width: 0; } +.opt input[type="text"], .opt select { + height: 34px; min-width: 170px; max-width: 100%; + padding: 0 10px; + border-radius: var(--r-control); + border: 1px solid var(--stroke); + border-bottom: 1px solid var(--text-3); + background: var(--card); + color: var(--text); + font: inherit; font-size: 14px; font-weight: 400; +} +.opt select option { background: var(--card-solid); color: var(--text); } +.opt input[type="text"]:focus, .opt select:focus { border-bottom: 2px solid var(--rust-fill); background: var(--card-solid); } +.opt input::placeholder { color: var(--text-3); } +.opt-hosts { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; min-height: 34px; padding-top: 18px; position: relative; } +.opt-hosts legend { position: absolute; top: 0; left: 0; padding: 0; } +.opt-hosts label { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; font-weight: 400; color: var(--text); cursor: pointer; } +.opt-hosts input { width: 16px; height: 16px; margin: 0; accent-color: var(--rust-fill); } +.tpl-panel .code { margin-bottom: 0; } + /* Crates */ .crate-tools { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 16px; } @@ -941,6 +1007,8 @@ table.crates tr[hidden] { display: none; } @media (forced-colors: active) { .reveal::before, .reveal::after { display: none; } .card, .btn, .chip, .tab, .code { border: 1px solid CanvasText; } + .tpl-tab[aria-selected="true"] { border-color: Highlight; } + .tpl-tab[aria-selected="true"]::before { background: Highlight; } } @media print { diff --git a/assets/js/site.js b/assets/js/site.js index 25c858c..65afcf1 100644 --- a/assets/js/site.js +++ b/assets/js/site.js @@ -1,5 +1,5 @@ -// FerroUI website: versions, code highlighting and copy, theme, navigation, tabs, crate filter, -// Reveal lighting and entrance motion. Plain JavaScript, no dependencies. +// FerroUI website: versions, code highlighting and copy, theme, navigation, tabs, template +// commands, crate filter, Reveal lighting and entrance motion. Plain JavaScript, no dependencies. (function () { "use strict"; @@ -224,8 +224,9 @@ tab.addEventListener("click", function () { select(tab, false); }); tab.addEventListener("keydown", function (e) { var next = null; - if (e.key === "ArrowRight") next = tabs[(i + 1) % tabs.length]; - else if (e.key === "ArrowLeft") next = tabs[(i - 1 + tabs.length) % tabs.length]; + var vertical = list.getAttribute("aria-orientation") === "vertical"; + if (e.key === "ArrowRight" || (vertical && e.key === "ArrowDown")) next = tabs[(i + 1) % tabs.length]; + else if (e.key === "ArrowLeft" || (vertical && e.key === "ArrowUp")) next = tabs[(i - 1 + tabs.length) % tabs.length]; else if (e.key === "Home") next = tabs[0]; else if (e.key === "End") next = tabs[tabs.length - 1]; if (next) { e.preventDefault(); select(next, true); } @@ -234,6 +235,62 @@ }); } + // The templates: each panel writes the cargo generate command of its template from its options. + // data-before and data-next are the lines before and after it, separated by "|"; a line that + // starts with "option?" (or "!option?") is written only when that checkbox is on (or off). + var TEMPLATES_REPO = "https://github.com/FerroUI/templates"; + function setupTemplates() { + document.querySelectorAll("[data-template]").forEach(function (panel) { + var code = panel.querySelector("code[data-command]"); + if (!code) return; + var template = panel.getAttribute("data-template"); + var nameInput = panel.querySelector("[data-name]"); + var controls = Array.prototype.slice.call(panel.querySelectorAll("[data-define]")); + function clean(text) { return text.replace(/[^A-Za-z0-9_.-]/g, ""); } + function valueOf(control) { + return control.type === "checkbox" ? (control.checked ? "true" : "false") : clean(control.value.trim()); + } + function lines(attribute, name) { + return (panel.getAttribute(attribute) || "").split("|").filter(Boolean).map(function (line) { + var condition = line.match(/^(!?)([a-z_]+)\?(.*)$/); + if (condition) { + var box = panel.querySelector("[data-define='" + condition[2] + "']"); + var on = box ? box.checked : false; + if (on === (condition[1] === "!")) return null; + line = condition[3]; + } + return line.split("{name}").join(name); + }).filter(function (line) { return line !== null; }); + } + // cargo generate names the directory of the project in kebab case (MyApp is my-app). + function kebab(text) { + return text.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase(); + } + function render() { + var name = nameInput ? clean(nameInput.value.trim()) || nameInput.defaultValue : "my-app"; + var defines = controls.filter(function (control) { + if (control.hasAttribute("data-always")) return valueOf(control) !== ""; + return valueOf(control) !== (control.getAttribute("data-default") || ""); + }).map(function (control) { return "--define " + control.getAttribute("data-define") + "=" + valueOf(control); }); + var command = "cargo generate --git " + TEMPLATES_REPO + " " + template + " --name " + name; + var parts = [command]; + // Two defines per continuation line keep the command readable on a phone. + for (var i = 0; i < defines.length; i += 2) parts.push(defines.slice(i, i + 2).join(" ")); + var text = lines("data-before", name).concat([parts.join(" \\\n ")], lines("data-next", kebab(name))).join("\n"); + if (code.textContent !== text) { + code.textContent = text; + highlight(code); + } + } + if (nameInput) nameInput.addEventListener("input", render); + controls.forEach(function (control) { + control.addEventListener("input", render); + control.addEventListener("change", render); + }); + render(); + }); + } + // The crate table: a search field and one filter per group. function setupCrates() { var table = document.querySelector("table.crates"); @@ -313,6 +370,7 @@ setupMenu(); setupScrollSpy(); setupTabs(); + setupTemplates(); setupCrates(); setupReveal(); setupRise(); diff --git a/index.html b/index.html index 158e83e..23b3a12 100644 --- a/index.html +++ b/index.html @@ -68,6 +68,7 @@
cargo new to a windowFerroUI is a set of crates released together at one version. Name the same version for every FerroUI crate.
+FerroUI is a set of crates released together at one version. Name the same version for every FerroUI crate, or generate a project from one of the templates.
One command creates a project with its theme, its first window and its tests, in XAML with MVVM or code-behind, with builders or with rsx!, or as a cross-platform repository. The templates list them with their options.
cargo install cargo-generate
+cargo generate --git https://github.com/FerroUI/templates app --name my-app
+ A desktop application names the base library, the controls, the desktop entry point and a theme:
Solitaire, Ember, FerroDesign and Calculator are set up this way: FerroUI as a git submodule, its workspace dependencies and profiles copied into the application's workspace, and FerroUI's reusable workflow for CI and GitHub Pages. The guide is docs/apps.md.
+Solitaire, Ember, FerroDesign and Calculator are set up this way: FerroUI as a git submodule, its workspace dependencies and profiles copied into the application's workspace, and FerroUI's reusable workflow for CI and GitHub Pages. The guide is docs/apps.md. The cross-platform template generates this layout with desktop, browser, Android and iOS hosts.
cargo generateProject and item templates in the spirit of the dotnet new templates of .NET UI frameworks: XAML with MVVM or code-behind, builders, rsx! with hooks, a cross-platform repository with desktop, browser, Android and iOS hosts, a control library, and the items you add to a project. Each project template asks for its theme.
cargo install cargo-generate
+
+# Pick a template from the list, or name it
+cargo generate --git https://github.com/FerroUI/templates
+cargo generate --git https://github.com/FerroUI/templates app --name my-app
+cd my-app
+cargo run
+ The templates live in FerroUI/templates, one folder per template, and are made with cargo-generate. Each option is a prompt, or --define name=value on the command line.
The single-crate templates name every FerroUI crate at =. The cross-platform template uses the layout of an application repository: FerroUI as a git submodule and its reusable CI workflow.
is being released. The generated projects build once it is on crates.io; the cross-platform template builds from the submodule already.
+A desktop application in XAML with the MVVM pattern, as in samples/MiniMvvm and samples/ReactiveUIExample. The build script compiles the documents to Rust; the view model and the window come with tests.
cargo generate --git https://github.com/FerroUI/templates app --name my-app
+cd my-app
+cargo run
+ A desktop application in XAML with code-behind and no view models: the window names its event handler (Click="OnGreetClick") and its class implements it.
cargo generate --git https://github.com/FerroUI/templates app-codebehind --name my-app
+cd my-app
+cargo run
+ A desktop application written in Rust with the builders of ferroui-view (stack_panel(), button()) and a signal the controls follow, as in samples/HelloWorld.Builders.
cargo generate --git https://github.com/FerroUI/templates declarative --name my-app
+cd my-app
+cargo run
+ A desktop application with the React-like API: rsx! trees, #[component] functions, a signal, and a component that renders again when its hook state changes (use_signal, #[component(rerender)]), as in samples/HelloWorld.Macros and samples/HelloWorld.Hooks.
cargo generate --git https://github.com/FerroUI/templates rsx --name my-app
+cd my-app
+cargo run
+ An application repository laid out as docs/apps.md describes: a shared crate in XAML with the MVVM pattern, the hosts you choose, FerroUI as a git submodule at FerroUI/ and a CI workflow that calls FerroUI's app.yml, with the browser site on GitHub Pages. scripts/setup.sh adds the submodule once.
cargo generate --git https://github.com/FerroUI/templates cross-platform --name my-app
+cd my-app
+scripts/setup.sh
+cargo run -p my-app-desktop
+ A library crate with a templated control: styled properties declared with ferro_properties!, a control theme in XAML keyed by the type of the control, the registration of both, and tests that apply the theme in a window.
cargo generate --git https://github.com/FerroUI/templates control-library --name my-controls
+cd my-controls
+cargo test
+ A window: a XAML document with x:Class and its class with InitializeComponent(). Its README.md says how to declare and register it.
cd my-app/src/views
+cargo generate --git https://github.com/FerroUI/templates window --name SettingsWindow \
+ --define namespace=MyApp.Views
+# Then wire it in as README.md in the new folder says
+ A user control: a XAML document with x:Class and its class, to show as the content of a window or of another control.
cd my-app/src/views
+cargo generate --git https://github.com/FerroUI/templates user-control --name StatusView \
+ --define namespace=MyApp.Views
+# Then wire it in as README.md in the new folder says
+ A templated control: its class with the styled properties Header and IsHighlighted, and its control theme, which the application merges into its resources.
cd my-app/src/views
+cargo generate --git https://github.com/FerroUI/templates templated-control --name BadgeCard \
+ --define namespace=MyApp.Views
+# Then wire it in as README.md in the new folder says
+ A resource dictionary in XAML, merged into the resources of the application with ResourceInclude.
cd my-app/src
+cargo generate --git https://github.com/FerroUI/templates resource-dictionary --name AppResources
+# Then wire it in as README.md in the new folder says
+ A set of styles in XAML, included after the theme with StyleInclude.
cd my-app/src
+cargo generate --git https://github.com/FerroUI/templates styles --name AppStyles
+# Then wire it in as README.md in the new folder says
+