@nextsparkjs/ai-workflow 0.1.0-beta.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/claude/_docs/workflows-optimizations.md +359 -0
- package/claude/agents/api-tester.md +634 -0
- package/claude/agents/architecture-supervisor.md +1351 -0
- package/claude/agents/backend-developer.md +997 -0
- package/claude/agents/backend-validator.md +417 -0
- package/claude/agents/bdd-docs-writer.md +737 -0
- package/claude/agents/block-developer.md +677 -0
- package/claude/agents/code-reviewer.md +1432 -0
- package/claude/agents/db-developer.md +721 -0
- package/claude/agents/db-validator.md +407 -0
- package/claude/agents/demo-video-generator.md +493 -0
- package/claude/agents/documentation-writer.md +1268 -0
- package/claude/agents/frontend-developer.md +1234 -0
- package/claude/agents/frontend-validator.md +777 -0
- package/claude/agents/functional-validator.md +630 -0
- package/claude/agents/mock-analyst.md +387 -0
- package/claude/agents/product-manager.md +963 -0
- package/claude/agents/qa-automation.md +1762 -0
- package/claude/agents/release-manager.md +634 -0
- package/claude/agents/selectors-translator.md +262 -0
- package/claude/agents/unit-test-writer.md +785 -0
- package/claude/agents/visual-comparator.md +329 -0
- package/claude/agents/workflow-maintainer.md +352 -0
- package/claude/commands/do/README.md +88 -0
- package/claude/commands/do/create-api.md +64 -0
- package/claude/commands/do/create-entity.md +66 -0
- package/claude/commands/do/create-migration.md +64 -0
- package/claude/commands/do/create-plugin.md +56 -0
- package/claude/commands/do/create-theme.md +70 -0
- package/claude/commands/do/mock-data.md +67 -0
- package/claude/commands/do/reset-db.md +71 -0
- package/claude/commands/do/setup-scheduled-action.md +75 -0
- package/claude/commands/do/sync-code-review.md +117 -0
- package/claude/commands/do/update-selectors.md +112 -0
- package/claude/commands/do/use-skills.md +90 -0
- package/claude/commands/do/validate-blocks.md +69 -0
- package/claude/commands/how-to/README.md +261 -0
- package/claude/commands/how-to/add-metadata.md +692 -0
- package/claude/commands/how-to/add-taxonomies.md +806 -0
- package/claude/commands/how-to/add-translations.md +571 -0
- package/claude/commands/how-to/create-api.md +577 -0
- package/claude/commands/how-to/create-block.md +575 -0
- package/claude/commands/how-to/create-child-entities.md +771 -0
- package/claude/commands/how-to/create-entity.md +597 -0
- package/claude/commands/how-to/create-migrations.md +605 -0
- package/claude/commands/how-to/create-plugin.md +654 -0
- package/claude/commands/how-to/customize-app.md +481 -0
- package/claude/commands/how-to/customize-dashboard.md +553 -0
- package/claude/commands/how-to/customize-theme.md +438 -0
- package/claude/commands/how-to/define-features-flows.md +632 -0
- package/claude/commands/how-to/deploy.md +507 -0
- package/claude/commands/how-to/handle-file-uploads.md +746 -0
- package/claude/commands/how-to/implement-search.md +1001 -0
- package/claude/commands/how-to/install-plugins.md +352 -0
- package/claude/commands/how-to/manage-test-coverage.md +984 -0
- package/claude/commands/how-to/run-tests.md +400 -0
- package/claude/commands/how-to/set-app-languages.md +601 -0
- package/claude/commands/how-to/set-plans-and-permissions.md +575 -0
- package/claude/commands/how-to/set-scheduled-actions.md +527 -0
- package/claude/commands/how-to/set-user-roles-and-permissions.md +550 -0
- package/claude/commands/how-to/setup-authentication.md +388 -0
- package/claude/commands/how-to/setup-claude-code.md +440 -0
- package/claude/commands/how-to/setup-database.md +274 -0
- package/claude/commands/how-to/setup-email-providers.md +598 -0
- package/claude/commands/how-to/setup-mobile-dev.md +627 -0
- package/claude/commands/how-to/start.md +500 -0
- package/claude/commands/how-to/use-devtools.md +639 -0
- package/claude/commands/how-to/use-superadmin.md +622 -0
- package/claude/commands/session/README.md +193 -0
- package/claude/commands/session/block-create.md +190 -0
- package/claude/commands/session/block-list.md +203 -0
- package/claude/commands/session/block-update.md +192 -0
- package/claude/commands/session/block-validate.md +218 -0
- package/claude/commands/session/changelog.md +115 -0
- package/claude/commands/session/close.md +225 -0
- package/claude/commands/session/commit.md +174 -0
- package/claude/commands/session/db-entity.md +206 -0
- package/claude/commands/session/db-fix.md +212 -0
- package/claude/commands/session/db-sample.md +206 -0
- package/claude/commands/session/demo.md +178 -0
- package/claude/commands/session/doc-bdd.md +207 -0
- package/claude/commands/session/doc-feature.md +218 -0
- package/claude/commands/session/doc-read.md +225 -0
- package/claude/commands/session/execute.md +204 -0
- package/claude/commands/session/explain.md +202 -0
- package/claude/commands/session/fix-bug.md +210 -0
- package/claude/commands/session/fix-build.md +182 -0
- package/claude/commands/session/fix-test.md +189 -0
- package/claude/commands/session/pending.md +232 -0
- package/claude/commands/session/refine.md +188 -0
- package/claude/commands/session/resume.md +192 -0
- package/claude/commands/session/review.md +192 -0
- package/claude/commands/session/scope-change.md +181 -0
- package/claude/commands/session/start-blocks.md +347 -0
- package/claude/commands/session/start.md +604 -0
- package/claude/commands/session/status.md +169 -0
- package/claude/commands/session/test-fix.md +221 -0
- package/claude/commands/session/test-run.md +203 -0
- package/claude/commands/session/test-write.md +242 -0
- package/claude/commands/session/validate.md +162 -0
- package/claude/config/context.json +40 -0
- package/claude/config/github.json +69 -0
- package/claude/config/github.schema.json +106 -0
- package/claude/config/team.json +46 -0
- package/claude/config/team.schema.json +106 -0
- package/claude/config/workspace.json +43 -0
- package/claude/config/workspace.schema.json +75 -0
- package/claude/skills/README.md +228 -0
- package/claude/skills/accessibility/SKILL.md +573 -0
- package/claude/skills/api-bypass-layers/SKILL.md +550 -0
- package/claude/skills/asana-integration/SKILL.md +499 -0
- package/claude/skills/better-auth/SKILL.md +666 -0
- package/claude/skills/billing-subscriptions/SKILL.md +660 -0
- package/claude/skills/block-decision-matrix/SKILL.md +359 -0
- package/claude/skills/clickup-integration/SKILL.md +434 -0
- package/claude/skills/core-theme-responsibilities/SKILL.md +485 -0
- package/claude/skills/create-plugin/SKILL.md +425 -0
- package/claude/skills/create-theme/SKILL.md +331 -0
- package/claude/skills/cypress-api/SKILL.md +511 -0
- package/claude/skills/cypress-api/scripts/generate-api-controller.py +329 -0
- package/claude/skills/cypress-api/scripts/generate-api-test.py +930 -0
- package/claude/skills/cypress-e2e/SKILL.md +526 -0
- package/claude/skills/cypress-e2e/scripts/extract-selectors.py +383 -0
- package/claude/skills/cypress-e2e/scripts/generate-uat-test.py +788 -0
- package/claude/skills/cypress-selectors/SKILL.md +309 -0
- package/claude/skills/cypress-selectors/scripts/extract-missing.py +243 -0
- package/claude/skills/cypress-selectors/scripts/generate-block-selectors.py +283 -0
- package/claude/skills/cypress-selectors/scripts/validate-selectors.py +145 -0
- package/claude/skills/database-migrations/SKILL.md +335 -0
- package/claude/skills/database-migrations/scripts/generate-sample-data.py +284 -0
- package/claude/skills/database-migrations/scripts/validate-migration.py +323 -0
- package/claude/skills/design-system/SKILL.md +682 -0
- package/claude/skills/documentation/SKILL.md +540 -0
- package/claude/skills/entity-api/SKILL.md +482 -0
- package/claude/skills/entity-system/SKILL.md +635 -0
- package/claude/skills/entity-system/scripts/generate-child-migration.py +298 -0
- package/claude/skills/entity-system/scripts/generate-metas-migration.py +233 -0
- package/claude/skills/entity-system/scripts/generate-migration.py +382 -0
- package/claude/skills/entity-system/scripts/generate-sample-data.py +418 -0
- package/claude/skills/entity-system/scripts/scaffold-entity.py +661 -0
- package/claude/skills/github/SKILL.md +467 -0
- package/claude/skills/i18n-nextintl/SKILL.md +302 -0
- package/claude/skills/i18n-nextintl/scripts/add-translation.py +243 -0
- package/claude/skills/i18n-nextintl/scripts/extract-hardcoded.py +246 -0
- package/claude/skills/i18n-nextintl/scripts/validate-translations.py +260 -0
- package/claude/skills/impact-analysis/SKILL.md +203 -0
- package/claude/skills/jest-unit/SKILL.md +306 -0
- package/claude/skills/jest-unit/references/component-testing.md +371 -0
- package/claude/skills/jest-unit/references/mocking-patterns.md +380 -0
- package/claude/skills/jest-unit/references/service-hook-testing.md +454 -0
- package/claude/skills/jira-integration/SKILL.md +539 -0
- package/claude/skills/media-library/SKILL.md +743 -0
- package/claude/skills/mock-analysis/SKILL.md +276 -0
- package/claude/skills/monorepo-architecture/SKILL.md +162 -0
- package/claude/skills/nextjs-api-development/SKILL.md +364 -0
- package/claude/skills/nextjs-api-development/scripts/generate-crud-tests.py +456 -0
- package/claude/skills/nextjs-api-development/scripts/scaffold-endpoint.py +481 -0
- package/claude/skills/nextjs-api-development/scripts/validate-api.py +283 -0
- package/claude/skills/notion-integration/SKILL.md +641 -0
- package/claude/skills/npm-development-workflow/SKILL.md +480 -0
- package/claude/skills/page-builder-blocks/SKILL.md +530 -0
- package/claude/skills/page-builder-blocks/scripts/scaffold-block.py +444 -0
- package/claude/skills/permissions-system/SKILL.md +619 -0
- package/claude/skills/plugins/SKILL.md +340 -0
- package/claude/skills/plugins/references/plugin-templates.md +414 -0
- package/claude/skills/plugins/references/plugin-testing.md +353 -0
- package/claude/skills/plugins/references/plugin-types.md +198 -0
- package/claude/skills/plugins/scripts/scaffold-plugin.py +443 -0
- package/claude/skills/pom-patterns/SKILL.md +452 -0
- package/claude/skills/pom-patterns/scripts/generate-pom.py +392 -0
- package/claude/skills/rate-limiting/SKILL.md +342 -0
- package/claude/skills/react-best-practices/AGENTS.md +2410 -0
- package/claude/skills/react-best-practices/README.md +123 -0
- package/claude/skills/react-best-practices/SKILL.md +125 -0
- package/claude/skills/react-best-practices/metadata.json +15 -0
- package/claude/skills/react-best-practices/rules/_sections.md +46 -0
- package/claude/skills/react-best-practices/rules/_template.md +28 -0
- package/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/claude/skills/react-best-practices/rules/advanced-use-latest.md +49 -0
- package/claude/skills/react-best-practices/rules/async-api-routes.md +38 -0
- package/claude/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/claude/skills/react-best-practices/rules/async-dependencies.md +36 -0
- package/claude/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/claude/skills/react-best-practices/rules/bundle-conditional.md +31 -0
- package/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/claude/skills/react-best-practices/rules/bundle-preload.md +50 -0
- package/claude/skills/react-best-practices/rules/client-event-listeners.md +74 -0
- package/claude/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/claude/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/claude/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/claude/skills/react-best-practices/rules/js-batch-dom-css.md +82 -0
- package/claude/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/claude/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/claude/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/claude/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/claude/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/claude/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/claude/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/claude/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/claude/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/claude/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/claude/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/claude/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/claude/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/claude/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/claude/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/claude/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/claude/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/claude/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/claude/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/claude/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/claude/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/claude/skills/react-best-practices/rules/server-cache-react.md +76 -0
- package/claude/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/claude/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/claude/skills/react-patterns/SKILL.md +688 -0
- package/claude/skills/registry-system/SKILL.md +331 -0
- package/claude/skills/scheduled-actions/SKILL.md +671 -0
- package/claude/skills/scope-enforcement/SKILL.md +542 -0
- package/claude/skills/scope-enforcement/scripts/validate-scope.py +357 -0
- package/claude/skills/server-actions/SKILL.md +493 -0
- package/claude/skills/service-layer/SKILL.md +587 -0
- package/claude/skills/session-management/SKILL.md +266 -0
- package/claude/skills/session-management/scripts/create-session.py +166 -0
- package/claude/skills/session-management/scripts/iteration-close.sh +105 -0
- package/claude/skills/session-management/scripts/iteration-init.sh +180 -0
- package/claude/skills/session-management/scripts/session-archive.sh +87 -0
- package/claude/skills/session-management/scripts/session-close.sh +133 -0
- package/claude/skills/session-management/scripts/session-init.sh +225 -0
- package/claude/skills/session-management/scripts/session-list.sh +163 -0
- package/claude/skills/session-management/scripts/split-plan.sh +116 -0
- package/claude/skills/shadcn-components/SKILL.md +586 -0
- package/claude/skills/shadcn-theming/SKILL.md +446 -0
- package/claude/skills/suspense-loading/SKILL.md +280 -0
- package/claude/skills/tailwind-theming/SKILL.md +507 -0
- package/claude/skills/tanstack-query/SKILL.md +608 -0
- package/claude/skills/test-coverage/SKILL.md +239 -0
- package/claude/skills/web-design-guidelines/SKILL.md +39 -0
- package/claude/skills/zod-validation/SKILL.md +537 -0
- package/claude/templates/blocks/progress.md +86 -0
- package/claude/templates/iteration/changes.md +61 -0
- package/claude/templates/iteration/progress.md +55 -0
- package/claude/templates/log.md +31 -0
- package/claude/templates/story/context.md +77 -0
- package/claude/templates/story/pendings.md +37 -0
- package/claude/templates/story/plan.md +299 -0
- package/claude/templates/story/requirements.md +109 -0
- package/claude/templates/story/scope.json +10 -0
- package/claude/templates/story/tests.md +91 -0
- package/claude/templates/task/progress.md +58 -0
- package/claude/templates/task/requirements.md +54 -0
- package/claude/workflows/README.md +154 -0
- package/claude/workflows/blocks.md +614 -0
- package/claude/workflows/story.md +1207 -0
- package/claude/workflows/task.md +927 -0
- package/claude/workflows/tweak.md +527 -0
- package/cursor/.gitkeep +0 -0
- package/package.json +35 -0
- package/scripts/postinstall.mjs +198 -0
- package/scripts/setup.mjs +282 -0
- package/scripts/sync.mjs +209 -0
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
# /how-to:implement-search
|
|
2
|
+
|
|
3
|
+
Interactive tutorial for implementing search functionality using the client-side entity search system.
|
|
4
|
+
|
|
5
|
+
## Required Skills
|
|
6
|
+
- `.claude/skills/react-patterns/SKILL.md`
|
|
7
|
+
- `.claude/skills/tanstack-query/SKILL.md`
|
|
8
|
+
- `.claude/skills/entity-system/SKILL.md`
|
|
9
|
+
- `.claude/skills/cypress-selectors/SKILL.md`
|
|
10
|
+
|
|
11
|
+
## Syntax
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/how-to:implement-search [options]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Options:
|
|
18
|
+
- `--step <n>` - Start at specific step (1-5)
|
|
19
|
+
- `--quick` - Quick reference without explanations
|
|
20
|
+
|
|
21
|
+
## Behavior
|
|
22
|
+
|
|
23
|
+
This command guides you through implementing search in your NextSpark application.
|
|
24
|
+
It covers the useEntitySearch hook, search components, and custom implementations.
|
|
25
|
+
|
|
26
|
+
## Tutorial Structure
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
30
|
+
│ /how-to:implement-search - 5 Steps Overview │
|
|
31
|
+
├─────────────────────────────────────────────────────────────┤
|
|
32
|
+
│ │
|
|
33
|
+
│ [1] Understanding Search → Client-side architecture │
|
|
34
|
+
│ │ │
|
|
35
|
+
│ ↓ │
|
|
36
|
+
│ [2] useEntitySearch Hook → Hook usage & configuration │
|
|
37
|
+
│ │ │
|
|
38
|
+
│ ↓ │
|
|
39
|
+
│ [3] Search Results → EntitySearchResult types │
|
|
40
|
+
│ │ │
|
|
41
|
+
│ ↓ │
|
|
42
|
+
│ [4] EntitySearch Component → Pre-built component variants │
|
|
43
|
+
│ │ │
|
|
44
|
+
│ ↓ │
|
|
45
|
+
│ [5] Custom Search → Advanced patterns │
|
|
46
|
+
│ │
|
|
47
|
+
└─────────────────────────────────────────────────────────────┘
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Step 1: Understanding Search Architecture
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
56
|
+
│ STEP 1: SEARCH ARCHITECTURE │
|
|
57
|
+
│ Client-side search with entity integration │
|
|
58
|
+
└─────────────────────────────────────────────────────────────┘
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### How Search Works in NextSpark
|
|
62
|
+
|
|
63
|
+
NextSpark uses **client-side search** that integrates with the Entity System:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
67
|
+
│ SEARCH FLOW │
|
|
68
|
+
├─────────────────────────────────────────────────────────────┤
|
|
69
|
+
│ │
|
|
70
|
+
│ [User Input] → [Debounce 150ms] → [Search Function] │
|
|
71
|
+
│ │ │
|
|
72
|
+
│ ↓ │
|
|
73
|
+
│ ┌──────────────────────────────────────────────────────┐ │
|
|
74
|
+
│ │ SEARCH SOURCES │ │
|
|
75
|
+
│ ├──────────────────────────────────────────────────────┤ │
|
|
76
|
+
│ │ • Entity Registry (searchable entities) │ │
|
|
77
|
+
│ │ • System Pages (dashboard, settings) │ │
|
|
78
|
+
│ │ • User Plan/Permission Filtering │ │
|
|
79
|
+
│ └──────────────────────────────────────────────────────┘ │
|
|
80
|
+
│ │ │
|
|
81
|
+
│ ↓ │
|
|
82
|
+
│ [Relevance Scoring] → [Sort Results] → [Return Top N] │
|
|
83
|
+
│ │
|
|
84
|
+
└─────────────────────────────────────────────────────────────┘
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Key Characteristics
|
|
88
|
+
|
|
89
|
+
1. **Client-Side Execution**
|
|
90
|
+
- No server round-trip for basic search
|
|
91
|
+
- Instant results with debouncing
|
|
92
|
+
- Entity registry populated by server component
|
|
93
|
+
|
|
94
|
+
2. **Multi-Source Search**
|
|
95
|
+
- Entities: From entity registry
|
|
96
|
+
- System Pages: Dashboard, settings, profile
|
|
97
|
+
- Filtered by user permissions and plan
|
|
98
|
+
|
|
99
|
+
3. **Relevance Scoring**
|
|
100
|
+
- Exact title match: +100 points
|
|
101
|
+
- Title starts with query: +50 points
|
|
102
|
+
- Title contains query: +25 points
|
|
103
|
+
- Entity type match: +80 points
|
|
104
|
+
- Description match: +10 points
|
|
105
|
+
|
|
106
|
+
4. **Configuration**
|
|
107
|
+
- Minimum 2 characters to trigger search
|
|
108
|
+
- Maximum 8-12 results returned
|
|
109
|
+
- 150ms debounce delay
|
|
110
|
+
|
|
111
|
+
### Making Entities Searchable
|
|
112
|
+
|
|
113
|
+
To make an entity searchable, enable in config:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// entity.config.ts
|
|
117
|
+
export const productConfig: EntityConfig = {
|
|
118
|
+
slug: 'products',
|
|
119
|
+
// ...
|
|
120
|
+
ui: {
|
|
121
|
+
features: {
|
|
122
|
+
searchable: true, // Enable for global search
|
|
123
|
+
// ...
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
fields: [
|
|
127
|
+
{
|
|
128
|
+
name: 'name',
|
|
129
|
+
type: 'text',
|
|
130
|
+
api: {
|
|
131
|
+
searchable: true, // This field is searchable
|
|
132
|
+
// ...
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Search Result Priority
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
143
|
+
│ RESULT PRIORITY ORDER │
|
|
144
|
+
├─────────────────────────────────────────────────────────────┤
|
|
145
|
+
│ │
|
|
146
|
+
│ 1. Relevance Score (highest first) │
|
|
147
|
+
│ 2. Entity vs System (entities prioritized) │
|
|
148
|
+
│ 3. Priority Level (high > medium > low) │
|
|
149
|
+
│ 4. Alphabetical (as tiebreaker) │
|
|
150
|
+
│ │
|
|
151
|
+
└─────────────────────────────────────────────────────────────┘
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Step 2: useEntitySearch Hook
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
160
|
+
│ STEP 2: useEntitySearch HOOK │
|
|
161
|
+
│ Primary hook for search functionality │
|
|
162
|
+
└─────────────────────────────────────────────────────────────┘
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Hook Location
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
/packages/core/src/hooks/useEntitySearch.ts
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Basic Usage
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
'use client'
|
|
175
|
+
|
|
176
|
+
import { useEntitySearch } from '@nextsparkjs/core/hooks'
|
|
177
|
+
|
|
178
|
+
export function SearchComponent() {
|
|
179
|
+
const {
|
|
180
|
+
query,
|
|
181
|
+
setQuery,
|
|
182
|
+
results,
|
|
183
|
+
isSearching,
|
|
184
|
+
clearSearch,
|
|
185
|
+
hasResults,
|
|
186
|
+
isEmpty,
|
|
187
|
+
availableEntities
|
|
188
|
+
} = useEntitySearch()
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div>
|
|
192
|
+
<input
|
|
193
|
+
type="text"
|
|
194
|
+
value={query}
|
|
195
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
196
|
+
placeholder="Search..."
|
|
197
|
+
data-cy="search-input"
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
{isSearching && <p>Searching...</p>}
|
|
201
|
+
|
|
202
|
+
{hasResults && (
|
|
203
|
+
<ul>
|
|
204
|
+
{results.map(result => (
|
|
205
|
+
<li key={result.id}>
|
|
206
|
+
<a href={result.url}>{result.title}</a>
|
|
207
|
+
</li>
|
|
208
|
+
))}
|
|
209
|
+
</ul>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Hook Return Values
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
220
|
+
│ useEntitySearch() RETURN TYPE │
|
|
221
|
+
├─────────────────────────────────────────────────────────────┤
|
|
222
|
+
│ │
|
|
223
|
+
│ interface EntitySearchHookResult { │
|
|
224
|
+
│ query: string // Current search query │
|
|
225
|
+
│ setQuery: (q) => void // Update query function │
|
|
226
|
+
│ results: EntitySearchResult[] // Search results │
|
|
227
|
+
│ isSearching: boolean // Loading state │
|
|
228
|
+
│ clearSearch: () => void // Reset search state │
|
|
229
|
+
│ hasResults: boolean // results.length > 0 │
|
|
230
|
+
│ isEmpty: boolean // query.trim() === '' │
|
|
231
|
+
│ availableEntities: EntityConfig[] // Searchable entities│
|
|
232
|
+
│ } │
|
|
233
|
+
│ │
|
|
234
|
+
└─────────────────────────────────────────────────────────────┘
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Complete Example with Keyboard Support
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
'use client'
|
|
241
|
+
|
|
242
|
+
import { useEntitySearch } from '@nextsparkjs/core/hooks'
|
|
243
|
+
import { useRouter } from 'next/navigation'
|
|
244
|
+
import { useEffect, useRef, useState } from 'react'
|
|
245
|
+
|
|
246
|
+
export function GlobalSearch() {
|
|
247
|
+
const {
|
|
248
|
+
query,
|
|
249
|
+
setQuery,
|
|
250
|
+
results,
|
|
251
|
+
isSearching,
|
|
252
|
+
clearSearch,
|
|
253
|
+
hasResults
|
|
254
|
+
} = useEntitySearch()
|
|
255
|
+
|
|
256
|
+
const router = useRouter()
|
|
257
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
258
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
259
|
+
|
|
260
|
+
// Keyboard navigation
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
263
|
+
// Cmd/Ctrl + K to focus
|
|
264
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
265
|
+
e.preventDefault()
|
|
266
|
+
inputRef.current?.focus()
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
271
|
+
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
272
|
+
}, [])
|
|
273
|
+
|
|
274
|
+
// Result keyboard navigation
|
|
275
|
+
const handleInputKeyDown = (e: React.KeyboardEvent) => {
|
|
276
|
+
if (e.key === 'ArrowDown') {
|
|
277
|
+
e.preventDefault()
|
|
278
|
+
setSelectedIndex(prev =>
|
|
279
|
+
Math.min(prev + 1, results.length - 1)
|
|
280
|
+
)
|
|
281
|
+
} else if (e.key === 'ArrowUp') {
|
|
282
|
+
e.preventDefault()
|
|
283
|
+
setSelectedIndex(prev => Math.max(prev - 1, 0))
|
|
284
|
+
} else if (e.key === 'Enter' && hasResults) {
|
|
285
|
+
e.preventDefault()
|
|
286
|
+
router.push(results[selectedIndex].url)
|
|
287
|
+
clearSearch()
|
|
288
|
+
} else if (e.key === 'Escape') {
|
|
289
|
+
clearSearch()
|
|
290
|
+
inputRef.current?.blur()
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Reset selection on query change
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
setSelectedIndex(0)
|
|
297
|
+
}, [query])
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<div className="relative" data-cy="global-search">
|
|
301
|
+
<input
|
|
302
|
+
ref={inputRef}
|
|
303
|
+
type="text"
|
|
304
|
+
value={query}
|
|
305
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
306
|
+
onKeyDown={handleInputKeyDown}
|
|
307
|
+
placeholder="Search... (Cmd+K)"
|
|
308
|
+
className="w-full px-4 py-2 border rounded"
|
|
309
|
+
data-cy="search-input"
|
|
310
|
+
/>
|
|
311
|
+
|
|
312
|
+
{hasResults && (
|
|
313
|
+
<ul className="absolute w-full mt-1 bg-white border rounded shadow-lg">
|
|
314
|
+
{results.map((result, index) => (
|
|
315
|
+
<li
|
|
316
|
+
key={result.id}
|
|
317
|
+
className={`px-4 py-2 cursor-pointer ${
|
|
318
|
+
index === selectedIndex ? 'bg-blue-100' : ''
|
|
319
|
+
}`}
|
|
320
|
+
onClick={() => {
|
|
321
|
+
router.push(result.url)
|
|
322
|
+
clearSearch()
|
|
323
|
+
}}
|
|
324
|
+
data-cy="search-result"
|
|
325
|
+
>
|
|
326
|
+
<div className="font-medium">{result.title}</div>
|
|
327
|
+
{result.description && (
|
|
328
|
+
<div className="text-sm text-gray-500">
|
|
329
|
+
{result.description}
|
|
330
|
+
</div>
|
|
331
|
+
)}
|
|
332
|
+
</li>
|
|
333
|
+
))}
|
|
334
|
+
</ul>
|
|
335
|
+
)}
|
|
336
|
+
</div>
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Step 3: Search Results Structure
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
347
|
+
│ STEP 3: SEARCH RESULTS STRUCTURE │
|
|
348
|
+
│ Understanding EntitySearchResult type │
|
|
349
|
+
└─────────────────────────────────────────────────────────────┘
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### EntitySearchResult Interface
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
interface EntitySearchResult {
|
|
356
|
+
id: string // Unique identifier
|
|
357
|
+
title: string // Display title
|
|
358
|
+
description?: string // Optional description
|
|
359
|
+
entityType: string // Entity slug or 'system'
|
|
360
|
+
type: 'entity' | 'system' // Result type
|
|
361
|
+
url: string // Navigation URL
|
|
362
|
+
category?: string // Category for grouping
|
|
363
|
+
priority?: 'low' | 'medium' | 'high' // Display priority
|
|
364
|
+
completed?: boolean // For task-like entities
|
|
365
|
+
icon?: string // Icon name (Lucide)
|
|
366
|
+
limitInfo?: { // Plan limit information
|
|
367
|
+
current: number
|
|
368
|
+
max: number | 'unlimited'
|
|
369
|
+
canCreate: boolean
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Result Types Explained
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
378
|
+
│ RESULT TYPES │
|
|
379
|
+
├─────────────────────────────────────────────────────────────┤
|
|
380
|
+
│ │
|
|
381
|
+
│ TYPE: 'entity' │
|
|
382
|
+
│ ├── From entity registry │
|
|
383
|
+
│ ├── URL: /dashboard/{entity.slug} │
|
|
384
|
+
│ ├── Category: 'Entities' │
|
|
385
|
+
│ └── Priority: 'medium' (default) │
|
|
386
|
+
│ │
|
|
387
|
+
│ TYPE: 'system' │
|
|
388
|
+
│ ├── Static system pages │
|
|
389
|
+
│ ├── URL: /dashboard/settings/{page} │
|
|
390
|
+
│ ├── Category: 'Navigation' | 'Settings' │
|
|
391
|
+
│ └── Priority: 'low' (default) │
|
|
392
|
+
│ │
|
|
393
|
+
└─────────────────────────────────────────────────────────────┘
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Working with Results
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
// Grouping results by category
|
|
400
|
+
const groupedResults = useMemo(() => {
|
|
401
|
+
const groups = new Map<string, EntitySearchResult[]>()
|
|
402
|
+
|
|
403
|
+
results.forEach(result => {
|
|
404
|
+
const category = result.category || 'Other'
|
|
405
|
+
const group = groups.get(category) || []
|
|
406
|
+
group.push(result)
|
|
407
|
+
groups.set(category, group)
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
return groups
|
|
411
|
+
}, [results])
|
|
412
|
+
|
|
413
|
+
// Render grouped results
|
|
414
|
+
return (
|
|
415
|
+
<div>
|
|
416
|
+
{Array.from(groupedResults.entries()).map(([category, items]) => (
|
|
417
|
+
<div key={category}>
|
|
418
|
+
<h3 className="font-bold">{category}</h3>
|
|
419
|
+
<ul>
|
|
420
|
+
{items.map(item => (
|
|
421
|
+
<li key={item.id}>
|
|
422
|
+
{item.type === 'entity' && (
|
|
423
|
+
<span className="text-blue-500">[Entity]</span>
|
|
424
|
+
)}
|
|
425
|
+
{item.title}
|
|
426
|
+
</li>
|
|
427
|
+
))}
|
|
428
|
+
</ul>
|
|
429
|
+
</div>
|
|
430
|
+
))}
|
|
431
|
+
</div>
|
|
432
|
+
)
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Displaying Plan Limits
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
// Show limit information for entity results
|
|
439
|
+
function ResultWithLimits({ result }: { result: EntitySearchResult }) {
|
|
440
|
+
const { limitInfo } = result
|
|
441
|
+
|
|
442
|
+
if (!limitInfo || result.type !== 'entity') {
|
|
443
|
+
return <span>{result.title}</span>
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const limitDisplay = limitInfo.max === 'unlimited'
|
|
447
|
+
? 'Unlimited'
|
|
448
|
+
: `${limitInfo.current}/${limitInfo.max}`
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<div className="flex items-center gap-2">
|
|
452
|
+
<span>{result.title}</span>
|
|
453
|
+
<span className={`text-xs ${
|
|
454
|
+
limitInfo.canCreate ? 'text-green-500' : 'text-red-500'
|
|
455
|
+
}`}>
|
|
456
|
+
({limitDisplay})
|
|
457
|
+
</span>
|
|
458
|
+
</div>
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Step 4: EntitySearch Component
|
|
466
|
+
|
|
467
|
+
```
|
|
468
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
469
|
+
│ STEP 4: EntitySearch COMPONENT │
|
|
470
|
+
│ Pre-built search component with variants │
|
|
471
|
+
└─────────────────────────────────────────────────────────────┘
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Component Location
|
|
475
|
+
|
|
476
|
+
```
|
|
477
|
+
/packages/core/src/components/entities/EntitySearch.tsx
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Available Variants
|
|
481
|
+
|
|
482
|
+
```
|
|
483
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
484
|
+
│ EntitySearch VARIANTS │
|
|
485
|
+
├─────────────────────────────────────────────────────────────┤
|
|
486
|
+
│ │
|
|
487
|
+
│ 'inline' - Full search with results below input │
|
|
488
|
+
│ 'dropdown' - Compact dropdown with popover results │
|
|
489
|
+
│ 'modal' - Full-screen modal search (EntitySearchModal) │
|
|
490
|
+
│ │
|
|
491
|
+
└─────────────────────────────────────────────────────────────┘
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Basic Usage
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
// Import from core
|
|
498
|
+
import { EntitySearch } from '@nextsparkjs/core/components/entities'
|
|
499
|
+
|
|
500
|
+
// In a Server Component - fetch entities
|
|
501
|
+
import { getEnabledEntities } from '@nextsparkjs/core/lib/entities/registry'
|
|
502
|
+
|
|
503
|
+
export default async function SearchPage() {
|
|
504
|
+
const entities = getEnabledEntities()
|
|
505
|
+
|
|
506
|
+
return (
|
|
507
|
+
<div>
|
|
508
|
+
<h1>Search</h1>
|
|
509
|
+
<EntitySearch
|
|
510
|
+
entities={entities}
|
|
511
|
+
placeholder="Search everything..."
|
|
512
|
+
variant="inline"
|
|
513
|
+
enableEntityFilter
|
|
514
|
+
maxResults={20}
|
|
515
|
+
/>
|
|
516
|
+
</div>
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Component Props
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
interface EntitySearchProps {
|
|
525
|
+
/** REQUIRED: Entity configs from server component */
|
|
526
|
+
entities: EntityConfig[]
|
|
527
|
+
|
|
528
|
+
/** Placeholder text */
|
|
529
|
+
placeholder?: string // default: 'Search everything...'
|
|
530
|
+
|
|
531
|
+
/** Display variant */
|
|
532
|
+
variant?: 'inline' | 'modal' | 'dropdown' // default: 'inline'
|
|
533
|
+
|
|
534
|
+
/** Custom result click handler */
|
|
535
|
+
onResultClick?: (result: SearchResult) => void
|
|
536
|
+
|
|
537
|
+
/** Search callback */
|
|
538
|
+
onSearch?: (query: string, entityName?: string) => void
|
|
539
|
+
|
|
540
|
+
/** Enable entity type filter */
|
|
541
|
+
enableEntityFilter?: boolean // default: true
|
|
542
|
+
|
|
543
|
+
/** Maximum results to show */
|
|
544
|
+
maxResults?: number // default: 20
|
|
545
|
+
|
|
546
|
+
/** Additional CSS classes */
|
|
547
|
+
className?: string
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Dropdown Variant Example
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
// Compact search in header/navbar
|
|
555
|
+
export function HeaderSearch({ entities }: { entities: EntityConfig[] }) {
|
|
556
|
+
return (
|
|
557
|
+
<EntitySearch
|
|
558
|
+
entities={entities}
|
|
559
|
+
variant="dropdown"
|
|
560
|
+
placeholder="Quick search..."
|
|
561
|
+
maxResults={8}
|
|
562
|
+
data-cy="header-search"
|
|
563
|
+
/>
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Modal Variant Example
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
import {
|
|
572
|
+
EntitySearchModal
|
|
573
|
+
} from '@nextsparkjs/core/components/entities'
|
|
574
|
+
import { useState } from 'react'
|
|
575
|
+
|
|
576
|
+
export function SearchWithModal({ entities }: { entities: EntityConfig[] }) {
|
|
577
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
578
|
+
|
|
579
|
+
// Open with keyboard shortcut
|
|
580
|
+
useEffect(() => {
|
|
581
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
582
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
583
|
+
e.preventDefault()
|
|
584
|
+
setIsOpen(true)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
589
|
+
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
590
|
+
}, [])
|
|
591
|
+
|
|
592
|
+
return (
|
|
593
|
+
<>
|
|
594
|
+
<button
|
|
595
|
+
onClick={() => setIsOpen(true)}
|
|
596
|
+
data-cy="open-search-modal"
|
|
597
|
+
>
|
|
598
|
+
Search (Cmd+K)
|
|
599
|
+
</button>
|
|
600
|
+
|
|
601
|
+
<EntitySearchModal
|
|
602
|
+
isOpen={isOpen}
|
|
603
|
+
onClose={() => setIsOpen(false)}
|
|
604
|
+
entities={entities}
|
|
605
|
+
onResultClick={(result) => {
|
|
606
|
+
console.log('Selected:', result)
|
|
607
|
+
}}
|
|
608
|
+
/>
|
|
609
|
+
</>
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### Custom Result Click Handler
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
export function SearchWithCustomAction({ entities }: { entities: EntityConfig[] }) {
|
|
618
|
+
const handleResultClick = (result: SearchResult) => {
|
|
619
|
+
// Custom action instead of navigation
|
|
620
|
+
if (result.entityName === 'tasks') {
|
|
621
|
+
// Open task in sidebar
|
|
622
|
+
openTaskSidebar(result.id)
|
|
623
|
+
} else {
|
|
624
|
+
// Default navigation
|
|
625
|
+
window.location.href = result.url
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return (
|
|
630
|
+
<EntitySearch
|
|
631
|
+
entities={entities}
|
|
632
|
+
onResultClick={handleResultClick}
|
|
633
|
+
/>
|
|
634
|
+
)
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## Step 5: Custom Search Implementation
|
|
641
|
+
|
|
642
|
+
```
|
|
643
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
644
|
+
│ STEP 5: CUSTOM SEARCH IMPLEMENTATION │
|
|
645
|
+
│ Advanced patterns for specialized search │
|
|
646
|
+
└─────────────────────────────────────────────────────────────┘
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### Custom Relevance Scoring
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
// Custom scoring function for your domain
|
|
653
|
+
function calculateRelevanceScore(
|
|
654
|
+
item: EntitySearchResult,
|
|
655
|
+
query: string
|
|
656
|
+
): number {
|
|
657
|
+
const lowerQuery = query.toLowerCase()
|
|
658
|
+
let score = 0
|
|
659
|
+
|
|
660
|
+
// Exact title match
|
|
661
|
+
if (item.title.toLowerCase() === lowerQuery) {
|
|
662
|
+
score += 100
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Title starts with query
|
|
666
|
+
if (item.title.toLowerCase().startsWith(lowerQuery)) {
|
|
667
|
+
score += 50
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Title contains query
|
|
671
|
+
if (item.title.toLowerCase().includes(lowerQuery)) {
|
|
672
|
+
score += 25
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Entity type matches
|
|
676
|
+
if (item.entityType.toLowerCase() === lowerQuery) {
|
|
677
|
+
score += 80
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Description contains query
|
|
681
|
+
if (item.description?.toLowerCase().includes(lowerQuery)) {
|
|
682
|
+
score += 10
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Boost entity results
|
|
686
|
+
if (item.type === 'entity') {
|
|
687
|
+
score += 5
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Custom domain-specific boosts
|
|
691
|
+
if (item.priority === 'high') {
|
|
692
|
+
score += 20
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return score
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### API-Backed Search
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
'use client'
|
|
703
|
+
|
|
704
|
+
import { useState, useCallback } from 'react'
|
|
705
|
+
import { useDebounce } from '@nextsparkjs/core/hooks'
|
|
706
|
+
|
|
707
|
+
interface APISearchResult {
|
|
708
|
+
id: string
|
|
709
|
+
title: string
|
|
710
|
+
entityType: string
|
|
711
|
+
score: number
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export function useAPISearch(entitySlug: string) {
|
|
715
|
+
const [query, setQuery] = useState('')
|
|
716
|
+
const [results, setResults] = useState<APISearchResult[]>([])
|
|
717
|
+
const [isSearching, setIsSearching] = useState(false)
|
|
718
|
+
|
|
719
|
+
const debouncedQuery = useDebounce(query, 300)
|
|
720
|
+
|
|
721
|
+
// Search effect
|
|
722
|
+
useEffect(() => {
|
|
723
|
+
if (!debouncedQuery.trim() || debouncedQuery.length < 2) {
|
|
724
|
+
setResults([])
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const performSearch = async () => {
|
|
729
|
+
setIsSearching(true)
|
|
730
|
+
|
|
731
|
+
try {
|
|
732
|
+
const response = await fetch(
|
|
733
|
+
`/api/v1/${entitySlug}?search=${encodeURIComponent(debouncedQuery)}`
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
if (!response.ok) throw new Error('Search failed')
|
|
737
|
+
|
|
738
|
+
const data = await response.json()
|
|
739
|
+
setResults(data.data || [])
|
|
740
|
+
} catch (error) {
|
|
741
|
+
console.error('Search error:', error)
|
|
742
|
+
setResults([])
|
|
743
|
+
} finally {
|
|
744
|
+
setIsSearching(false)
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
performSearch()
|
|
749
|
+
}, [debouncedQuery, entitySlug])
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
query,
|
|
753
|
+
setQuery,
|
|
754
|
+
results,
|
|
755
|
+
isSearching,
|
|
756
|
+
clearSearch: useCallback(() => {
|
|
757
|
+
setQuery('')
|
|
758
|
+
setResults([])
|
|
759
|
+
}, [])
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### Multi-Entity Search with Filters
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
'use client'
|
|
768
|
+
|
|
769
|
+
import { useState, useMemo } from 'react'
|
|
770
|
+
import { useEntitySearch } from '@nextsparkjs/core/hooks'
|
|
771
|
+
|
|
772
|
+
interface SearchFilters {
|
|
773
|
+
entityTypes: string[]
|
|
774
|
+
dateRange?: { from: Date; to: Date }
|
|
775
|
+
priority?: 'low' | 'medium' | 'high'
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
export function useFilteredSearch() {
|
|
779
|
+
const search = useEntitySearch()
|
|
780
|
+
const [filters, setFilters] = useState<SearchFilters>({
|
|
781
|
+
entityTypes: []
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
// Filter results based on active filters
|
|
785
|
+
const filteredResults = useMemo(() => {
|
|
786
|
+
let results = search.results
|
|
787
|
+
|
|
788
|
+
// Filter by entity types
|
|
789
|
+
if (filters.entityTypes.length > 0) {
|
|
790
|
+
results = results.filter(r =>
|
|
791
|
+
filters.entityTypes.includes(r.entityType)
|
|
792
|
+
)
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Filter by priority
|
|
796
|
+
if (filters.priority) {
|
|
797
|
+
results = results.filter(r =>
|
|
798
|
+
r.priority === filters.priority
|
|
799
|
+
)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return results
|
|
803
|
+
}, [search.results, filters])
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
...search,
|
|
807
|
+
results: filteredResults,
|
|
808
|
+
filters,
|
|
809
|
+
setFilters,
|
|
810
|
+
// Available entity types from results
|
|
811
|
+
availableTypes: useMemo(() => {
|
|
812
|
+
const types = new Set(search.results.map(r => r.entityType))
|
|
813
|
+
return Array.from(types)
|
|
814
|
+
}, [search.results])
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Usage
|
|
819
|
+
function FilteredSearchUI() {
|
|
820
|
+
const {
|
|
821
|
+
query,
|
|
822
|
+
setQuery,
|
|
823
|
+
results,
|
|
824
|
+
filters,
|
|
825
|
+
setFilters,
|
|
826
|
+
availableTypes
|
|
827
|
+
} = useFilteredSearch()
|
|
828
|
+
|
|
829
|
+
return (
|
|
830
|
+
<div>
|
|
831
|
+
<input
|
|
832
|
+
value={query}
|
|
833
|
+
onChange={e => setQuery(e.target.value)}
|
|
834
|
+
data-cy="filtered-search-input"
|
|
835
|
+
/>
|
|
836
|
+
|
|
837
|
+
<div className="filters">
|
|
838
|
+
{availableTypes.map(type => (
|
|
839
|
+
<label key={type}>
|
|
840
|
+
<input
|
|
841
|
+
type="checkbox"
|
|
842
|
+
checked={filters.entityTypes.includes(type)}
|
|
843
|
+
onChange={e => {
|
|
844
|
+
setFilters(prev => ({
|
|
845
|
+
...prev,
|
|
846
|
+
entityTypes: e.target.checked
|
|
847
|
+
? [...prev.entityTypes, type]
|
|
848
|
+
: prev.entityTypes.filter(t => t !== type)
|
|
849
|
+
}))
|
|
850
|
+
}}
|
|
851
|
+
data-cy={`filter-${type}`}
|
|
852
|
+
/>
|
|
853
|
+
{type}
|
|
854
|
+
</label>
|
|
855
|
+
))}
|
|
856
|
+
</div>
|
|
857
|
+
|
|
858
|
+
<ul>
|
|
859
|
+
{results.map(r => (
|
|
860
|
+
<li key={r.id} data-cy="search-result">
|
|
861
|
+
{r.title}
|
|
862
|
+
</li>
|
|
863
|
+
))}
|
|
864
|
+
</ul>
|
|
865
|
+
</div>
|
|
866
|
+
)
|
|
867
|
+
}
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
### Search with Highlighting
|
|
871
|
+
|
|
872
|
+
```typescript
|
|
873
|
+
// Highlight matches in search results
|
|
874
|
+
function highlightMatches(
|
|
875
|
+
text: string,
|
|
876
|
+
query: string
|
|
877
|
+
): React.ReactNode {
|
|
878
|
+
if (!query.trim()) return text
|
|
879
|
+
|
|
880
|
+
const parts = text.split(new RegExp(`(${query})`, 'gi'))
|
|
881
|
+
|
|
882
|
+
return (
|
|
883
|
+
<>
|
|
884
|
+
{parts.map((part, i) =>
|
|
885
|
+
part.toLowerCase() === query.toLowerCase() ? (
|
|
886
|
+
<mark key={i} className="bg-yellow-200 px-0.5 rounded">
|
|
887
|
+
{part}
|
|
888
|
+
</mark>
|
|
889
|
+
) : (
|
|
890
|
+
part
|
|
891
|
+
)
|
|
892
|
+
)}
|
|
893
|
+
</>
|
|
894
|
+
)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Usage in results
|
|
898
|
+
function SearchResultItem({
|
|
899
|
+
result,
|
|
900
|
+
query
|
|
901
|
+
}: {
|
|
902
|
+
result: EntitySearchResult
|
|
903
|
+
query: string
|
|
904
|
+
}) {
|
|
905
|
+
return (
|
|
906
|
+
<div data-cy="search-result">
|
|
907
|
+
<h4>{highlightMatches(result.title, query)}</h4>
|
|
908
|
+
{result.description && (
|
|
909
|
+
<p className="text-sm text-gray-500">
|
|
910
|
+
{highlightMatches(result.description, query)}
|
|
911
|
+
</p>
|
|
912
|
+
)}
|
|
913
|
+
</div>
|
|
914
|
+
)
|
|
915
|
+
}
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
### Test Selectors for Search
|
|
919
|
+
|
|
920
|
+
```typescript
|
|
921
|
+
// Always use data-cy attributes for search components
|
|
922
|
+
const SEARCH_SELECTORS = {
|
|
923
|
+
container: 'search-container',
|
|
924
|
+
input: 'search-input',
|
|
925
|
+
results: 'search-results',
|
|
926
|
+
result: 'search-result',
|
|
927
|
+
resultLink: 'search-result-link',
|
|
928
|
+
clearButton: 'search-clear',
|
|
929
|
+
filter: (type: string) => `search-filter-${type}`,
|
|
930
|
+
loading: 'search-loading',
|
|
931
|
+
empty: 'search-empty'
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Usage in component
|
|
935
|
+
<div data-cy={SEARCH_SELECTORS.container}>
|
|
936
|
+
<input data-cy={SEARCH_SELECTORS.input} />
|
|
937
|
+
<div data-cy={SEARCH_SELECTORS.results}>
|
|
938
|
+
{results.map(r => (
|
|
939
|
+
<div key={r.id} data-cy={SEARCH_SELECTORS.result}>
|
|
940
|
+
<a data-cy={SEARCH_SELECTORS.resultLink} href={r.url}>
|
|
941
|
+
{r.title}
|
|
942
|
+
</a>
|
|
943
|
+
</div>
|
|
944
|
+
))}
|
|
945
|
+
</div>
|
|
946
|
+
</div>
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
---
|
|
950
|
+
|
|
951
|
+
## Interactive Options
|
|
952
|
+
|
|
953
|
+
When running `/how-to:implement-search`:
|
|
954
|
+
|
|
955
|
+
```
|
|
956
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
957
|
+
│ What would you like to learn? │
|
|
958
|
+
├─────────────────────────────────────────────────────────────┤
|
|
959
|
+
│ │
|
|
960
|
+
│ [1] Basic hook usage (useEntitySearch) │
|
|
961
|
+
│ [2] Pre-built component (EntitySearch) │
|
|
962
|
+
│ [3] Custom search implementation │
|
|
963
|
+
│ [4] Search with API integration │
|
|
964
|
+
│ [5] Start from Step 1 (full tutorial) │
|
|
965
|
+
│ │
|
|
966
|
+
└─────────────────────────────────────────────────────────────┘
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
---
|
|
970
|
+
|
|
971
|
+
## Common Questions
|
|
972
|
+
|
|
973
|
+
### Q: Why is search client-side?
|
|
974
|
+
A: Client-side search provides instant results without server round-trips.
|
|
975
|
+
For large datasets, you can extend with API-backed search.
|
|
976
|
+
|
|
977
|
+
### Q: How do I make a field searchable?
|
|
978
|
+
A: In your entity config, set `api.searchable: true` on the field.
|
|
979
|
+
|
|
980
|
+
### Q: Can I search across multiple entities?
|
|
981
|
+
A: Yes, useEntitySearch automatically searches all entities with
|
|
982
|
+
`ui.features.searchable: true` in their config.
|
|
983
|
+
|
|
984
|
+
### Q: How do I customize result ranking?
|
|
985
|
+
A: Override the relevance scoring function or post-process results
|
|
986
|
+
with your own sorting logic.
|
|
987
|
+
|
|
988
|
+
### Q: Why do I need to pass entities from server?
|
|
989
|
+
A: Entity configs must come from server components to ensure
|
|
990
|
+
proper permission filtering and avoid client-side registry access.
|
|
991
|
+
|
|
992
|
+
---
|
|
993
|
+
|
|
994
|
+
## Related Commands
|
|
995
|
+
|
|
996
|
+
- `/how-to:create-entity` - Create entities with searchable fields
|
|
997
|
+
- `/how-to:create-api` - API endpoints that support search
|
|
998
|
+
- `/how-to:use-devtools` - Test search in API explorer
|
|
999
|
+
- `/skill:react-patterns` - React patterns for search UIs
|
|
1000
|
+
- `/skill:tanstack-query` - Data fetching patterns
|
|
1001
|
+
- `/skill:cypress-selectors` - Test selectors for search components
|