diff --git a/Controls/LeftPanel.xaml.cs b/Controls/LeftPanel.xaml.cs index 73acf38..0c9927c 100644 --- a/Controls/LeftPanel.xaml.cs +++ b/Controls/LeftPanel.xaml.cs @@ -54,9 +54,12 @@ public partial class LeftPanel : UserControl { while (child != null && !ReferenceEquals(child, ancestor)) { - child = child is Visual or System.Windows.Media.Media3D.Visual3D + var next = child is Visual or System.Windows.Media.Media3D.Visual3D ? VisualTreeHelper.GetParent(child) - : LogicalTreeHelper.GetParent(child); + : null; + child = next + ?? LogicalTreeHelper.GetParent(child) + ?? (child as FrameworkElement)?.TemplatedParent; } return child != null; } diff --git a/Controls/index.md b/Controls/index.md index 7d8d009..7d0e099 100644 --- a/Controls/index.md +++ b/Controls/index.md @@ -12,7 +12,7 @@ See [`schema.md`](../schema.md) for the memory-map conventions. | `TopBar.xaml(.cs)` | Row 0 session surface (2026-09-22 redesign): brand + gear menu (left), one segmented REC|ON-AIR mode switch + world actions (Record: Start Recording · Stream: Go Live + Test) + reality line while running (dot/word/elapsed) + End (center), account zone — Sign In or avatar w/ Change Account/Logout context menu (right). `RefreshAvatar()`, `GearButton_Click` | | `PreviewPane.xaml(.cs)` | Row 1 Col 1 center: the 1920×1080 `OverlayCanvas`/`SelectionOverlay`/`BrandFlashLayer`/`SocialBarElement`, text pull-out drawer, and the live-controls row (TRAX / game / mic meters + sliders + speakers + socials). **TASK 57:** dragging a webcam under the vertical tier routes through `MainViewModel.MoveWebcamInVerticalTier` (the six-spot snap inside the cropped band) instead of free-floating over the master, and a resize re-snaps the tile onto its spot (TASK 57 Unit 2: the resize routes through `MainViewModel.ResizeWebcamInVerticalTier` — the Chat camera's two sizes, then the snap) | | `SceneThumbnailStrip.xaml(.cs)` | Row 0 scene-picker strip (`ItemsControl` over `Scenes`), staged-red / live-green border highlight | -| `LeftPanel.xaml(.cs)` | Row 1 Col 0 left panel: edit mode → layers+properties; live → YouTube chat. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on). **TASK 57:** the webcam properties carry a Geometry section (X/Y/W/H) — manual placement of the camera, bound to the **live** placement (the one the current tier shows), gated on `IsWebcamSelected` (not `SelectedElement.IsWebcam` — the TASK 51 test finds that panel by binding text and would match twice); the Shape button's `Visibility` is bound to `CanToggleClipShape`, so the circle option is NOT OFFERED in the vertical Chat layout. **TASK 58:** every drop-down, check box and field in the properties panel carries the theme's style (`YtComboBox`/`YtCheckBox`/`YtTextBoxCompact`) and paints no colour of its own — the guard is `ytLive.Tests/PropertiesStyleTests.cs`. **TASK 59:** the webcam's properties are a HIERARCHY — a `Shape` title owns the mirror switch and the shape choice (`Rect` | `Circle` as two `SegmentToggle` options, Rect selected by default, the circle option hidden where `CanToggleClipShape` is false), and every titled block (Geometry, Border Color, Border Thickness, Background) owns its content one indent deeper (Strength sits under Blur). **TASK 60:** the border-colour drop-down builds its menu from `Models/BorderColorPalette.cs` — each row is the hex plus the colour's NAME (`#e94560 Crimson`, tooltip "Crimson (#e94560)"); the swatch icon is unchanged. **TASK 61:** the four geometry fields (X/Y/W/H) share one fixed width (44px — the largest value, 1920, plus a sign) and their rows set `LastChildFill="False"`, so Y/H no longer stretch to fill the row. **TASK 62:** the border-colour field shows the palette's name beside the hex (`SelectedElement.BorderColorName`, empty for a custom hex) and keeps a fixed width so the name stays next to the hex. **TASK 63:** the border colour is picked with the **app's own drop-down** (`YtComboBox`) — the `...` button and its code-built `ContextMenu` are gone; the closed box shows the swatch + the colour's NAME, the rows show swatch + hex + name (the panel's own `DataTemplate`s + `Helpers/ClosedBoxTemplateSelector.cs`), and the hex field stays for a custom colour | +| `LeftPanel.xaml(.cs)` | Row 1 Col 0 left panel: edit mode → layers+properties; live → YouTube chat. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on). **TASK 57:** the webcam properties carry a Geometry section (X/Y/W/H) — manual placement of the camera, bound to the **live** placement (the one the current tier shows), gated on `IsWebcamSelected` (not `SelectedElement.IsWebcam` — the TASK 51 test finds that panel by binding text and would match twice); the Shape button's `Visibility` is bound to `CanToggleClipShape`, so the circle option is NOT OFFERED in the vertical Chat layout. **TASK 58:** every drop-down, check box and field in the properties panel carries the theme's style (`YtComboBox`/`YtCheckBox`/`YtTextBoxCompact`) and paints no colour of its own — the guard is `ytLive.Tests/PropertiesStyleTests.cs`. **TASK 59:** the webcam's properties are a HIERARCHY — a `Shape` title owns the mirror switch and the shape choice (`Rect` | `Circle` as two `SegmentToggle` options, Rect selected by default, the circle option hidden where `CanToggleClipShape` is false), and every titled block (Geometry, Border Color, Border Thickness, Background) owns its content one indent deeper (Strength sits under Blur). **TASK 60:** the border-colour drop-down builds its menu from `Models/BorderColorPalette.cs` — each row is the hex plus the colour's NAME (`#e94560 Crimson`, tooltip "Crimson (#e94560)"); the swatch icon is unchanged. **TASK 61:** the four geometry fields (X/Y/W/H) share one fixed width (44px — the largest value, 1920, plus a sign) and their rows set `LastChildFill="False"`, so Y/H no longer stretch to fill the row. **TASK 62:** the border-colour field shows the palette's name beside the hex (`SelectedElement.BorderColorName`, empty for a custom hex) and keeps a fixed width so the name stays next to the hex. **TASK 63:** the border colour is picked with the **app's own drop-down** (`YtComboBox`) — the `...` button and its code-built `ContextMenu` are gone; the closed box shows the swatch + the colour's NAME, the rows show swatch + hex + name (the panel's own `DataTemplate`s + `Helpers/ClosedBoxTemplateSelector.cs`), and the hex field stays for a custom colour; `IsDescendantOf` falls back to `LogicalTreeHelper.GetParent` / `TemplatedParent` when `VisualTreeHelper.GetParent` returns `null` (at `PopupRoot`) so clicking a popup row does not trigger `MainWindow`'s deselection guard | | `OverlayHost.xaml(.cs)` | Root-grid overlay dialogs: Settings / Report Bug / Feature Request / About (+ licenses + license-key entry). `BackToAbout_Click` lives here | | `SceneThumbnailStrip.xaml.cs` | Code-behind for the strip above | | `LeftPanel.xaml.cs` | Code-behind for the panel above | diff --git a/HANDOFF.md b/HANDOFF.md index 40d7c8f..a9aaf25 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -52,7 +52,8 @@ (`SceneElement.BorderColorName`, empty for a custom hex), and the hex field keeps a fixed width so the name stays next to the hex. The field stays editable (a custom hex can still be typed). Guard: `ytLive.Tests/BorderColorNameTests.cs`. -- **TASK 63 - the border-colour picker IS the app's drop-down** (shipped 2026-10-21). The creator: +- **TASK 63 - the border-colour picker IS the app's drop-down** (shipped 2026-10-21; **popup click + deselection fixed 2026-10-22**). The creator: *"why does the drop down under the web cam properties not show the same style for the border color drop down list box?"* and *"we established look-and-feel standards. And yet you keep ignoring these and I'm wondering why."* **Root cause of the miss:** the TASK 58 sweep looked for @@ -66,8 +67,17 @@ `WebcamBorderColorPicker_Click` are **gone**. The closed box != the rows only through `Helpers/ClosedBoxTemplateSelector.cs` (WPF copies `ItemTemplate` into the read-only `SelectionBoxItemTemplate`; the selector is told them apart by the `ContentPresenter`'s - templated parent - `ComboBox` = closed box, `ComboBoxItem` = a row). Guard: - `ytLive.Tests/BorderColorPickerTests.cs` (model + panel + the **real widget**). The TASK 60 + templated parent - `ComboBox` = closed box, `ComboBoxItem` = a row). **2026-10-22 creator bug fix:** + clicking a colour in the open `ComboBox` popup registered as an exit from the properties listing + because `LeftPanel.IsDescendantOf` used `child is Visual ? VisualTreeHelper.GetParent(child) : + LogicalTreeHelper.GetParent(child)` — at `PopupRoot` (which IS a `Visual`), + `VisualTreeHelper.GetParent` returns `null`, so `IsDescendantOf` stopped without crossing + `PopupRoot`'s logical parent (`PART_Popup` → `ComboBox` → `ElementsScrollViewer`), and + `MainWindow.Window_PreviewMouseLeftButtonDown` cleared `SelectedElement = null`. Fixed by falling + back to `LogicalTreeHelper.GetParent` / `TemplatedParent` when `VisualTreeHelper.GetParent` + returns `null`. Guard: `ytLive.Tests/BorderColorPickerTests.cs` (model + panel + the **real + widget**, including `LeftPanel.IsClickInside` on an open popup row and clicking a row to change + `BorderColor`). The TASK 60 guard (`BorderColorPaletteTests`) was updated to guard the new wiring (the drop-down is bound to the palette; no second list of colours anywhere). - **TASK 55 slice 1 — the log is capped (shipped 2026-10-22).** `startup.log` was uncapped (23 MB @@ -233,11 +243,10 @@ Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbel the full top area and the circle option is not offered. **PASSES.** ## Next step -- **Creator check of TASK 63 (needs the creator's eyes)** — open Live → Layers → webcam → - Properties → Border Color: the drop-down must now LOOK like the Display/Font drop-downs (same - dark themed chrome, same chevron), the CLOSED box must show the colour's name beside the hex - field ("Crimson"), and the open list must show swatch + hex + name. The hex field still accepts a - typed colour (the drop-down then shows the swatch with no name — "no name" is the honest answer). +- **✅ TASK 63 + popup-click fix verified by creator (2026-10-22)** — clicking a colour in the + Border Color `YtComboBox` dropdown now stays inside `LeftPanel` (`IsDescendantOf` crosses + `PopupRoot` → `PART_Popup` via `LogicalTreeHelper.GetParent`), updates the webcam border colour, + and does not exit the properties listing. - **Creator check (needs the creator's hands)** — the last piece of the vv feature: switch the resolution dropdown to "Vertical 1080p60", confirm the Live camera tile lands inside the band and the **Circle** option (TASK 59 turned the Shape button into a Rect | Circle choice) turns the tile diff --git a/MyMistakes.md b/MyMistakes.md index a45ce9f..1c2d351 100644 --- a/MyMistakes.md +++ b/MyMistakes.md @@ -2275,6 +2275,36 @@ sizes to its **content**, not to the ComboBox — measured 142.7px of popup for whose rows read "swatch + hex + name". A long row is never clipped, and the popup may be wider than the closed box. Measure with `ActualWidth` in a throwaway test; do not guess widths. +**2026-10-22 follow-up — `PopupRoot` IS a `Visual`, yet its `VisualTreeHelper.GetParent` is `null`:** +Switching from a code-built `ContextMenu` (which is not in `MainWindow`'s routed-event path) to a +`ComboBox` (`PART_Popup`) exposed a latent bug in `LeftPanel.IsDescendantOf`: +```csharp +child = child is Visual or System.Windows.Media.Media3D.Visual3D + ? VisualTreeHelper.GetParent(child) + : LogicalTreeHelper.GetParent(child); +``` +When the user clicked a colour row inside the open `ComboBox` popup, `PreviewMouseLeftButtonDown` +tunnelled through `MainWindow.Window_PreviewMouseLeftButtonDown` first. `IsDescendantOf` walked up +`VisualTreeHelper.GetParent` to `PopupRoot` (the root `Visual` of the popup's separate Win32 +`HwndSource`). Because `PopupRoot` IS a `Visual`, the ternary took the `VisualTreeHelper.GetParent` +branch and got `null` — never crossing `PopupRoot`'s **logical** parent +(`LogicalTreeHelper.GetParent(popupRoot) == PART_Popup`). `LeftPanel.IsClickInside` returned +`false`, and `MainWindow` set `SelectedElement = null` (exiting the properties listing before the +`ComboBoxItem` click could register). Fix: always coalesce +`VisualTreeHelper.GetParent(child) ?? LogicalTreeHelper.GetParent(child) ?? (child as FrameworkElement)?.TemplatedParent`. + +## 2026-10-22 — WINDOWS GIT IDENTITY + PRE-TASK GIT STATUS (RECIPE) + +- **Windows `git` vs WSL `~/.gitconfig`:** the creator's `user.name` / `user.email` lived only in + WSL's `~/.gitconfig`, so running `git commit` from the Windows PowerShell host failed with + `Author identity unknown`. Fixed permanently in `.git/config` (`[user] name = gramps`, + `email = gramps@llamachile.shop`). If you ever need the repo's commit author without running a + shell command, read `.git/logs/HEAD` directly with `read_file`. +- **Check `git status` BEFORE starting a task** (and never run multiline/compound shell commands + when a single simple command or `read_file`/`edit_file` works): `verify.sh` runs `scope-check.sh` + over `git diff HEAD`, so any pre-existing uncommitted files in the worktree will fail step 3 at + the end of a 2-minute build+test run unless spotted up front. + ## 2026-10-22 — SCANNING A GIT HISTORY FOR SECRETS WITHOUT EVER PRINTING ONE (RECIPE) A release gate ("the repo goes public — check the history first"). The rule from diff --git a/TASKS.md b/TASKS.md index cfa54f3..aa78ae9 100644 --- a/TASKS.md +++ b/TASKS.md @@ -78,7 +78,7 @@ | 60 | **The border-colour drop-down names every colour** | ✅ Done — 2026-10-21 (the creator's "the #hex values are good, but add a corresponding color name/label next to the value" — the 14 colours moved out of the click handler into `Models/BorderColorPalette.cs`, and each row now reads "#e94560 Crimson") | [`TASKS/task-60-border-color-names.md`](TASKS/task-60-border-color-names.md) | | 61 | **The geometry fields share one fixed width** | ✅ Done — 2026-10-21 (the creator's "the Y and H boxes should be the same width as the X and W boxes and those widths should be fixed to accomodate the max number of possible digits" — the Y/H fields used to stretch to fill the row; all four are now 44px, wide enough for 1920 plus a sign) | [`TASKS/task-61-geometry-field-widths.md`](TASKS/task-61-geometry-field-widths.md) | | 62 | **The border-colour field shows the colour's name next to the hex** | ✅ Done — 2026-10-21 (the creator: "the border color drop down box content should also include the name of the color next to the hex value as they in the drop-down list box" — the field keeps the hex editable and the palette's name sits right beside it, empty for a custom hex) | [`TASKS/task-62-border-colour-name-in-the-field.md`](TASKS/task-62-border-colour-name-in-the-field.md) | -| 63 | **The border-colour picker is the app's own drop-down** | ✅ Done — 2026-10-21 (the creator's "why does the drop down under the web cam properties not show the same style for the border color drop down list box?" + "we established look-and-feel standards. And yet you keep ignoring these" — the hand-rolled `...` button and its code-built menu are gone; the border colour is now a `YtComboBox` like every other picker, the closed box shows the swatch + the colour's name, the rows show swatch + hex + name, and the hex field stays for a custom colour) | [`TASKS/task-63-border-colour-app-dropdown.md`](TASKS/task-63-border-colour-app-dropdown.md) | +| 63 | **The border-colour picker is the app's own drop-down** | ✅ Done — 2026-10-21 (popup row click deselection fixed 2026-10-22: `LeftPanel.IsDescendantOf` now crosses `PopupRoot` → `PART_Popup` via `LogicalTreeHelper.GetParent` when `VisualTreeHelper.GetParent` returns `null`, so clicking a colour in the open `YtComboBox` updates `BorderColor` instead of exiting properties) | [`TASKS/task-63-border-colour-app-dropdown.md`](TASKS/task-63-border-colour-app-dropdown.md) | --- diff --git a/TASKS/task-63-border-colour-app-dropdown.md b/TASKS/task-63-border-colour-app-dropdown.md index e61afc4..26c2ccb 100644 --- a/TASKS/task-63-border-colour-app-dropdown.md +++ b/TASKS/task-63-border-colour-app-dropdown.md @@ -31,6 +31,17 @@ not *"is this control type themed?"*. matches `SelectedValue` ordinally (case-sensitively) and the palette mixes `#FFFFFF` and `#e94560`; the model matches by VALUE, so the hex the creator typed in any case still lands on the right row. +- **Clicking a popup row stays inside `LeftPanel` (2026-10-22 fix):** a `ComboBox`'s open + `PART_Popup` lives in its own `PopupRoot` visual tree (`VisualTreeHelper.GetParent(popupRoot)` + is `null`), connected back to the `Popup` and `ComboBox` only via `PopupRoot`'s **logical** + parent (`LogicalTreeHelper.GetParent(popupRoot)`). `LeftPanel.IsDescendantOf` previously used + `child is Visual ? VisualTreeHelper.GetParent(child) : LogicalTreeHelper.GetParent(child)`, + which returned `null` at `PopupRoot` because `PopupRoot` is a `Visual`. That caused + `MainWindow.Window_PreviewMouseLeftButtonDown` to see `IsClickInside == false` on any click + inside the open dropdown and clear `SelectedElement = null` — exiting the properties panel + before the `ComboBoxItem` click could select the colour. `IsDescendantOf` now falls back to + `LogicalTreeHelper.GetParent` and `TemplatedParent` whenever `VisualTreeHelper.GetParent` + returns `null`. - **The closed box shows the swatch + the colour's NAME; the rows show swatch + hex + name** (the rows the creator approved in TASK 60). A ComboBox cannot do that with one `ItemTemplate` — WPF copies it into the read-only `SelectionBoxItemTemplate` — so diff --git a/ai.md b/ai.md index d46a3dd..897d5ee 100644 --- a/ai.md +++ b/ai.md @@ -311,7 +311,13 @@ C# / WPF (.NET 8) following MVVM: the `YtComboBox` popup carries no width binding, so the pop-up is as wide as its widest row (measured 142.7px for the border-colour rows against a 100px ComboBox) — a row is never clipped, and the popup may be wider than the closed box. Measure (`RenderTargetBitmap`/`ActualWidth` in a - throwaway test), don't guess. + throwaway test), don't guess. **And a `Popup`'s visual root (`PopupRoot`) has a `null` visual + parent (`VisualTreeHelper.GetParent(popupRoot) == null`)** even though `PopupRoot` IS a `Visual`: + its only link back to the `Popup` and the owning `ComboBox` is `LogicalTreeHelper.GetParent`. Any + ancestor walk used by a window-level click-outside guard (`LeftPanel.IsDescendantOf`) MUST fall + back to `LogicalTreeHelper.GetParent(child)` when `VisualTreeHelper.GetParent(child)` returns + `null`, or clicking a `ComboBoxItem` inside `PART_Popup` looks like an outside click and + deselects `SelectedElement` before the item click registers (fixed 2026-10-22). - **A field in a dense panel carries a FIXED width (TASK 61):** a WPF element sizes itself to its content, and in a `DockPanel` the **last child fills the row** — so a row of same-kind fields looks broken unless every field has an explicit `Width` AND the row sets `LastChildFill="False"`. The @@ -2068,13 +2074,13 @@ Apply this to every UI decision: > Everything below is the historical paid-tier design. Full pricing and the Year 1 revenue forecast in `MONETIZATION.md` (gitignored). ⚠️ **No Polar -product, discount code, or license key exists any more** — billing is Microsoft Store IAP. +product, discount code, or license key exists any more** — nothing bills today; Polar is dormant. Brand palette and assets in `MARCOM.md` (gitignored). This section covers the in-app model. -Free forever: **all features unlocked for everyone** — no feature lock between free and paid. -The **only difference is watermarking**. This is deliberate: no creator will tolerate a watermark, -and as a good-guy developer, we give them complete access to every feature so no one can call us -crooked. +Free forever: **all features unlocked for everyone** — no feature lock, no paid tier, and since +2026-10-08 **no watermark either** (the credit is OFF by default; see the banner above). No +creator will tolerate a watermark, and as a good-guy developer we give everyone complete access +so no one can call us crooked. Money comes from gratuity/merch, not gating (TASK 54). - **Free:** a periodic branding credit — "made with LlamaCasty!" in **full-frame neon** (white core, feathered red/blue halo with the channels split in opposite directions), at a **random position @@ -2122,34 +2128,24 @@ crooked. finish. That's it. No feature gating. Alerts, social bar slots, voice filters, TRAX, recording — everything is free. **How `IsPremium` is obtained is a billing concern and has changed twice; what it gates has not.** -- **Pricing (✅ DECIDED 2026-09-27 — supersedes the 2026-09-21 one-time "own it" model):** - **`$10/mo` subscription, or `$400` lifetime. No annual. No `.99` prices.** ⛔ The old `$29` - founder → `$49` list lifetime ruling is **dead — do not cite it.** Two tiers, one delta: they - break even at **month 40**, so they barely compete, which is why lifetime is safe to offer at - launch instead of saving it for later. The free tier is the *full* app (only the watermark - differs), so accessibility is preserved without pricing down. ⛔ No annual tier: Microsoft does - not pro-rate and **the developer cannot refund** the forfeited remainder — verbatim the - grievance that drove the ruling. **ADA §6(h): raising the price disables auto-renew, so `$10` is - effectively locked for the product's life.** Full reasoning in `MONETIZATION.md` → Pricing. -- **Billing: ⛔ Polar is DELETED — Microsoft Store IAP, Microsoft as merchant of record** (ruling - 2026-09-27; teardown in `TASKS/task-48-distribution-msix.md` item 0). **Revenue share 15%, - subscriptions included** — verified from the ADA v8.10 PDF, §6(b)(i); the agreement's own v8.0 - changelog says *"85/15 revenue share for non-Game subscriptions"*. ⇒ net **~$8.50/mo**, - **~$340** lifetime. Microsoft handles VAT/GST/sales tax, refunds, chargebacks, the customer - portal, and device deactivation — the strongest argument for the switch. Net receipts are after - tax, so commission is not paid on pass-through tax. Payouts monthly, $50 threshold. - ⚠️ **No Polar product, org ID, key prefix, or discount code exists any more** — `d105dfa1-…`, - `c05fb364-…`, `LCYT-`, `LLAMAFOUNDER`/`LLAMA50`, the customer-portal validate endpoint, and the - 14-day offline-grace machine are all being deleted. Products are declared in **Partner Center** - instead: one auto-renewing monthly subscription + one non-expiring lifetime IAP. - ⛔ A discounted subscriber→lifetime upgrade SKU is **not** approved. +- **Pricing: ⛔ SUPERSEDED 2026-10-08 by the free-forever ruling (TASK 54).** Retained as + reasoning only: `$10/mo` subscription or `$400` lifetime, no annual, ruled 2026-09-27 and + overtaken ten days later. If a paid tier ever returns, these are the numbers to argue from. + **How `IsPremium` would be obtained is a billing concern; what it gates is not.** +- **Billing: ⏸ dormant — no paid tier exists** (free-forever ruling 2026-10-08, TASK 54). The + Store IAP plan of 2026-09-27 was never implemented (`Windows.Services.Store` appears nowhere + in the repo) and was withdrawn with the Store route on 2026-10-22. If a paid tier ever + returns, the provider is **Polar** — merchant of record for VAT/sales tax, 5% + $0.50 Starter, + **$50 already paid**; `Distribution.md` §4.2/§4.5 holds the design. ⚠️ No Polar product, org + ID, key prefix, or discount code exists (`d105dfa1-…`, `c05fb364-…`, `LCYT-`, `LLAMAFOUNDER`/ + `LLAMA50`, the customer-portal validate endpoint, and the 14-day offline-grace machine are all + gone). - **Support (creator's model, corrected 2026-09-01):** support = email + GitHub issues — the once-planned in-app bug-reporter is **out of product** (TASKS.md → closed list). Most queries are how-tos / feature requests / manual-skimmers. Maintenance cadence = "when I get around to it" with - emergency patches; not a 24/7 service promise. The only license chatter is the paid-user **expiry - reminder — now moot** (superseded twice: keys are moot because the lifetime entitlement never - expires, and the entire license-key system is moot because Store IAP has no keys); paid users see - zero license UI. TASK 36 item 5 (expiry reminder) is struck. + emergency patches; not a 24/7 service promise. There is **no licence chatter at all** — the app + is free, there are no keys, and no user sees licence UI. TASK 36 item 5 (expiry reminder) is + struck. **Monetization awareness (built-in, ungated, free — 2026-09-01; v1 SCOPE per the complete-v1 ruling — build order: capture → report → journey → Alerts):** the app is monetization-aware by @@ -2569,52 +2565,57 @@ published decisions (recorded in `TASKS/task-43-native-alerts.md`): --- -## Windows packaging & distribution (✅ DECIDED 2026-09-27 — Store MSIX + Store IAP) +## Windows packaging & distribution (⛔ REVERSED 2026-10-22 — direct + Steam; Store dropped) -**The distribution route is settled: Microsoft Store, MSIX package, Store IAP.** Creator's -criteria, verbatim: *"zero headaches, minimal maintenance (for me) while still providing -accountability and a reasonably easy upgrade flow."* Route A was chosen because it is the -only option where all four are solved by handing the work to Microsoft rather than to a -certificate vendor: **$0/yr, no certificate, no HSM, no annual renewal, no SmartScreen -ramp**, plus Store auto-update, Store-side payments/entitlements/refunds/support, and -Microsoft review as the accountability layer. The rejected options and their reasons live in -`TASKS/research-store-certification.md` §3 — **read that before reopening the question**, not -before re-deriving it. Executable checklist: `TASKS/task-48-distribution-msix.md`. +**The Microsoft Store route is dead.** A brand name on the Store's *publisher* line requires a +**Company** account, which requires a registered legal entity (an LLC — ~$85 New Mexico with a +~2-week SOS backlog, ~$143 Wyoming). Partner Center does **not** convert an Individual account to +a Company account later, and an Individual account publishes under the verified legal name. The +creator will not fund an entity for a nameplate, so the 2026-09-27 Store ruling was withdrawn on +2026-10-22. Store IAP was never implemented either — `Windows.Services.Store` appears nowhere in +the repo. + +**The route now:** + +1. **Direct download from `llamacasty.com`** — the app is **free forever** (TASK 54), so there is + nothing to bill in v1. +2. **Steam when $100 exists** — Steam Direct is $100 per product (recoupable after $1,000 AGR), + accepted categories include *Audio/Video Production* / *Player Tools* (OBS Studio is the + precedent), the **developer/publisher name is self-chosen** so the brand needs no entity, + 5+ screenshots at 1920×1080 + capsule art, a build Valve runs, and a **2-week live page** + before release. Valve takes 30%. +3. **Polar is dormant, not deleted-in-principle** — chosen checkout/licence provider if a paid + tier ever returns; $50 already paid. `Distribution.md` §4.2/§4.5 holds the design. +4. **The Microsoft Store: not pursued.** The MSIX package stays as a working artifact and is + submitted nowhere. History: `TASKS/research-store-certification.md`, + `TASKS/task-48-distribution-msix.md`. **Consequences that are architecture, not packaging detail:** -- **⛔ Velopack and the update URL are deleted** — the Store auto-updates. **DONE 2026-10-22** - (TASK 48 slice 3): the `PackageReference` in `ytLive.csproj` and the bootstrap in - `App.xaml.cs` are gone, and `ytLive.Tests/StorePackagingTests.cs` guards that no updater - package or updater API comes back. The self-hosted DO droplet dies with it. -- **The package is built by `scripts/package-msix.sh` (2026-10-22, TASK 48 slice 2)** — publish +- **⛔ The updater is now MISSING, not deleted-by-design.** Velopack and its bootstrap were + removed on 2026-10-22 (TASK 48 slice 3) because the Store was going to deliver updates — that + premise is gone. An update path must come back, and + `ytLive.Tests/StorePackagingTests.cs` (which asserts no updater returns) must be rewritten + with the chosen updater rather than left to veto one. +- **The MSIX package still builds** via `scripts/package-msix.sh` (TASK 48 slice 2) — publish `Release -r win-x64 --self-contained` into `artifacts/msix-stage`, copy `Packaging/Package.appxmanifest` in as `AppxManifest.xml` + `Packaging/Assets/`, then `makeappx pack` → `artifacts/LlamaCasty.msix` (159 MB, gitignored; `MSIX_PFX` signs it for a local install). The manifest is a **full-trust `packagedClassicApp` at `mediumIL`** with - `runFullTrust` + webcam + microphone — that is what lets the app launch the bundled ffmpeg as - a child process; an `appContainer` package would virtualise the recordings folder instead. - `Square310x310Logo`/`Wide310x150Logo` live on `uap:DefaultTile`, **not** on `VisualElements`. - Logos come from the app's own artwork via `scripts/make-store-logos.py`. -- **⛔ The Polar licensing subsystem is deleted** — `PolarLicenseService`, `PolarLicense`, - `MainViewModel.License.cs` (`PremiumUrl`, customer portal, the - `OfflineGracePeriod = 14 days` subscription-era artifact, renewal/lapse copy), and the - wrong "Polar unlocks alerts" string in `OverlayHost.xaml`. **`IsPremium` is derived from the - Store entitlement**, not a remote HTTP call. That deletes the entire "network flaky → app - thinks I'm expired" bug class along with the offline-grace machine. -- **⛔ Bundle ffmpeg — no runtime downloads** (Store policy 10.2.2; also the 2026-09-01 404). - See the FFmpeg locator section above. **This is the first code unit.** + `runFullTrust` + webcam + microphone. `Square310x310Logo`/`Wide150x150Logo` traps are in + `MyMistakes.md`. Logos come from the app's own artwork via `scripts/make-store-logos.py`. +- **⛔ Bundle ffmpeg — no runtime downloads** (was Store policy 10.2.2; still right for a direct + download, and it fixes the 2026-09-01 404). See the FFmpeg locator section above. - **⛔ 64-bit Windows only (creator ruling 2026-10-09).** Both csproj files set `x64` (the test project too — an AnyCPU test project referencing an x64 app raises MSB3270 and breaks the 0-warning gate). Why: the bundled ffmpeg is the win64 build, ONNX Runtime 1.31 ships no win-x86 native (only win-x64/win-arm64), and a - 32-bit address space can't hold the 1080p frame buffers. Windows 11 is 64-bit-only anyway. The - MSIX publish targets x64 only (ARM64 is a possible later add: both ffmpeg and ONNX Runtime have - arm64 builds). Public copy says "64-bit Windows 10 or 11". + 32-bit address space can't hold the 1080p frame buffers. Windows 11 is 64-bit-only anyway. + Public copy says "64-bit Windows 10 or 11". -**What does NOT change: the monetization posture.** Free gets everything; the branding flash -stays the **only** paid delta (`TASK 36` item 2). Store IAP changes how `IsPremium` is -*obtained*, never what it *gates*. `Monetization` above still governs. +**What does NOT change: the monetization posture.** The app is **free forever with no watermark** +(TASK 54, 2026-10-08), so `IsPremium` gates nothing in v1 and the branding flash is OFF by +default. `Monetization` above governs. ### ⛔ Durable invariant: full trust, or recording silently breaks @@ -2642,19 +2643,21 @@ EV-signed file now accrues reputation exactly like an OV-signed one, so EV ($400 what OV (~$150–300/yr) buys. Signing is **not** instant regardless — a valid cert stops the *malware* warning immediately, but "unrecognized publisher" clears only as SmartScreen accrues reputation from real download volume. Cert validity is capped at **460 days** (recurring -line item) and private keys must live on an **HSM or hardware token**. The **Store MSIX route -needs no certificate at all** — Microsoft re-signs. `Distribution.md` §4.3 was corrected -2026-09-27; it previously recommended EV. +line item) and private keys must live on an **HSM or hardware token**. **The 2026-10-22 ruling +is to ship unsigned and accept the "unrecognized publisher" warning** until download reputation +accrues — buying a certificate is deferred, not rejected. `Distribution.md` §4.3 was corrected +2026-09-27 (it previously recommended EV). -### Distribution and licensing are independent — but the ruling collapsed them +### Distribution and licensing are independent — and the channel decision does not decide licensing -A **Store-distributed** app *may* still verify licenses with **Polar** (policy 10.8.1 -explicitly permits a secure third-party purchase API for non-game PC products), so "should we -use the Store?" did not logically imply "drop Polar?" — route B was a real option. **The -creator chose route A instead**, so Polar is gone. Recording the distinction because it was -the reason the decision was thought-through rather than assumed: the licensing choice rode on -a distribution decision, and the *only* route that removed the licensing backend was the one -that also removed the certificate. Both burdens had the same single solution. +A **Store-distributed** app *may* still verify licences with **Polar** (Store policy 10.8.1 +explicitly permits a secure third-party purchase API for non-game PC products), so "which +channel?" never implied "which billing?" — route B was a real option. The 2026-10-22 reversal +proves the point in the other direction: dropping the Store did not by itself restart Polar, +because the **2026-10-08 free-forever ruling** (TASK 54) had already removed the paid tier. +Polar is therefore *dormant*, ready if a paid tier ever returns, and nothing in v1 depends on it. +The lesson worth keeping: the licensing choice rides on a **monetization** decision, not on a +distribution decision. ### ⛔ "Only when the user says so" is enforced by Windows, not by us diff --git a/ytLive.Tests/BorderColorPickerTests.cs b/ytLive.Tests/BorderColorPickerTests.cs index c41914e..881a476 100644 --- a/ytLive.Tests/BorderColorPickerTests.cs +++ b/ytLive.Tests/BorderColorPickerTests.cs @@ -132,9 +132,107 @@ public sealed class BorderColorPickerTests $"the drop-down rows do not show the colour's name: '{rows}'"); window.Close(); + + // ── 4. clicking a row inside the open popup stays inside LeftPanel ─────── + // MainWindow_PreviewMouseLeftButtonDown deselects SelectedElement (exiting + // the properties listing) whenever LeftPanel.IsClickInside(e.OriginalSource) + // returns false. A ComboBox's PART_Popup lives in its own PopupRoot visual + // tree whose VisualTreeHelper.GetParent is null — the only link back to the + // ComboBox and ElementsScrollViewer is PopupRoot's LOGICAL parent. + panel.DataContext = new PanelContext { SelectedElement = element }; + var hostWindow = new Window { Width = 320, Height = 720, Content = panel }; + hostWindow.Show(); + panel.UpdateLayout(); + Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Loaded); + + var realCombo = FindVisualChildren(panel) + .Single(c => c.ItemsSource is IEnumerable opts && opts.Any(o => o.Hex == "#e94560")); + realCombo.IsDropDownOpen = true; + realCombo.UpdateLayout(); + Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Loaded); + + var crimsonEntry = BorderColorPalette.EntryFor("#e94560"); + var crimsonItem = realCombo.ItemContainerGenerator.ContainerFromItem(crimsonEntry) as ComboBoxItem; + Assert.NotNull(crimsonItem); + + // The click target inside the popup row (e.g. a TextBlock inside the item) + // must be recognised as inside LeftPanel, or MainWindow clears SelectedElement. + var clickTarget = FindVisualChildren(crimsonItem!).FirstOrDefault() + ?? (DependencyObject)crimsonItem!; + Assert.True(panel.IsClickInside(clickTarget), + "clicking a colour row in the open ComboBox popup is treated as a click outside LeftPanel (exits properties)"); + + // Clicking the row in the open popup updates the webcam's BorderColor and closes the popup. + crimsonItem!.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs( + System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left) + { + RoutedEvent = UIElement.MouseLeftButtonDownEvent, + Source = clickTarget, + }); + crimsonItem.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs( + System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left) + { + RoutedEvent = UIElement.MouseLeftButtonUpEvent, + Source = clickTarget, + }); + realCombo.UpdateLayout(); + Assert.Equal("#e94560", element.BorderColor); + Assert.Equal(crimsonEntry, realCombo.SelectedItem); + Assert.Contains("Crimson", Texts(realCombo)); + Assert.False(realCombo.IsDropDownOpen); + + // And when starting from a typed custom hex (#123456), picking a palette + // row in the open popup still lands cleanly on that palette colour. + element.BorderColor = "#123456"; + realCombo.IsDropDownOpen = true; + realCombo.UpdateLayout(); + Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Loaded); + + var emeraldEntry = BorderColorPalette.EntryFor("#22c55e"); + var emeraldItem = realCombo.ItemContainerGenerator.ContainerFromItem(emeraldEntry) as ComboBoxItem; + Assert.NotNull(emeraldItem); + var emeraldTarget = FindVisualChildren(emeraldItem!).FirstOrDefault() + ?? (DependencyObject)emeraldItem!; + Assert.True(panel.IsClickInside(emeraldTarget)); + emeraldItem!.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs( + System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left) + { + RoutedEvent = UIElement.MouseLeftButtonDownEvent, + Source = emeraldTarget, + }); + emeraldItem.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs( + System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left) + { + RoutedEvent = UIElement.MouseLeftButtonUpEvent, + Source = emeraldTarget, + }); + realCombo.UpdateLayout(); + Assert.Equal("#22c55e", element.BorderColor); + Assert.Equal(emeraldEntry, realCombo.SelectedItem); + Assert.Contains("Emerald", Texts(realCombo)); + + hostWindow.Close(); }); } + private sealed class PanelContext + { + public SceneElement? SelectedElement { get; set; } + public bool IsWebcamSelected => true; + public bool CanToggleClipShape => true; + } + + private static IEnumerable FindVisualChildren(DependencyObject root) where T : DependencyObject + { + for (var i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++) + { + var child = VisualTreeHelper.GetChild(root, i); + if (child is T match) yield return match; + foreach (var nested in FindVisualChildren(child)) + yield return nested; + } + } + /// Every text in a visual tree, so a template's content can be read back. private static string Texts(DependencyObject root) {