From 683ecbfab89b6d9b3a5eb5a0555a3438ce37b372 Mon Sep 17 00:00:00 2001 From: Pierre Wizla <4233866+pwizla@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:03:37 +0200 Subject: [PATCH 001/201] Port redesign, add JS API layout variant, reconstruct Document Service pages --- docusaurus/docs/cms/api/document-service.md | 1093 +++++++---------- .../docs/cms/api/document-service/filters.md | 685 ++++++----- .../docs/cms/api/document-service/locale.md | 615 +++++----- .../api/document-service/sort-pagination.md | 146 ++- .../docs/cms/api/document-service/status.md | 230 ++-- docusaurus/docusaurus.config.js | 36 +- docusaurus/package.json | 5 +- docusaurus/scripts/generate-llms.js | 637 +++++++++- .../components/ApiDocLayout/ApiDocLayout.jsx | 19 + .../ApiDocLayout/ApiDocLayout.module.scss | 84 ++ .../components/ApiExplorer/ApiExplorer.jsx | 511 ++++++++ .../ApiExplorer/ApiExplorer.module.scss | 374 ++++++ .../src/components/ApiReference/ApiHeader.jsx | 24 + .../ApiReference/ApiReferencePage.jsx | 39 + .../components/ApiReference/ApiSidebar.jsx | 64 + .../src/components/ApiReference/CodePanel.jsx | 69 ++ .../ApiReference/CopyCodeButton.jsx | 44 + .../src/components/ApiReference/Endpoint.jsx | 119 ++ .../components/ApiReference/MethodPill.jsx | 21 + .../components/ApiReference/ParamTable.jsx | 35 + .../components/ApiReference/ResponsePanel.jsx | 76 ++ .../ApiReference/api-reference.module.scss | 767 ++++++++++++ .../src/components/ApiReference/index.js | 8 + .../src/components/Card/card.module.scss | 18 +- .../CollapsibleTOC/CollapsibleTOC.jsx | 45 + .../CollapsibleTOC/CollapsibleTOC.module.scss | 88 ++ docusaurus/src/components/CustomDocCard.js | 19 + .../src/components/CustomDocCardsWrapper.js | 12 + .../src/components/Hero/hero.module.scss | 128 +- .../HomepageAIButton/HomepageAIButton.js | 7 +- .../homepageaibutton.module.scss | 157 ++- .../KapaThemeInjector/KapaThemeInjector.jsx | 536 ++++++++ .../NavbarBreadcrumbs/NavbarBreadcrumbs.jsx | 132 ++ .../NavbarBreadcrumbs.module.scss | 127 ++ .../NavbarSearchAI/NavbarSearchAI.jsx | 23 + .../NavbarSearchAI/NavbarSearchAI.module.scss | 194 +++ .../src/components/NextSteps/NextSteps.jsx | 62 + .../NextSteps/next-steps.module.scss | 127 ++ .../components/PageFeedback/FeedbackForm.jsx | 82 ++ .../SelectionFeedback/HeadingAnchor.jsx | 95 ++ .../PageFeedback/SelectionFeedback/index.jsx | 105 ++ .../SelectionFeedback/selectionHelpers.js | 75 ++ .../src/components/PageFeedback/ThankYou.jsx | 53 + docusaurus/src/components/PageFeedback/api.js | 28 + .../src/components/PageFeedback/config.js | 5 + .../src/components/PageFeedback/index.jsx | 142 +++ .../PageFeedback/styles.module.scss | 303 +++++ .../ProductSwitcher/ProductSwitcher.jsx | 67 + .../ProductSwitcher.module.scss | 102 ++ .../ReadingProgressBar/ReadingProgressBar.jsx | 53 + .../ReadingProgressBar.module.scss | 19 + .../components/StepDetails/StepDetails.jsx | 85 ++ .../StepDetails/StepDetails.module.scss | 74 ++ docusaurus/src/components/ViewMode/AiPanel.js | 302 +++++ .../components/ViewMode/ViewModeContext.js | 91 ++ .../components/ViewMode/ViewModeSwitcher.js | 75 ++ .../components/ViewMode/aiPanel.module.scss | 609 +++++++++ .../ViewMode/viewModeSwitcher.module.scss | 104 ++ .../src/components/WidthToggle/WidthToggle.js | 106 ++ .../WidthToggle/widthToggle.module.scss | 58 + docusaurus/src/components/index.js | 3 +- docusaurus/src/hooks/useScrollReveal.js | 40 + docusaurus/src/pages/home/Home.jsx | 539 +++++--- docusaurus/src/pages/home/home.module.scss | 874 +++++++++++-- docusaurus/src/scss/__index.scss | 4 + docusaurus/src/scss/_animations.scss | 151 +++ docusaurus/src/scss/_base.scss | 230 +++- docusaurus/src/scss/_fonts.scss | 19 +- docusaurus/src/scss/_tokens-overrides.scss | 47 +- docusaurus/src/scss/_tokens.scss | 111 +- docusaurus/src/scss/admonition.scss | 6 +- docusaurus/src/scss/ai-toolbar.scss | 2 +- docusaurus/src/scss/api-call.scss | 85 +- docusaurus/src/scss/breadcrumbs.scss | 84 +- docusaurus/src/scss/card.scss | 40 +- docusaurus/src/scss/code-block.scss | 377 +++++- docusaurus/src/scss/copy-markdown-button.scss | 2 +- docusaurus/src/scss/custom-search-bar.scss | 12 +- docusaurus/src/scss/details.scss | 94 +- docusaurus/src/scss/footer.scss | 28 +- docusaurus/src/scss/kapa.scss | 13 +- docusaurus/src/scss/navbar.scss | 105 +- docusaurus/src/scss/pagination-nav.scss | 117 +- docusaurus/src/scss/sidebar.scss | 210 +++- docusaurus/src/scss/table-of-contents.scss | 27 +- docusaurus/src/scss/table.scss | 5 - docusaurus/src/scss/tabs.scss | 45 +- docusaurus/src/scss/tldr.scss | 33 +- docusaurus/src/scss/typography.scss | 43 +- docusaurus/src/scss/view-modes.scss | 544 ++++++++ docusaurus/src/theme/CodeBlock/index.js | 272 ++-- docusaurus/src/theme/DocItem/Footer/index.js | 29 + docusaurus/src/theme/DocItem/Layout/index.js | 69 ++ .../src/theme/DocRoot/Layout/Sidebar/index.js | 150 +++ docusaurus/src/theme/DocSidebar/index.js | 71 +- docusaurus/src/theme/MDXComponents.js | 9 + docusaurus/src/theme/Navbar/Content/index.js | 83 ++ .../theme/Navbar/Content/styles.module.css | 15 + docusaurus/src/theme/Root.js | 14 + docusaurus/src/theme/TOC/index.js | 7 +- docusaurus/src/theme/Tabs/index.js | 40 + docusaurus/yarn.lock | 147 ++- 102 files changed, 12477 insertions(+), 2328 deletions(-) create mode 100644 docusaurus/src/components/ApiDocLayout/ApiDocLayout.jsx create mode 100644 docusaurus/src/components/ApiDocLayout/ApiDocLayout.module.scss create mode 100644 docusaurus/src/components/ApiExplorer/ApiExplorer.jsx create mode 100644 docusaurus/src/components/ApiExplorer/ApiExplorer.module.scss create mode 100644 docusaurus/src/components/ApiReference/ApiHeader.jsx create mode 100644 docusaurus/src/components/ApiReference/ApiReferencePage.jsx create mode 100644 docusaurus/src/components/ApiReference/ApiSidebar.jsx create mode 100644 docusaurus/src/components/ApiReference/CodePanel.jsx create mode 100644 docusaurus/src/components/ApiReference/CopyCodeButton.jsx create mode 100644 docusaurus/src/components/ApiReference/Endpoint.jsx create mode 100644 docusaurus/src/components/ApiReference/MethodPill.jsx create mode 100644 docusaurus/src/components/ApiReference/ParamTable.jsx create mode 100644 docusaurus/src/components/ApiReference/ResponsePanel.jsx create mode 100644 docusaurus/src/components/ApiReference/api-reference.module.scss create mode 100644 docusaurus/src/components/ApiReference/index.js create mode 100644 docusaurus/src/components/CollapsibleTOC/CollapsibleTOC.jsx create mode 100644 docusaurus/src/components/CollapsibleTOC/CollapsibleTOC.module.scss create mode 100644 docusaurus/src/components/KapaThemeInjector/KapaThemeInjector.jsx create mode 100644 docusaurus/src/components/NavbarBreadcrumbs/NavbarBreadcrumbs.jsx create mode 100644 docusaurus/src/components/NavbarBreadcrumbs/NavbarBreadcrumbs.module.scss create mode 100644 docusaurus/src/components/NavbarSearchAI/NavbarSearchAI.jsx create mode 100644 docusaurus/src/components/NavbarSearchAI/NavbarSearchAI.module.scss create mode 100644 docusaurus/src/components/NextSteps/NextSteps.jsx create mode 100644 docusaurus/src/components/NextSteps/next-steps.module.scss create mode 100644 docusaurus/src/components/PageFeedback/FeedbackForm.jsx create mode 100644 docusaurus/src/components/PageFeedback/SelectionFeedback/HeadingAnchor.jsx create mode 100644 docusaurus/src/components/PageFeedback/SelectionFeedback/index.jsx create mode 100644 docusaurus/src/components/PageFeedback/SelectionFeedback/selectionHelpers.js create mode 100644 docusaurus/src/components/PageFeedback/ThankYou.jsx create mode 100644 docusaurus/src/components/PageFeedback/api.js create mode 100644 docusaurus/src/components/PageFeedback/config.js create mode 100644 docusaurus/src/components/PageFeedback/index.jsx create mode 100644 docusaurus/src/components/PageFeedback/styles.module.scss create mode 100644 docusaurus/src/components/ProductSwitcher/ProductSwitcher.jsx create mode 100644 docusaurus/src/components/ProductSwitcher/ProductSwitcher.module.scss create mode 100644 docusaurus/src/components/ReadingProgressBar/ReadingProgressBar.jsx create mode 100644 docusaurus/src/components/ReadingProgressBar/ReadingProgressBar.module.scss create mode 100644 docusaurus/src/components/StepDetails/StepDetails.jsx create mode 100644 docusaurus/src/components/StepDetails/StepDetails.module.scss create mode 100644 docusaurus/src/components/ViewMode/AiPanel.js create mode 100644 docusaurus/src/components/ViewMode/ViewModeContext.js create mode 100644 docusaurus/src/components/ViewMode/ViewModeSwitcher.js create mode 100644 docusaurus/src/components/ViewMode/aiPanel.module.scss create mode 100644 docusaurus/src/components/ViewMode/viewModeSwitcher.module.scss create mode 100644 docusaurus/src/components/WidthToggle/WidthToggle.js create mode 100644 docusaurus/src/components/WidthToggle/widthToggle.module.scss create mode 100644 docusaurus/src/hooks/useScrollReveal.js create mode 100644 docusaurus/src/scss/_animations.scss create mode 100644 docusaurus/src/scss/view-modes.scss create mode 100644 docusaurus/src/theme/DocItem/Footer/index.js create mode 100644 docusaurus/src/theme/DocItem/Layout/index.js create mode 100644 docusaurus/src/theme/DocRoot/Layout/Sidebar/index.js create mode 100644 docusaurus/src/theme/Navbar/Content/index.js create mode 100644 docusaurus/src/theme/Navbar/Content/styles.module.css create mode 100644 docusaurus/src/theme/Root.js create mode 100644 docusaurus/src/theme/Tabs/index.js diff --git a/docusaurus/docs/cms/api/document-service.md b/docusaurus/docs/cms/api/document-service.md index 75caa83314..ee1c867879 100644 --- a/docusaurus/docs/cms/api/document-service.md +++ b/docusaurus/docs/cms/api/document-service.md @@ -90,116 +90,70 @@ The [`publish()`](#publish), [`unpublish()`](#unpublish), and [`discardDraft()`] ### `findOne()` -Find a document matching the passed `documentId` and parameters. - Syntax: `findOne(parameters: Params) => Document` -#### Parameters - -| Parameter | Description | Default | Type | -|-----------|-------------|---------|------| -| `documentId` | Document id | | `ID` | -| [`locale`](/cms/api/document-service/locale#find-one)| Locale of the document to find. | Default locale | String or `undefined` | -| [`status`](/cms/api/document-service/status#find-one) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Publication status, can be: | `'draft'` | `'published'` or `'draft'` | -| [`fields`](/cms/api/document-service/fields#findone) | [Select fields](/cms/api/document-service/fields#findone) to return | All fields
(except those not populated by default) | Object | -| [`populate`](/cms/api/document-service/populate) | [Populate](/cms/api/document-service/populate) results with additional fields. | `null` | Object | - -#### Example - -If only a `documentId` is passed without any other parameters, `findOne()` returns the draft version of a document in the default locale: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').findOne({ - documentId: 'a1b2c3d4e5f6g7h8i9j0klm', -}); -``` - - - - - -```js {4,5} -{ - documentId: "a1b2c3d4e5f6g7h8i9j0klm", - name: "Biscotte Restaurant", - publishedAt: null, // draft version (default) - locale: "en", // default locale - // … -} -``` - - - - - -The `findOne()` method returns the matching document if found, otherwise returns `null`. +See locale docs.' }, + { name: 'status', type: "'published' | 'draft'", required: false, description: 'If Draft & Publish is enabled: publication status. Can be published or draft. Default: draft. See status docs.' }, + { name: 'fields', type: 'Object', required: false, description: 'Select fields to return. Defaults to all fields (except those not populated by default).' }, + { name: 'populate', type: 'Object', required: false, description: 'Populate results with additional fields. Default: null.' }, + ]} + codeTabs={[ + { + label: 'Request', + code: `await strapi.documents('api::restaurant.restaurant').findOne({ + documentId: 'a1b2c3d4e5f6g7h8i9j0klm' +})`, + }, + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: JSON.stringify({ + documentId: "a1b2c3d4e5f6g7h8i9j0klm", + name: "Biscotte Restaurant", + publishedAt: null, + locale: "en", + }, null, 2), + }, + ]} +/> ### `findFirst()` -Find the first document matching the parameters. - Syntax: `findFirst(parameters: Params) => Document` -#### Parameters - -| Parameter | Description | Default | Type | -|-----------|-------------|---------|------| -| [`locale`](/cms/api/document-service/locale#find-first) | Locale of the documents to find. | Default locale | String or `undefined` | -| [`status`](/cms/api/document-service/status#find-first) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Publication status, can be: | `'draft'` | `'published'` or `'draft'` | -| [`filters`](/cms/api/document-service/filters) | [Filters](/cms/api/document-service/filters) to use | `null` | Object | -| [`fields`](/cms/api/document-service/fields#findfirst) | [Select fields](/cms/api/document-service/fields#findfirst) to return | All fields
(except those not populate by default) | Object | -| [`populate`](/cms/api/document-service/populate) | [Populate](/cms/api/document-service/populate) results with additional fields. | `null` | Object | - -#### Examples - -
- -##### Generic example - -By default, `findFirst()` returns the draft version, in the default locale, of the first document for the passed unique identifier (collection type id or single type id): - - - - - -```js -await strapi.documents('api::restaurant.restaurant').findFirst() -``` - - - - - -```js -{ - documentId: "a1b2c3d4e5f6g7h8i9j0klm", - name: "Restaurant Biscotte", - publishedAt: null, - locale: "en" - // … -} -``` - - - - - -##### Find the first document matching parameters - -Pass some parameters to `findFirst()` to return the first document matching them. - -If no `locale` or `status` parameters are passed, results return the draft version for the default locale: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').findFirst( +See locale docs.' }, + { name: 'status', type: "'published' | 'draft'", required: false, description: 'If Draft & Publish is enabled: publication status. Can be published or draft. Default: draft. See status docs.' }, + { name: 'filters', type: 'Object', required: false, description: 'Filters to use. Default: null.' }, + { name: 'fields', type: 'Object', required: false, description: 'Select fields to return. Defaults to all fields (except those not populated by default).' }, + { name: 'populate', type: 'Object', required: false, description: 'Populate results with additional fields. Default: null.' }, + ]} + codeTabs={[ + { + label: 'Generic example', + code: `await strapi.documents('api::restaurant.restaurant').findFirst()`, + }, + { + label: 'With filters', + code: `await strapi.documents('api::restaurant.restaurant').findFirst( { filters: { name: { @@ -207,238 +161,203 @@ await strapi.documents('api::restaurant.restaurant').findFirst( } } } -) -``` - - - - - -```js -{ - documentId: "j9k8l7m6n5o4p3q2r1s0tuv", - name: "Pizzeria Arrivederci", - publishedAt: null, - locale: "en" - // … -} -``` - - - - +)`, + }, + ]} + responses={[ + { + status: 200, + statusText: 'Generic', + body: JSON.stringify({ + documentId: "a1b2c3d4e5f6g7h8i9j0klm", + name: "Restaurant Biscotte", + publishedAt: null, + locale: "en", + }, null, 2), + }, + { + status: 200, + statusText: 'With filters', + body: JSON.stringify({ + documentId: "j9k8l7m6n5o4p3q2r1s0tuv", + name: "Pizzeria Arrivederci", + publishedAt: null, + locale: "en", + }, null, 2), + }, + ]} +> + +If no `locale` or `status` parameters are passed, results return the draft version for the default locale. + +
### `findMany()` -Find documents matching the parameters. - Syntax: `findMany(parameters: Params) => Document[]` -#### Parameters - -| Parameter | Description | Default | Type | -|-----------|-------------|---------|------| -| [`locale`](/cms/api/document-service/locale#find-many) | Locale of the documents to find. | Default locale | String or `undefined` | -| [`status`](/cms/api/document-service/status#find-many) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Publication status, can be: | `'draft'` | `'published'` or `'draft'` | -| [`filters`](/cms/api/document-service/filters) | [Filters](/cms/api/document-service/filters) to use | `null` | Object | -| [`fields`](/cms/api/document-service/fields#findmany) | [Select fields](/cms/api/document-service/fields#findmany) to return | All fields
(except those not populate by default) | Object | -| [`populate`](/cms/api/document-service/populate) | [Populate](/cms/api/document-service/populate) results with additional fields. | `null` | Object | -| [`pagination`](/cms/api/document-service/sort-pagination#pagination) | [Paginate](/cms/api/document-service/sort-pagination#pagination) results | -| [`sort`](/cms/api/document-service/sort-pagination#sort) | [Sort](/cms/api/document-service/sort-pagination#sort) results | | | - -#### Examples - -
- -##### Generic example - -When no parameter is passed, `findMany()` returns the draft version in the default locale for each document: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').findMany() -``` - - - - - -```js {5,6} -[ - { - documentId: "a1b2c3d4e5f6g7h8i9j0klm", - name: "Biscotte Restaurant", - publishedAt: null, // draft version (default) - locale: "en" // default locale - // … - }, - { - documentId: "j9k8l7m6n5o4p3q2r1s0tuv", - name: "Pizzeria Arrivederci", - publishedAt: null, - locale: "en" - // … - }, -] -``` - - - - - -##### Find documents matching parameters - -Available filters are detailed in the [filters](/cms/api/document-service/filters) page of the Document Service API reference. - -If no `locale` or `status` parameters are passed, results return the draft version for the default locale: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').findMany( +See locale docs.' }, + { name: 'status', type: "'published' | 'draft'", required: false, description: 'If Draft & Publish is enabled: publication status. Can be published or draft. Default: draft. See status docs.' }, + { name: 'filters', type: 'Object', required: false, description: 'Filters to use. Default: null.' }, + { name: 'fields', type: 'Object', required: false, description: 'Select fields to return. Defaults to all fields (except those not populated by default).' }, + { name: 'populate', type: 'Object', required: false, description: 'Populate results with additional fields. Default: null.' }, + { name: 'pagination', type: 'Object', required: false, description: 'Paginate results.' }, + { name: 'sort', type: 'Object', required: false, description: 'Sort results.' }, + ]} + codeTabs={[ + { + label: 'Generic example', + code: `await strapi.documents('api::restaurant.restaurant').findMany()`, + }, + { + label: 'With filters', + code: `await strapi.documents('api::restaurant.restaurant').findMany( { - filters: { + filters: { name: { $startsWith: 'Pizzeria' } } } -) -``` +)`, + }, + ]} + responses={[ + { + status: 200, + statusText: 'Generic', + body: JSON.stringify([ + { + documentId: "a1b2c3d4e5f6g7h8i9j0klm", + name: "Biscotte Restaurant", + publishedAt: null, + locale: "en", + }, + { + documentId: "j9k8l7m6n5o4p3q2r1s0tuv", + name: "Pizzeria Arrivederci", + publishedAt: null, + locale: "en", + }, + ], null, 2), + }, + { + status: 200, + statusText: 'With filters', + body: JSON.stringify([ + { + documentId: "j9k8l7m6n5o4p3q2r1s0tuv", + name: "Pizzeria Arrivederci", + locale: "en", + publishedAt: null, + }, + ], null, 2), + }, + ]} +> - +Available filters are detailed in the [filters](/cms/api/document-service/filters) page of the Document Service API reference. - +If no `locale` or `status` parameters are passed, results return the draft version for the default locale. -```js -[ - { - documentId: "j9k8l7m6n5o4p3q2r1s0tuv", - name: "Pizzeria Arrivederci", - locale: "en", // default locale - publishedAt: null, // draft version (default) - // … - }, - // … -] -``` - - - - - - - - - - + ### `create()` -Creates a drafted document and returns it. - -Pass fields for the content to create in a `data` object. - Syntax: `create(parameters: Params) => Document` -#### Parameters - -| Parameter | Description | Default | Type | -|-----------|-------------|---------|------| -| [`locale`](/cms/api/document-service/locale#create) | Locale of the documents to create. | Default locale | String or `undefined` | -| [`fields`](/cms/api/document-service/fields#create) | [Select fields](/cms/api/document-service/fields#create) to return | All fields
(except those not populated by default) | Object | -| [`status`](/cms/api/document-service/status#create) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Can be set to `'published'` to automatically publish the draft version of a document while creating it | -| `'published'` | -| [`populate`](/cms/api/document-service/populate) | [Populate](/cms/api/document-service/populate) results with additional fields. | `null` | Object | - -#### Example - -If no `locale` parameter is passed, `create()` creates the draft version of the document for the default locale: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').create({ +See locale docs.' }, + { name: 'fields', type: 'Object', required: false, description: 'Select fields to return. Defaults to all fields (except those not populated by default).' }, + { name: 'status', type: "'published'", required: false, description: 'If Draft & Publish is enabled: can be set to published to automatically publish the draft version of a document while creating it. See status docs.' }, + { name: 'populate', type: 'Object', required: false, description: 'Populate results with additional fields. Default: null.' }, + ]} + codeTabs={[ + { + label: 'Request', + code: `await strapi.documents('api::restaurant.restaurant').create({ data: { name: 'Restaurant B' } -}) -``` - - - - - -```js -{ - documentId: "ln1gkzs6ojl9d707xn6v86mw", - name: "Restaurant B", - publishedAt: null, - locale: "en", -} -``` - - - +})`, + }, + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: JSON.stringify({ + documentId: "ln1gkzs6ojl9d707xn6v86mw", + name: "Restaurant B", + publishedAt: null, + locale: "en", + }, null, 2), + }, + ]} +> :::tip If the [Draft & Publish](/cms/features/draft-and-publish) feature is enabled on the content-type, you can automatically publish a document while creating it (see [`status` documentation](/cms/api/document-service/status#create)). ::: -### `update()` + -Updates document versions and returns them. +### `update()` Syntax: `update(parameters: Params) => Promise` -#### Parameters - -| Parameter | Description | Default | Type | -|-----------|-------------|---------|------| -| `documentId` | Document id | | `ID` | -| [`locale`](/cms/api/document-service/locale#update) | Locale of the document to update. | Default locale | String or `null` | -| [`filters`](/cms/api/document-service/filters) | [Filters](/cms/api/document-service/filters) to use | `null` | Object | -| [`fields`](/cms/api/document-service/fields#update) | [Select fields](/cms/api/document-service/fields#update) to return | All fields
(except those not populate by default) | Object | -| [`status`](/cms/api/document-service/status#update) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Can be set to `'published'` to automatically publish the draft version of a document while updating it | - | `'published'` | -| [`populate`](/cms/api/document-service/populate) | [Populate](/cms/api/document-service/populate) results with additional fields. | `null` | Object | +See locale docs.' }, + { name: 'filters', type: 'Object', required: false, description: 'Filters to use. Default: null.' }, + { name: 'fields', type: 'Object', required: false, description: 'Select fields to return. Defaults to all fields (except those not populated by default).' }, + { name: 'status', type: "'published'", required: false, description: 'If Draft & Publish is enabled: can be set to published to automatically publish the draft version of a document while updating it. See status docs.' }, + { name: 'populate', type: 'Object', required: false, description: 'Populate results with additional fields. Default: null.' }, + ]} + codeTabs={[ + { + label: 'Request', + code: `await strapi.documents('api::restaurant.restaurant').update({ + documentId: 'a1b2c3d4e5f6g7h8i9j0klm', + data: { name: "New restaurant name" } +})`, + }, + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: JSON.stringify({ + documentId: "a1b2c3d4e5f6g7h8i9j0klm", + name: "New restaurant name", + locale: "en", + publishedAt: null, + }, null, 2), + }, + ]} +> :::tip Published versions are read-only, so you can not technically update the published version of a document. @@ -452,137 +371,74 @@ To update a document and publish the new version right away, you can: It's not recommended to update repeatable components with the Document Service API (see the related [breaking change entry](/cms/migration/v4-to-v5/breaking-changes/do-not-update-repeatable-components-with-document-service-api.md) for more details). ::: -#### Example - -If no `locale` parameter is passed, `update()` updates the document for the default locale: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').update({ - documentId: 'a1b2c3d4e5f6g7h8i9j0klm', - data: { name: "New restaurant name" } -}) -``` - - - - - -```js {3} -{ - documentId: 'a1b2c3d4e5f6g7h8i9j0klm', - name: "New restaurant name", - locale: "en", - publishedAt: null, // draft - // … -} -``` - - - - - - - + ### `delete()` -Deletes one document, or a specific locale of it. - Syntax: `delete(parameters: Params): Promise<{ documentId: ID, entries: Number }>` -#### Parameters - -| Parameter | Description | Default | Type | -|-----------|-------------|---------|------| -| `documentId`| Document id | | `ID`| -| [`locale`](/cms/api/document-service/locale#delete) | Locale version of the document to delete. | `null`
(deletes only the default locale) | String, `'*'`, or `null` | -| [`filters`](/cms/api/document-service/filters) | [Filters](/cms/api/document-service/filters) to use | `null` | Object | -| [`fields`](/cms/api/document-service/fields#delete) | [Select fields](/cms/api/document-service/fields#delete) to return | All fields
(except those not populate by default) | Object | -| [`populate`](/cms/api/document-service/populate) | [Populate](/cms/api/document-service/populate) results with additional fields. | `null` | Object | - -#### Example - -If no `locale` parameter is passed, `delete()` only deletes the default locale version of a document. This deletes both the draft and published versions: - - - -```js -await strapi.documents('api::restaurant.restaurant').delete({ - documentId: 'a1b2c3d4e5f6g7h8i9j0klm', // documentId, -}) -``` - - - - - - -```js {6} -{ - documentId: "a1b2c3d4e5f6g7h8i9j0klm", - entries: [ +null (deletes only the default locale). See locale docs.' }, + { name: 'filters', type: 'Object', required: false, description: 'Filters to use. Default: null.' }, + { name: 'fields', type: 'Object', required: false, description: 'Select fields to return. Defaults to all fields (except those not populated by default).' }, + { name: 'populate', type: 'Object', required: false, description: 'Populate results with additional fields. Default: null.' }, + ]} + codeTabs={[ { - "documentId": "a1b2c3d4e5f6g7h8i9j0klm", - "name": "Biscotte Restaurant", - "publishedAt": "2024-03-14T18:30:48.870Z", - "locale": "en" - // … - } - ] -} -``` - - - - - + label: 'Request', + code: `await strapi.documents('api::restaurant.restaurant').delete({ + documentId: 'a1b2c3d4e5f6g7h8i9j0klm', +})`, + }, + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: JSON.stringify({ + documentId: "a1b2c3d4e5f6g7h8i9j0klm", + entries: [ + { + documentId: "a1b2c3d4e5f6g7h8i9j0klm", + name: "Biscotte Restaurant", + publishedAt: "2024-03-14T18:30:48.870Z", + locale: "en", + } + ] + }, null, 2), + }, + ]} +/> ### `deleteMany()` -Delete multiple documents matching filters and relation parameters. - Syntax: `deleteMany(parameters: Params): Promise<{ documentId: ID, entries: Number }>` -#### Parameters - -| Parameter | Description | Default | Type | -|-----------|-------------|---------|------| -| [`locale`](/cms/api/document-service/locale#delete) | Locale version of documents to delete. | `null`
(deletes only the default locale) | String, `'*'`, or `null` | -| [`filters`](/cms/api/document-service/filters) | [Filters](/cms/api/document-service/filters) to use | `null` | Object | -| [`fields`](/cms/api/document-service/fields#delete) | [Select fields](/cms/api/document-service/fields#delete) to return | All fields
(except those not populate by default) | Object | -| [`populate`](/cms/api/document-service/populate) | [Populate](/cms/api/document-service/populate) results with additional fields. | `null` | Object | - -#### Example - -Delete multiple documents matching filters, including filters on related fields: - - - -```js -await strapi.documents('api::restaurant.restaurant').deleteMany({ +See locale docs.' }, + { name: 'filters', type: 'Object', required: false, description: 'Filters to use. Default: null.' }, + { name: 'fields', type: 'Object', required: false, description: 'Select fields to return. Defaults to all fields (except those not populated by default).' }, + { name: 'populate', type: 'Object', required: false, description: 'Populate results with additional fields. Default: null.' }, + ]} + codeTabs={[ + { + label: 'Request', + code: `await strapi.documents('api::restaurant.restaurant').deleteMany({ filters: { city: { name: { @@ -590,210 +446,193 @@ await strapi.documents('api::restaurant.restaurant').deleteMany({ } } } -}) -``` - - - - - -```js -{ - documentId: "multiple_documents", - entries: 3 -} -``` - - +});`, + }, + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: JSON.stringify({ + documentId: "multiple_documents", + entries: 3 + }, null, 2), + }, + ]} +/> ### `publish()` -Publishes one or multiple locales of a document. - -This method is only available if [Draft & Publish](/cms/features/draft-and-publish) is enabled on the content-type. - Syntax: `publish(parameters: Params): Promise<{ documentId: ID, entries: Number }>` -#### Parameters - -| Parameter | Description | Default | Type | -|-----------|-------------|---------|------| -| `documentId`| Document id | | `ID`| -| [`locale`](/cms/api/document-service/locale#publish) | Locale of the documents to publish. | Only the default locale | String, `'*'`, or `null` | -| [`filters`](/cms/api/document-service/filters) | [Filters](/cms/api/document-service/filters) to use | `null` | Object | -| [`fields`](/cms/api/document-service/fields#publish) | [Select fields](/cms/api/document-service/fields#publish) to return | All fields
(except those not populate by default) | Object | -| [`populate`](/cms/api/document-service/populate) | [Populate](/cms/api/document-service/populate) results with additional fields. | `null` | Object | - -#### Example - -If no `locale` parameter is passed, `publish()` only publishes the default locale version of the document: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').publish({ +See locale docs.' }, + { name: 'filters', type: 'Object', required: false, description: 'Filters to use. Default: null.' }, + { name: 'fields', type: 'Object', required: false, description: 'Select fields to return. Defaults to all fields (except those not populated by default).' }, + { name: 'populate', type: 'Object', required: false, description: 'Populate results with additional fields. Default: null.' }, + ]} + codeTabs={[ + { + label: 'Request', + code: `await strapi.documents('api::restaurant.restaurant').publish({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', -}); -``` - - - - - -```js {6} -{ - documentId: "a1b2c3d4e5f6g7h8i9j0klm", - entries: [ +});`, + }, + ]} + responses={[ { - "documentId": "a1b2c3d4e5f6g7h8i9j0klm", - "name": "Biscotte Restaurant", - "publishedAt": "2024-03-14T18:30:48.870Z", - "locale": "en" - // … - } - ] -} -``` - - - - - - - + status: 200, + statusText: 'OK', + body: JSON.stringify({ + documentId: "a1b2c3d4e5f6g7h8i9j0klm", + entries: [ + { + documentId: "a1b2c3d4e5f6g7h8i9j0klm", + name: "Biscotte Restaurant", + publishedAt: "2024-03-14T18:30:48.870Z", + locale: "en", + } + ] + }, null, 2), + }, + ]} +/> ### `unpublish()` -Unpublishes one or all locale versions of a document, and returns how many locale versions were unpublished. - -This method is only available if [Draft & Publish](/cms/features/draft-and-publish) is enabled on the content-type. - Syntax: `unpublish(parameters: Params): Promise<{ documentId: ID, entries: Number }>` -#### Parameters - -| Parameter | Description | Default | Type | -|-----------|-------------|---------|------| -| `documentId`| Document id | | `ID`| -| [`locale`](/cms/api/document-service/locale#unpublish) | Locale of the documents to unpublish. | Only the default locale | String, `'*'`, or `null` | -| [`filters`](/cms/api/document-service/filters) | [Filters](/cms/api/document-service/filters) to use | `null` | Object | -| [`fields`](/cms/api/document-service/fields#unpublish) | [Select fields](/cms/api/document-service/fields#unpublish) to return | All fields
(except those not populate by default) | Object | -| [`populate`](/cms/api/document-service/populate) | [Populate](/cms/api/document-service/populate) results with additional fields. | `null` | Object | - -#### Example - -If no `locale` parameter is passed, `unpublish()` only unpublishes the default locale version of the document: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').unpublish({ - documentId: 'a1b2c3d4e5f6g7h8i9j0klm' -}); -``` - - - - - -```js -{ - documentId: "lviw819d5htwvga8s3kovdij", - entries: [ +See locale docs.' }, + { name: 'filters', type: 'Object', required: false, description: 'Filters to use. Default: null.' }, + { name: 'fields', type: 'Object', required: false, description: 'Select fields to return. Defaults to all fields (except those not populated by default).' }, + { name: 'populate', type: 'Object', required: false, description: 'Populate results with additional fields. Default: null.' }, + ]} + codeTabs={[ { - documentId: "lviw819d5htwvga8s3kovdij", - name: "Biscotte Restaurant", - publishedAt: null, - locale: "en" - // … - } - ] -} -``` - - - - + label: 'Request', + code: `await strapi.documents('api::restaurant.restaurant').unpublish({ + documentId: 'a1b2c3d4e5f6g7h8i9j0klm' +});`, + }, + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: JSON.stringify({ + documentId: "lviw819d5htwvga8s3kovdij", + entries: [ + { + documentId: "lviw819d5htwvga8s3kovdij", + name: "Biscotte Restaurant", + publishedAt: null, + locale: "en", + } + ] + }, null, 2), + }, + ]} +/> ### `discardDraft()` -Discards draft data and overrides it with the published version. - -This method is only available if [Draft & Publish](/cms/features/draft-and-publish) is enabled on the content-type. - Syntax: `discardDraft(parameters: Params): Promise<{ documentId: ID, entries: Number }>` -#### Parameters - -| Parameter | Description | Default | Type | -|-----------|-------------|---------|------| -| `documentId`| Document id | | `ID`| -| [`locale`](/cms/api/document-service/locale#discard-draft) | Locale of the documents to discard. | Only the default locale. | String, `'*'`, or `null` | -| [`filters`](/cms/api/document-service/filters) | [Filters](/cms/api/document-service/filters) to use | `null` | Object | -| [`fields`](/cms/api/document-service/fields#discarddraft) | [Select fields](/cms/api/document-service/fields#discarddraft) to return | All fields
(except those not populate by default) | Object | -| [`populate`](/cms/api/document-service/populate) | [Populate](/cms/api/document-service/populate) results with additional fields. | `null` | Object | - -#### Example - -If no `locale` parameter is passed, `discardDraft()` discards draft data and overrides it with the published version only for the default locale: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').discardDraft({ +See locale docs.' }, + { name: 'filters', type: 'Object', required: false, description: 'Filters to use. Default: null.' }, + { name: 'fields', type: 'Object', required: false, description: 'Select fields to return. Defaults to all fields (except those not populated by default).' }, + { name: 'populate', type: 'Object', required: false, description: 'Populate results with additional fields. Default: null.' }, + ]} + codeTabs={[ + { + label: 'Request', + code: `strapi.documents('api::restaurant.restaurant').discardDraft({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', -}); -``` - - - - - -```js -{ - documentId: "lviw819d5htwvga8s3kovdij", - entries: [ +});`, + }, + ]} + responses={[ { - documentId: "lviw819d5htwvga8s3kovdij", - name: "Biscotte Restaurant", - publishedAt: null, - locale: "en" - // … - } - ] -} -``` - - - - + status: 200, + statusText: 'OK', + body: JSON.stringify({ + documentId: "lviw819d5htwvga8s3kovdij", + entries: [ + { + documentId: "lviw819d5htwvga8s3kovdij", + name: "Biscotte Restaurant", + publishedAt: null, + locale: "en", + } + ] + }, null, 2), + }, + ]} +/> ### `count()` -Count the number of documents that match the provided parameters. - Syntax: `count(parameters: Params) => number` -#### Parameters - -| Parameter | Description | Default | Type | -|-----------|-------------|---------|------| -| [`locale`](/cms/api/document-service/locale#count) | Locale of the documents to count | Default locale | String or `null` | -| [`status`](/cms/api/document-service/status#count) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Publication status, can be: | `'draft'` | `'published'` or `'draft'` | -| [`filters`](/cms/api/document-service/filters) | [Filters](/cms/api/document-service/filters) to use | `null` | Object | +See locale docs.' }, + { name: 'status', type: "'published' | 'draft'", required: false, description: 'If Draft & Publish is enabled: publication status. published to count only published documents, draft to count draft documents (returns all documents). Default: draft. See status docs.' }, + { name: 'filters', type: 'Object', required: false, description: 'Filters to use. Default: null.' }, + ]} + codeTabs={[ + { + label: 'Generic example', + code: `await strapi.documents('api::restaurant.restaurant').count()`, + }, + { + label: 'Count published', + code: `strapi.documents('api::restaurant.restaurant').count({ status: 'published' })`, + }, + { + label: 'With filters', + code: `/** + * Count number of draft documents (default if status is omitted) + * in English (default locale) + * whose name starts with 'Pizzeria' + */ +strapi.documents('api::restaurant.restaurant').count({ filters: { name: { $startsWith: "Pizzeria" }}})`, + }, + ]} + isLast={true} +> :::note Since published documents necessarily also have a draft counterpart, a published document is still counted as having a draft version. @@ -801,50 +640,4 @@ Since published documents necessarily also have a draft counterpart, a published This means that counting with the `status: 'draft'` parameter still returns the total number of documents matching other parameters, even if some documents have already been published and are not displayed as "draft" or "modified" in the Content Manager anymore. There currently is no way to prevent already published documents from being counted. ::: -#### Examples - -
- -##### Generic example - -If no parameter is passed, the `count()` method returns the total number of documents for the default locale: - - - - -```js -await strapi.documents('api::restaurant.restaurant').count() -``` - - - - - -##### Count published documents - -To count only published documents, pass `status: 'published'` along with other parameters to the `count()` method. - -If no `locale` parameter is passed, documents are counted for the default locale. - - - -```js -strapi.documents('api::restaurant.restaurant').count({ status: 'published' }) -``` - - - -##### Count documents with filters - -Any [filters](/cms/api/document-service/filters) can be passed to the `count()` method. - -If no `locale` and no `status` parameter is passed, draft documents (which is the total of available documents for the locale since even published documents are counted as having a draft version) are counted only for the default locale: - -```js -/** - * Count number of draft documents (default if status is omitted) - * in English (default locale) - * whose name starts with 'Pizzeria' - */ -strapi.documents('api::restaurant.restaurant').count({ filters: { name: { $startsWith: "Pizzeria" }}}) -``` +
diff --git a/docusaurus/docs/cms/api/document-service/filters.md b/docusaurus/docs/cms/api/document-service/filters.md index 6c05e02ff3..63d59b4dc2 100644 --- a/docusaurus/docs/cms/api/document-service/filters.md +++ b/docusaurus/docs/cms/api/document-service/filters.md @@ -12,6 +12,7 @@ tags: --- import DeepFilteringBlogLink from '/docs/snippets/deep-filtering-blog.md' +import Endpoint from '@site/src/components/ApiReference/Endpoint'; # Document Service API: Filters @@ -52,14 +53,16 @@ The following operators are available:
-### `$not` - -Negates the nested condition(s). - -**Example** - -```js -const entries = await strapi.documents('api::article.article').findMany({ + + + + + + + + + + + + + + + + + + + + + + +Attribute is between the 2 input values, boundaries included (e.g., $between[1, 3] will also return 1 and 3).} + codeTabs={[ + { + label: 'JavaScript', + code: `const entries = await strapi.documents('api::article.article').findMany({ filters: { rating: { $between: [1, 20], }, }, -}); -``` - -### `$contains` - -Attribute contains the input value (case-sensitive). - -**Example** - -```js -const entries = await strapi.documents('api::article.article').findMany({ +});` + } + ]} +/> + + + + + +$containsi is not case-sensitive, while $contains is.} + codeTabs={[ + { + label: 'JavaScript', + code: `const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $containsi: 'hello', }, }, -}); -``` - -### `$notContainsi` - -Attribute does not contain the input value. `$notContainsi` is not case-sensitive, while [$notContains](#notcontains) is. - -**Example** - -```js -const entries = await strapi.documents('api::article.article').findMany({ +});` + } + ]} +/> + +$notContainsi is not case-sensitive, while $notContains is.} + codeTabs={[ + { + label: 'JavaScript', + code: `const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $notContainsi: 'hello', }, }, -}); -``` - -### `$startsWith` - -Attribute starts with input value (case-sensitive). - -**Example** - -```js -const entries = await strapi.documents('api::article.article').findMany({ +});` + } + ]} +/> + + + + + + + + + +Attribute is null.} + codeTabs={[ + { + label: 'JavaScript', + code: `const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $null: true, }, }, -}); -``` - -### `$notNull` - -Attribute is not `null`. - -**Example** - -```js -const entries = await strapi.documents('api::article.article').findMany({ +});` + } + ]} +/> + +Attribute is not null.} + codeTabs={[ + { + label: 'JavaScript', + code: `const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $notNull: true, }, }, -}); -``` +});` + } + ]} +/> ## Logical operators -### `$and` - -All nested conditions must be `true`. - -**Example** - -```js -const entries = await strapi.documents('api::article.article').findMany({ +All nested conditions must be true.} + codeTabs={[ + { + label: 'JavaScript', + code: `const entries = await strapi.documents('api::article.article').findMany({ filters: { $and: [ { @@ -447,28 +535,31 @@ const entries = await strapi.documents('api::article.article').findMany({ }, ], }, -}); -``` - -`$and` will be used implicitly when passing an object with nested conditions: - -```js +});` + }, + { + label: 'Implicit $and', + code: `// $and will be used implicitly when passing an object with nested conditions: const entries = await strapi.documents('api::article.article').findMany({ filters: { title: 'Hello World', createdAt: { $gt: '2021-11-17T14:28:25.843Z' }, }, -}); -``` - -### `$or` - -One or many nested conditions must be `true`. - -**Example** - -```js -const entries = await strapi.documents('api::article.article').findMany({ +});` + } + ]} +/> + +One or many nested conditions must be true.} + codeTabs={[ + { + label: 'JavaScript', + code: `const entries = await strapi.documents('api::article.article').findMany({ filters: { $or: [ { @@ -479,30 +570,36 @@ const entries = await strapi.documents('api::article.article').findMany({ }, ], }, -}); -``` - -### `$not` - -Negates the nested conditions. - -**Example** - -```js -const entries = await strapi.documents('api::article.article').findMany({ +});` + } + ]} +/> + + :::note `$not` can be used as: -- a logical operator (e.g. in `filters: { $not: { // conditions… }}`) -- [an attribute operator](#not) (e.g. in `filters: { attribute-name: $not: { … } }`). +- a logical operator (e.g. in `filters: { $not: { // conditions... }}`) +- [an attribute operator](#not) (e.g. in `filters: { attribute-name: $not: { ... } }`). ::: :::tip diff --git a/docusaurus/docs/cms/api/document-service/locale.md b/docusaurus/docs/cms/api/document-service/locale.md index 4d7a7acbdf..9af146aab1 100644 --- a/docusaurus/docs/cms/api/document-service/locale.md +++ b/docusaurus/docs/cms/api/document-service/locale.md @@ -20,95 +20,99 @@ tags: - unpublishing content --- +import Endpoint from '@site/src/components/ApiReference/Endpoint'; + # Document Service API: Using the `locale` parameter By default the [Document Service API](/cms/api/document-service) returns the default locale version of documents (which is 'en', i.e. the English version, unless another default locale has been set for the application, see [Internationalization (i18n) feature](/cms/features/internationalization)). This page describes how to use the `locale` parameter to get or manipulate data only for specific locales. ## Get a locale version with `findOne()` {#find-one} -If a `locale` is passed, the [`findOne()` method](/cms/api/document-service#findone) of the Document Service API returns the version of the document for this locale: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').findOne({ + - - - -```js {5} -{ +});` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ documentId: "a1b2c3d4e5f6g7h8i9j0klm", name: "Biscotte Restaurant", publishedAt: null, // draft version (default) locale: "fr", // as asked from the parameters // … -} -``` - - - - +}` + } + ]} +/> If no `status` parameter is passed, the `draft` version is returned by default. ## Get a locale version with `findFirst()` {#find-first} -To return a specific locale while [finding the first document](/cms/api/document-service#findfirst) matching the parameters with the Document Service API: - - - - -```js -const document = await strapi.documents('api::article.article').findFirst({ + - - - -```json -{ +});` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ "documentId": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article" // … -} -``` - - - +}` + } + ]} +/> If no `status` parameter is passed, the `draft` version is returned by default. ## Get locale versions with `findMany()` {#find-many} -When a `locale` is passed to the [`findMany()` method](/cms/api/document-service#findmany) of the Document Service API, the response will return all documents that have this locale available. - If no `status` parameter is passed, the `draft` versions are returned by default. - - - -```js -// Defaults to status: draft -await strapi.documents('api::restaurant.restaurant').findMany({ locale: 'fr' }); -``` - - - - - -```js {6} -[ + - +]` + } + ]} +/>
Explanation: @@ -141,76 +144,74 @@ Given the following 4 documents that have various locales: - `fr` - it -`findMany({ locale: 'fr' })` would only return the draft version of the documents that have a `‘fr’` locale version, that is documents A, C, and D. +`findMany({ locale: 'fr' })` would only return the draft version of the documents that have a `'fr'` locale version, that is documents A, C, and D.
## `create()` a document for a locale {#create} -To create a document for specific locale, pass the `locale` as a parameter to the [`create` method](/cms/api/document-service#create) of the Document Service API: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').create({ + - - - -```js -{ +})` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ documentId: "pw2s0nh5ub1zmnk0d80vgqrh", name: "Restaurante B", publishedAt: null, locale: "es" // … -} -``` - - - - +}` + } + ]} +/> ## `update()` a locale version {#update} -To update only a specific locale version of a document, pass the `locale` parameter to the [`update()` method](/cms/api/document-service#update) of the Document Service API: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').update({ + - - - -```js {3} -{ +});` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ documentId: "a1b2c3d4e5f6g7h8i9j0klm", name: "Nuevo nombre del restaurante", locale: "es", publishedAt: null, // … -} -``` - - - - +}` + } + ]} +/> ## `delete()` locale versions {#delete} @@ -218,39 +219,45 @@ Use the `locale` parameter with the [`delete()` method](/cms/api/document-servic ### Delete a locale version -To delete a specific locale version of a document: - - - -```js -await strapi.documents('api::restaurant.restaurant').delete({ + +});` + } + ]} +/> ### Delete all locale versions -The `*` wildcard is supported by the `locale` parameter and can be used to delete all locale versions of a document: - - - - -```js -await strapi.documents('api::restaurant.restaurant').delete({ + - - - -```json -{ +}); // for all existing locales` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ "documentId": "a1b2c3d4e5f6g7h8i9j0klm", // All of the deleted locale versions are returned "versions": [ @@ -258,11 +265,10 @@ await strapi.documents('api::restaurant.restaurant').delete({ "title": "Test Article" } ] -} -``` - - - +}` + } + ]} +/> ## `publish()` locale versions {#publish} @@ -270,25 +276,26 @@ To publish only specific locale versions of a document with the [`publish()` met ### Publish a locale version -To publish a specific locale version of a document: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').publish({ + - - - -```js -{ +});` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ versions: [ { documentId: 'a1b2c3d4e5f6g7h8i9j0klm', @@ -297,34 +304,33 @@ await strapi.documents('api::restaurant.restaurant').publish({ locale: 'fr', // … }, - ]; -} -``` - - - - + ] +}` + } + ]} +/> ### Publish all locale versions -The `*` wildcard is supported by the `locale` parameter to publish all locale versions of a document: - - - - - -```js -await strapi + - - - -```js -{ + .publish({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', locale: '*' });` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ "versions": [ { "documentId": "a1b2c3d4e5f6g7h8i9j0klm", @@ -345,12 +351,10 @@ await strapi // … } ] -} -``` - - - - +}` + } + ]} +/> ## `unpublish()` locale versions {#unpublish} @@ -358,76 +362,78 @@ To publish only specific locale versions of a document with the [`unpublish()` m ### Unpublish a locale version -To unpublish a specific locale version of a document, pass the `locale` as a parameter to `unpublish()`: - - - - - -```js -await strapi + - - - -```js -{ - versions: 1; -} -``` - - - - + .unpublish({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', locale: 'fr' });` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ + versions: 1 +}` + } + ]} +/> ### Unpublish all locale versions -The `*` wildcard is supported by the `locale` parameter, to unpublish all locale versions of a document: - - - - - -```js -await strapi + - - - -```js -{ - versions: 3; -} -``` - - - - - - - - -```js -const document = await strapi.documents('api::article.article').unpublish({ + .unpublish({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', locale: '*' });` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ + versions: 3 +}` + } + ]} +/> + + - - - -```json -{ +});` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ "documentId": "cjld2cjxh0000qzrmn831i7rn", // All of the unpublished locale versions are returned "versions": [ @@ -435,11 +441,10 @@ const document = await strapi.documents('api::article.article').unpublish({ "title": "Test Article" } ] -} -``` - - - +}` + } + ]} +/> ## `discardDraft()` for locale versions {#discard-draft} @@ -447,24 +452,25 @@ To discard draft data only for some locales versions of a document with the [`di ### Discard draft for a locale version -To discard draft data for a specific locale version of a document and override it with data from the published version for this locale, pass the `locale` as a parameter to `discardDraft()`: - - - - - -```js -await strapi + - - - -```js -{ + .discardDraft({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', locale: 'fr' });` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ versions: [ { documentId: 'a1b2c3d4e5f6g7h8i9j0klm', @@ -473,34 +479,33 @@ await strapi locale: 'fr', // … }, - ]; -} -``` - - - - + ] +}` + } + ]} +/> ### Discard drafts for all locale versions -The `*` wildcard is supported by the `locale` parameter, to discard draft data for all locale versions of a document and replace them with the data from the published versions: - - - - - -```js -await strapi + - - - -```js -{ + .discardDraft({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', locale: '*' });` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ versions: [ { documentId: 'a1b2c3d4e5f6g7h8i9j0klm', @@ -523,13 +528,11 @@ await strapi locale: 'es', // … }, - ]; -} -``` - - - - + ] +}` + } + ]} +/> ## `count()` documents for a locale {#count} diff --git a/docusaurus/docs/cms/api/document-service/sort-pagination.md b/docusaurus/docs/cms/api/document-service/sort-pagination.md index 27020d6f65..773c0fe26a 100644 --- a/docusaurus/docs/cms/api/document-service/sort-pagination.md +++ b/docusaurus/docs/cms/api/document-service/sort-pagination.md @@ -4,13 +4,15 @@ description: Use Strapi's Document Service API to sort and paginate query result displayed_sidebar: cmsSidebar sidebar_label: Sort & Pagination tags: -- API -- Content API -- Document Service API +- API +- Content API +- Document Service API - sort - pagination --- +import Endpoint from '@site/src/components/ApiReference/Endpoint'; + # Document Service API: Sorting and paginating results The [Document Service API](/cms/api/document-service) offers the ability to sort and paginate query results. @@ -21,121 +23,115 @@ To sort results returned by the Document Service API, include the `sort` paramet ### Sort on a single field -To sort results based on a single field: - - - - -```js -const documents = await strapi.documents("api::article.article").findMany({ + - - - -```json -[ +});`, + }, + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `[ { "documentId": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article", "slug": "test-article", "body": "Test 1" - // ... }, { "documentId": "cjld2cjxh0001qzrm5q1j5q7m", "title": "Test Article 2", "slug": "test-article-2", "body": "Test 2" - // ... } - // ... -] -``` - - - +]`, + }, + ]} +/> ### Sort on multiple fields -To sort on multiple fields, pass them all in an array: - - - - -```js -const documents = await strapi.documents("api::article.article").findMany({ + - - - -```json -[ +});`, + }, + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `[ { "documentId": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article", "slug": "test-article", "body": "Test 1" - // ... }, { "documentId": "cjld2cjxh0001qzrm5q1j5q7m", "title": "Test Article 2", "slug": "test-article-2", "body": "Test 2" - // ... } - // ... -] -``` - - - +]`, + }, + ]} +/> ## Pagination -To paginate results, pass the `limit` and `start` parameters: - - - - -```js -const documents = await strapi.documents("api::article.article").findMany({ + - - - -```json -[ +});`, + }, + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `[ { "documentId": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article", "slug": "test-article", "body": "Test 1" - // ... }, { "documentId": "cjld2cjxh0001qzrm5q1j5q7m", "title": "Test Article 2", "slug": "test-article-2", "body": "Test 2" - // ... } - // ... (8 more) -] -``` - - - +]`, + }, + ]} +/> diff --git a/docusaurus/docs/cms/api/document-service/status.md b/docusaurus/docs/cms/api/document-service/status.md index efb7042581..cda7369a63 100644 --- a/docusaurus/docs/cms/api/document-service/status.md +++ b/docusaurus/docs/cms/api/document-service/status.md @@ -17,12 +17,14 @@ tags: --- +import Endpoint from '@site/src/components/ApiReference/Endpoint'; + # Document Service API: Usage with Draft & Publish By default the [Document Service API](/cms/api/document-service) returns the draft version of a document when the [Draft & Publish](/cms/features/draft-and-publish) feature is enabled. This page describes how to use the `status` parameter to: - return the published version of a document, -- count documents depending on their status, +- count documents depending on their status, - and directly publish a document while creating it or updating it. :::note @@ -31,92 +33,88 @@ Passing `{ status: 'draft' }` to a Document Service API query returns the same r ## Get the published version with `findOne()` {#find-one} -`findOne()` queries return the draft version of a document by default. - -To return the published version while [finding a specific document](/cms/api/document-service#findone) with the Document Service API, pass `status: 'published'`: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').findOne({ + - - - -```js {4} -{ +});` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ documentId: "a1b2c3d4e5f6g7h8i9j0klm", name: "Biscotte Restaurant", publishedAt: "2024-03-14T15:40:45.330Z", locale: "en", // default locale // … -} -``` - - - - +}` + } + ]} +/> ## Get the published version with `findFirst()` {#find-first} -`findFirst()` queries return the draft version of a document by default. - -To return the published version while [finding the first document](/cms/api/document-service#findfirst) with the Document Service API, pass `status: 'published'`: - - - - -```js -const document = await strapi.documents("api::restaurant.restaurant").findFirst({ + - - - -```js {4} -{ +});` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ documentId: "a1b2c3d4e5f6g7h8i9j0klm", name: "Biscotte Restaurant", publishedAt: "2024-03-14T15:40:45.330Z", locale: "en", // default locale // … -} -``` - - - +}` + } + ]} +/> ## Get the published version with `findMany()` {#find-many} -`findMany()` queries return the draft version of documents by default. - -To return the published version while [finding documents](/cms/api/document-service#findmany) with the Document Service API, pass `status: 'published'`: - - - - -```js -const documents = await strapi.documents("api::restaurant.restaurant").findMany({ + - - - -```js {5} -[ +});` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `[ { documentId: "a1b2c3d4e5f6g7h8i9j0klm", name: "Biscotte Restaurant", @@ -125,11 +123,10 @@ const documents = await strapi.documents("api::restaurant.restaurant").findMany( // … } // … -] -``` - - - +]` + } + ]} +/> ## `count()` only draft or published versions {#count} @@ -157,70 +154,69 @@ This means that counting with the `status: 'draft'` parameter still returns the ## Create a draft and publish it {#create} -To automatically publish a document while creating it, add `status: 'published'` to parameters passed to `create()`: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').create({ + - - - -```js {5} -{ +})` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ documentId: "d41r46wac4xix5vpba7561at", name: "New Restaurant", publishedAt: "2024-03-14T17:29:03.399Z", locale: "en" // default locale // … -} -``` - - - +}` + } + ]} +/> ## Update a draft and publish it {#update} -To automatically publish a document while updating it, add `status: 'published'` to parameters passed to `update()`: - - - - - -```js -await strapi.documents('api::restaurant.restaurant').update({ + - - - -```js {4} -{ +})` + } + ]} + responses={[ + { + status: 200, + statusText: 'OK', + body: `{ documentId: "a1b2c3d4e5f6g7h8i9j0klm", name: "Biscotte Restaurant (closed)", publishedAt: "2024-03-14T17:29:03.399Z", locale: "en" // default locale // … -} -``` - - - - +}` + } + ]} +/> diff --git a/docusaurus/docusaurus.config.js b/docusaurus/docusaurus.config.js index b6d27bdc9c..27c0c54e6f 100644 --- a/docusaurus/docusaurus.config.js +++ b/docusaurus/docusaurus.config.js @@ -31,6 +31,21 @@ const config = { themes: ['@docusaurus/theme-live-codeblock', '@docusaurus/theme-mermaid'], + headTags: [ + { + // Anti-FOUC: apply saved content width before React hydrates + tagName: 'script', + attributes: {}, + innerHTML: '(function(){try{var w=localStorage.getItem("strapi-content-width");if(w)document.documentElement.dataset.contentWidth=w}catch(e){}})();', + }, + { + // Anti-FOUC: apply saved view mode before React hydrates + tagName: 'script', + attributes: {}, + innerHTML: '(function(){try{var m=localStorage.getItem("strapi-view-mode");if(m&&m!=="ai")document.documentElement.dataset.viewMode=m;else document.documentElement.dataset.viewMode="elegant"}catch(e){document.documentElement.dataset.viewMode="elegant"}})();', + }, + ], + scripts: [ { src: '/js/redirector.js', @@ -89,26 +104,7 @@ const config = { 'data-modal-disclaimer': 'Disclaimer: Answers are AI-generated and might be inaccurate. Please ensure you double-check the information provided by visiting source pages.', 'data-project-color': '#4945FF', - 'data-project-color-dark': '#7B79FF', 'data-button-bg-color': '#32324D', - // Dark mode: sync with Docusaurus theme toggle - 'data-color-scheme': 'auto', - 'data-color-scheme-selector': "[data-theme='dark']", - // Light mode palette (defaults are fine, override only what we need) - 'data-surface-color': '#ffffff', - 'data-surface-elevated-color': '#f8f9fa', - 'data-text-color': '#212529', - 'data-text-muted-color': '#868e96', - 'data-border-color': '#dee2e6', - 'data-anchor-color': '#4945FF', - // Dark mode palette - 'data-surface-color-dark': '#181826', - 'data-surface-elevated-color-dark': '#212134', - 'data-surface-hover-color-dark': '#2a2a3e', - 'data-text-color-dark': '#e4e4e7', - 'data-text-muted-color-dark': '#a1a1a9', - 'data-border-color-dark': '#3f3f45', - 'data-anchor-color-dark': '#7B79FF', 'data-modal-example-questions': "How to create a Strapi project?,How does population work?,How to customize the admin panel?,Explain the Growth plan benefits", // 'data-modal-override-open-class-search': 'DocSearch-Button', // 'data-modal-title-search': 'Search Strapi Docs', @@ -116,7 +112,7 @@ const config = { // 'data-search-mode-enabled': true, 'data-modal-override-open-class': 'kapa-widget-button', 'data-modal-title-ask-ai': 'Ask your question', - 'data-modal-border-radius': '4px', + 'data-modal-border-radius': '16px', 'data-submit-query-button-bg-color': '#4945FF', 'data-modal-body-padding-top': '20px', 'data-user-analytics-cookie-enabled': true, diff --git a/docusaurus/package.json b/docusaurus/package.json index 3d69b66b5f..4b5d26614b 100644 --- a/docusaurus/package.json +++ b/docusaurus/package.json @@ -1,6 +1,6 @@ { "name": "strapi-docs", - "version": "6.28.0", + "version": "7.0.0", "private": true, "scripts": { "docusaurus": "docusaurus", @@ -31,6 +31,7 @@ "@docusaurus/preset-classic": "3.5.2", "@docusaurus/theme-live-codeblock": "3.5.2", "@docusaurus/theme-mermaid": "3.5.2", + "@kapaai/react-sdk": "^0.9.9", "@mdx-js/react": "^3.0.0", "@octokit/rest": "^22.0.0", "axios": "1.13.5", @@ -53,6 +54,8 @@ "qs": "^6.11.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", "sass": ">=1.58.3 <2.0.0" }, "devDependencies": { diff --git a/docusaurus/scripts/generate-llms.js b/docusaurus/scripts/generate-llms.js index 567372d0cd..a9e89d9380 100644 --- a/docusaurus/scripts/generate-llms.js +++ b/docusaurus/scripts/generate-llms.js @@ -204,21 +204,644 @@ class DocusaurusLlmsGenerator { } cleanContent(content) { - return content + let cleaned = content // Deletes frontmatter metadata .replace(/^---[\s\S]*?---\n/, '') - // Deletes React/MDX components - .replace(/<[A-Z][a-zA-Z]*[^>]*>[\s\S]*?<\/[A-Z][a-zA-Z]*>/g, '') - .replace(/<[A-Z][a-zA-Z]*[^>]*\/>/g, '') // Deletes imports .replace(/^import\s+.*$/gm, '') // Deletes exports - .replace(/^export\s+.*$/gm, '') - // Cleans up empty lines - .replace(/\n\s*\n\s*\n/g, '\n\n') + .replace(/^export\s+.*$/gm, ''); + + // Transform components into readable text + cleaned = this.transformEndpointComponents(cleaned); + + // Transform legacy // into readable text + cleaned = this.transformApiCallComponents(cleaned); + + // Remove remaining React/MDX components (balanced tag matching) + cleaned = this.stripJsxComponents(cleaned); + + // Cleans up empty lines + cleaned = cleaned.replace(/\n\s*\n\s*\n/g, '\n\n').trim(); + + return cleaned; + } + + /** + * Transform or children + * into structured, LLM-readable text. + */ + transformEndpointComponents(content) { + const result = []; + let remaining = content; + + while (true) { + const startIdx = remaining.indexOf('......... + * into structured, LLM-readable text. + */ + transformApiCallComponents(content) { + const result = []; + let remaining = content; + + while (true) { + const startIdx = remaining.indexOf('` or `>`) + let depth = { brace: 0, bracket: 0, paren: 0 }; + let inString = null; // null, '"', "'", '`' + let propsStart = i; + let selfClosing = false; + let openTagEnd = -1; + + while (i < len) { + const ch = text[i]; + const prev = i > 0 ? text[i - 1] : ''; + + // Handle strings + if (inString) { + if (inString === '`') { + // Template literal: handle ${...} + if (ch === '$' && i + 1 < len && text[i + 1] === '{') { + depth.brace++; + i += 2; + continue; + } + if (ch === '`' && prev !== '\\') { + inString = null; + } + } else if (ch === inString && prev !== '\\') { + inString = null; + } + i++; + continue; + } + + if (ch === '"' || ch === "'" || ch === '`') { + inString = ch; + i++; + continue; + } + + // Track nesting + if (ch === '{') { depth.brace++; i++; continue; } + if (ch === '}') { depth.brace--; i++; continue; } + if (ch === '[') { depth.bracket++; i++; continue; } + if (ch === ']') { depth.bracket--; i++; continue; } + if (ch === '(') { depth.paren++; i++; continue; } + if (ch === ')') { depth.paren--; i++; continue; } + + // Only look for tag end when we're at the top level (no nested braces/brackets) + if (depth.brace === 0 && depth.bracket === 0 && depth.paren === 0) { + if (ch === '/' && i + 1 < len && text[i + 1] === '>') { + // Self-closing tag /> + selfClosing = true; + openTagEnd = i + 2; + break; + } + if (ch === '>') { + openTagEnd = i + 1; + break; + } + } + + i++; + } + + if (openTagEnd === -1) return null; + + const propsString = text.substring(propsStart, selfClosing ? i : i).trim(); + + if (selfClosing) { + return { + fullMatch: text.substring(startIdx, openTagEnd), + propsString, + children: '' + }; + } + + // Phase 2: Find closing tag + const closingTag = ``; + let tagDepth = 1; + let j = openTagEnd; + const openTag = `<${componentName}`; + + while (j < len && tagDepth > 0) { + // Check for nested opening tags of same component + if (text.substring(j, j + openTag.length) === openTag) { + tagDepth++; + j += openTag.length; + continue; + } + if (text.substring(j, j + closingTag.length) === closingTag) { + tagDepth--; + if (tagDepth === 0) break; + j += closingTag.length; + continue; + } + j++; + } + + if (tagDepth !== 0) return null; + + const children = text.substring(openTagEnd, j); + const fullEnd = j + closingTag.length; + + return { + fullMatch: text.substring(startIdx, fullEnd), + propsString, + children + }; + } + + /** + * Parse Endpoint props and produce readable text. + */ + endpointToText(propsString, children) { + const method = this.extractStringProp(propsString, 'method') || 'GET'; + const epPath = this.extractStringProp(propsString, 'path') || ''; + const title = this.extractStringProp(propsString, 'title') || ''; + const description = this.extractStringProp(propsString, 'description') || ''; + + const lines = []; + + // Header + lines.push(`#### ${method} ${epPath}${title ? ' — ' + title : ''}`); + lines.push(''); + + // Description (strip HTML tags for readability) + if (description) { + lines.push(description.replace(/<[^>]+>/g, '').trim()); + lines.push(''); + } + + // Parameters + const params = this.extractArrayProp(propsString, 'params'); + if (params) { + lines.push('**Parameters:**'); + const paramItems = this.parseSimpleArray(params); + for (const param of paramItems) { + const name = this.getObjectField(param, 'name'); + const type = this.getObjectField(param, 'type'); + const required = param.includes("required: true") || param.includes("required:true"); + const desc = this.extractHtmlDescription(param, 'description'); + lines.push(`- \`${name}\` (${type}${required ? ', required' : ''}): ${desc}`); + } + lines.push(''); + } + + // Code examples + const codeTabs = this.extractArrayProp(propsString, 'codeTabs'); + if (codeTabs) { + const tabs = this.parseSimpleArray(codeTabs); + for (const tab of tabs) { + const label = this.getObjectField(tab, 'label') || 'Example'; + const code = this.extractTemplateLiteral(tab, 'code') || this.getObjectField(tab, 'code') || ''; + lines.push(`**${label}:**`); + lines.push('```'); + lines.push(code.trim()); + lines.push('```'); + lines.push(''); + } + } + + // Responses + const responses = this.extractArrayProp(propsString, 'responses'); + if (responses) { + const respItems = this.parseSimpleArray(responses); + for (const resp of respItems) { + const status = this.getObjectField(resp, 'status') || '200'; + const statusText = this.getObjectField(resp, 'statusText') || 'OK'; + + lines.push(`**Response ${status} ${statusText}:**`); + lines.push('```json'); + + // Try to extract body — could be a JSON.stringify call or a template literal + const body = this.extractResponseBody(resp); + lines.push(body.trim()); + lines.push('```'); + lines.push(''); + } + } + + // Children (extra notes, admonitions, etc.) + if (children && children.trim()) { + lines.push(children.trim()); + lines.push(''); + } + + return lines.join('\n'); + } + + /** + * Transform legacy children into readable text. + * Children typically contain ```code``` + * and ```code```. + */ + apiCallToText(children) { + if (!children || !children.trim()) return ''; + + const lines = []; + + // Extract Request blocks + const requestMatch = children.match(/]*title="([^"]*)"[^>]*>([\s\S]*?)<\/Request>/); + if (requestMatch) { + lines.push(`**${requestMatch[1] || 'Request'}:**`); + // The content is typically markdown with code blocks — keep it + lines.push(requestMatch[2].trim()); + lines.push(''); + } + + // Extract Response blocks + const responseMatch = children.match(/]*title="([^"]*)"[^>]*>([\s\S]*?)<\/Response>/); + if (responseMatch) { + lines.push(`**${responseMatch[1] || 'Response'}:**`); + lines.push(responseMatch[2].trim()); + lines.push(''); + } + + // If no Request/Response found, just return the children with tags stripped + if (lines.length === 0) { + return children.replace(/<\/?(?:Request|Response)[^>]*>/g, '').trim(); + } + + return lines.join('\n'); + } + + /** + * Remove remaining JSX components that weren't specially handled. + * Uses bracket-counting instead of broken regex. + */ + stripJsxComponents(content) { + // Match self-closing components: + // Use iterative approach to handle nested angle brackets in props + let result = content; + + // Simple self-closing tags (no nested < in props) + result = result.replace(/<[A-Z][a-zA-Z]*\s[^<]*?\/>/g, ''); + // Self-closing tags with no props + result = result.replace(/<[A-Z][a-zA-Z]*\s*\/>/g, ''); + + // Opening + closing pairs: iteratively remove innermost first + let prev = ''; + while (prev !== result) { + prev = result; + // Match components that don't contain other uppercase components inside + result = result.replace(/<([A-Z][a-zA-Z]*)[^>]*>([^<]*(?:<(?![A-Z/])[^<]*)*)<\/\1>/g, (match, tag, inner) => { + // Keep the inner text content (strip the wrapper tags) + return inner.trim(); + }); + } + + // Clean up any remaining bare closing tags + result = result.replace(/<\/[A-Z][a-zA-Z]*>/g, ''); + + return result; + } + + // ---- Prop extraction helpers ---- + + /** Extract a simple string prop: prop="value" */ + extractStringProp(propsString, propName) { + // Match prop="value" or prop='value' + const regex = new RegExp(`${propName}=["']([^"']*?)["']`); + const match = propsString.match(regex); + return match ? match[1] : null; + } + + /** Extract an array prop: prop={[...]} — returns the raw string inside [...] */ + extractArrayProp(propsString, propName) { + const marker = `${propName}={[`; + const idx = propsString.indexOf(marker); + if (idx === -1) return null; + + let i = idx + marker.length; + let depth = 1; + let inString = null; + const len = propsString.length; + + while (i < len && depth > 0) { + const ch = propsString[i]; + const prev = i > 0 ? propsString[i - 1] : ''; + + if (inString) { + if (inString === '`') { + if (ch === '$' && i + 1 < len && propsString[i + 1] === '{') { + i += 2; continue; + } + if (ch === '`' && prev !== '\\') inString = null; + } else if (ch === inString && prev !== '\\') { + inString = null; + } + i++; continue; + } + + if (ch === '"' || ch === "'" || ch === '`') { inString = ch; i++; continue; } + if (ch === '[' || ch === '{' || ch === '(') { depth++; i++; continue; } + if (ch === ']') { depth--; if (depth === 0) break; i++; continue; } + if (ch === '}' || ch === ')') { depth--; i++; continue; } + i++; + } + + return propsString.substring(idx + marker.length, i); + } + + /** Split an array string into individual object items (top-level { ... } blocks) */ + parseSimpleArray(arrayContent) { + const items = []; + let depth = 0; + let inString = null; + let start = -1; + + for (let i = 0; i < arrayContent.length; i++) { + const ch = arrayContent[i]; + const prev = i > 0 ? arrayContent[i - 1] : ''; + + if (inString) { + if (inString === '`') { + if (ch === '$' && i + 1 < arrayContent.length && arrayContent[i + 1] === '{') { + i++; continue; + } + if (ch === '`' && prev !== '\\') inString = null; + } else if (ch === inString && prev !== '\\') { + inString = null; + } + continue; + } + + if (ch === '"' || ch === "'" || ch === '`') { inString = ch; continue; } + + if (ch === '{') { + if (depth === 0) start = i; + depth++; + } else if (ch === '}') { + depth--; + if (depth === 0 && start !== -1) { + items.push(arrayContent.substring(start, i + 1)); + start = -1; + } + } + } + + return items; + } + + /** Extract a simple field value from an object string: field: 'value' or field: "value" */ + getObjectField(objString, fieldName) { + // Match: fieldName: 'value' or fieldName: "value" + const regex = new RegExp(`${fieldName}:\\s*['"]([\\s\\S]*?)(?, , etc. by finding the matching quote + * through bracket counting instead of simple regex. + */ + extractHtmlDescription(objString, fieldName) { + const patterns = [ + `${fieldName}: '`, + `${fieldName}: "`, + `${fieldName}:'`, + `${fieldName}:"`, + ]; + + let startIdx = -1; + let quote = "'"; + + for (const pat of patterns) { + const idx = objString.indexOf(pat); + if (idx !== -1) { + startIdx = idx + pat.length; + quote = pat[pat.length - 1]; + break; + } + } + + if (startIdx === -1) return ''; + + // Find the matching closing quote, accounting for escaped quotes + let i = startIdx; + const len = objString.length; + while (i < len) { + if (objString[i] === quote && objString[i - 1] !== '\\') { + break; + } + i++; + } + + const raw = objString.substring(startIdx, i); + + // Convert HTML to readable text + return raw + .replace(/]*>([^<]*)<\/a>/g, '$2 ($1)') + .replace(/([^<]*)<\/code>/g, '`$1`') + .replace(/<[^>]+>/g, '') + .replace(/\s+/g, ' ') .trim(); } + /** Extract a template literal field: field: `value` */ + extractTemplateLiteral(objString, fieldName) { + const marker = `${fieldName}: \``; + let idx = objString.indexOf(marker); + if (idx === -1) { + // Try without space + const marker2 = `${fieldName}:\``; + idx = objString.indexOf(marker2); + if (idx === -1) return null; + idx += marker2.length; + } else { + idx += marker.length; + } + + let i = idx; + let depth = 0; + const len = objString.length; + + while (i < len) { + const ch = objString[i]; + if (ch === '$' && i + 1 < len && objString[i + 1] === '{') { + depth++; + i += 2; + continue; + } + if (ch === '}' && depth > 0) { + depth--; + i++; + continue; + } + if (ch === '`' && depth === 0) { + return objString.substring(idx, i); + } + i++; + } + + return objString.substring(idx); + } + + /** Extract response body — handles JSON.stringify(...) calls and template literals */ + extractResponseBody(respString) { + // Check for JSON.stringify call + const stringifyIdx = respString.indexOf('JSON.stringify('); + if (stringifyIdx !== -1) { + // Extract the first argument to JSON.stringify + let i = stringifyIdx + 'JSON.stringify('.length; + let depth = 1; + let inString = null; + const len = respString.length; + const start = i; + + while (i < len && depth > 0) { + const ch = respString[i]; + const prev = i > 0 ? respString[i - 1] : ''; + + if (inString) { + if (ch === inString && prev !== '\\') inString = null; + i++; continue; + } + if (ch === '"' || ch === "'") { inString = ch; i++; continue; } + if (ch === '(' || ch === '{' || ch === '[') { depth++; i++; continue; } + if (ch === ')' || ch === '}' || ch === ']') { + depth--; + if (depth === 0) break; + i++; + continue; + } + i++; + } + + // Get the object literal, try to format it as JSON + const objLiteral = respString.substring(start, i).trim(); + // Remove trailing ", null, 2" args if the closing paren was for stringify + const firstArgEnd = this.findFirstArgEnd(objLiteral); + const firstArg = firstArgEnd !== -1 ? objLiteral.substring(0, firstArgEnd) : objLiteral; + + // Convert JS object literal to approximate JSON + return this.jsObjectToReadableJson(firstArg); + } + + // Check for template literal body + const body = this.extractTemplateLiteral(respString, 'body'); + if (body) return body; + + // Check for string body + const strBody = this.getObjectField(respString, 'body'); + if (strBody) return strBody; + + return '(response body)'; + } + + /** Find end of first argument in a potentially multi-arg string like "obj, null, 2" */ + findFirstArgEnd(str) { + let depth = 0; + let inString = null; + + for (let i = 0; i < str.length; i++) { + const ch = str[i]; + const prev = i > 0 ? str[i - 1] : ''; + + if (inString) { + if (ch === inString && prev !== '\\') inString = null; + continue; + } + if (ch === '"' || ch === "'") { inString = ch; continue; } + if (ch === '{' || ch === '[' || ch === '(') { depth++; continue; } + if (ch === '}' || ch === ']' || ch === ')') { depth--; continue; } + if (ch === ',' && depth === 0) return i; + } + + return -1; + } + + /** Convert a JS object literal to readable JSON-like text */ + jsObjectToReadableJson(jsObj) { + try { + // Simple transforms: unquoted keys -> quoted, single quotes -> double quotes + let json = jsObj + // Add quotes around unquoted keys + .replace(/(\s)(\w+)\s*:/g, '$1"$2":') + // Handle keys at start of line + .replace(/^(\s*)(\w+)\s*:/gm, '$1"$2":') + // Single quotes to double quotes (simple cases) + .replace(/:\s*'([^']*)'/g, ': "$1"') + // Remove trailing commas + .replace(/,(\s*[}\]])/g, '$1'); + + // Try to parse and re-format + const parsed = JSON.parse(json); + return JSON.stringify(parsed, null, 2); + } catch { + // If parsing fails, return the raw object literal cleaned up + return jsObj.trim(); + } + } + generateLlmsTxt(pages) { const lines = [`# ${this.siteName}`, '']; diff --git a/docusaurus/src/components/ApiDocLayout/ApiDocLayout.jsx b/docusaurus/src/components/ApiDocLayout/ApiDocLayout.jsx new file mode 100644 index 0000000000..33145bc25d --- /dev/null +++ b/docusaurus/src/components/ApiDocLayout/ApiDocLayout.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import styles from './ApiDocLayout.module.scss'; + +function Description({ children }) { + return
{children}
; +} + +function Code({ children }) { + return
{children}
; +} + +function ApiDocLayout({ children }) { + return
{children}
; +} + +ApiDocLayout.Description = Description; +ApiDocLayout.Code = Code; + +export default ApiDocLayout; diff --git a/docusaurus/src/components/ApiDocLayout/ApiDocLayout.module.scss b/docusaurus/src/components/ApiDocLayout/ApiDocLayout.module.scss new file mode 100644 index 0000000000..7334ed7367 --- /dev/null +++ b/docusaurus/src/components/ApiDocLayout/ApiDocLayout.module.scss @@ -0,0 +1,84 @@ +/** Component: API 2-Column Layout — V3 Redesign */ +@use '../../scss/_mixins.scss' as *; + +.layout { + display: grid; + grid-template-columns: 1fr; + gap: var(--strapi-spacing-7); + margin: var(--strapi-spacing-7) 0; + + /** Break out of article max-width constraint */ + width: calc(100% + 200px); + margin-left: -100px; + max-width: none; +} + +.description { + min-width: 0; + + h2, h3, h4 { + margin-top: 0; + padding-top: 0; + } + + p:last-child { + margin-bottom: 0; + } +} + +.code { + min-width: 0; + background: var(--strapi-surface-1); + border: 1px solid var(--strapi-border); + border-radius: var(--strapi-radius-lg); + padding: var(--strapi-spacing-6); + position: relative; + + /** Override nested code block styles */ + .theme-code-block { + border: none; + margin: 0; + + &[class*="codeBlockContainer"], + [class*="codeBlockContainer"] { + padding: 0 !important; + } + } + + pre { + margin: 0; + border: none; + } +} + +/** Responsive: 2-column on desktop */ +@include medium-up { + .layout { + grid-template-columns: 1fr 1fr; + gap: var(--strapi-spacing-9); + align-items: start; + } + + .code { + position: sticky; + top: calc(var(--ifm-navbar-height) + 24px); + max-height: calc(100vh - var(--ifm-navbar-height) - 48px); + overflow-y: auto; + } +} + +/** Responsive: reset breakout on small screens */ +@include medium-down { + .layout { + width: 100%; + margin-left: 0; + } +} + +/** Dark mode */ +@include dark { + .code { + background: var(--strapi-surface-2); + border-color: var(--strapi-border); + } +} diff --git a/docusaurus/src/components/ApiExplorer/ApiExplorer.jsx b/docusaurus/src/components/ApiExplorer/ApiExplorer.jsx new file mode 100644 index 0000000000..4b693ff3ce --- /dev/null +++ b/docusaurus/src/components/ApiExplorer/ApiExplorer.jsx @@ -0,0 +1,511 @@ +import React, { useState, useCallback, useRef, useEffect } from 'react'; +import styles from './ApiExplorer.module.scss'; + +/** + * Simulated API data — all responses match actual Strapi v5 behavior. + * Status codes, response shapes, and field names are verified against docs. + */ +const API_DATA = { + rest: { + label: 'REST API', + docBase: '/cms/api/rest', + endpoints: [ + { + method: 'GET', + name: 'List entries', + path: '/api/restaurants?populate=*', + docHash: '#get-all', + response: { + status: 200, + statusText: 'OK', + time: 23, + body: { + data: [ + { + id: 1, + documentId: 'a1b2c3d4e5f6g7h8i9j0klm', + name: 'Biscotte Restaurant', + description: [ + { + type: 'paragraph', + children: [{ type: 'text', text: 'A cozy place for brunch lovers.' }], + }, + ], + createdAt: '2026-01-15T09:00:00.000Z', + updatedAt: '2026-03-20T14:22:00.000Z', + publishedAt: '2026-01-15T09:05:00.000Z', + locale: 'en', + }, + ], + meta: { + pagination: { page: 1, pageSize: 25, pageCount: 1, total: 1 }, + }, + }, + }, + }, + { + method: 'GET', + name: 'Get entry', + path: '/api/restaurants/a1b2c3d4e5f6g7h8i9j0klm', + docHash: '#get', + response: { + status: 200, + statusText: 'OK', + time: 12, + body: { + data: { + id: 1, + documentId: 'a1b2c3d4e5f6g7h8i9j0klm', + name: 'Biscotte Restaurant', + description: [ + { + type: 'paragraph', + children: [{ type: 'text', text: 'A cozy place for brunch lovers.' }], + }, + ], + createdAt: '2026-01-15T09:00:00.000Z', + updatedAt: '2026-03-20T14:22:00.000Z', + publishedAt: '2026-01-15T09:05:00.000Z', + locale: 'en', + }, + meta: {}, + }, + }, + }, + { + method: 'POST', + name: 'Create entry', + path: '/api/restaurants', + docHash: '#create', + requestBody: { + data: { + name: 'Restaurant D', + description: [ + { + type: 'paragraph', + children: [{ type: 'text', text: 'A very short description goes here.' }], + }, + ], + }, + }, + response: { + status: 200, + statusText: 'OK', + time: 45, + body: { + data: { + id: 2, + documentId: 'f6g7h8i9j0k1l2m3n4o5pqr', + name: 'Restaurant D', + description: [ + { + type: 'paragraph', + children: [{ type: 'text', text: 'A very short description goes here.' }], + }, + ], + createdAt: '2026-04-05T14:30:00.000Z', + updatedAt: '2026-04-05T14:30:00.000Z', + publishedAt: null, + locale: 'en', + }, + meta: {}, + }, + }, + }, + { + method: 'PUT', + name: 'Update entry', + path: '/api/restaurants/a1b2c3d4e5f6g7h8i9j0klm', + docHash: '#update', + requestBody: { + data: { + name: 'BMK Paris Bamako', + description: [ + { + type: 'paragraph', + children: [{ type: 'text', text: 'A very short description goes here.' }], + }, + ], + }, + }, + response: { + status: 200, + statusText: 'OK', + time: 31, + body: { + data: { + id: 1, + documentId: 'a1b2c3d4e5f6g7h8i9j0klm', + name: 'BMK Paris Bamako', + description: [ + { + type: 'paragraph', + children: [{ type: 'text', text: 'A very short description goes here.' }], + }, + ], + createdAt: '2026-01-15T09:00:00.000Z', + updatedAt: '2026-04-05T15:00:00.000Z', + publishedAt: '2026-01-15T09:05:00.000Z', + locale: 'en', + }, + meta: {}, + }, + }, + }, + { + method: 'DELETE', + name: 'Delete entry', + path: '/api/restaurants/a1b2c3d4e5f6g7h8i9j0klm', + docHash: '#delete', + response: { + status: 204, + statusText: 'No Content', + time: 18, + body: null, + }, + }, + ], + }, + graphql: { + label: 'GraphQL', + docBase: '/cms/api/graphql', + endpoints: [ + { + method: 'POST', + name: 'Query collection', + path: '/graphql', + docHash: '#queries', + requestBody: `{ + restaurants_connection { + nodes { + documentId + name + } + pageInfo { + page + pageSize + total + } + } +}`, + response: { + status: 200, + statusText: 'OK', + time: 35, + body: { + data: { + restaurants_connection: { + nodes: [ + { + documentId: 'a1b2c3d4e5f6g7h8i9j0klm', + name: 'Biscotte Restaurant', + }, + ], + pageInfo: { page: 1, pageSize: 10, total: 1 }, + }, + }, + }, + }, + }, + { + method: 'POST', + name: 'Query single', + path: '/graphql', + docHash: '#queries', + requestBody: `{ + restaurant(documentId: "a1b2c3d4e5f6g7h8i9j0klm") { + name + description + } +}`, + response: { + status: 200, + statusText: 'OK', + time: 18, + body: { + data: { + restaurant: { + name: 'Biscotte Restaurant', + description: [ + { + type: 'paragraph', + children: [{ type: 'text', text: 'A cozy place for brunch lovers.' }], + }, + ], + }, + }, + }, + }, + }, + { + method: 'POST', + name: 'Mutation create', + path: '/graphql', + docHash: '#mutations', + requestBody: `mutation { + createRestaurant(data: { + name: "Pizzeria Arrivederci" + }) { + documentId + name + } +}`, + response: { + status: 200, + statusText: 'OK', + time: 42, + body: { + data: { + createRestaurant: { + documentId: 'f6g7h8i9j0k1l2m3n4o5pqr', + name: 'Pizzeria Arrivederci', + }, + }, + }, + }, + }, + ], + }, + document: { + label: 'Document Service', + docBase: '/cms/api/document-service', + endpoints: [ + { + method: 'GET', + name: 'findMany()', + path: "await strapi.documents('api::restaurant.restaurant').findMany()", + docHash: '#findmany', + response: { + status: 200, + statusText: 'OK', + time: 8, + body: [ + { + documentId: 'a1b2c3d4e5f6g7h8i9j0klm', + name: 'Biscotte Restaurant', + description: [ + { + type: 'paragraph', + children: [{ type: 'text', text: 'A cozy place for brunch lovers.' }], + }, + ], + locale: 'en', + }, + ], + }, + }, + { + method: 'GET', + name: 'findOne()', + path: "await strapi.documents('api::restaurant.restaurant').findOne({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm' })", + docHash: '#findone', + response: { + status: 200, + statusText: 'OK', + time: 5, + body: { + documentId: 'a1b2c3d4e5f6g7h8i9j0klm', + name: 'Biscotte Restaurant', + description: [ + { + type: 'paragraph', + children: [{ type: 'text', text: 'A cozy place for brunch lovers.' }], + }, + ], + locale: 'en', + }, + }, + }, + { + method: 'POST', + name: 'create()', + path: "await strapi.documents('api::restaurant.restaurant').create({ data: { name: 'Restaurant B' } })", + docHash: '#create', + response: { + status: 200, + statusText: 'OK', + time: 15, + body: { + documentId: 'f6g7h8i9j0k1l2m3n4o5pqr', + name: 'Restaurant B', + createdAt: '2026-04-05T14:30:00.000Z', + locale: 'en', + }, + }, + }, + ], + }, +}; + +const METHOD_COLORS = { + GET: 'get', + POST: 'post', + PUT: 'put', + DELETE: 'delete', +}; + +/** + * Syntax-highlighted JSON renderer + */ +function JsonView({ data }) { + const json = JSON.stringify(data, null, 2); + const highlighted = json + .replace(/&/g, '&') + .replace(/"$1"') + .replace(/:\s*"([^"]*)"/g, ': "$1"') + .replace(/:\s*(\d+\.?\d*)/g, ': $1') + .replace(/:\s*(true|false|null)/g, ': $1'); + + return ( +
+  );
+}
+
+export default function ApiExplorer() {
+  const [activeTab, setActiveTab] = useState('rest');
+  const [activeEndpoint, setActiveEndpoint] = useState(0);
+  const [isLoading, setIsLoading] = useState(false);
+  const [showResponse, setShowResponse] = useState(true);
+  const [showRequest, setShowRequest] = useState(false);
+  const responseRef = useRef(null);
+
+  const apiGroup = API_DATA[activeTab];
+  const endpoint = apiGroup.endpoints[activeEndpoint];
+
+  const handleTabChange = useCallback((tab) => {
+    setActiveTab(tab);
+    setActiveEndpoint(0);
+    setShowResponse(true);
+    setShowRequest(false);
+  }, []);
+
+  const handleEndpointChange = useCallback((index) => {
+    setActiveEndpoint(index);
+    setShowResponse(false);
+    setShowRequest(false);
+  }, []);
+
+  const handleSend = useCallback(() => {
+    setIsLoading(true);
+    setShowResponse(false);
+    setShowRequest(false);
+    setTimeout(() => {
+      setIsLoading(false);
+      setShowResponse(true);
+    }, 300 + Math.random() * 400);
+  }, []);
+
+  useEffect(() => {
+    setShowResponse(true);
+  }, []);
+
+  const docLink = endpoint.docHash
+    ? `${apiGroup.docBase}${endpoint.docHash}`
+    : apiGroup.docBase;
+
+  return (
+    
+
API Explorer
+
+ {/* Tabs */} +
+ {Object.entries(API_DATA).map(([key, group]) => ( + + ))} +
+ +
+ {/* Endpoint sidebar */} +
+ {apiGroup.endpoints.map((ep, i) => ( + + ))} +
+ + {/* Request + Response */} +
+ {/* URL bar */} +
+ + {endpoint.method} + + {endpoint.path} + +
+ + {/* Request body (for POST/PUT) */} + {endpoint.requestBody && !showResponse && !isLoading && ( +
+
Request Body
+
+ {typeof endpoint.requestBody === 'string' + ?
{endpoint.requestBody}
+ : + } +
+
+ )} + + {/* Response */} + {showResponse && endpoint.response && ( +
+
+ + + {endpoint.response.status} {endpoint.response.statusText} + + {endpoint.response.time}ms +
+
+ {endpoint.response.body !== null + ? + :
No response body
+ } +
+
+ )} + + {isLoading && ( +
+
+
+
+
+ )} +
+
+
+ + {/* Per-endpoint doc link */} + + Read the {apiGroup.label} docs → + +
+ ); +} diff --git a/docusaurus/src/components/ApiExplorer/ApiExplorer.module.scss b/docusaurus/src/components/ApiExplorer/ApiExplorer.module.scss new file mode 100644 index 0000000000..36f85c1984 --- /dev/null +++ b/docusaurus/src/components/ApiExplorer/ApiExplorer.module.scss @@ -0,0 +1,374 @@ +@use '../../scss/mixins' as *; + +/* ─── Explorer wrapper ─── */ +.explorer { + width: 100%; + max-width: 960px; + margin: 0 auto; +} + +.explorerLabel { + font-family: var(--strapi-font-family-technical); + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--strapi-neutral-400); + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 12px; + + &::after { + content: ''; + flex: 1; + height: 1px; + background: var(--strapi-neutral-200); + } +} + +/* ─── Dark card container ─── */ +.explorerCard { + background: #1a1a2e; + border-radius: 16px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.24), 0 2px 8px rgba(0, 0, 0, 0.12); +} + +/* ─── API Tabs ─── */ +.tabs { + display: flex; + gap: 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + padding: 0 20px; +} + +.tab { + display: flex; + align-items: center; + gap: 8px; + padding: 14px 20px; + border: none; + background: transparent; + color: rgba(255, 255, 255, 0.4); + font-family: var(--strapi-font-family-technical); + font-size: 13px; + font-weight: 600; + cursor: pointer; + position: relative; + transition: color 0.15s ease; + white-space: nowrap; + + &:hover { + color: rgba(255, 255, 255, 0.7); + } + + &::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + right: 0; + height: 2px; + background: transparent; + transition: background 0.2s ease; + } +} + +.tabActive { + color: #fff; + + &::after { + background: var(--strapi-primary-500); + } + + .tabDot { + background: #4ade80; + } +} + +.tabDot { + width: 6px; + height: 6px; + border-radius: 50%; + background: transparent; + transition: background 0.2s ease; +} + +/* ─── Body: sidebar + request pane ─── */ +.explorerBody { + display: flex; + min-height: 520px; +} + +/* ─── Endpoint sidebar ─── */ +.endpointList { + width: 200px; + flex-shrink: 0; + border-right: 1px solid rgba(255, 255, 255, 0.06); + padding: 12px 8px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.endpointItem { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border: none; + background: transparent; + color: rgba(255, 255, 255, 0.6); + font-family: var(--strapi-font-family-body); + font-size: 13px; + cursor: pointer; + border-radius: 8px; + transition: background 0.12s ease, color 0.12s ease; + text-align: left; + + &:hover { + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.85); + } +} + +.endpointItemActive { + background: rgba(255, 255, 255, 0.08); + color: #fff; +} + +.endpointName { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ─── Method badges ─── */ +.methodBadge { + font-family: var(--strapi-font-family-technical); + font-size: 10px; + font-weight: 700; + padding: 2px 6px; + border-radius: 4px; + text-transform: uppercase; + flex-shrink: 0; + letter-spacing: 0.02em; +} + +.methodget { background: rgba(74, 222, 128, 0.15); color: #4ade80; } +.methodpost { background: rgba(251, 191, 36, 0.15); color: #fbbf24; } +.methodput { background: rgba(96, 165, 250, 0.15); color: #60a5fa; } +.methoddelete { background: rgba(248, 113, 113, 0.15); color: #f87171; } + +/* ─── Request pane ─── */ +.requestPane { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +/* ─── URL bar ─── */ +.urlBar { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(255, 255, 255, 0.02); +} + +.urlMethod { + font-family: var(--strapi-font-family-technical); + font-size: 12px; + font-weight: 700; + padding: 4px 10px; + border-radius: 6px; + flex-shrink: 0; +} + +.urlPath { + flex: 1; + font-family: var(--strapi-font-family-mono); + font-size: 13px; + color: rgba(255, 255, 255, 0.7); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sendBtn { + flex-shrink: 0; + padding: 6px 16px; + border: none; + border-radius: 8px; + background: var(--strapi-primary-600); + color: #fff; + font-family: var(--strapi-font-family-technical); + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s ease, transform 0.1s ease; + white-space: nowrap; + + &:hover { + background: var(--strapi-primary-500); + } + + &:active { + transform: scale(0.97); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +/* ─── Request body ─── */ +.requestBodySection { + display: flex; + flex-direction: column; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.requestBodyHeader { + font-family: var(--strapi-font-family-technical); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: rgba(255, 255, 255, 0.35); + padding: 8px 16px 0; +} + +/* ─── Response area ─── */ +.response { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.responseHeader { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.04); +} + +.statusDot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.statusOk { background: #4ade80; } +.statusErr { background: #f87171; } + +.statusCode { + font-family: var(--strapi-font-family-technical); + font-size: 13px; + font-weight: 600; + color: #4ade80; +} + +.responseTime { + margin-left: auto; + font-family: var(--strapi-font-family-technical); + font-size: 12px; + color: rgba(255, 255, 255, 0.35); +} + +.responseBody { + flex: 1; + overflow: auto; + padding: 16px; + max-height: 480px; + + /* Thin scrollbar */ + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.15) transparent; + &::-webkit-scrollbar { width: 4px; } + &::-webkit-scrollbar-track { background: transparent; } + &::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); border-radius: 4px; } +} + +/* ─── JSON syntax highlighting ─── */ +.jsonPre { + margin: 0; + font-family: var(--strapi-font-family-mono); + font-size: 13px; + line-height: 1.6; + color: rgba(255, 255, 255, 0.7); + background: transparent !important; + white-space: pre; + tab-size: 2; + + :global(.json-key) { color: #c4b5fd; } + :global(.json-string) { color: #fbbf24; } + :global(.json-number) { color: #4ade80; } + :global(.json-bool) { color: #60a5fa; } +} + +/* ─── Loading animation ─── */ +.loadingBar { + height: 2px; + background: rgba(255, 255, 255, 0.05); + overflow: hidden; +} + +.loadingBarInner { + height: 100%; + width: 40%; + background: var(--strapi-primary-500); + border-radius: 2px; + animation: loadSlide 0.8s ease-in-out infinite; +} + +@keyframes loadSlide { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(350%); } +} + +/* ─── Doc link ─── */ +.explorerDocLink { + display: inline-flex; + align-items: center; + margin-top: 12px; + font-family: var(--strapi-font-family-technical); + font-size: 13px; + font-weight: 500; + color: var(--strapi-neutral-400); + text-decoration: none; + transition: color 0.15s ease; + + &:hover { + color: var(--strapi-primary-500); + text-decoration: none; + } +} + +/* ─── Responsive ─── */ +@media (max-width: 768px) { + .explorerBody { + flex-direction: column; + } + + .endpointList { + width: 100%; + border-right: none; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + flex-direction: row; + overflow-x: auto; + padding: 8px; + gap: 4px; + } + + .endpointItem { + white-space: nowrap; + } +} diff --git a/docusaurus/src/components/ApiReference/ApiHeader.jsx b/docusaurus/src/components/ApiReference/ApiHeader.jsx new file mode 100644 index 0000000000..8cb3f73fd4 --- /dev/null +++ b/docusaurus/src/components/ApiReference/ApiHeader.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import styles from './api-reference.module.scss'; + +/** + * API page header with gradient accent bar. + * + * + */ +export default function ApiHeader({ title, description, baseUrl = 'http://localhost:1337' }) { + return ( +
+

{title}

+ {description &&

{description}

} +
+ Base + {baseUrl} +
+
+ ); +} diff --git a/docusaurus/src/components/ApiReference/ApiReferencePage.jsx b/docusaurus/src/components/ApiReference/ApiReferencePage.jsx new file mode 100644 index 0000000000..c378e2cbbe --- /dev/null +++ b/docusaurus/src/components/ApiReference/ApiReferencePage.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Layout from '@theme/Layout'; +import styles from './api-reference.module.scss'; +import ApiSidebar from './ApiSidebar'; +import ApiHeader from './ApiHeader'; + +/** + * Full API reference page layout. + * Replaces the standard Docusaurus doc page layout for API reference. + * + * + * + * + * + */ +export default function ApiReferencePage({ + title, + description, + baseUrl = 'http://localhost:1337', + sidebarSections = [], + children, +}) { + return ( + +
+ +
+ + {children} +
+
+
+ ); +} diff --git a/docusaurus/src/components/ApiReference/ApiSidebar.jsx b/docusaurus/src/components/ApiReference/ApiSidebar.jsx new file mode 100644 index 0000000000..b9fdc42946 --- /dev/null +++ b/docusaurus/src/components/ApiReference/ApiSidebar.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import styles from './api-reference.module.scss'; +import clsx from 'clsx'; +import { useLocation, useHistory } from '@docusaurus/router'; + +const METHOD_KEY = { + GET: 'get', POST: 'post', PUT: 'put', DELETE: 'del', DEL: 'del', PATCH: 'patch', +}; + +/** + * Custom API sidebar with method badges. + * + * + */ +export default function ApiSidebar({ sections = [] }) { + const location = useLocation(); + + const isActive = (href) => { + if (href.startsWith('#')) { + return location.hash === href; + } + return location.pathname === href || location.pathname === href + '/'; + }; + + return ( + + ); +} diff --git a/docusaurus/src/components/ApiReference/CodePanel.jsx b/docusaurus/src/components/ApiReference/CodePanel.jsx new file mode 100644 index 0000000000..62ef38200f --- /dev/null +++ b/docusaurus/src/components/ApiReference/CodePanel.jsx @@ -0,0 +1,69 @@ +import React, { useState } from 'react'; +import CopyCodeButton from './CopyCodeButton'; +import styles from './api-reference.module.scss'; +import clsx from 'clsx'; + +/** + * CodePanel with language tabs and URL bar. + * + * Usage: + * + */ +export default function CodePanel({ kind = 'http', method = 'GET', path, pathHighlights = [], tabs = [] }) { + const [activeTab, setActiveTab] = useState(0); + const methodKey = method.toUpperCase() === 'DELETE' ? 'del' : method.toLowerCase(); + const displayMethod = method.toUpperCase() === 'DELETE' ? 'DEL' : method.toUpperCase(); + const isJs = kind === 'js'; + + // Highlight path params + const renderPath = (p) => { + if (!p) return null; + let result = p; + pathHighlights.forEach(h => { + result = result.replace(h, `${h}`); + }); + // Also highlight :param patterns + result = result.replace(/:(\w+)/g, `:$1`); + return ; + }; + + return ( +
+ {tabs.length > 1 && ( +
+ {tabs.map((tab, i) => ( + setActiveTab(i)} + > + {tab.label} + + ))} +
+ )} + {path && !isJs && ( +
+ + {displayMethod} + + {renderPath(path)} +
+ )} +
+ +
+          {tabs[activeTab]?.code || ''}
+        
+
+
+ ); +} diff --git a/docusaurus/src/components/ApiReference/CopyCodeButton.jsx b/docusaurus/src/components/ApiReference/CopyCodeButton.jsx new file mode 100644 index 0000000000..50571bdaf8 --- /dev/null +++ b/docusaurus/src/components/ApiReference/CopyCodeButton.jsx @@ -0,0 +1,44 @@ +import React, { useState, useCallback } from 'react'; +import styles from './api-reference.module.scss'; +import clsx from 'clsx'; + +/** + * Self-contained copy button for the API reference code/response panels. + * + * The Docusaurus @theme/CodeBlock/CopyButton only renders correctly inside a + * real (it depends on that component's CSS modules for sizing and + * icons). Used standalone it collapses to a 0x0 element. This button owns its + * own markup and styles, so it works anywhere. + */ +export default function CopyCodeButton({ code = '' }) { + const [copied, setCopied] = useState(false); + + const onCopy = useCallback(() => { + if (!code) return; + navigator.clipboard.writeText(code).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }, [code]); + + return ( + + ); +} diff --git a/docusaurus/src/components/ApiReference/Endpoint.jsx b/docusaurus/src/components/ApiReference/Endpoint.jsx new file mode 100644 index 0000000000..e4dcadf697 --- /dev/null +++ b/docusaurus/src/components/ApiReference/Endpoint.jsx @@ -0,0 +1,119 @@ +import React from 'react'; +import useBrokenLinks from '@docusaurus/useBrokenLinks'; +import styles from './api-reference.module.scss'; +import MethodPill from './MethodPill'; +import ParamTable from './ParamTable'; +import CodePanel from './CodePanel'; +import ResponsePanel from './ResponsePanel'; + +/** + * Full 2-column endpoint block matching the V3 mockup. + * + * Usage (HTTP / REST): + * + * + * Usage (JS API — Document Service, etc.): pass kind="js". This hides the HTTP + * method pill and the URL bar, renders `path` as the JS method signature, and + * labels the result "Returns" instead of an HTTP status. A JS call has no HTTP + * verb and no HTTP status, so the http chrome is suppressed. + * + */ +export default function Endpoint({ + id, + kind = 'http', + method = 'GET', + path, + title, + description, + params = [], + paramTitle = 'Parameters', + codeTabs = [], + codePath, + codePathHighlights = [], + responses = [], + isLast = false, + children, +}) { + useBrokenLinks().collectAnchor(id); + const hasColumns = (params.length > 0 || children) && (codeTabs.length > 0 || responses.length > 0); + const isJs = kind === 'js'; + + return ( +
+ {/* Header: full-width, above the 2-column grid */} +
+
+ {isJs ? ( + {path} + ) : ( + <> + + + {path} + + + )} +
+ {description &&

{description}

} +
+ + {/* 2-column grid: params left, code right */} + {hasColumns ? ( +
+
+ {params.length > 0 && } + {children} +
+
+ {codeTabs.length > 0 && ( + + )} + {responses.length > 0 && } +
+
+ ) : ( + /* Fallback: no params, just code below the header */ + <> + {(codeTabs.length > 0 || responses.length > 0) && ( +
+ {codeTabs.length > 0 && ( + + )} + {responses.length > 0 && } +
+ )} + {children &&
{children}
} + + )} +
+ ); +} diff --git a/docusaurus/src/components/ApiReference/MethodPill.jsx b/docusaurus/src/components/ApiReference/MethodPill.jsx new file mode 100644 index 0000000000..b89cd7edfa --- /dev/null +++ b/docusaurus/src/components/ApiReference/MethodPill.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import styles from './api-reference.module.scss'; +import clsx from 'clsx'; + +const METHOD_MAP = { + GET: 'get', + POST: 'post', + PUT: 'put', + DELETE: 'del', + DEL: 'del', + PATCH: 'patch', +}; + +export default function MethodPill({ method = 'GET' }) { + const key = METHOD_MAP[method.toUpperCase()] || 'get'; + return ( + + {method.toUpperCase() === 'DELETE' ? 'DEL' : method.toUpperCase()} + + ); +} diff --git a/docusaurus/src/components/ApiReference/ParamTable.jsx b/docusaurus/src/components/ApiReference/ParamTable.jsx new file mode 100644 index 0000000000..624d9a4dd7 --- /dev/null +++ b/docusaurus/src/components/ApiReference/ParamTable.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import styles from './api-reference.module.scss'; +import clsx from 'clsx'; + +/** + * Structured parameter table for API endpoints. + * + * Usage: + * + */ +export default function ParamTable({ title = 'Parameters', params = [] }) { + if (!params.length) return null; + return ( +
+
{title}
+ {params.map((param, i) => ( +
+
+
{param.name}
+ {param.type &&
{param.type}
} +
+ + {param.required ? 'required' : 'optional'} + +
+
+ ))} +
+ ); +} diff --git a/docusaurus/src/components/ApiReference/ResponsePanel.jsx b/docusaurus/src/components/ApiReference/ResponsePanel.jsx new file mode 100644 index 0000000000..2151bcc484 --- /dev/null +++ b/docusaurus/src/components/ApiReference/ResponsePanel.jsx @@ -0,0 +1,76 @@ +import React, { useState } from 'react'; +import CopyCodeButton from './CopyCodeButton'; +import styles from './api-reference.module.scss'; +import clsx from 'clsx'; + +/** + * ResponsePanel with status tabs and animated dot. + * + * Usage: + * + */ +export default function ResponsePanel({ kind = 'http', responses = [] }) { + const [activeIdx, setActiveIdx] = useState(0); + if (!responses.length) return null; + + const isJs = kind === 'js'; + const active = responses[activeIdx]; + const isOk = active.status < 400; + + const getTabStyle = (status) => { + if (status < 300) return 'response-tab--2xx'; + if (status < 400) return 'response-tab--3xx'; + if (status < 500) return 'response-tab--4xx'; + return 'response-tab--5xx'; + }; + + return ( +
+ {!isJs && responses.length > 1 && ( +
+ {responses.map((r, i) => ( + setActiveIdx(i)} + > + {r.status} + + ))} +
+ )} + +
+ {isJs ? ( + + Returns + + ) : ( + <> + + + {active.status} {active.statusText} + + {active.time && {active.time}} + + )} +
+ +
+
+ +
+            {active.body}
+          
+
+
+
+ ); +} diff --git a/docusaurus/src/components/ApiReference/api-reference.module.scss b/docusaurus/src/components/ApiReference/api-reference.module.scss new file mode 100644 index 0000000000..7dbc23ced2 --- /dev/null +++ b/docusaurus/src/components/ApiReference/api-reference.module.scss @@ -0,0 +1,767 @@ +/** Component: V3 API Reference — Stripe-style 2-column layout */ +@use '../../scss/_mixins.scss' as *; + +/* ═══ LAYOUT ═══ */ +.apiLayout { + display: flex; + min-height: calc(100vh - var(--ifm-navbar-height)); + margin-top: 0; +} + +.apiMain { + flex: 1; + min-width: 0; +} + +/* ═══ API SIDEBAR ═══ */ +.apiSidebar { + width: 240px; + min-width: 240px; + border-right: 1px solid var(--strapi-border); + height: calc(100vh - var(--ifm-navbar-height)); + position: sticky; + top: var(--ifm-navbar-height); + overflow-y: auto; + background: var(--strapi-surface-0); + padding: 8px 0; + scrollbar-width: thin; + scrollbar-color: var(--strapi-surface-3) transparent; + + &__section { + padding: 0 8px; + margin-bottom: 2px; + } + + &__sectionTitle { + font-family: var(--strapi-font-family-technical); + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--strapi-fg-5); + padding: 14px 10px 6px; + } + + &__divider { + height: 1px; + background: var(--strapi-border); + margin: 4px 12px; + } +} + +/* Sidebar items */ +.sItem { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border-radius: 7px; + font-size: 12.5px; + color: var(--strapi-fg-3); + text-decoration: none; + transition: all 0.12s; + cursor: pointer; + + &:hover { + background: var(--strapi-surface-3); + color: var(--strapi-fg-1); + text-decoration: none; + } + + &--active { + background: rgba(73, 69, 255, 0.08); + color: var(--strapi-accent-light); + font-weight: 600; + } + + &__badge { + font-family: var(--strapi-font-family-technical); + font-size: 9px; + font-weight: 700; + padding: 2px 5px; + border-radius: 4px; + text-transform: uppercase; + width: 34px; + text-align: center; + flex-shrink: 0; + + &--get { + background: rgba(52, 211, 153, 0.1); + color: var(--strapi-green); + } + &--post { + background: rgba(73, 69, 255, 0.1); + color: var(--strapi-accent-light); + } + &--put { + background: rgba(251, 191, 36, 0.1); + color: var(--strapi-amber); + } + &--del { + background: rgba(248, 113, 113, 0.1); + color: var(--strapi-red); + } + &--patch { + background: rgba(192, 132, 252, 0.1); + color: var(--strapi-accent-2); + } + } +} + +/* ═══ API HEADER ═══ */ +.apiHeader { + padding: 40px 48px 32px; + border-bottom: 1px solid var(--strapi-border); + position: relative; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 48px; + width: 60px; + height: 3px; + border-radius: 2px; + background: linear-gradient(90deg, var(--strapi-accent), var(--strapi-accent-2)); + animation: apiLineExpand 0.6s var(--strapi-ease) both; + } + + &__title { + font-family: var(--strapi-font-family-display); + font-size: 34px; + font-weight: 800; + letter-spacing: -0.03em; + margin-bottom: 10px; + color: var(--strapi-fg-1); + } + + &__desc { + font-size: 16px; + line-height: 1.6; + color: var(--strapi-fg-3); + max-width: 560px; + } + + &__base { + margin-top: 20px; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 8px 14px; + border-radius: var(--strapi-radius-md); + background: var(--strapi-surface-2); + border: 1px solid var(--strapi-border); + font-family: var(--strapi-font-family-mono); + font-size: 13px; + } + + &__baseLabel { + font-size: 10px; + font-weight: 600; + color: var(--strapi-fg-5); + text-transform: uppercase; + letter-spacing: 0.06em; + } + + &__baseUrl { + color: var(--strapi-fg-2); + } +} + +@keyframes apiLineExpand { + from { width: 0; opacity: 0; } + to { width: 60px; opacity: 1; } +} + +/* ═══ ENDPOINT — 2-column split ═══ */ +.endpoint { + border-bottom: 1px solid var(--strapi-border); + + /* Full-width header above the 2-column grid */ + &__header { + padding: 40px 44px 20px; + } + + /* 2-column grid: params left, code right */ + &__columns { + display: grid; + grid-template-columns: 1fr 1fr; + } + + &__desc { + padding: 24px 44px 40px; + border-right: 1px solid var(--strapi-border); + } + + &__methodRow { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; + } + + &__path { + font-family: var(--strapi-font-family-mono); + font-size: 14px; + color: var(--strapi-fg-2); + } + + // JS API signature (Document Service etc.): shown instead of a method pill + + // URL path, because a JS method call has no HTTP verb. Renders the call + // signature (e.g. strapi.documents().findOne()) as the header anchor. + &__signature { + font-family: var(--strapi-font-family-mono); + font-size: 15px; + font-weight: 600; + color: var(--strapi-fg-1); + background: var(--strapi-surface-2); + border: 1px solid var(--strapi-border); + border-radius: var(--strapi-radius-md, 6px); + padding: 4px 10px; + } + + &__title { + font-family: var(--strapi-font-family-display); + font-size: 22px; + font-weight: 700; + letter-spacing: -0.02em; + margin-bottom: 12px; + color: var(--strapi-fg-1); + border: none; + + &::before, + &::after { + display: none; + } + } + + &__text { + font-size: 15px; + line-height: 1.7; + color: var(--strapi-fg-3); + margin-bottom: 0; + } + + &__code { + padding: 24px 32px 40px; + background: var(--strapi-surface-1); + position: sticky; + top: var(--ifm-navbar-height); + align-self: start; + max-height: calc(100vh - var(--ifm-navbar-height)); + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--strapi-surface-3) transparent; + } + + /* Fallback: code-only layout when there are no params */ + &__codeOnly { + padding: 24px 44px 40px; + max-width: 720px; + } +} + +/* ═══ METHOD PILL ═══ */ +.methodPill { + font-family: var(--strapi-font-family-technical); + font-size: 11px; + font-weight: 700; + padding: 4px 10px; + border-radius: 6px; + text-transform: uppercase; + letter-spacing: 0.02em; + position: relative; + overflow: hidden; + + &::after { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent); + animation: methodShimmer 3s ease-in-out infinite; + } + + &--get { + background: rgba(52, 211, 153, 0.1); + color: var(--strapi-green); + } + &--post { + background: rgba(73, 69, 255, 0.1); + color: var(--strapi-accent-light); + } + &--put { + background: rgba(251, 191, 36, 0.1); + color: var(--strapi-amber); + } + &--del { + background: rgba(248, 113, 113, 0.1); + color: var(--strapi-red); + } + &--patch { + background: rgba(192, 132, 252, 0.1); + color: var(--strapi-accent-2); + } +} + +@keyframes methodShimmer { + 0%, 70% { left: -100%; } + 100% { left: 100%; } +} + +/* ═══ PARAMS TABLE ═══ */ +.params { + &__header { + font-family: var(--strapi-font-family-technical); + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--strapi-fg-3); + padding-bottom: 10px; + border-bottom: 1px solid var(--strapi-border); + margin-bottom: 0; + } +} + +.paramRow { + display: grid; + grid-template-columns: 1.2fr auto 1.5fr; + gap: 12px; + padding: 12px 0; + border-bottom: 1px solid var(--strapi-border); + align-items: start; + transition: background 0.15s; + + &:hover { + background: var(--strapi-surface-1); + margin: 0 -8px; + padding: 12px 8px; + border-radius: 8px; + } + + &:last-child { + border-bottom: none; + } +} + +.param { + &__name { + font-family: var(--strapi-font-family-mono); + font-size: 13px; + font-weight: 500; + color: var(--strapi-fg-1); + } + + &__type { + font-family: var(--strapi-font-family-mono); + font-size: 11px; + color: var(--strapi-fg-4); + margin-top: 2px; + } + + &__badge { + font-family: var(--strapi-font-family-technical); + font-size: 9px; + font-weight: 700; + padding: 2px 6px; + border-radius: 4px; + text-transform: uppercase; + letter-spacing: 0.02em; + + &--req { + background: rgba(248, 113, 113, 0.1); + color: var(--strapi-red); + } + &--opt { + background: var(--strapi-surface-3); + color: var(--strapi-fg-3); + } + } + + &__desc { + font-size: 13px; + line-height: 1.5; + color: var(--strapi-fg-3); + + code { + font-family: var(--strapi-font-family-mono); + font-size: 12px; + padding: 1px 5px; + border-radius: 4px; + background: var(--strapi-surface-2); + color: var(--strapi-accent); + border: 1px solid var(--strapi-border); + } + } +} + +/* ═══ CODE PANEL ═══ */ +.codePanel { + border-radius: 12px; + border: 1px solid var(--strapi-border-strong); + background: var(--strapi-surface-0); + overflow: hidden; + margin-bottom: 16px; + transition: border-color 0.3s; + + &:hover { + border-color: rgba(73, 69, 255, 0.12); + } + + &__tabs { + display: flex; + border-bottom: 1px solid var(--strapi-border); + padding: 0 4px; + background: var(--strapi-surface-2); + } + + &__tab { + font-family: var(--strapi-font-family-mono); + font-size: 11px; + font-weight: 500; + padding: 9px 12px; + color: var(--strapi-fg-4); + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.15s; + user-select: none; + + &:hover { + color: var(--strapi-fg-2); + } + + &--active { + color: var(--strapi-fg-1); + border-bottom-color: var(--strapi-accent); + } + } + + &__url { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border-bottom: 1px solid var(--strapi-border); + font-family: var(--strapi-font-family-mono); + font-size: 12px; + } + + &__urlMethod { + font-weight: 700; + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + text-transform: uppercase; + + &--get { + background: rgba(52, 211, 153, 0.1); + color: var(--strapi-green); + } + &--post { + background: rgba(73, 69, 255, 0.1); + color: var(--strapi-accent-light); + } + &--put { + background: rgba(251, 191, 36, 0.1); + color: var(--strapi-amber); + } + &--del { + background: rgba(248, 113, 113, 0.1); + color: var(--strapi-red); + } + &--patch { + background: rgba(192, 132, 252, 0.1); + color: var(--strapi-accent-2); + } + } + + &__urlPath { + color: var(--strapi-fg-2); + } + + // Wraps the
 so the copy button can anchor to its top-right corner.
+  // Reveals the copy button on hover.
+  &__codeWrap {
+    position: relative;
+
+    &:hover .copyCodeBtn,
+    .copyCodeBtn:focus-visible {
+      opacity: 1;
+    }
+  }
+
+  &__pre {
+    padding: 14px;
+    margin: 0;
+    overflow-x: auto;
+    background: transparent;
+
+    code {
+      font-family: var(--strapi-font-family-mono);
+      font-size: 12.5px;
+      line-height: 1.65;
+      color: var(--strapi-fg-2);
+    }
+  }
+}
+
+.pathHighlight {
+  color: var(--strapi-accent-light);
+}
+
+/* ═══ COPY BUTTON (self-contained, used by CodePanel + ResponsePanel) ═══ */
+.copyCodeBtn {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  z-index: 2;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 30px;
+  height: 30px;
+  padding: 0;
+  border: 1px solid var(--strapi-border);
+  border-radius: 6px;
+  background: var(--strapi-surface-2);
+  color: var(--strapi-fg-2);
+  cursor: pointer;
+  opacity: 0;
+  transition: opacity 0.15s ease-in-out, color 0.15s, border-color 0.15s;
+
+  &:hover {
+    color: var(--strapi-fg-1);
+    border-color: var(--strapi-border-strong);
+  }
+
+  &--copied {
+    opacity: 1;
+    color: var(--strapi-green);
+    border-color: var(--strapi-green);
+  }
+
+  svg {
+    display: block;
+  }
+}
+
+/* ═══ RESPONSE PANEL ═══ */
+.responsePanel {
+  margin-top: 4px;
+}
+
+.responseTabs {
+  display: flex;
+  gap: 2px;
+  margin-bottom: 8px;
+}
+
+.responseTab {
+  font-family: var(--strapi-font-family-technical);
+  font-size: 10px;
+  font-weight: 600;
+  padding: 3px 8px;
+  border-radius: 5px;
+  cursor: pointer;
+  transition: all 0.15s;
+  text-transform: uppercase;
+  letter-spacing: 0.04em;
+  border: 1px solid transparent;
+  user-select: none;
+
+  &--2xx {
+    background: rgba(52, 211, 153, 0.1);
+    color: var(--strapi-green);
+  }
+  &--3xx {
+    background: rgba(96, 165, 250, 0.1);
+    color: #60a5fa;
+  }
+  &--4xx {
+    background: rgba(248, 113, 113, 0.1);
+    color: var(--strapi-red);
+  }
+  &--5xx {
+    background: rgba(251, 191, 36, 0.1);
+    color: var(--strapi-amber);
+  }
+
+  &--active {
+    &.responseTab--2xx { border-color: var(--strapi-green); }
+    &.responseTab--4xx { border-color: var(--strapi-red); }
+    &.responseTab--5xx { border-color: var(--strapi-amber); }
+  }
+}
+
+.responseHeader {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 8px;
+  font-family: var(--strapi-font-family-mono);
+  font-size: 11px;
+}
+
+.responseDot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  animation: apiPulse 2s ease-in-out infinite;
+
+  &--ok {
+    background: var(--strapi-green);
+    box-shadow: 0 0 8px var(--strapi-green);
+  }
+  &--err {
+    background: var(--strapi-red);
+    box-shadow: 0 0 8px var(--strapi-red);
+  }
+}
+
+.responseStatus {
+  font-weight: 600;
+
+  &--ok { color: var(--strapi-green); }
+  &--err { color: var(--strapi-red); }
+}
+
+.responseTime {
+  color: var(--strapi-fg-5);
+  margin-left: auto;
+}
+
+@keyframes apiPulse {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.4; }
+}
+
+/* ═══ RESPONSIVE ═══ */
+@include medium-down {
+  .apiLayout {
+    flex-direction: column;
+  }
+
+  .apiSidebar {
+    width: 100%;
+    min-width: 100%;
+    height: auto;
+    position: static;
+    border-right: none;
+    border-bottom: 1px solid var(--strapi-border);
+    display: flex;
+    overflow-x: auto;
+    padding: 8px;
+    gap: 4px;
+
+    &__section {
+      display: flex;
+      gap: 4px;
+      padding: 0;
+      flex-shrink: 0;
+    }
+
+    &__sectionTitle {
+      display: none;
+    }
+
+    &__divider {
+      width: 1px;
+      height: auto;
+      margin: 0 4px;
+    }
+  }
+
+  .sItem {
+    white-space: nowrap;
+    font-size: 11px;
+    padding: 4px 8px;
+  }
+
+  .endpoint__header {
+    padding: 24px 20px 0;
+  }
+
+  .endpoint__columns {
+    grid-template-columns: 1fr;
+  }
+
+  .endpoint__desc {
+    border-right: none;
+    border-bottom: 1px solid var(--strapi-border);
+    padding: 16px 20px 24px;
+  }
+
+  .endpoint__code {
+    position: static;
+    max-height: none;
+    padding: 16px 20px 24px;
+  }
+
+  .endpoint__codeOnly {
+    padding: 16px 20px 24px;
+  }
+
+  .apiHeader {
+    padding: 24px 20px 20px;
+
+    &::before {
+      left: 20px;
+    }
+
+    &__title {
+      font-size: 24px;
+    }
+  }
+}
+
+/* ═══ LIGHT MODE specifics ═══ */
+.endpoint__code {
+  background: var(--strapi-surface-1);
+}
+
+.codePanel {
+  background: var(--strapi-surface-0);
+}
+
+.codePanel__tabs {
+  background: var(--strapi-surface-2);
+}
+
+/* ═══ DARK MODE ═══ */
+@include dark {
+  .endpoint__code {
+    background: var(--strapi-surface-1);
+  }
+
+  .codePanel {
+    background: var(--strapi-surface-0);
+  }
+
+  .codePanel__tabs {
+    background: var(--strapi-surface-2);
+  }
+
+  .paramRow:hover {
+    background: var(--strapi-surface-1);
+  }
+}
+
+/* ═══ REDUCED MOTION ═══ */
+@media (prefers-reduced-motion: reduce) {
+  .methodPill::after {
+    animation: none;
+  }
+
+  .responseDot {
+    animation: none;
+  }
+
+  .apiHeader::before {
+    animation: none;
+    width: 60px;
+    opacity: 1;
+  }
+}
diff --git a/docusaurus/src/components/ApiReference/index.js b/docusaurus/src/components/ApiReference/index.js
new file mode 100644
index 0000000000..c0e7a8b4e6
--- /dev/null
+++ b/docusaurus/src/components/ApiReference/index.js
@@ -0,0 +1,8 @@
+export { default as ApiReferencePage } from './ApiReferencePage';
+export { default as ApiHeader } from './ApiHeader';
+export { default as ApiSidebar } from './ApiSidebar';
+export { default as Endpoint } from './Endpoint';
+export { default as MethodPill } from './MethodPill';
+export { default as ParamTable } from './ParamTable';
+export { default as CodePanel } from './CodePanel';
+export { default as ResponsePanel } from './ResponsePanel';
diff --git a/docusaurus/src/components/Card/card.module.scss b/docusaurus/src/components/Card/card.module.scss
index ef0ca9933a..c4e53c5a3b 100644
--- a/docusaurus/src/components/Card/card.module.scss
+++ b/docusaurus/src/components/Card/card.module.scss
@@ -6,13 +6,13 @@
   --strapi-card-border-color: var(--strapi-neutral-150);
   --strapi-card-border-color-dark: var(--strapi-neutral-150);
   --strapi-card-border-color-light: var(--strapi-neutral-150);
