mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-24 01:23:02 +08:00
Sidebar: 11 separator groups become 5, with each module a collapsible folder that auto-opens on the active page. 61 always-visible rows drop to 16. Groups mirror the app's own nav (Chats/Workspace/Workflows) rather than inventing a taxonomy; Enterprise and Self-Hosting are hoisted out of Platform. Chrome: register the `hover-hover` variant, without which every @sim/emcn hover state silently compiled to nothing; restore the sidebar's Geist font stack; add 11 emcn tokens that were falling back to currentColor; adopt the named type scale; align row geometry, hover tokens and group labels with the app. Rename: mothership/ -> chat/ with redirects for the old URLs. Asset paths, the @mothership.sim.ai domain and the `mothership` log-trigger enum value are deliberately left alone -- they are CDN objects, a real domain, and a live product value. Also removes the page-type badge, drops the "Next" heading from the ToC, and lets FAQ rows open independently so expanding one no longer shifts the page.
114 lines
3.1 KiB
TypeScript
114 lines
3.1 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { ChevronRight } from '@sim/emcn/icons'
|
|
import { serializeJsonLd } from '@/lib/json-ld'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface FAQItem {
|
|
question: string
|
|
answer: string
|
|
}
|
|
|
|
interface FAQProps {
|
|
items: FAQItem[]
|
|
title?: string
|
|
}
|
|
|
|
function FAQItemRow({
|
|
item,
|
|
isOpen,
|
|
onToggle,
|
|
}: {
|
|
item: FAQItem
|
|
isOpen: boolean
|
|
onToggle: () => void
|
|
}) {
|
|
return (
|
|
<div>
|
|
<button
|
|
type='button'
|
|
onClick={onToggle}
|
|
aria-expanded={isOpen}
|
|
className='flex w-full cursor-pointer items-center gap-3 px-4 py-2.5 text-left font-medium text-[var(--text-body)] text-sm transition-colors hover:bg-[var(--surface-active)]'
|
|
>
|
|
<ChevronRight
|
|
className={cn(
|
|
'size-[14px] shrink-0 text-[var(--text-icon)] transition-transform duration-200',
|
|
isOpen && 'rotate-90'
|
|
)}
|
|
/>
|
|
{item.question}
|
|
</button>
|
|
<div
|
|
className='grid transition-[grid-template-rows,opacity] duration-200 ease-in-out'
|
|
style={{
|
|
gridTemplateRows: isOpen ? '1fr' : '0fr',
|
|
opacity: isOpen ? 1 : 0,
|
|
}}
|
|
>
|
|
<div className='overflow-hidden'>
|
|
<div className='px-4 pt-2 pb-2.5 pl-11 text-[var(--text-secondary)] text-sm leading-relaxed'>
|
|
{item.answer}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function FAQ({ items, title = 'Common Questions' }: FAQProps) {
|
|
/**
|
|
* Rows open independently rather than as a single-open accordion. Auto-closing
|
|
* a sibling collapses content *above* the row being opened, which yanks that
|
|
* row out from under the pointer — and since the FAQ is the last block on the
|
|
* page, the shrinking document also clamps the scroll position and lurches the
|
|
* whole viewport. Opening a row now only ever pushes content below it down.
|
|
*/
|
|
const [openIndices, setOpenIndices] = useState<ReadonlySet<number>>(() => new Set())
|
|
|
|
const toggle = (index: number) =>
|
|
setOpenIndices((previous) => {
|
|
const next = new Set(previous)
|
|
if (!next.delete(index)) next.add(index)
|
|
return next
|
|
})
|
|
|
|
const faqSchema = {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'FAQPage',
|
|
mainEntity: items.map((item) => ({
|
|
'@type': 'Question',
|
|
name: item.question,
|
|
acceptedAnswer: {
|
|
'@type': 'Answer',
|
|
text: item.answer,
|
|
},
|
|
})),
|
|
}
|
|
|
|
return (
|
|
<div className='mt-12'>
|
|
<script
|
|
type='application/ld+json'
|
|
dangerouslySetInnerHTML={{ __html: serializeJsonLd(faqSchema) }}
|
|
/>
|
|
<h2 className='mb-4 font-medium text-xl'>{title}</h2>
|
|
<div className='border-[var(--border)] border-t border-b'>
|
|
{items.map((item, index) => (
|
|
<div
|
|
key={item.question}
|
|
className={cn(index !== items.length - 1 && 'border-[var(--border)] border-b')}
|
|
>
|
|
<FAQItemRow
|
|
item={item}
|
|
isOpen={openIndices.has(index)}
|
|
onToggle={() => toggle(index)}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|