diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8215fa1 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# Algolia Configuration +# Get these values from your Algolia dashboard: https://www.algolia.com/dashboard + +# Your Algolia Application ID +ALGOLIA_APP_ID=YOUR_APP_ID + +# Admin API Key (for indexing script only - DO NOT expose this in client code) +ALGOLIA_ADMIN_API_KEY=YOUR_ADMIN_API_KEY + +# Search-Only API Key (safe to use in client code) +ALGOLIA_SEARCH_API_KEY=YOUR_SEARCH_API_KEY + +# Your Algolia index name +ALGOLIA_INDEX_NAME=typecomposer_docs diff --git a/ALGOLIA_SETUP.md b/ALGOLIA_SETUP.md new file mode 100644 index 0000000..4389c5e --- /dev/null +++ b/ALGOLIA_SETUP.md @@ -0,0 +1,175 @@ +# Algolia InstantSearch Integration + +This documentation provides instructions for setting up and configuring Algolia InstantSearch for the TypeComposer documentation. + +## Overview + +The TypeComposer docs now include Algolia InstantSearch for fast and relevant search functionality. The search is accessible through: + +- **Search button** in the navigation bar +- **Keyboard shortcut**: `⌘K` (Mac) or `Ctrl+K` (Windows/Linux) +- **ESC key** to close the search modal + +## Configuration + +### 1. Algolia Account Setup + +1. Create an account at [Algolia](https://www.algolia.com/) +2. Create a new application or use an existing one +3. Create a new index for your documentation (e.g., `typecomposer_docs`) + +### 2. Update Algolia Credentials + +Update the following constants in `/src/components/search/AlgoliaSearch.ts`: + +```typescript +const ALGOLIA_APP_ID = "YOUR_APP_ID"; // Replace with your Algolia Application ID +const ALGOLIA_SEARCH_API_KEY = "YOUR_SEARCH_API_KEY"; // Replace with your Search-Only API Key +const ALGOLIA_INDEX_NAME = "typecomposer_docs"; // Replace with your index name +``` + +**Important**: Use the **Search-Only API Key** (not the Admin API Key) for client-side searches. + +### 3. Index Your Documentation + +To make your documentation searchable, you need to index it in Algolia. There are several ways to do this: + +#### Option A: Using Algolia Crawler (Recommended) + +1. Go to your Algolia dashboard +2. Navigate to **Data sources** > **Crawler** +3. Create a new crawler with your website URL +4. Configure the crawler to extract: + - `title` - Page title + - `content` - Page content/text + - `path` - Document path for navigation + - `hierarchy` - Document structure (h1, h2, h3, etc.) + +#### Option B: Using Algolia DocSearch (For Open Source) + +If your project is open source and publicly available, you can apply for [Algolia DocSearch](https://docsearch.algolia.com/) which provides free search for documentation. + +#### Option C: Manual Indexing + +You can create a script to manually index your MDX content: + +```javascript +import algoliasearch from 'algoliasearch'; +import fs from 'fs'; +import path from 'path'; + +const client = algoliasearch('YOUR_APP_ID', 'YOUR_ADMIN_API_KEY'); +const index = client.initIndex('typecomposer_docs'); + +// Read and parse your MDX files +const contentDir = './content'; +const records = []; + +// Process each MDX file +fs.readdirSync(contentDir, { recursive: true }).forEach(file => { + if (file.endsWith('.mdx')) { + const content = fs.readFileSync(path.join(contentDir, file), 'utf-8'); + + records.push({ + objectID: file, + title: extractTitle(content), + content: extractContent(content), + path: file.replace('.mdx', '').replace('/content', '/docs'), + }); + } +}); + +// Upload to Algolia +index.saveObjects(records); +``` + +## Search Configuration + +### Customizing Search Behavior + +The search is configured in `/src/components/search/AlgoliaSearch.ts`. You can customize: + +- **Number of results**: Change `hitsPerPage` in the `configure` widget +- **Search attributes**: Modify the `searchableAttributes` in your Algolia index settings +- **Faceting**: Add filters for categories, tags, etc. + +### Customizing Search UI + +The search styles are defined in `/src/components/search/search.scss`. You can customize: + +- Modal appearance and positioning +- Search input styling +- Results display format +- Colors to match your theme + +### Search Result Template + +The search results template is configured in the `hits` widget: + +```typescript +templates: { + item: (hit: any, { html, components }: any) => html` + +
${components.Highlight({ hit, attribute: "title" })}
+
${components.Snippet({ hit, attribute: "content" })}
+
+ `, +} +``` + +Customize this template to change how search results are displayed. + +## Features + +### Current Features + +- ✅ InstantSearch integration with Algolia +- ✅ Modal-based search interface +- ✅ Keyboard shortcuts (⌘K / Ctrl+K) +- ✅ Highlighted search terms +- ✅ Content snippets in results +- ✅ Responsive design +- ✅ Theme-aware styling (dark/light mode) + +### Planned Enhancements + +- 🔄 Autocomplete suggestions +- 🔄 Search filters (by category, type) +- 🔄 Recent searches +- 🔄 Keyboard navigation in results + +## Troubleshooting + +### Search Not Working + +1. **Check credentials**: Ensure `ALGOLIA_APP_ID`, `ALGOLIA_SEARCH_API_KEY`, and `ALGOLIA_INDEX_NAME` are correct +2. **Verify index**: Make sure your Algolia index contains records +3. **Check browser console**: Look for error messages +4. **API Key permissions**: Ensure the Search-Only API Key has the correct permissions + +### No Results Found + +1. **Index is empty**: Index your documentation content +2. **Attribute configuration**: Ensure `searchableAttributes` in Algolia includes `title` and `content` +3. **Path format**: Verify the `path` field in your records matches the expected format + +### Styling Issues + +1. **Theme variables**: Ensure CSS variables are defined in `/src/styles/style.scss` +2. **Import order**: Check that search styles are imported after base styles +3. **Specificity**: Some styles may need `!important` to override defaults + +## Resources + +- [Algolia InstantSearch Documentation](https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/js/) +- [Algolia Dashboard](https://www.algolia.com/dashboard) +- [DocSearch Program](https://docsearch.algolia.com/) +- [Algolia API Reference](https://www.algolia.com/doc/api-reference/) + +## Support + +For issues or questions: + +1. Check the [Algolia community forum](https://discourse.algolia.com/) +2. Review [InstantSearch issues on GitHub](https://github.com/algolia/instantsearch/issues) +3. Open an issue in the TypeComposer docs repository diff --git a/README.md b/README.md new file mode 100644 index 0000000..1195e3f --- /dev/null +++ b/README.md @@ -0,0 +1,160 @@ +# TypeComposer Documentation + +Official documentation for [TypeComposer](https://github.com/typecomposer/typecomposer) - a framework for building web and native user interfaces. + +## Features + +- 📚 Comprehensive documentation for TypeComposer framework +- 🔍 **Fast search powered by Algolia InstantSearch** +- 🎨 Dark/Light theme support +- 📱 Responsive design +- 💻 Interactive playground +- 🎯 Component examples and API reference + +## Getting Started + +### Prerequisites + +- Node.js 18+ +- npm or yarn + +### Installation + +```bash +# Clone the repository +git clone https://github.com/TypeComposer/docs.git +cd docs + +# Install dependencies +npm install + +# Start development server +npm run dev +``` + +### Build + +```bash +# Build for production +npm run build + +# Preview production build +npm run preview +``` + +## Search Setup + +This documentation includes Algolia InstantSearch for fast and relevant search functionality. See [ALGOLIA_SETUP.md](./ALGOLIA_SETUP.md) for detailed setup instructions. + +### Quick Setup + +1. Copy `.env.example` to `.env`: + ```bash + cp .env.example .env + ``` + +2. Update `.env` with your Algolia credentials: + ```env + ALGOLIA_APP_ID=your_app_id + ALGOLIA_SEARCH_API_KEY=your_search_api_key + ALGOLIA_INDEX_NAME=typecomposer_docs + ``` + +3. Update credentials in `/src/components/search/AlgoliaSearch.ts` + +4. Index your documentation: + ```bash + node scripts/index-algolia.js + ``` + +### Using Search + +- Click the search icon in the navigation bar +- Or use keyboard shortcut: `⌘K` (Mac) or `Ctrl+K` (Windows/Linux) +- Press `ESC` to close the search modal + +## Project Structure + +``` +docs/ +├── content/ # MDX documentation files +│ ├── components/ # Component documentation +│ ├── elements/ # Element documentation +│ └── layout/ # Layout documentation +├── src/ +│ ├── components/ # UI components +│ │ ├── navbar/ # Navigation bar +│ │ ├── sidebar/ # Sidebar navigation +│ │ └── search/ # Algolia search components +│ ├── pages/ # Page components +│ ├── styles/ # Global styles +│ └── utils/ # Utility functions +├── scripts/ # Build and utility scripts +│ └── index-algolia.js # Algolia indexing script +└── public/ # Static assets +``` + +## Technology Stack + +- **Framework**: [TypeComposer](https://github.com/typecomposer/typecomposer) +- **Build Tool**: [Vite](https://vitejs.dev/) +- **Styling**: [Tailwind CSS](https://tailwindcss.com/) + SCSS +- **Search**: [Algolia InstantSearch](https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/js/) +- **Content**: MDX (Markdown + JSX) +- **Syntax Highlighting**: [Highlight.js](https://highlightjs.org/) + +## Development + +### Adding Documentation + +1. Create a new `.mdx` file in the appropriate `content/` subdirectory +2. Add the route in `src/router/router.ts` +3. Update sidebar navigation in `src/assets/data.json` +4. Re-index for search: `node scripts/index-algolia.js` + +### Customizing Theme + +Theme colors are defined in `src/styles/style.scss`: +- Light theme: `[data-theme="light"]` +- Dark theme: `[data-theme="dark"]` + +### Search Customization + +Search UI can be customized in: +- `/src/components/search/AlgoliaSearch.ts` - Search logic and widgets +- `/src/components/search/SearchModal.ts` - Modal behavior +- `/src/components/search/search.scss` - Search styles + +## Scripts + +- `npm run dev` - Start development server +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run clean` - Clean build cache and restart + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/AmazingFeature`) +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Links + +- [TypeComposer Framework](https://github.com/typecomposer/typecomposer) +- [Documentation Website](https://www.typecomposer.io) +- [NPM Package](https://www.npmjs.com/package/typecomposer) +- [Algolia Setup Guide](./ALGOLIA_SETUP.md) + +## Support + +- 📧 Email: support@typecomposer.io +- 💬 Discord: [Join our community](https://discord.gg/typecomposer) +- 🐛 Issues: [GitHub Issues](https://github.com/TypeComposer/docs/issues) diff --git a/package-lock.json b/package-lock.json index 60bbdc0..dfb12f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,9 @@ "dependencies": { "@codesandbox/sandpack-client": "^2.19.8", "@mdx-js/mdx": "^3.1.1", + "algoliasearch": "^5.40.0", "highlight.js": "^11.11.1", + "instantsearch.js": "^4.80.0", "lucide": "^0.544.0", "remark-gfm": "^4.0.1", "typecomposer": "^0.1.54", @@ -27,17 +29,214 @@ "vite": "^7.1.9" } }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, + "node_modules/@algolia/abtesting": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.6.0.tgz", + "integrity": "sha512-c4M/Z/KWkEG+RHpZsWKDTTlApXu3fe4vlABNcpankWBhdMe4oPZ/r4JxEr2zKUP6K+BT66tnp8UbHmgOd/vvqQ==", "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.40.0", + "@algolia/requester-browser-xhr": "5.40.0", + "@algolia/requester-fetch": "5.40.0", + "@algolia/requester-node-http": "5.40.0" + }, "engines": { - "node": ">=10" + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.40.0.tgz", + "integrity": "sha512-qegVlgHtmiS8m9nEsuKUVhlw1FHsIshtt5nhNnA6EYz3g+tm9+xkVZZMzkrMLPP7kpoheHJZAwz2MYnHtwFa9A==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.40.0", + "@algolia/requester-browser-xhr": "5.40.0", + "@algolia/requester-fetch": "5.40.0", + "@algolia/requester-node-http": "5.40.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.40.0.tgz", + "integrity": "sha512-Dw2c+6KGkw7mucnnxPyyMsIGEY8+hqv6oB+viYB612OMM3l8aNaWToBZMnNvXsyP+fArwq7XGR+k3boPZyV53A==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.40.0", + "@algolia/requester-browser-xhr": "5.40.0", + "@algolia/requester-fetch": "5.40.0", + "@algolia/requester-node-http": "5.40.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.40.0.tgz", + "integrity": "sha512-dbE4+MJIDsTghG3hUYWBq7THhaAmqNqvW9g2vzwPf5edU4IRmuYpKtY3MMotes8/wdTasWG07XoaVhplJBlvdg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.40.0.tgz", + "integrity": "sha512-SH6zlROyGUCDDWg71DlCnbbZ/zEHYPZC8k901EAaBVhvY43Ju8Wa6LAcMPC4tahcDBgkG2poBy8nJZXvwEWAlQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.40.0", + "@algolia/requester-browser-xhr": "5.40.0", + "@algolia/requester-fetch": "5.40.0", + "@algolia/requester-node-http": "5.40.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.40.0.tgz", + "integrity": "sha512-EgHjJEEf7CbUL9gJHI1ULmAtAFeym2cFNSAi1uwHelWgLPcnLjYW2opruPxigOV7NcetkGu+t2pcWOWmZFuvKQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.40.0", + "@algolia/requester-browser-xhr": "5.40.0", + "@algolia/requester-fetch": "5.40.0", + "@algolia/requester-node-http": "5.40.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.40.0.tgz", + "integrity": "sha512-HvE1jtCag95DR41tDh7cGwrMk4X0aQXPOBIhZRmsBPolMeqRJz0kvfVw8VCKvA1uuoAkjFfTG0X0IZED+rKXoA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.40.0", + "@algolia/requester-browser-xhr": "5.40.0", + "@algolia/requester-fetch": "5.40.0", + "@algolia/requester-node-http": "5.40.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.40.0.tgz", + "integrity": "sha512-nlr/MMgoLNUHcfWC5Ns2ENrzKx9x51orPc6wJ8Ignv1DsrUmKm0LUih+Tj3J+kxYofzqQIQRU495d4xn3ozMbg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.40.0", + "@algolia/requester-browser-xhr": "5.40.0", + "@algolia/requester-fetch": "5.40.0", + "@algolia/requester-node-http": "5.40.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/events": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz", + "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==", + "license": "MIT" + }, + "node_modules/@algolia/ingestion": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.40.0.tgz", + "integrity": "sha512-OfHnhE+P0f+p3i90Kmshf9Epgesw5oPV1IEUOY4Mq1HV7cQk16gvklVN1EaY/T9sVavl+Vc3g4ojlfpIwZFA4g==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.40.0", + "@algolia/requester-browser-xhr": "5.40.0", + "@algolia/requester-fetch": "5.40.0", + "@algolia/requester-node-http": "5.40.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.40.0.tgz", + "integrity": "sha512-SWANV32PTKhBYvwKozeWP9HOnVabOixAuPdFFGoqtysTkkwutrtGI/rrh80tvG+BnQAmZX0vUmD/RqFZVfr/Yg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.40.0", + "@algolia/requester-browser-xhr": "5.40.0", + "@algolia/requester-fetch": "5.40.0", + "@algolia/requester-node-http": "5.40.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.40.0.tgz", + "integrity": "sha512-1Qxy9I5bSb3mrhPk809DllMa561zl5hLsMR6YhIqNkqQ0OyXXQokvJ2zApSxvd39veRZZnhN+oGe+XNoNwLgkw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.40.0", + "@algolia/requester-browser-xhr": "5.40.0", + "@algolia/requester-fetch": "5.40.0", + "@algolia/requester-node-http": "5.40.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.40.0.tgz", + "integrity": "sha512-MGt94rdHfkrVjfN/KwUfWcnaeohYbWGINrPs96f5J7ZyRYpVLF+VtPQ2FmcddFvK4gnKXSu8BAi81hiIhUpm3w==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.40.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.40.0.tgz", + "integrity": "sha512-wXQ05JZZ10Dr642QVAkAZ4ZZlU+lh5r6dIBGmm9WElz+1EaQ6BNYtEOTV6pkXuFYsZpeJA89JpDOiwBOP9j24w==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.40.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.40.0.tgz", + "integrity": "sha512-5qCRoySnzpbQVg2IPLGFCm4LF75pToxI5tdjOYgUMNL/um91aJ4dH3SVdBEuFlVsalxl8mh3bWPgkUmv6NpJiQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.40.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, "node_modules/@codesandbox/nodebox": { @@ -601,44 +800,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", @@ -1537,6 +1698,12 @@ "@types/ms": "*" } }, + "node_modules/@types/dom-speech-recognition": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.1.tgz", + "integrity": "sha512-udCxb8DvjcDKfk1WTBzDsxFbLgYxmQGKrE/ricoMqHRNjSlSUCcamVTA5lIQqzY10mY5qCY0QDwBfFEwhfoDPw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1552,6 +1719,12 @@ "@types/estree": "*" } }, + "node_modules/@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -1561,6 +1734,12 @@ "@types/unist": "*" } }, + "node_modules/@types/hogan.js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hogan.js/-/hogan.js-3.0.5.tgz", + "integrity": "sha512-/uRaY3HGPWyLqOyhgvW9Aa43BNnLZrNeQxl2p8wqId4UHMfPKolSB+U7BlZyO1ng7MkLnyEAItsBzCG0SDhqrA==", + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -1592,6 +1771,12 @@ "undici-types": "~7.14.0" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -1604,6 +1789,12 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1625,6 +1816,43 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/algoliasearch": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.40.0.tgz", + "integrity": "sha512-a9aIL2E3Z7uYUPMCmjMFFd5MWhn+ccTubEvnMy7rOTZCB62dXBJtz0R5BZ/TPuX3R9ocBsgWuAbGWQ+Ph4Fmlg==", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.6.0", + "@algolia/client-abtesting": "5.40.0", + "@algolia/client-analytics": "5.40.0", + "@algolia/client-common": "5.40.0", + "@algolia/client-insights": "5.40.0", + "@algolia/client-personalization": "5.40.0", + "@algolia/client-query-suggestions": "5.40.0", + "@algolia/client-search": "5.40.0", + "@algolia/ingestion": "1.40.0", + "@algolia/monitoring": "1.40.0", + "@algolia/recommend": "5.40.0", + "@algolia/requester-browser-xhr": "5.40.0", + "@algolia/requester-fetch": "5.40.0", + "@algolia/requester-node-http": "5.40.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/algoliasearch-helper": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.26.0.tgz", + "integrity": "sha512-Rv2x3GXleQ3ygwhkhJubhhYGsICmShLAiqtUuJTUkr9uOCOXyF2E71LVT4XDnVffbknv8XgScP4U0Oxtgm+hIw==", + "license": "MIT", + "dependencies": { + "@algolia/events": "^4.0.1" + }, + "peerDependencies": { + "algoliasearch": ">= 3.1 < 6" + } + }, "node_modules/astring": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", @@ -1682,13 +1910,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1790,16 +2011,6 @@ "ieee754": "^1.2.1" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001749", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz", @@ -1992,19 +2203,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/dompurify": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", - "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", - "license": "(MPL-2.0 OR Apache-2.0)" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -2017,13 +2215,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/electron-to-chromium": { "version": "1.5.234", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", @@ -2368,6 +2559,24 @@ "node": ">=12.0.0" } }, + "node_modules/hogan.js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz", + "integrity": "sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==", + "dependencies": { + "mkdirp": "0.3.0", + "nopt": "1.0.10" + }, + "bin": { + "hulk": "bin/hulk" + } + }, + "node_modules/htm": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", + "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", + "license": "Apache-2.0" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2401,6 +2610,38 @@ "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", "license": "MIT" }, + "node_modules/instantsearch-ui-components": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/instantsearch-ui-components/-/instantsearch-ui-components-0.11.2.tgz", + "integrity": "sha512-XxwqUY6NifxSvHYfyfJRiGhqYqHXQFcFNOEjNyFptB9HOkx14yCdayKar4BZxGx353FnFD6b4Z8LdzbGud+RFA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6" + } + }, + "node_modules/instantsearch.js": { + "version": "4.80.0", + "resolved": "https://registry.npmjs.org/instantsearch.js/-/instantsearch.js-4.80.0.tgz", + "integrity": "sha512-l2HTW6+tBs2hGrby43Y7HbWayTifFFKk/ikRU5BXFpCigG6CRAYZLE1NFifAskaj3SVQiY8O8B/T6hJyeMsScA==", + "license": "MIT", + "dependencies": { + "@algolia/events": "^4.0.1", + "@types/dom-speech-recognition": "^0.0.1", + "@types/google.maps": "^3.55.12", + "@types/hogan.js": "^3.0.0", + "@types/qs": "^6.5.3", + "algoliasearch-helper": "3.26.0", + "hogan.js": "^3.0.2", + "htm": "^3.0.0", + "instantsearch-ui-components": "0.11.2", + "preact": "^10.10.0", + "qs": "^6.5.1 < 6.10", + "search-insights": "^2.17.2" + }, + "peerDependencies": { + "algoliasearch": ">= 3.1 < 6" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -2740,9 +2981,9 @@ } }, "node_modules/lucide": { - "version": "0.545.0", - "resolved": "https://registry.npmjs.org/lucide/-/lucide-0.545.0.tgz", - "integrity": "sha512-mrBH0upkb1TH8ZLkf0XERQAKMdTJ1C+Offr7eSvanmdQ7JrV8jkrFw5jgRAS8fygiZgG4X9mfEg/BXCL+mNMRw==", + "version": "0.544.0", + "resolved": "https://registry.npmjs.org/lucide/-/lucide-0.544.0.tgz", + "integrity": "sha512-U5ORwr5z9Sx7bNTDFaW55RbjVdQEnAcT3vws9uz3vRT1G4XXJUDAhRZdxhFoIyHEvjmTkzzlEhjSLYM5n4mb5w==", "license": "ISC" }, "node_modules/magic-string": { @@ -2777,18 +3018,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/marked": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", - "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -3841,6 +4070,29 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3879,49 +4131,37 @@ "dev": true, "license": "MIT" }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", "license": "MIT", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, "engines": { - "node": ">=0.10.0" + "node": "*" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/outvariant": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.0.tgz", "integrity": "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==", "license": "MIT" }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -4007,6 +4247,16 @@ "dev": true, "license": "MIT" }, + "node_modules/preact": { + "version": "10.27.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", + "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -4017,6 +4267,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -4255,6 +4517,12 @@ "@parcel/watcher": "^2.4.1" } }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "license": "MIT" + }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", diff --git a/package.json b/package.json index 9e0c2d5..e5a11cf 100644 --- a/package.json +++ b/package.json @@ -14,19 +14,21 @@ "dependencies": { "@codesandbox/sandpack-client": "^2.19.8", "@mdx-js/mdx": "^3.1.1", + "algoliasearch": "^5.40.0", "highlight.js": "^11.11.1", + "instantsearch.js": "^4.80.0", "lucide": "^0.544.0", "remark-gfm": "^4.0.1", "typecomposer": "^0.1.54", "typecomposer-plugin": "^1.0.10" }, "devDependencies": { + "@tailwindcss/vite": "^4.1.14", "@types/node": "^24.7.1", "autoprefixer": "^10.4.21", "csstype": "^3.1.3", "sass": "^1.93.2", "tailwindcss": "^4.1.14", - "@tailwindcss/vite": "^4.1.14", "typescript": "^5.9.3", "vite": "^7.1.9" } diff --git a/scripts/index-algolia.js b/scripts/index-algolia.js new file mode 100755 index 0000000..36b662a --- /dev/null +++ b/scripts/index-algolia.js @@ -0,0 +1,177 @@ +#!/usr/bin/env node + +/** + * Algolia Documentation Indexer + * + * This script indexes MDX documentation files to Algolia for search functionality. + * + * Usage: + * 1. Install dependencies: npm install algoliasearch + * 2. Set environment variables: + * - ALGOLIA_APP_ID + * - ALGOLIA_ADMIN_API_KEY (use Admin API Key, not Search-Only) + * - ALGOLIA_INDEX_NAME + * 3. Run: node scripts/index-algolia.js + */ + +import algoliasearch from 'algoliasearch'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.URL); +const __dirname = path.dirname(__filename); + +// Algolia configuration from environment variables +const ALGOLIA_APP_ID = process.env.ALGOLIA_APP_ID; +const ALGOLIA_ADMIN_API_KEY = process.env.ALGOLIA_ADMIN_API_KEY; +const ALGOLIA_INDEX_NAME = process.env.ALGOLIA_INDEX_NAME || 'typecomposer_docs'; + +if (!ALGOLIA_APP_ID || !ALGOLIA_ADMIN_API_KEY) { + console.error('❌ Missing required environment variables:'); + console.error(' - ALGOLIA_APP_ID'); + console.error(' - ALGOLIA_ADMIN_API_KEY'); + console.error('\nPlease set these environment variables and try again.'); + process.exit(1); +} + +// Initialize Algolia client +const client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_ADMIN_API_KEY); +const index = client.initIndex(ALGOLIA_INDEX_NAME); + +/** + * Extract title from MDX content + */ +function extractTitle(content) { + const titleMatch = content.match(/^#\s+(.+)$/m); + return titleMatch ? titleMatch[1].trim() : 'Untitled'; +} + +/** + * Extract plain text content from MDX + */ +function extractContent(content) { + // Remove MDX/MD syntax and code blocks + let text = content + .replace(/```[\s\S]*?```/g, '') // Remove code blocks + .replace(/`[^`]+`/g, '') // Remove inline code + .replace(/^#+\s+/gm, '') // Remove headers + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Convert links to text + .replace(/[*_~`]/g, '') // Remove formatting + .replace(/\n+/g, ' ') // Replace newlines with spaces + .trim(); + + // Limit content length for better search results + if (text.length > 500) { + text = text.substring(0, 500) + '...'; + } + + return text; +} + +/** + * Extract headings structure from MDX + */ +function extractHierarchy(content) { + const headings = content.match(/^#+\s+.+$/gm) || []; + return headings.map(h => { + const level = h.match(/^#+/)[0].length; + const text = h.replace(/^#+\s+/, '').trim(); + return { level, text }; + }); +} + +/** + * Recursively read all MDX files from a directory + */ +function getMDXFiles(dir, fileList = []) { + const files = fs.readdirSync(dir); + + files.forEach(file => { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + getMDXFiles(filePath, fileList); + } else if (file.endsWith('.mdx')) { + fileList.push(filePath); + } + }); + + return fileList; +} + +/** + * Process MDX files and create Algolia records + */ +async function indexDocumentation() { + console.log('🔍 Starting documentation indexing...\n'); + + const contentDir = path.join(__dirname, '..', 'content'); + const mdxFiles = getMDXFiles(contentDir); + + console.log(`📚 Found ${mdxFiles.length} MDX files\n`); + + const records = []; + + mdxFiles.forEach((filePath, index) => { + const content = fs.readFileSync(filePath, 'utf-8'); + const relativePath = path.relative(contentDir, filePath); + const docPath = relativePath.replace('.mdx', '').toLowerCase(); + + const title = extractTitle(content); + const textContent = extractContent(content); + const hierarchy = extractHierarchy(content); + + records.push({ + objectID: docPath, + title, + content: textContent, + path: docPath, + hierarchy: hierarchy, + url: `#/docs/${docPath}`, + }); + + console.log(` ✓ Processed: ${relativePath}`); + }); + + console.log('\n📤 Uploading records to Algolia...\n'); + + try { + // Clear existing records + await index.clearObjects(); + console.log(' ✓ Cleared existing index'); + + // Upload new records + const { objectIDs } = await index.saveObjects(records); + console.log(` ✓ Uploaded ${objectIDs.length} records`); + + // Configure index settings + await index.setSettings({ + searchableAttributes: [ + 'title', + 'hierarchy.text', + 'content', + ], + attributesToHighlight: ['title', 'content'], + attributesToSnippet: ['content:30'], + highlightPreTag: '', + highlightPostTag: '', + }); + console.log(' ✓ Configured index settings'); + + console.log('\n✅ Indexing complete!\n'); + console.log(` Index name: ${ALGOLIA_INDEX_NAME}`); + console.log(` Total records: ${objectIDs.length}\n`); + + } catch (error) { + console.error('❌ Error indexing documentation:', error.message); + process.exit(1); + } +} + +// Run indexing +indexDocumentation().catch(error => { + console.error('❌ Fatal error:', error); + process.exit(1); +}); diff --git a/src/components/navbar/NavBar.ts b/src/components/navbar/NavBar.ts index a3dd030..a1aaabc 100644 --- a/src/components/navbar/NavBar.ts +++ b/src/components/navbar/NavBar.ts @@ -1,5 +1,7 @@ import { AnchorElement, Component, DivElement, ImageElement, Router, SvgElement, App, ref, computed, ButtonElement, BorderPanel } from "typecomposer"; -import { Sun, Moon, createElement, Menu } from "lucide"; +import { Sun, Moon, createElement, Menu, Search } from "lucide"; +import { SearchModal } from "@/components/search/SearchModal"; +import "@/components/search/search.scss"; class ThemeToggle extends Component { constructor() { @@ -34,17 +36,35 @@ class NavLinks extends Component { this.append(new AnchorElement({ rlink: "docs", text: "Docs" })); this.append(new AnchorElement({ rlink: "playground", text: "Playground" })); this.append(new AnchorElement({ text: "GitHub", href: "https://github.com/typecomposer/typecomposer" })); - this.append(new ThemeToggle()); } } export class NavBar extends Component { open = false; + private searchModal: SearchModal; constructor() { super({ className: "flex items-center justify-between w-screen h-16 px-6 navbar" }); this.append(new Logo()); this.append(new NavLinks()); + + // Add search button + this.append( + new ButtonElement({ + className: "search-button flex items-center gap-2 px-3 py-2 rounded-md", + children: [ + createElement(Search), + new DivElement({ + className: "search-shortcut hidden md:block", + textContent: "⌘K" + }) + ], + onclick: () => this.searchModal.open(), + }) + ); + + this.append(new ThemeToggle()); + this.append( new ButtonElement({ className: "btn-sidebar m-2", @@ -56,6 +76,10 @@ export class NavBar extends Component { }) ); this.btn(); + + // Add search modal + this.searchModal = new SearchModal(); + this.append(this.searchModal); } btn() { diff --git a/src/components/search/AlgoliaSearch.ts b/src/components/search/AlgoliaSearch.ts new file mode 100644 index 0000000..84a7300 --- /dev/null +++ b/src/components/search/AlgoliaSearch.ts @@ -0,0 +1,119 @@ +import { Component, DivElement, InputElement } from "typecomposer"; +import { liteClient as algoliasearch } from "algoliasearch/lite"; +import instantsearch from "instantsearch.js"; +import { searchBox, hits, configure } from "instantsearch.js/es/widgets"; + +// Algolia credentials - these should be replaced with actual values +const ALGOLIA_APP_ID = "YOUR_APP_ID"; +const ALGOLIA_SEARCH_API_KEY = "YOUR_SEARCH_API_KEY"; +const ALGOLIA_INDEX_NAME = "typecomposer_docs"; + +// Instance counter for unique IDs - safe in browser's single-threaded environment +let instanceCounter = 0; + +export class AlgoliaSearch extends Component { + private searchInstance: any; + private searchContainer: DivElement; + private searchBoxDiv: DivElement; + private hitsDiv: DivElement; + private searchBoxId: string; + private hitsId: string; + + constructor() { + super({ + className: "algolia-search-container", + }); + + // Generate unique IDs for this instance + const instanceId = ++instanceCounter; + this.searchBoxId = `searchbox-${instanceId}`; + this.hitsId = `hits-${instanceId}`; + + // Create container for search + this.searchContainer = new DivElement({ + className: "search-wrapper", + }); + + // Create search elements with unique IDs + this.searchBoxDiv = new DivElement({ id: this.searchBoxId }); + this.hitsDiv = new DivElement({ id: this.hitsId }); + + this.searchContainer.append(this.searchBoxDiv, this.hitsDiv); + this.append(this.searchContainer); + } + + onInit(): void { + // Use requestAnimationFrame to ensure DOM is ready before initializing InstantSearch + // This ensures the search container elements are mounted before InstantSearch tries to use them + requestAnimationFrame(() => { + // Verify DOM elements exist before initializing + if (this.searchBoxDiv && this.hitsDiv) { + this.initializeSearch(); + } + }); + } + + private initializeSearch(): void { + // Initialize Algolia search client + const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY); + + // Create InstantSearch instance + this.searchInstance = instantsearch({ + indexName: ALGOLIA_INDEX_NAME, + searchClient, + insights: false, + }); + + // Configure search widgets + this.searchInstance.addWidgets([ + configure({ + hitsPerPage: 8, + }), + + searchBox({ + container: `#${this.searchBoxId}`, + placeholder: "Search documentation...", + showReset: true, + showSubmit: false, + cssClasses: { + root: "search-box-root", + form: "search-box-form", + input: "search-box-input", + reset: "search-box-reset", + }, + }), + + hits({ + container: `#${this.hitsId}`, + cssClasses: { + root: "hits-root", + list: "hits-list", + item: "hits-item", + }, + templates: { + item: (hit: any, { html, components }: any) => html` + +
${components.Highlight({ hit, attribute: "title" })}
+
${components.Snippet({ hit, attribute: "content" })}
+
+ `, + empty: (results: any, { html }: any) => html` +
+ No results found for ${results.query}. +
+ `, + }, + }), + ]); + + // Start InstantSearch + this.searchInstance.start(); + } + + onDestroy(): void { + // Clean up InstantSearch instance when component is destroyed + if (this.searchInstance) { + this.searchInstance.dispose(); + } + } +} diff --git a/src/components/search/SearchModal.ts b/src/components/search/SearchModal.ts new file mode 100644 index 0000000..6c08613 --- /dev/null +++ b/src/components/search/SearchModal.ts @@ -0,0 +1,80 @@ +import { Component, DivElement, ButtonElement } from "typecomposer"; +import { X, createElement } from "lucide"; +import { AlgoliaSearch } from "./AlgoliaSearch"; + +export class SearchModal extends Component { + private modal: DivElement; + private backdrop: DivElement; + private searchComponent: AlgoliaSearch; + + constructor() { + super({ + className: "search-modal-wrapper", + style: { + display: "none", + }, + }); + + // Create backdrop + this.backdrop = new DivElement({ + className: "search-modal-backdrop", + onclick: () => this.close(), + }); + + // Create modal + this.modal = new DivElement({ + className: "search-modal", + }); + + // Create close button + const closeButton = new ButtonElement({ + className: "search-modal-close", + children: [createElement(X)], + onclick: () => this.close(), + }); + + // Create search component + this.searchComponent = new AlgoliaSearch(); + + this.modal.append(closeButton, this.searchComponent); + this.append(this.backdrop, this.modal); + } + + open(): void { + this.style.display = "flex"; + document.body.style.overflow = "hidden"; + + // Focus on search input after a small delay + setTimeout(() => { + const searchInput = this.querySelector(".ais-SearchBox-input"); + searchInput?.focus(); + }, 100); + } + + close(): void { + this.style.display = "none"; + document.body.style.overflow = ""; + } + + // Handle keyboard shortcuts + onInit(): void { + const handleKeydown = (e: KeyboardEvent) => { + // Open search with Cmd/Ctrl + K + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); + this.open(); + } + // Close search with Escape + if (e.key === "Escape" && this.style.display === "flex") { + this.close(); + } + }; + + document.addEventListener("keydown", handleKeydown); + + // Clean up event listener on destroy + this.addEventListener("destroy", () => { + document.removeEventListener("keydown", handleKeydown); + }); + } +} diff --git a/src/components/search/search.scss b/src/components/search/search.scss new file mode 100644 index 0000000..f1ebe37 --- /dev/null +++ b/src/components/search/search.scss @@ -0,0 +1,226 @@ +/* Algolia Search Styles */ + +/* Search Modal Wrapper */ +.search-modal-wrapper { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + align-items: flex-start; + justify-content: center; + padding-top: 10vh; +} + +.search-modal-backdrop { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); +} + +.search-modal { + position: relative; + width: 90%; + max-width: 640px; + max-height: 80vh; + background-color: var(--primary-background-color); + border-radius: 12px; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + overflow: hidden; + display: flex; + flex-direction: column; +} + +.search-modal-close { + position: absolute; + top: 16px; + right: 16px; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + color: var(--text-secondary); + cursor: pointer; + border-radius: 6px; + transition: all 0.2s; + z-index: 10; +} + +.search-modal-close:hover { + background-color: var(--secondary-background-color); + color: var(--text-primary); +} + +/* Search Button in NavBar */ +.search-button { + background: var(--secondary-background-color); + border: 1px solid var(--border-color); + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s; +} + +.search-button:hover { + background: var(--primary-background-color); + border-color: var(--text-primary); + color: var(--text-primary); +} + +.search-shortcut { + font-size: 12px; + padding: 2px 6px; + background: var(--primary-background-color); + border: 1px solid var(--border-color); + border-radius: 4px; + font-family: monospace; +} + +/* Algolia Search Container */ +.algolia-search-container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +} + +.search-wrapper { + display: flex; + flex-direction: column; + height: 100%; +} + +/* Search Box Customization */ +.ais-SearchBox { + padding: 16px; + border-bottom: 1px solid var(--border-color); +} + +.ais-SearchBox-form { + position: relative; +} + +.ais-SearchBox-input { + width: 100%; + padding: 12px 40px 12px 16px; + font-size: 16px; + border: 1px solid var(--border-color); + border-radius: 8px; + background-color: var(--secondary-background-color); + color: var(--text-primary); + outline: none; + transition: all 0.2s; +} + +.ais-SearchBox-input:focus { + border-color: var(--accent-color, #007bff); + box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1); +} + +.ais-SearchBox-input::placeholder { + color: var(--text-secondary); +} + +.ais-SearchBox-reset { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + background: transparent; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.ais-SearchBox-reset:hover { + color: var(--text-primary); +} + +.ais-SearchBox-resetIcon { + width: 16px; + height: 16px; +} + +/* Hits (Results) Customization */ +.ais-Hits { + flex: 1; + overflow-y: auto; + padding: 8px; +} + +.ais-Hits-list { + list-style: none; + padding: 0; + margin: 0; +} + +.ais-Hits-item { + margin-bottom: 4px; +} + +.hit-link { + display: block; + padding: 12px 16px; + border-radius: 8px; + text-decoration: none; + color: var(--text-primary); + transition: background-color 0.2s; +} + +.hit-link:hover { + background-color: var(--secondary-background-color); +} + +.hit-title { + font-size: 16px; + font-weight: 600; + margin-bottom: 4px; + color: var(--text-primary); +} + +.hit-content { + font-size: 14px; + color: var(--text-secondary); + line-height: 1.5; +} + +.hits-empty { + padding: 32px 16px; + text-align: center; + color: var(--text-secondary); +} + +/* Highlight styles */ +.ais-Highlight-highlighted, +.ais-Snippet-highlighted { + background-color: rgba(255, 215, 0, 0.3); + font-weight: 600; + font-style: normal; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .search-modal { + width: 95%; + max-height: 90vh; + } + + .search-modal-wrapper { + padding-top: 5vh; + } + + .search-shortcut { + display: none !important; + } +}