diff --git a/.gitignore b/.gitignore index 19c3015d..5332d144 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ node_modules/ .env.test.local .env.production.local .secrets +notes.txt .direnv diff --git a/docs/automation.mdx b/docs/automation.mdx index f431e15b..a69f2c94 100644 --- a/docs/automation.mdx +++ b/docs/automation.mdx @@ -1,6 +1,6 @@ --- sidebar_position: 1.8 -sidebar_label: Automation +sidebar_label: Build Automations description: "Learn how to build automated workflows and processes with PromptQL for reliable, repeatable business tasks." keywords: diff --git a/docs/billing/_category_.json b/docs/billing/_category_.json deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/capabilities.mdx b/docs/capabilities.mdx index c86f08ac..f0c75073 100644 --- a/docs/capabilities.mdx +++ b/docs/capabilities.mdx @@ -1,6 +1,6 @@ --- -sidebar_position: 1.6 -sidebar_label: What can PromptQL do? +sidebar_position: 1.5 +sidebar_label: Capabilities description: "Learn what PromptQL can do to help you make decisions quicker or automate tasks with relability and accuracy." keywords: diff --git a/docs/decision-making.mdx b/docs/decision-making.mdx index 0ce944fa..1dc8c7cc 100644 --- a/docs/decision-making.mdx +++ b/docs/decision-making.mdx @@ -1,6 +1,6 @@ --- sidebar_position: 1.7 -sidebar_label: Decision Making +sidebar_label: Make Decisions description: "Learn how you can use PrompQL for accurate AI in your decision-making processes." keywords: - promptql diff --git a/docs/how-to-talk-to-promptql.mdx b/docs/how-to-talk-to-promptql.mdx index f7a10d67..35693505 100644 --- a/docs/how-to-talk-to-promptql.mdx +++ b/docs/how-to-talk-to-promptql.mdx @@ -1,6 +1,6 @@ --- -sidebar_position: 1.5 -sidebar_label: How to talk to PromptQL +sidebar_position: 1.6 +sidebar_label: Talk to PromptQL description: "Learn how to talk with your data via PromptQL." keywords: - promptql diff --git a/src/css/custom.css b/src/css/custom.css index b3e29341..4febd77a 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -25,6 +25,8 @@ --main-bg-color: #ffffff; --sidebar-bg-color: #f9fafb; --sidebar-bg-color-hover: #f1f5f9; + --sidebar-bg-color-active: #e5e7eb; + --sidebar-margin-color-active: #9ca3af; --heading-color: #1f2937; --heading-color-hover: #0f172a; --sidebar-title-color: #374151; @@ -122,6 +124,8 @@ html[data-theme='dark'] { --main-bg-color: #111827; --sidebar-bg-color: #060e23; --sidebar-bg-color-hover: #1f2937; + --sidebar-bg-color-active: #142046; + --sidebar-margin-color-active: #1f5aff; --heading-color: #e5e7eb; --heading-color-hover: #e5e7eb; --sidebar-title-color: #d1d5db; diff --git a/src/theme/DocRoot/Layout/Sidebar/styles.module.css b/src/theme/DocRoot/Layout/Sidebar/styles.module.css index 221aabf5..76b1058d 100644 --- a/src/theme/DocRoot/Layout/Sidebar/styles.module.css +++ b/src/theme/DocRoot/Layout/Sidebar/styles.module.css @@ -1,10 +1,20 @@ :root { - --doc-sidebar-width: 300px; + --doc-sidebar-width: 250px; --doc-sidebar-hidden-width: 30px; } .docSidebarContainer { display: none; + +} + +.sidebarViewport { + top: 0; + position: sticky; + height: 100%; + max-height: 100vh; + width: 100%; + overflow-x: hidden; } @media (min-width: 997px) { diff --git a/src/theme/DocSidebar/CustomSidebar.css b/src/theme/DocSidebar/CustomSidebar.css new file mode 100644 index 00000000..128558be --- /dev/null +++ b/src/theme/DocSidebar/CustomSidebar.css @@ -0,0 +1,155 @@ +.custom-sidebar { + height: 100vh; + background-color: var(--sidebar-bg-color) !important; + width: 100%; + overflow-y: auto; + padding-top: 5rem; +} + +.custom-sidebar__content { + padding: 1.5rem 0; +} + +.custom-sidebar__category { + margin-bottom: 2rem; +} + +.custom-sidebar__category:last-child { + margin-bottom: 1rem; +} + +.custom-sidebar__category-title { + margin: 0 0 0.75rem 0; + padding: 0 1.5rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #6b7280; +} + +.custom-sidebar__category-content { + display: flex; + flex-direction: column; +} + +.custom-sidebar__empty { + color: #9ca3af; + font-style: italic; + margin: 0; + font-size: 0.875rem; + padding: 0 1.5rem; +} + +.custom-sidebar__items { + display: flex; + flex-direction: column; +} + +.custom-sidebar__item { + margin: 0; +} + +.custom-sidebar__link { + display: flex; + align-items: center; + padding: 0.5rem 1.5rem; + color: var(--sidebar-title-color) !important; + text-decoration: none; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 500; + transition: all 0.15s ease; + border-left: 3px solid transparent; +} + +.custom-sidebar__link:hover { + color: var(--body-link-color-hover) !important; +} + +.custom-sidebar__link--active { + background-color: var(--sidebar-bg-color-active); + border-left-color: var(--sidebar-margin-color-active); + font-weight: 600; +} + +.custom-sidebar__collapsible { + margin: 0; +} + + +.custom-sidebar__folder-title { + display: flex; + align-items: center; + padding: 0.5rem 1.5rem; + cursor: pointer; + font-size: 0.875rem; + transition: all 0.15s ease; + border: none; + background: none; + width: 100%; + text-align: left; + border-left: 3px solid transparent; + list-style: none; +} + +.custom-sidebar__subitems .custom-sidebar__folder-title { + padding-left: 40px; +} + + +.custom-sidebar__collapsible[open] .custom-sidebar__subitems .custom-sidebar__subitems .custom-sidebar__link { + padding-left: 60px !important; +} + +.custom-sidebar__folder-title:hover { + color: var(--body-link-color-hover); +} + +.custom-sidebar__folder-title::-webkit-details-marker { + display: none; +} + +.custom-sidebar__subitems { + list-style: none; + padding: 0; + margin: 0; +} + +.custom-sidebar__subitems .custom-sidebar__link { + padding-left: 2.5rem; + font-weight: 400; + color: #6b7280; + border-left: 3px solid transparent; +} + +.custom-sidebar__subitems .custom-sidebar__link--active { + border-left: 3px solid var(--sidebar-margin-color-active); +} + +/* Scrollbar styling */ +.custom-sidebar::-webkit-scrollbar { + width: 6px; +} + +.custom-sidebar::-webkit-scrollbar-track { + background: transparent; +} + +.custom-sidebar::-webkit-scrollbar-thumb { + background: #d1d5db; + border-radius: 3px; +} + +/* Dark mode support (optional) */ +@media (prefers-color-scheme: dark) { + .custom-sidebar { + background: #111827; + border-right-color: #374151; + } + + .custom-sidebar__link--active { + background-color: var(--sidebar-bg-color-active); + border-left-color: var(--sidebar-margin-color-active); + } +} diff --git a/src/theme/DocSidebar/Desktop/CollapseButton/index.tsx b/src/theme/DocSidebar/Desktop/CollapseButton/index.tsx new file mode 100644 index 00000000..d2c5b662 --- /dev/null +++ b/src/theme/DocSidebar/Desktop/CollapseButton/index.tsx @@ -0,0 +1,31 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import {translate} from '@docusaurus/Translate'; +import IconArrow from '@theme/Icon/Arrow'; +import type {Props} from '@theme/DocSidebar/Desktop/CollapseButton'; + +import styles from './styles.module.css'; + +export default function CollapseButton({onClick}: Props): ReactNode { + return ( + + ); +} diff --git a/src/theme/DocSidebar/Desktop/CollapseButton/styles.module.css b/src/theme/DocSidebar/Desktop/CollapseButton/styles.module.css new file mode 100644 index 00000000..df46519f --- /dev/null +++ b/src/theme/DocSidebar/Desktop/CollapseButton/styles.module.css @@ -0,0 +1,40 @@ +:root { + --docusaurus-collapse-button-bg: transparent; + --docusaurus-collapse-button-bg-hover: rgb(0 0 0 / 10%); +} + +[data-theme='dark']:root { + --docusaurus-collapse-button-bg: rgb(255 255 255 / 5%); + --docusaurus-collapse-button-bg-hover: rgb(255 255 255 / 10%); +} + +@media (min-width: 997px) { + .collapseSidebarButton { + display: block !important; + background-color: var(--docusaurus-collapse-button-bg); + height: 40px; + position: sticky; + bottom: 0; + border-radius: 0; + border: 1px solid var(--ifm-toc-border-color); + } + + .collapseSidebarButtonIcon { + transform: rotate(180deg); + margin-top: 4px; + } + + [dir='rtl'] .collapseSidebarButtonIcon { + transform: rotate(0); + } + + .collapseSidebarButton:hover, + .collapseSidebarButton:focus { + background-color: var(--docusaurus-collapse-button-bg-hover); + } +} + +.collapseSidebarButton { + display: none; + margin: 0; +} diff --git a/src/theme/DocSidebar/Desktop/Content/index.tsx b/src/theme/DocSidebar/Desktop/Content/index.tsx new file mode 100644 index 00000000..fbd058c3 --- /dev/null +++ b/src/theme/DocSidebar/Desktop/Content/index.tsx @@ -0,0 +1,54 @@ +import React, {type ReactNode, useState} from 'react'; +import clsx from 'clsx'; +import {ThemeClassNames} from '@docusaurus/theme-common'; +import { + useAnnouncementBar, + useScrollPosition, +} from '@docusaurus/theme-common/internal'; +import {translate} from '@docusaurus/Translate'; +import DocSidebarItems from '@theme/DocSidebarItems'; +import type {Props} from '@theme/DocSidebar/Desktop/Content'; + +import styles from './styles.module.css'; + +function useShowAnnouncementBar() { + const {isActive} = useAnnouncementBar(); + const [showAnnouncementBar, setShowAnnouncementBar] = useState(isActive); + + useScrollPosition( + ({scrollY}) => { + if (isActive) { + setShowAnnouncementBar(scrollY === 0); + } + }, + [isActive], + ); + return isActive && showAnnouncementBar; +} + +export default function DocSidebarDesktopContent({ + path, + sidebar, + className, +}: Props): ReactNode { + const showAnnouncementBar = useShowAnnouncementBar(); + + return ( + + ); +} diff --git a/src/theme/DocSidebar/Desktop/Content/styles.module.css b/src/theme/DocSidebar/Desktop/Content/styles.module.css new file mode 100644 index 00000000..0c43a4e4 --- /dev/null +++ b/src/theme/DocSidebar/Desktop/Content/styles.module.css @@ -0,0 +1,16 @@ +@media (min-width: 997px) { + .menu { + flex-grow: 1; + padding: 0.5rem; + } + @supports (scrollbar-gutter: stable) { + .menu { + padding: 0.5rem 0 0.5rem 0.5rem; + scrollbar-gutter: stable; + } + } + + .menuWithAnnouncementBar { + margin-bottom: var(--docusaurus-announcement-bar-height); + } +} diff --git a/src/theme/DocSidebar/Desktop/index.tsx b/src/theme/DocSidebar/Desktop/index.tsx new file mode 100644 index 00000000..9446ba23 --- /dev/null +++ b/src/theme/DocSidebar/Desktop/index.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import clsx from 'clsx'; +import {useThemeConfig} from '@docusaurus/theme-common'; +import Logo from '@theme/Logo'; +import CollapseButton from '@theme/DocSidebar/Desktop/CollapseButton'; +import Content from '@theme/DocSidebar/Desktop/Content'; +import type {Props} from '@theme/DocSidebar/Desktop'; + +import styles from './styles.module.css'; + +function DocSidebarDesktop({path, sidebar, onCollapse, isHidden}: Props) { + const { + navbar: {hideOnScroll}, + docs: { + sidebar: {hideable}, + }, + } = useThemeConfig(); + + return ( +
+ {hideOnScroll && } + + {hideable && } +
+ ); +} + +export default React.memo(DocSidebarDesktop); diff --git a/src/theme/DocSidebar/Desktop/styles.module.css b/src/theme/DocSidebar/Desktop/styles.module.css new file mode 100644 index 00000000..c5d5e50e --- /dev/null +++ b/src/theme/DocSidebar/Desktop/styles.module.css @@ -0,0 +1,37 @@ +@media (min-width: 997px) { + .sidebar { + display: flex; + flex-direction: column; + height: 100%; + padding-top: var(--ifm-navbar-height); + width: var(--doc-sidebar-width); + } + + .sidebarWithHideableNavbar { + padding-top: 0; + } + + .sidebarHidden { + opacity: 0; + visibility: hidden; + } + + .sidebarLogo { + display: flex !important; + align-items: center; + margin: 0 var(--ifm-navbar-padding-horizontal); + min-height: var(--ifm-navbar-height); + max-height: var(--ifm-navbar-height); + color: inherit !important; + text-decoration: none !important; + } + + .sidebarLogo img { + margin-right: 0.5rem; + height: 2rem; + } +} + +.sidebarLogo { + display: none; +} diff --git a/src/theme/DocSidebar/Mobile/index.tsx b/src/theme/DocSidebar/Mobile/index.tsx new file mode 100644 index 00000000..c4b33172 --- /dev/null +++ b/src/theme/DocSidebar/Mobile/index.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import clsx from 'clsx'; +import { + NavbarSecondaryMenuFiller, + type NavbarSecondaryMenuComponent, + ThemeClassNames, +} from '@docusaurus/theme-common'; +import {useNavbarMobileSidebar} from '@docusaurus/theme-common/internal'; +import DocSidebarItems from '@theme/DocSidebarItems'; +import type {Props} from '@theme/DocSidebar/Mobile'; + +// eslint-disable-next-line react/function-component-definition +const DocSidebarMobileSecondaryMenu: NavbarSecondaryMenuComponent = ({ + sidebar, + path, +}) => { + const mobileSidebar = useNavbarMobileSidebar(); + return ( + + ); +}; + +function DocSidebarMobile(props: Props) { + return ( + + ); +} + +export default React.memo(DocSidebarMobile); diff --git a/src/theme/DocSidebar/categories.ts b/src/theme/DocSidebar/categories.ts new file mode 100644 index 00000000..d566c812 --- /dev/null +++ b/src/theme/DocSidebar/categories.ts @@ -0,0 +1,38 @@ +type CategoryConfig = { + title: string; + directories: string[]; + exactMatch: boolean; +}; + +export const CATEGORY_CONFIG: Record = { + gettingStarted: { + title: 'Getting Started', + directories: ['quickstart', 'capabilities', 'how-to-talk-to-promptql', 'decision-making', 'automation'], + exactMatch: true, + }, + coreConcepts: { + title: 'Core Concepts', + directories: ['data-modeling', 'data-sources', 'business-logic', 'auth'], + exactMatch: false, + }, + buildingApps: { + title: 'Building Apps', + directories: ['project-configuration', 'how-to-build-with-promptql', 'promptql-apis', 'promptql-playground'], + exactMatch: true, + }, + deployment: { + title: 'Deployment & Operations', + directories: ['deployment', 'observability', 'private-ddn'], + exactMatch: true, + }, + guides: { + title: 'Guides & Recipes', + directories: ['recipes'], + exactMatch: true, + }, + reference: { + title: 'Reference', + directories: ['reference', 'billing', 'help'], + exactMatch: true, + }, +}; diff --git a/src/theme/DocSidebar/index.tsx b/src/theme/DocSidebar/index.tsx new file mode 100644 index 00000000..fd850496 --- /dev/null +++ b/src/theme/DocSidebar/index.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { useLocation } from '@docusaurus/router'; +import type { PropSidebarItem } from '@docusaurus/plugin-content-docs'; +import Link from '@docusaurus/Link'; +import { + buildCategories, + isActiveLink, + hasActiveChild, + type Category, +} from './utils'; +import './CustomSidebar.css'; + +interface CustomSidebarProps { + sidebar: PropSidebarItem[]; +} + +const CustomSidebar: React.FC = ({ sidebar }) => { + const location = useLocation(); + const categories: Category[] = buildCategories(sidebar); + + const renderSidebarItem = (item: PropSidebarItem): React.ReactElement => { + if (item.type === 'link') { + const href = (item as any).href || ''; + const isActive = isActiveLink(href, location.pathname); + + return ( + + {(item as any).label || 'Untitled'} + + ); + } + + if (item.type === 'category') { + const items = (item as any).items; + if (items && Array.isArray(items)) { + const shouldExpand = hasActiveChild(items, location.pathname); + + return ( +
+ + {(item as any).label || 'Untitled Category'} + +
    + {items.map((subItem: PropSidebarItem, subIndex: number) => ( +
  • {renderSidebarItem(subItem)}
  • + ))} +
