Skip to content

feat: add ProductChooser ("Choose this if / not for" + framework matrix) - #417

Open
eugenia-scandit wants to merge 4 commits into
mainfrom
docs/product-chooser
Open

feat: add ProductChooser ("Choose this if / not for" + framework matrix)#417
eugenia-scandit wants to merge 4 commits into
mainfrom
docs/product-chooser

Conversation

@eugenia-scandit

Copy link
Copy Markdown
Collaborator

A product-overview decision block, fed entirely from single sources of truth so the visible content can never drift from the machine-readable metadata:

  • "Choose this if you want to" <- the page's user_intents frontmatter
  • "Consider another product if" <- the page's not_for frontmatter
  • "Available on" matrix <- src/data/products.json (the product registry the Agent Skills pages already use), keyed by the page's product

Renders nothing if the fields are unset. Frontmatter is passed in as a prop (MDX exposes frontMatter in scope).

eugenia-scandit and others added 2 commits August 5, 2026 11:28
A product-overview decision block, fed entirely from single sources of truth
so the visible content can never drift from the machine-readable metadata:

- "Choose this if you want to" <- the page's `user_intents` frontmatter
- "Consider another product if" <- the page's `not_for` frontmatter
- "Available on" matrix <- src/data/products.json (the product registry the
  Agent Skills pages already use), keyed by the page's `product`

Demonstrated on the SparkScan iOS intro. Renders nothing if the fields are
unset. Frontmatter is passed in as a prop (MDX exposes `frontMatter` in scope).

Note: products.json lists Titanium for SparkScan, which contradicts the
availability stubs (SparkScan is not available on Titanium) — a data
inconsistency to reconcile separately (which source is authoritative).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… matrix

products.json marks frameworks a product does NOT support with version "n/a"
(e.g. SparkScan on Titanium). The chooser now filters those out of the
"Available on" matrix, so SparkScan correctly shows 9 frameworks without
Titanium. Fixes the data-vs-display mismatch; no products.json change needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
eugenia-scandit and others added 2 commits August 7, 2026 11:31
…nother product' item

Each not_for line that names another product (e.g. '…— use MatrixScan Count') now renders that product name as a framework-aware link to its About page, resolved from products.json (SSOT): the current framework's URL, else Web, else any supported. Longest-name match so 'MatrixScan Count' wins over 'MatrixScan'. Falls back to plain text when there's no match. No frontmatter schema change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@raffaelefarinaro raffaelefarinaro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This review was AI-assisted. I verified the four blocking findings by hand against the branch and the failing gate run before posting.

Start with the copied block, because it is the cheapest fix and it clears CI. Lines 26 to 72 of docs/sdks/ios/sparkscan/intro.md reproduce the body of docs/partials/intro/_about-sparkscan.mdx, and line 73 still renders that partial. So the published page shows every section twice. It is also the entire reason the gate is red: "simply" on 45, 46 and 48, "e.g." on 48, and "overlayed" on 32 are all copied partial text. Deleting lines 26 to 72 fixes the double render and clears all four Vale errors in one move. Please do not spend time rewording "simply" and "e.g." in that block, it should not be there at all. The gate only lints changed docs, which is why the identical sentences pass in the untouched partial.

Second, the two registries do not agree on the product key, so the "can never drift" claim does not hold yet. docs-schema.yml constrains product to an enum of URL slugs, and two of them differ from the products.json keys: the schema allows matrixscan where the registry has matrixscan-batch, and label-capture where the registry has smart-label-capture. On those two intro pages the lookup misses, the heading degrades to "Choose this product if you want to", and the "Available on" row disappears with no error. URL_PRODUCT_MAPPING in src/components/utils/frameworks.ts already maps exactly those two cases and is used by parseSdksRoute, so importing it is the fix. Schema values id-bolt, express, core and platform have no registry entry at all.

