diff --git a/samples/collectible-card-binder/README.md b/samples/collectible-card-binder/README.md new file mode 100644 index 00000000..60a40569 --- /dev/null +++ b/samples/collectible-card-binder/README.md @@ -0,0 +1,100 @@ +# Moonfield Archive + +## Summary + +A collectible-card binder built as a real Power Apps canvas app. Open free three-card packs, collect twelve original fictional wonders across three realms, and track your progress. No external data source or connector is required. + +![Moonfield Archive binder in the real Power Apps player](assets/binder.jpeg) +![Free pack results in the real Power Apps player](assets/packs.jpeg) + +## Applies to + +![Power Apps](https://img.shields.io/badge/Power%20Apps-Yes-green) +![Power Fx](https://img.shields.io/badge/Power%20Fx-Yes-green) +![Power Automate](https://img.shields.io/badge/Power%20Automate-No-red) +![Custom Connectors](https://img.shields.io/badge/Custom%20Connectors-No-red) + +## Compatibility + +No premium connectors, external APIs, flows, or runtime Dataverse tables. A suitable Power Apps entitlement and an environment where you can create canvas apps are still required. The app uses classic canvas controls; its exported YAML authoring format is Preview. Prefer editing the `.msapp` in Studio. Desktop/tablet layout, 1366 × 768 with scale-to-fit; phone layout is not optimized. + +## Contributors + +* [Gavin Warner](https://github.com/GavinWarner05) + +## Version history + +| Version | Date | Comments | +| --- | --- | --- | +| 1.0.0.3 | 2026-10-12 | Free unlimited packs, weighted rarity, automatic awards and duplicate-safe completion | +| 1.0.0.2 | 2026-10-12 | Accessibility cleanup and unmanaged solution export | +| 1.0.0 | 2026-10-12 | Initial private sample, original SVG artwork, device-local collection | + +## Prerequisites + +- Power Apps access and maker permission in a development environment. +- A current desktop browser. Browser storage must be available for persistence. +- Optional: Python 3 for the dependency-free reference tests and artwork generator; Microsoft Power Platform CLI for source inspection. + +## Minimal path to awesome + +1. Open the Power Apps maker portal and select your development environment. +2. Choose **Apps → Import app → From file (.msapp)** and select `solution/MoonfieldArchive.msapp`. Depending on portal version, use Studio **Open → Browse** instead. +3. Save the imported app under a new name. Run App OnStart or restart Preview to initialize the catalogue. +4. Preview it, then save and publish your own private development version. Publishing an app version does not itself share it. +5. Open the app in the Power Apps player. Select **Open packs → Open free pack**, then tap each card to reveal it. New cards are automatically awarded and saved before reveal. Return to the binder, switch **Owned/Missing**, combine rarity with search, or select **Details** for a card's story. +6. **Reset** requires confirmation. **Keep collection** cancels it. + +No connection credentials or sample data import are needed. Alternatively, import `solution/MoonfieldArchive_1_0_0_3.zip` through **Solutions → Import solution** in a Dataverse-enabled development environment with appropriate maker/customizer permissions. This unmanaged ZIP contains only the sample canvas app and its neutral publisher. Review any existing component matches before importing; use a separate development environment for a clean import. The export, local unpack/repack, and a clean-target solution import passed. The imported private player was tested in Microsoft Edge on macOS on 2026-10-18. See [validation](docs/validation.md). + +## Features + +- Twelve original SVG illustrations, names and short stories, embedded in the app. +- Four named rarities with distinct borders, searchable names and realms. +- Combined ownership, rarity and search filters; explicit empty states. +- Free unlimited three-card packs; no purchases, currency, daily limits or trading. +- Tap-to-reveal cards marked New or Already owned; duplicate quantities are not tracked. +- Completion count, rounded percentage and progress meter; only unique cards count. +- Gentle fade when opening card details; explicit buttons and accessible image descriptions. +- Device-local ownership with a visible session-only fallback when storage fails. + +Ownership is stored in the current device/browser only. It is not backed up or synchronized across browsers or accounts. Clearing browser storage can erase it. Never adapt this store for sensitive data without revisiting the design. + +## Pack odds and saving + +Each of three slots independently selects a rarity, then a card uniformly within that rarity. Draws are with replacement, so a pack can contain duplicates. + +| Rarity | Per-slot chance | Cards in pool | Chance of each card per slot | +| --- | ---: | ---: | ---: | +| Common | 60% | 4 | 15% | +| Uncommon | 25% | 3 | 8⅓% | +| Rare | 12% | 3 | 4% | +| Legendary | 3% | 2 | 1.5% | + +Odds are displayed before opening. New/Already owned is determined at pack creation in slot order: only the first occurrence of a previously missing card is New. Reveal all three to enable the next pack. Revealing never awards a second copy. + +Leaving the screen preserves the current reveal session. Reloading closes the transient pack, but successfully saved awards remain in the binder, even when left unrevealed. If saving fails, the app clearly shows **NOT SAVED · Session only** and **Retry saving**. Keep the app open and retry; closing can lose unsaved cards. Existing `MoonfieldOwnedV1` collections retain their IDs and are compatible with this update in the same app/browser. Importing as a different app can use a separate storage scope. + +## Source and verification + +`sourcecode/solution` contains the extracted unmanaged solution. `sourcecode/studio` contains the final Studio-normalized `.pa.yaml` source and `.msapr`. `sourcecode/cards.json` and `sourcecode/generate.py` contain the original catalogue and artwork generator. Edits to JSON do not automatically update the embedded app catalogue. + +To rebuild the solution archive from the extracted solution, run this from the sample directory with Power Platform CLI: + +```bash +pac solution pack --zipfile MoonfieldArchive-repacked.zip --folder sourcecode/solution --packagetype Unmanaged +``` + +This packages the embedded `.msapp` in `sourcecode/solution/CanvasApps`; it does not rebuild that app from the separate review YAML. Edit the app in Studio and export it again when changing formulas. PAC-generated internal folder names and casing are preserved. + +Run `python3 -m unittest discover -s tests -v` from this sample directory. These are local reference/data checks, not Power Fx execution. See [architecture](docs/architecture.md) and [exact validation results](docs/validation.md). + +## Help + +We do not support samples, but this community is always willing to help. For issues, questions or suggestions, [open an issue in the Power Platform samples repository](https://github.com/pnp/powerplatform-samples/issues/new/choose). + +## Disclaimer + +**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** All characters, artwork and lore in this sample were created for this sample; no third-party card franchise assets are included. Review licensing and tenant requirements before production use. + + diff --git a/samples/collectible-card-binder/assets/binder.jpeg b/samples/collectible-card-binder/assets/binder.jpeg new file mode 100644 index 00000000..a3e7c9ee Binary files /dev/null and b/samples/collectible-card-binder/assets/binder.jpeg differ diff --git a/samples/collectible-card-binder/assets/cards/MF-001.svg b/samples/collectible-card-binder/assets/cards/MF-001.svg new file mode 100644 index 00000000..a4c1114c --- /dev/null +++ b/samples/collectible-card-binder/assets/cards/MF-001.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/collectible-card-binder/assets/cards/MF-002.svg b/samples/collectible-card-binder/assets/cards/MF-002.svg new file mode 100644 index 00000000..bb8f9612 --- /dev/null +++ b/samples/collectible-card-binder/assets/cards/MF-002.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/collectible-card-binder/assets/cards/MF-003.svg b/samples/collectible-card-binder/assets/cards/MF-003.svg new file mode 100644 index 00000000..4f2db2b8 --- /dev/null +++ b/samples/collectible-card-binder/assets/cards/MF-003.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/collectible-card-binder/assets/cards/MF-004.svg b/samples/collectible-card-binder/assets/cards/MF-004.svg new file mode 100644 index 00000000..702278c5 --- /dev/null +++ b/samples/collectible-card-binder/assets/cards/MF-004.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/collectible-card-binder/assets/cards/MF-005.svg b/samples/collectible-card-binder/assets/cards/MF-005.svg new file mode 100644 index 00000000..4c37b266 --- /dev/null +++ b/samples/collectible-card-binder/assets/cards/MF-005.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/collectible-card-binder/assets/cards/MF-006.svg b/samples/collectible-card-binder/assets/cards/MF-006.svg new file mode 100644 index 00000000..e103a37b --- /dev/null +++ b/samples/collectible-card-binder/assets/cards/MF-006.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/collectible-card-binder/assets/cards/MF-007.svg b/samples/collectible-card-binder/assets/cards/MF-007.svg new file mode 100644 index 00000000..d1d49505 --- /dev/null +++ b/samples/collectible-card-binder/assets/cards/MF-007.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/collectible-card-binder/assets/cards/MF-008.svg b/samples/collectible-card-binder/assets/cards/MF-008.svg new file mode 100644 index 00000000..fc222eda --- /dev/null +++ b/samples/collectible-card-binder/assets/cards/MF-008.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/collectible-card-binder/assets/cards/MF-009.svg b/samples/collectible-card-binder/assets/cards/MF-009.svg new file mode 100644 index 00000000..034723ac --- /dev/null +++ b/samples/collectible-card-binder/assets/cards/MF-009.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/collectible-card-binder/assets/cards/MF-010.svg b/samples/collectible-card-binder/assets/cards/MF-010.svg new file mode 100644 index 00000000..c772e371 --- /dev/null +++ b/samples/collectible-card-binder/assets/cards/MF-010.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/collectible-card-binder/assets/cards/MF-011.svg b/samples/collectible-card-binder/assets/cards/MF-011.svg new file mode 100644 index 00000000..d991fa7b --- /dev/null +++ b/samples/collectible-card-binder/assets/cards/MF-011.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/collectible-card-binder/assets/cards/MF-012.svg b/samples/collectible-card-binder/assets/cards/MF-012.svg new file mode 100644 index 00000000..b40b678e --- /dev/null +++ b/samples/collectible-card-binder/assets/cards/MF-012.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/collectible-card-binder/assets/details.jpeg b/samples/collectible-card-binder/assets/details.jpeg new file mode 100644 index 00000000..d3c3f0fb Binary files /dev/null and b/samples/collectible-card-binder/assets/details.jpeg differ diff --git a/samples/collectible-card-binder/assets/packs.jpeg b/samples/collectible-card-binder/assets/packs.jpeg new file mode 100644 index 00000000..f56e4f8d Binary files /dev/null and b/samples/collectible-card-binder/assets/packs.jpeg differ diff --git a/samples/collectible-card-binder/assets/sample.json b/samples/collectible-card-binder/assets/sample.json new file mode 100644 index 00000000..18dcb62a --- /dev/null +++ b/samples/collectible-card-binder/assets/sample.json @@ -0,0 +1,61 @@ +[ + { + "name": "pnp-powerplatform-samples-collectible-card-binder", + "source": "pnp", + "title": "Moonfield Archive", + "shortDescription": "Free three-card packs, original fictional art, a collectible binder and device-local progress.", + "url": "https://github.com/pnp/powerplatform-samples/tree/main/samples/collectible-card-binder", + "longDescription": [ + "Collect twelve original fictional cards through unlimited free packs with visible rarity odds, duplicate-safe awards, tap reveals and device-local ownership." + ], + "creationDateTime": "2026-10-12", + "updateDateTime": "2026-10-18", + "products": [ + "Power Apps", + "Power Fx" + ], + "tags": [ + "Gallery", + "Collectibles", + "SaveData", + "SVG" + ], + "categories": [ + "CANVAS" + ], + "metadata": [ + { + "key": "POWERAPPS-EXPERIMENTAL", + "value": "Yes" + }, + { + "key": "POWERAPPS-PREMIUM", + "value": "No" + }, + { + "key": "POWERAPPS-ONPREM", + "value": "No" + }, + { + "key": "POWERAPPS-CUSTOMCONNECTOR", + "value": "No" + } + ], + "thumbnails": [ + { + "type": "image", + "order": 100, + "url": "https://github.com/pnp/powerplatform-samples/raw/main/samples/collectible-card-binder/assets/binder.jpeg", + "alt": "Moonfield Archive in the real Power Apps player" + } + ], + "authors": [ + { + "gitHubAccount": "GavinWarner05", + "name": "Gavin Warner", + "pictureUrl": "https://avatars.githubusercontent.com/u/146680790?v=4" + } + ], + "version": "1.0.0.3" + } +] diff --git a/samples/collectible-card-binder/docs/app-checker.sarif b/samples/collectible-card-binder/docs/app-checker.sarif new file mode 100644 index 00000000..a5b03729 --- /dev/null +++ b/samples/collectible-card-binder/docs/app-checker.sarif @@ -0,0 +1 @@ +{"$schema":"https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.4.json","version":"2.1.0","runs":[{"results":[{"ruleId":"app-CollectingReadOnlyTable","ruleIndex":0,"message":{"id":"issue"},"locations":[{"physicalLocation":{"address":{"relativeAddress":0,"fullyQualifiedName":"App.OnStart"}},"logicalLocations":[{"fullyQualifiedName":"App.OnStart"}],"properties":{"module":"App","type":"App","member":"OnStart"}}],"properties":{"level":"Medium"}}],"tool":{"driver":{"name":"PowerApps app checker","fullName":"PowerApps app checker","version":"1.349","rules":[{"id":"app-CollectingReadOnlyTable","messageStrings":{"issue":{"text":"Collection is initialized but never updated"}},"properties":{"howToFix":["Replace the Collect or ClearCollect with a named formula on App.Formulas. For example, MyTable = Filter(source, condition). Named formulas are computed when their inputs change and do not carry the tracking cost of a collection."],"whyFix":"Collections are meant to be updated. Power Apps does extra work to keep track of every collection so that anything depending on it stays in sync when the data changes. When the data never changes after it is set, that tracking work is wasted. A named formula gives the same data without the extra cost.","componentType":"app","primaryCategory":"performance","level":"Medium"}}]}},"invocations":[{"executionSuccessful":true}],"columnKind":"utf16CodeUnits"}]} \ No newline at end of file diff --git a/samples/collectible-card-binder/docs/architecture.md b/samples/collectible-card-binder/docs/architecture.md new file mode 100644 index 00000000..82816f00 --- /dev/null +++ b/samples/collectible-card-binder/docs/architecture.md @@ -0,0 +1,46 @@ +# Architecture and authoring + +The app has three screens: `BinderScreen` holds the catalogue, completion meter, filters and reset confirmation; `DetailScreen` displays `varCard`; `PackScreen` generates and reveals packs. `colCards` is a twelve-row in-memory collection initialized in `App.OnStart`. SVG strings become image data URIs with `EncodeUrl`. No remote artwork is fetched. + +`colOwned` contains only stable `Id` values. `LoadData` and `SaveData` use `MoonfieldOwnedV1`; unknown IDs are discarded on load and progress counts distinct IDs. `IfError` exposes a storage warning and allows session-only use. The twelve IDs serialize to less than 1 KB. Browser/Teams SaveData has a 1 MB limit and browser storage is not suitable for secrets. This intentionally avoids tenant lists, premium connectors and per-user server provisioning. It does not provide cross-device sync, collaborative ownership, backup or confidential storage. + +The gallery applies ownership, rarity and trimmed case-insensitive search together. Rarity appears as text as well as border color. The reset overlay hides background interactive controls. Buttons have meaningful text/tooltips, and artwork has accessible labels. Keyboard cancellation was exercised. Decorative controls have empty OnSelect actions and do not create spurious keyboard stops. The disabled card-frame button retains its default TabIndex 0, but DisplayMode.Disabled excludes it from interaction. Inputs have explicit gold focus borders. Zero accessibility checker warnings remain; this is not a WCAG conformance claim. Full screen-reader testing is still needed. The fixed tablet design scales down on smaller screens; it does not reflow for phones. The fade transition has no user-selectable reduced-motion setting. + +## Pack state and award ordering + +`GeneratePack.OnSelect` guards against busy state and an unfinished pack. It samples exactly three records with `Rand()` for rarity and `RandBetween(1, CountRows(choices))` for the uniform within-rarity choice. Cutoffs are 0.60, 0.85 and 0.97. `colPackDraws` records explicit slot numbers; the UI sorts by slot rather than relying on ForAll execution order. + +Before mutating ownership, `colPack` snapshots each slot’s `IsNew`: its ID must be absent from the prior binder and all lower-numbered slots. One bulk `Collect` adds the distinct missing IDs. One `SaveData` call follows immediately, before any reveal. Reveal buttons only patch `Revealed`; they never mutate ownership or save again. Retry saving also does not award. This avoids ordering-sensitive collection mutation inside ForAll. + +A busy flag plus unfinished-pack guard blocks repeat opening. Each reveal is idempotent. No counts, currency, purchase data, dates or limits are stored. Reset clears ownership and both pack collections, then saves the empty ownership table. The unchanged `MoonfieldOwnedV1` key and Id-only schema preserve existing data when updating the same app. + +Pack reveal state is intentionally session-only. Navigating away and back retains it; a full reload starts with no active pack but loads all successfully saved awards. SaveData is device-local, not a server transaction: a storage failure leaves in-memory awards with a conspicuous warning and retry action. Concurrent app tabs are not coordinated; use one session to avoid last-writer-wins saves. Randomness is for a free fictional demonstration, not a certified or auditable reward service. + +## Supported editing route and limits + +The deliverable app was opened, corrected, saved, published privately and downloaded through actual Power Apps Studio. Use the final `.msapp` as the import/edit entry point. PAC 2.12.2 extracted `sourcecode/studio` with `canvas unpack --layout SourceCode` after the final Studio save. Microsoft describes generated YAML as review/minor-edit Preview source, and PAC canvas pack/unpack as deprecated. Do not promise arbitrary YAML round-tripping as a stable production build interface. Prefer Studio and supported Power Platform Git integration where available. + +A local scaffold was initially packed with the installed PAC and then validated and normalized by Studio. Studio caught an unsupported Classic/Button property; this was fixed before the delivered export. Studio also exposed the percentage formatting and decorative frame display-mode issues, both corrected and verified in the real player. The delivered source is the final Studio export, not the initial scaffold. + +Optional extraction command: + +```sh +pac canvas unpack --msapp solution/MoonfieldArchive.msapp --sources extracted --layout SourceCode +``` + +Original asset regeneration (does not overwrite Studio source): + +```sh +python3 sourcecode/generate.py +``` + +## References checked 2026-10-12 + +- [Canvas app YAML source](https://learn.microsoft.com/en-us/power-apps/maker/canvas-apps/power-apps-yaml) +- [PAC canvas commands](https://learn.microsoft.com/en-us/power-platform/developer/cli/reference/canvas) +- [SaveData and LoadData](https://learn.microsoft.com/en-us/power-platform/power-fx/reference/function-savedata-loaddata) +- [PnP contribution guide](https://github.com/pnp/powerplatform-samples/blob/main/CONTRIBUTING.md) + +Contribution template, metadata schema and PR template were rechecked against current upstream main on 2026-10-18 (unchanged commit). Related `dynamic-tag-gallery` and `responsive-canvas-template` samples were reviewed at repository commit `46cdaee574ba1a08c1eb4613709d75e544b2bb90`. No external dependencies were installed. + +The one remaining App Checker advisory is `app-CollectingReadOnlyTable` at App.OnStart. The static catalogue is intentionally initialized once as a twelve-row collection so this small sample stays easy to inspect and extend in Studio. It has no network fetch, delegation, repeated reload or large-data cost. A named formula is a reasonable future refactor but not required for this sample. Empty-state visibility now evaluates the same source filter as the gallery, rather than counting virtualized Gallery.AllItems; it is hidden during reset confirmation. diff --git a/samples/collectible-card-binder/docs/local-tests.txt b/samples/collectible-card-binder/docs/local-tests.txt new file mode 100644 index 00000000..3d85bd1c --- /dev/null +++ b/samples/collectible-card-binder/docs/local-tests.txt @@ -0,0 +1,27 @@ +test_catalogue_matches_embedded_powerfx (test_catalog.CatalogTests) ... ok +test_combined_filter (test_catalog.CatalogTests) ... ok +test_empty_collection (test_catalog.CatalogTests) ... ok +test_full_collection (test_catalog.CatalogTests) ... ok +test_no_match (test_catalog.CatalogTests) ... ok +test_original_assets_valid_and_embedded (test_catalog.CatalogTests) ... ok +test_owned_ids_well_under_storage_limit (test_catalog.CatalogTests) ... ok +test_rarity_distribution (test_catalog.CatalogTests) ... ok +test_source_has_error_handling_and_no_connectors (test_catalog.CatalogTests) ... ok +test_toggle_and_reset_reference (test_catalog.CatalogTests) ... ok +test_twelve_unique_stable_ids (test_catalog.CatalogTests) ... ok +test_all_threshold_boundaries (test_packs.PackTests) ... ok +test_every_uniform_pool_index_is_reachable (test_packs.PackTests) ... ok +test_interrupted_reveal_does_not_change_awards (test_packs.PackTests) ... ok +test_invalid_rolls (test_packs.PackTests) ... ok +test_previously_owned_duplicate (test_packs.PackTests) ... ok +test_probabilities_sum_to_one (test_packs.PackTests) ... ok +test_repeated_award_is_idempotent (test_packs.PackTests) ... ok +test_source_guard_and_single_save (test_packs.PackTests) ... ok +test_within_pack_duplicate (test_packs.PackTests) ... ok +test_production_rng_and_award_location (test_packs.ProductionSourceTests) ... ok +test_storage_key_compatibility (test_packs.ProductionSourceTests) ... ok + +---------------------------------------------------------------------- +Ran 22 tests in 0.005s + +OK diff --git a/samples/collectible-card-binder/docs/solution-validation.txt b/samples/collectible-card-binder/docs/solution-validation.txt new file mode 100644 index 00000000..5a8aab70 --- /dev/null +++ b/samples/collectible-card-binder/docs/solution-validation.txt @@ -0,0 +1,12 @@ +2026-10-12 — PAC 2.12.2+ga6b1ffa +Final unmanaged solution export: MoonfieldArchive 1.0.0.3 +solution unpack: exit 0 +solution pack: exit 0 +Original and repacked archives: 6 members, identical member names, CRC integrity PASS +Embedded app bytes: identical between exported and repacked solution +All XML entries: normalized XML tree comparison PASS +Embedded app Src/*.pa.yaml: exact byte match with final Studio-downloaded standalone app (5 entries) +Both apps: ParserErrorCount 0, BindingErrorCount 0 +Both app checkers: 0 accessibility warnings; 1 app-CollectingReadOnlyTable advisory +Known tenant/account/environment/app identifier scan of public sample (including nested archives): PASS +Import: NOT RUN — no approved separate target; no existing app overwritten through import diff --git a/samples/collectible-card-binder/docs/validation-manual-baseline.md b/samples/collectible-card-binder/docs/validation-manual-baseline.md new file mode 100644 index 00000000..bc11d825 --- /dev/null +++ b/samples/collectible-card-binder/docs/validation-manual-baseline.md @@ -0,0 +1,59 @@ +> Historical 1.0.0.2 manual-binder evidence only. Current pack-version results and files are described in validation.md. Manual add/remove controls no longer exist. + +# Validation — 2026-10-12 + +## Local reference tests + +Command: `python3 -m unittest discover -s tests -v`. Eleven tests pass; exact output is in `local-tests.txt`. They check catalogue IDs, rarity distribution, SVG validity and embedding, storage size, empty/full reference states, combined filters, no-match handling and source error handling. An exact catalogue/source parity assertion prevents JSON and embedded Power Fx records from drifting. The Python filter model does not execute Power Fx. The same full-completion scenario was also exercised independently in the real player below. + +## Actual Studio and player tests + +Tested in Microsoft Edge on macOS, in the owner's existing private developer environment. No new CLI sign-in or software installation was used. + +| Test | Actual observation | +| --- | --- | +| Import candidate `.msapp` into Studio | Passed after correcting two unsupported Classic/Button AccessibleLabel properties | +| Final Studio export integrity | ParserErrorCount 0; BindingErrorCount 0 | +| Catalogue | Twelve original cards loaded, with labels and artwork | +| Own first card | 1/12 and 8% displayed | +| Reload published player | First card remained owned; device storage confirmed | +| Rare filter | Velvet Comet, Aurora Stag and Tidekeeper returned | +| Own second card | 2/12 and 17% displayed | +| Combined Owned + Rare + ` emberwood ` | Only Aurora Stag returned | +| No matching search | Explicit empty state appeared | +| Details | Aurora Stag and Lantern Fox names, artwork and lore matched catalogue | +| Reset cancel | Tab/Return activated Keep collection; 2/12 remained | +| Reset confirm | 0/12; Owned filter showed empty state | +| Reload after reset | 0/12 persisted; default filters restored | +| Storage-unavailable fallback in Studio Preview | Warning appeared; session collection still worked | +| Detail ownership add/remove | Ownership text changed in both directions; return to binder showed 0/12 | +| Missing + Legendary | Exactly Sunseed Phoenix and The Last Observatory returned | +| Own every missing card | All twelve transitions passed; 12/12 and 100%, Missing empty state | +| Reload full collection | 12/12 and 100% persisted; reset afterward | +| Final frame refinement | Published player showed common, uncommon and rare border colors | + +Static screenshots were captured from the actual published Power Apps player after the visual frame refinement (the later accessibility edits preserve this appearance). Only browser/tenant header pixels were cropped; no generated screen mockups or retouching were used. + +## App Checker warnings + +Final Studio export: ParserErrorCount **0**, BindingErrorCount **0**, accessibility warnings **0**, performance advisories **1**. The original 40 warnings were reviewed by category: + +| Category | Before | Final | Resolution | +| --- | ---: | ---: | --- | +| acc-TabIndexShouldBeDefinedForInteractiveControl | 20 | 0 | Empty OnSelect actions on decorative labels, artwork and shapes. CardFrame is disabled and uses its default TabIndex; it cannot receive keyboard focus while disabled. | +| acc-FocusBorderShouldBeVisible | 15 | 0 | Removed misleading actions from static labels; actionable inputs also received explicit gold focus borders. | +| acc-AccessibleLabelNeeded | 3 | 0 | Progress shapes and reset shade are noninteractive decorations; the live completion text conveys progress. | +| app-CountRowsGalleryAllItems | 1 | 0 | Empty state uses the same underlying Filter as the gallery, with IsEmpty, and is hidden during confirmation. | +| app-CollectingReadOnlyTable | 1 | 1 | Intentional one-time twelve-row catalogue collection; no network or delegation cost. Retained as a documented small-sample tradeoff. | + +The SARIF from the final Studio export is included as `app-checker.sarif`. Screen-reader conformance is not claimed. After the edits, real-player regression checks confirmed card rendering, own/remove from details, correct lore, empty state, reset overlay hiding the empty state, and Tab/Return cancellation. The collection was left reset to 0/12. Earlier full-collection/persistence tests remain documented above. + +## Import/export boundaries + +The candidate app was actually imported, opened, saved and run in Studio. The final file is Studio's downloaded `.msapp`. The unmanaged solution ZIP was exported successfully after the author's own Studio lock expired normally; no force unlock was used. Installed PAC 2.12.2 unpacked and repacked the solution successfully. Archive entries and embedded app bytes were checked. A separate suitable target was not approved, so no final solution import or clean-environment import is claimed. No existing app was overwritten through import. + +Not tested: mobile player, Teams, Safari/Firefox, cross-user isolation, screen-reader traversal, production deployment. No automated Test Studio test suite is claimed. + +## Packaging checks + +All delivered ZIP-format files passed archive integrity checks. A scan of their expanded entries found none of the known author account, tenant, environment or app identifiers. Metadata required fields, top-level patterns, date formats and author syntax passed standard-library checks; full JSON Schema validation was not run because the validator was unavailable. The author placeholder remains deliberately unresolved. Exported YAML comment-line trailing whitespace was normalized without changing formulas. diff --git a/samples/collectible-card-binder/docs/validation.md b/samples/collectible-card-binder/docs/validation.md new file mode 100644 index 00000000..1ce7d9ef --- /dev/null +++ b/samples/collectible-card-binder/docs/validation.md @@ -0,0 +1,65 @@ +# Pack update validation — 2026-10-12 + +## Local reference checks + +Run `python3 -m unittest discover -s tests -v` from the sample directory. Exact output is in `local-tests.txt`. All 22 checks passed. These checks model catalogue/filter/storage and pack behavior in Python and inspect exported source; they do not execute Power Fx. Boundary fixtures cover 0, 0.599999, 0.60, 0.849999, 0.85, 0.969999, 0.97 and 0.999999. They also check uniform pool reachability, duplicates, repeated awards and preservation of awarded IDs across a simulated interrupted reveal. + +## Real Power Apps tests + +Executed in Studio Preview and the published private player in Microsoft Edge on macOS. The same existing app/environment was updated. The final published production formula uses Rand/RandBetween; deterministic fixture substitutions were restored before final save, publish and export. + +| Test | Actual observation | +| --- | --- | +| Studio natural random pack | Coral Compass, Lantern Fox, Lantern Fox; New, New, Already owned; 2/12 | +| Studio lower boundary fixture: 0, .60, .85 | Common MF-001, Uncommon MF-005, Rare MF-008; all New; 3/12 | +| Studio upper boundary fixture: .599999, .849999, .969999 | Same three rarities/cards; all Already owned; still 3/12 | +| Studio Legendary fixture: .97, .999999, .85 | MF-011 New, MF-011 Already owned, MF-008 Already owned; 4/12 | +| Studio repeated open/reveal clicks | One active pack; next-pack button disabled until all reveals; repeated reveal counted once | +| Studio storage failure | Explicit NOT SAVED / Session only warning and Retry saving action appeared; session remained usable | +| Published final bulk-award formula, double-click Open | One pack, 0/3 revealed, 3/12 collected; next pack disabled; Saved on this device before reveal | +| Navigate away before reveal | Binder Owned filter contained exactly Moss Courier, Aurora Stag, Tidekeeper; return retained 0/3 reveal state | +| Double-click reveal then reload | Only 1/3 revealed; reload retained the exact same three owned IDs and 25% completion | +| Reopen pack screen after reload | Fresh Open free pack state; no extra award or restoration of transient reveal state | +| Next published random pack | Coral Compass New, Lantern Fox New, Moss Courier Already owned; 5/12; save confirmed | +| Acquisition navigation | Binder Open packs and detail Open free packs reach pack screen; no manual add/remove actions | +| Reset cancel | Keep collection retained 5/12 | +| Reset confirm | 0/12; active pack cleared; pack screen offered Open free pack | +| Reload after reset | 0/12 persisted; Saved on this device; collection left empty | + +The deterministic Studio tests preceded a refactor from per-ID mutation to a single distinct-ID bulk award. The final bulk formula was separately exercised in the published-player rows. Rarity selection and New-status snapshot formulas were unchanged. No temporary deterministic rolls remain in the delivered source. + +Original manual-binder/filter/full-completion test evidence is retained separately in `validation-manual-baseline.md`; its manual acquisition actions describe the historical baseline, not the current UI. Full completion was not repeated through random packs. + +## Export and checker + +Final Studio-downloaded app: ParserErrorCount 0, BindingErrorCount 0; zero accessibility warnings; one intentional `app-CollectingReadOnlyTable` advisory for the twelve-row static catalogue. The saved export SARIF is `app-checker.sarif`. Pack gallery has an explicit keyboard tab stop and visible focus border; decorative elements have no actions. The earlier ForAll mutation advisory is absent after bulk-award refactoring. Full screen-reader conformance is not claimed. + +`binder.jpeg`, `packs.jpeg` and `details.jpeg` are real final published-player screenshots. Only the browser/tenant header and outer margins were cropped; no mockups or retouching. + +The updated app was edited, saved and privately published in place. The original candidate `.msapp` import is historical. On 2026-10-18, the unchanged final solution was imported into a newly created, approved Developer environment in a separate tenant. Microsoft solution history recorded Import / New / Success for version 1.0.0.3 and Overwrite customizations: No. No existing app was overwritten. `solution-validation.txt` records export/unpack/repack and parity results; the new import and player checks are below. + +## Remaining limits + +Clean-target solution import passed. Public release and the proposed author credit are subject to submitter approval. No mobile, Teams, other-browser, concurrent-tab, full screen-reader or production test is claimed. Save-failure UI was exercised in Preview; recovery after an actual browser storage outage was not simulated. No automated Test Studio suite is claimed. The client RNG is not certified; local reference tests are not empirical proof of long-run odds. + +## Clean-target import and runtime — 2026-10-18 UTC + +The approved temporary target reached Ready with Dataverse enabled. The maker portal imported `MoonfieldArchive_1_0_0_3.zip` without connection setup, role changes, or additional credentials. The imported app opened directly through Apps > Play. The test used the exported production formulas, with no fixture substitutions or app edits. + +Package SHA256: `185d4201037c378825ad89668c85854731b3d0eceb250e9d30b65e10e5b0848e`. + +| Check | Actual result | +| --- | --- | +| Solution import | Version 1.0.0.3, Import / New / Success; no error code; Overwrite customizations No | +| Initial observation | 2/12 already saved (Pebble Dreamer and Coral Compass). Origin of that local state was not established; pristine first-launch empty state is not claimed. Approved reset established the test baseline. | +| Reset cancellation and baseline | Keep collection retained 2/12; confirmed reset showed 0/12 | +| Double-click pack opening | One active pack, 0/3 revealed, 2/12 collected, next-pack disabled, Saved on this device | +| Double-click first reveal | Only 1/3 revealed; Lantern Fox New; completion stayed 2/12 | +| Mid-reveal navigation | Owned filter showed exactly Lantern Fox and Pebble Dreamer; returning retained 1/3 reveal state | +| Second reveal and reload | Pebble Dreamer New; reload at 2/3 retained both owned cards and 17% completion; transient pack cleared | +| Second natural pack | Rainbell Finch New, Rainbell Finch Already owned, Lantern Fox Already owned; 3/12 throughout reveals; next-pack enabled at 3/3 | +| Details/navigation | Lantern Fox illustration, Common/Emberwood and lore displayed; Open free packs returned to existing completed pack | +| Combined filters | Owned + Lantern search returned one card; adding Rare showed explicit empty-state text; Missing + Rare with search cleared returned Velvet Comet, Aurora Stag, Tidekeeper | +| Final reset/reload | Confirmed reset and reload retained 0/12 and Saved on this device; pack screen returned to Open free pack with no items | + +A real full-page screenshot of the imported second pack was captured and visually inspected; private tenant branding is retained only in private evidence. The sample's existing three screenshots remain the previously captured app-only images. No new Studio editing/checker run, deterministic boundary test, full-completion run, mobile or screen-reader test was performed in this target. The original 22 local checks and earlier Studio boundary tests remain distinct evidence. diff --git a/samples/collectible-card-binder/solution/MoonfieldArchive.msapp b/samples/collectible-card-binder/solution/MoonfieldArchive.msapp new file mode 100644 index 00000000..2efbdb0e Binary files /dev/null and b/samples/collectible-card-binder/solution/MoonfieldArchive.msapp differ diff --git a/samples/collectible-card-binder/solution/MoonfieldArchive_1_0_0_3.zip b/samples/collectible-card-binder/solution/MoonfieldArchive_1_0_0_3.zip new file mode 100644 index 00000000..6e77631e Binary files /dev/null and b/samples/collectible-card-binder/solution/MoonfieldArchive_1_0_0_3.zip differ diff --git a/samples/collectible-card-binder/sourcecode/cards.json b/samples/collectible-card-binder/sourcecode/cards.json new file mode 100644 index 00000000..67b507ef --- /dev/null +++ b/samples/collectible-card-binder/sourcecode/cards.json @@ -0,0 +1,122 @@ +[ + { + "Id": "MF-001", + "Number": 1, + "Name": "Lantern Fox", + "Rarity": "Common", + "Realm": "Emberwood", + "Lore": "A quiet guide whose tail keeps the last light of dusk.", + "Accent": "#E6A86B", + "Art": "" + }, + { + "Id": "MF-002", + "Number": 2, + "Name": "Moss Courier", + "Rarity": "Common", + "Realm": "Emberwood", + "Lore": "Carries tiny letters between the roots of ancient trees.", + "Accent": "#8DC99A", + "Art": "" + }, + { + "Id": "MF-003", + "Number": 3, + "Name": "Rainbell Finch", + "Rarity": "Common", + "Realm": "Tideglass", + "Lore": "Its song rings only when the first raindrop falls.", + "Accent": "#87CBD5", + "Art": "" + }, + { + "Id": "MF-004", + "Number": 4, + "Name": "Pebble Dreamer", + "Rarity": "Common", + "Realm": "Tideglass", + "Lore": "Sleeps beneath the river and remembers every passing star.", + "Accent": "#A6BBD6", + "Art": "" + }, + { + "Id": "MF-005", + "Number": 5, + "Name": "Cinder Moth", + "Rarity": "Uncommon", + "Realm": "Emberwood", + "Lore": "Follows the warmth of stories told around a winter fire.", + "Accent": "#F2B078", + "Art": "" + }, + { + "Id": "MF-006", + "Number": 6, + "Name": "Cloud Orchard", + "Rarity": "Uncommon", + "Realm": "Skyreach", + "Lore": "A floating grove where silver fruit ripens in moonlight.", + "Accent": "#BAC7E8", + "Art": "" + }, + { + "Id": "MF-007", + "Number": 7, + "Name": "Coral Compass", + "Rarity": "Uncommon", + "Realm": "Tideglass", + "Lore": "Always points toward a place you have never imagined.", + "Accent": "#EFADAB", + "Art": "" + }, + { + "Id": "MF-008", + "Number": 8, + "Name": "Velvet Comet", + "Rarity": "Rare", + "Realm": "Skyreach", + "Lore": "Crosses the sky so softly that even the owls keep sleeping.", + "Accent": "#BDACEA", + "Art": "" + }, + { + "Id": "MF-009", + "Number": 9, + "Name": "Aurora Stag", + "Rarity": "Rare", + "Realm": "Emberwood", + "Lore": "Its antlers gather ribbons of color from the northern sky.", + "Accent": "#90D5C8", + "Art": "" + }, + { + "Id": "MF-010", + "Number": 10, + "Name": "Tidekeeper", + "Rarity": "Rare", + "Realm": "Tideglass", + "Lore": "Counts the waves so that no shore is ever forgotten.", + "Accent": "#89BEEB", + "Art": "" + }, + { + "Id": "MF-011", + "Number": 11, + "Name": "Sunseed Phoenix", + "Rarity": "Legendary", + "Realm": "Skyreach", + "Lore": "Plants a new dawn wherever its golden feathers land.", + "Accent": "#F3CD79", + "Art": "" + }, + { + "Id": "MF-012", + "Number": 12, + "Name": "The Last Observatory", + "Rarity": "Legendary", + "Realm": "Skyreach", + "Lore": "A little tower watching over every unfinished wish.", + "Accent": "#EBD995", + "Art": "" + } +] diff --git a/samples/collectible-card-binder/sourcecode/generate.py b/samples/collectible-card-binder/sourcecode/generate.py new file mode 100644 index 00000000..761201eb --- /dev/null +++ b/samples/collectible-card-binder/sourcecode/generate.py @@ -0,0 +1,40 @@ +"""Generate original Moonfield Archive sample data and SVG artwork (stdlib only).""" +from pathlib import Path +import json, math +ROOT=Path(__file__).resolve().parents[1] +CARDS=[ +('Lantern Fox','Common','Emberwood','A quiet guide whose tail keeps the last light of dusk.', 'fox', '#E6A86B'), +('Moss Courier','Common','Emberwood','Carries tiny letters between the roots of ancient trees.', 'moth','#8DC99A'), +('Rainbell Finch','Common','Tideglass','Its song rings only when the first raindrop falls.', 'bird','#87CBD5'), +('Pebble Dreamer','Common','Tideglass','Sleeps beneath the river and remembers every passing star.', 'orb','#A6BBD6'), +('Cinder Moth','Uncommon','Emberwood','Follows the warmth of stories told around a winter fire.', 'moth','#F2B078'), +('Cloud Orchard','Uncommon','Skyreach','A floating grove where silver fruit ripens in moonlight.', 'tree','#BAC7E8'), +('Coral Compass','Uncommon','Tideglass','Always points toward a place you have never imagined.', 'star','#EFADAB'), +('Velvet Comet','Rare','Skyreach','Crosses the sky so softly that even the owls keep sleeping.', 'comet','#BDACEA'), +('Aurora Stag','Rare','Emberwood','Its antlers gather ribbons of color from the northern sky.', 'stag','#90D5C8'), +('Tidekeeper','Rare','Tideglass','Counts the waves so that no shore is ever forgotten.', 'orb','#89BEEB'), +('Sunseed Phoenix','Legendary','Skyreach','Plants a new dawn wherever its golden feathers land.', 'bird','#F3CD79'), +('The Last Observatory','Legendary','Skyreach','A little tower watching over every unfinished wish.', 'tower','#EBD995')] + +def artwork(shape, color, index): + motifs={ + 'fox':'', + 'moth':'', + 'bird':'', + 'orb':'', + 'tree':'', + 'star':'', + 'comet':'', + 'stag':'', + 'tower':''} + stars=''.join(f'' for i in range(16)) + return f'{stars}{motifs[shape]}' + +cards=[] +for i,(name,rarity,realm,lore,shape,color) in enumerate(CARDS,1): + id=f'MF-{i:03}' + svg=artwork(shape,color,i) + (ROOT/'assets/cards'/f'{id}.svg').write_text(svg) + cards.append(dict(Id=id,Number=i,Name=name,Rarity=rarity,Realm=realm,Lore=lore,Accent=color,Art=svg)) +(ROOT/'sourcecode/cards.json').write_text(json.dumps(cards,indent=2)+'\n') +print(f'Generated {len(cards)} original cards and SVG assets.') diff --git a/samples/collectible-card-binder/sourcecode/solution/CanvasApps/mfarc_moonfieldarchive_2732b.meta.xml b/samples/collectible-card-binder/sourcecode/solution/CanvasApps/mfarc_moonfieldarchive_2732b.meta.xml new file mode 100644 index 00000000..0049aa4a --- /dev/null +++ b/samples/collectible-card-binder/sourcecode/solution/CanvasApps/mfarc_moonfieldarchive_2732b.meta.xml @@ -0,0 +1,34 @@ + + + mfarc_moonfieldarchive_2732b + 2026-10-12T22:04:50Z + Ready + 3.26094.9.0 + 3.26094.9.0 + {"primaryDeviceWidth":"1366","primaryDeviceHeight":"768","supportsPortrait":"true","supportsLandscape":"true","primaryFormFactor":"Tablet","showStatusBar":"false","publisherVersion":"3.26094.9","minimumRequiredApiVersion":"2.2.0","hasComponent":"false","hasUnlockedComponent":"false","isUnifiedRootApp":"false","m365CopilotSettingValue":"0","sienaVersion":"20261012T202103Z-3.26094.9.0"} + 0 + + RGBA(0,176,240,1) + Moonfield Archive + + + + [] + {} + {} + [] + [] + 1 + 0 + 0 + 0 + + 1.0 + {"cdsdependencies":[]} + 1 + /CanvasApps/mfarc_moonfieldarchive_2732b_BackgroundImageUri + /CanvasApps/mfarc_moonfieldarchive_2732b_DocumentUri.msapp + + /CanvasApps/mfarc_moonfieldarchive_2732b_AdditionalUris0_identity.json + + \ No newline at end of file diff --git a/samples/collectible-card-binder/sourcecode/solution/CanvasApps/mfarc_moonfieldarchive_2732b_AdditionalUris0_identity.json b/samples/collectible-card-binder/sourcecode/solution/CanvasApps/mfarc_moonfieldarchive_2732b_AdditionalUris0_identity.json new file mode 100644 index 00000000..959b6290 --- /dev/null +++ b/samples/collectible-card-binder/sourcecode/solution/CanvasApps/mfarc_moonfieldarchive_2732b_AdditionalUris0_identity.json @@ -0,0 +1 @@ +{"__Version":"0.1","App":"243d0318-87ef-42aa-a1a3-734a18446eb2","Test_7F478737223C4B69":"432f9603-5fa9-41fe-8a25-476e787e29f5","Host":"4170679d-28ea-4d63-b474-8766aa16c722","BinderScreen":"50475eba-2a42-43d4-8243-07aae609e2ee","ResetShade":"3c930666-560f-48cf-91a6-c5f9fa4be2e8","Eyebrow":"ca749a38-7d55-4742-80d1-db3f80f7202c","Title":"9d98f179-0124-46c7-9b05-643e7774ae01","Completion":"93894394-3809-4b14-82b5-40804e454a6c","ProgressTrack":"9a739926-98f0-4a7f-bb31-b8a67cdc7298","ProgressFill":"48f1c102-dfb0-4491-a0b1-657799250012","Subtitle":"2c998464-00f5-4bf8-8cfb-4fc8229cae07","AllFilter":"219b3dba-2e01-427c-bf5b-49e6d6d2f008","OwnedFilter":"0c2e9a9c-57a9-40b2-8c93-dff1a73bfb5d","MissingFilter":"1af10e56-8e8f-4326-a519-9494c9d93912","RarityFilter":"1e1276f6-28d4-4ce3-a3b4-eabac2a8d3aa","SearchCards":"df179fe1-5b59-4d0b-a56d-2b8693fdeda4","BinderOpenPacks":"71c37a91-1283-4335-b40e-3b0cbbbd2972","ResetCollection":"2505ea23-7cc6-4a6f-879e-a0743f54ba11","CardGallery":"d14cadfb-26f5-48d8-ac68-8cb5e5d78be5","6d24abed-909a-4a02-8011-a683cd41b38f":"df82f7f2-8f42-418a-a5f6-f1b11d82014b","CardFrame":"68a991fd-2368-452a-85f8-dda71989dfab","CardArt":"a8ce90a6-f5b7-46b3-b385-1074e712da50","CardNumber":"38643939-c0e6-4f06-b4bb-503665f79c7e","CardName":"693489cb-43d8-41f7-87e8-4cb389fca856","CardOpen":"382db663-1365-4b42-9228-b784fcdcf721","CardOwned":"e5c55c10-a4ff-4eea-8341-2772fb58a5cb","ResetTitle":"5c76cded-baad-4a31-9722-c6659b39b97b","ResetBody":"89da18d0-7c32-4129-8fec-3fea4d1db0f2","EmptyState":"a7b72395-9c0f-4c29-9fff-d845b7aeb166","CancelReset":"86f70892-1fa4-4481-b8a0-90da81ff005f","ConfirmReset":"d9b9a775-5cf0-4d84-97d3-c2ef13c4dfb2","StorageNote":"f9c097cf-52d2-4e91-aac0-d4004f8b525c","DetailScreen":"7652aaf7-1da3-469f-994d-6c8be089dd7c","BackButton":"d1e06ba2-c843-450a-8cc7-4c052dcfd354","DetailArt":"35c152a5-7d17-4710-838d-e227a7b30f04","DetailNumber":"22c22a75-54f2-4b67-b6dc-521502fea962","DetailName":"609bda53-b8d9-4c81-93d8-2466d93c4c72","DetailRarity":"fdfb3f3d-26ff-491a-b0ae-2d4e36a053dd","DetailLore":"df35285f-8ce7-4a29-9e69-b41dab74e85c","DetailOwned":"010e0369-60d8-460b-9ca3-374d824b19d5","DetailStatus":"2f2f9b9c-a9c6-4cca-8d06-24788305f4ea","PackScreen":"125c1428-f199-461d-8715-e74f26cfe799","PackTitle":"0cb6cd88-4d5d-409c-99f1-a3fc942b3ccf","PackBack":"09f20538-91f2-4c97-9361-59c62006664e","PackKicker":"eeb8a97a-be00-41fc-9995-b40d7465ebc8","PackOdds":"0ce030cd-15ef-4597-8278-c221840afdfa","GeneratePack":"fef6cf2b-51d0-420e-bad6-69b5d77bbec7","PackSummary":"39315864-280a-4464-a79a-0cf8b5ca603b","PackGallery":"ce98b38b-4939-4339-a5b8-7cbb8860044a","bf1283a3-8968-4da3-97c6-e03942b97184":"f2b03ef2-42c9-4919-86f1-561e808b63a7","PackFrame":"bfc9bb23-4f6c-40e2-b099-a75f5f822dc4","RevealCard":"0c262878-3ea9-46bc-8ac5-68f27d7f50a4","PackArt":"b9eaa000-06cc-481e-b0c0-a750cfa18a15","PackRarity":"9c448d56-1454-4e9e-8e6f-c604974c80c5","PackName":"1cdc8a1b-c458-4c43-8d07-dc8bd7e5824a","PackOutcome":"c9e9f7a1-700d-490f-b0b1-2cc429bf320c","PackDetails":"2e8b10fd-f19d-4b60-bf9c-e0c5e1ccfa74","PackRules":"495edfbd-abf8-45c4-9b3f-011c86f66398","PackSaveStatus":"d6e0e39d-6778-4da3-b88c-a07863a4836a","RetryPackSave":"4f50e8b1-383b-4935-bb24-55701ef9d661"} \ No newline at end of file diff --git a/samples/collectible-card-binder/sourcecode/solution/CanvasApps/mfarc_moonfieldarchive_2732b_BackgroundImageUri b/samples/collectible-card-binder/sourcecode/solution/CanvasApps/mfarc_moonfieldarchive_2732b_BackgroundImageUri new file mode 100644 index 00000000..c67f0065 Binary files /dev/null and b/samples/collectible-card-binder/sourcecode/solution/CanvasApps/mfarc_moonfieldarchive_2732b_BackgroundImageUri differ diff --git a/samples/collectible-card-binder/sourcecode/solution/CanvasApps/mfarc_moonfieldarchive_2732b_DocumentUri.msapp b/samples/collectible-card-binder/sourcecode/solution/CanvasApps/mfarc_moonfieldarchive_2732b_DocumentUri.msapp new file mode 100644 index 00000000..bd08b8f0 Binary files /dev/null and b/samples/collectible-card-binder/sourcecode/solution/CanvasApps/mfarc_moonfieldarchive_2732b_DocumentUri.msapp differ diff --git a/samples/collectible-card-binder/sourcecode/solution/Other/Customizations.xml b/samples/collectible-card-binder/sourcecode/solution/Other/Customizations.xml new file mode 100644 index 00000000..2caf69b0 --- /dev/null +++ b/samples/collectible-card-binder/sourcecode/solution/Other/Customizations.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + 1033 + + \ No newline at end of file diff --git a/samples/collectible-card-binder/sourcecode/solution/Other/Solution.xml b/samples/collectible-card-binder/sourcecode/solution/Other/Solution.xml new file mode 100644 index 00000000..3b21f428 --- /dev/null +++ b/samples/collectible-card-binder/sourcecode/solution/Other/Solution.xml @@ -0,0 +1,85 @@ + + + + MoonfieldArchive + + + + + 1.0.0.3 + 0 + + MoonfieldSamples + + + + + + + mfarc + 11561 + +
+ 1 + 1 + + + + + + + + + + + + + + + + 1 + + + + + + + + +
+
+ 2 + 1 + + + + + + + + + + + + + + + + 1 + + + + + + + + +
+
+
+ + + + +
+
\ No newline at end of file diff --git a/samples/collectible-card-binder/sourcecode/studio/MoonfieldArchive.msapr b/samples/collectible-card-binder/sourcecode/studio/MoonfieldArchive.msapr new file mode 100644 index 00000000..e7aa91a6 Binary files /dev/null and b/samples/collectible-card-binder/sourcecode/studio/MoonfieldArchive.msapr differ diff --git a/samples/collectible-card-binder/sourcecode/studio/Src/App.pa.yaml b/samples/collectible-card-binder/sourcecode/studio/Src/App.pa.yaml new file mode 100644 index 00000000..5da23fea --- /dev/null +++ b/samples/collectible-card-binder/sourcecode/studio/Src/App.pa.yaml @@ -0,0 +1,14 @@ +# ************************************************************************************************ +# Warning: YAML source code for Canvas Apps should only be used to review changes made within Power Apps Studio and for minor edits (Preview). +# Use the maker portal to create and edit your Power Apps. +# +# The schema file for Canvas Apps is available at https://go.microsoft.com/fwlink/?linkid=2304907 +# +# For more information, visit https://go.microsoft.com/fwlink/?linkid=2292623 +# ************************************************************************************************ +App: + Properties: + OnStart: |- + =ClearCollect(colCards, {Id: "MF-001", Number: 1, Name: "Lantern Fox", Rarity: "Common", Realm: "Emberwood", Lore: "A quiet guide whose tail keeps the last light of dusk.", Accent: "#E6A86B", Art: ""}, {Id: "MF-002", Number: 2, Name: "Moss Courier", Rarity: "Common", Realm: "Emberwood", Lore: "Carries tiny letters between the roots of ancient trees.", Accent: "#8DC99A", Art: ""}, {Id: "MF-003", Number: 3, Name: "Rainbell Finch", Rarity: "Common", Realm: "Tideglass", Lore: "Its song rings only when the first raindrop falls.", Accent: "#87CBD5", Art: ""}, {Id: "MF-004", Number: 4, Name: "Pebble Dreamer", Rarity: "Common", Realm: "Tideglass", Lore: "Sleeps beneath the river and remembers every passing star.", Accent: "#A6BBD6", Art: ""}, {Id: "MF-005", Number: 5, Name: "Cinder Moth", Rarity: "Uncommon", Realm: "Emberwood", Lore: "Follows the warmth of stories told around a winter fire.", Accent: "#F2B078", Art: ""}, {Id: "MF-006", Number: 6, Name: "Cloud Orchard", Rarity: "Uncommon", Realm: "Skyreach", Lore: "A floating grove where silver fruit ripens in moonlight.", Accent: "#BAC7E8", Art: ""}, {Id: "MF-007", Number: 7, Name: "Coral Compass", Rarity: "Uncommon", Realm: "Tideglass", Lore: "Always points toward a place you have never imagined.", Accent: "#EFADAB", Art: ""}, {Id: "MF-008", Number: 8, Name: "Velvet Comet", Rarity: "Rare", Realm: "Skyreach", Lore: "Crosses the sky so softly that even the owls keep sleeping.", Accent: "#BDACEA", Art: ""}, {Id: "MF-009", Number: 9, Name: "Aurora Stag", Rarity: "Rare", Realm: "Emberwood", Lore: "Its antlers gather ribbons of color from the northern sky.", Accent: "#90D5C8", Art: ""}, {Id: "MF-010", Number: 10, Name: "Tidekeeper", Rarity: "Rare", Realm: "Tideglass", Lore: "Counts the waves so that no shore is ever forgotten.", Accent: "#89BEEB", Art: ""}, {Id: "MF-011", Number: 11, Name: "Sunseed Phoenix", Rarity: "Legendary", Realm: "Skyreach", Lore: "Plants a new dawn wherever its golden feathers land.", Accent: "#F3CD79", Art: ""}, {Id: "MF-012", Number: 12, Name: "The Last Observatory", Rarity: "Legendary", Realm: "Skyreach", Lore: "A little tower watching over every unfinished wish.", Accent: "#EBD995", Art: ""}); ClearCollect(colOwned, {Id: ""}); Clear(colOwned); Set(varStorageWarning, false); IfError(LoadData(colOwned, "MoonfieldOwnedV1", true), Set(varStorageWarning, true)); RemoveIf(colOwned, !(Id in colCards.Id)); Set(varFilter, "All"); Set(varReset, false) + StartScreen: =BinderScreen + Theme: =PowerAppsTheme diff --git a/samples/collectible-card-binder/sourcecode/studio/Src/BinderScreen.pa.yaml b/samples/collectible-card-binder/sourcecode/studio/Src/BinderScreen.pa.yaml new file mode 100644 index 00000000..0b11356b --- /dev/null +++ b/samples/collectible-card-binder/sourcecode/studio/Src/BinderScreen.pa.yaml @@ -0,0 +1,450 @@ +# ************************************************************************************************ +# Warning: YAML source code for Canvas Apps should only be used to review changes made within Power Apps Studio and for minor edits (Preview). +# Use the maker portal to create and edit your Power Apps. +# +# The schema file for Canvas Apps is available at https://go.microsoft.com/fwlink/?linkid=2304907 +# +# For more information, visit https://go.microsoft.com/fwlink/?linkid=2292623 +# ************************************************************************************************ +Screens: + BinderScreen: + Properties: + Fill: =ColorValue("#101C25") + Children: + - Eyebrow: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#EACB88") + Font: =Font.Lato + Height: =28 + OnSelect: = + Size: =11 + Text: ="A COLLECTION OF SMALL WONDERS" + Width: =700 + X: =36 + Y: =22 + - Title: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#F5F1E8") + Font: =Font.Lato + FontWeight: =FontWeight.Bold + Height: =64 + OnSelect: = + Size: =34 + Text: '="Moonfield Archive" ' + Width: =780 + X: =36 + Y: =51 + - Subtitle: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#B7C8CC") + Font: =Font.Lato + Height: =32 + OnSelect: = + Size: =14 + Text: '="Volume 01 · Twelve curiosities from Emberwood, Tideglass & Skyreach" ' + Width: =960 + X: =38 + Y: =119 + - Completion: + Control: Label@2.5.1 + Properties: + Align: =Align.Right + Color: =ColorValue("#F5F1E8") + Font: =Font.Lato + Height: =42 + Live: =Live.Polite + OnSelect: = + Size: =16 + Text: '=CountRows(Distinct(colOwned, Id)) & " / 12 collected · " & Text(Round(100 * CountRows(Distinct(colOwned, Id)) / 12, 0), "0") & "%" ' + Width: =360 + X: =970 + Y: =65 + - ProgressTrack: + Control: Rectangle@2.3.0 + Properties: + Fill: '=ColorValue("#1D303C") ' + Height: =6 + OnSelect: = + Width: =356 + X: =970 + Y: =117 + - ProgressFill: + Control: Rectangle@2.3.0 + Properties: + Fill: '=ColorValue("#EACB88") ' + Height: =6 + OnSelect: = + Width: =356 * CountRows(Distinct(colOwned, Id)) / 12 + X: =970 + Y: =117 + - AllFilter: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#B7C8CC") + BorderThickness: =1 + Color: =ColorValue("#F5F1E8") + Fill: =If(varFilter = "All", ColorValue("#50635F"), ColorValue("#1D303C")) + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =44 + HoverColor: =ColorValue("#F5F1E8") + HoverFill: =ColorValue("#354D59") + OnSelect: =Set(varFilter, "All") + PressedColor: =ColorValue("#F5F1E8") + PressedFill: =ColorValue("#50635F") + Size: =12 + Text: ="All cards" + Visible: =!varReset + Width: =125 + X: =38 + Y: =173 + - OwnedFilter: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#B7C8CC") + BorderThickness: =1 + Color: =ColorValue("#F5F1E8") + Fill: =If(varFilter = "Owned", ColorValue("#50635F"), ColorValue("#1D303C")) + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =44 + HoverColor: =ColorValue("#F5F1E8") + HoverFill: =ColorValue("#354D59") + OnSelect: =Set(varFilter, "Owned") + PressedColor: =ColorValue("#F5F1E8") + PressedFill: =ColorValue("#50635F") + Size: =12 + Text: ="Owned" + Visible: =!varReset + Width: =125 + X: =173 + Y: =173 + - MissingFilter: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#B7C8CC") + BorderThickness: =1 + Color: =ColorValue("#F5F1E8") + Fill: =If(varFilter = "Missing", ColorValue("#50635F"), ColorValue("#1D303C")) + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =44 + HoverColor: =ColorValue("#F5F1E8") + HoverFill: =ColorValue("#354D59") + OnSelect: =Set(varFilter, "Missing") + PressedColor: =ColorValue("#F5F1E8") + PressedFill: =ColorValue("#50635F") + Size: =12 + Text: ="Missing" + Visible: =!varReset + Width: =125 + X: =308 + Y: =173 + - RarityFilter: + Control: Classic/DropDown@2.3.1 + Properties: + AccessibleLabel: ="Filter cards by rarity" + ChevronBackground: =ColorValue("#1D303C") + ChevronFill: =ColorValue("#F5F1E8") + Color: =ColorValue("#F5F1E8") + Default: ="All rarities" + Fill: =ColorValue("#1D303C") + FocusedBorderColor: '=ColorValue("#EACB88") ' + FocusedBorderThickness: '=3 ' + Height: =44 + Items: =["All rarities", "Common", "Uncommon", "Rare", "Legendary"] + Items.Value: =Value + Size: =12 + Visible: =!varReset + Width: =230 + X: =453 + Y: =173 + - SearchCards: + Control: Classic/TextInput@2.3.2 + Properties: + AccessibleLabel: ="Search cards by name or realm" + Color: =ColorValue("#F5F1E8") + Default: ="" + DelayOutput: =true + Fill: =ColorValue("#1D303C") + FocusedBorderColor: '=ColorValue("#EACB88") ' + FocusedBorderThickness: '=3 ' + Height: =44 + HintText: ="Search name or realm" + Size: =12 + Visible: =!varReset + Width: =362 + X: =705 + Y: =173 + - ResetCollection: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#B7C8CC") + BorderThickness: =1 + Color: =ColorValue("#F5F1E8") + Fill: =ColorValue("#1D303C") + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =44 + HoverColor: =ColorValue("#F5F1E8") + HoverFill: =ColorValue("#354D59") + OnSelect: =Set(varReset, true) + PressedColor: =ColorValue("#F5F1E8") + PressedFill: =ColorValue("#50635F") + Size: =12 + Text: ="Reset" + Visible: =!varReset + Width: =96 + X: =1230 + Y: =173 + - CardGallery: + Control: Gallery@2.15.0 + Variant: Vertical + Properties: + AccessibleLabel: ="Collectible card binder" + Height: =478 + Items: =Filter(colCards, (varFilter = "All" || (varFilter = "Owned" && Id in colOwned.Id) || (varFilter = "Missing" && !(Id in colOwned.Id))) && (RarityFilter.Selected.Value = "All rarities" || Rarity = RarityFilter.Selected.Value) && (IsBlank(Trim(SearchCards.Text)) || Lower(Trim(SearchCards.Text)) in Lower(Name & " " & Realm))) + OnSelect: =Set(varCard, ThisItem); Navigate(DetailScreen, ScreenTransition.Fade) + TabIndex: =0 + TemplatePadding: =6 + TemplateSize: =336 + Visible: =!varReset + Width: =1310 + WrapCount: =4 + X: =28 + Y: =238 + Children: + - CardFrame: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =Switch(ThisItem.Rarity, "Legendary", ColorValue("#EACB88"), "Rare", ColorValue("#BBA7EA"), "Uncommon", ColorValue("#8ECFB4"), ColorValue("#AFC3CC")) + Color: =ColorValue("#F5F1E8") + DisabledBorderColor: =Switch(ThisItem.Rarity, "Legendary", ColorValue("#EACB88"), "Rare", ColorValue("#BBA7EA"), "Uncommon", ColorValue("#8ECFB4"), ColorValue("#AFC3CC")) + DisabledFill: =ColorValue("#1D303C") + DisplayMode: '=DisplayMode.Disabled ' + Fill: =ColorValue("#1D303C") + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =318 + HoverColor: =ColorValue("#F5F1E8") + HoverFill: =ColorValue("#354D59") + OnSelect: = + PressedColor: =ColorValue("#F5F1E8") + PressedFill: =ColorValue("#50635F") + Size: =12 + Text: '="" ' + Width: =Parent.TemplateWidth - 20 + X: =8 + Y: =4 + - CardArt: + Control: Image@2.2.3 + Properties: + AccessibleLabel: =ThisItem.Name & ", original fictional illustration" + Height: =190 + Image: '="data:image/svg+xml;utf8," & EncodeUrl(ThisItem.Art) ' + OnSelect: = + Width: =Parent.TemplateWidth - 40 + X: =18 + Y: =12 + - CardNumber: + Control: Label@2.5.1 + Properties: + Color: =Switch(ThisItem.Rarity, "Legendary", ColorValue("#EACB88"), "Rare", ColorValue("#BBA7EA"), "Uncommon", ColorValue("#8ECFB4"), ColorValue("#AFC3CC")) + Font: =Font.Lato + Height: =24 + OnSelect: = + Size: =10 + Text: '=ThisItem.Id & " / " & Upper(ThisItem.Rarity) ' + Width: =Parent.TemplateWidth - 40 + X: =20 + Y: =208 + - CardName: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#F5F1E8") + Font: =Font.Lato + FontWeight: =FontWeight.Semibold + Height: =32 + OnSelect: = + Size: =16 + Text: '=ThisItem.Name ' + Width: =Parent.TemplateWidth - 40 + X: =20 + Y: =234 + - CardOpen: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#B7C8CC") + BorderThickness: =1 + Color: =ColorValue("#F5F1E8") + Fill: =ColorValue("#1D303C") + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =44 + HoverColor: =ColorValue("#F5F1E8") + HoverFill: =ColorValue("#354D59") + OnSelect: =Set(varCard, ThisItem); Navigate(DetailScreen, ScreenTransition.Fade) + PressedColor: =ColorValue("#F5F1E8") + PressedFill: =ColorValue("#50635F") + Size: =12 + Text: ="Details" + Tooltip: ="View " & ThisItem.Name & " details" + Width: =118 + X: =20 + Y: =274 + - CardOwned: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#B7C8CC") + BorderThickness: =1 + Color: =ColorValue("#F5F1E8") + DisabledColor: =ColorValue("#F5F1E8") + DisabledFill: =If(ThisItem.Id in colOwned.Id, ColorValue("#356351"), ColorValue("#1D303C")) + DisplayMode: =DisplayMode.Disabled + Fill: =If(ThisItem.Id in colOwned.Id, ColorValue("#356351"), ColorValue("#1D303C")) + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =44 + HoverColor: =ColorValue("#F5F1E8") + HoverFill: =ColorValue("#354D59") + OnSelect: = + PressedColor: =ColorValue("#F5F1E8") + PressedFill: =ColorValue("#50635F") + Size: =12 + Text: =If(ThisItem.Id in colOwned.Id, "Owned ✓", "Missing") + Tooltip: =If(ThisItem.Id in colOwned.Id, "In your collection", "Find this card in free packs") + Width: =Parent.TemplateWidth - 172 + X: =148 + Y: =274 + - EmptyState: + Control: Label@2.5.1 + Properties: + Align: =Align.Center + Color: =ColorValue("#F5F1E8") + Font: =Font.Lato + Height: =100 + OnSelect: = + Size: =20 + Text: '="No cards match. Try another rarity, ownership filter, or search." ' + Visible: '=IsEmpty(Filter(colCards, (varFilter = "All" || (varFilter = "Owned" && Id in colOwned.Id) || (varFilter = "Missing" && !(Id in colOwned.Id))) && (RarityFilter.Selected.Value = "All rarities" || Rarity = RarityFilter.Selected.Value) && (IsBlank(Trim(SearchCards.Text)) || Lower(Trim(SearchCards.Text)) in Lower(Name & " " & Realm)))) && !varReset ' + Width: =1150 + X: =100 + Y: =380 + - StorageNote: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#B7C8CC") + Font: =Font.Lato + Height: =26 + Live: =Live.Polite + OnSelect: = + Size: =11 + Text: "=If(varStorageWarning, \"Session only: local storage is unavailable. Changes may be lost when you close the app.\", \"Saved on this device · Your collection does not sync between browsers or accounts.\") " + Width: =1250 + X: =38 + Y: =728 + - ResetShade: + Control: Rectangle@2.3.0 + Properties: + Fill: '=RGBA(0,0,0,0.85) ' + Height: =768 + OnSelect: = + Visible: =varReset + Width: =1366 + - ResetTitle: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#F5F1E8") + Font: =Font.Lato + Height: =70 + OnSelect: = + Size: =28 + Text: '="Reset this device’s collection?" ' + Visible: =varReset + Width: =680 + X: =350 + Y: =255 + - ResetBody: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#F5F1E8") + Font: =Font.Lato + Height: =70 + OnSelect: = + Size: =17 + Text: ="All twelve cards will become missing and the current pack will close. Card artwork stays in the album." + Visible: =varReset + Width: =680 + X: =350 + Y: =325 + - CancelReset: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#B7C8CC") + BorderThickness: =1 + Color: =ColorValue("#F5F1E8") + Fill: =ColorValue("#1D303C") + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =44 + HoverColor: =ColorValue("#F5F1E8") + HoverFill: =ColorValue("#354D59") + OnSelect: =Set(varReset, false) + PressedColor: =ColorValue("#F5F1E8") + PressedFill: =ColorValue("#50635F") + Size: =12 + Text: ="Keep collection" + Visible: =varReset + Width: =310 + X: =350 + Y: =430 + - ConfirmReset: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#B7C8CC") + BorderThickness: =1 + Color: =ColorValue("#F5F1E8") + Fill: =ColorValue("#1D303C") + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =44 + HoverColor: =ColorValue("#F5F1E8") + HoverFill: =ColorValue("#354D59") + OnSelect: =Clear(colOwned); Clear(colPack); Clear(colPackDraws); Set(varPackBusy, false); IfError(SaveData(colOwned, "MoonfieldOwnedV1"), Set(varStorageWarning, true), Set(varStorageWarning, false)); Set(varReset, false) + PressedColor: =ColorValue("#F5F1E8") + PressedFill: =ColorValue("#50635F") + Size: =12 + Text: ="Reset to missing" + Visible: =varReset + Width: =310 + X: =685 + Y: =430 + - BinderOpenPacks: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#EACB88") + Color: =ColorValue("#F5F1E8") + Fill: =ColorValue("#1D303C") + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =44 + OnSelect: =Navigate(PackScreen, ScreenTransition.Fade) + Size: =12 + Text: ="Open packs" + Visible: =!varReset + Width: =126 + X: =1092 + Y: =173 diff --git a/samples/collectible-card-binder/sourcecode/studio/Src/DetailScreen.pa.yaml b/samples/collectible-card-binder/sourcecode/studio/Src/DetailScreen.pa.yaml new file mode 100644 index 00000000..3f2e687b --- /dev/null +++ b/samples/collectible-card-binder/sourcecode/studio/Src/DetailScreen.pa.yaml @@ -0,0 +1,127 @@ +# ************************************************************************************************ +# Warning: YAML source code for Canvas Apps should only be used to review changes made within Power Apps Studio and for minor edits (Preview). +# Use the maker portal to create and edit your Power Apps. +# +# The schema file for Canvas Apps is available at https://go.microsoft.com/fwlink/?linkid=2304907 +# +# For more information, visit https://go.microsoft.com/fwlink/?linkid=2292623 +# ************************************************************************************************ +Screens: + DetailScreen: + Properties: + Fill: =ColorValue("#101C25") + Children: + - BackButton: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#B7C8CC") + BorderThickness: =1 + Color: =ColorValue("#F5F1E8") + Fill: =ColorValue("#1D303C") + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =44 + HoverColor: =ColorValue("#F5F1E8") + HoverFill: =ColorValue("#354D59") + OnSelect: =Navigate(BinderScreen, ScreenTransition.None) + PressedColor: =ColorValue("#F5F1E8") + PressedFill: =ColorValue("#50635F") + Size: =12 + Text: ="← Back to binder" + Width: =230 + X: =38 + Y: =28 + - DetailArt: + Control: Image@2.2.3 + Properties: + AccessibleLabel: =varCard.Name & ", original fictional illustration" + Height: =540 + Image: ="data:image/svg+xml;utf8," & EncodeUrl(varCard.Art) + OnSelect: = + Width: =510 + X: =65 + Y: =128 + - DetailNumber: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#EACB88") + Font: =Font.Lato + Height: =35 + OnSelect: = + Size: =12 + Text: =varCard.Id & " / VOLUME 01" + Width: =660 + X: =640 + Y: =148 + - DetailName: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#F5F1E8") + Font: =Font.Lato + FontWeight: =FontWeight.Bold + Height: =108 + OnSelect: = + Size: =36 + Text: =varCard.Name + Width: =660 + X: =640 + Y: =199 + - DetailRarity: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#EACB88") + Font: =Font.Lato + Height: =42 + OnSelect: = + Size: =16 + Text: =Upper(varCard.Rarity) & " · " & varCard.Realm + Width: =660 + X: =640 + Y: =322 + - DetailLore: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#F5F1E8") + Font: =Font.Lato + Height: =116 + OnSelect: = + Size: =24 + Text: =varCard.Lore + Width: =620 + X: =640 + Y: =390 + - DetailOwned: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#B7C8CC") + BorderThickness: =1 + Color: =ColorValue("#F5F1E8") + Fill: =ColorValue("#1D303C") + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =54 + HoverColor: =ColorValue("#F5F1E8") + HoverFill: =ColorValue("#354D59") + OnSelect: =Navigate(PackScreen, ScreenTransition.Fade) + PressedColor: =ColorValue("#F5F1E8") + PressedFill: =ColorValue("#50635F") + Size: =12 + Text: ="Open free packs" + Width: =390 + X: =640 + Y: =556 + - DetailStatus: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#B7C8CC") + Font: =Font.Lato + Height: =62 + Live: =Live.Polite + OnSelect: = + Size: =14 + Text: =If(varCard.Id in colOwned.Id, "This wonder is part of your collection.", "A space in your binder is waiting for this wonder.") + Width: =620 + X: =640 + Y: =635 diff --git a/samples/collectible-card-binder/sourcecode/studio/Src/PackScreen.pa.yaml b/samples/collectible-card-binder/sourcecode/studio/Src/PackScreen.pa.yaml new file mode 100644 index 00000000..96657242 --- /dev/null +++ b/samples/collectible-card-binder/sourcecode/studio/Src/PackScreen.pa.yaml @@ -0,0 +1,258 @@ +# ************************************************************************************************ +# Warning: YAML source code for Canvas Apps should only be used to review changes made within Power Apps Studio and for minor edits (Preview). +# Use the maker portal to create and edit your Power Apps. +# +# The schema file for Canvas Apps is available at https://go.microsoft.com/fwlink/?linkid=2304907 +# +# For more information, visit https://go.microsoft.com/fwlink/?linkid=2292623 +# ************************************************************************************************ +Screens: + PackScreen: + Properties: + Fill: =ColorValue("#101C25") + LoadingSpinnerColor: =RGBA(56, 96, 178, 1) + Children: + - PackBack: + Control: Classic/Button@2.2.0 + Properties: + Color: =ColorValue("#F5F1E8") + Fill: =ColorValue("#1D303C") + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Height: =44 + OnSelect: =Navigate(BinderScreen, ScreenTransition.None) + Text: ="← Binder" + Width: =180 + X: =38 + Y: =24 + - PackTitle: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#F5F1E8") + Font: =Font.Lato + FontWeight: =FontWeight.Bold + Height: =58 + OnSelect: = + Size: =28 + Text: ="A little wonder, three times." + Width: =750 + X: =250 + Y: =20 + - PackKicker: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#EACB88") + Font: =Font.Lato + Height: =30 + OnSelect: = + Text: ="FREE PACKS · THREE CARDS · NO LIMITS" + Width: =1250 + X: =38 + Y: =88 + - PackOdds: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#F5F1E8") + Font: =Font.Lato + Height: =34 + OnSelect: = + Size: =14 + Text: |- + ="Per-card odds: Common 60% · Uncommon 25% · Rare 12% · Legendary 3%" + Width: =1270 + X: =38 + Y: =126 + - GeneratePack: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#EACB88") + Color: =ColorValue("#F5F1E8") + DisplayMode: =If(Coalesce(varPackBusy, false) || (!IsEmpty(colPack) && CountIf(colPack, !Revealed) > 0), DisplayMode.Disabled, DisplayMode.Edit) + Fill: =ColorValue("#1D303C") + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =44 + OnSelect: |- + =If(!Coalesce(varPackBusy, false) && (IsEmpty(colPack) || CountIf(colPack, !Revealed) = 0), Set(varPackBusy, true); ClearCollect(colPackDraws, ForAll(Sequence(3) As slot, With({rarityRoll: Rand()}, With({drawRarity: If(rarityRoll < 0.60, "Common", rarityRoll < 0.85, "Uncommon", rarityRoll < 0.97, "Rare", "Legendary")}, With({choices: Filter(colCards, Rarity = drawRarity)}, {Slot: slot.Value, Card: Index(choices, RandBetween(1, CountRows(choices)))}))))); ClearCollect(colPack, ForAll(colPackDraws As draw, {Slot: draw.Slot, Card: draw.Card, Revealed: false, IsNew: !(draw.Card.Id in colOwned.Id) && IsEmpty(Filter(colPackDraws, Slot < draw.Slot && Card.Id = draw.Card.Id))})); Collect(colOwned, RenameColumns(Filter(Distinct(colPack, Card.Id), !(Value in colOwned.Id)), Value, Id)); IfError(SaveData(colOwned, "MoonfieldOwnedV1"), Set(varStorageWarning, true), Set(varStorageWarning, false)); Set(varPackBusy, false)) + Size: =16 + Text: =If(IsEmpty(colPack), "Open free pack", "Open next pack") + Width: =288 + X: =1040 + Y: =160 + - PackSummary: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#F5F1E8") + Font: =Font.Lato + Height: =34 + Live: =Live.Polite + OnSelect: = + Size: =15 + Text: =If(IsEmpty(colPack), "Open a pack to discover three cards.", CountIf(colPack, Revealed) & " / 3 revealed · " & CountRows(Distinct(colOwned, Id)) & " / 12 collected") + Width: =960 + X: =38 + Y: =169 + - PackGallery: + Control: Gallery@2.15.0 + Variant: Vertical + Properties: + AccessibleLabel: ="Three-card pack. Reveal each card in order or choose any card." + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Height: =415 + Items: =Sort(colPack, Slot) + OnSelect: = + TabIndex: =0 + TemplatePadding: =8 + TemplateSize: =405 + Width: =1310 + WrapCount: =3 + X: =28 + Y: =215 + Children: + - PackFrame: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#EACB88") + Color: =ColorValue("#F5F1E8") + DisabledBorderColor: =If(ThisItem.Revealed, Switch(ThisItem.Card.Rarity, "Legendary", ColorValue("#EACB88"), "Rare", ColorValue("#BBA7EA"), "Uncommon", ColorValue("#8ECFB4"), ColorValue("#AFC3CC")), ColorValue("#EACB88")) + DisabledFill: =ColorValue("#1D303C") + DisplayMode: =DisplayMode.Disabled + Fill: =ColorValue("#1D303C") + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =400 + OnSelect: = + Text: ="" + Width: =Parent.TemplateWidth - 10 + X: =5 + - PackArt: + Control: Image@2.2.3 + Properties: + AccessibleLabel: =ThisItem.Card.Name & ", original fictional illustration" + Height: =212 + Image: ="data:image/svg+xml;utf8," & EncodeUrl(ThisItem.Card.Art) + OnSelect: = + Visible: =ThisItem.Revealed + Width: =Parent.TemplateWidth - 40 + X: =20 + Y: =12 + - PackRarity: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#F5F1E8") + Font: =Font.Lato + Height: =26 + OnSelect: = + Size: =12 + Text: =ThisItem.Card.Id & " / " & Upper(ThisItem.Card.Rarity) + Visible: =ThisItem.Revealed + Width: =Parent.TemplateWidth - 40 + X: =20 + Y: =230 + - PackName: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#F5F1E8") + Font: =Font.Lato + FontWeight: =FontWeight.Bold + Height: =44 + OnSelect: = + Size: =20 + Text: =ThisItem.Card.Name + Visible: =ThisItem.Revealed + Width: =Parent.TemplateWidth - 40 + X: =20 + Y: =260 + - PackOutcome: + Control: Label@2.5.1 + Properties: + Color: =If(ThisItem.IsNew, ColorValue("#8ECFB4"), ColorValue("#B7C8CC")) + Font: =Font.Lato + Live: =Live.Polite + OnSelect: = + Size: =12 + Text: =If(ThisItem.IsNew, "NEW · Added to your binder", "ALREADY OWNED · Completion unchanged") + Visible: =ThisItem.Revealed + Width: =Parent.TemplateWidth - 40 + X: =20 + Y: =308 + - PackDetails: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#EACB88") + Color: =ColorValue("#F5F1E8") + Fill: =ColorValue("#1D303C") + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + OnSelect: =Set(varCard, ThisItem.Card); Navigate(DetailScreen, ScreenTransition.Fade) + Size: =13 + Text: ="Card details" + Tooltip: ="Read " & ThisItem.Card.Name & " details" + Visible: =ThisItem.Revealed + Width: =Parent.TemplateWidth - 40 + X: =20 + Y: =350 + - RevealCard: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#EACB88") + Color: =ColorValue("#F5F1E8") + Fill: =ColorValue("#1D303C") + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =400 + OnSelect: |- + =If(!ThisItem.Revealed, Patch(colPack, ThisItem, {Revealed: true})) + Size: =22 + Text: ="✦" & Char(10) & "REVEAL CARD " & ThisItem.Slot + Tooltip: ="Reveal card " & ThisItem.Slot & ". Ownership was awarded when the pack opened." + Visible: =!ThisItem.Revealed + Width: =Parent.TemplateWidth - 10 + X: =5 + - PackRules: + Control: Label@2.5.1 + Properties: + Color: =ColorValue("#B7C8CC") + Font: =Font.Lato + Height: =56 + OnSelect: = + Size: =12 + Text: ="Cards are awarded before reveal. Each slot rolls independently; duplicates are possible." & Char(10) & "Rarity is chosen first, then a card uniformly within that rarity. Duplicates never increase completion." + Width: =1285 + X: =38 + Y: =638 + - PackSaveStatus: + Control: Label@2.5.1 + Properties: + Color: =If(varStorageWarning, ColorValue("#FFB8A8"), ColorValue("#B7C8CC")) + Font: =Font.Lato + Height: =54 + Live: =Live.Polite + OnSelect: = + Size: =12 + Text: =If(varStorageWarning, "NOT SAVED · Session only. Keep this app open and retry saving; closing can lose your cards.", If(IsEmpty(colPack), "Your existing binder stays with you. Free packs have no purchases or currency.", "Saved on this device. You can leave mid-reveal; awarded cards stay in your binder.")) + Width: =1020 + X: =38 + Y: =700 + - RetryPackSave: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =ColorValue("#EACB88") + Color: =ColorValue("#F5F1E8") + Fill: =ColorValue("#1D303C") + FocusedBorderColor: =ColorValue("#EACB88") + FocusedBorderThickness: =3 + Font: =Font.Lato + Height: =44 + OnSelect: =IfError(SaveData(colOwned, "MoonfieldOwnedV1"), Set(varStorageWarning, true), Set(varStorageWarning, false)) + Size: =14 + Text: ="Retry saving" + Visible: =varStorageWarning + Width: =238 + X: =1090 + Y: =702 diff --git a/samples/collectible-card-binder/sourcecode/studio/Src/_EditorState.pa.yaml b/samples/collectible-card-binder/sourcecode/studio/Src/_EditorState.pa.yaml new file mode 100644 index 00000000..47c4dc6d --- /dev/null +++ b/samples/collectible-card-binder/sourcecode/studio/Src/_EditorState.pa.yaml @@ -0,0 +1,13 @@ +# ************************************************************************************************ +# Warning: YAML source code for Canvas Apps should only be used to review changes made within Power Apps Studio and for minor edits (Preview). +# Use the maker portal to create and edit your Power Apps. +# +# The schema file for Canvas Apps is available at https://go.microsoft.com/fwlink/?linkid=2304907 +# +# For more information, visit https://go.microsoft.com/fwlink/?linkid=2292623 +# ************************************************************************************************ +EditorState: + ScreensOrder: + - BinderScreen + - DetailScreen + - PackScreen diff --git a/samples/collectible-card-binder/tests/test_catalog.py b/samples/collectible-card-binder/tests/test_catalog.py new file mode 100644 index 00000000..77285c64 --- /dev/null +++ b/samples/collectible-card-binder/tests/test_catalog.py @@ -0,0 +1,43 @@ +"""Local reference/data checks. These do not execute Power Fx or test import.""" +import collections,json,unittest,xml.etree.ElementTree as ET +from pathlib import Path +ROOT=Path(__file__).resolve().parents[1] +CARDS=json.loads((ROOT/'sourcecode/cards.json').read_text()) +def filter_cards(owned,mode='All',rarity='All rarities',query=''): + return [c for c in CARDS if (mode=='All' or (c['Id'] in owned)==(mode=='Owned')) and (rarity=='All rarities' or c['Rarity']==rarity) and query.strip().lower() in (c['Name']+' '+c['Realm']).lower()] +class CatalogTests(unittest.TestCase): + def test_twelve_unique_stable_ids(self): + self.assertEqual([c['Id'] for c in CARDS],[f'MF-{n:03}' for n in range(1,13)]) + def test_rarity_distribution(self): + self.assertEqual(collections.Counter(c['Rarity'] for c in CARDS),{'Common':4,'Uncommon':3,'Rare':3,'Legendary':2}) + def test_original_assets_valid_and_embedded(self): + for c in CARDS: + asset=(ROOT/'assets/cards'/f"{c['Id']}.svg").read_text() + self.assertEqual(asset,c['Art']); self.assertEqual(ET.fromstring(asset).tag,'{http://www.w3.org/2000/svg}svg') + self.assertNotIn('