+
+ ); + } + } + + return {(item as any).label || 'Unknown item'}; + }; + + const renderCategory = (category: Category) => ( +
+

{category.title}

+
+ {category.items.length === 0 ? ( +

No items yet

+ ) : ( +
+ {category.items.map((item, index) => ( +
+ {renderSidebarItem(item)} +
+ ))} +
+ )} +
+
+ ); + + return ( +
+
+ {categories.map(renderCategory)} +
+
+ ); +}; + +export default CustomSidebar; diff --git a/src/theme/DocSidebar/utils.ts b/src/theme/DocSidebar/utils.ts new file mode 100644 index 00000000..438090da --- /dev/null +++ b/src/theme/DocSidebar/utils.ts @@ -0,0 +1,207 @@ +import type { PropSidebarItem } from '@docusaurus/plugin-content-docs'; +import { CATEGORY_CONFIG } from './categories'; + +export interface Category { + id: string; + title: string; + items: PropSidebarItem[]; +} + +/** + * Extract directory name from a Docusaurus path + */ +export const getDirectoryFromPath = (path: string): string => { + const cleanPath = path.replace(/^\/+|\/+$/g, ''); + const segments = cleanPath.split('/'); + + if (segments[0] === 'docs' && segments.length > 1) { + return segments[1]; + } + return segments[0] || ''; +}; + +/** + * Get the primary directory for an item (handles nested categories) + */ +export const getPrimaryDirectory = (item: PropSidebarItem): string => { + if (item.type === 'link') { + const href = (item as any).href; + if (href) { + return getDirectoryFromPath(href); + } + } + + if (item.type === 'category') { + // Check if the category itself has an href + const href = (item as any).href; + if (href) { + return getDirectoryFromPath(href); + } + + // Otherwise, get the directory from the first child item + const items = (item as any).items; + if (items && Array.isArray(items) && items.length > 0) { + return getPrimaryDirectory(items[0]); + } + } + + return ''; +}; + +/** + * Recursively sort items within a category based on directory order + */ +export const sortItemsByDirectoryOrder = ( + items: PropSidebarItem[], + directoryOrder: readonly string[], + exactMatch: boolean = false +): PropSidebarItem[] => { + return items + .map(item => { + // If this is a category, recursively sort its children + if (item.type === 'category') { + const childItems = (item as any).items; + if (childItems && Array.isArray(childItems)) { + return { + ...item, + items: sortItemsByDirectoryOrder(childItems, directoryOrder, exactMatch) + }; + } + } + return item; + }) + .sort((a, b) => { + const aPrimaryDir = getPrimaryDirectory(a); + const bPrimaryDir = getPrimaryDirectory(b); + + let aIndex = -1; + let bIndex = -1; + + if (exactMatch) { + aIndex = directoryOrder.indexOf(aPrimaryDir); + bIndex = directoryOrder.indexOf(bPrimaryDir); + } else { + // For non-exact match, find the first directory that contains the pattern + aIndex = directoryOrder.findIndex(dir => aPrimaryDir.includes(dir)); + bIndex = directoryOrder.findIndex(dir => bPrimaryDir.includes(dir)); + } + + // Items not found in the directory order go to the end + if (aIndex === -1 && bIndex === -1) return 0; + if (aIndex === -1) return 1; + if (bIndex === -1) return -1; + + return aIndex - bIndex; + }); +}; + +/** + * Check if a sidebar item matches any of the given patterns + */ +export const itemMatchesPattern = ( + item: PropSidebarItem, + patterns: readonly string[], + exactMatch: boolean = false +): boolean => { + return patterns.some(pattern => { + if (item.type === 'link') { + const href = (item as any).href; + if (href) { + const directory = getDirectoryFromPath(href); + if (exactMatch) { + return directory === pattern; + } else { + return directory.includes(pattern); + } + } + } + + if (item.type === 'category') { + const href = (item as any).href; + if (href) { + const directory = getDirectoryFromPath(href); + if (exactMatch) { + if (directory === pattern) return true; + } else { + if (directory.includes(pattern)) return true; + } + } + + const items = (item as any).items; + if (items && Array.isArray(items)) { + const hasMatchingChild = items.some((subItem: any) => { + if (subItem.type === 'link' && subItem.href) { + const directory = getDirectoryFromPath(subItem.href); + if (exactMatch) { + return directory === pattern; + } else { + return directory.includes(pattern); + } + } + return false; + }); + if (hasMatchingChild) { + return true; + } + } + } + + return false; + }); +}; + +/** + * Filter sidebar items that match the given category configuration + */ +export const getItemsForCategory = ( + sidebar: PropSidebarItem[], + categoryConfig: readonly string[], + exactMatch: boolean = false +): PropSidebarItem[] => { + if (!sidebar || !Array.isArray(sidebar)) return []; + const matchedItems = sidebar.filter((item: PropSidebarItem) => { + return itemMatchesPattern(item, categoryConfig, exactMatch); + }); + return matchedItems; +}; + +/** + * Build categories from the configuration + */ +export const buildCategories = (sidebar: PropSidebarItem[]): Category[] => { + return Object.entries(CATEGORY_CONFIG).map(([id, config]) => { + const matchedItems = getItemsForCategory(sidebar, config.directories, config.exactMatch); + + // Sort items recursively by directory order + const sortedItems = sortItemsByDirectoryOrder(matchedItems, config.directories, config.exactMatch); + + return { + id, + title: config.title, + items: sortedItems, + }; + }); +}; + +/** + * Check if a link is currently active based on the current pathname + */ +export const isActiveLink = (href: string, currentPathname: string): boolean => { + return currentPathname === href || currentPathname.startsWith(href + '/'); +}; + +/** + * Check if a category has any active child items + */ +export const hasActiveChild = (items: PropSidebarItem[], pathname: string): boolean => { + return items.some(item => { + if (item.type === 'link') { + const href = (item as any).href; + return pathname.startsWith(href); // deep match + } + if (item.type === 'category') { + return hasActiveChild((item as any).items || [], pathname); + } + return false; + }); +};