@mohasinac/appkit 4.52.1 → 4.52.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/.build-stamp.json +4 -4
  2. package/dist/_internal/client/features/digital-code/CodeRevealPanel.js +4 -4
  3. package/dist/_internal/server/features/products/list-public.js +37 -1
  4. package/dist/features/about/components/PolicyPageView.js +1 -1
  5. package/dist/features/auctions/components/MarketplaceAuctionCard.js +2 -2
  6. package/dist/features/auth/components/SocialAuthButtons.js +1 -1
  7. package/dist/features/categories/components/CategoryBundlesListing.js +1 -1
  8. package/dist/features/categories/components/CategoryDetailPageView.js +8 -7
  9. package/dist/features/checkout/components/CheckoutStepper.js +1 -1
  10. package/dist/features/faq/components/FAQAccordion.js +1 -1
  11. package/dist/features/filters/FilterFacetSection.js +1 -1
  12. package/dist/features/filters/RangeFilter.js +1 -1
  13. package/dist/features/filters/SwitchFilter.js +1 -1
  14. package/dist/features/homepage/components/BeforeAfterCard.js +2 -2
  15. package/dist/features/homepage/components/CharacterHotspot.js +1 -1
  16. package/dist/features/homepage/components/FAQSection.js +1 -1
  17. package/dist/features/homepage/components/HeroBanner.js +1 -1
  18. package/dist/features/homepage/components/HeroCarousel.js +4 -4
  19. package/dist/features/homepage/components/HowItWorksSection.js +1 -1
  20. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  21. package/dist/features/homepage/components/SecurityHighlightsSection.js +1 -1
  22. package/dist/features/homepage/components/TrustBadges.js +1 -1
  23. package/dist/features/homepage/components/WelcomeSection.js +1 -1
  24. package/dist/features/layout/AppLayoutShell.js +8 -1
  25. package/dist/features/layout/BackToTop.js +2 -2
  26. package/dist/features/layout/BottomActions.js +20 -5
  27. package/dist/features/layout/BottomChrome.d.ts +0 -29
  28. package/dist/features/layout/BottomChrome.js +72 -7
  29. package/dist/features/live/components/LiveItemDetailPageView.js +1 -1
  30. package/dist/features/products/components/ProductFeaturesSelector.d.ts +38 -1
  31. package/dist/features/products/components/ProductFeaturesSelector.js +133 -34
  32. package/dist/features/products/hooks/useProducts.js +18 -1
  33. package/dist/features/reviews/components/ReviewDetailShell.js +2 -1
  34. package/dist/features/reviews/components/ReviewSummary.js +1 -1
  35. package/dist/features/tester/seed-data/_happy-path.js +1 -1
  36. package/dist/features/tester/seed-data/authored/admin__catalog-listings.js +1 -1
  37. package/dist/features/tester/seed-data/authored/content-discovery__category-brand-relations.js +3 -3
  38. package/dist/features/tester/seed-data/authored/content-discovery__category-tabs-franchise-bundles.d.ts +2 -0
  39. package/dist/features/tester/seed-data/authored/content-discovery__category-tabs-franchise-bundles.js +152 -0
  40. package/dist/features/tester/seed-data/authored/design-ux__action-bar-placement.d.ts +2 -0
  41. package/dist/features/tester/seed-data/authored/design-ux__action-bar-placement.js +89 -0
  42. package/dist/features/tester/seed-data/authored/design-ux__feature-badge-picker.d.ts +2 -0
  43. package/dist/features/tester/seed-data/authored/design-ux__feature-badge-picker.js +82 -0
  44. package/dist/features/tester/seed-data/authored/design-ux__rich-text-readability.d.ts +2 -0
  45. package/dist/features/tester/seed-data/authored/design-ux__rich-text-readability.js +84 -0
  46. package/dist/features/tester/seed-data/authored/index.js +8 -0
  47. package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +2 -2
  48. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +157 -3
  49. package/dist/styles.css +74 -1
  50. package/dist/tailwind-utilities.css +1 -1
  51. package/dist/ui/components/RichTextEditor.js +1 -1
  52. package/dist/ui/rich-text/RichText.style.css +73 -0
  53. package/package.json +1 -1
@@ -44,14 +44,14 @@ export const authored = {
44
44
  startPage: "/categories",
45
45
  steps: [
46
46
  "Open /categories and read every top-level category shown.",
47
- "Check both Spinning Tops and Living Collectibles are present.",
47
+ "Check all six roots are present: Spinning Tops, Trading Cards, Collectible Figures, Hot Wheels, Model Kits, Living Collectibles.",
48
48
  "Click Living Collectibles and read its children.",
49
49
  "Click down to a leaf under it.",
50
50
  "Check the leaf lists at least one live-item listing.",
51
51
  ],
52
52
  inputs: { rootA: "category-spinning-tops", rootB: "category-living-collectibles" },
53
- expectedBehaviour: "Both roots are on the index. The tree had one root and two tiers before it was rebuilt, so the live-item listings carried no category at all and were unreachable from every category and brand page — a whole listing type invisible to browsing while its detail pages worked fine.",
54
- expectedUiState: "Two roots on the index, and the Living Collectibles branch walks down to a leaf holding a live listing. One root, or a second root that leads nowhere, is the finding.",
53
+ expectedBehaviour: "All SIX roots are on the index. The tree had one root and two tiers before it was rebuilt, so the live-item listings carried no category at all and were unreachable from every category and brand page — a whole listing type invisible to browsing while its detail pages worked fine. Living Collectibles is the one to walk, because it is the root whose leaves hold the live-item listings.",
54
+ expectedUiState: "Six roots on the index, and the Living Collectibles branch walks down to a leaf holding a live listing. A missing root, or a root that leads nowhere, is the finding.",
55
55
  endResult: "Read-only.",
56
56
  },