-  --strapi-card-border-radius: 4px;
+  --strapi-card-border-radius: var(--strapi-radius-lg);
   --strapi-card-background-dark: linear-gradient(180deg, var(--strapi-neutral-100) 0%, var(--strapi-neutral-0) 100%);
   --strapi-card-background-light: var(--strapi-neutral-0);
   --strapi-card-box-shadow: 0 0 0 transparent;
   --strapi-card-content-delimited: 395px;
   --strapi-card-img-border-width: 1px;
-  --strapi-card-img-border-radius: 4px;
+  --strapi-card-img-border-radius: var(--strapi-radius-md);
   --strapi-card-img-bg-scale: 1;
   --strapi-card-justify-content: center;
   --strapi-card-position: relative;
@@ -47,11 +47,9 @@
 .card {
   position: var(--strapi-card-position);
   overflow: var(--strapi-card-overflow);
-  // background: var(--strapi-card-background);
-  background-color: transparent;
+  background: transparent;
   border-radius: var(--strapi-card-border-radius);
   border: 1px solid var(--strapi-card-border-color-light);
-  // box-shadow: var(--strapi-card-box-shadow);
   display: flex;
   flex-direction: column;
   gap: var(--strapi-card-gap);
@@ -60,7 +58,6 @@
   text-align: var(--strapi-card-text-align);
   padding: var(--strapi-card-py) var(--strapi-card-px);
   transition: all 0.2s ease;
-  background: var(--strapi-card-background-light);
 
   &:focus, &:hover {
     --strapi-card-border-color: var(--strapi-card-hover-border-color);
@@ -68,7 +65,8 @@
     --strapi-card-img-bg-scale: var(--strapi-card-hover-img-bg-scale);
     cursor: pointer;
     transform: translateY(-2px);
-    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+    border-color: var(--strapi-card-hover-border-color);
+    box-shadow: 0px 1px 4px rgba(33, 33, 52, 0.1);
   }
 
   &-category-icon-container {
@@ -90,7 +88,7 @@
     font-weight: var(--strapi-card-title-font-weight);
     line-height: var(--strapi-card-title-line-height);
     margin: 0;
-    font-family: "SF Pro Text", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; 
+    font-family: var(--strapi-font-family-body) !important; 
     font-size: 28px;
     font-style: normal;
     font-weight: 600;
@@ -114,7 +112,7 @@
     --ifm-link-decoration: underline;
 
     color:#A5A5BA;
-    font-family: "SF Pro Text", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; 
+    font-family: var(--strapi-font-family-body) !important; 
     font-size: 16px;
     font-style: normal;
     font-weight: 400;
@@ -222,7 +220,7 @@
   --strapi-card-container-icon-background-color-cloud: var(--strapi-neutral-0);
 
   .card {
-    background: var(--strapi-card-background-dark);
+    background: transparent;
 
     &-category-icon-container {
       /* Couleur par défaut pour tous les conteneurs d'icônes */
diff --git a/docusaurus/src/components/CollapsibleTOC/CollapsibleTOC.jsx b/docusaurus/src/components/CollapsibleTOC/CollapsibleTOC.jsx
new file mode 100644
index 0000000000..d24132af80
--- /dev/null
+++ b/docusaurus/src/components/CollapsibleTOC/CollapsibleTOC.jsx
@@ -0,0 +1,45 @@
+import React, { useState, useCallback } from 'react';
+import styles from './CollapsibleTOC.module.scss';
+
+const STORAGE_KEY = 'strapi-toc-collapsed';
+
+function getInitialCollapsed() {
+  try {
+    return localStorage.getItem(STORAGE_KEY) === 'true';
+  } catch {
+    return false;
+  }
+}
+
+export default function CollapsibleTOC({ children }) {
+  const [collapsed, setCollapsed] = useState(getInitialCollapsed);
+
+  const toggle = useCallback(() => {
+    setCollapsed(prev => {
+      const next = !prev;
+      try { localStorage.setItem(STORAGE_KEY, String(next)); } catch {}
+      return next;
+    });
+  }, []);
+
+  return (
+    
+
+ {!collapsed && On this page} + +
+ {!collapsed && ( +
+ {children} +
+ )} +
+ ); +} diff --git a/docusaurus/src/components/CollapsibleTOC/CollapsibleTOC.module.scss b/docusaurus/src/components/CollapsibleTOC/CollapsibleTOC.module.scss new file mode 100644 index 0000000000..f9cb4416f2 --- /dev/null +++ b/docusaurus/src/components/CollapsibleTOC/CollapsibleTOC.module.scss @@ -0,0 +1,88 @@ +/** V3: Collapsible Table of Contents — Sanity-inspired */ + +.wrapper { + position: sticky; + top: var(--ifm-navbar-height, 56px); + width: 100%; + max-height: calc(100vh - var(--ifm-navbar-height, 56px)); + overflow-y: auto; + border-left: none; + padding: 0 8px 28px 20px; + + /* Thin scrollbar when TOC content overflows */ + scrollbar-width: thin; + scrollbar-color: var(--strapi-neutral-200) transparent; + &::-webkit-scrollbar { width: 3px; } + &::-webkit-scrollbar-track { background: transparent; } + &::-webkit-scrollbar-thumb { background-color: var(--strapi-neutral-200); border-radius: 3px; } + + // Hide on mobile/tablet — Docusaurus shows a "On this page" dropdown instead + @media (max-width: 996px) { + display: none; + } +} + +.collapsed { + border-left: none; + padding: 0; + display: flex; + align-items: flex-start; + justify-content: center; +} + +/** Header: label + toggle button */ +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding-bottom: 8px; + min-height: 44px; +} + +.collapsed .header { + padding-bottom: 0; + min-height: auto; + padding-top: 4px; +} + +.label { + font-family: var(--strapi-font-family-technical, 'JetBrains Mono', monospace); + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--strapi-neutral-400); + white-space: nowrap; +} + +.toggle { + width: 28px; + height: 28px; + border-radius: 6px; + border: 1px solid var(--strapi-neutral-200); + background: transparent; + cursor: pointer; + display: grid; + place-items: center; + color: var(--strapi-neutral-400); + font-size: 14px; + line-height: 1; + transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; + flex-shrink: 0; + padding: 0; + + &:hover { + background: var(--strapi-neutral-100); + color: var(--strapi-neutral-800); + border-color: var(--strapi-neutral-300); + } +} + +.content { + /* Override inner Docusaurus TOC sticky — our wrapper handles it */ + :global(.theme-doc-toc-desktop) { + position: static; + max-height: none; + overflow-y: visible; + } +} diff --git a/docusaurus/src/components/CustomDocCard.js b/docusaurus/src/components/CustomDocCard.js index 3a30253f18..2ce93b7787 100644 --- a/docusaurus/src/components/CustomDocCard.js +++ b/docusaurus/src/components/CustomDocCard.js @@ -1,9 +1,28 @@ import React from 'react' import classNames from 'classnames'; import Link from '@docusaurus/Link'; +import { useViewMode } from './ViewMode/ViewModeContext'; export default function CustomDocCard(props) { const { title, description, link, icon, small = false } = props; + const { viewMode } = useViewMode(); + + // Markdown mode: render as simple bullet point + if (viewMode === 'markdown') { + return ( +
  • + + {title} + + {description && ( +
    + {description} +
    + )} +
  • + ); + } + const linkClasses = classNames({ card: true, cardContainer: true, diff --git a/docusaurus/src/components/CustomDocCardsWrapper.js b/docusaurus/src/components/CustomDocCardsWrapper.js index 29b5baf059..d32015b0ee 100644 --- a/docusaurus/src/components/CustomDocCardsWrapper.js +++ b/docusaurus/src/components/CustomDocCardsWrapper.js @@ -1,6 +1,18 @@ import React from 'react'; +import { useViewMode } from './ViewMode/ViewModeContext'; export default function CustomDocCardsWrapper({ children }) { + const { viewMode } = useViewMode(); + + // Markdown mode: render as a plain
      + if (viewMode === 'markdown') { + return ( +
        + {children} +
      + ); + } + return (
      {children} diff --git a/docusaurus/src/components/Hero/hero.module.scss b/docusaurus/src/components/Hero/hero.module.scss index 08cc4980f9..760b2414bf 100644 --- a/docusaurus/src/components/Hero/hero.module.scss +++ b/docusaurus/src/components/Hero/hero.module.scss @@ -1,55 +1,115 @@ -/** Component: Hero */ +/** Component: Hero — V3 Redesign */ @use '../../scss/_mixins.scss' as *; -:root { - --strapi-hero-py: var(--strapi-spacing-6); - --strapi-hero-gap: var(--strapi-spacing-4); - --strapi-hero-title-color: #1D1B84; - --strapi-hero-title-font-size: var(--strapi-font-size-xl); - --strapi-hero-title-line-height: 1.25; - --strapi-hero-description-color: #4E6294; - --strapi-hero-description-font-size: var(--strapi-font-size-lg); - --strapi-hero-description-line-height: 1.25; -} - .hero { - padding-top: var(--strapi-hero-py); - padding-bottom: var(--strapi-hero-py); + position: relative; + padding-top: 96px; + padding-bottom: 48px; text-align: center; + overflow: hidden; + + /** V3: Mesh gradient orbs */ + &::before, + &::after { + content: ''; + position: absolute; + border-radius: 50%; + filter: blur(100px); + pointer-events: none; + z-index: 0; + } + + &::before { + width: 500px; + height: 500px; + background: rgba(73, 69, 255, 0.12); + top: -100px; + left: -100px; + animation: meshFloat 20s ease-in-out infinite; + } + + &::after { + width: 400px; + height: 400px; + background: rgba(192, 132, 252, 0.08); + bottom: -50px; + right: -50px; + animation: meshFloat 25s ease-in-out infinite reverse; + } &__title { - color: var(--strapi-hero-title-color); - font-weight: 700; - font-size: var(--strapi-hero-title-font-size); - line-height: var(--strapi-hero-title-line-height); - letter-spacing: 0.4px; - margin: 0 0 var(--strapi-hero-gap); - margin-top: 70px; + font-family: var(--strapi-font-family-display); + color: var(--strapi-fg-1); + font-weight: 800; + font-size: 36px; + line-height: 1.15; + letter-spacing: -0.03em; + margin: 0 0 var(--strapi-spacing-5); + position: relative; + z-index: 1; } &__description { - color: var(--strapi-hero-description-color); - font-size: var(--strapi-hero-description-font-size); - line-height: var(--strapi-hero-description-line-height); + font-family: var(--strapi-font-family-body); + color: var(--strapi-fg-3); + font-size: 18px; + line-height: 1.6; margin: 0; + position: relative; + z-index: 1; + max-width: 520px; + margin-left: auto; + margin-right: auto; } } /** Responsive */ @include medium-up { - :root { - --strapi-hero-py: 40px; - --strapi-hero-title-font-size: 43px; - --strapi-hero-title-line-height: 56px; - --strapi-hero-description-font-size: 21px; - --strapi-hero-description-line-height: 28px; + .hero { + padding-top: 120px; + padding-bottom: 56px; + + &__title { + font-size: 56px; + line-height: 1.1; + } + + &__description { + font-size: 20px; + } + + &::before { + width: 700px; + height: 700px; + } + + &::after { + width: 600px; + height: 600px; + } } } /** Dark mode */ @include dark { - :root { - --strapi-hero-title-color: #FFFFFF; - --strapi-hero-description-color: #A5A5BA; + .hero { + &::before { + background: rgba(73, 69, 255, 0.15); + } + &::after { + background: rgba(192, 132, 252, 0.1); + } } -} \ No newline at end of file +} + +@keyframes meshFloat { + 0%, 100% { + transform: translate(0, 0) scale(1); + } + 33% { + transform: translate(30px, -20px) scale(1.05); + } + 66% { + transform: translate(-20px, 15px) scale(0.95); + } +} diff --git a/docusaurus/src/components/HomepageAIButton/HomepageAIButton.js b/docusaurus/src/components/HomepageAIButton/HomepageAIButton.js index fd4b59cee3..51b794f182 100644 --- a/docusaurus/src/components/HomepageAIButton/HomepageAIButton.js +++ b/docusaurus/src/components/HomepageAIButton/HomepageAIButton.js @@ -1,13 +1,12 @@ -import React, { useState } from 'react'; +import React from 'react'; import styles from './homepageaibutton.module.scss' -import Icon from '../Icon' export default function HomepageAIButton() { return (
      ); diff --git a/docusaurus/src/components/HomepageAIButton/homepageaibutton.module.scss b/docusaurus/src/components/HomepageAIButton/homepageaibutton.module.scss index 9927afe07b..736a1c7b1c 100644 --- a/docusaurus/src/components/HomepageAIButton/homepageaibutton.module.scss +++ b/docusaurus/src/components/HomepageAIButton/homepageaibutton.module.scss @@ -1,87 +1,142 @@ +/** Component: Homepage AI Command Bar — V3 matching mockup-v2 exactly */ @use '@site/src/scss/_mixins.scss' as mixins; -:root { - --button-hover-border: rgba(255,255,255,.2); - --button-hover-transition-timing: 0.3s; -} - .homepage-ai-button-container { display: flex; align-items: center; justify-content: center; text-align: center; - margin-top: 10px; - margin-bottom: 72px; z-index: 100; + position: relative; } .homepage-ai-button { position: relative; - background-color: var(--strapi-neutral-0); - border-radius: 4px; - border: 1px solid #666687; + background: var(--strapi-surface-1); + border-radius: 16px; + border: 1px solid var(--strapi-border-strong); display: flex; align-items: center; - justify-content: center; - gap: 10px; - border: solid 1px #DCDCE4; - cursor: pointer; - padding: 26px 34px 26px 26px; - color: var(--strapi-neutral-500, #8E8EA9); - font-feature-settings: 'liga' off, 'clig' off; - font-family: "SF Pro Text", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; - font-size: 18px; - font-style: normal; - font-weight: 600; - line-height: 20px; - letter-spacing: -0.36px; + gap: 12px; + cursor: text; + padding: 0 24px; + width: 100%; + max-width: 580px; + height: 56px; + color: var(--strapi-fg-5); + font-family: var(--strapi-font-family-body); + font-size: 15px; + font-weight: 400; overflow: hidden; + transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} - > svg { - fill: #c0c0cf; - transition: all 0.3s ease-in-out; - } - transition: all 0.3s ease-in-out; +/** Gradient border glow on hover */ +.homepage-ai-button::before { + content: ''; + position: absolute; + inset: -1px; + border-radius: 17px; + background: linear-gradient(135deg, var(--strapi-accent, #4945FF), transparent 40%, transparent 60%, var(--strapi-accent-light, #7B79FF)); + opacity: 0; + transition: opacity 0.3s; + z-index: -1; +} - p { - margin-bottom: 0; - color: var(--strapi-neutral-500); - } +.homepage-ai-button:hover::before { + opacity: 1; } .homepage-ai-button:hover { - cursor: pointer; - background-color: #9736E8; - color: white; - box-shadow: 0 0px 400px 30px #ac73e6, 0 0px 0px 12px #9736e833; - border-color: rgba(255, 255, 255, 0.20) !important; - p { - color: white; - } - > svg { - fill: white; + border-color: transparent; + box-shadow: 0 0 40px var(--strapi-accent-glow), 0 8px 32px rgba(0, 0, 0, 0.1); +} + +/** 4-point star sparkle icon */ +.sparkle { + font-size: 22px; + background: linear-gradient(135deg, var(--strapi-accent-light, #7B79FF), #c084fc); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + flex-shrink: 0; +} + +.homepage-ai-button:hover .sparkle { + animation: sparkleWobble 3s ease-in-out infinite; +} + +@keyframes sparkleWobble { + 0% { transform: rotate(0deg); } + 25% { transform: rotate(15deg); } + 75% { transform: rotate(-15deg); } + 100% { transform: rotate(0deg); } +} + +.placeholder { + flex: 1; + text-align: left; + color: var(--strapi-fg-5); +} + +/** Keyboard shortcut pills */ +.keys { + display: flex; + gap: 4px; + + kbd { + font-family: var(--strapi-font-family-mono, 'JetBrains Mono', monospace); + font-size: 12px; + padding: 4px 8px; + border-radius: 6px; + background: var(--strapi-surface-2); + color: var(--strapi-fg-5); + border: 1px solid var(--strapi-border); + line-height: 1; + min-width: 28px; + text-align: center; } } +/** Shimmer sweep on hover */ .homepage-ai-button::after { content: ""; position: absolute; - z-index: 1; - background-color: rgba(255, 255, 255, 0.50); - height: 200px; - width: 200px; + inset: 0; + background: linear-gradient(90deg, transparent, rgba(73, 69, 255, 0.06), transparent); + background-size: 200% 100%; opacity: 0; - transition: all 0.3s ease-in-out; - border-radius: 50%; - filter: blur(80px); + transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1); } .homepage-ai-button:hover::after { opacity: 1; + animation: shimmer 2s ease-in-out infinite; +} + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } } +/** Dark mode — more visible text, darker surface */ @include mixins.dark { .homepage-ai-button { - border-color: #32324D; + background: var(--strapi-surface-2); + border-color: var(--strapi-border-strong); + } + + .placeholder { + color: var(--strapi-fg-5); + } + + .keys kbd { + background: var(--strapi-surface-3); + border-color: var(--strapi-border-strong); + color: var(--strapi-fg-4); + } + + .homepage-ai-button:hover { + box-shadow: 0 0 60px var(--strapi-accent-glow), 0 8px 32px rgba(0, 0, 0, 0.3); } -} \ No newline at end of file +} diff --git a/docusaurus/src/components/KapaThemeInjector/KapaThemeInjector.jsx b/docusaurus/src/components/KapaThemeInjector/KapaThemeInjector.jsx new file mode 100644 index 0000000000..e4c6aa54dd --- /dev/null +++ b/docusaurus/src/components/KapaThemeInjector/KapaThemeInjector.jsx @@ -0,0 +1,536 @@ +/** + * KapaThemeInjector — Injects theme-aware styles into Kapa's Shadow DOM. + * + * Kapa renders its modal inside a Shadow DOM (#kapa-widget-container), + * which prevents external CSS from reaching its elements. This component + * observes the page theme (data-theme on ) and injects matching + * styles directly into the shadow root. + */ +import { useEffect } from 'react'; + +const STYLE_ID = 'strapi-kapa-theme'; + +/** Shared styles applied in both modes */ +const SHARED_STYLES = ` + .mantine-Paper-root { + border-radius: 16px !important; + overflow: hidden; + font-family: var(--strapi-font-family-body, 'Inter', sans-serif); + } + .mantine-Modal-header { + padding: 20px 24px !important; + } + .mantine-Modal-header .mantine-Title-root { + font-family: var(--strapi-font-family-display, 'Inter Tight', sans-serif); + font-weight: 700; + letter-spacing: -0.02em; + } + .mantine-CloseButton-root { + border-radius: 10px !important; + transition: all 0.2s !important; + } + .mantine-Button-root { + border-radius: 10px !important; + font-family: var(--strapi-font-family-body, 'Inter', sans-serif) !important; + font-size: 13px !important; + font-weight: 500 !important; + transition: all 0.2s !important; + } + .mantine-Input-input, + .mantine-Textarea-input { + font-family: var(--strapi-font-family-body, 'Inter', sans-serif) !important; + font-size: 14px !important; + transition: border-color 0.2s !important; + } + .mantine-Input-input:focus, + .mantine-Textarea-input:focus { + border-color: #4945FF !important; + box-shadow: none !important; + outline: none !important; + } + .mantine-ActionIcon-root { + background: #4945FF !important; + border: none !important; + border-radius: 10px !important; + transition: all 0.2s !important; + } + .mantine-ActionIcon-root:hover { + background: #7B79FF !important; + } + .mantine-ActionIcon-root svg, + .mantine-ActionIcon-root .mantine-ActionIcon-icon { + color: #FFFFFF !important; + } + .mantine-Overlay-root { + backdrop-filter: blur(4px) !important; + } + .mantine-Popover-dropdown { + border-radius: 10px !important; + } + /* Response links — smooth transition on hover */ + .mantine-Modal-body a:not(.mantine-Paper-root) { + transition: color 0.15s ease !important; + } + /* Code blocks — add padding-right so copy button doesn't overlap text */ + div:has(> .mantine-ActionIcon-root) > div { + padding-right: 40px !important; + } +`; + +const LIGHT_STYLES = ` + .mantine-Paper-root { + background: #FFFFFF !important; + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.06) !important; + } + .mantine-Modal-header { + background: #FFFFFF !important; + border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important; + } + .mantine-Modal-header .mantine-Title-root { + color: #09090B !important; + } + .mantine-CloseButton-root { + background: transparent !important; + border: 1px solid rgba(0, 0, 0, 0.08) !important; + color: #09090B !important; + } + .mantine-CloseButton-root:hover { + background: #F4F4F5 !important; + border-color: rgba(0, 0, 0, 0.12) !important; + } + .mantine-Modal-body { + background: #FFFFFF !important; + } + .mantine-Modal-body .mantine-Text-root { + color: #27272A !important; + } + /* Suggestion buttons (default variant, no icon) */ + .mantine-Button-root[data-variant="default"]:not([data-with-left-section]) { + background: #FFFFFF !important; + border: 1px solid rgba(0, 0, 0, 0.08) !important; + color: #27272A !important; + } + .mantine-Button-root[data-variant="default"]:not([data-with-left-section]):hover { + background: #F4F4F5 !important; + border-color: rgba(0, 0, 0, 0.12) !important; + } + .mantine-Button-root[data-variant="default"]:not([data-with-left-section]) .mantine-Button-label { + color: #27272A !important; + } + .mantine-Input-input, + .mantine-Textarea-input { + background: #FFFFFF !important; + border-color: rgba(0, 0, 0, 0.08) !important; + color: #09090B !important; + } + .mantine-Input-input::placeholder, + .mantine-Textarea-input::placeholder { + color: #A1A1AA !important; + } + .mantine-Anchor-root { + color: #A1A1AA !important; + } + /* Disclaimer banner — remove tinted background */ + .scrollable-container div:has(> .mantine-Group-root > .mantine-Text-root) { + background: transparent !important; + border: none !important; + } + /* Links hover — dark violet in light mode */ + .mantine-Modal-body a:not(.mantine-Paper-root):hover { + color: #4945FF !important; + } + .mantine-Popover-dropdown { + background: #FFFFFF !important; + border: 1px solid rgba(0, 0, 0, 0.08) !important; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08) !important; + } +`; + +const DARK_STYLES = ` + /* Override Mantine root CSS variables — Kapa forces data-mantine-color-scheme="light", + so we must redefine these for dark mode to propagate through var() references */ + #kapa-widget-root { + --mantine-color-gray-light: rgba(255, 255, 255, 0.06) !important; + --mantine-color-gray-light-hover: rgba(255, 255, 255, 0.1) !important; + --mantine-color-gray-light-color: #A1A1AA !important; + } + .mantine-Paper-root { + background: #111113 !important; + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.08) !important; + } + .mantine-Modal-header { + background: #111113 !important; + border-bottom: 1px solid rgba(255, 255, 255, 0.06) !important; + } + .mantine-Modal-header .mantine-Title-root { + color: #FAFAFA !important; + } + .mantine-CloseButton-root { + background: transparent !important; + border: 1px solid rgba(255, 255, 255, 0.06) !important; + color: #FAFAFA !important; + } + .mantine-CloseButton-root:hover { + background: #1F1F23 !important; + } + .mantine-CloseButton-root svg { + color: #FAFAFA !important; + } + .mantine-Modal-body { + background: #111113 !important; + color: #D4D4D8 !important; + } + .mantine-Modal-body .mantine-Text-root { + color: #D4D4D8 !important; + } + /* Response content — headings, text, tables, links */ + .mantine-Modal-body h1, + .mantine-Modal-body h2, + .mantine-Modal-body h3, + .mantine-Modal-body h4 { + color: #FAFAFA !important; + } + .mantine-Modal-body p, + .mantine-Modal-body li, + .mantine-Modal-body td, + .mantine-Modal-body span { + color: #D4D4D8 !important; + } + .mantine-Modal-body th { + color: #FAFAFA !important; + background: rgba(255, 255, 255, 0.04) !important; + } + .mantine-Modal-body table { + border-color: rgba(255, 255, 255, 0.08) !important; + } + .mantine-Modal-body td, + .mantine-Modal-body th { + border-color: rgba(255, 255, 255, 0.08) !important; + } + .mantine-Modal-body a { + color: #7B79FF !important; + } + /* Links hover — brighter violet in dark mode */ + .mantine-Modal-body a:not(.mantine-Paper-root):hover { + color: #A5A3FF !important; + } + .mantine-Modal-body strong { + color: #FAFAFA !important; + } + /* Source cards (anchor elements styled as Paper) */ + a.mantine-Paper-root { + background: #18181B !important; + border: 1px solid rgba(255, 255, 255, 0.06) !important; + color: #D4D4D8 !important; + } + a.mantine-Paper-root:hover { + background: #1F1F23 !important; + border-color: rgba(255, 255, 255, 0.1) !important; + } + a.mantine-Paper-root .mantine-Text-root { + color: #A1A1AA !important; + } + /* Action buttons (New chat, Copy, Good/Bad answer) */ + .mantine-Modal-body .mantine-UnstyledButton-root { + color: #D4D4D8 !important; + } + .mantine-Modal-body .mantine-UnstyledButton-root:hover { + background: rgba(255, 255, 255, 0.05) !important; + } + /* SVGs in modal body inherit light color */ + .mantine-Modal-body svg { + color: #A1A1AA !important; + } + /* Disclaimer banner — remove the light bg, keep it transparent */ + .scrollable-container div:has(> .mantine-Group-root > .mantine-Text-root) { + background: transparent !important; + border: none !important; + } + /* Use MCP button in header — dark mode */ + .mantine-Modal-header .mantine-Group-root:has(> .mantine-Text-root) { + background: #18181B !important; + border: 1px solid rgba(255, 255, 255, 0.06) !important; + color: #D4D4D8 !important; + } + .mantine-Modal-header .mantine-Group-root:has(> .mantine-Text-root):hover { + background: #1F1F23 !important; + border-color: rgba(255, 255, 255, 0.1) !important; + } + .mantine-Modal-header .mantine-Group-root:has(> .mantine-Text-root) .mantine-Text-root { + color: #D4D4D8 !important; + } + .mantine-Modal-header .mantine-Group-root:has(> .mantine-Text-root) svg { + color: #D4D4D8 !important; + } + /* Suggestion buttons (default variant, no icon) */ + .mantine-Button-root[data-variant="default"]:not([data-with-left-section]) { + --button-bg: #18181B !important; + --button-hover: #1F1F23 !important; + --button-color: #D4D4D8 !important; + --button-bd: 1px solid rgba(255, 255, 255, 0.06) !important; + background: #18181B !important; + border: 1px solid rgba(255, 255, 255, 0.06) !important; + color: #D4D4D8 !important; + } + .mantine-Button-root[data-variant="default"]:not([data-with-left-section]):hover { + background: #1F1F23 !important; + border-color: rgba(255, 255, 255, 0.1) !important; + } + .mantine-Button-root[data-variant="default"]:not([data-with-left-section]) .mantine-Button-label { + color: #D4D4D8 !important; + } + /* Action buttons — New chat, Copy, Good/Bad answer (light variant) */ + .mantine-Button-root[data-variant="light"] { + --button-bg: rgba(255, 255, 255, 0.04) !important; + --button-hover: rgba(255, 255, 255, 0.1) !important; + --button-color: #71717A !important; + --button-bd: 1px solid transparent !important; + background: rgba(255, 255, 255, 0.04) !important; + border: 1px solid transparent !important; + color: #71717A !important; + } + .mantine-Button-root[data-variant="light"]:hover { + background: rgba(255, 255, 255, 0.1) !important; + border-color: transparent !important; + color: #D4D4D8 !important; + } + .mantine-Button-root[data-variant="light"] .mantine-Button-label { + color: inherit !important; + } + .mantine-Button-root[data-variant="light"] svg { + color: inherit !important; + } + /* Input wrapper (inner Paper-root) — match modal background */ + .mantine-Paper-root:has(.mantine-Textarea-input) { + background: transparent !important; + border: 1px solid rgba(255, 255, 255, 0.08) !important; + } + .mantine-Input-input, + .mantine-Textarea-input { + background: transparent !important; + border-color: rgba(255, 255, 255, 0.06) !important; + color: #FAFAFA !important; + } + .mantine-Input-input::placeholder, + .mantine-Textarea-input::placeholder { + color: #71717A !important; + } + .mantine-Anchor-root { + color: #71717A !important; + } + .mantine-Popover-dropdown { + background: #18181B !important; + border: 1px solid rgba(255, 255, 255, 0.06) !important; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3) !important; + color: #FAFAFA !important; + } + .mantine-Popover-dropdown * { + color: #D4D4D8 !important; + } + .mantine-Popover-dropdown h1, + .mantine-Popover-dropdown h2, + .mantine-Popover-dropdown h3, + .mantine-Popover-dropdown h4, + .mantine-Popover-dropdown strong { + color: #FAFAFA !important; + } + .mantine-Popover-dropdown a { + color: #7B79FF !important; + } + .mantine-Popover-dropdown a:hover { + color: #A5A3FF !important; + } + .mantine-Popover-dropdown svg { + color: #A1A1AA !important; + } + .mantine-Popover-dropdown .mantine-Paper-root { + background: #1F1F23 !important; + box-shadow: none !important; + border: 1px solid rgba(255, 255, 255, 0.06) !important; + } + .mantine-Popover-dropdown .mantine-Paper-root:hover { + background: #27272A !important; + border-color: rgba(255, 255, 255, 0.1) !important; + } + /* Deep Thinking button — dark-mode base state (default variant + left section) */ + .mantine-Button-root[data-variant="default"][data-with-left-section] { + background: #18181B !important; + border: 1px solid rgba(255, 255, 255, 0.06) !important; + color: #D4D4D8 !important; + } + .mantine-Button-root[data-variant="default"][data-with-left-section] .mantine-Button-label { + color: #D4D4D8 !important; + } + .mantine-Button-root[data-variant="default"][data-with-left-section]:hover { + background: #1F1F23 !important; + border-color: rgba(255, 255, 255, 0.1) !important; + } + /* Deep Thinking active/expanded state — violet accent */ + .mantine-Button-root[data-variant="default"][data-with-left-section][aria-expanded="true"] { + background: rgba(73, 69, 255, 0.12) !important; + border-color: rgba(73, 69, 255, 0.4) !important; + color: #A5B4FC !important; + } + .mantine-Button-root[data-variant="default"][data-with-left-section][aria-expanded="true"] .mantine-Button-label { + color: #A5B4FC !important; + } + .mantine-Button-root[data-variant="default"][data-with-left-section][aria-expanded="true"] svg { + color: #A5B4FC !important; + } + .mantine-Alert-root { + background: transparent !important; + border: 1px solid rgba(255, 255, 255, 0.06) !important; + } + .mantine-Modal-body code, + .mantine-Modal-header code { + background: #09090B !important; + color: #E4E4E7 !important; + } + .mantine-Code-root { + background: rgba(255, 255, 255, 0.06) !important; + color: #E4E4E7 !important; + } + .mantine-Modal-body pre, + .mantine-Modal-header pre { + border: 1px solid rgba(255, 255, 255, 0.06) !important; + } +`; + +function injectStyles(theme) { + const container = document.getElementById('kapa-widget-container'); + const shadow = container?.shadowRoot; + if (!shadow) return; + + let styleEl = shadow.getElementById(STYLE_ID); + if (!styleEl) { + styleEl = document.createElement('style'); + styleEl.id = STYLE_ID; + shadow.appendChild(styleEl); + } + + const themeStyles = theme === 'dark' ? DARK_STYLES : LIGHT_STYLES; + styleEl.textContent = SHARED_STYLES + themeStyles; +} + +const DEEP_THINKING_LABEL_DISABLED = 'Enable deep thinking'; +const DEEP_THINKING_LABEL_ENABLED = 'Deep thinking enabled'; + +/** + * Overrides the Deep Thinking button label based on its toggle state. + * + * Kapa uses data-variant="outline" when enabled, "default" when disabled. + * React re-renders the label text on every hover/state change, replacing + * any DOM text we set. To counteract this we observe both: + * – button attributes (data-variant) to detect toggle state changes + * – label childList to catch React text-node replacements + * + * Returns a cleanup function, or null if the button is not found yet. + */ +function setupDeepThinkingLabel(shadow) { + if (!shadow) return null; + + const btn = shadow.querySelector( + '.mantine-Button-root[data-with-left-section][aria-haspopup="dialog"]' + ); + if (!btn) return null; + + const labelEl = btn.querySelector('.mantine-Button-label'); + if (!labelEl) return null; + + let updating = false; // guard against infinite recursion + + const update = () => { + if (updating) return; + updating = true; + const isEnabled = btn.getAttribute('data-variant') === 'outline'; + const desired = isEnabled + ? DEEP_THINKING_LABEL_ENABLED + : DEEP_THINKING_LABEL_DISABLED; + if (labelEl.textContent !== desired) { + labelEl.textContent = desired; + } + updating = false; + }; + + // Set initial label + update(); + + // Watch button attributes for state changes (variant, aria-expanded) + const btnObs = new MutationObserver(update); + btnObs.observe(btn, { + attributes: true, + attributeFilter: ['data-variant', 'aria-expanded'], + }); + + // Watch label childList to catch React re-renders that replace our text + const labelObs = new MutationObserver(update); + labelObs.observe(labelEl, { childList: true, subtree: true }); + + return { + disconnect() { + btnObs.disconnect(); + labelObs.disconnect(); + }, + }; +} + +export default function KapaThemeInjector() { + useEffect(() => { + if (typeof window === 'undefined') return; + + const getTheme = () => document.documentElement.dataset.theme || 'light'; + let deepThinkingObserver = null; + let trackedBtn = null; // the DOM node we're currently observing + + function ensureDeepThinkingLabel(shadow) { + if (!shadow) return; + const btn = shadow.querySelector( + '.mantine-Button-root[data-with-left-section][aria-haspopup="dialog"]' + ); + // Re-attach if the button is new (Kapa re-rendered its tree) + if (btn && btn !== trackedBtn) { + deepThinkingObserver?.disconnect(); + deepThinkingObserver = setupDeepThinkingLabel(shadow); + trackedBtn = btn; + } + } + + // Inject immediately if container already exists + injectStyles(getTheme()); + + const container = document.getElementById('kapa-widget-container'); + if (container?.shadowRoot) { + ensureDeepThinkingLabel(container.shadowRoot); + } + + // Observe theme changes on + const themeObserver = new MutationObserver(() => { + injectStyles(getTheme()); + // Kapa may re-render on theme change — re-attach label observer + const c = document.getElementById('kapa-widget-container'); + if (c?.shadowRoot) ensureDeepThinkingLabel(c.shadowRoot); + }); + themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-theme'], + }); + + // Observe when Kapa container appears or re-renders in the DOM + const bodyObserver = new MutationObserver(() => { + const c = document.getElementById('kapa-widget-container'); + if (c?.shadowRoot) { + injectStyles(getTheme()); + ensureDeepThinkingLabel(c.shadowRoot); + } + }); + bodyObserver.observe(document.body, { childList: true, subtree: true }); + + return () => { + themeObserver.disconnect(); + bodyObserver.disconnect(); + deepThinkingObserver?.disconnect(); + }; + }, []); + + return null; +} diff --git a/docusaurus/src/components/NavbarBreadcrumbs/NavbarBreadcrumbs.jsx b/docusaurus/src/components/NavbarBreadcrumbs/NavbarBreadcrumbs.jsx new file mode 100644 index 0000000000..1cbfd094d3 --- /dev/null +++ b/docusaurus/src/components/NavbarBreadcrumbs/NavbarBreadcrumbs.jsx @@ -0,0 +1,132 @@ +import React, { useRef, useEffect, useCallback } from 'react'; +import styles from './NavbarBreadcrumbs.module.scss'; + +const PRODUCTS = { + cms: { switchTo: 'cloud', switchHref: '/cloud/intro' }, + cloud: { switchTo: 'cms', switchHref: '/cms/intro' }, +}; + +const SEGMENT_REWRITES = { + 'cms/api': '/cms/api/content-api', +}; + +const ROOT_LINKS = { cms: '/cms/intro', cloud: '/cloud/intro' }; + +function buildCrumbs(pathname) { + const segments = pathname.replace(/^\//, '').replace(/\/$/, '').split('/'); + if (segments.length === 0) return []; + + return segments.map((segment, index) => { + const cumPath = segments.slice(0, index + 1).join('/'); + let href; + if (index === 0 && ROOT_LINKS[segment]) { + href = ROOT_LINKS[segment]; + } else if (SEGMENT_REWRITES[cumPath]) { + href = SEGMENT_REWRITES[cumPath]; + } else { + href = '/' + cumPath; + } + return { label: segment, href, isLast: index === segments.length - 1, isRoot: index === 0 }; + }); +} + +/** + * Renders breadcrumbs into the container ref imperatively. + * This bypasses React's render cycle which Docusaurus blocks for the navbar. + */ +function renderBreadcrumbs(container, pathname) { + if (!container) return; + + const isDocPage = pathname.startsWith('/cms/') || pathname.startsWith('/cloud/'); + container.style.display = ''; + + if (!isDocPage) { + // Homepage: show product links + let html = `
      `; + html += ``; + container.innerHTML = html; + return; + } + + const rootSegment = pathname.split('/')[1]; // 'cms' or 'cloud' + const product = PRODUCTS[rootSegment]; + const crumbs = buildCrumbs(pathname); + + // Build HTML + let html = `
      `; + html += ``; + container.innerHTML = html; +} + +/** + * Mockup-v3 style breadcrumbs that render inside the navbar. + * Shows a monospace path-style trail: cms / getting-started / quick-start + * The first segment (cms/cloud) shows a subtle switch link on hover. + * + * Uses imperative DOM updates because Docusaurus memoizes the navbar tree + * and prevents React re-renders on client-side navigation. + */ +export default function NavbarBreadcrumbs() { + const containerRef = useRef(null); + const lastPathRef = useRef(''); + + const update = useCallback(() => { + const current = window.location.pathname; + if (current !== lastPathRef.current) { + lastPathRef.current = current; + renderBreadcrumbs(containerRef.current, current); + } + }, []); + + useEffect(() => { + // Initial render + lastPathRef.current = window.location.pathname; + renderBreadcrumbs(containerRef.current, window.location.pathname); + + // Listen for all navigation types + // 1. popstate (back/forward) + window.addEventListener('popstate', update); + + // 2. Patch pushState/replaceState for SPA navigations + const origPush = history.pushState; + const origReplace = history.replaceState; + history.pushState = function (...args) { + origPush.apply(this, args); + // Defer to next microtask so URL is updated + Promise.resolve().then(update); + }; + history.replaceState = function (...args) { + origReplace.apply(this, args); + Promise.resolve().then(update); + }; + + return () => { + window.removeEventListener('popstate', update); + history.pushState = origPush; + history.replaceState = origReplace; + }; + }, [update]); + + return
      ; +} diff --git a/docusaurus/src/components/NavbarBreadcrumbs/NavbarBreadcrumbs.module.scss b/docusaurus/src/components/NavbarBreadcrumbs/NavbarBreadcrumbs.module.scss new file mode 100644 index 0000000000..6f5a084062 --- /dev/null +++ b/docusaurus/src/components/NavbarBreadcrumbs/NavbarBreadcrumbs.module.scss @@ -0,0 +1,127 @@ +@use '../../scss/mixins' as *; + +.navbarBreadcrumbs { + display: flex; + align-items: center; + gap: 20px; + margin-left: 20px; +} + +.separator { + width: 1px; + height: 20px; + background: var(--strapi-neutral-300); + flex-shrink: 0; +} + +.crumbs { + display: flex; + align-items: center; + gap: 6px; + font-family: var(--strapi-font-family-technical, 'JetBrains Mono', monospace); + font-size: 13px; + color: var(--strapi-neutral-600); +} + +.slash { + color: var(--strapi-neutral-400); + user-select: none; +} + +.homeDash { + color: var(--strapi-neutral-400); + user-select: none; + margin: 0 2px; +} + +.crumbLink { + color: var(--strapi-neutral-600); + text-decoration: none; + transition: color 0.15s ease; + + &:hover { + color: var(--strapi-primary-600); + text-decoration: none; + } +} + +.current { + color: var(--strapi-primary-600); + font-weight: 500; +} + +/* ── Root crumb with inline product switch ── */ +.rootCrumb { + position: relative; + display: inline-flex; + align-items: center; + gap: 0; + + &:hover .switchLink { + opacity: 1; + max-width: 80px; + margin-left: 8px; + } +} + +.switchLink { + display: inline-flex; + align-items: center; + font-family: var(--strapi-font-family-technical, 'JetBrains Mono', monospace); + font-size: 12px; + color: var(--strapi-neutral-400); + text-decoration: none; + white-space: nowrap; + overflow: hidden; + max-width: 0; + opacity: 0; + margin-left: 0; + transition: opacity 0.2s ease, max-width 0.25s ease, margin-left 0.25s ease; + + &:hover { + color: var(--strapi-primary-600); + text-decoration: none; + } +} + +/* ── Dark mode ── */ +@include dark { + .separator { + background: var(--strapi-neutral-400); + } + + .slash { + color: var(--strapi-neutral-500); + } + + .crumbLink { + color: var(--strapi-neutral-500); + + &:hover { + color: var(--strapi-primary-600); + } + } + + .current { + color: var(--strapi-primary-600); + } + + .switchLink { + color: var(--strapi-neutral-500); + + &:hover { + color: var(--strapi-primary-600); + } + } + + .homeDash { + color: var(--strapi-neutral-500); + } +} + +// Hide on mobile — too cluttered +@include medium-down { + .navbarBreadcrumbs { + display: none; + } +} diff --git a/docusaurus/src/components/NavbarSearchAI/NavbarSearchAI.jsx b/docusaurus/src/components/NavbarSearchAI/NavbarSearchAI.jsx new file mode 100644 index 0000000000..2b2f03f722 --- /dev/null +++ b/docusaurus/src/components/NavbarSearchAI/NavbarSearchAI.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import SearchBar from '@theme/SearchBar'; +import Icon from '../Icon'; +import styles from './NavbarSearchAI.module.scss'; + +export default function NavbarSearchAI() { + return ( +
      +
      + +
      +
      + +
      + ); +} diff --git a/docusaurus/src/components/NavbarSearchAI/NavbarSearchAI.module.scss b/docusaurus/src/components/NavbarSearchAI/NavbarSearchAI.module.scss new file mode 100644 index 0000000000..c0adcf8d9e --- /dev/null +++ b/docusaurus/src/components/NavbarSearchAI/NavbarSearchAI.module.scss @@ -0,0 +1,194 @@ +@use '../../scss/mixins' as *; + +.wrapper { + display: flex; + align-items: center; + border: 1px solid var(--strapi-neutral-200); + border-radius: 10px; + background: var(--strapi-neutral-0); + height: 36px; + min-width: 310px; + overflow: hidden; + transition: border-color 0.15s ease, box-shadow 0.15s ease; + + &:hover { + border-color: var(--strapi-neutral-300); + } + + &:focus-within { + border-color: var(--strapi-primary-500); + box-shadow: 0 0 0 3px rgba(73, 69, 255, 0.1); + } +} + +/** Search slot: override MeiliSearch defaults */ +.searchSlot { + display: flex; + align-items: center; + flex: 1 1 50%; + height: 100%; + overflow: hidden; + border-radius: 9px 0 0 9px; + transition: background 0.15s ease; + + &:hover { + background: var(--strapi-neutral-100); + + :global(.docsearch-btn-placeholder) { + color: var(--strapi-neutral-600); + } + + :global(.docsearch-btn-placeholder)::before { + color: var(--strapi-primary-500); + } + } + + :global(.navbar__search) { + width: 100%; + min-width: 0; + padding: 0; + } + + :global(.docsearch-btn) { + all: unset; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + height: 36px; + width: 100%; + min-width: 0; + box-sizing: border-box; + cursor: pointer; + padding: 0 10px; + font-family: var(--strapi-font-family-body); + font-size: 13px; + color: var(--strapi-neutral-500); + } + + :global(.docsearch-btn-icon-container) { + display: none; + } + + :global(.docsearch-btn-placeholder) { + font-family: var(--strapi-font-family-body); + font-size: 13px; + font-weight: 500; + color: var(--strapi-neutral-400); + transition: color 0.15s ease; + + &::before { + content: '\E30C'; + font-family: "Phosphor"; + color: var(--strapi-neutral-400); + margin-right: 8px; + font-size: 15px; + position: relative; + top: 2px; + transition: color 0.15s ease; + } + } + + :global(.docsearch-btn-keys) { + display: none; + } + + :global(.docsearch-btn-key) { + font-family: var(--strapi-font-family-technical); + font-size: 10px; + font-weight: 500; + color: var(--strapi-neutral-400); + background: var(--strapi-neutral-100); + border: none; + border-radius: 4px; + padding: 1px 4px; + box-shadow: none; + line-height: 1.4; + width: auto; + min-width: 0; + } +} + +.divider { + width: 1px; + height: 20px; + background: var(--strapi-neutral-200); + flex-shrink: 0; +} + +.askAI { + display: flex; + align-items: center; + justify-content: center; + gap: 7px; + flex: 1 1 50%; + height: 100%; + padding: 0 12px; + border: none; + background: transparent; + cursor: pointer; + white-space: nowrap; + color: var(--strapi-neutral-400); + font-family: var(--strapi-font-family-body); + font-size: 13px; + font-weight: 500; + border-radius: 0 9px 9px 0; + transition: color 0.15s ease, background 0.15s ease; + + i { + font-size: 15px; + color: var(--strapi-neutral-400); + position: relative; + top: 1px; + transition: color 0.15s ease; + } + + &:hover { + background: var(--strapi-neutral-100); + color: var(--strapi-neutral-600); + + i { + color: var(--strapi-primary-500); + } + } +} + +/** Dark mode */ +@include dark { + .wrapper { + background: var(--strapi-neutral-100); + border-color: var(--strapi-neutral-300); + + &:hover { + border-color: var(--strapi-neutral-400); + } + } + + .searchSlot:hover { + background: rgba(255, 255, 255, 0.06); + + :global(.docsearch-btn-placeholder) { + color: rgba(255, 255, 255, 0.8); + } + } + + .divider { + background: var(--strapi-neutral-300); + } + + .askAI:hover { + background: rgba(255, 255, 255, 0.06); + color: rgba(255, 255, 255, 0.8); + + i { + color: var(--strapi-primary-500); + } + } +} + +/** Hide on mobile — use mobile sidebar search instead */ +@include medium-down { + .wrapper { + display: none; + } +} diff --git a/docusaurus/src/components/NextSteps/NextSteps.jsx b/docusaurus/src/components/NextSteps/NextSteps.jsx new file mode 100644 index 0000000000..d4ed1f807d --- /dev/null +++ b/docusaurus/src/components/NextSteps/NextSteps.jsx @@ -0,0 +1,62 @@ +import React from 'react'; +import styles from './next-steps.module.scss'; + +/** + * NextSteps — a beautiful numbered step list for "What's next?" sections. + * + * Usage in MDX: + * ```mdx + * + * + * + * + * ``` + */ +function Step({ title, description, link, index }) { + const content = ( +
      +
      + {index} +
      +
      +
      {title}
      + {description &&
      {description}
      } +
      + {link &&
      } +
      + ); + + if (link) { + return {content}; + } + return content; +} + +export default function NextSteps({ title = "What's next?", children }) { + // Inject index into Step children + const steps = React.Children.toArray(children).filter(Boolean); + + return ( +
      + {title &&

      {title}

      } +
      + {steps.map((child, i) => { + if (React.isValidElement(child) && child.type === Step) { + return React.cloneElement(child, { index: i + 1, key: i }); + } + return child; + })} +
      +
      + ); +} + +NextSteps.Step = Step; diff --git a/docusaurus/src/components/NextSteps/next-steps.module.scss b/docusaurus/src/components/NextSteps/next-steps.module.scss new file mode 100644 index 0000000000..a796c47e7d --- /dev/null +++ b/docusaurus/src/components/NextSteps/next-steps.module.scss @@ -0,0 +1,127 @@ +/** NextSteps — "What's next?" numbered step list + * Design matching the v3 quick-start guide screenshot: + * - Numbered circles with accent background + * - Clean separators between steps + * - Generous vertical spacing + */ +@use '@site/src/scss/_mixins.scss' as *; + +.wrapper { + margin: 48px 0 32px; + padding: 0; +} + +.title { + font-family: var(--strapi-font-family-display, 'Outfit', sans-serif); + font-size: 24px; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--strapi-fg-1, var(--ifm-font-color-base)); + margin: 0 0 32px; + border: none; + padding: 0; +} + +.steps { + display: flex; + flex-direction: column; + gap: 0; +} + +.stepLink { + text-decoration: none !important; + color: inherit; + + &:hover { + text-decoration: none !important; + color: inherit; + } + + &:hover .step { + background: var(--strapi-surface-2, #F4F4F5); + } + + &:hover .stepArrow { + opacity: 1; + transform: translateX(0); + color: var(--strapi-accent, #4945FF); + } + + &:hover .stepTitle { + color: var(--strapi-accent, #4945FF); + } +} + +.step { + display: flex; + align-items: center; + gap: 20px; + padding: 28px 24px; + border-bottom: 1px solid var(--strapi-border, rgba(0, 0, 0, 0.06)); + transition: background 0.2s; + border-radius: 12px; + margin: 0 -24px; + width: calc(100% + 48px); + + &:last-child { + border-bottom: none; + } +} + +.stepNumber { + width: 40px; + height: 40px; + border-radius: 10px; + background: rgba(73, 69, 255, 0.08); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + span { + font-family: var(--strapi-font-family-display, 'Outfit', sans-serif); + font-size: 15px; + font-weight: 600; + color: var(--strapi-accent, #4945FF); + } +} + +.stepContent { + flex: 1; + min-width: 0; +} + +.stepTitle { + font-family: var(--strapi-font-family-display, 'Outfit', sans-serif); + font-size: 16px; + font-weight: 600; + color: var(--strapi-fg-1, var(--ifm-font-color-base)); + margin-bottom: 4px; + transition: color 0.2s; +} + +.stepDesc { + font-size: 14px; + line-height: 1.5; + color: var(--strapi-fg-4, var(--ifm-font-color-secondary)); +} + +.stepArrow { + font-size: 18px; + color: var(--strapi-fg-5, #A1A1AA); + opacity: 0; + transform: translateX(-4px); + transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); + flex-shrink: 0; +} + +/* Dark mode */ +@include dark { + .stepNumber { + background: rgba(73, 69, 255, 0.15); + } + + .stepLink:hover .step { + background: var(--strapi-surface-2, #18181B); + } +} diff --git a/docusaurus/src/components/PageFeedback/FeedbackForm.jsx b/docusaurus/src/components/PageFeedback/FeedbackForm.jsx new file mode 100644 index 0000000000..79a0eea00c --- /dev/null +++ b/docusaurus/src/components/PageFeedback/FeedbackForm.jsx @@ -0,0 +1,82 @@ +import React, { useState } from 'react'; + +const MIN_COMMENT_LENGTH = 20; +const MAX_COMMENT_LENGTH = 2000; + +export default function FeedbackForm({ + onSubmit, + onCancel, + isSubmitting, + required, +}) { + const [comment, setComment] = useState(''); + + const trimmed = comment.trim(); + const isTooLong = trimmed.length > MAX_COMMENT_LENGTH; + const canSubmit = required + ? trimmed.length >= MIN_COMMENT_LENGTH && !isTooLong && !isSubmitting + : !isTooLong && !isSubmitting; + + function handleSubmit(e) { + e.preventDefault(); + if (!canSubmit) return; + const hp = e.target.elements._hp?.value || ''; + onSubmit(trimmed || null, hp); + } + + return ( +
      + +