Third, a single-item user_intents written as a YAML scalar takes the site build down, which blocks every other PR in the repo. It is latent, not live, and cheap to close. Details inline.

On reuse, FW_TO_PJ is a third copy of a mapping that already exists as FRAMEWORK_MAPPING in src/components/utils/frameworks.ts, and its .NET and React Native keys do not match what pages actually set. Details inline. Related but not yours to fix: framework: netIos (31 pages) and framework: react (30 pages) are not in the schema's framework enum, and the singular framework field is not validated at all. That is worth a separate ticket.

One more reuse note. FeatureList already renders a supported-framework list off the same frameworks map with the same n/a filter, as links labelled {framework} v{version}. The version is the useful part, since "available since 6.8" answers a question that "iOS" does not. Worth matching that instead of inert chips. Minor: the PR calls this a matrix, but it renders as a flat list of what is supported with no signal about what is not, so a reader cannot tell whether Titanium is unsupported or just unlisted.

Two things I would keep as they are: the empty-state guard correctly returns null only when all three inputs are empty while still rendering partial data, and isSupported handles the registry's conventions properly, including barcode-sequence where the version is real but apiUrl is "n/a".

Nothing here needs a redesign. The component is worth landing once the copied block is gone and the product lookup goes through the existing mapping.

Comment on lines +26 to +72
SparkScan bundles multiple scanning features together and addresses many common challenges associated with scanning on smart devices. It is designed to be easily integrated into any application, and can be customized to fit your specific needs.

<ProductChooser frontMatter={frontMatter}/>

## UI Overview

The UI elements in SparkScan are intentionally minimalistic, meant to be overlayed on any application without the need to adapt the existing app while offering the best user experience.

Two main elements compose the UI:

![SparkScan UI](/img/sparkscan/features_web.png)

- **Camera preview**: A small camera preview that helps with aiming and shows scan feedback. When not in use, the camera preview is hidden. It can be expanded and hosts easy to access controls (zoom level, flash etc).
- **Trigger button**: A large-sized, semi-transparent floating button that users can drag to position it in the most ergonomic position. When not in use, the trigger button collapses to occupy less space.

There are additional UI elements available for displaying additional scanning modes, errors, or providing feedback to the user. These are described in the [Advanced](./advanced.md) section.

## Workflow Description

When SparkScan is started, the UI presents just the trigger button, collapsed. The user can move the trigger button by simply dragging it around: the position of the trigger button is remembered across sessions, so the user can place the button where it's the most comfortable to use.
To start scanning, the user can simply tap on it.

When the scanner is active, the mini preview is shown. The mini preview too can be placed anywhere in the view by simply pressing on it for a little while and then dragging it around. Also the position of the mini preview is remembered across sessions, so the user can place it where it prefers (e.g. not to cover an important information at the top of the app).

In the default configuration:
- Upon scan the user will receive audio/haptic feedback confirming the scan, and the mini preview will display the scanned barcode for a small amount of time before fading away.
- Tapping on the trigger button or the mini preview will restart immediately the scanner.

Upon completing the scanning process (or to interact with the customer app layer), the user can tap in any area outside the trigger button and the mini preview. This collapses the scanner button, going back to the initial state.

If instead of tapping on the trigger button the user taps and holds it pressed, he will be able to scan multiple barcodes in a row. The scanner will stop when the trigger button is released.

<p align="center">
<img src="/img/sparkscan/workflow-example.gif" alt="SparkScan Workflow" /><br></br>List building use case using SparkScan.
</p>

The default workflow just described has been carefully designed as a result of extensive user testing and customer feedback from the field.

But not all use-cases look the same, and your needs may differ for most users. That's why SparkScan comes with a set of options to configure the scanner and to best fit in the desired workflow. Check the [Workflow Options](./advanced.md#workflow-options) guide to discover more.

## Supported Symbologies

SparkScan supports all of the major symbologies listed here: [Barcode Symbologies](../barcode-symbologies.mdx).

