diff --git a/.github/actions/build-framework-docs/action.yml b/.github/actions/build-framework-docs/action.yml new file mode 100644 index 000000000..9c5f60bfe --- /dev/null +++ b/.github/actions/build-framework-docs/action.yml @@ -0,0 +1,205 @@ +name: Build framework documentation +description: > + Runs the documentation pipeline for a single framework (export → inject → rewrite → + compress) and uploads the compressed docs and updated baseline as artifacts. + Deliberately stops before build:db — the database must be assembled once, from all + frameworks at the same time, or it ends up containing only this one. + +inputs: + framework: + description: angular | react | blazor | webcomponents + required: true + mode: + description: incremental | full + required: true + model: + description: Compression model override. Empty uses the compress scripts' default. + required: false + default: "" + submodule-branch: + description: Branch to move the documentation submodules to. + required: true + default: master + openai-api-key: + description: OpenAI API key used by the compression step. + required: true + +runs: + using: composite + steps: + - uses: actions/checkout@v6 + + # Deliberately not `submodules: recursive` — that also clones blazor/api-docs, + # blazor/igniteui-blazor and react/igniteui-react. api-docs is a private repository + # the default token cannot read, and none of the three are used by the documentation + # pipeline. Only the sources this framework reads are checked out. + - name: Check out documentation submodules + shell: bash + env: + FW: ${{ inputs.framework }} + run: | + set -euo pipefail + BASE=packages/igniteui-mcp/igniteui-doc-mcp + case "$FW" in + angular) + SUBS="angular/igniteui-docfx angular/igniteui-angular-samples angular/igniteui-angular-examples" ;; + react) + SUBS="common/igniteui-xplat-docs react/igniteui-react-examples" ;; + blazor) + SUBS="common/igniteui-xplat-docs blazor/igniteui-blazor-examples" ;; + webcomponents) + SUBS="common/igniteui-xplat-docs webcomponents/igniteui-wc-examples" ;; + *) + echo "::error::Unknown framework '$FW'"; exit 1 ;; + esac + for sub in $SUBS; do + echo "--- $sub ---" + git submodule update --init "$BASE/$sub" + done + + # Node 24, not 22: rewrite-api-links.ts uses URLPattern, which only became a + # global in Node 24. On 22 it fails with "URLPattern is not defined". + - uses: actions/setup-node@v6 + with: + node-version: 24.x + cache: yarn + + # The cross-platform gulp build restores the docfx dotnet tool. + - uses: actions/setup-dotnet@v6 + if: inputs.framework != 'angular' + with: + dotnet-version: 8.x + + - name: Install packages + shell: bash + run: yarn --frozen-lockfile + + - name: Move submodules to ${{ inputs.submodule-branch }} + shell: bash + working-directory: packages/igniteui-mcp/igniteui-doc-mcp + run: ./switch-submodules.sh "${{ inputs.submodule-branch }}" + + - name: Configure OpenAI credentials + shell: bash + working-directory: packages/igniteui-mcp/igniteui-doc-mcp + run: echo "OPENAI_API_KEY=${{ inputs.openai-api-key }}" > .env + + # dist/ is gitignored, so an incremental run starts with no compressed docs at all. + # Incremental compression only writes the files that changed, so without this the + # artifact would contain a handful of docs instead of the full set. + - name: Restore compressed docs from the committed DB + if: inputs.mode == 'incremental' + shell: bash + working-directory: packages/igniteui-mcp/igniteui-doc-mcp + run: npx tsx scripts/restore-docs-final.ts --framework "${{ inputs.framework }}" + + - name: Build documentation + shell: bash + working-directory: packages/igniteui-mcp/igniteui-doc-mcp + env: + FW: ${{ inputs.framework }} + MODE: ${{ inputs.mode }} + COMPRESS_MODEL: ${{ inputs.model }} + run: | + set -euo pipefail + + # Only the xplat gulp target uses an abbreviated name. + case "$FW" in + webcomponents) XPLAT="wc" ;; + angular) XPLAT="" ;; + *) XPLAT="$FW" ;; + esac + + if [ "$MODE" = "full" ]; then + npm run "clear:$FW" + else + npm run clear:build + fi + + if [ -n "$XPLAT" ]; then + npm run "build:xplat-$XPLAT" + fi + + npm run "export:$FW" + npm run "inject:$FW" + npm run "rewrite-api-urls:$FW" + + if [ "$MODE" = "full" ]; then + npm run "compress:$FW" -- --batch submit + npm run "compress:$FW" -- --batch poll + npm run "derive-components:$FW" + npx tsx scripts/update-baseline.ts --framework "$FW" --full + else + npm run "diff:$FW" + # An empty manifest means nothing changed upstream. batchSubmit exits without + # writing _batch_state.json, which would make the subsequent poll fail, so + # skip compression entirely — the restored docs are already current. + CHANGED=$(node -e "const m=require('./dist/diff-manifest.json');console.log((m.changed||[]).length+(m.added||[]).length)") + echo "Manifest reports $CHANGED changed/added document(s)" + if [ "$CHANGED" -gt 0 ]; then + npm run "compress:$FW" -- --batch submit --manifest dist/diff-manifest.json + npm run "compress:$FW" -- --batch poll + # Only the freshly compressed docs need it; restored ones already carry + # derived values from the committed DB. + npm run "derive-components:$FW" + fi + npm run "update-baseline:$FW" + fi + + # Compression must yield one document per input. A batch entry that fails is + # simply absent from docs_final — the pipeline continues, the DB is published + # short, and document-count floors are far too loose to notice one missing file. + # This is not hypothetical: a full run reported "375 succeeded, 1 failed" and + # silently dropped hierarchicalgrid-editing.md. + IN=$(find "dist/docs_prepeared/$FW" -name '*.md' -not -name '_*' | wc -l) + OUT=$(find "dist/docs_final/$FW" -name '*.md' -not -name '_*' | wc -l) + if [ "$OUT" -lt "$IN" ]; then + echo "::warning::$FW: $OUT of $IN documents present after compression — retrying failed batch entries" + # `--batch retry` only submits a new batch; polling downloads its results. + # batchPoll reads state.retry_batch_id, so it follows the retry batch. + if npm run "compress:$FW" -- --batch retry; then + npm run "compress:$FW" -- --batch poll || true + npm run "derive-components:$FW" || true + fi + OUT=$(find "dist/docs_final/$FW" -name '*.md' -not -name '_*' | wc -l) + fi + if [ "$OUT" -lt "$IN" ]; then + echo "::error::$FW: compression produced only $OUT of $IN documents. Refusing to publish an incomplete set." + comm -23 \ + <(find "dist/docs_prepeared/$FW" -name '*.md' -not -name '_*' -printf '%f\n' | sort) \ + <(find "dist/docs_final/$FW" -name '*.md' -not -name '_*' -printf '%f\n' | sort) + exit 1 + fi + echo "$FW: $OUT of $IN documents compressed" + + # Always runs, so a framework that skipped compression still reports why. + - name: Report build summary + if: always() + shell: bash + working-directory: packages/igniteui-mcp/igniteui-doc-mcp + run: | + npx tsx scripts/report-build-summary.ts \ + --framework "${{ inputs.framework }}" \ + --mode "${{ inputs.mode }}" >> "$GITHUB_STEP_SUMMARY" + + - name: Upload compressed docs + uses: actions/upload-artifact@v7 + with: + name: docs-final-${{ inputs.framework }} + path: packages/igniteui-mcp/igniteui-doc-mcp/dist/docs_final/${{ inputs.framework }} + retention-days: 5 + + # build-db reads _tocName from here. Without it every row's toc_name would be NULL. + - name: Upload prepared docs + uses: actions/upload-artifact@v7 + with: + name: docs-prepeared-${{ inputs.framework }} + path: packages/igniteui-mcp/igniteui-doc-mcp/dist/docs_prepeared/${{ inputs.framework }} + retention-days: 5 + + - name: Upload updated baseline + uses: actions/upload-artifact@v7 + with: + name: docs-baseline-${{ inputs.framework }} + path: packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/${{ inputs.framework }} + retention-days: 5 diff --git a/.github/workflows/build-docs-db.yml b/.github/workflows/build-docs-db.yml new file mode 100644 index 000000000..3c0802b32 --- /dev/null +++ b/.github/workflows/build-docs-db.yml @@ -0,0 +1,263 @@ +name: Build documentation DB + +# Manual trigger only. A rebuild costs real money (a full run compresses ~1230 +# documents, roughly 3.5M output tokens), so it is always a deliberate decision. +on: + workflow_dispatch: + inputs: + mode: + description: Recompress everything, or only what changed upstream + type: choice + options: [incremental, full] + default: incremental + frameworks: + description: Comma-separated subset to rebuild + type: string + default: angular,react,blazor,webcomponents + submodule_branch: + description: Branch to move the documentation submodules to + type: string + default: master + model: + description: Compression model override (empty uses the script default) + type: string + default: "" + +permissions: + contents: read + +jobs: + # The four compress jobs run strictly one after another. Their state is per-framework + # so they *could* run in parallel, but concurrent batch submissions contend for the + # same account-level OpenAI limits — in particular enqueued tokens per model. + angular: + if: contains(inputs.frameworks, 'angular') + runs-on: ubuntu-latest + timeout-minutes: 330 + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/build-framework-docs + with: + framework: angular + mode: ${{ inputs.mode }} + model: ${{ inputs.model }} + submodule-branch: ${{ inputs.submodule_branch }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + + react: + needs: angular + if: always() && !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && contains(inputs.frameworks, 'react') + runs-on: ubuntu-latest + timeout-minutes: 330 + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/build-framework-docs + with: + framework: react + mode: ${{ inputs.mode }} + model: ${{ inputs.model }} + submodule-branch: ${{ inputs.submodule_branch }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + + blazor: + needs: react + if: always() && !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && contains(inputs.frameworks, 'blazor') + runs-on: ubuntu-latest + timeout-minutes: 330 + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/build-framework-docs + with: + framework: blazor + mode: ${{ inputs.mode }} + model: ${{ inputs.model }} + submodule-branch: ${{ inputs.submodule_branch }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + + webcomponents: + needs: blazor + if: always() && !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && contains(inputs.frameworks, 'webcomponents') + runs-on: ubuntu-latest + timeout-minutes: 330 + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/build-framework-docs + with: + framework: webcomponents + mode: ${{ inputs.mode }} + model: ${{ inputs.model }} + submodule-branch: ${{ inputs.submodule_branch }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + + # The database is assembled exactly once, here, with every framework's docs present. + # A per-framework build:db on a fresh runner finds no existing DB and rebuilds from + # scratch with only that framework — the bug that shipped a 112-doc and later an + # angular-only database. + assemble: + needs: [angular, react, blazor, webcomponents] + if: always() && !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24.x + cache: yarn + - name: Install packages + run: yarn --frozen-lockfile + + - uses: actions/download-artifact@v8 + with: + pattern: docs-final-* + path: packages/igniteui-mcp/igniteui-doc-mcp/dist/docs_final + - uses: actions/download-artifact@v8 + with: + pattern: docs-prepeared-* + path: packages/igniteui-mcp/igniteui-doc-mcp/dist/docs_prepeared + - uses: actions/download-artifact@v8 + with: + pattern: docs-baseline-* + path: packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline + + # download-artifact nests each artifact under its own name; flatten to the + # framework directories that build-db expects. + - name: Flatten artifact layout + working-directory: packages/igniteui-mcp/igniteui-doc-mcp + run: | + set -euo pipefail + for kind in docs_final:dist/docs_final docs_prepeared:dist/docs_prepeared docs_baseline:docs_baseline; do + prefix="${kind%%:*}"; dir="${kind##*:}" + for fw in angular react blazor webcomponents; do + src="$dir/${prefix//_/-}-$fw" + [ -d "$src" ] && rm -rf "$dir/$fw" && mv "$src" "$dir/$fw" || true + done + done + ls -la dist/docs_final + + # Any framework missing from this run keeps the copy already committed, so the + # database is always assembled from a complete set. --toc-stubs also emits the + # minimal docs_prepeared entries build-db needs to populate toc_name. + - name: Restore frameworks not rebuilt in this run + working-directory: packages/igniteui-mcp/igniteui-doc-mcp + env: + REQUESTED: ${{ inputs.frameworks }} + run: | + set -euo pipefail + for fw in angular react blazor webcomponents; do + if [ -d "dist/docs_final/$fw" ] && [ -n "$(ls -A "dist/docs_final/$fw" 2>/dev/null)" ]; then + continue + fi + # A framework that was rebuilt but has no docs here means its artifact did + # not arrive. Restoring from the DB would silently publish stale docs for it + # with counts that look perfectly healthy, so fail instead. + if echo "$REQUESTED" | grep -qw "$fw"; then + echo "::error::$fw was part of this run but its artifact is missing — refusing to build a database from stale $fw documents." + ls -R dist/docs_final || true + exit 1 + fi + echo "$fw was not part of this run — restoring from the committed DB" + npx tsx scripts/restore-docs-final.ts --framework "$fw" --toc-stubs + done + + - name: Build database + working-directory: packages/igniteui-mcp/igniteui-doc-mcp + run: npm run build:db + + - name: Verify document counts + run: | + npx tsc spec/unit/docs-db-counts-spec.ts --target es6 --module commonjs --esModuleInterop --skipLibCheck + npx jasmine spec/unit/docs-db-counts-spec.js + + - uses: actions/upload-artifact@v7 + with: + name: igniteui-docs-db + path: | + packages/igniteui-mcp/igniteui-doc-mcp/db/igniteui-docs.db + packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline + retention-days: 5 + + # The only job that writes to the repository. It opens a PR for review — nothing is + # pushed to a protected branch and nothing auto-merges. + publish: + needs: assemble + # Not `success()`: at job level that evaluates the whole ancestor chain, so a run + # scoped to a subset of frameworks (leaving the others skipped) would make it false + # and silently skip publishing. Check the direct dependency's result instead. + if: always() && !cancelled() && needs.assemble.result == 'success' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v6 + - uses: actions/download-artifact@v8 + with: + name: igniteui-docs-db + path: artifact + + - name: Apply rebuilt database and baselines + run: | + set -euo pipefail + + # upload-artifact roots an artifact at the least common ancestor of its paths, + # so the layout under artifact/ depends on which paths were uploaded together. + # Locate the contents instead of assuming a depth — this step runs after hours + # of compression, so it must not fail on a path guess. + DB=$(find artifact -type f -name igniteui-docs.db | head -1) + BASELINE=$(find artifact -type d -name docs_baseline | head -1) + + if [ -z "$DB" ] || [ -z "$BASELINE" ]; then + echo "::error::Could not locate the database or baselines in the artifact." + find artifact + exit 1 + fi + echo "Using DB: $DB" + echo "Using baselines: $BASELINE" + + cp "$DB" packages/igniteui-mcp/igniteui-doc-mcp/db/igniteui-docs.db + # Kept in sync with the doc-mcp copy, as every prior doc-update commit has done. + cp "$DB" packages/igniteui-mcp/docs-backend/docs-backend/igniteui-docs.db + rm -rf packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline + cp -r "$BASELINE" packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline + rm -rf artifact + + - name: Commit and open pull request + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + BRANCH="chore/docs-db-${{ github.run_id }}" + git config user.name github-actions + git config user.email github-actions@github.com + git checkout -b "$BRANCH" + + # Submodule pointers are deliberately excluded — the release pipeline checks + # submodules out fresh, so recording them here would only add noise. + git add packages/igniteui-mcp/igniteui-doc-mcp/db/igniteui-docs.db \ + packages/igniteui-mcp/docs-backend/docs-backend/igniteui-docs.db \ + packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline + + if git diff --cached --quiet; then + echo "No changes to publish — the documentation is already up to date." + exit 0 + fi + + git commit -m "chore(mcp): rebuild documentation database (${{ inputs.mode }})" + git push origin "$BRANCH" + gh pr create \ + --base "${{ github.ref_name }}" \ + --head "$BRANCH" \ + --title "chore(mcp): rebuild documentation database" \ + --body "Automated rebuild of the Ignite UI documentation database. + + | | | + |---|---| + | mode | \`${{ inputs.mode }}\` | + | frameworks | \`${{ inputs.frameworks }}\` | + | submodule branch | \`${{ inputs.submodule_branch }}\` | + | model | \`${{ inputs.model || 'script default' }}\` | + | run | [#${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | + + Document counts were verified by \`spec/unit/docs-db-counts-spec.ts\` before this PR was opened. + + Requires manual review and merge." diff --git a/packages/igniteui-mcp/docs-backend/docs-backend/igniteui-docs.db b/packages/igniteui-mcp/docs-backend/docs-backend/igniteui-docs.db index 05f833b3c..846340ceb 100644 Binary files a/packages/igniteui-mcp/docs-backend/docs-backend/igniteui-docs.db and b/packages/igniteui-mcp/docs-backend/docs-backend/igniteui-docs.db differ diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/db/igniteui-docs.db b/packages/igniteui-mcp/igniteui-doc-mcp/db/igniteui-docs.db index 05f833b3c..846340ceb 100644 Binary files a/packages/igniteui-mcp/igniteui-doc-mcp/db/igniteui-docs.db and b/packages/igniteui-mcp/igniteui-doc-mcp/db/igniteui-docs.db differ diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs/knowledgebase.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs/knowledgebase.md index 0c8d189b9..428f20687 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs/knowledgebase.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs/knowledgebase.md @@ -1,6 +1,6 @@ # Documentation Processing Knowledgebase -Lessons learned and issues encountered while building the documentation processing pipelines. Entries 1-16 are from the Angular pipeline; entries 17-22 from React and MCP server; entries 23-28 from WebComponents and cross-platform improvements; entries 29-33 from Blazor, cross-platform architecture, and prompt improvements. +Lessons learned and issues encountered while building the documentation processing pipelines. Entries 1-16 are from the Angular pipeline; entries 17-22 from React and MCP server; entries 23-28 from WebComponents and cross-platform improvements; entries 29-34 from Blazor, cross-platform architecture, and prompt improvements. ## 1. LLM Compression: Wrong Component Prefix (Hallucination) @@ -371,6 +371,32 @@ This is the opposite logic from what you might expect — `exclude` means "hide **Rule:** This three-pronged approach is needed because the LLM's merge behavior is a chain: it first decides sections are "redundant" → merges headers → then drops examples from the merged section. Blocking any single step isn't enough — all three rules must reinforce each other. +## 34. LLM Compression: `component` Frontmatter Drifts and Names Sample-App Classes + +**Problem:** The `component` field was decided entirely by the compression model, and it is not stable across runs. Two full rebuilds with the same model (`gpt-5.6-luna`) over effectively unchanged sources produced **1231 of 1232 documents with changed content and 374 with a changed `component` field** — 144 listing fewer components, 129 more, 64 genuinely different names, 37 merely reordered. + +The worst case was `angular/angular-reactive-form-validation.md`: + +``` +before: IgxSelectComponent, IgxInputDirective, IgxComboComponent, IgxDatePickerComponent, … +after: DateValueValidatorDirective, DateValueAsyncValidatorDirective, ReactiveFormsSampleComponent, MyComponent +``` + +The model listed the **sample application's own classes** while the document body still documented `IgxSelectComponent`, `IgxInputDirective` and a dozen more. The prompt invited this by asking for "the exact class name(s) **as found in the document's source code**" — which those demo classes literally are. + +**Impact:** `component` drives `list_components` and component-filtered `search_docs`. A document indexed under `MyComponent` is effectively unreachable. Dropped entries (`cli-component-templates.md` went 28 → 6) shrink discoverability, and pure reordering churns the committed DB binary for no benefit. + +**Fix:** Two layers, because neither is sufficient alone. + +1. **Prompt** (all four compress scripts): every name must carry the platform prefix; never list classes the sample application defines for itself; list every component the doc covers rather than a subset; order by first appearance with the primary subject first. +2. **Deterministic post-pass** — `scripts/derive-components.ts`, wired into every `pipeline:*` after compression. It keeps a supplied name when it carries an Ignite UI prefix, or the API index knows it, or a heading names it; otherwise it drops it. It then puts the filename-derived primary first and adds indexed components named in headings. + +**Rule:** Prefer the **prefix** over API-index membership when deciding whether a name is real. The index built from `llms-full.txt` is incomplete — it lacks the data-visualisation components (`IgxCategoryChartComponent`), so filtering on index membership alone silently deletes valid entries. Equally, do not require the platform's own prefix exclusively: Angular docs legitimately reference `Igc*` Web Components wrappers (`IgcDockManagerComponent`, `IgcRatingComponent`, `IgcTileManagerComponent`), and the Excel library documents unprefixed classes (`Workbook`, `WorksheetChart`) that only the heading check preserves. + +**Rule:** Never let the derivation *substitute* when it has no positive evidence — falling back to "every component mentioned in the body" buries the subject under components used incidentally by demo code (`badge.md` became `IgxAvatarComponent, IgxBadgeComponent, IgxIconService, IgxListComponent…`). The one exception is when *every* supplied name was rejected, which means the model returned nothing usable. + +**Rule:** A `full` rebuild rewrites essentially the whole corpus even when nothing upstream changed. Prefer `incremental`, which only recompresses genuinely changed documents and therefore cannot churn metadata wholesale. + ## Related Documentation | Document | Description | Status | diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/badge.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/badge.md index 39bf84b43..16eb2559d 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/badge.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/badge.md @@ -16,18 +16,61 @@ The Ignite UI for Blazor Badge is a component used in conjunction with avatars, ```razor @using IgniteUI.Blazor.Controls -
-
-
- - -
- Terrance Orta -
+
+
+
+ +
+ 23 +
+
+ + + + +
+
+
+
+ 1 +
+ Orders +
+ +
+
+ 2 + +
+ Payment +
+ +
+
+ 3 +
+ Shipping +
+
@code { - + private const string FavoriteBorderIcon = ""; + private const string CloseIcon = ""; + + private IgbIcon iconRef; + + protected override void OnAfterRender(bool firstRender) + { + if (firstRender && iconRef != null) + { + iconRef.EnsureReady().ContinueWith(_ => + { + iconRef.RegisterIconFromText("favorite_border", FavoriteBorderIcon, "material"); + iconRef.RegisterIconFromText("close", CloseIcon, "material"); + }); + } + } } ``` @@ -74,13 +117,77 @@ The Ignite UI for Blazor badge supports several pre-defined stylistic variants. ```razor @using IgniteUI.Blazor.Controls - -
- +
+
+
+ + + + 2 +
+ Primary +
+
+
+ + + + +
+ Info +
+
+
+ + + + +
+ Success +
+
+
+ + + + 2 +
+ Warn +
+
+
+ + + + +
+ Error +
@code { - + private const string CheckIcon = ""; + private const string CloseIcon = ""; + private const string MailIcon = ""; + private const string NotificationsIcon = ""; + + private IgbIcon iconRef; + + protected override void OnAfterRender(bool firstRender) + { + if (firstRender && iconRef != null) + { + iconRef.EnsureReady().ContinueWith(_ => + { + iconRef.RegisterIconFromText("check", CheckIcon, "material"); + iconRef.RegisterIconFromText("close", CloseIcon, "material"); + iconRef.RegisterIconFromText("mail", MailIcon, "material"); + iconRef.RegisterIconFromText("notifications", NotificationsIcon, "material"); + }); + } + } } ``` @@ -95,13 +202,44 @@ The badge component supports `rounded`(default) and `square` shapes. These value ```razor @using IgniteUI.Blazor.Controls - -
- +
+
+ Rounded + + + + 2 + + + +
+
+ Square + + + + 2 + + + +
@code { - + private const string CheckIcon = ""; + + private IgbIcon checkIconRef; + + protected override void OnAfterRender(bool firstRender) + { + if (firstRender && checkIconRef != null) + { + checkIconRef.EnsureReady().ContinueWith(_ => + { + checkIconRef.RegisterIconFromText("check", CheckIcon, "material"); + }); + } + } } ``` @@ -116,12 +254,81 @@ The Ignite UI for Blazor badge component can also render as a minimal dot indica ```razor @using IgniteUI.Blazor.Controls -
- +
+
+
+ +
+ +
+
+
+ + + + Contract renewal + 09:12 + +
+
+ + Weekly digest + Yesterday + +
+
+
+ + +
+
@code { - + private const string NotificationsIcon = ""; + private const string ChevronRightIcon = ""; + private const string HomeIcon = ""; + private const string PersonIcon = ""; + private const string FacebookMessengerIcon = ""; + + private IgbIcon iconRef; + + protected override void OnAfterRender(bool firstRender) + { + if (firstRender && iconRef != null) + { + iconRef.EnsureReady().ContinueWith(_ => + { + iconRef.RegisterIconFromText("notifications", NotificationsIcon, "material"); + iconRef.RegisterIconFromText("chevron_right", ChevronRightIcon, "material"); + iconRef.RegisterIconFromText("home", HomeIcon, "material"); + iconRef.RegisterIconFromText("person", PersonIcon, "material"); + iconRef.RegisterIconFromText("facebookMessenger", FacebookMessengerIcon, "material"); + }); + } + } } ``` @@ -139,13 +346,56 @@ igc-badge::part(base) { ```razor @using IgniteUI.Blazor.Controls - -
- +
+
+ + + + + + +
+
+ + + + +
+
+ + + + 2 +
+
+ + +
@code { - + private const string PersonIcon = ""; + private const string PhotoCameraIcon = ""; + private const string StarBorderIcon = ""; + private const string FavoriteBorderIcon = ""; + + private IgbIcon iconRef; + + protected override void OnAfterRender(bool firstRender) + { + if (firstRender && iconRef != null) + { + iconRef.EnsureReady().ContinueWith(_ => + { + iconRef.RegisterIconFromText("person", PersonIcon, "material"); + iconRef.RegisterIconFromText("photo_camera", PhotoCameraIcon, "material"); + iconRef.RegisterIconFromText("star_border", StarBorderIcon, "material"); + iconRef.RegisterIconFromText("favorite_border", FavoriteBorderIcon, "material"); + }); + } + } } ``` diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/excel-library-using-tables.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/excel-library-using-tables.md index bfecd5f91..49b25b11b 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/excel-library-using-tables.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/excel-library-using-tables.md @@ -14,10 +14,8 @@ The Infragistics Blazor Excel Engine's `WorksheetTable` functionality allows you
diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-data-model.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-data-model.md index bb74b2c5a..7285aee0f 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-data-model.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-data-model.md @@ -115,7 +115,6 @@ The following code shows how to bind the [`IgbGeographicSymbolSeries`](mcp:get_a ```razor @using IgniteUI.Blazor.Controls - @for (int i = 0; i < this.DataSource.Count; i++) { diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-multiple-shapes.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-multiple-shapes.md index 7dcd05cbe..6b058ab19 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-multiple-shapes.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-multiple-shapes.md @@ -131,7 +131,6 @@ For your convenience, all above code snippets are combined into one code block b ```razor @using IgniteUI.Blazor.Controls - diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-multiple-sources.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-multiple-sources.md index c9a927400..1b3aeee64 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-multiple-sources.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-multiple-sources.md @@ -119,7 +119,6 @@ For your convenience, all above code snippets are combined into one code block b ```razor @using IgniteUI.Blazor.Controls - diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-shp-file.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-shp-file.md index 0b61468a1..f43418b27 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-shp-file.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-binding-shp-file.md @@ -84,7 +84,6 @@ The following code binds [`IgbGeographicPolylineSeries`](mcp:get_api_reference?p ```razor @using IgniteUI.Blazor.Controls - @@ -89,7 +88,6 @@ Alternatively, you can use the [EsriUtility](geo-map-resources-esri.md) which de ```razor @using IgniteUI.Blazor.Controls - diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-type-scatter-area-series.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-type-scatter-area-series.md index 5073d60ca..ba2611683 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-type-scatter-area-series.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-type-scatter-area-series.md @@ -100,7 +100,6 @@ The following code shows how to bind the [`IgbGeographicScatterAreaSeries`](mcp: ```razor @using IgniteUI.Blazor.Controls - diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-type-shape-polyline-series.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-type-shape-polyline-series.md index 0a35bf772..cc89ea79d 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-type-shape-polyline-series.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/geo-map-type-shape-polyline-series.md @@ -69,7 +69,6 @@ The following code shows how to bind the [`IgbGeographicPolylineSeries`](mcp:get ```razor @using IgniteUI.Blazor.Controls - diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-groupby.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-groupby.md index cc65507c7..abdce71ab 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-groupby.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-groupby.md @@ -261,7 +261,6 @@ During runtime the expressions are gettable and settable from the `groupingExpre new IgbGroupingExpression() { FieldName = "ShipCity", Dir= SortingDirection.Asc } }; - private void GroupGrid() { this.grid.GroupBy(GroupingExpression1); @@ -323,7 +322,6 @@ As an example, the following template would make the group rows summary more ver ```razor - //In JavaScript: igRegisterScript("WebGridGroupByRowTemplate", (ctx) => { var html = window.igTemplating.html; diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-lite-filtering.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-lite-filtering.md index 11ec27e1e..d707b1a57 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-lite-filtering.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-lite-filtering.md @@ -124,7 +124,7 @@ await grid.Filter(new IgbGridLiteFilterExpression { Key = "FirstName", Condition await grid.Filter(new IgbGridLiteFilterExpression[] { new IgbGridLiteFilterExpression { Key = "FirstName", Condition = "startsWith", SearchTerm = "a" }, - new IgbGridLiteFilterExpression { Key = "FirstName", Condition = "startsWith", SearchTerm = "g", Criteria = "or" } ``` + new IgbGridLiteFilterExpression { Key = "FirstName", Condition = "startsWith", SearchTerm = "g", Criteria = "or" }``` The `ClearFilter()` method, as the name implies, clears the filter state of a single column or the whole grid component, depending on the passed arguments. diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-lite-sorting.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-lite-sorting.md index 835294454..b8797621a 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-lite-sorting.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-lite-sorting.md @@ -187,7 +187,7 @@ await grid.Sort(new IgbGridLiteSortingExpression { Key = "Price", Direction = Gr await grid.Sort(new IgbGridLiteSortingExpression[] { new IgbGridLiteSortingExpression { Key = "Price", Direction = GridLiteSortingDirection.Descending }, - new IgbGridLiteSortingExpression { Key = "Name", Direction = GridLiteSortingDirection.Descending } ``` + new IgbGridLiteSortingExpression { Key = "Name", Direction = GridLiteSortingDirection.Descending }``` The `ClearSort()` method, as the name implies, clears the sort state of a single column or the whole grid component, depending on the passed arguments. diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-selection.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-selection.md index ec5733a3d..1404260ea 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-selection.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/grid-selection.md @@ -237,7 +237,6 @@ Basically the main function will look like this: this.MenuY = e.ClientY + "px"; } - public void onMenuShow(IgbGridCellEventArgs e) { IgbGridCellEventArgsDetail detail = e.Detail; @@ -260,7 +259,6 @@ The context menu will have the following functions: StateHasChanged(); } - public async void CopyRowData() { this.ShowMenu = false; diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/tree-grid-column-pinning.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/tree-grid-column-pinning.md index b955eedb8..a0d64bcc5 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/tree-grid-column-pinning.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/blazor/tree-grid-column-pinning.md @@ -588,7 +588,6 @@ This can be done by creating a header template for the columns with a custom ico HeaderTemplateScript="WebTreeGridPinHeaderTemplate" Name="column6" @ref="column6"> - // In JavaScript igRegisterScript("WebTreeGridPinHeaderTemplate", (ctx) => { diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/badge.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/badge.md index e2b635b57..9fa0166b5 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/badge.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/badge.md @@ -17,62 +17,203 @@ The Ignite UI for React Badge is a component used in conjunction with avatars, n /* shared styles are loaded from: */ /* https://dl.infragistics.com/x/css/samples/shared.v8.css */ -.wrapper { +igc-badge { + --ig-size: var(--ig-size-small); +} + +igc-avatar { + --size: 40px; + --ig-avatar-background: var(--ig-gray-700); + --ig-avatar-color: var(--ig-gray-50); +} + +.outlined-example igc-avatar igc-icon { + color: var(--ig-gray-50); +} + +.outlined-example:nth-child(2) igc-badge { + inset-block-start: auto; + inset-block-end: -6px; + inset-inline-end: -6px; +} + +.step-marker igc-badge { + position: absolute; + inset-block-start: -1px; + inset-inline-end: -2px; +} + +.outlined-example igc-badge { + position: absolute; + inset-block-start: -6px; + inset-inline-end: -10px; +} + +.icon-circle igc-icon { + font-size: 20px; +} + +.badge-outlined { + display: flex; + align-items: center; + justify-content: center; + gap: 60px; + min-height: 7rem; +} + +.outlined-example { position: relative; display: flex; - width: fit-content; +} + +.icon-circle, +.step-circle { + display: flex; align-items: center; - margin-block: 1rem; - gap: 0.5rem; - padding: 0.5rem; - border: 1px solid var(--ig-gray-300); - border-radius: .25rem; + justify-content: center; + border-radius: 50%; } -igc-avatar { - @container style(--ig-theme: indigo) { - --ig-size: var(--ig-size-large); - } +.icon-circle { + width: 36px; + height: 36px; + background: var(--ig-gray-200); + color: var(--ig-gray-800); +} - anchor-name: --avatar; +.badge-info-blue { + --ig-badge-background-color: #0057A9; } -igc-badge { - --size: .75rem; +.badge-info-blue::part(base) { + background-color: #0057A9; +} - position: absolute; - inset-block-end: anchor(--avatar bottom); - inset-inline-end: anchor(--avatar right); +.payment-dot-blue { + --ig-badge-background-color: #0057A9; + --ig-badge-dot-size: 0.5rem; +} + +.payment-dot-blue::part(base) { + background-color: #0057A9; +} + +igc-badge::part(base), +igc-badge igc-icon { + color: var(--ig-gray-50); +} + +igc-badge igc-icon { + fill: var(--ig-gray-50); +} + +.steps { + display: flex; + align-items: flex-start; + gap: 8px; +} + +.step { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.step-marker { + position: relative; + display: flex; +} + +.step-circle { + width: 26px; + height: 26px; + background: var(--ig-gray-900); + color: var(--ig-gray-50); + font-size: 12px; +} + +.step-circle.pending { + background: var(--ig-gray-200); + color: var(--ig-gray-800); +} + +.step-connector { + width: 56px; + margin-block-start: 13px; + border-block-start: 1px solid var(--ig-gray-900); +} + +.step-connector.pending { + border-block-start-style: dashed; + border-block-start-color: var(--ig-gray-400); } -span { - display: block; - font-weight: 600; +.step-label { + color: var(--ig-gray-700); + font-size: 13px; } ``` ```tsx -import React from 'react'; +import React, { useEffect } from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; -import { IgrBadge, IgrAvatar } from 'igniteui-react'; +import { IgrBadge, IgrAvatar, IgrIcon, registerIconFromText } from 'igniteui-react'; import 'igniteui-webcomponents/themes/light/bootstrap.css'; -export default class BadgeOutlined extends React.Component { - constructor(props: any) { - super(props); - } - - public render(): JSX.Element { - return ( -
-
- - -
- Terrance Orta -
- ); - } +const favoriteBorderIcon = + ''; +const closeIcon = + ''; + +const steps = [ + { index: 1, label: 'Orders', current: false, pending: false }, + { index: 2, label: 'Payment', current: true, pending: false }, + { index: 3, label: 'Shipping', current: false, pending: true } +]; + +export default function BadgeOutlined(): JSX.Element { + useEffect(() => { + registerIconFromText('favorite_border', favoriteBorderIcon, 'material'); + registerIconFromText('close', closeIcon, 'material'); + }, []); + + return ( +
+
+
+ +
+ 23 +
+
+ + + + +
+
+ {steps.map((step, i) => ( + + {i > 0 && ( + + )} +
+
+ + {step.index} + + {step.current && ( + + )} +
+ {step.label} +
+
+ ))} +
+
+ ); } // rendering above class to the React DOM @@ -122,32 +263,165 @@ The Ignite UI for React badge supports several pre-defined stylistic variants. Y ```css /* shared styles are loaded from: */ /* https://dl.infragistics.com/x/css/samples/shared.v8.css */ + +igc-badge { + --ig-size: var(--ig-size-small); + position: absolute; + inset-block-end: -4px; + inset-inline-end: -4px; +} + +igc-avatar { + --ig-size: var(--ig-size-small); + --ig-avatar-background: var(--ig-gray-700); + --ig-avatar-color: var(--ig-gray-50); +} + +.variant-item:first-child igc-avatar igc-icon, +.variant-item:nth-child(4) igc-avatar igc-icon { + color: var(--ig-gray-50); +} + +.badge-variants { + display: flex; + align-items: center; + justify-content: center; + gap: 40px; + min-height: 7rem; +} + +.variant-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + min-width: 56px; +} + +.variant-item span { + color: var(--ig-gray-700); + font-size: 13px; +} + +.avatar-wrapper { + position: relative; + display: flex; +} + +.badge-info-blue { + --ig-badge-background-color: #0057A9; +} + +.badge-info-blue::part(base) { + background-color: #0057A9; +} + +.badge-primary-blue { + --ig-badge-background-color: #0070BA; +} + +.badge-primary-blue::part(base) { + background-color: #0070BA; +} + +.badge-warning-black::part(base) { + background-color: #FAA419; + color: #000; +} + +igc-badge::part(base), +igc-badge igc-icon { + color: var(--ig-gray-50); +} + +igc-badge igc-icon { + fill: var(--ig-gray-50); +} ``` ```tsx -import React from 'react'; +import React, { useEffect } from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; -import { IgrBadge } from 'igniteui-react'; +import { IgrAvatar, IgrBadge, IgrIcon, registerIconFromText } from 'igniteui-react'; import 'igniteui-webcomponents/themes/light/bootstrap.css'; -export default class BadgeVariants extends React.Component { - - constructor(props: any) { - super(props); - } +const checkIcon = + ''; +const closeIcon = + ''; +const mailIcon = + ''; +const notificationsIcon = + ''; + +export default function BadgeVariants(): JSX.Element { + useEffect(() => { + registerIconFromText('check', checkIcon, 'material'); + registerIconFromText('close', closeIcon, 'material'); + registerIconFromText('mail', mailIcon, 'material'); + registerIconFromText('notifications', notificationsIcon, 'material'); + }, []); - public render(): JSX.Element { - return ( -
- + return ( +
+
+
+ + + + 2 +
+ Primary +
+
+
+ + + + +
+ Info +
+
+
+ + + + +
+ Success
- ); - } +
+
+ + + + 2 +
+ Warn +
+
+
+ + + + +
+ Error +
+
+ ); } // rendering above class to the React DOM const root = ReactDOM.createRoot(document.getElementById('root')); -root.render(); +root.render(); ``` ### Shape @@ -161,27 +435,90 @@ The badge component supports `rounded`(default) and `square` shapes. These value ```css /* shared styles are loaded from: */ /* https://dl.infragistics.com/x/css/samples/shared.v8.css */ + +.badge-shape { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 40px; + min-height: 7rem; +} + +.badge-shape-row { + display: grid; + grid-template-columns: 80px 40px 40px 40px; + align-items: center; + justify-items: center; + gap: 8px; +} + +.row-label { + justify-self: end; + color: var(--ig-gray-700); + font-size: 14px; +} + +.badge-small { + --ig-size: var(--ig-size-small); +} + +.badge-info-blue { + --ig-badge-background-color: #0057A9; +} + +.badge-info-blue::part(base) { + background-color: #0057A9; +} + +igc-badge::part(base), +igc-badge igc-icon { + color: var(--ig-gray-50); +} + +igc-badge igc-icon { + fill: var(--ig-gray-50); +} ``` ```tsx -import React from 'react'; +import React, { useEffect } from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; -import { IgrBadge } from 'igniteui-react'; +import { IgrBadge, IgrIcon, registerIconFromText } from 'igniteui-react'; import 'igniteui-webcomponents/themes/light/bootstrap.css'; -export default class BadgeShape extends React.Component { +const checkIcon = + ''; - constructor(props: any) { - super(props); - } +export default function BadgeShape(): JSX.Element { + useEffect(() => { + registerIconFromText('check', checkIcon, 'material'); + }, []); - public render(): JSX.Element { - return ( -
- + return ( +
+
+ Rounded + + + + 2 + + +
- ); - } +
+ Square + + + + 2 + + + +
+
+ ); } // rendering above class to the React DOM @@ -200,25 +537,228 @@ The Ignite UI for React badge component can also render as a minimal dot indicat ```css /* shared styles are loaded from: */ /* https://dl.infragistics.com/x/css/samples/shared.v8.css */ + +igc-badge { + --ig-size: var(--ig-size-small); + --ig-badge-dot-size: 0.5rem; +} + +igc-avatar { + --size: 40px; +} + +.icon-example igc-badge, +.avatar-example igc-badge, +.nav-icon igc-badge { + position: absolute; +} + +.icon-example igc-badge { + inset-block-start: 0; + inset-inline-end: 2px; +} + +.avatar-example igc-badge { + inset-block-start: -0.25rem; + inset-inline-end: -2px; +} + +.nav-icon igc-badge { + inset-block-start: -2px; + inset-inline-end: -6px; +} + +.badge-dot { + display: flex; + align-items: center; + justify-content: center; + gap: 40px; + min-height: 7rem; +} + +.dot-example { + position: relative; +} + +.icon-example, +.avatar-example, +.icon-circle { + display: flex; + align-items: center; + width: 36px; + height: 36px; +} + +.icon-circle { + justify-content: center; + border-radius: 50%; + background: var(--ig-gray-200); + color: var(--ig-gray-800); +} + +.notifications-card, +.nav-card { + background: var(--ig-surface-500); + border-radius: 4px; + box-shadow: 0 1px 3px hsl(from var(--ig-gray-900) h s l / 0.12); +} + +.notifications-card { + min-width: 272px; + padding: 8px 0; +} + +.notification-row { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + font-size: 14px; + color: var(--ig-gray-900); +} + +.row-indicator { + display: inline-flex; + justify-content: center; + width: 12px; +} + +.row-indicator igc-badge, +.nav-icon igc-badge { + --ig-badge-background-color: #0057A9; +} + +.row-indicator igc-badge::part(base), +.nav-icon igc-badge::part(base) { + background-color: #0057A9; +} + +.row-title { + flex: 1; +} + +.row-time { + font-size: 13px; + color: var(--ig-gray-900); +} + +.row-time.unread { + color: #0057A9; + font-weight: 600; +} + +.row-chevron { + --ig-size: 1; + + color: var(--ig-gray-600); + font-size: 18px; +} + +.nav-card { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; +} + +.nav-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + min-width: 56px; + color: var(--ig-gray-700); + font-size: 12px; +} + +.nav-item.active { + color: #0075D2; +} + +.nav-icon { + position: relative; + display: inline-flex; +} ``` ```tsx -import React from 'react'; +import React, { useEffect } from 'react'; import ReactDOM from 'react-dom/client'; -import { IgrBadge } from 'igniteui-react'; +import { IgrAvatar, IgrBadge, IgrIcon, registerIconFromText } from 'igniteui-react'; +import './index.css'; import 'igniteui-webcomponents/themes/light/bootstrap.css'; -export default class BadgeDot extends React.Component { - constructor(props: any) { - super(props); - } - - public render(): JSX.Element { - return ( -
- +const notificationsIcon = + ''; +const chevronRightIcon = + ''; +const homeIcon = + ''; +const personIcon = + ''; +const facebookMessengerIcon = + ''; + +const notifications = [ + { title: 'Contract renewal', time: '09:12', unread: true }, + { title: 'Weekly digest', time: 'Yesterday', unread: false } +]; + +const tabs = [ + { label: 'Home', icon: 'home', active: true, hasUpdates: false }, + { label: 'Chat', icon: 'facebookMessenger', active: false, hasUpdates: true }, + { label: 'Profile', icon: 'person', active: false, hasUpdates: false } +]; + +export default function BadgeDot(): JSX.Element { + useEffect(() => { + registerIconFromText('notifications', notificationsIcon, 'material'); + registerIconFromText('chevron_right', chevronRightIcon, 'material'); + registerIconFromText('home', homeIcon, 'material'); + registerIconFromText('person', personIcon, 'material'); + registerIconFromText('facebookMessenger', facebookMessengerIcon, 'material'); + }, []); + + return ( +
+
+
+ +
+
- ); - } +
+ {notifications.map((item) => ( +
+ + {item.unread && } + + {item.title} + {item.time} + +
+ ))} +
+
+ + +
+
+ {tabs.map((tab) => ( +
+ + + {tab.hasUpdates && } + + {tab.label} +
+ ))} +
+
+ ); } // rendering above class to the React DOM @@ -241,31 +781,145 @@ igc-badge::part(base) { /* shared styles are loaded from: */ /* https://dl.infragistics.com/x/css/samples/shared.v8.css */ -igc-badge::part(base) { - --background-color: var(--ig-error-A100); - --border-radius: 2px; +igc-badge { + --ig-size: var(--ig-size-small); +} + +.badge-teal { + --ig-badge-background-color: var(--ig-success-700); + --ig-badge-border-radius: 50%; +} + +.badge-amber { + --ig-badge-background-color: #C97C00; + --ig-badge-border-radius: 50%; +} + +.badge-magenta { + --ig-badge-background-color: #9C27B0; + --ig-badge-text-color: var(--ig-gray-50); + --ig-badge-border-radius: 50%; +} + +.badge-lime { + --ig-badge-background-color: var(--ig-success-700); + --ig-badge-border-radius: 50%; + --ig-badge-dot-size: 0.5rem; +} + +.badge-teal igc-icon, +.badge-amber igc-icon, +.styling-item.pink igc-avatar igc-icon { + color: var(--ig-gray-50); +} + +.badge-teal igc-icon { + position: relative; +} + +.badge-teal igc-icon::after { + content: ""; + position: absolute; + inset: 50% auto auto 50%; + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--ig-success-700); + transform: translate(-50%, -50%); +} + +.styling-item igc-avatar { + --size: 40px; +} + +.styling-item.green igc-avatar { + --icon-color: #248436; + --ig-avatar-background: #A9CEB0; + --ig-avatar-color: #248436; +} + +.styling-item.pink igc-avatar { + --ig-avatar-background: #DA64FF; + --ig-avatar-color: var(--ig-gray-50); +} + +.styling-item igc-badge { + position: absolute; + inset-block-end: -2px; + inset-inline-end: -2px; +} + +.badge-styling { + display: flex; + align-items: center; + justify-content: center; + gap: 60px; + min-height: 7rem; +} + +.styling-item { + position: relative; + display: flex; } ``` ```tsx -import React from 'react'; +import React, { useEffect } from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; -import { IgrBadge } from 'igniteui-react'; +import { IgrAvatar, IgrBadge, IgrIcon, registerIconFromText } from 'igniteui-react'; import 'igniteui-webcomponents/themes/light/bootstrap.css'; -export default class BadgeStyling extends React.Component { +const personIcon = + ''; +const photoCameraIcon = + ''; +const starBorderIcon = + ''; +const favoriteBorderIcon = + ''; + +export default function BadgeStyling(): JSX.Element { + useEffect(() => { + registerIconFromText('person', personIcon, 'material'); + registerIconFromText('photo_camera', photoCameraIcon, 'material'); + registerIconFromText('star_border', starBorderIcon, 'material'); + registerIconFromText('favorite_border', favoriteBorderIcon, 'material'); + }, []); - constructor(props: any) { - super(props); - } - - public render(): JSX.Element { - return ( -
- + return ( +
+
+ + + + + + +
+
+ + + +
- ); - } +
+ + + + 2 +
+
+ + +
+
+ ); } // rendering above class to the React DOM diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/excel-library-using-tables.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/excel-library-using-tables.md index a9800eba7..bce9fe6d8 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/excel-library-using-tables.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/excel-library-using-tables.md @@ -14,10 +14,8 @@ The Infragistics React Excel Engine's [`WorksheetTable`](https://www.infragistic
diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/features.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/features.md index 3a623da0b..408473d04 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/features.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/features.md @@ -140,7 +140,6 @@ const [comboDisabled, setComboDisabled] = useState(false); disabled={comboDisabled}> - setDisableFiltering(e.detail.checked)}> Disable Filtering diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/grid-lite-binding.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/grid-lite-binding.md index 5f3198289..236fd82ab 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/grid-lite-binding.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/grid-lite-binding.md @@ -43,7 +43,6 @@ If the grid has `autoGenerate` enabled, it will "_infer_" the new column configu ```tsx const [data, setData] = React.useState([/* initial data */]); - /** After the new binding the grid will infer the column collection from the bound data. */ const updateData = () => { setData([/* new data */]); diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/grid-lite-header-template.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/grid-lite-header-template.md index 2f77ec730..feee3bf29 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/grid-lite-header-template.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/react/grid-lite-header-template.md @@ -48,7 +48,6 @@ const ratingHeaderTemplate = (ctx: IgrHeaderContext) => (

{"⭐ Rating ⭐"}

); - return ( diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/badge.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/badge.md index 27b1fd103..69edd3f0b 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/badge.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/badge.md @@ -17,37 +17,141 @@ The Ignite UI for Web Components Badge is a component used in conjunction with a /* shared styles are loaded from: */ /* https://dl.infragistics.com/x/css/samples/shared.v8.css */ -.wrapper { +igc-badge { + --ig-size: var(--ig-size-small); +} + +igc-avatar { + --size: 40px; + --ig-avatar-background: var(--ig-gray-700); + --ig-avatar-color: var(--ig-gray-50); +} + +.outlined-example igc-avatar igc-icon { + color: var(--ig-gray-50); +} + +.outlined-example:nth-child(2) igc-badge { + inset-block-start: auto; + inset-block-end: -6px; + inset-inline-end: -6px; +} + +.step-marker igc-badge { + position: absolute; + inset-block-start: -1px; + inset-inline-end: -2px; +} + +.outlined-example igc-badge { + position: absolute; + inset-block-start: -6px; + inset-inline-end: -10px; +} + +.icon-circle igc-icon { + font-size: 20px; +} + +.badge-outlined { + display: flex; + align-items: center; + justify-content: center; + gap: 60px; + min-height: 7rem; +} + +.outlined-example { position: relative; display: flex; - width: fit-content; +} + +.icon-circle, +.step-circle { + display: flex; align-items: center; - margin-block: 1rem; - gap: 0.5rem; - padding: 0.5rem; - border: 1px solid var(--ig-gray-300); - border-radius: .25rem; + justify-content: center; + border-radius: 50%; } -igc-avatar { - @container style(--ig-theme: indigo) { - --ig-size: var(--ig-size-large); - } +.icon-circle { + width: 36px; + height: 36px; + background: var(--ig-gray-200); + color: var(--ig-gray-800); +} - anchor-name: --avatar; +.badge-info-blue { + --ig-badge-background-color: #0057A9; } -igc-badge { - --size: .75rem; +.badge-info-blue::part(base) { + background-color: #0057A9; +} - position: absolute; - inset-block-end: anchor(--avatar bottom); - inset-inline-end: anchor(--avatar right); +.payment-dot-blue { + --ig-badge-background-color: #0057A9; + --ig-badge-dot-size: 0.5rem; +} + +.payment-dot-blue::part(base) { + background-color: #0057A9; +} + +igc-badge::part(base), +igc-badge igc-icon { + color: var(--ig-gray-50); +} + +igc-badge igc-icon { + fill: var(--ig-gray-50); +} + +.steps { + display: flex; + align-items: flex-start; + gap: 8px; +} + +.step { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.step-marker { + position: relative; + display: flex; } -span { - display: block; - font-weight: 600; +.step-circle { + width: 26px; + height: 26px; + background: var(--ig-gray-900); + color: var(--ig-gray-50); + font-size: 12px; +} + +.step-circle.pending { + background: var(--ig-gray-200); + color: var(--ig-gray-800); +} + +.step-connector { + width: 56px; + margin-block-start: 13px; + border-block-start: 1px solid var(--ig-gray-900); +} + +.step-connector.pending { + border-block-start-style: dashed; + border-block-start-color: var(--ig-gray-400); +} + +.step-label { + color: var(--ig-gray-700); + font-size: 13px; } ``` @@ -97,6 +201,80 @@ The Ignite UI for Web Components badge supports several pre-defined stylistic va ```css /* shared styles are loaded from: */ /* https://dl.infragistics.com/x/css/samples/shared.v8.css */ + +igc-badge { + --ig-size: var(--ig-size-small); + position: absolute; + inset-block-end: -4px; + inset-inline-end: -4px; +} + +igc-avatar { + --ig-size: var(--ig-size-small); + --ig-avatar-background: var(--ig-gray-700); + --ig-avatar-color: var(--ig-gray-50); +} + +.variant-item:first-child igc-avatar igc-icon, +.variant-item:nth-child(4) igc-avatar igc-icon { + color: var(--ig-gray-50); +} + +.badge-variants { + display: flex; + align-items: center; + justify-content: center; + gap: 40px; + min-height: 7rem; +} + +.variant-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + min-width: 56px; +} + +.variant-item span { + color: var(--ig-gray-700); + font-size: 13px; +} + +.avatar-wrapper { + position: relative; + display: flex; +} + +.badge-info-blue { + --ig-badge-background-color: #0057A9; +} + +.badge-info-blue::part(base) { + background-color: #0057A9; +} + +.badge-primary-blue { + --ig-badge-background-color: #0070BA; +} + +.badge-primary-blue::part(base) { + background-color: #0070BA; +} + +.badge-warning-black::part(base) { + background-color: #FAA419; + color: #000; +} + +igc-badge::part(base), +igc-badge igc-icon { + color: var(--ig-gray-50); +} + +igc-badge igc-icon { + fill: var(--ig-gray-50); +} ``` ### Shape @@ -110,6 +288,50 @@ The badge component supports `rounded`(default) and `square` shapes. These value ```css /* shared styles are loaded from: */ /* https://dl.infragistics.com/x/css/samples/shared.v8.css */ + +.badge-shape { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 40px; + min-height: 7rem; +} + +.badge-shape-row { + display: grid; + grid-template-columns: 80px 40px 40px 40px; + align-items: center; + justify-items: center; + gap: 8px; +} + +.row-label { + justify-self: end; + color: var(--ig-gray-700); + font-size: 14px; +} + +.badge-small { + --ig-size: var(--ig-size-small); +} + +.badge-info-blue { + --ig-badge-background-color: #0057A9; +} + +.badge-info-blue::part(base) { + background-color: #0057A9; +} + +igc-badge::part(base), +igc-badge igc-icon { + color: var(--ig-gray-50); +} + +igc-badge igc-icon { + fill: var(--ig-gray-50); +} ``` ### Dot @@ -123,6 +345,148 @@ The Ignite UI for Web Components badge component can also render as a minimal do ```css /* shared styles are loaded from: */ /* https://dl.infragistics.com/x/css/samples/shared.v8.css */ + +igc-badge { + --ig-size: var(--ig-size-small); + --ig-badge-dot-size: 0.5rem; +} + +igc-avatar { + --size: 40px; +} + +.icon-example igc-badge, +.avatar-example igc-badge, +.nav-icon igc-badge { + position: absolute; +} + +.icon-example igc-badge { + inset-block-start: 0; + inset-inline-end: 2px; +} + +.avatar-example igc-badge { + inset-block-start: -0.25rem; + inset-inline-end: -2px; +} + +.nav-icon igc-badge { + inset-block-start: -2px; + inset-inline-end: -6px; +} + +.badge-dot { + display: flex; + align-items: center; + justify-content: center; + gap: 40px; + min-height: 7rem; +} + +.dot-example { + position: relative; +} + +.icon-example, +.avatar-example, +.icon-circle { + display: flex; + align-items: center; + width: 36px; + height: 36px; +} + +.icon-circle { + justify-content: center; + border-radius: 50%; + background: var(--ig-gray-200); + color: var(--ig-gray-800); +} + +.notifications-card, +.nav-card { + background: var(--ig-surface-500); + border-radius: 4px; + box-shadow: 0 1px 3px hsl(from var(--ig-gray-900) h s l / 0.12); +} + +.notifications-card { + min-width: 272px; + padding: 8px 0; +} + +.notification-row { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + font-size: 14px; + color: var(--ig-gray-900); +} + +.row-indicator { + display: inline-flex; + justify-content: center; + width: 12px; +} + +.row-indicator igc-badge, +.nav-icon igc-badge { + --ig-badge-background-color: #0057A9; +} + +.row-indicator igc-badge::part(base), +.nav-icon igc-badge::part(base) { + background-color: #0057A9; +} + +.row-title { + flex: 1; +} + +.row-time { + font-size: 13px; + color: var(--ig-gray-900); +} + +.row-time.unread { + color: #0057A9; + font-weight: 600; +} + +.row-chevron { + --ig-size: 1; + + color: var(--ig-gray-600); + font-size: 18px; +} + +.nav-card { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; +} + +.nav-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + min-width: 56px; + color: var(--ig-gray-700); + font-size: 12px; +} + +.nav-item.active { + color: #0075D2; +} + +.nav-icon { + position: relative; + display: inline-flex; +} ``` ## Styling @@ -136,15 +500,90 @@ igc-badge::part(base) { } ``` -```css -igc-badge::part(base) { - --background-color: var(--ig-error-A100); - --border-radius: 2px; -} -``` ```css /* shared styles are loaded from: */ /* https://dl.infragistics.com/x/css/samples/shared.v8.css */ + +igc-badge { + --ig-size: var(--ig-size-small); +} + +.badge-teal { + --ig-badge-background-color: var(--ig-success-700); + --ig-badge-border-radius: 50%; +} + +.badge-amber { + --ig-badge-background-color: #C97C00; + --ig-badge-border-radius: 50%; +} + +.badge-magenta { + --ig-badge-background-color: #9C27B0; + --ig-badge-text-color: var(--ig-gray-50); + --ig-badge-border-radius: 50%; +} + +.badge-lime { + --ig-badge-background-color: var(--ig-success-700); + --ig-badge-border-radius: 50%; + --ig-badge-dot-size: 0.5rem; +} + +.badge-teal igc-icon, +.badge-amber igc-icon, +.styling-item.pink igc-avatar igc-icon { + color: var(--ig-gray-50); +} + +.badge-teal igc-icon { + position: relative; +} + +.badge-teal igc-icon::after { + content: ""; + position: absolute; + inset: 50% auto auto 50%; + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--ig-success-700); + transform: translate(-50%, -50%); +} + +.styling-item igc-avatar { + --size: 40px; +} + +.styling-item.green igc-avatar { + --icon-color: #248436; + --ig-avatar-background: #A9CEB0; + --ig-avatar-color: #248436; +} + +.styling-item.pink igc-avatar { + --ig-avatar-background: #DA64FF; + --ig-avatar-color: var(--ig-gray-50); +} + +.styling-item igc-badge { + position: absolute; + inset-block-end: -2px; + inset-inline-end: -2px; +} + +.badge-styling { + display: flex; + align-items: center; + justify-content: center; + gap: 60px; + min-height: 7rem; +} + +.styling-item { + position: relative; + display: flex; +} ```
diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/excel-library-using-tables.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/excel-library-using-tables.md index 6b18c8dbe..ced38d170 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/excel-library-using-tables.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/excel-library-using-tables.md @@ -14,10 +14,8 @@ The Infragistics Web Components Excel Engine's [`WorksheetTable`](https://www.in
diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/grid-cell-editing.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/grid-cell-editing.md index 0ab053c28..55d112b26 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/grid-cell-editing.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/grid-cell-editing.md @@ -117,7 +117,6 @@ constructor() { column3.inlineEditorTemplate = this.webGridCellEditCellTemplate; } - public webGridCellEditCellTemplate = (ctx: IgcCellTemplateContext) => { let cellValues: any = []; let uniqueValues: any = []; diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/grid-lite-header-template.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/grid-lite-header-template.md index e51f23eb0..fc05db80e 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/grid-lite-header-template.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/grid-lite-header-template.md @@ -40,7 +40,6 @@ Similar to the cell template, you can also pass a custom template renderer and c ```typescript import { html } from 'lit'; - const column = document.querySelector('igc-grid-lite-column'); column.headerTemplate = () => html`

⭐ Rating ⭐

`; ``` diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/grid-selection.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/grid-selection.md index 2fa90b1f6..28f85f3cf 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/grid-selection.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/grid-selection.md @@ -143,7 +143,6 @@ The context menu will have the following functions: this.toggleContextMenu(); } - public copySelectedData() { const selectedData = this.grid.getSelectedData(); this.copyData(selectedData); diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/pivot-grid-overview.md b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/pivot-grid-overview.md index 95d4997d3..f65f7c5b6 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/pivot-grid-overview.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/docs_baseline/webcomponents/pivot-grid-overview.md @@ -256,7 +256,6 @@ Let's take a look at a basic pivot configuration: enabled: true } - ], rows: [ { diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/package.json b/packages/igniteui-mcp/igniteui-doc-mcp/package.json index f76f584af..8d623986f 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/package.json +++ b/packages/igniteui-mcp/igniteui-doc-mcp/package.json @@ -66,9 +66,13 @@ "inject:react": "npx tsx scripts/inject-react-docs.ts", "inject:webcomponents": "npx tsx scripts/inject-wc-docs.ts", "rewrite-api-urls:angular": "npx tsx scripts/rewrite-api-links.ts --platform angular", + "derive-components:angular": "npx tsx scripts/derive-components.ts --framework angular", "rewrite-api-urls:blazor": "npx tsx scripts/rewrite-api-links.ts --platform blazor", + "derive-components:blazor": "npx tsx scripts/derive-components.ts --framework blazor", "rewrite-api-urls:react": "npx tsx scripts/rewrite-api-links.ts --platform react", + "derive-components:react": "npx tsx scripts/derive-components.ts --framework react", "rewrite-api-urls:webcomponents": "npx tsx scripts/rewrite-api-links.ts --platform webcomponents", + "derive-components:webcomponents": "npx tsx scripts/derive-components.ts --framework webcomponents", "compress:angular": "npx tsx --env-file=.env scripts/compress-angular-docs.ts", "compress:blazor": "npx tsx --env-file=.env scripts/compress-blazor-docs.ts", "compress:react": "npx tsx --env-file=.env scripts/compress-react-docs.ts", @@ -94,14 +98,14 @@ "update-baseline:react": "npx tsx scripts/update-baseline.ts --framework react --manifest dist/diff-manifest.json", "update-baseline:webcomponents": "npx tsx scripts/update-baseline.ts --framework webcomponents --manifest dist/diff-manifest.json", "clear:build": "npx tsx -e \"import{rmSync}from'fs';['docs_processing','docs_prepeared'].forEach(d=>{rmSync('dist/'+d,{recursive:true,force:true})})\"", - "pipeline:angular": "npm run clear:build && npm run export:angular && npm run inject:angular && npm run rewrite-api-urls:angular && npm run diff:angular && npm run compress:angular -- --batch submit --manifest dist/diff-manifest.json && npm run compress:angular -- --batch poll && npm run update-baseline:angular && npm run build:db -- --framework angular", - "pipeline:blazor": "npm run clear:build && npm run build:xplat-blazor && npm run export:blazor && npm run inject:blazor && npm run rewrite-api-urls:blazor && npm run diff:blazor && npm run compress:blazor -- --batch submit --manifest dist/diff-manifest.json && npm run compress:blazor -- --batch poll && npm run update-baseline:blazor && npm run build:db -- --framework blazor", - "pipeline:react": "npm run clear:build && npm run build:xplat-react && npm run export:react && npm run inject:react && npm run rewrite-api-urls:react && npm run diff:react && npm run compress:react -- --batch submit --manifest dist/diff-manifest.json && npm run compress:react -- --batch poll && npm run update-baseline:react && npm run build:db -- --framework react", - "pipeline:webcomponents": "npm run clear:build && npm run build:xplat-wc && npm run export:webcomponents && npm run inject:webcomponents && npm run rewrite-api-urls:webcomponents && npm run diff:webcomponents && npm run compress:webcomponents -- --batch submit --manifest dist/diff-manifest.json && npm run compress:webcomponents -- --batch poll && npm run update-baseline:webcomponents && npm run build:db -- --framework webcomponents", - "pipeline:angular:full": "npm run clear:angular && npm run export:angular && npm run inject:angular && npm run rewrite-api-urls:angular && npm run compress:angular -- --batch submit && npm run compress:angular -- --batch poll && npx tsx scripts/update-baseline.ts --framework angular --full && npm run build:db -- --framework angular", - "pipeline:blazor:full": "npm run clear:blazor && npm run build:xplat-blazor && npm run export:blazor && npm run inject:blazor && npm run rewrite-api-urls:blazor && npm run compress:blazor -- --batch submit && npm run compress:blazor -- --batch poll && npx tsx scripts/update-baseline.ts --framework blazor --full && npm run build:db -- --framework blazor", - "pipeline:react:full": "npm run clear:react && npm run build:xplat-react && npm run export:react && npm run inject:react && npm run rewrite-api-urls:react && npm run compress:react -- --batch submit && npm run compress:react -- --batch poll && npx tsx scripts/update-baseline.ts --framework react --full && npm run build:db -- --framework react", - "pipeline:webcomponents:full": "npm run clear:webcomponents && npm run build:xplat-wc && npm run export:webcomponents && npm run inject:webcomponents && npm run rewrite-api-urls:webcomponents && npm run compress:webcomponents -- --batch submit && npm run compress:webcomponents -- --batch poll && npx tsx scripts/update-baseline.ts --framework webcomponents --full && npm run build:db -- --framework webcomponents" + "pipeline:angular": "npm run clear:build && npm run export:angular && npm run inject:angular && npm run rewrite-api-urls:angular && npm run diff:angular && npm run compress:angular -- --batch submit --manifest dist/diff-manifest.json && npm run compress:angular -- --batch poll && npm run derive-components:angular && npm run update-baseline:angular && npm run build:db -- --framework angular", + "pipeline:blazor": "npm run clear:build && npm run build:xplat-blazor && npm run export:blazor && npm run inject:blazor && npm run rewrite-api-urls:blazor && npm run diff:blazor && npm run compress:blazor -- --batch submit --manifest dist/diff-manifest.json && npm run compress:blazor -- --batch poll && npm run derive-components:blazor && npm run update-baseline:blazor && npm run build:db -- --framework blazor", + "pipeline:react": "npm run clear:build && npm run build:xplat-react && npm run export:react && npm run inject:react && npm run rewrite-api-urls:react && npm run diff:react && npm run compress:react -- --batch submit --manifest dist/diff-manifest.json && npm run compress:react -- --batch poll && npm run derive-components:react && npm run update-baseline:react && npm run build:db -- --framework react", + "pipeline:webcomponents": "npm run clear:build && npm run build:xplat-wc && npm run export:webcomponents && npm run inject:webcomponents && npm run rewrite-api-urls:webcomponents && npm run diff:webcomponents && npm run compress:webcomponents -- --batch submit --manifest dist/diff-manifest.json && npm run compress:webcomponents -- --batch poll && npm run derive-components:webcomponents && npm run update-baseline:webcomponents && npm run build:db -- --framework webcomponents", + "pipeline:angular:full": "npm run clear:angular && npm run export:angular && npm run inject:angular && npm run rewrite-api-urls:angular && npm run compress:angular -- --batch submit && npm run compress:angular -- --batch poll && npm run derive-components:angular && npx tsx scripts/update-baseline.ts --framework angular --full && npm run build:db -- --framework angular", + "pipeline:blazor:full": "npm run clear:blazor && npm run build:xplat-blazor && npm run export:blazor && npm run inject:blazor && npm run rewrite-api-urls:blazor && npm run compress:blazor -- --batch submit && npm run compress:blazor -- --batch poll && npm run derive-components:blazor && npx tsx scripts/update-baseline.ts --framework blazor --full && npm run build:db -- --framework blazor", + "pipeline:react:full": "npm run clear:react && npm run build:xplat-react && npm run export:react && npm run inject:react && npm run rewrite-api-urls:react && npm run compress:react -- --batch submit && npm run compress:react -- --batch poll && npm run derive-components:react && npx tsx scripts/update-baseline.ts --framework react --full && npm run build:db -- --framework react", + "pipeline:webcomponents:full": "npm run clear:webcomponents && npm run build:xplat-wc && npm run export:webcomponents && npm run inject:webcomponents && npm run rewrite-api-urls:webcomponents && npm run compress:webcomponents -- --batch submit && npm run compress:webcomponents -- --batch poll && npm run derive-components:webcomponents && npx tsx scripts/update-baseline.ts --framework webcomponents --full && npm run build:db -- --framework webcomponents" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.30.0", diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/scripts/build-db.ts b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/build-db.ts index 74288c483..853af4113 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/scripts/build-db.ts +++ b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/build-db.ts @@ -120,9 +120,11 @@ function main() { } const isFullRebuild = !targetFramework; + // Captured before opening — opening the database creates the file. + const dbExisted = fs.existsSync(DB_PATH); let db: Database.Database; - if (isFullRebuild || !fs.existsSync(DB_PATH)) { + if (isFullRebuild || !dbExisted) { db = new Database(DB_PATH); db.exec("DROP TABLE IF EXISTS docs_fts"); db.exec("DROP TABLE IF EXISTS docs"); @@ -172,6 +174,13 @@ function main() { } db.exec("INSERT INTO docs_fts(docs_fts) VALUES('rebuild')"); + + // DROP/DELETE frees pages but never shrinks the file, and this DB is committed to git. + // A file created by this run has no free pages, so only vacuum an inherited one. + if (dbExisted) { + db.exec("VACUUM"); + } + db.pragma("optimize"); const totalRows = (db.prepare("SELECT COUNT(*) AS cnt FROM docs").get() as any).cnt; diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-angular-docs.ts b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-angular-docs.ts index 75a8afe93..eed84c2a8 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-angular-docs.ts +++ b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-angular-docs.ts @@ -56,7 +56,7 @@ interface BatchState { function parseArgs(): CliArgs { const args = process.argv.slice(2); const opts: CliArgs = { - model: "gpt-5.6-luna", + model: process.env.COMPRESS_MODEL || "gpt-5.6-luna", minSize: 0, dryRun: false, delay: 0.5, @@ -123,7 +123,11 @@ premium: true --- Rules for frontmatter fields: -- **component**: The exact Ignite UI for Angular component/directive class name(s) as found in the document's source code (e.g. IgxGridComponent, IgxButtonDirective, IgxComboComponent, IgxDatePickerComponent). Use the PascalCase Igx-prefixed name including the Component/Directive suffix as used in Angular. If the doc covers multiple components, comma-separate them. +- **component**: The Ignite UI for Angular component/directive class name(s) documented here (e.g. IgxGridComponent, IgxButtonDirective, IgxComboComponent, IgxDatePickerComponent). Use the PascalCase Igx-prefixed name including the Component/Directive suffix as used in Angular. Comma-separate multiple names. These rules are strict: + - Every name MUST begin with \`Igx\`. A class in the sample code that does not start with \`Igx\` is application code, not a library component. + - NEVER list classes the sample application defines for itself — custom validators, \`MyComponent\`, \`AppComponent\`, \`*SampleComponent\`, demo services, demo pipes, or demo directives. List only components from the Ignite UI library, even when the sample's own classes are more prominent in the code. + - List the components the document actually explains or demonstrates in its own sections — not every class that happens to appear somewhere in the sample code. About 15 is plenty; a long tail of incidental references is noise. + - Order them by first appearance in the document, with the document's primary subject first. - **keywords**: Do NOT repeat the component name from the \`component\` field. Include the short common name (e.g. grid, combo-box, date-picker), related UI concepts (e.g. filtering, sorting, paging, selection), and common synonyms developers might search for (e.g. card, avatar, badge, dropdown, dialog, modal, table). Use lowercase, comma-separated. Aim for 5-15 keywords. - **summary**: A concise 1-2 sentence description of what the document covers and what a developer can learn from it. Focus on the component's purpose and key capabilities. - **premium**: If the input frontmatter contains \`_premium: true\`, include \`premium: true\` in your output frontmatter. Otherwise omit the premium field entirely. @@ -496,10 +500,44 @@ async function processBatchResults( if (result.response?.status_code === 200) { let compressed = result.response.body.choices?.[0]?.message?.content ?? ""; compressed = stripResponseWrapper(compressed); - const { valid, issues } = validateStructure(compressed); + let { valid, issues } = validateStructure(compressed); + + if (!valid) { + // finish_reason and usage explain WHY a response was unusable. "length" means + // the model exhausted max_completion_tokens — reasoning tokens count against + // the same ceiling — and returned a truncated or empty body with no frontmatter. + const choice = result.response.body.choices?.[0]; + const usage = result.response.body.usage; + console.log( + ` INVALID ${name}: ${issues.join(", ")} ` + + `[finish_reason=${choice?.finish_reason ?? "?"}, ` + + `completion=${usage?.completion_tokens ?? "?"}, ` + + `reasoning=${usage?.completion_tokens_details?.reasoning_tokens ?? "?"}, ` + + `chars=${compressed.length}]` + ); + + // The sync path retries once on a validation failure; the batch path used to + // discard the document instead, which is how a full run published 375 of 376 + // Angular documents with nothing failing. Retry inline before giving up. + if (originalContent) { + console.log(` RETRY ${name}...`); + try { + const retried = await compressWithLLM(client, originalContent, state.model); + const recheck = validateStructure(retried); + if (recheck.valid) { + compressed = retried; + valid = true; + console.log(` RECOVERED ${name}`); + } else { + console.log(` still invalid after retry: ${recheck.issues.join(", ")}`); + } + } catch (err) { + console.log(` retry errored: ${(err as Error).message}`); + } + } + } if (!valid) { - console.log(` INVALID ${name}: ${issues.join(", ")}`); invalidCount++; invalidFiles.push(name); continue; diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-blazor-docs.ts b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-blazor-docs.ts index 7e1465f33..0b016cdba 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-blazor-docs.ts +++ b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-blazor-docs.ts @@ -56,7 +56,7 @@ interface BatchState { function parseArgs(): CliArgs { const args = process.argv.slice(2); const opts: CliArgs = { - model: "gpt-5.6-luna", + model: process.env.COMPRESS_MODEL || "gpt-5.6-luna", minSize: 0, dryRun: false, delay: 0.5, @@ -123,7 +123,11 @@ premium: true --- Rules for frontmatter fields: -- **component**: The exact Ignite UI for Blazor component class name(s) as found in the document's source code (e.g. IgbGrid, IgbButton, IgbCombo, IgbDatePicker). Use the PascalCase Igb-prefixed name as used in Blazor. Blazor components do NOT use suffixes like Component or Directive — just use the base name (e.g. IgbGrid, not IgbGridComponent). If the doc covers multiple components, comma-separate them. +- **component**: The Ignite UI for Blazor component class name(s) documented here (e.g. IgbGrid, IgbButton, IgbCombo, IgbDatePicker). Use the PascalCase Igb-prefixed name as used in Blazor. Blazor components do NOT use suffixes like Component or Directive — just use the base name (e.g. IgbGrid, not IgbGridComponent). Comma-separate multiple names. These rules are strict: + - Every name MUST begin with \`Igb\`. A class in the sample code that does not start with \`Igb\` is application code, not a library component. + - NEVER list classes the sample application defines for itself — page models, \`*Sample\`, demo services, or local record/DTO types. List only components from the Ignite UI library, even when the sample's own classes are more prominent. + - List the components the document actually explains or demonstrates in its own sections — not every class that happens to appear somewhere in the sample code. About 15 is plenty; a long tail of incidental references is noise. + - Order them by first appearance in the document, with the document's primary subject first. - **keywords**: Do NOT repeat the component name from the \`component\` field. Include the short common name (e.g. grid, combo-box, date-picker), related UI concepts (e.g. filtering, sorting, paging, selection), and common synonyms developers might search for (e.g. card, avatar, badge, dropdown, dialog, modal, table). Use lowercase, comma-separated. Aim for 5-15 keywords. - **summary**: A concise 1-2 sentence description of what the document covers and what a developer can learn from it. Focus on the component's purpose and key capabilities. - **premium**: If the input frontmatter contains \`_premium: true\`, include \`premium: true\` in your output frontmatter. Otherwise omit the premium field entirely. @@ -497,10 +501,44 @@ async function processBatchResults( if (result.response?.status_code === 200) { let compressed = result.response.body.choices?.[0]?.message?.content ?? ""; compressed = stripResponseWrapper(compressed); - const { valid, issues } = validateStructure(compressed); + let { valid, issues } = validateStructure(compressed); + + if (!valid) { + // finish_reason and usage explain WHY a response was unusable. "length" means + // the model exhausted max_completion_tokens — reasoning tokens count against + // the same ceiling — and returned a truncated or empty body with no frontmatter. + const choice = result.response.body.choices?.[0]; + const usage = result.response.body.usage; + console.log( + ` INVALID ${name}: ${issues.join(", ")} ` + + `[finish_reason=${choice?.finish_reason ?? "?"}, ` + + `completion=${usage?.completion_tokens ?? "?"}, ` + + `reasoning=${usage?.completion_tokens_details?.reasoning_tokens ?? "?"}, ` + + `chars=${compressed.length}]` + ); + + // The sync path retries once on a validation failure; the batch path used to + // discard the document instead, which is how a full run published 375 of 376 + // Angular documents with nothing failing. Retry inline before giving up. + if (originalContent) { + console.log(` RETRY ${name}...`); + try { + const retried = await compressWithLLM(client, originalContent, state.model); + const recheck = validateStructure(retried); + if (recheck.valid) { + compressed = retried; + valid = true; + console.log(` RECOVERED ${name}`); + } else { + console.log(` still invalid after retry: ${recheck.issues.join(", ")}`); + } + } catch (err) { + console.log(` retry errored: ${(err as Error).message}`); + } + } + } if (!valid) { - console.log(` INVALID ${name}: ${issues.join(", ")}`); invalidCount++; invalidFiles.push(name); continue; diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-react-docs.ts b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-react-docs.ts index ae6a5cf01..bc71990cf 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-react-docs.ts +++ b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-react-docs.ts @@ -56,7 +56,7 @@ interface BatchState { function parseArgs(): CliArgs { const args = process.argv.slice(2); const opts: CliArgs = { - model: "gpt-5.6-luna", + model: process.env.COMPRESS_MODEL || "gpt-5.6-luna", minSize: 0, dryRun: false, delay: 0.5, @@ -123,7 +123,11 @@ premium: true --- Rules for frontmatter fields: -- **component**: The exact Ignite UI for React component class name(s) as found in the document's source code (e.g. IgrGrid, IgrButton, IgrCombo, IgrDatePicker). Use the PascalCase Igr-prefixed name as used in React. React components do NOT use Angular-style suffixes like Component or Directive — just use the base name (e.g. IgrGrid, not IgrGridComponent). If the doc covers multiple components, comma-separate them. +- **component**: The Ignite UI for React component class name(s) documented here (e.g. IgrGrid, IgrButton, IgrCombo, IgrDatePicker). Use the PascalCase Igr-prefixed name as used in React. React components do NOT use Angular-style suffixes like Component or Directive — just use the base name (e.g. IgrGrid, not IgrGridComponent). Comma-separate multiple names. These rules are strict: + - Every name MUST begin with \`Igr\`. A class or function in the sample code that does not start with \`Igr\` is application code, not a library component. + - NEVER list things the sample application defines for itself — \`App\`, \`MyComponent\`, \`*Sample\`, demo hooks, demo helpers, or local state types. List only components from the Ignite UI library, even when the sample's own code is more prominent. + - List the components the document actually explains or demonstrates in its own sections — not every class that happens to appear somewhere in the sample code. About 15 is plenty; a long tail of incidental references is noise. + - Order them by first appearance in the document, with the document's primary subject first. - **keywords**: Do NOT repeat the component name from the \`component\` field. Include the short common name (e.g. grid, combo-box, date-picker), related UI concepts (e.g. filtering, sorting, paging, selection), and common synonyms developers might search for (e.g. card, avatar, badge, dropdown, dialog, modal, table). Use lowercase, comma-separated. Aim for 5-15 keywords. - **summary**: A concise 1-2 sentence description of what the document covers and what a developer can learn from it. Focus on the component's purpose and key capabilities. - **premium**: If the input frontmatter contains \`_premium: true\`, include \`premium: true\` in your output frontmatter. Otherwise omit the premium field entirely. @@ -497,10 +501,44 @@ async function processBatchResults( if (result.response?.status_code === 200) { let compressed = result.response.body.choices?.[0]?.message?.content ?? ""; compressed = stripResponseWrapper(compressed); - const { valid, issues } = validateStructure(compressed); + let { valid, issues } = validateStructure(compressed); + + if (!valid) { + // finish_reason and usage explain WHY a response was unusable. "length" means + // the model exhausted max_completion_tokens — reasoning tokens count against + // the same ceiling — and returned a truncated or empty body with no frontmatter. + const choice = result.response.body.choices?.[0]; + const usage = result.response.body.usage; + console.log( + ` INVALID ${name}: ${issues.join(", ")} ` + + `[finish_reason=${choice?.finish_reason ?? "?"}, ` + + `completion=${usage?.completion_tokens ?? "?"}, ` + + `reasoning=${usage?.completion_tokens_details?.reasoning_tokens ?? "?"}, ` + + `chars=${compressed.length}]` + ); + + // The sync path retries once on a validation failure; the batch path used to + // discard the document instead, which is how a full run published 375 of 376 + // Angular documents with nothing failing. Retry inline before giving up. + if (originalContent) { + console.log(` RETRY ${name}...`); + try { + const retried = await compressWithLLM(client, originalContent, state.model); + const recheck = validateStructure(retried); + if (recheck.valid) { + compressed = retried; + valid = true; + console.log(` RECOVERED ${name}`); + } else { + console.log(` still invalid after retry: ${recheck.issues.join(", ")}`); + } + } catch (err) { + console.log(` retry errored: ${(err as Error).message}`); + } + } + } if (!valid) { - console.log(` INVALID ${name}: ${issues.join(", ")}`); invalidCount++; invalidFiles.push(name); continue; diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-wc-docs.ts b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-wc-docs.ts index e4caf9870..0fe62d7e6 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-wc-docs.ts +++ b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/compress-wc-docs.ts @@ -56,7 +56,7 @@ interface BatchState { function parseArgs(): CliArgs { const args = process.argv.slice(2); const opts: CliArgs = { - model: "gpt-5.6-luna", + model: process.env.COMPRESS_MODEL || "gpt-5.6-luna", minSize: 0, dryRun: false, delay: 0.5, @@ -123,7 +123,11 @@ premium: true --- Rules for frontmatter fields: -- **component**: The exact Ignite UI for Web Components component class name(s) as found in the document's source code (e.g. IgcGridComponent, IgcButtonComponent, IgcComboComponent, IgcDatePickerComponent). Use the PascalCase Igc-prefixed name with the Component suffix as used in Web Components. If the doc covers multiple components, comma-separate them. +- **component**: The Ignite UI for Web Components component class name(s) documented here (e.g. IgcGridComponent, IgcButtonComponent, IgcComboComponent, IgcDatePickerComponent). Use the PascalCase Igc-prefixed name with the Component suffix as used in Web Components. Comma-separate multiple names. These rules are strict: + - Every name MUST begin with \`Igc\`. A class in the sample code that does not start with \`Igc\` is application code, not a library component. + - NEVER list classes the sample application defines for itself — \`App\`, \`MyComponent\`, \`*Sample\`, demo services, or local data types. List only components from the Ignite UI library, even when the sample's own classes are more prominent. + - List the components the document actually explains or demonstrates in its own sections — not every class that happens to appear somewhere in the sample code. About 15 is plenty; a long tail of incidental references is noise. + - Order them by first appearance in the document, with the document's primary subject first. - **keywords**: Do NOT repeat the component name from the \`component\` field. Include the short common name (e.g. grid, combo-box, date-picker), related UI concepts (e.g. filtering, sorting, paging, selection), and common synonyms developers might search for (e.g. card, avatar, badge, dropdown, dialog, modal, table). Use lowercase, comma-separated. Aim for 5-15 keywords. - **summary**: A concise 1-2 sentence description of what the document covers and what a developer can learn from it. Focus on the component's purpose and key capabilities. - **premium**: If the input frontmatter contains \`_premium: true\`, include \`premium: true\` in your output frontmatter. Otherwise omit the premium field entirely. @@ -497,10 +501,44 @@ async function processBatchResults( if (result.response?.status_code === 200) { let compressed = result.response.body.choices?.[0]?.message?.content ?? ""; compressed = stripResponseWrapper(compressed); - const { valid, issues } = validateStructure(compressed); + let { valid, issues } = validateStructure(compressed); + + if (!valid) { + // finish_reason and usage explain WHY a response was unusable. "length" means + // the model exhausted max_completion_tokens — reasoning tokens count against + // the same ceiling — and returned a truncated or empty body with no frontmatter. + const choice = result.response.body.choices?.[0]; + const usage = result.response.body.usage; + console.log( + ` INVALID ${name}: ${issues.join(", ")} ` + + `[finish_reason=${choice?.finish_reason ?? "?"}, ` + + `completion=${usage?.completion_tokens ?? "?"}, ` + + `reasoning=${usage?.completion_tokens_details?.reasoning_tokens ?? "?"}, ` + + `chars=${compressed.length}]` + ); + + // The sync path retries once on a validation failure; the batch path used to + // discard the document instead, which is how a full run published 375 of 376 + // Angular documents with nothing failing. Retry inline before giving up. + if (originalContent) { + console.log(` RETRY ${name}...`); + try { + const retried = await compressWithLLM(client, originalContent, state.model); + const recheck = validateStructure(retried); + if (recheck.valid) { + compressed = retried; + valid = true; + console.log(` RECOVERED ${name}`); + } else { + console.log(` still invalid after retry: ${recheck.issues.join(", ")}`); + } + } catch (err) { + console.log(` retry errored: ${(err as Error).message}`); + } + } + } if (!valid) { - console.log(` INVALID ${name}: ${issues.join(", ")}`); invalidCount++; invalidFiles.push(name); continue; diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/scripts/derive-components.ts b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/derive-components.ts new file mode 100644 index 000000000..6d2754677 --- /dev/null +++ b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/derive-components.ts @@ -0,0 +1,221 @@ +/** + * Rewrites the `component` frontmatter field in dist/docs_final// from the + * document body, validated against the platform API index. + * + * The compression model decides this field today, and it drifts badly: a full rebuild + * over unchanged sources changed `component` on 374 of 1232 documents, in one case + * replacing the Ignite UI components with sample-app class names (MyComponent, + * ReactiveFormsSampleComponent), which makes the doc unreachable through + * list_components and component-filtered search. + * + * Deriving it mechanically removes that entire class of drift: names come from the + * document text, every one is checked against the real API index, ordering is stable, + * and repeated runs produce identical output. + * + * Usage: + * npx tsx scripts/derive-components.ts --framework angular + * npx tsx scripts/derive-components.ts --framework angular --dry-run + */ +import { readFileSync, writeFileSync, existsSync, readdirSync } from "fs"; +import { join, resolve } from "path"; +import { buildCanonicalIndex } from "./rewrite-api-links.js"; +import { PLATFORMS, type Platform } from "../src/config/platforms.js"; + +const ROOT = resolve(import.meta.dirname, ".."); + +// Component prefix per platform. Only names carrying the platform's own prefix are +// considered — anything else in a code sample is application code. +const PREFIX: Record = { + angular: "Igx", + react: "Igr", + blazor: "Igb", + webcomponents: "Igc", +}; + +function arg(name: string): string | undefined { + const i = process.argv.indexOf(`--${name}`); + return i !== -1 ? process.argv[i + 1] : undefined; +} + +interface Frontmatter { + block: string; + body: string; + component: string; +} + +function splitFrontmatter(raw: string): Frontmatter | null { + const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (!m) return null; + const componentLine = m[1].match(/^component:[ \t]*(.*)$/m); + return { + block: m[0], + body: raw.slice(m[0].length), + component: componentLine ? componentLine[1].trim() : "", + }; +} + +/** Prefixed names from `text`, in first-appearance order, that exist in the API index. */ +function extract(text: string, prefix: string, index: Map): string[] { + const seen = new Set(); + const out: string[] = []; + const re = new RegExp(`\\b${prefix}[A-Za-z0-9]+\\b`, "g"); + for (const match of text.matchAll(re)) { + const canonical = index.get(match[0].toLowerCase()); + if (!canonical || seen.has(canonical)) continue; + seen.add(canonical); + out.push(canonical); + } + return out; +} + +/** + * The component the document is primarily about, guessed from its filename. + * "action-strip.md" -> IgxActionStripComponent, "grid-paging.md" -> IgxGridComponent. + */ +function primaryFromFilename(file: string, prefix: string, index: Map): string | null { + const tokens = file.replace(/\.md$/, "").split(/[-_.]/).filter(Boolean); + for (let take = tokens.length; take > 0; take--) { + const stem = (prefix + tokens.slice(0, take).join("")).toLowerCase(); + const exact = index.get(stem) ?? index.get(stem + "component") ?? index.get(stem + "directive"); + if (exact) return exact; + } + return null; +} + +/** + * Start from the model's list and remove anything not in the API index — that alone + * drops sample-app classes and hallucinated names. Then make sure the document's + * primary component leads, and add any indexed component named in a heading, which + * catches subjects the model omitted. Body-wide extraction is only a fallback: every + * component mentioned anywhere includes those merely used by demo code, which buries + * the actual subject. + */ +function derive( + modelValue: string, + body: string, + file: string, + prefix: string, + index: Map +): string[] { + // The model sometimes emits `component: ""` for documents with no library component + // (CLI guides, migration walkthroughs). Unquote so those become genuinely empty + // rather than a component literally named `""`. + const supplied = modelValue + .split(",") + .map(s => s.trim().replace(/^["']+|["']+$/g, "").trim()) + .filter(Boolean); + const headings = body.split("\n").filter(l => /^#{1,4}\s/.test(l)).join("\n"); + const fromHeadings = extract(headings, prefix, index); + + // Keep a supplied name when any of these hold, and drop it otherwise: + // * it carries an Ignite UI prefix — the API index is incomplete (it lacks the + // data-visualisation components, and Angular docs legitimately reference the + // Igc* Web Components wrappers), so the prefix is the more reliable signal; + // * the index knows it; + // * a heading names it — covers documented API outside the index, such as the + // Excel library's Workbook and WorksheetChart. + // Sample-application classes — MyComponent, ReactiveFormsSampleComponent, custom + // validators — satisfy none of these and are what this removes. + const kept = supplied + .filter(s => + /^Ig[xrbc][A-Z]/.test(s) || + index.has(s.toLowerCase()) || + new RegExp(`\\b${s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(headings)) + .map(s => index.get(s.toLowerCase()) ?? s); + + // Every supplied name was rejected, so the model listed nothing usable — the + // sample-app-classes case. Here the body is the better source even though it also + // picks up components used incidentally by demo code. + const allRejected = supplied.length > 0 && kept.length === 0; + const fallback = allRejected ? extract(body, prefix, index).slice(0, 12) : []; + + const primary = primaryFromFilename(file, prefix, index); + const ordered = [...(primary ? [primary] : []), ...kept, ...fromHeadings, ...fallback]; + + const seen = new Set(); + const out: string[] = []; + for (const name of ordered) { + if (seen.has(name)) continue; + seen.add(name); + out.push(name); + } + // No positive evidence at all — leave the model's value alone rather than replace it + // with components that merely appear in demo code. + return out; +} + +function main(): void { + const framework = arg("framework") as Platform | undefined; + const dryRun = process.argv.includes("--dry-run"); + + if (!framework || !PLATFORMS.includes(framework)) { + console.error(`--framework is required. Valid: ${PLATFORMS.join(", ")}`); + process.exit(1); + } + + const dir = join(ROOT, "dist", "docs_final", framework); + if (!existsSync(dir)) { + console.error(`Not found: ${dir}. Run the pipeline first.`); + process.exit(1); + } + + console.log(`Loading ${framework} API index…`); + const index = buildCanonicalIndex(framework); + console.log(` ${index.size} components indexed`); + + const only = arg("only"); + const files = readdirSync(dir) + .filter(f => f.endsWith(".md") && !f.startsWith("_")) + .filter(f => !only || f === only); + let rewritten = 0; + let unchanged = 0; + let kept = 0; + const samples: string[] = []; + + for (const file of files) { + const path = join(dir, file); + const raw = readFileSync(path, "utf-8"); + const fm = splitFrontmatter(raw); + if (!fm) { + console.warn(` [warn] ${file}: no frontmatter, skipped`); + continue; + } + + const derived = derive(fm.component, fm.body, file, PREFIX[framework], index); + if (derived.length === 0) { + // Nothing verifiable in the body — the model's value is better than an empty + // field. Covers docs whose components carry another platform's prefix, such as + // the IgcDockManagerComponent wrappers used from Angular. + kept++; + continue; + } + + const next = derived.join(", "); + if (next === fm.component) { + unchanged++; + continue; + } + + if (samples.length < 5) { + samples.push(` ${file}\n was: ${fm.component}\n now: ${next}`); + } + rewritten++; + + if (!dryRun) { + const block = fm.block.match(/^component:/m) + ? fm.block.replace(/^component:[ \t]*.*$/m, `component: ${next}`) + : fm.block.replace(/^---\r?\n/, `---\ncomponent: ${next}\n`); + writeFileSync(path, block + fm.body, "utf-8"); + } + } + + console.log(`\n${framework}: ${files.length} documents`); + console.log(` rewritten : ${rewritten}${dryRun ? " (dry run — nothing written)" : ""}`); + console.log(` already correct: ${unchanged}`); + console.log(` kept model value (no indexed component found): ${kept}`); + if (samples.length) { + console.log(`\nsample changes:\n${samples.join("\n")}`); + } +} + +main(); diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/scripts/report-build-summary.ts b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/report-build-summary.ts new file mode 100644 index 000000000..a1a05b086 --- /dev/null +++ b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/report-build-summary.ts @@ -0,0 +1,105 @@ +/** + * Prints a Markdown summary of one framework's documentation build. + * + * The workflow appends the output to $GITHUB_STEP_SUMMARY. It reports what changed + * upstream, how much was actually compressed and at what cost — the numbers you need + * to judge a run without opening the logs. + * + * Usage: npx tsx scripts/report-build-summary.ts --framework react --mode incremental + */ +import * as fs from "fs"; +import * as path from "path"; + +function arg(name: string): string { + const i = process.argv.indexOf(`--${name}`); + return i !== -1 ? process.argv[i + 1] : ""; +} + +function readJson(file: string): any | null { + try { + return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf-8")) : null; + } catch { + return null; + } +} + +const n = (v: number): string => v.toLocaleString("en-US"); +const kb = (v: number): string => (v >= 1024 ? `${(v / 1024).toFixed(1)} MB` : `${v.toFixed(0)} KB`); + +const framework = arg("framework"); +const mode = arg("mode") || "unknown"; +const finalDir = path.resolve("dist", "docs_final", framework); + +const rows: [string, string][] = []; + +const docCount = fs.existsSync(finalDir) + ? fs.readdirSync(finalDir).filter(f => f.endsWith(".md") && !f.startsWith("_")).length + : 0; +rows.push(["Documents in framework", n(docCount)]); +rows.push(["Mode", `\`${mode}\``]); + +const manifest = readJson(path.resolve("dist", "diff-manifest.json")); +const manifestApplies = manifest && manifest.framework === framework; +const changed = manifestApplies ? (manifest.changed ?? []).length : 0; +const added = manifestApplies ? (manifest.added ?? []).length : 0; +const deleted = manifestApplies ? (manifest.deleted ?? []).length : 0; +const unchanged = manifestApplies ? (manifest.unchanged ?? []).length : 0; + +if (manifestApplies) { + rows.push([ + "Changed upstream", + changed + added + deleted === 0 + ? `none — all ${n(unchanged)} documents unchanged` + : `${n(changed)} changed, ${n(added)} added, ${n(deleted)} deleted (${n(unchanged)} unchanged)` + ]); +} + +const stats = readJson(path.join(finalDir, "_compression_stats.json")); +const batch = readJson(path.join(finalDir, "_batch_state.json")); + +if (!stats) { + rows.push(["Compression", manifestApplies && changed + added === 0 + ? "**skipped** — nothing to recompress" + : "**did not run**"]); +} else { + const errors = Array.isArray(stats.errors) ? stats.errors.length : Number(stats.errors ?? 0); + rows.push(["Documents compressed", `${n(stats.files_processed ?? 0)} of ${n(docCount)}`]); + if (stats.files_skipped) { + rows.push(["Skipped", n(stats.files_skipped)]); + } + rows.push(["Model", `\`${stats.model ?? "unknown"}\``]); + if (stats.original_size_kb && stats.compressed_size_kb) { + rows.push([ + "Size of compressed set", + `${kb(stats.original_size_kb)} → ${kb(stats.compressed_size_kb)} (${(stats.compression_ratio ?? 0).toFixed(1)}% smaller)` + ]); + } + rows.push([ + "Generated output", + `${n(stats.total_tokens ?? 0)} tokens — size of the produced documents, not API usage` + ]); + if (errors > 0) { + rows.push(["Errors", `**${n(errors)}**`]); + } +} + +if (batch) { + const failed = Number(batch.failed ?? 0) + Number(batch.invalid ?? 0); + rows.push([ + "Batch", + `\`${batch.batch_id}\` — ${batch.status}, ${n(Number(batch.succeeded ?? 0))} succeeded` + + (failed > 0 ? `, **${n(failed)} failed/invalid**` : "") + ]); +} + +const out: string[] = []; +out.push(`### ${framework}`); +out.push(""); +out.push("| | |"); +out.push("|---|---|"); +for (const [label, value] of rows) { + out.push(`| ${label} | ${value} |`); +} +out.push(""); + +console.log(out.join("\n")); diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/scripts/restore-docs-final.ts b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/restore-docs-final.ts new file mode 100644 index 000000000..a43ea81af --- /dev/null +++ b/packages/igniteui-mcp/igniteui-doc-mcp/scripts/restore-docs-final.ts @@ -0,0 +1,113 @@ +/** + * Restores dist/docs_final//*.md from the committed SQLite DB. + * + * Incremental compression only writes changed and added docs into docs_final — it + * assumes the unchanged ones are already there from an earlier run. dist/ is + * gitignored, so a fresh checkout (CI) has nothing. Without this step an incremental + * run would leave docs_final holding only the handful of changed files, and build-db + * would produce a near-empty database. + * + * The DB is committed and always matches what was last published, so it is the + * natural source. The frontmatter written here round-trips exactly through + * build-db.ts's parseFrontmatter(). + * + * Usage: + * npx tsx scripts/restore-docs-final.ts # all frameworks + * npx tsx scripts/restore-docs-final.ts --framework angular # one framework + * npx tsx scripts/restore-docs-final.ts --db path/to.db # non-default source + */ +import Database from "better-sqlite3"; +import * as fs from "fs"; +import * as path from "path"; + +const FRAMEWORKS = ["angular", "react", "blazor", "webcomponents"]; +const DOCS_FINAL_DIR = path.resolve("dist", "docs_final"); +const DOCS_PREPARED_DIR = path.resolve("dist", "docs_prepeared"); +const DEFAULT_DB = path.resolve("db", "igniteui-docs.db"); + +interface DocRow { + filename: string; + component: string; + premium: number; + keywords: string; + summary: string; + content: string; + toc_name: string | null; +} + +function buildDoc(row: DocRow): string { + const lines = ["---", `component: ${row.component}`]; + if (row.keywords) { + lines.push(`keywords: ${row.keywords}`); + } + if (row.summary) { + lines.push(`summary: ${row.summary}`); + } + if (row.premium) { + lines.push("premium: true"); + } + lines.push("---"); + // parseFrontmatter() strips exactly one newline after the closing ---, and the + // stored content keeps its own leading newline, so a single \n round-trips. + return `${lines.join("\n")}\n${row.content}`; +} + +function main(): void { + const args = process.argv.slice(2); + + const fwIdx = args.indexOf("--framework"); + const targetFramework = fwIdx !== -1 ? args[fwIdx + 1] : null; + if (targetFramework && !FRAMEWORKS.includes(targetFramework)) { + console.error(`Unknown framework: ${targetFramework}. Valid: ${FRAMEWORKS.join(", ")}`); + process.exit(1); + } + + // build-db reads _tocName out of docs_prepeared. When that directory is unavailable + // (the assemble job only has compressed docs), stubs carrying just _tocName keep + // toc_name populated instead of silently writing NULL for every row. + const tocStubs = args.includes("--toc-stubs"); + + const dbIdx = args.indexOf("--db"); + const dbPath = dbIdx !== -1 ? path.resolve(args[dbIdx + 1]) : DEFAULT_DB; + if (!fs.existsSync(dbPath)) { + console.error(`Database not found: ${dbPath}`); + process.exit(1); + } + + const db = new Database(dbPath, { readonly: true }); + const select = db.prepare( + "SELECT filename, component, premium, keywords, summary, content, toc_name FROM docs WHERE framework = ?" + ); + + let grandTotal = 0; + for (const fw of targetFramework ? [targetFramework] : FRAMEWORKS) { + const rows = select.all(fw) as DocRow[]; + if (rows.length === 0) { + console.warn(` [warn] ${fw}: no rows in ${path.basename(dbPath)} — nothing restored`); + continue; + } + + const outDir = path.join(DOCS_FINAL_DIR, fw); + fs.mkdirSync(outDir, { recursive: true }); + + const stubDir = path.join(DOCS_PREPARED_DIR, fw); + if (tocStubs) { + fs.mkdirSync(stubDir, { recursive: true }); + } + + for (const row of rows) { + fs.writeFileSync(path.join(outDir, row.filename), buildDoc(row), "utf-8"); + if (tocStubs && row.toc_name) { + fs.writeFileSync(path.join(stubDir, row.filename), `---\n_tocName: ${row.toc_name}\n---\n`, "utf-8"); + } + } + + grandTotal += rows.length; + console.log(` ${fw}: ${rows.length} docs restored to dist/docs_final/${fw}/${tocStubs ? " (+ toc stubs)" : ""}`); + } + + db.close(); + console.log(`\nRestored ${grandTotal} documents from ${dbPath}`); +} + +main(); diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/switch-submodules.sh b/packages/igniteui-mcp/igniteui-doc-mcp/switch-submodules.sh new file mode 100755 index 000000000..19e9f84f5 --- /dev/null +++ b/packages/igniteui-mcp/igniteui-doc-mcp/switch-submodules.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -euo pipefail + +BRANCH="${1:-master}" +BASE="$(cd "$(dirname "$0")" && pwd)" +SUBMODULES=( + angular/igniteui-angular + angular/igniteui-angular-examples + blazor/igniteui-blazor-examples + common/igniteui-xplat-docs + react/igniteui-react-examples + webcomponents/igniteui-wc-examples +) + +for sub in "${SUBMODULES[@]}"; do + dir="$BASE/$sub" + echo "--- $sub ---" + # CI checks out only the submodules the framework being built actually needs, so + # anything uninitialized here is skipped rather than aborting the run. + if [ ! -e "$dir/.git" ]; then + echo "not initialized — skipping" + continue + fi + git -C "$dir" fetch origin + if git -C "$dir" rev-parse --verify "origin/$BRANCH" >/dev/null 2>&1 \ + || git -C "$dir" fetch origin "$BRANCH:refs/remotes/origin/$BRANCH" 2>/dev/null; then + git -C "$dir" checkout "$BRANCH" && git -C "$dir" pull + else + echo "Branch '$BRANCH' not found, using master" + git -C "$dir" checkout master && git -C "$dir" pull + fi +done diff --git a/spec/unit/docs-db-counts-spec.ts b/spec/unit/docs-db-counts-spec.ts new file mode 100644 index 000000000..2e5f9f0d9 --- /dev/null +++ b/spec/unit/docs-db-counts-spec.ts @@ -0,0 +1,139 @@ +import * as fs from "fs"; +import * as path from "path"; + +const sqljs = require("sql.js"); +const initSqlJs: any = sqljs.default ?? sqljs; +const DB_PATH = process.env.DOCS_DB_PATH || + path.join(__dirname, "..", "..", "packages", "igniteui-mcp", "igniteui-doc-mcp", "db", "igniteui-docs.db"); + +const FRAMEWORKS = ["angular", "react", "blazor", "webcomponents"]; + +// Floors sit ~20% below the counts at the time of writing (angular 376, react 287, +// blazor 270, webcomponents 299). They tolerate ordinary doc churn but fail on a +// partial build — the failure mode that shipped a 112-doc and later an angular-only DB. +const MIN_DOCS: { [fw: string]: number } = { + angular: 300, + react: 230, + blazor: 215, + webcomponents: 240 +}; +const MIN_TOTAL = 1000; + +describe("Unit - documentation database", () => { + let db: any; + const counts: { [fw: string]: number } = {}; + let total = 0; + + function rows(sql: string): any[] { + const res = db.exec(sql); + if (!res.length) { + return []; + } + return res[0].values.map((v: any[]) => + res[0].columns.reduce((acc: any, col: string, i: number) => { + acc[col] = v[i]; + return acc; + }, {})); + } + + beforeAll(async () => { + expect(fs.existsSync(DB_PATH)).toBe(true, `Database not found at ${DB_PATH}. Run 'npm run build:db'.`); + + const wasm = fs.readFileSync(require.resolve("sql.js/dist/sql-wasm.wasm")); + const SQL = await initSqlJs({ + wasmBinary: wasm.buffer.slice(wasm.byteOffset, wasm.byteOffset + wasm.byteLength) + }); + db = new SQL.Database(fs.readFileSync(DB_PATH)); + + for (const r of rows("select framework, count(*) c from docs group by framework")) { + counts[r.framework] = r.c; + } + total = rows("select count(*) c from docs")[0].c; + }); + + afterAll(() => { + if (db) { + db.close(); + } + }); + + it("should contain every framework", () => { + expect(Object.keys(counts).sort()).toEqual(FRAMEWORKS.slice().sort()); + }); + + it("should meet the minimum document count per framework", () => { + for (const fw of FRAMEWORKS) { + expect(counts[fw] || 0) + .toBeGreaterThanOrEqual(MIN_DOCS[fw], `${fw} has ${counts[fw] || 0} docs, expected >= ${MIN_DOCS[fw]}`); + } + }); + + it("should have a total matching the sum of all frameworks", () => { + expect(total).toBeGreaterThanOrEqual(MIN_TOTAL); + expect(total).toEqual(FRAMEWORKS.reduce((sum, fw) => sum + (counts[fw] || 0), 0)); + }); + + it("should not have any framework starved relative to the others", () => { + // A partial build leaves one framework whole and the rest tiny. + const values = FRAMEWORKS.map(fw => counts[fw] || 0); + expect(Math.min(...values) / Math.max(...values)).toBeGreaterThan(0.4); + }); + + it("should not contain empty or truncated documents", () => { + const bad = rows("select framework, filename from docs where content is null or length(trim(content)) < 200"); + expect(bad.map(r => `${r.framework}/${r.filename}`)).toEqual([]); + }); + + it("should have required frontmatter on every document", () => { + const bad = rows(` + select framework, filename from docs + where summary is null or trim(summary) = '' + or keywords is null or trim(keywords) = '' + `); + expect(bad.map(r => `${r.framework}/${r.filename}`)).toEqual([]); + }); + + it("should only leave component empty when the document has no library component", () => { + // A few docs legitimately have none — CLI guides, migration walkthroughs. Any + // other empty value means the field was lost. Quote characters count as empty: + // the model has emitted `component: ""`, which reaches the DB as a component + // literally named `""` and shows up as one in list_components. + const bad = rows(` + select framework, filename, component, content from docs + where trim(replace(replace(coalesce(component, ''), '"', ''), '''', '')) = '' + `).filter(r => /\bIg[xrbc][A-Z]\w+/.test(r.content)); + expect(bad.map(r => `${r.framework}/${r.filename}`)).toEqual([]); + }); + + it("should not index documents under sample-application class names", () => { + // Every listed name should carry an Ignite UI prefix, be a documented API class, + // or at minimum not look like demo code. Sample classes such as MyComponent or + // ReactiveFormsSampleComponent make a document unreachable via list_components. + const suspect = /(^My[A-Z]|Sample(Component|Page)?$|^App(Component|Module)?$|Validator(Directive)?$)/; + const bad = rows("select framework, filename, component from docs where component is not null") + .filter(r => (r.component || "").split(",") + .map((s: string) => s.trim()) + .some((s: string) => s && !/^Ig[xrbc][A-Z]/.test(s) && suspect.test(s))); + expect(bad.map(r => `${r.framework}/${r.filename}: ${r.component}`)).toEqual([]); + }); + + it("should have a toc name on every document", () => { + // build-db reads _tocName from docs_prepeared; if that directory is missing it + // silently writes NULL for every row instead of failing. + const bad = rows("select framework, filename from docs where toc_name is null or trim(toc_name) = ''"); + expect(bad.map(r => `${r.framework}/${r.filename}`)).toEqual([]); + }); + + it("should not contain duplicate documents", () => { + const dupes = rows("select framework, filename from docs group by framework, filename having count(*) > 1"); + expect(dupes.map(r => `${r.framework}/${r.filename}`)).toEqual([]); + }); + + it("should keep the FTS index in sync with the docs table", () => { + expect(rows("select count(*) c from docs_fts")[0].c).toEqual(total); + }); + + it("should return results from a full-text search", () => { + expect(rows("select rowid from docs_fts where docs_fts match 'grid' limit 5").length).toBeGreaterThan(0); + }); +});