57
57
  "checklist-content-discovery-category-brand-relations-root-page-shows-descendant-products": {
@@ -0,0 +1,2 @@
1
+ import type { AuthoredCase } from "./_types";
2
+ export declare const authored: Record<string, AuthoredCase>;
@@ -0,0 +1,152 @@
1
+ /*
2
+ * WHY: Authored six-part procedures for the categories-index tabs, the
3
+ * franchise unit and the bundle collage (2026-10-14).
4
+ * WHAT: 8 cases, keyed by full checklist id.
5
+ *
6
+ * 🛑 EVERY CASE IS BEFORE → AFTER, because every defect this work produced
7
+ * rendered a page that looked fine:
8
+ *
9
+ * 1. The index rendered all 1,185 rows flat — six roots buried under 752
10
+ * part leaves. A case reading "the categories page loads" passes against
11
+ * that.
12
+ * 2. The Parts tab first returned 772, catching the 20 classification NODES
13
+ * as well as the leaves, because their own `parentIds` also pass through
14
+ * a `*-parts` node. A list mixing "Attack Rings" with attack rings is not
15
+ * visibly wrong.
16
+ * 3. Seven franchises have no stock. Active, each is a card opening an empty
17
+ * shop; absent, seventeen `CharacterHotspot` links stay 404. Both look
18
+ * correct from any single page — which is why two cases here check the
19
+ * index and the detail page SEPARATELY.
20
+ * 4. Every bundle also carries a placeholder cover, so a collage tested
21
+ * after the cover never renders — and one grey tile is not "broken".
22
+ *
23
+ * So each case counts an exact number or compares two surfaces. Several carry
24
+ * a nonsense control, because a filter that returns everything looks identical
25
+ * to a filter that works.
26
+ *
27
+ * @tag domain:tester
28
+ * @tag layer:seed
29
+ * @tag pattern:none
30
+ * @tag access:server-only
31
+ * @tag consumers:seed-data/authored/index.ts
32
+ * @tag sideEffects:none
33
+ */
34
+ export const authored = {
35
+ "checklist-content-discovery-category-tabs-franchise-bundles-index-shows-roots-only": {
36
+ roles: ["guest"],
37
+ startPage: "/categories",
38
+ steps: [
39
+ "Open /categories.",
40
+ "Confirm the Categories tab is the one selected on arrival.",
41
+ "Count the cards. There must be exactly six.",
42
+ "Read the six names: Spinning Tops, Trading Cards, Collectible Figures, Hot Wheels, Model Kits, Living Collectibles.",
43
+ "Answer no if any deeper category appears here — 'Attack Rings', 'Beyblade Burst', '1B' or any single part name.",
44
+ ],
45
+ expectedBehaviour: "BEFORE: the index rendered the whole flat forest — 1,185 rows, of which 752 are part leaves and 220 are model leaves — so the six roots were buried and a visitor met a wall of names with no sense of where they were. AFTER: the Categories tab shows ONLY the tier-0 roots and you walk down from one.",
46
+ expectedUiState: "Exactly six cards, each a root. No tier-1-or-deeper row anywhere on this tab.",
47
+ expectedData: { rootCards: 6 },
48
+ endResult: "Nothing is changed; this case only reads.",
49
+ },
50
+ "checklist-content-discovery-category-tabs-franchise-bundles-five-tabs-no-all": {
51
+ roles: ["guest"],
52
+ startPage: "/categories",
53
+ steps: [
54
+ "Open /categories.",
55
+ "Read the tab bar.",
56
+ "Confirm there are exactly five tabs: Categories, Brands, Franchises, Parts, Bundles.",
57
+ "Answer no if a tab labelled 'All' is present.",
58
+ ],
59
+ expectedBehaviour: "BEFORE: the tabs were All / Categories / Brands, and 'All' was the DEFAULT — it is what rendered the 1,185 rows. AFTER: five tabs, no 'All'. Every tab answers a question ('which parts do you have?'); 'all' answered none of them, which is why it was removed rather than reordered.",
60
+ expectedUiState: "Five tabs, named exactly as above, with no 'All'.",
61
+ expectedData: { tabCount: 5, hasAllTab: false },
62
+ endResult: "Nothing is changed; this case only reads.",
63
+ },
64
+ "checklist-content-discovery-category-tabs-franchise-bundles-parts-tab-is-leaves-only": {
65
+ roles: ["guest"],
66
+ startPage: "/categories",
67
+ steps: [
68
+ "Open /categories and click the Parts tab.",
69
+ "Read the total the pager reports.",
70
+ "Confirm it reads 752 and not 772.",
71
+ "Scan the first page and confirm every card is an individual PART — '1B', 'Ten Heavy', 'Neo Right SG'.",
72
+ "Answer no if a classification GROUP appears as a card: 'Attack Rings', 'Weight Disks', 'Performance Tips', 'Bit Chips'.",
73
+ ],
74
+ expectedBehaviour: "BEFORE: the tab returned 772, because the predicate matched any row whose ancestry passes a `*-parts` node — which is true of the 20 classification NODES as well as the leaves beneath them. The list mixed groups with their own contents and was not visibly wrong. AFTER: leaves only, 752. A part is a leaf by itself; a classification is a branch you reach by drilling down.",
75
+ expectedUiState: "752 parts reported. Every card is a single part; no classification group appears.",
76
+ expectedData: { partsTotal: 752, classificationNodesShown: 0 },
77
+ endResult: "Nothing is changed; this case only reads.",
78
+ },
79
+ "checklist-content-discovery-category-tabs-franchise-bundles-tabs-do-not-overlap": {
80
+ roles: ["guest"],
81
+ startPage: "/categories",
82
+ steps: [
83
+ "Open /categories and note the total each of the five tabs reports: Categories, Brands, Franchises, Parts, Bundles.",
84
+ "Expect 6, 28, 8, 752, 5.",
85
+ "Pick one brand by name on the Brands tab — for example Takara-Tomy.",
86
+ "Open each of the other four tabs and search for that same name.",
87
+ "Answer no if it appears on any tab other than Brands.",
88
+ "Repeat once with a bundle name taken from the Bundles tab.",
89
+ ],
90
+ expectedBehaviour: "Brands, franchises and bundles are all tier-0 rows, so a predicate that tested the tier before the discriminator would list every one of them under Categories as well. The type check has to come first. A row on two tabs double-counts the catalogue and makes the totals unreadable.",
91
+ expectedUiState: "Tab totals read 6 / 28 / 8 / 752 / 5. The sampled brand and bundle each appear on exactly one tab.",
92
+ expectedData: { categories: 6, brands: 28, franchises: 8, parts: 752, bundles: 5 },
93
+ endResult: "Nothing is changed; this case only reads.",
94
+ },
95
+ "checklist-content-discovery-category-tabs-franchise-bundles-franchise-index-hides-unstocked": {
96
+ roles: ["guest"],
97
+ startPage: "/franchise",
98
+ steps: [
99
+ "Open /franchise.",
100
+ "Count the cards. Exactly one must be present: Beyblade.",
101
+ "Answer no if Marvel, DC Comics, Dragon Ball, Naruto, Bleach, One Piece or One Punch Man appears here.",
102
+ ],
103
+ expectedBehaviour: "Eight franchises are seeded; seven have no stock. A row with no product is a dead facet — a card that opens an empty shop — so the index lists ACTIVE franchises only. The seven still exist and their pages still resolve; they are simply not advertised as somewhere to shop until they have something to sell.",
104
+ expectedUiState: "One card, Beyblade. None of the seven unstocked IPs.",
105
+ expectedData: { franchiseCards: 1 },
106
+ endResult: "Nothing is changed; this case only reads.",
107
+ },
108
+ "checklist-content-discovery-category-tabs-franchise-bundles-unstocked-franchise-page-resolves": {
109
+ roles: ["guest"],
110
+ startPage: "/franchise/marvel",
111
+ steps: [
112
+ "Open /franchise/marvel directly.",
113
+ "Confirm a real page renders — a heading reading Marvel — and NOT a 404 or not-found view.",
114
+ "Confirm a message says plainly that this franchise is not stocked yet.",
115
+ "Answer no if the page 404s, or if it renders an empty product grid with no explanation.",
116
+ ],
117
+ expectedBehaviour: "BEFORE: `/franchise/*` was not a route at all, so seventeen links in CharacterHotspot 404ed. AFTER: the page resolves. An unstocked franchise is NOT a 404 — the row exists deliberately so those links land somewhere honest. An unexplained empty grid would be the other wrong answer: the visitor arrived from a real link and should be told it is coming, not left to assume something broke.",
118
+ expectedUiState: "The Marvel page renders with its name and an explicit 'not stocking this yet' message. No 404, no bare empty grid.",
119
+ expectedData: { httpOk: true, explanationShown: true },
120
+ endResult: "Nothing is changed; this case only reads.",
121
+ },
122
+ "checklist-content-discovery-category-tabs-franchise-bundles-franchise-detail-lists-its-products": {
123
+ roles: ["guest"],
124
+ startPage: "/franchise/franchise-beyblade",
125
+ steps: [
126
+ "Open the Beyblade franchise page from /franchise.",
127
+ "Confirm it reports a listing count in the thousands, not zero and not a handful.",
128
+ "Confirm product cards render.",
129
+ "CONTROL: now open /franchise/zzznope by typing it.",
130
+ "Confirm that page 404s rather than showing the whole catalogue.",
131
+ ],
132
+ expectedBehaviour: "The products filter matches on `franchiseSlug`, so a clause that was silently dropped would return the ENTIRE catalogue on every franchise page — which looks like a working page full of products. The nonsense control is the only step that tells a working filter from an absent one.",
133
+ expectedUiState: "Beyblade lists ~1,304 listings. An unknown franchise slug 404s and never renders a populated grid.",
134
+ expectedData: { beybladeListings: 1304, nonsenseSlug404s: true },
135
+ endResult: "Nothing is changed; this case only reads.",
136
+ },
137
+ "checklist-content-discovery-category-tabs-franchise-bundles-bundle-card-shows-a-collage": {
138
+ roles: ["guest"],
139
+ startPage: "/categories",
140
+ steps: [
141
+ "Open /categories and click the Bundles tab.",
142
+ "Confirm five bundle cards render.",
143
+ "Look at each card's image area.",
144
+ "Confirm it shows a COLLAGE of several product photos, not one single image.",
145
+ "Answer no if any card shows a single flat colour tile or one lone photo.",
146
+ ],
147
+ expectedBehaviour: "BEFORE: a bundle card showed one generic placeholder tile — the least informative thing a bundle card can show, since a bundle IS its contents. AFTER: each card shows a collage built from its members' own photos, resolved at seed time so the card costs no extra query. All five bundles carry 3 or 4 member images, which clears the 3-image minimum the collage needs.",
148
+ expectedUiState: "Five bundle cards, each showing a multi-image collage rather than a single tile.",
149
+ expectedData: { bundleCards: 5, cardsWithCollage: 5 },
150
+ endResult: "Nothing is changed; this case only reads.",
151
+ },
152
+ };
@@ -0,0 +1,2 @@
1
+ import type { AuthoredCase } from "./_types";
2
+ export declare const authored: Record<string, AuthoredCase>;
@@ -0,0 +1,89 @@
1
+ /*
2
+ * WHY: Authored six-part procedures for the design-ux/action-bar-placement page.
3
+ * WHAT: 6 case(s), keyed by full checklist id.
4
+ *
5
+ * @tag domain:tester
6
+ * @tag layer:seed
7
+ * @tag pattern:none
8
+ * @tag access:server-only
9
+ * @tag consumers:seed-data/authored/index.ts
10
+ * @tag sideEffects:none
11
+ */
12
+ export const authored = {
13
+ "checklist-design-ux-action-bar-placement-back-to-top-dismiss-visible": {
14
+ roles: ["guest"],
15
+ startPage: "/products",
16
+ steps: [
17
+ "Open /products and scroll down more than about 400 pixels.",
18
+ "Look at the floating controls in the bottom corner of the window.",
19
+ "Click the smaller of the two controls.",
20
+ "Navigate to /auctions and scroll past 400 pixels again.",
21
+ ],
22
+ expectedBehaviour: "The dismiss control is an icon button sized as a true square, so its glyph has room to render. Dismissal is in-memory and resets on route change, so it never hides the control for the rest of the session.",
23
+ expectedUiState: "TWO round controls are visible: a larger one showing an upward chevron, and a smaller one showing a clearly visible ✕. The prior symptom was the smaller control rendering as an EMPTY outlined pill — present and clickable, with no ✕ inside it, because its content box computed to zero width. After clicking the ✕, both controls disappear.",
24
+ endResult: "On /auctions both controls are back after scrolling. Nothing is persisted — a reload also brings them back.",
25
+ },
26
+ "checklist-design-ux-action-bar-placement-action-bar-top-on-desktop": {
27
+ roles: ["guest"],
28
+ startPage: "/products",
29
+ steps: [
30
+ "Maximise the browser window so it is wider than about 1024 pixels.",
31
+ "Open any product detail page from /products.",
32
+ "Look immediately below the site header.",
33
+ "Scroll the page down and watch the bar.",
34
+ ],
35
+ expectedBehaviour: "At `lg` and up the chrome tier is pinned under the header rather than to the bottom edge, and its shadow is cast downward onto the page.",
36
+ expectedUiState: "The full-width action bar sits directly beneath the header with no gap, and stays there while the page scrolls. Its drop shadow falls DOWNWARD. It must NOT be at the bottom of the window — that was the prior behaviour at every width.",
37
+ endResult: "Nothing persists; this is layout only. The bar stays under the header for the whole visit at this width.",
38
+ },
39
+ "checklist-design-ux-action-bar-placement-action-bar-bottom-on-mobile": {
40
+ roles: ["guest"],
41
+ startPage: "/products",
42
+ steps: [
43
+ "On the same product detail page, narrow the browser window to phone width (under about 1024 pixels), or switch to device emulation.",
44
+ "Look at the bottom of the window.",
45
+ "Slowly widen the window back past the 1024-pixel line, then narrow it again.",
46
+ ],
47
+ expectedBehaviour: "One component serves both layouts, switching on a media query, so the bar must move between the two positions rather than being duplicated or dropped.",
48
+ expectedUiState: "At phone width the action bar is at the BOTTOM, stacked immediately above the bottom navigation bar, with its shadow falling UPWARD. While resizing across the breakpoint the bar moves cleanly from one end to the other — it is never visible in BOTH places at once, and never disappears entirely at any width.",
49
+ endResult: "No state is written. The bar's position always matches the current window width, including after a reload at either width.",
50
+ },
51
+ "checklist-design-ux-action-bar-placement-action-bar-no-content-overlap": {
52
+ roles: ["guest"],
53
+ startPage: "/products",
54
+ steps: [
55
+ "At desktop width, open a product detail page that shows an action bar.",
56
+ "Read the very first line of page content under the header.",
57
+ "Now open a page with no action bar at all, such as /about.",
58
+ "Look at the space immediately under the header.",
59
+ ],
60
+ expectedBehaviour: "The bar is fixed, so it is out of flow and would cover content unless space is reserved. `<Main>` reserves exactly the published tier height, which is zero when no bar exists — so the reservation appears and disappears with the bar rather than being a constant allowance.",
61
+ expectedUiState: "With a bar present, the first heading and first row of content are fully visible and not clipped behind it. On /about, with no bar, there is NO empty band under the header — the content starts where it always did.",
62
+ endResult: "Nothing persists. Navigating between a page with a bar and one without must not leave a residual gap on the page that has none.",
63
+ },
64
+ "checklist-design-ux-action-bar-placement-action-bar-panels-open-into-screen": {
65
+ roles: ["admin"],
66
+ startPage: "/admin/products",
67
+ steps: [
68
+ "Sign in as admin@letitrip.in / TempPass123! and open /admin/products at desktop width.",
69
+ "Tick the checkboxes on two rows to enter bulk mode.",
70
+ "Open the bulk action-type picker on the bar.",
71
+ "Narrow the window to phone width and open the same picker again.",
72
+ ],
73
+ expectedBehaviour: "The panel is positioned against the bar, so its open direction must follow whichever screen edge the bar is attached to. Opening away from the screen would put the panel partly or wholly out of view.",
74
+ expectedUiState: "At desktop width the picker expands DOWNWARD over the page content, below the bar. At phone width it expands UPWARD from the bar. In both cases the whole panel is on screen and scrollable, and no part of it is cut off by the window edge.",
75
+ endResult: "Closing the picker leaves the bulk selection intact. Clearing the selection removes the bar and the panel together.",
76
+ },
77
+ "checklist-design-ux-action-bar-placement-back-to-top-clears-the-bar": {
78
+ roles: ["guest"],
79
+ startPage: "/products",
80
+ steps: [
81
+ "At phone width, open a product detail page with an action bar and scroll past 400 pixels.",
82
+ "Look at the vertical gap between the back-to-top control and the action bar.",
83
+ "Slowly widen the window past the desktop breakpoint, watching the control.",
84
+ ],
85
+ expectedBehaviour: "The back-to-top offset is expressed against the measured tier rather than against any individual bar, so it tracks the bar's height automatically — and when the tier moves to the top on desktop, the bottom measurement collapses to zero and the control settles lower.",
86
+ expectedUiState: "At phone width the control floats clear ABOVE the action bar, which itself sits above the bottom navigation — none of the three overlap. As the window widens past the breakpoint the control moves DOWN into the space the bar vacated. At no point during the resize do the control and any bar overlap.",
87
+ endResult: "Position is derived from layout alone and nothing is stored; reloading at either width reproduces the same spacing.",
88
+ },
89
+ };
@@ -0,0 +1,2 @@
1
+ import type { AuthoredCase } from "./_types";
2
+ export declare const authored: Record<string, AuthoredCase>;
@@ -0,0 +1,82 @@
1
+ /*
2
+ * WHY: Authored six-part procedures for the design-ux/feature-badge-picker page.
3
+ * WHAT: 5 case(s), keyed by full checklist id.
4
+ *
5
+ * @tag domain:tester
6
+ * @tag layer:seed
7
+ * @tag pattern:none
8
+ * @tag access:server-only
9
+ * @tag consumers:seed-data/authored/index.ts
10
+ * @tag sideEffects:none
11
+ */
12
+ export const authored = {
13
+ "checklist-design-ux-feature-badge-picker-features-are-searchable": {
14
+ roles: ["seller"],
15
+ startPage: "/store/products",
16
+ steps: [
17
+ "Sign in as the seller who owns the store and open /store/products.",
18
+ "Open any product for editing.",
19
+ "Scroll to the 'Feature badges' section of the form.",
20
+ "Open the control and type a few letters of a badge name.",
21
+ ],
22
+ expectedBehaviour: "Anything over five options is a searchable picker in this codebase. The control loads the platform badges eligible for this listing type plus the store's own, and filters them client-side as you type.",
23
+ expectedUiState: "'Feature badges' is a SINGLE control that opens a searchable list with a text input — not a two-column grid of bordered checkbox tiles, which is what it was. Typing narrows the visible options, and selected badges appear as removable chips on the control.",
24
+ endResult: "Closing and reopening the picker preserves the current selection. Nothing is saved until the product form itself is saved.",
25
+ },
26
+ "checklist-design-ux-feature-badge-picker-features-search-matches-description": {
27
+ roles: ["seller"],
28
+ startPage: "/store/products",
29
+ steps: [
30
+ "With the badge picker open, note a badge whose description contains a word that is NOT in its label — 'Sealed' is described as 'Factory wrap intact and never opened'.",
31
+ "Type that description-only word into the picker's search box, e.g. 'factory'.",
32
+ ],
33
+ inputs: { "picker search": "factory" },
34
+ expectedBehaviour: "Each option's description is folded into its label text specifically so it remains searchable after the conversion. In the old grid the description was visible on every tile, so it was findable by eye; in a filtered list it would be unreachable unless it is part of what is matched.",
35
+ expectedUiState: "The badge whose DESCRIPTION contains the typed word is listed, even though its name does not contain it. Searching a word in neither the label nor any description shows the no-results message instead.",
36
+ endResult: "Clearing the search box restores the full list. No selection has changed.",
37
+ },
38
+ "checklist-design-ux-feature-badge-picker-features-scope-is-visible": {
39
+ roles: ["seller"],
40
+ startPage: "/store/products",
41
+ steps: [
42
+ "Open the badge picker on a product belonging to a store that has at least one custom badge of its own.",
43
+ "Read the full text of several options in the list.",
44
+ "Select one platform badge and one store badge, then close the picker and read the two chips.",
45
+ ],
46
+ expectedBehaviour: "The grid separated the two scopes under 'Platform features' and 'Store custom features' headings. A flat searchable list has no headings, so the scope is carried in each option's own text — which also means it survives into the selected chip, where the grid's headings never reached.",
47
+ expectedUiState: "Every option ends with '(platform)' or '(your store)'. The two selected chips each still show which scope they came from. An option with no scope marker at all is a regression.",
48
+ endResult: "The distinction persists after saving and reopening the product — it is derived from the badge's own scope, not from selection order.",
49
+ },
50
+ "checklist-design-ux-feature-badge-picker-features-respects-the-cap": {
51
+ roles: ["seller"],
52
+ startPage: "/store/products",
53
+ steps: [
54
+ "Open the badge picker and read the 'Selected: N of 24' line above it.",
55
+ "Select badges one at a time until N reaches the stated maximum.",
56
+ "Try to select one more badge.",
57
+ ],
58
+ expectedBehaviour: "The ceiling is applied to the incoming selection rather than by disabling unticked rows, because a multi-select has no per-row disabled state. Selections beyond the cap are refused, keeping the earliest ones — so the badge that fails is the one just clicked, which is what a user expects.",
59
+ expectedUiState: "The counter stops at the maximum and does not go above it. Attempting to add one more does not increase the count and does not silently replace an existing selection. The counter line always matches the number of chips shown.",
60
+ expectedData: { "selected badge count": "never exceeds the stated maximum" },
61
+ endResult: "Saving the product stores at most the maximum number of badges. Reopening the product shows the same set.",
62
+ },
63
+ "checklist-design-ux-feature-badge-picker-features-inline-create": {
64
+ roles: ["seller"],
65
+ startPage: "/store/products",
66
+ steps: [
67
+ "Open the badge picker on one of your own products.",
68
+ "Choose 'Create a badge for my store'.",
69
+ "Enter a label such as 'QA inline badge' and leave the icon as its default.",
70
+ "Submit the create form.",
71
+ "Close the picker, reload the page, and reopen the picker.",
72
+ ],
73
+ inputs: { "Badge label": "QA inline badge", Icon: "star" },
74
+ expectedBehaviour: "Creating posts to the store-scope features route, which sets the owning store server-side rather than trusting the request body, and scopes the new badge to the listing type being edited. The picker's option list is refetched afterwards so the badge becomes a real option rather than a chip with nothing behind it.",
75
+ expectedUiState: "The create drawer closes on success and the new badge appears immediately as a SELECTED chip. Reopening the picker lists it among the options, marked '(your store)'. After a full page reload it is still listed and still selected on the product.",
76
+ expectedData: {
77
+ "new badge scope": "store",
78
+ "new badge owner": "the signed-in seller's store",
79
+ },
80
+ endResult: "The badge persists as a store-scope feature and is available on that store's other products too. Remove it from the store's feature settings afterwards so it does not accumulate.",
81
+ },
82
+ };
@@ -0,0 +1,2 @@
1
+ import type { AuthoredCase } from "./_types";
2
+ export declare const authored: Record<string, AuthoredCase>;
@@ -0,0 +1,84 @@
1
+ /*
2
+ * WHY: Authored six-part procedures for the design-ux/rich-text-readability page.
3
+ * WHAT: 5 case(s), keyed by full checklist id.
4
+ *
5
+ * @tag domain:tester
6
+ * @tag layer:seed
7
+ * @tag pattern:none
8
+ * @tag access:server-only
9
+ * @tag consumers:seed-data/authored/index.ts
10
+ * @tag sideEffects:none
11
+ */
12
+ export const authored = {
13
+ "checklist-design-ux-rich-text-readability-prose-body-readable-dark": {
14
+ roles: ["guest"],
15
+ startPage: "/categories/category-spinning-tops",
16
+ steps: [
17
+ "Set the site to DARK mode using the theme toggle in the header.",
18
+ "Open /categories/category-spinning-tops.",
19
+ "Scroll to the multi-paragraph write-up directly below the hero banner (it begins 'Four generations of Beyblade').",
20
+ "Read the full write-up at normal viewing distance without leaning in or changing zoom.",
21
+ ],
22
+ expectedBehaviour: "The rich-text block takes its body colour from the active theme's own text token rather than from the Tailwind typography plugin's light-theme default. No element in the block should be rendering a colour that was chosen for a white page.",
23
+ expectedUiState: "Every sentence of the write-up is comfortably readable against the dark page. The body copy is a light grey, clearly brighter than the page background. It is NOT the dim, low-contrast grey that appears to recede into the background — the specific prior symptom was body text at gray-700 on a dark surface.",
24
+ endResult: "Nothing is written and nothing persists. Reloading the page in dark mode shows the same readable text; this is a pure rendering check.",
25
+ },
26
+ "checklist-design-ux-rich-text-readability-prose-links-visible-dark": {
27
+ roles: ["guest"],
28
+ startPage: "/categories/category-spinning-tops",
29
+ steps: [
30
+ "Stay in DARK mode on /categories/category-spinning-tops.",
31
+ "Without moving the mouse over them, locate the five inline links in the write-up: 'Beyblade Original', 'Metal Fight', 'Burst', 'Beyblade X' and 'Accessories'.",
32
+ "Compare each link's text colour against the paragraph text immediately surrounding it.",
33
+ ],
34
+ expectedBehaviour: "Link colour resolves to the theme's secondary accent in dark mode, not to the typography plugin's default link colour. Identifying a link must not require hovering, because hover is unavailable on touch devices.",
35
+ expectedUiState: "All five links are a visibly different colour from the body copy around them and are obviously clickable at a glance. The prior symptom was links rendering at gray-900 — near-black on a dark page, effectively invisible, findable only by sweeping the cursor and watching for the pointer to change. An underline alone does NOT satisfy this case: the text colour itself must differ.",
36
+ endResult: "No state changes. Clicking any of the five navigates to that category, which also confirms they were real links and not styled text.",
37
+ },
38
+ "checklist-design-ux-rich-text-readability-prose-readable-light-regression": {
39
+ roles: ["guest"],
40
+ startPage: "/categories/category-spinning-tops",
41
+ steps: [
42
+ "On /categories/category-spinning-tops, switch the theme toggle to LIGHT mode.",
43
+ "Read the same write-up and locate the same five inline links.",
44
+ "Toggle between light and dark three times, pausing on each to re-read one paragraph and one link.",
45
+ ],
46
+ expectedBehaviour: "Both themes resolve their colours from the same rebound token set, so a regression in one is a plausible consequence of a change to the other. Headings, body copy and links must each stay distinct from one another in both themes.",
47
+ expectedUiState: "In LIGHT mode the body copy is dark on the white surface and the links are the primary brand colour. Headings remain visibly darker/stronger than body copy. Nothing is washed out, and no text becomes near-invisible at either end of the toggle.",
48
+ endResult: "The theme choice persists across a reload (it is stored client-side), and the write-up renders correctly in whichever mode was last selected.",
49
+ },
50
+ "checklist-design-ux-rich-text-readability-prose-readable-across-surfaces": {
51
+ roles: ["guest"],
52
+ startPage: "/blog",
53
+ steps: [
54
+ "Set the site to DARK mode.",
55
+ "Open any published post from /blog and read its body copy and any inline links.",
56
+ "Open /terms (or /privacy) and read the policy body copy.",
57
+ "Open /faqs and expand any answer.",
58
+ "Open any product from /products and read its Description tab.",
59
+ ],
60
+ expectedBehaviour: "All four surfaces render through the same shared rich-text renderer, so one shared fix must reach all of them. A surface that is still unreadable means the fix did not apply there and the renderer is being bypassed.",
61
+ expectedUiState: "On each of the four pages the body copy is readable light-grey-on-dark and any inline links are a visibly distinct colour. No page shows the dim gray-700 body or the near-black links described in the dark-mode cases above.",
62
+ endResult: "Nothing persists. If any ONE of the four fails while the others pass, record which one — that narrows the cause to that surface's own markup rather than the shared renderer.",
63
+ },
64
+ "checklist-design-ux-rich-text-readability-prose-follows-custom-theme": {
65
+ roles: ["admin"],
66
+ startPage: "/admin/site",
67
+ steps: [
68
+ "Sign in as admin@letitrip.in / TempPass123!.",
69
+ "Open /admin/site and go to the Themes tab.",
70
+ "Note which theme is currently the default, then set a visibly different one as the active default (for example Cobalt Night, or Sunset).",
71
+ "Open /categories/category-spinning-tops in a new tab and read the write-up's body copy and links.",
72
+ "Return to /admin/site → Themes and restore the original default theme.",
73
+ ],
74
+ inputs: {
75
+ "Themes tab — active default": "a theme other than the current default",
76
+ },
77
+ expectedBehaviour: "Rich-text colours are bound to `--appkit-color-*` theme tokens, so switching the active theme must restyle admin-authored prose along with the rest of the site. This is only possible since the 2026-10-15 fix: the typography plugin's palette is hardcoded Tailwind greys that no theme record could ever reach.",
78
+ expectedUiState: "After the theme change, the write-up's paragraph colour and link colour both shift to match the new theme — they do not stay the same grey the previous theme used. Body copy remains readable against whatever surface the new theme sets.",
79
+ expectedData: {
80
+ "siteSettings.theme.defaultDarkThemeId": "restored to its original value at the end",
81
+ },
82
+ endResult: "The original default theme is restored, so no other tester inherits a changed site theme. Reloading the category page shows prose in the original theme's colours again.",
83
+ },
84
+ };
@@ -78,6 +78,7 @@ import { authored as a_content_discovery__coupons } from "./content-discovery__c
78
78
  import { authored as a_content_discovery__events } from "./content-discovery__events.js";
79
79
  import { authored as a_content_discovery__faq_help } from "./content-discovery__faq-help.js";
80
80
  import { authored as a_content_discovery__parts_database } from "./content-discovery__parts-database.js";
81
+ import { authored as a_content_discovery__category_tabs_franchise_bundles } from "./content-discovery__category-tabs-franchise-bundles.js";
81
82
  import { authored as a_content_discovery__notifications } from "./content-discovery__notifications.js";
82
83
  import { authored as a_content_discovery__search } from "./content-discovery__search.js";
83
84
  import { authored as a_cta_layout__checkout_bottom_bar } from "./cta-layout__checkout-bottom-bar.js";
@@ -85,6 +86,9 @@ import { authored as a_cta_layout__dialog_footers } from "./cta-layout__dialog-f
85
86
  import { authored as a_cta_layout__editor_action_bar } from "./cta-layout__editor-action-bar.js";
86
87
  import { authored as a_cta_layout__product_bottom_bar } from "./cta-layout__product-bottom-bar.js";
87
88
  import { authored as a_design_ux__back_to_top_button } from "./design-ux__back-to-top-button.js";
89
+ import { authored as a_design_ux__rich_text_readability } from "./design-ux__rich-text-readability.js";
90
+ import { authored as a_design_ux__action_bar_placement } from "./design-ux__action-bar-placement.js";
91
+ import { authored as a_design_ux__feature_badge_picker } from "./design-ux__feature-badge-picker.js";
88
92
  import { authored as a_design_ux__dashboard_layout } from "./design-ux__dashboard-layout.js";
89
93
  import { authored as a_design_ux__footer_theme } from "./design-ux__footer-theme.js";
90
94
  import { authored as a_design_ux__form_validation_errors } from "./design-ux__form-validation-errors.js";
@@ -199,6 +203,7 @@ export const AUTHORED_CASES = {
199
203
  ...a_content_discovery__events,
200
204
  ...a_content_discovery__faq_help,
201
205
  ...a_content_discovery__parts_database,
206
+ ...a_content_discovery__category_tabs_franchise_bundles,
202
207
  ...a_content_discovery__notifications,
203
208
  ...a_content_discovery__search,
204
209
  ...a_cta_layout__checkout_bottom_bar,
@@ -206,6 +211,9 @@ export const AUTHORED_CASES = {
206
211
  ...a_cta_layout__editor_action_bar,
207
212
  ...a_cta_layout__product_bottom_bar,
208
213
  ...a_design_ux__back_to_top_button,
214
+ ...a_design_ux__rich_text_readability,
215
+ ...a_design_ux__action_bar_placement,
216
+ ...a_design_ux__feature_badge_picker,
209
217
  ...a_design_ux__dashboard_layout,
210
218
  ...a_design_ux__footer_theme,
211
219
  ...a_design_ux__form_validation_errors,
@@ -180,8 +180,8 @@ export const authored = {
180
180
  "Click into one root category and read its page.",
181
181
  "Go back and click a tier-3 category and read its page.",
182
182
  ],
183
- expectedBehaviour: "The index renders the category forest — two roots across four tiers — and every card links to a page that resolves. Products carry their full ancestor chain, so a category page matches on its own id alone and needs no descendant expansion.",
184
- expectedUiState: "Both roots, Spinning Tops and Living Collectibles, are present. Every card shows a name and a count rather than a bare name. Both the root and the tier-3 page render with products rather than an empty grid.",
183
+ expectedBehaviour: "The index renders the SIX roots of the category forest — Spinning Tops, Trading Cards, Collectible Figures, Hot Wheels, Model Kits, Living Collectibles — and every card links to a page that resolves. It shows roots ONLY: the forest is 1,185 rows across five tiers (0-4) and the index used to render every one of them flat. Products carry their full ancestor chain, so a category page matches on its own id alone and needs no descendant expansion.",
184
+ expectedUiState: "All six roots are present, and NO tier-1-or-deeper category appears on the Categories tab. Every card shows a name and a count rather than a bare name. Both the root and the tier-3 page render with products rather than an empty grid.",
185
185
  expectedData: { rootCategoryCount: 2 },
186
186
  endResult: "Read-only; nothing persists. A root category page rendering blank with no error is the specific failure worth watching for — the query that produces it is wrapped in a catch that returns nothing.",
187
187
  },