## AI-Powered Features

SparkScan includes AI-powered scanning capabilities that enhance accuracy and user experience. These features automatically handle challenging scenarios such as avoiding unintentional scans, selecting barcodes in dense environments, scanning damaged barcodes with OCR fallback, and intelligently filtering duplicate scans. Learn more about these capabilities in our [AI-Powered Barcode Scanning](../ai-powered-barcode-scanning.md) guide.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block reproduces the body of docs/partials/intro/_about-sparkscan.mdx, and line 73 below still renders that partial. The published page will show two "UI Overview" sections, two "Workflow Description", two "Supported Symbologies", two "AI-Powered Features", the screenshot and the GIF twice, and Docusaurus will emit duplicate anchors (#ui-overview and #ui-overview-1).

This is also the whole reason the gate is red. "simply" on 45, 46 and 48, "e.g." on 48, and "overlayed" on 32 all appear identically in the partial. The gate only lints changed docs, so the partial's copy of the same sentences is never checked. Deleting 26 to 72 clears all four Vale errors and the cspell warning at once, with no rewording needed.

Nine framework pages share this partial: ios, android, web, cordova, capacitor, flutter, react-native, net/ios and net/android. Copying the body into one of them makes iOS the page that drifts when the partial changes, which is the opposite of what this PR is going for. The copy has already lost something: the partial builds its links from props.framework, and this version hardcodes ./advanced.md and ../barcode-symbologies.mdx, which only resolve because this happens to be the iOS page.

Suggested fix: keep the import and the <ProductChooser> tag, move the tag directly under the # About SparkScan heading, and let <AboutSparkScan framework="ios"/> render the rest. If some of this prose was meant to move out of the partial permanently, that is a reasonable change but a separate PR, with the partial edited to match.

? (fm.product[0] as string)
: (fm.product as string | undefined);

const product = (productsData as ProductEntry[]).find((p) => p.key === productKey);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lookup misses for two products, and the failure is silent.

docs-schema.yml constrains the product frontmatter field to an enum, and that enum is URL slugs rather than products.json keys. It allows matrixscan where the registry key is matrixscan-batch, and label-capture where the registry key is smart-label-capture. So on a MatrixScan Batch or Smart Label Capture intro page, the only schema-legal value returns undefined here. name then falls back to the literal 'this product' on the next line, so the heading publishes as "Choose this product if you want to", and frameworks ends up [], so the entire "Available on" row disappears. The gate passes, so nothing catches it except someone noticing the missing row by eye.

URL_PRODUCT_MAPPING in src/components/utils/frameworks.ts already maps exactly these two cases and is used by parseSdksRoute. Running productKey through it fixes both.

Separately, I would make an unresolved key loud rather than let it degrade. Publishing a heading with "this product" in it is worse than rendering nothing.

Comment on lines +92 to +93
const intents = (fm.user_intents as string[]) || [];
const notFor = (fm.not_for as string[]) || [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two casts will take the site build down if anyone writes a single intent the natural YAML way.

user_intents: scan one barcode at a time is a YAML scalar, so fm.user_intents arrives as a string. It is truthy, so the guard on line 108 passes, and intents.length is the character count rather than 0. Rendering then reaches intents.map(...) on line 118 and throws TypeError: intents.map is not a function. I confirmed that under node. A non-string item in not_for fails the same way at text.includes(p.name) on line 69.

That throws during SSR, so the Docusaurus build fails, which blocks every other PR in the repo until someone traces it back to one page's frontmatter.

Neither field is in docs-schema.yml, and that schema is additionalProperties: true, so the gate gives them no type checking at all. Two small fixes: guard the shape here with Array.isArray(...) and filter to strings, and add user_intents and not_for to docs-schema.yml as arrays of strings so the gate catches the mistake at push time. The schema is also the natural place to document what these fields are, since nothing in the repo does that yet.

This is latent rather than live. The one page using the component has correct frontmatter, which is why the build is currently green.

* It is fed entirely from single sources of truth, so the visible block can
* never drift from the machine-readable metadata:
* - `user_intents` and `not_for` come from the page's own frontmatter
* (the same fields that power GEO/AI routing).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot find anything in this repo that reads user_intents or not_for. Grepping the whole tree, both fields appear in exactly two files: this component, and the SparkScan iOS page this PR adds them to. Neither is in docs-schema.yml.

That does not make the design wrong, and moving this copy into frontmatter is defensible on its own terms. But as written the frontmatter is the visible content, so there is one producer and one consumer and nothing yet to drift from. This comment and the PR description both read as though the fields are already wired into something.

Is the GEO/AI routing consumer somewhere outside this repo? If so it is worth naming here. If it is planned rather than built, I would reword this to say so.

Comment on lines +44 to +48
const FW_TO_PJ: Record<string, string> = {
web: 'Web', ios: 'iOS', android: 'Android', 'react-native': 'React Native',
flutter: 'Flutter', cordova: 'Cordova', capacitor: 'Capacitor', titanium: 'Titanium',
'net/ios': '.NET iOS', 'net/android': '.NET Android',
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keys here do not match what pages actually set, and there is an existing map to reuse.

FRAMEWORK_MAPPING in src/components/utils/frameworks.ts is the same slug to display-name mapping, and SkillsCallout already keeps a third copy as FRAMEWORK_URL_PATH. Worth importing the shared one rather than adding a fourth.

On the keys, counting framework: values across docs/: 31 pages use netIos and 31 use netAndroid, plus 5 each of net-ios and net-android. None of those match the net/ios and net/android keys here. And 30 pages use framework: react against only 6 using react-native. So roughly 92 pages would miss this map.

A miss does not look like a miss. apiUrlFor falls through to the "any framework that supports it" fallback on line 59, which returns whichever framework comes first in products.json, and that is always iOS. I simulated web, netIos, netAndroid, react, net-ios and titanium against the real products.json: a not_for line naming MatrixScan Count resolves to /sdks/ios/matrixscan-count/intro in every single case.

The Web case is the one that bothers me most. MatrixScan Count is "Web": {"version": "n/a"}, so on the Web SparkScan page this would tell a Web reader to use a product with no Web support, and link them into the iOS docs to find that out. I would render plain text instead of a link when the reader's own framework does not support the recommended product.

None of this bites today, since ios maps correctly and it is the only page using the component.

frontMatter?: Record<string, unknown>;
}

export default function ProductChooser({ frontMatter = {} }: ProductChooserProps): JSX.Element | null {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider reading frontmatter with useDoc() instead of taking it as a prop. src/theme/DocItem/Layout/index.js already does const { frontMatter, toc } = useDoc();, so it is the established pattern in this repo.

The practical difference is rollout. With a prop, every page has to be wired by hand, and a page that writes <ProductChooser /> and forgets the prop renders nothing at all, with no error to notice. With useDoc() the usage is just <ProductChooser />. 70 pages import an _about-*.mdx intro partial, so that is the scale this has to survive.

<div className={styles.decide}>
{intents.length > 0 && (
<div className={`${styles.col} ${styles.good}`}>
<h4 className={styles.colTitle}>Choose {name} if you want to</h4>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two accessibility details. The aria-label on the <aside> is a good touch, keep it.

These <h4> elements, here and on line 126, sit on a page whose only preceding heading is the <h1>, so heading navigation jumps two levels. <h3> is the smallest fix, or drop the heading semantics entirely since the aside's label already names the region.

The chips on lines 139 to 145 are <span> elements inside a <span>, so a screen reader reads them as one run-on string with no boundaries or count. A <ul> with <li> children, with list styling reset in CSS, announces "list, 9 items" and reads them as separate entries, with the same visual result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants