From e92369ad6e361e81e9a40820b27700a701f47c4c Mon Sep 17 00:00:00 2001 From: Yevheniia Sorokina Date: Tue, 1 Sep 2026 11:50:20 +0100 Subject: [PATCH 1/4] design: one preview page for the three IA change proposals A designer needs to see ProductChooser, IntentNavigator and ErrorCodeFinder to review them - they ARE their interactions, so a screenshot shows almost nothing and a .tsx shows less. The three live on three different refs, so there was no single thing to look at. This puts all three on one route, /component-preview, so the PR preview gives one URL in real site chrome: real tokens, real fonts, real spacing, and a working light/dark toggle. It is a src/pages route, NOT a docs page, so it is in no sidebar, no framework tree and no docs version, and it is noindex. Nothing here changes the documentation; the preview is removed when this PR closes. The components are copied unmodified, via `git checkout -- ` rather than cherry-pick, so none of the unrelated work on those branches (a package-lock deletion, the SkillsCallout rewrite, Vale rules, docs-gate scripts) comes along: ProductChooser docs/product-chooser (PR #417) IntentNavigator docs/phase-e-intent-navigator (+ src/data/intents.json) ErrorCodeFinder 0fd2e0a4 on docs/sparkscan-ios-rewrite (+ src/data/troubleshooting-errors.json) ProductChooser normally reads the host page's frontmatter; the page passes the same shape by hand, copied verbatim from docs/sdks/ios/sparkscan/intro.md, and renders it three times - SparkScan on iOS, the same product on Web to show the availability chips and outgoing links following the reader's framework, and MatrixScan Pick for a different block length. ErrorCodeFinder still carries its SAMPLE- placeholder entries. The page says so in a callout, so the reviewer looks at layout and interaction rather than wording. Verified: tsc --noEmit clean, and `docusaurus build` succeeds with the page rendering all three components server-side. Co-Authored-By: Claude Opus 5 (1M context) --- src/components/ErrorCodeFinder/index.tsx | 145 ++++++++++++ .../ErrorCodeFinder/styles.module.css | 114 ++++++++++ src/components/IntentNavigator/index.tsx | 127 +++++++++++ .../IntentNavigator/styles.module.css | 92 ++++++++ src/components/ProductChooser/index.tsx | 150 +++++++++++++ .../ProductChooser/styles.module.css | 102 +++++++++ src/data/intents.json | 74 +++++++ src/data/troubleshooting-errors.json | 109 +++++++++ src/pages/component-preview.tsx | 209 ++++++++++++++++++ 9 files changed, 1122 insertions(+) create mode 100644 src/components/ErrorCodeFinder/index.tsx create mode 100644 src/components/ErrorCodeFinder/styles.module.css create mode 100644 src/components/IntentNavigator/index.tsx create mode 100644 src/components/IntentNavigator/styles.module.css create mode 100644 src/components/ProductChooser/index.tsx create mode 100644 src/components/ProductChooser/styles.module.css create mode 100644 src/data/intents.json create mode 100644 src/data/troubleshooting-errors.json create mode 100644 src/pages/component-preview.tsx diff --git a/src/components/ErrorCodeFinder/index.tsx b/src/components/ErrorCodeFinder/index.tsx new file mode 100644 index 0000000000..54c77c64c1 --- /dev/null +++ b/src/components/ErrorCodeFinder/index.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import Head from '@docusaurus/Head'; +import Link from '@docusaurus/Link'; +import errorData from '@site/src/data/troubleshooting-errors.json'; +import styles from './styles.module.css'; + +/** + * ErrorCodeFinder — the paste-to-filter troubleshooting index (backlog #5). + * + * A reader who hits an SDK error pastes the console message here and the list + * narrows to the matching entry with its cause and fix. Typing a fragment works + * too. A get-started page links here with the input auto-focused so the reader + * can paste immediately; a `?q=` URL param pre-filters (e.g. a deep link). + * + * LLM / GEO retrieval: + * - The full entry list is rendered SERVER-SIDE (not behind BrowserOnly), so + * every message/cause/fix is in the static HTML for crawlers, Algolia, and + * our knowledge-extractor. The search box is progressive enhancement: + * with no JS every entry still shows. + * - Each entry has a stable anchor id, so an answer can cite + * /troubleshooting/#. + * - We emit schema.org FAQPage JSON-LD built from the same catalog, so + * search engines and LLMs get a machine-readable question→answer map. + */ + +interface ErrorEntry { + id: string; + code: string; + area: string; + sample?: boolean; + title: string; + message: string; + match?: string[]; + cause: string; + fix: string; + link?: string; + linkLabel?: string; +} + +const ENTRIES = ((errorData as { entries?: ErrorEntry[] }).entries || []) as ErrorEntry[]; + +function normalize(s: string | undefined): string { + return (s || '').toLowerCase().replace(/\s+/g, ' ').trim(); +} + +/** Match either way: a typed fragment appears in a field, OR the reader pasted + * a whole console line that CONTAINS one of the entry's invariant substrings + * (their own key/app id in the pasted text is ignored this way). */ +function matches(entry: ErrorEntry, nq: string): boolean { + if (!nq) return true; + const fields = [entry.title, entry.code, entry.message, entry.cause, ...(entry.match || [])].map( + normalize, + ); + const forwardHit = fields.some((h) => h && h.includes(nq)); + const invariants = [entry.message, ...(entry.match || [])].map(normalize).filter(Boolean); + const pasteHit = nq.length >= 12 && invariants.some((h) => nq.includes(h)); + return forwardHit || pasteHit; +} + +function buildJsonLd() { + return { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: ENTRIES.map((e) => ({ + '@type': 'Question', + name: e.message, + acceptedAnswer: { + '@type': 'Answer', + text: `Cause: ${e.cause} Fix: ${e.fix}`, + }, + })), + }; +} + +export default function ErrorCodeFinder(): JSX.Element { + const [q, setQ] = React.useState(''); + const inputRef = React.useRef(null); + + React.useEffect(() => { + try { + const fromQuery = new URLSearchParams(window.location.search).get('q') || ''; + if (fromQuery) setQ(fromQuery); + } catch { + /* SSR / no window */ + } + inputRef.current?.focus(); + }, []); + + const nq = normalize(q); + const visible = ENTRIES.filter((e) => matches(e, nq)); + + return ( +
+ + + + + setQ(e.target.value)} + placeholder="Paste your error message here, or start typing…" + aria-label="Search Scandit error messages" + spellCheck={false} + autoComplete="off" + /> +

+ {q ? `${visible.length} of ${ENTRIES.length} matching` : `${ENTRIES.length} known issues`} +

+ + {visible.length === 0 && ( +

+ No match yet. Try pasting a shorter part of the message, or search the full documentation. +

+ )} + +
    + {visible.map((e) => ( +
  • +
    + {e.area} + {e.sample && SAMPLE} + {e.code} +
    +

    {e.title}

    +
    {e.message}
    +

    + Cause: {e.cause} +

    +

    + Fix: {e.fix} +

    + {e.link && ( + + {e.linkLabel || 'Learn more'} → + + )} +
  • + ))} +
+
+ ); +} diff --git a/src/components/ErrorCodeFinder/styles.module.css b/src/components/ErrorCodeFinder/styles.module.css new file mode 100644 index 0000000000..7105438ff3 --- /dev/null +++ b/src/components/ErrorCodeFinder/styles.module.css @@ -0,0 +1,114 @@ +.finder { + margin: 1.5rem 0 2rem; +} + +.search { + width: 100%; + box-sizing: border-box; + font-size: 1rem; + padding: 0.7rem 0.9rem; + border: 2px solid var(--ifm-color-emphasis-300); + border-radius: 10px; + background: var(--ifm-background-surface-color); + color: var(--ifm-font-color-base); +} + +.search:focus { + outline: none; + border-color: var(--ifm-color-primary); +} + +.count { + margin: 0.5rem 0 1rem; + font-size: 0.8rem; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--ifm-color-emphasis-600); +} + +.empty { + padding: 1rem 1.1rem; + border: 1px dashed var(--ifm-color-emphasis-300); + border-radius: 10px; + color: var(--ifm-color-emphasis-700); +} + +.list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.card { + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 10px; + padding: 1rem 1.25rem 1.1rem; + background: var(--ifm-background-surface-color); + scroll-margin-top: 5rem; +} + +.head { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.4rem; +} + +.area { + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--ifm-color-emphasis-600); +} + +.sampleTag { + font-size: 0.66rem; + font-weight: 800; + letter-spacing: 0.08em; + padding: 0.1rem 0.4rem; + border-radius: 4px; + background: var(--ifm-color-warning-contrast-background); + color: var(--ifm-color-warning-darker); + border: 1px solid var(--ifm-color-warning); +} + +.code { + margin-left: auto; + font-size: 0.74rem; + color: var(--ifm-color-emphasis-600); +} + +.title { + margin: 0 0 0.5rem; + font-size: 1.05rem; +} + +.message { + margin: 0 0 0.7rem; + padding: 0.5rem 0.8rem; + border-left: 3px solid var(--ifm-color-danger); + background: var(--ifm-color-emphasis-100); + font-family: var(--ifm-font-family-monospace); + font-size: 0.85rem; + color: var(--ifm-font-color-base); + border-radius: 0 6px 6px 0; +} + +.line { + margin: 0.3rem 0; + font-size: 0.92rem; + line-height: 1.5; +} + +.link { + display: inline-block; + margin-top: 0.5rem; + font-size: 0.9rem; + font-weight: 600; +} diff --git a/src/components/IntentNavigator/index.tsx b/src/components/IntentNavigator/index.tsx new file mode 100644 index 0000000000..ea08ea6865 --- /dev/null +++ b/src/components/IntentNavigator/index.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import intents from '@site/src/data/intents.json'; +import productsData from '@site/src/data/products.json'; +import styles from './styles.module.css'; + +/** + * IntentNavigator — the "Start here / I want to…" entry point (Phase E MVP). + * + * Renders the same intent taxonomy as the Unified-Signup PRD product quiz + * (scan type → task → delivery → product), so the docs and the signup flow + * route the same intent to the same product. Each option links to that + * product's docs, upgraded to the reader's currently-selected framework when + * that framework actually has the product (checked against products.json); + * otherwise it keeps the verified web landing. Additive and URL-preserving — + * no existing route changes. + */ + +interface Option { + product: string; + name: string; + delivery: string; + href: string; +} +interface Goal { + task: string; + context: string; + options: Option[]; +} +interface ScanType { + id: string; + label: string; + hint: string; + goals: Goal[]; +} +interface ProductEntry { + key: string; + frameworks?: Record; +} + +// Raw ?framework= value (homepage selector or path style) → { display name in +// products.json, path segment in /sdks// }. +const FRAMEWORKS: Record = { + ios: { name: 'iOS', seg: 'ios' }, + android: { name: 'Android', seg: 'android' }, + web: { name: 'Web', seg: 'web' }, + cordova: { name: 'Cordova', seg: 'cordova' }, + capacitor: { name: 'Capacitor', seg: 'capacitor' }, + flutter: { name: 'Flutter', seg: 'flutter' }, + 'react-native': { name: 'React Native', seg: 'react-native' }, + react: { name: 'React Native', seg: 'react-native' }, + netios: { name: '.NET iOS', seg: 'net/ios' }, + 'net-ios': { name: '.NET iOS', seg: 'net/ios' }, + 'net/ios': { name: '.NET iOS', seg: 'net/ios' }, + netandroid: { name: '.NET Android', seg: 'net/android' }, + 'net-android': { name: '.NET Android', seg: 'net/android' }, + 'net/android': { name: '.NET Android', seg: 'net/android' }, +}; + +function currentFramework(): { name: string; seg: string } | null { + if (typeof window === 'undefined') return null; + let raw = ''; + try { + raw = + new URLSearchParams(window.location.search).get('framework') || + window.localStorage.getItem('framework') || + ''; + } catch { + raw = ''; + } + return FRAMEWORKS[raw.trim().toLowerCase()] || null; +} + +function productOnFramework(productKey: string, displayName: string): boolean { + const p = (productsData as ProductEntry[]).find((x) => x.key === productKey); + const entry = p?.frameworks?.[displayName]; + const v = String(entry?.version ?? '').trim().toLowerCase(); + return Boolean(entry) && v !== '' && v !== 'n/a'; +} + +export default function IntentNavigator(): JSX.Element { + const [fw, setFw] = React.useState<{ name: string; seg: string } | null>(null); + React.useEffect(() => setFw(currentFramework()), []); + + const resolve = (opt: Option): string => { + // Only SDK web landings are framework-adjustable; hosted links are as-is. + if (!fw || fw.seg === 'web' || !opt.href.startsWith('/sdks/web/')) return opt.href; + return productOnFramework(opt.product, fw.name) + ? opt.href.replace('/sdks/web/', `/sdks/${fw.seg}/`) + : opt.href; + }; + + return ( +
+ {fw && fw.seg !== 'web' && ( +

+ Showing links for {fw.name}. Pick a framework on the{' '} + home page to change it. +

+ )} + {(intents.scanTypes as ScanType[]).map((st) => ( +
+

{st.label}

+

{st.hint}

+
+ {st.goals.map((g, gi) => ( +
+
+ {g.task} + {g.context} +
+
+ {g.options.map((o, oi) => ( + + {o.name} + {o.delivery} + + ))} +
+
+ ))} +
+
+ ))} +
+ ); +} diff --git a/src/components/IntentNavigator/styles.module.css b/src/components/IntentNavigator/styles.module.css new file mode 100644 index 0000000000..1c8466caa9 --- /dev/null +++ b/src/components/IntentNavigator/styles.module.css @@ -0,0 +1,92 @@ +.nav { + margin: 1rem 0 2rem; +} + +.fwNote { + font-size: 0.85rem; + color: var(--ifm-color-emphasis-700); + background: var(--ifm-color-emphasis-100); + border-radius: 6px; + padding: 0.5rem 0.75rem; + margin-bottom: 1.25rem; +} + +.scanType { + margin-bottom: 1.75rem; +} + +.stTitle { + margin: 0 0 0.15rem; + font-size: 1.15rem; +} + +.stHint { + margin: 0 0 0.9rem; + font-size: 0.85rem; + color: var(--ifm-color-emphasis-600); +} + +.goals { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.goal { + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 10px; + padding: 0.85rem 1rem 0.95rem; + background: var(--ifm-background-surface-color); +} + +.goalHead { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.5rem 0.75rem; + margin-bottom: 0.65rem; +} + +.goalTask { + font-weight: 700; + font-size: 0.98rem; +} + +.goalContext { + font-size: 0.8rem; + color: var(--ifm-color-emphasis-600); +} + +.options { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); + gap: 0.6rem; +} + +.option { + display: flex; + flex-direction: column; + gap: 0.15rem; + padding: 0.6rem 0.75rem; + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 8px; + text-decoration: none; + background: var(--ifm-background-color); + transition: border-color 0.15s ease, background 0.15s ease; +} + +.option:hover { + border-color: var(--ifm-color-primary); + background: var(--ifm-color-primary-contrast-background); + text-decoration: none; +} + +.optName { + font-weight: 650; + color: var(--ifm-color-primary); +} + +.optDelivery { + font-size: 0.8rem; + color: var(--ifm-color-emphasis-700); +} diff --git a/src/components/ProductChooser/index.tsx b/src/components/ProductChooser/index.tsx new file mode 100644 index 0000000000..4cf95ff186 --- /dev/null +++ b/src/components/ProductChooser/index.tsx @@ -0,0 +1,150 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import productsData from '@site/src/data/products.json'; +import styles from './styles.module.css'; + +/** + * ProductChooser — a "Choose this if / Consider another product if" decision + * block plus a supported-frameworks matrix, for product-overview pages. + * + * 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). + * - the supported-frameworks matrix comes from `src/data/products.json` + * (the product registry the Agent Skills pages already use). + * + * Drop it on any product-overview page and pass the page's frontmatter, which + * Docusaurus makes available in MDX scope as `frontMatter`: + * + * + * + * Renders nothing if `product`, `user_intents`, and `not_for` are all unset. + */ + +interface FrameworkEntry { + version?: string; + apiUrl?: string; +} + +interface ProductEntry { + key: string; + name: string; + frameworks?: Record | string[]; +} + +// products.json marks a framework the product does NOT support with version "n/a" +// (e.g. SparkScan on Titanium). Treat those as unsupported. +function isSupported(entry: FrameworkEntry): boolean { + const v = String(entry?.version ?? '').trim().toLowerCase(); + return v !== '' && v !== 'n/a'; +} + +// Map the page's `framework` frontmatter value to the display key products.json uses. +const FW_TO_PJ: Record = { + 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', +}; + +// Best doc URL for a product on the current framework: the framework's own page, +// else Web, else any framework the product supports. All from products.json (SSOT). +function apiUrlFor(product: ProductEntry, fw: string): string | null { + const fks = product.frameworks; + if (!fks || Array.isArray(fks)) return null; + for (const name of [FW_TO_PJ[fw], 'Web']) { + const e = name ? fks[name] : undefined; + if (e && isSupported(e) && e.apiUrl && e.apiUrl !== 'n/a') return e.apiUrl; + } + const any = Object.values(fks).find((e) => isSupported(e) && e.apiUrl && e.apiUrl !== 'n/a'); + return any ? (any.apiUrl as string) : null; +} + +// If a `not_for` line names another product (e.g. "…— use MatrixScan Count"), +// turn that product name into a framework-aware link to its About page. Matches +// the LONGEST product name present so "MatrixScan Count" wins over "MatrixScan". +// Falls back to plain text when there's no match or no resolvable URL. +function renderNotFor(text: string, fw: string): React.ReactNode { + const match = (productsData as ProductEntry[]) + .filter((p) => p.name && text.includes(p.name)) + .sort((a, b) => b.name.length - a.name.length)[0]; + if (!match) return text; + const url = apiUrlFor(match, fw); + if (!url) return text; + const i = text.indexOf(match.name); + return ( + <> + {text.slice(0, i)} + {match.name} + {text.slice(i + match.name.length)} + + ); +} + +interface ProductChooserProps { + frontMatter?: Record; +} + +export default function ProductChooser({ frontMatter = {} }: ProductChooserProps): JSX.Element | null { + const fm = frontMatter; + const fw = (fm.framework as string) || 'web'; + + const intents = (fm.user_intents as string[]) || []; + const notFor = (fm.not_for as string[]) || []; + const productKey = Array.isArray(fm.product) + ? (fm.product[0] as string) + : (fm.product as string | undefined); + + const product = (productsData as ProductEntry[]).find((p) => p.key === productKey); + const name = product?.name || 'this product'; + const frameworks = product?.frameworks + ? Array.isArray(product.frameworks) + ? product.frameworks + : Object.entries(product.frameworks) + .filter(([, entry]) => isSupported(entry)) + .map(([name]) => name) + : []; + + if (!intents.length && !notFor.length && !frameworks.length) return null; + + return ( + + ); +} diff --git a/src/components/ProductChooser/styles.module.css b/src/components/ProductChooser/styles.module.css new file mode 100644 index 0000000000..8deb6f42e2 --- /dev/null +++ b/src/components/ProductChooser/styles.module.css @@ -0,0 +1,102 @@ +.chooser { + margin: 1.5rem 0 2rem; + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 10px; + overflow: hidden; + background: var(--ifm-background-surface-color); +} + +.decide { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1px; + background: var(--ifm-color-emphasis-200); +} + +@media (max-width: 700px) { + .decide { + grid-template-columns: 1fr; + } +} + +.col { + background: var(--ifm-background-surface-color); + padding: 1rem 1.25rem 1.1rem; +} + +.colTitle { + margin: 0 0 0.6rem; + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.02em; + text-transform: uppercase; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.colTitle::before { + content: ''; + width: 0.7rem; + height: 0.7rem; + border-radius: 50%; + flex: none; +} + +.good .colTitle { + color: var(--ifm-color-success-darker); +} +.good .colTitle::before { + background: var(--ifm-color-success); +} + +.bad .colTitle { + color: var(--ifm-color-warning-darker); +} +.bad .colTitle::before { + background: var(--ifm-color-warning); +} + +.col ul { + margin: 0; + padding-left: 1.1rem; +} + +.col li { + margin: 0.35rem 0; + font-size: 0.92rem; + line-height: 1.45; +} + +.matrix { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem 0.6rem; + padding: 0.8rem 1.25rem; + border-top: 1px solid var(--ifm-color-emphasis-200); +} + +.matrixLabel { + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--ifm-color-emphasis-600); +} + +.chips { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} + +.chip { + font-size: 0.78rem; + font-weight: 600; + padding: 0.15rem 0.55rem; + border-radius: 999px; + background: var(--ifm-color-emphasis-100); + color: var(--ifm-color-emphasis-800); + border: 1px solid var(--ifm-color-emphasis-200); +} diff --git a/src/data/intents.json b/src/data/intents.json new file mode 100644 index 0000000000..95ffeb7277 --- /dev/null +++ b/src/data/intents.json @@ -0,0 +1,74 @@ +{ + "_comment": "Intent taxonomy for the docs 'Start here' navigator (Phase E MVP). Mirrors the Unified-Signup PRD product quiz (scan type -> task -> delivery -> product) so the docs entry point and the signup quiz recommend the same product for the same intent. `href` is the verified web landing for each product; the IntentNavigator upgrades it to the reader's selected framework when that framework has the product. Keep this in sync with the PRD decision matrix.", + "scanTypes": [ + { + "id": "barcodes", + "label": "Barcodes", + "hint": "QR, EAN-13, Code 128, Data Matrix, and more", + "goals": [ + { + "task": "Scan one item at a time", + "context": "point of sale, lookup, list-building", + "options": [ + { "product": "sparkscan", "name": "SparkScan", "delivery": "Ready-made scanning UI", "href": "/sdks/web/sparkscan/intro" }, + { "product": "barcode-capture", "name": "Barcode Capture", "delivery": "Full control (SDK)", "href": "/sdks/web/barcode-capture/get-started" }, + { "product": "express", "name": "Scandit Express", "delivery": "No code", "href": "/hosted/express/overview" } + ] + }, + { + "task": "Scan and tally many quickly", + "context": "stocktake, bulk scanning", + "options": [ + { "product": "matrixscan-batch", "name": "MatrixScan Batch", "delivery": "Full control (SDK)", "href": "/sdks/web/matrixscan/intro" }, + { "product": "express", "name": "Scandit Express", "delivery": "No code", "href": "/hosted/express/overview" } + ] + }, + { + "task": "Count and check against an expected list", + "context": "proof of delivery, receiving", + "options": [ + { "product": "matrixscan-count", "name": "MatrixScan Count", "delivery": "Full control (SDK)", "href": "/sdks/web/matrixscan-count/intro" }, + { "product": "express", "name": "Scandit Express", "delivery": "No code", "href": "/hosted/express/overview" } + ] + }, + { + "task": "Find a specific item among many", + "context": "order picking", + "options": [ + { "product": "matrixscan-find", "name": "MatrixScan Find", "delivery": "Guided AR search (SDK)", "href": "/sdks/web/matrixscan-find/intro" }, + { "product": "matrixscan-pick", "name": "MatrixScan Pick", "delivery": "Pick workflow (SDK)", "href": "/sdks/web/matrixscan-pick/intro" } + ] + } + ] + }, + { + "id": "labels", + "label": "Text and barcodes on labels", + "hint": "expiry dates, prices, lot numbers, addresses", + "goals": [ + { + "task": "Capture fields from a label", + "context": "expiry, price, lot number, address", + "options": [ + { "product": "smart-label-capture", "name": "Smart Label Capture", "delivery": "Full control (SDK)", "href": "/sdks/web/label-capture/intro" } + ] + } + ] + }, + { + "id": "id", + "label": "Identity documents", + "hint": "passports, driver's licenses, ID cards", + "goals": [ + { + "task": "Scan and extract ID data", + "context": "onboarding, identity and age checks", + "options": [ + { "product": "id-capture", "name": "ID Capture", "delivery": "Full control (SDK)", "href": "/sdks/web/id-capture/intro" }, + { "product": "id-bolt", "name": "ID Bolt", "delivery": "No code (web)", "href": "/hosted/id-bolt/getting-started" } + ] + } + ] + } + ] +} diff --git a/src/data/troubleshooting-errors.json b/src/data/troubleshooting-errors.json new file mode 100644 index 0000000000..65f3d2598d --- /dev/null +++ b/src/data/troubleshooting-errors.json @@ -0,0 +1,109 @@ +{ + "_comment": "DEMO SHELL DATA. Every entry below is an ILLUSTRATIVE SAMPLE with a SAMPLE-* code, not a real Scandit error. Real content (literal console messages + verified fixes) will be supplied from the support/licensing ticket data and replace these. The `match` array holds the invariant substrings used for paste-to-filter matching.", + "entries": [ + { + "id": "sample-license-placeholder", + "code": "SAMPLE-LICENSE-01", + "area": "Licensing & activation", + "sample": true, + "title": "License key placeholder was never replaced", + "message": "A valid license key is required. The provided key looks like the documentation placeholder.", + "match": [ + "enter your scandit license key here", + "a valid license key is required", + "no license key" + ], + "cause": "The code still contains the docs placeholder `-- ENTER YOUR SCANDIT LICENSE KEY HERE --` instead of a real key.", + "fix": "Paste your real key into the license field at the top of the get-started page (or set it directly in the DataCaptureContext call). Get a free key from the trial dashboard if you don't have one.", + "link": "/sdks/ios/sparkscan/get-started/", + "linkLabel": "Go to the license step" + }, + { + "id": "sample-license-invalid", + "code": "SAMPLE-LICENSE-02", + "area": "Licensing & activation", + "sample": true, + "title": "License key is invalid or malformed", + "message": "License validation failed: the provided key could not be parsed.", + "match": [ + "license validation failed", + "could not be parsed", + "invalid license key", + "key is invalid" + ], + "cause": "The key string is truncated, has extra whitespace/line breaks, or was copied only partially.", + "fix": "Re-copy the entire key from the dashboard as a single line with no wrapping, and make sure no characters were dropped when pasting.", + "link": "/sdks/ios/sparkscan/get-started/", + "linkLabel": "Go to the license step" + }, + { + "id": "sample-license-expired", + "code": "SAMPLE-LICENSE-03", + "area": "Licensing & activation", + "sample": true, + "title": "License has expired", + "message": "This license key has expired.", + "match": [ + "license key has expired", + "license expired", + "expired" + ], + "cause": "The trial or subscription tied to this key has ended.", + "fix": "Generate a new key or extend your trial in the dashboard, then replace the key in your app.", + "link": "/sdks/ios/sparkscan/get-started/", + "linkLabel": "Go to the license step" + }, + { + "id": "sample-license-bundle-mismatch", + "code": "SAMPLE-LICENSE-04", + "area": "Licensing & activation", + "sample": true, + "title": "License is not valid for this application", + "message": "The license key is not valid for this application's bundle identifier.", + "match": [ + "not valid for this application", + "bundle identifier", + "app id mismatch", + "package name" + ], + "cause": "The key was issued for a different bundle ID / package name than the app it is running in.", + "fix": "Create a key for the current bundle identifier (or add this bundle ID to the existing key's allowed apps in the dashboard).", + "link": "/sdks/ios/sparkscan/get-started/", + "linkLabel": "Go to the license step" + }, + { + "id": "sample-license-feature", + "code": "SAMPLE-LICENSE-05", + "area": "Licensing & activation", + "sample": true, + "title": "Enabled feature is not included in the license", + "message": "The license does not include the enabled functionality.", + "match": [ + "does not include the enabled", + "feature not licensed", + "functionality is not licensed" + ], + "cause": "The code enables a mode (for example a specific scanning capability) that this key is not entitled to.", + "fix": "Either disable the unlicensed mode, or request a key that includes the feature you need.", + "link": "/sdks/ios/sparkscan/get-started/", + "linkLabel": "Go to the license step" + }, + { + "id": "sample-camera-permission", + "code": "SAMPLE-RUNTIME-01", + "area": "Camera & runtime", + "sample": true, + "title": "Camera permission was denied", + "message": "Camera access denied. The scanner cannot start without camera permission.", + "match": [ + "camera access denied", + "camera permission", + "cannot start without camera" + ], + "cause": "The app did not request, or the user declined, camera permission — so the preview never opens.", + "fix": "Add the camera usage description to your app configuration and request permission before starting the scanner. On device, re-enable camera access for the app in system settings.", + "link": "/sdks/ios/sparkscan/get-started/", + "linkLabel": "Back to get-started" + } + ] +} diff --git a/src/pages/component-preview.tsx b/src/pages/component-preview.tsx new file mode 100644 index 0000000000..8974b74384 --- /dev/null +++ b/src/pages/component-preview.tsx @@ -0,0 +1,209 @@ +import React from "react"; +import Head from "@docusaurus/Head"; +import Layout from "@theme/Layout"; +import ProductChooser from "@site/src/components/ProductChooser"; +import IntentNavigator from "@site/src/components/IntentNavigator"; +import ErrorCodeFinder from "@site/src/components/ErrorCodeFinder"; + +/** + * /component-preview - a single page rendering the three IA change proposals so a + * designer can review them in real site chrome (real tokens, fonts, spacing, and a + * working light/dark toggle) from ONE link. + * + * Deliberately a src/pages route, NOT a docs page: it therefore appears in no + * sidebar, in no framework tree, and in no docs version. It is a review surface, + * not documentation. `noindex` keeps it out of search engines, and the PR preview + * that hosts it is deleted automatically when the PR closes. + * + * The components are unmodified - copied from their own branches so the designer + * sees exactly what was built: + * ProductChooser docs/product-chooser (PR #417) + * IntentNavigator docs/phase-e-intent-navigator + * ErrorCodeFinder 0fd2e0a4 on docs/sparkscan-ios-rewrite + */ + +// ProductChooser reads the page's own frontmatter when it sits on a docs page. +// Here we pass the same shape by hand, copied verbatim from the frontmatter of +// docs/sdks/ios/sparkscan/intro.md on the docs/product-chooser branch, so the +// rendering matches the real page rather than an invented example. +const SPARKSCAN_IOS = { + framework: "ios", + product: "sparkscan", + user_intents: [ + "scan one barcode at a time on iOS", + "add a ready-made scanning UI without building your own", + "scan items one by one to build a list (inventory, receiving)", + ], + not_for: [ + "scan and count many barcodes at once — use MatrixScan Count", + "build a fully custom scanning UI — use Barcode Capture", + ], +}; + +// The same product on Web, to show that the "Available on" chips and the +// framework-aware anti-use-case links change with the reader's framework. +const SPARKSCAN_WEB = { ...SPARKSCAN_IOS, framework: "web" }; + +// A second product, so the designer sees the block at a different length and with +// an anti-use-case pointing at a different product. +const MATRIXSCAN_PICK_IOS = { + framework: "ios", + product: "matrixscan-pick", + user_intents: [ + "guide a picker to the right items for an order", + "show what to pick and confirm each item on screen", + ], + not_for: [ + "just count items against a list — use MatrixScan Count", + "find one specific item — use MatrixScan Find", + ], +}; + +function Section({ + n, + title, + branch, + intent, + children, + note, +}: { + n: string; + title: string; + branch: string; + intent: string; + children: React.ReactNode; + note?: React.ReactNode; +}) { + return ( +
+

+ {n}. {title} +

+

+ Serves: {intent} +

+

+ {branch} +

+ {note && ( +
+ {note} +
+ )} +
{children}
+
+ ); +} + +export default function ComponentPreview(): JSX.Element { + return ( + + + + + +
+

Component preview

+

+ Three proposed documentation components, on one page, in the real site so + colours, fonts and spacing are exactly what a reader would get.{" "} + Try the light/dark toggle in the header — all three have to + work in both. +

+

+ This page is not part of the documentation: it is in no sidebar, no framework + tree and no docs version, and it is noindex. It disappears when + the pull request that hosts this preview is closed. +

+ +
+ +
+ +
+ +
+

SparkScan, reader on iOS

+ + +

+ The same product, reader on Web — the availability chips and the outgoing + links follow the reader’s framework +

+ + +

+ A different product, so the block is a different length +

+ +
+ +
+ The entries below are placeholder data (each tagged{" "} + SAMPLE). They demonstrate the interaction only and are not real + Scandit error messages. The verified catalogue exists and will replace them. + Please review the layout and interaction, not the wording. + + } + > + +
+ +
+ +

What we would value your view on

+
    +
  • + Does each block read as one clear decision, or does it compete with the page + around it? +
  • +
  • + The “choose this / consider another” pair — is the contrast strong enough + without looking like a warning? +
  • +
  • + Dark mode: all three use existing tokens from{" "} + src/css/custom.scss. Anything that breaks or reads as low + contrast? +
  • +
  • + The finder’s search field — is it obviously a paste target rather + than a normal site search? +
  • +
  • + Mobile widths: the availability chips and the three-step navigator are the + most likely to wrap awkwardly. +
  • +
+
+
+ ); +} From aa7631d91b41fc1420955286fc6f890f0780705b Mon Sep 17 00:00:00 2001 From: Yevheniia Sorokina Date: Tue, 1 Sep 2026 12:18:35 +0100 Subject: [PATCH 2/4] design: port the full Phase E demo into IntentNavigator The shipped component was a reduced port of the demo: a flat taxonomy with no platform picker, no steps, no expert shortcut, no availability filtering and no badges. The screenshots of the demo are the intended design, so this brings the whole thing across, grouped exactly as the demo grouped it: 1. platform bar 11 platforms, remembered between visits 2. expert shortcut "Already know your product? Jump straight in ->" 3. three numbered steps with progressive disclosure, plus "start over" 4. "Why this matters" docs <-> signup parity note 5. the Linux caveat, and a provenance footer Also adds the 5th barcode task the demo has and the component lacked - "Show custom info on each item" -> MatrixScan AR - plus Scandit Express on the goals where the demo offers it, and delivery badges (ready-made UI / full-control SDK / no code / no code (web)). Styles are now a module SCSS in the house idiom: `@import '../../css/_mixins'`, a :root block of component tokens, nesting, and the `dark`/`transition`/ `flex-row` mixins - matching src/css/components/*.scss and the existing card.module.scss. The demo's private hex palette is gone; every colour is a token custom.scss already defines, and those tokens ALREADY flip for dark (--light-black, --light-blue-100, --light-gray-100, --light-blue-60, --nav-border, --frameworksBgLight), so both themes come for free. Only the four badges needed explicit dark values, because the soft green/blue tokens are not among the ones redeclared for dark. Three things done differently from the demo, deliberately: - Availability is COMPUTED from products.json, not copied. The demo hard-coded a PROD table "derived verbatim from products.json"; a copy drifts the moment the registry changes. Only what is genuinely absent from that file is declared here: the hosted products, and Linux. - Disclosure is CSS, not conditional rendering. The server already renders the stepped UI, so there is no flash of a different layout before hydration, and the expert grid stays mounted-but-hidden so every product link is in the static HTML for search and the retrieval layer. - The demo's Linux note never appeared: it set `el("linuxNote").hidden` on an element that does not exist, throwing a TypeError on init and on every platform change. Here it is a conditional render, so it works. start.mdx takes the demo's headline and framing copy. The "already know your product" shortcut now lives in the component, where it belongs, rather than as a page-level tip. Verified: tsc --noEmit clean; docusaurus build succeeds; the built /start HTML contains the platform bar, the shortcut, step 1, the parity note and the product links. Co-Authored-By: Claude Opus 5 (1M context) --- docs/sdks/ios/sparkscan/intro.md | 57 ++ docs/start.mdx | 29 + docs/troubleshooting.md | 41 ++ src/components/IntentNavigator/index.tsx | 418 +++++++++++--- .../IntentNavigator/styles.module.css | 92 ---- .../IntentNavigator/styles.module.scss | 520 ++++++++++++++++++ src/data/intents.json | 54 +- src/pages/component-preview.tsx | 205 ++++--- 8 files changed, 1123 insertions(+), 293 deletions(-) create mode 100644 docs/start.mdx create mode 100644 docs/troubleshooting.md delete mode 100644 src/components/IntentNavigator/styles.module.css create mode 100644 src/components/IntentNavigator/styles.module.scss diff --git a/docs/sdks/ios/sparkscan/intro.md b/docs/sdks/ios/sparkscan/intro.md index 0c4784a07f..da671845b4 100644 --- a/docs/sdks/ios/sparkscan/intro.md +++ b/docs/sdks/ios/sparkscan/intro.md @@ -5,12 +5,69 @@ sidebar_position: 1 displayed_sidebar: iosSidebar pagination_prev: null framework: ios +product: sparkscan +user_intents: + - "scan one barcode at a time on iOS" + - "add a ready-made scanning UI without building your own" + - "scan items one by one to build a list (inventory, receiving)" +not_for: + - "scan and count many barcodes at once — use MatrixScan Count" + - "build a fully custom scanning UI — use Barcode Capture" keywords: - ios --- +import ProductChooser from '@site/src/components/ProductChooser'; + # About SparkScan import AboutSparkScan from '../../../partials/intro/_about-sparkscan.mdx'; +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. + + + +## 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. + +

+ SparkScan Workflow

List building use case using SparkScan. +

+ +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. diff --git a/docs/start.mdx b/docs/start.mdx new file mode 100644 index 0000000000..cff78ef7ea --- /dev/null +++ b/docs/start.mdx @@ -0,0 +1,29 @@ +--- +title: "Not sure which Scandit product? Start here" +description: "Pick what you want to scan and the task you need to do, and go straight to the right Scandit product's docs for your platform." +sidebar_position: 0 +pagination_prev: null +pagination_next: null +keywords: + - get started + - which product + - product chooser + - start here + - scan one item + - stocktake + - order picking + - proof of delivery +--- + +import IntentNavigator from '@site/src/components/IntentNavigator'; + +# Not sure which Scandit product? Start here + +The home page's platform picker assumes you already know your product. This is the other +axis — **pick your intent, get the product** — for the cross-product confusion the replay +and ticket data shows (SparkScan vs Barcode Capture, ID Capture vs ID Bolt…). It is driven +by **one shared intent taxonomy**, the same scan type → task → product map the sign-up quiz +uses, so the docs and sign-up recommend the **same product**. Each recommendation links to +that product's docs for your platform. + + diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000000..a040282299 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,41 @@ +--- +title: "Troubleshooting: Scandit SDK error messages and fixes" +description: "Paste a Scandit SDK error message to find its cause and fix — licensing, activation, and camera errors." +product: [sparkscan, barcode-capture, id-capture] +topic_type: troubleshooting +version_status: current +canonical_id: scandit.troubleshooting.error-codes +user_intents: + - "find out what a Scandit SDK error message means" + - "fix a Scandit license or activation error" + - "resolve a camera permission error when scanning" +not_for: + - "reporting a new bug — contact support instead" +keywords: + - error + - error code + - license error + - invalid license + - license expired + - troubleshooting + - activation +hide_table_of_contents: true +--- + +import ErrorCodeFinder from '@site/src/components/ErrorCodeFinder'; + +# Troubleshooting: Scandit SDK error messages and fixes + +Hit an error while integrating the SDK? **Paste the exact message from your console** into the box below (or start typing a few words from it). The list narrows to the matching issue with its cause and fix. You can also press Ctrl/Cmd + F to search the page directly. + +Most integration failures are licensing problems — an invalid, expired, or wrong-application key, or the documentation placeholder left in the code. Those are grouped first. + +:::caution Demo preview — sample content +The entries below are **illustrative samples** (each tagged `SAMPLE` with a `SAMPLE-…` code) that demonstrate the finder. They are **not** real Scandit error messages and must not be relied on. Verified messages and fixes will replace them once the support/licensing error data is supplied. +::: + + + +## Still stuck? + +If your error isn't listed, or a fix doesn't resolve it, contact [Scandit support](https://www.scandit.com/support/) with the full console output, your SDK version, and the framework you're using. diff --git a/src/components/IntentNavigator/index.tsx b/src/components/IntentNavigator/index.tsx index ea08ea6865..9e55fc5c2f 100644 --- a/src/components/IntentNavigator/index.tsx +++ b/src/components/IntentNavigator/index.tsx @@ -2,24 +2,51 @@ import React from 'react'; import Link from '@docusaurus/Link'; import intents from '@site/src/data/intents.json'; import productsData from '@site/src/data/products.json'; -import styles from './styles.module.css'; +import styles from './styles.module.scss'; /** - * IntentNavigator — the "Start here / I want to…" entry point (Phase E MVP). + * IntentNavigator — the "Start here / I want to…" entry point. * * Renders the same intent taxonomy as the Unified-Signup PRD product quiz - * (scan type → task → delivery → product), so the docs and the signup flow - * route the same intent to the same product. Each option links to that - * product's docs, upgraded to the reader's currently-selected framework when - * that framework actually has the product (checked against products.json); - * otherwise it keeps the verified web landing. Additive and URL-preserving — - * no existing route changes. + * (scan type → task → delivery → product), so the docs and the signup flow route + * the same intent to the same product. Every recommendation links to that + * product's docs on the reader's chosen platform. + * + * Ported from the Phase E demo, keeping its full structure and grouping: + * 1. platform bar — pick your platform, remembered between visits + * 2. expert shortcut — "Already know your product? Jump straight in →" + * 3. three numbered steps with progressive disclosure, plus "start over" + * 4. "Why this matters" — docs ↔ signup parity + * 5. the Linux caveat, and a provenance footer + * + * Two deliberate differences from the demo: + * + * - COLOURS COME FROM THE SITE, NOT A PRIVATE PALETTE. The demo carried its own + * hex values; here every colour is an existing docs token so the component + * matches the surrounding page and follows the site's light/dark toggle. + * + * - AVAILABILITY IS COMPUTED, NOT COPIED. The demo hard-coded a PROD table + * "derived verbatim from products.json"; a copy drifts the moment products.json + * changes, so this reads products.json directly. Only the two things genuinely + * absent from that file are declared here (hosted products, and Linux). + * + * Disclosure is done with CSS, exactly as the demo did it, NOT by conditional + * rendering — which matters for two reasons. There is no flash of a different + * layout before hydration, because the server already renders the stepped UI. And + * every task phrasing and every product link is present in the static HTML, so + * search, crawlers and the AI retrieval layer see the whole taxonomy even though + * the reader is shown one step at a time. A React-state-gated render would have + * hidden all of it from them. + * + * With no JavaScript the step buttons cannot do anything, so a