@adres/mgda-ui 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/brand-knowledge.json +451 -0
- package/knowledge/accordion.md +135 -0
- package/knowledge/activity-rings.md +123 -0
- package/knowledge/alert.md +184 -0
- package/knowledge/answer-actions.md +217 -0
- package/knowledge/aspect-ratio.md +82 -0
- package/knowledge/avatar.md +158 -0
- package/knowledge/badge.md +172 -0
- package/knowledge/bar-graph.md +167 -0
- package/knowledge/bento-grid.md +115 -0
- package/knowledge/blockquote.md +153 -0
- package/knowledge/box-list.md +177 -0
- package/knowledge/bullet-list.md +128 -0
- package/knowledge/button-group.md +179 -0
- package/knowledge/button.md +231 -0
- package/knowledge/calendar.md +168 -0
- package/knowledge/card.md +321 -0
- package/knowledge/cards-nav.md +170 -0
- package/knowledge/chain-of-thought.md +164 -0
- package/knowledge/chat-container.md +135 -0
- package/knowledge/chat-message.md +224 -0
- package/knowledge/checkbox-card.md +172 -0
- package/knowledge/checkbox.md +111 -0
- package/knowledge/citations.md +241 -0
- package/knowledge/combobox.md +265 -0
- package/knowledge/countdown-pill.md +153 -0
- package/knowledge/cta.md +118 -0
- package/knowledge/data-list.md +173 -0
- package/knowledge/data-table.md +220 -0
- package/knowledge/dialog.md +249 -0
- package/knowledge/drawer.md +236 -0
- package/knowledge/dropdown-menu.md +238 -0
- package/knowledge/dropzone.md +153 -0
- package/knowledge/email-chips-input.md +161 -0
- package/knowledge/empty-state.md +106 -0
- package/knowledge/entity-header.md +166 -0
- package/knowledge/feature-grid.md +120 -0
- package/knowledge/feedback-bar.md +127 -0
- package/knowledge/footer.md +116 -0
- package/knowledge/heading.md +189 -0
- package/knowledge/hero.md +361 -0
- package/knowledge/input-button.md +187 -0
- package/knowledge/input-group.md +185 -0
- package/knowledge/input.md +256 -0
- package/knowledge/inset.md +177 -0
- package/knowledge/kpi-card.md +206 -0
- package/knowledge/list-nav.md +181 -0
- package/knowledge/logo-cloud.md +102 -0
- package/knowledge/media-gallery.md +158 -0
- package/knowledge/message-actions.md +175 -0
- package/knowledge/nav-sidebar.md +225 -0
- package/knowledge/number-stepper.md +159 -0
- package/knowledge/page-header.md +109 -0
- package/knowledge/pagination.md +225 -0
- package/knowledge/popover.md +159 -0
- package/knowledge/pricing.md +132 -0
- package/knowledge/progress.md +156 -0
- package/knowledge/prompt-input.md +337 -0
- package/knowledge/prompt-suggestions.md +168 -0
- package/knowledge/quota-bar.md +160 -0
- package/knowledge/quote.md +116 -0
- package/knowledge/radio-card.md +172 -0
- package/knowledge/radio-group.md +135 -0
- package/knowledge/reasoning.md +142 -0
- package/knowledge/search.md +131 -0
- package/knowledge/section-nav.md +162 -0
- package/knowledge/segmented-control.md +181 -0
- package/knowledge/select-button.md +205 -0
- package/knowledge/select.md +210 -0
- package/knowledge/separator.md +75 -0
- package/knowledge/shimmer-text.md +140 -0
- package/knowledge/sidebar.md +227 -0
- package/knowledge/skeleton.md +142 -0
- package/knowledge/slider.md +133 -0
- package/knowledge/slideshow.md +159 -0
- package/knowledge/source.md +156 -0
- package/knowledge/spinner.md +120 -0
- package/knowledge/stat-strip.md +128 -0
- package/knowledge/stats.md +117 -0
- package/knowledge/step-indicator.md +157 -0
- package/knowledge/switch.md +88 -0
- package/knowledge/table.md +190 -0
- package/knowledge/tabs.md +169 -0
- package/knowledge/testimonials.md +115 -0
- package/knowledge/text-link.md +192 -0
- package/knowledge/text.md +194 -0
- package/knowledge/thinking-bar.md +115 -0
- package/knowledge/thinking-steps.md +140 -0
- package/knowledge/toaster.md +127 -0
- package/knowledge/toggle.md +113 -0
- package/knowledge/tool-call-card.md +241 -0
- package/knowledge/tooltip.md +120 -0
- package/knowledge/top-nav.md +138 -0
- package/knowledge.json +19854 -0
- package/package.json +94 -4
- package/src/components/accordion.stories.tsx +46 -0
- package/src/components/accordion.tsx +74 -0
- package/src/components/activity-rings.stories.tsx +73 -0
- package/src/components/activity-rings.tsx +121 -0
- package/src/components/alert.stories.tsx +43 -0
- package/src/components/alert.tsx +132 -0
- package/src/components/answer-actions.stories.tsx +54 -0
- package/src/components/answer-actions.test.tsx +37 -0
- package/src/components/answer-actions.tsx +313 -0
- package/src/components/aspect-ratio.stories.tsx +32 -0
- package/src/components/aspect-ratio.tsx +5 -0
- package/src/components/avatar.stories.tsx +40 -0
- package/src/components/avatar.tsx +154 -0
- package/src/components/badge.stories.tsx +27 -0
- package/src/components/badge.tsx +63 -0
- package/src/components/bar-graph.stories.tsx +28 -0
- package/src/components/bar-graph.tsx +204 -0
- package/src/components/bento-grid.stories.tsx +23 -0
- package/src/components/bento-grid.tsx +67 -0
- package/src/components/blockquote.stories.tsx +28 -0
- package/src/components/blockquote.tsx +88 -0
- package/src/components/box-list.stories.tsx +25 -0
- package/src/components/box-list.tsx +108 -0
- package/src/components/bullet-list.stories.tsx +22 -0
- package/src/components/bullet-list.tsx +101 -0
- package/src/components/button-group.stories.tsx +31 -0
- package/src/components/button-group.tsx +156 -0
- package/src/components/button.stories.tsx +66 -0
- package/src/components/button.test.ts +17 -0
- package/src/components/button.tsx +139 -0
- package/src/components/calendar.stories.tsx +25 -0
- package/src/components/calendar.tsx +367 -0
- package/src/components/card.stories.tsx +44 -0
- package/src/components/card.tsx +392 -0
- package/src/components/cards-nav.stories.tsx +35 -0
- package/src/components/cards-nav.tsx +115 -0
- package/src/components/chain-of-thought.stories.tsx +67 -0
- package/src/components/chain-of-thought.test.tsx +50 -0
- package/src/components/chain-of-thought.tsx +170 -0
- package/src/components/chat-container.stories.tsx +47 -0
- package/src/components/chat-container.test.tsx +49 -0
- package/src/components/chat-container.tsx +222 -0
- package/src/components/chat-message.stories.tsx +52 -0
- package/src/components/chat-message.test.tsx +36 -0
- package/src/components/chat-message.tsx +163 -0
- package/src/components/checkbox-card.stories.tsx +27 -0
- package/src/components/checkbox-card.tsx +200 -0
- package/src/components/checkbox.stories.tsx +26 -0
- package/src/components/checkbox.tsx +61 -0
- package/src/components/citations.stories.tsx +64 -0
- package/src/components/citations.test.tsx +61 -0
- package/src/components/citations.tsx +220 -0
- package/src/components/combobox.stories.tsx +184 -0
- package/src/components/combobox.test.tsx +82 -0
- package/src/components/combobox.tsx +407 -0
- package/src/components/countdown-pill.stories.tsx +24 -0
- package/src/components/countdown-pill.tsx +129 -0
- package/src/components/cta.stories.tsx +29 -0
- package/src/components/cta.tsx +52 -0
- package/src/components/data-list.stories.tsx +50 -0
- package/src/components/data-list.test.tsx +35 -0
- package/src/components/data-list.tsx +181 -0
- package/src/components/data-table.stories.tsx +29 -0
- package/src/components/data-table.tsx +200 -0
- package/src/components/dialog.stories.tsx +42 -0
- package/src/components/dialog.tsx +200 -0
- package/src/components/drawer.stories.tsx +48 -0
- package/src/components/drawer.tsx +385 -0
- package/src/components/dropdown-menu.stories.tsx +41 -0
- package/src/components/dropdown-menu.tsx +252 -0
- package/src/components/dropzone.stories.tsx +19 -0
- package/src/components/dropzone.tsx +250 -0
- package/src/components/email-chips-input.stories.tsx +135 -0
- package/src/components/email-chips-input.tsx +210 -0
- package/src/components/empty-state.stories.tsx +22 -0
- package/src/components/empty-state.tsx +35 -0
- package/src/components/entity-header.stories.tsx +43 -0
- package/src/components/entity-header.test.tsx +35 -0
- package/src/components/entity-header.tsx +92 -0
- package/src/components/feature-grid.stories.tsx +25 -0
- package/src/components/feature-grid.tsx +87 -0
- package/src/components/feedback-bar.stories.tsx +25 -0
- package/src/components/feedback-bar.test.tsx +24 -0
- package/src/components/feedback-bar.tsx +62 -0
- package/src/components/footer.stories.tsx +23 -0
- package/src/components/footer.tsx +78 -0
- package/src/components/heading.stories.tsx +45 -0
- package/src/components/heading.test.tsx +22 -0
- package/src/components/heading.tsx +77 -0
- package/src/components/hero.stories.tsx +38 -0
- package/src/components/hero.tsx +498 -0
- package/src/components/input-button.stories.tsx +19 -0
- package/src/components/input-button.tsx +112 -0
- package/src/components/input-group.stories.tsx +38 -0
- package/src/components/input-group.tsx +157 -0
- package/src/components/input.stories.tsx +31 -0
- package/src/components/input.tsx +284 -0
- package/src/components/inset.stories.tsx +29 -0
- package/src/components/inset.tsx +70 -0
- package/src/components/kpi-card.stories.tsx +29 -0
- package/src/components/kpi-card.tsx +284 -0
- package/src/components/list-nav.stories.tsx +22 -0
- package/src/components/list-nav.tsx +145 -0
- package/src/components/logo-cloud.stories.tsx +23 -0
- package/src/components/logo-cloud.tsx +42 -0
- package/src/components/media-gallery.stories.tsx +25 -0
- package/src/components/media-gallery.tsx +347 -0
- package/src/components/message-actions.stories.tsx +45 -0
- package/src/components/message-actions.test.tsx +32 -0
- package/src/components/message-actions.tsx +119 -0
- package/src/components/nav-sidebar.stories.tsx +58 -0
- package/src/components/nav-sidebar.test.tsx +54 -0
- package/src/components/nav-sidebar.tsx +462 -0
- package/src/components/number-stepper.stories.tsx +19 -0
- package/src/components/number-stepper.tsx +158 -0
- package/src/components/page-header.stories.tsx +25 -0
- package/src/components/page-header.tsx +64 -0
- package/src/components/pagination.stories.tsx +110 -0
- package/src/components/pagination.test.tsx +46 -0
- package/src/components/pagination.tsx +144 -0
- package/src/components/popover.stories.tsx +26 -0
- package/src/components/popover.tsx +62 -0
- package/src/components/pricing.stories.tsx +42 -0
- package/src/components/pricing.tsx +130 -0
- package/src/components/progress.stories.tsx +22 -0
- package/src/components/progress.tsx +189 -0
- package/src/components/prompt-input.stories.tsx +62 -0
- package/src/components/prompt-input.test.tsx +106 -0
- package/src/components/prompt-input.tsx +343 -0
- package/src/components/prompt-suggestions.stories.tsx +53 -0
- package/src/components/prompt-suggestions.test.tsx +37 -0
- package/src/components/prompt-suggestions.tsx +122 -0
- package/src/components/quota-bar.stories.tsx +19 -0
- package/src/components/quota-bar.tsx +191 -0
- package/src/components/quote.stories.tsx +18 -0
- package/src/components/quote.tsx +43 -0
- package/src/components/radio-card.stories.tsx +27 -0
- package/src/components/radio-card.tsx +225 -0
- package/src/components/radio-group.stories.tsx +32 -0
- package/src/components/radio-group.tsx +68 -0
- package/src/components/reasoning.stories.tsx +50 -0
- package/src/components/reasoning.test.tsx +48 -0
- package/src/components/reasoning.tsx +143 -0
- package/src/components/search.stories.tsx +18 -0
- package/src/components/search.tsx +133 -0
- package/src/components/section-nav.stories.tsx +59 -0
- package/src/components/section-nav.test.tsx +36 -0
- package/src/components/section-nav.tsx +146 -0
- package/src/components/segmented-control.stories.tsx +22 -0
- package/src/components/segmented-control.tsx +301 -0
- package/src/components/select-button.stories.tsx +27 -0
- package/src/components/select-button.tsx +174 -0
- package/src/components/select.stories.tsx +27 -0
- package/src/components/select.tsx +198 -0
- package/src/components/separator.stories.tsx +22 -0
- package/src/components/separator.tsx +23 -0
- package/src/components/shimmer-text.stories.tsx +23 -0
- package/src/components/shimmer-text.test.tsx +35 -0
- package/src/components/shimmer-text.tsx +43 -0
- package/src/components/sidebar.stories.tsx +25 -0
- package/src/components/sidebar.tsx +234 -0
- package/src/components/skeleton.stories.tsx +22 -0
- package/src/components/skeleton.tsx +96 -0
- package/src/components/slider.stories.tsx +20 -0
- package/src/components/slider.tsx +176 -0
- package/src/components/slideshow.stories.tsx +30 -0
- package/src/components/slideshow.tsx +291 -0
- package/src/components/source.stories.tsx +32 -0
- package/src/components/source.test.tsx +40 -0
- package/src/components/source.tsx +161 -0
- package/src/components/spinner.stories.tsx +19 -0
- package/src/components/spinner.tsx +76 -0
- package/src/components/stat-strip.stories.tsx +36 -0
- package/src/components/stat-strip.test.tsx +25 -0
- package/src/components/stat-strip.tsx +85 -0
- package/src/components/stats.stories.tsx +24 -0
- package/src/components/stats.tsx +83 -0
- package/src/components/step-indicator.stories.tsx +18 -0
- package/src/components/step-indicator.test.tsx +18 -0
- package/src/components/step-indicator.tsx +286 -0
- package/src/components/switch.stories.tsx +26 -0
- package/src/components/switch.tsx +32 -0
- package/src/components/table.stories.tsx +38 -0
- package/src/components/table.tsx +220 -0
- package/src/components/tabs.stories.tsx +25 -0
- package/src/components/tabs.tsx +276 -0
- package/src/components/testimonials.stories.tsx +22 -0
- package/src/components/testimonials.tsx +109 -0
- package/src/components/text-link.stories.tsx +47 -0
- package/src/components/text-link.test.tsx +18 -0
- package/src/components/text-link.tsx +88 -0
- package/src/components/text.stories.tsx +49 -0
- package/src/components/text.tsx +105 -0
- package/src/components/thinking-bar.stories.tsx +19 -0
- package/src/components/thinking-bar.test.tsx +27 -0
- package/src/components/thinking-bar.tsx +64 -0
- package/src/components/thinking-steps.stories.tsx +44 -0
- package/src/components/thinking-steps.test.tsx +42 -0
- package/src/components/thinking-steps.tsx +92 -0
- package/src/components/toaster.stories.tsx +80 -0
- package/src/components/toaster.tsx +36 -0
- package/src/components/toggle.stories.tsx +21 -0
- package/src/components/toggle.tsx +103 -0
- package/src/components/tool-call-card.stories.tsx +97 -0
- package/src/components/tool-call-card.test.tsx +88 -0
- package/src/components/tool-call-card.tsx +231 -0
- package/src/components/tooltip.stories.tsx +25 -0
- package/src/components/tooltip.tsx +43 -0
- package/src/components/top-nav.stories.tsx +20 -0
- package/src/components/top-nav.tsx +76 -0
- package/src/docs/Colors.stories.tsx +192 -0
- package/src/docs/GettingStarted.mdx +72 -0
- package/src/docs/Mcp.mdx +52 -0
- package/src/docs/Typography.stories.tsx +114 -0
- package/src/hooks/use-in-view.ts +32 -0
- package/src/hooks/use-reduced-motion.ts +26 -0
- package/src/index.ts +343 -0
- package/src/lib/cn.test.ts +67 -0
- package/src/lib/cn.ts +81 -0
- package/src/lib/motion.ts +109 -0
- package/src/lib/prefix-utilities.test.ts +56 -0
- package/src/lib/prefix-utilities.ts +50 -0
- package/src/styles/base.css +489 -0
- package/src/styles/motion.css +69 -0
- package/src/styles/overrides.css +44 -0
- package/src/styles/typography.css +173 -0
- package/src/theme/theme.css +391 -0
- package/src/theme/tokens.ts +29 -0
- package/src/theme/types.ts +26 -0
- package/README.md +0 -3
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
4
|
+
import { Reasoning, ReasoningContent, ReasoningTrigger } from "./reasoning"
|
|
5
|
+
|
|
6
|
+
const contentTag = (html: string) => html.match(/<div[^>]*data-slot="reasoning-content"[^>]*>/)?.[0] ?? ""
|
|
7
|
+
|
|
8
|
+
function render(props: React.ComponentProps<typeof Reasoning>) {
|
|
9
|
+
return renderToStaticMarkup(
|
|
10
|
+
<Reasoning {...props}>
|
|
11
|
+
<ReasoningTrigger>Reasoning</ReasoningTrigger>
|
|
12
|
+
<ReasoningContent>Compared 42 sales</ReasoningContent>
|
|
13
|
+
</Reasoning>,
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("Reasoning", () => {
|
|
18
|
+
it("starts closed, with the content inert and hidden from assistive tech", () => {
|
|
19
|
+
const html = render({})
|
|
20
|
+
expect(html).toContain('aria-expanded="false"')
|
|
21
|
+
expect(contentTag(html)).toContain('aria-hidden="true"')
|
|
22
|
+
expect(contentTag(html)).toContain("inert")
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("starts open while streaming and shimmers the label", () => {
|
|
26
|
+
const html = render({ streaming: true })
|
|
27
|
+
expect(html).toContain('aria-expanded="true"')
|
|
28
|
+
expect(contentTag(html)).not.toContain("aria-hidden")
|
|
29
|
+
expect(contentTag(html)).not.toContain("inert")
|
|
30
|
+
expect(html).toContain('data-slot="shimmer-text"')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("opens with defaultOpen and points the trigger at the content", () => {
|
|
34
|
+
const html = render({ defaultOpen: true })
|
|
35
|
+
const controls = html.match(/aria-controls="([^"]+)"/)?.[1]
|
|
36
|
+
expect(controls).toBeTruthy()
|
|
37
|
+
expect(html).toContain(`id="${controls}"`)
|
|
38
|
+
expect(html).toContain('data-state="open"')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("lets a controlled open win over streaming", () => {
|
|
42
|
+
expect(render({ open: false, streaming: true })).toContain('aria-expanded="false"')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("throws when a part is used outside Reasoning", () => {
|
|
46
|
+
expect(() => renderToStaticMarkup(<ReasoningTrigger>Orphan</ReasoningTrigger>)).toThrow(/Reasoning/)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ChevronDown } from "lucide-react"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
import { ShimmerText } from "./shimmer-text"
|
|
7
|
+
|
|
8
|
+
interface ReasoningContextValue {
|
|
9
|
+
open: boolean
|
|
10
|
+
setOpen: (open: boolean) => void
|
|
11
|
+
streaming: boolean
|
|
12
|
+
contentId: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ReasoningContext = React.createContext<ReasoningContextValue | null>(null)
|
|
16
|
+
|
|
17
|
+
function useReasoning(part: string) {
|
|
18
|
+
const context = React.useContext(ReasoningContext)
|
|
19
|
+
if (!context) throw new Error(`${part} must be used inside Reasoning`)
|
|
20
|
+
return context
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ReasoningProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
24
|
+
open?: boolean
|
|
25
|
+
defaultOpen?: boolean
|
|
26
|
+
onOpenChange?: (open: boolean) => void
|
|
27
|
+
/** True while the model is still reasoning: the panel opens, then closes when it turns false. */
|
|
28
|
+
streaming?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reasoning - the model's thinking behind an answer, in a panel that opens
|
|
33
|
+
* while it streams and folds away once the answer is ready.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* <Reasoning streaming={thinking}>
|
|
37
|
+
* <ReasoningTrigger>Show reasoning</ReasoningTrigger>
|
|
38
|
+
* <ReasoningContent>{reasoningText}</ReasoningContent>
|
|
39
|
+
* </Reasoning>
|
|
40
|
+
*/
|
|
41
|
+
export const Reasoning = React.forwardRef<HTMLDivElement, ReasoningProps>(
|
|
42
|
+
({ open: openProp, defaultOpen = false, onOpenChange, streaming = false, className, children, ...props }, ref) => {
|
|
43
|
+
const contentId = React.useId()
|
|
44
|
+
const [uncontrolled, setUncontrolled] = React.useState(defaultOpen || streaming)
|
|
45
|
+
const open = openProp ?? uncontrolled
|
|
46
|
+
|
|
47
|
+
const setOpen = React.useCallback(
|
|
48
|
+
(next: boolean) => {
|
|
49
|
+
if (openProp === undefined) setUncontrolled(next)
|
|
50
|
+
onOpenChange?.(next)
|
|
51
|
+
},
|
|
52
|
+
[openProp, onOpenChange],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// Follow streaming edges only, so a manual toggle mid-stream sticks until the next edge.
|
|
56
|
+
const wasStreaming = React.useRef(streaming)
|
|
57
|
+
React.useEffect(() => {
|
|
58
|
+
if (streaming === wasStreaming.current) return
|
|
59
|
+
wasStreaming.current = streaming
|
|
60
|
+
setOpen(streaming)
|
|
61
|
+
}, [streaming, setOpen])
|
|
62
|
+
|
|
63
|
+
const value = React.useMemo(() => ({ open, setOpen, streaming, contentId }), [open, setOpen, streaming, contentId])
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<ReasoningContext.Provider value={value}>
|
|
67
|
+
<div ref={ref} data-slot="reasoning" data-state={open ? "open" : "closed"} className={className} {...props}>
|
|
68
|
+
{children}
|
|
69
|
+
</div>
|
|
70
|
+
</ReasoningContext.Provider>
|
|
71
|
+
)
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
Reasoning.displayName = "Reasoning"
|
|
75
|
+
|
|
76
|
+
export interface ReasoningTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
|
|
77
|
+
|
|
78
|
+
/** Toggles the panel. The label shimmers while the model is still reasoning. */
|
|
79
|
+
export const ReasoningTrigger = React.forwardRef<HTMLButtonElement, ReasoningTriggerProps>(
|
|
80
|
+
({ className, children, onClick, ...props }, ref) => {
|
|
81
|
+
const { open, setOpen, streaming, contentId } = useReasoning("ReasoningTrigger")
|
|
82
|
+
return (
|
|
83
|
+
<button
|
|
84
|
+
ref={ref}
|
|
85
|
+
type="button"
|
|
86
|
+
aria-expanded={open}
|
|
87
|
+
aria-controls={contentId}
|
|
88
|
+
onClick={(event) => {
|
|
89
|
+
onClick?.(event)
|
|
90
|
+
if (!event.defaultPrevented) setOpen(!open)
|
|
91
|
+
}}
|
|
92
|
+
className={cn(
|
|
93
|
+
"ax:flex ax:items-center ax:gap-1 ax:rounded-xs ax:t-body-sm ax:text-muted-foreground ax:transition-colors",
|
|
94
|
+
"ax:hover:text-foreground ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring",
|
|
95
|
+
className,
|
|
96
|
+
)}
|
|
97
|
+
{...props}
|
|
98
|
+
>
|
|
99
|
+
{streaming ? <ShimmerText className="ax:font-medium">{children}</ShimmerText> : <span>{children}</span>}
|
|
100
|
+
<ChevronDown className={cn("ax:size-4 ax:transition-transform ax:motion-reduce:transition-none", open && "ax:rotate-180")} aria-hidden />
|
|
101
|
+
</button>
|
|
102
|
+
)
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
ReasoningTrigger.displayName = "ReasoningTrigger"
|
|
106
|
+
|
|
107
|
+
export interface ReasoningContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
108
|
+
/** Extra classes for the inner text block, inside the animated wrapper. */
|
|
109
|
+
contentClassName?: string
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The reasoning text beside a thin rule. Grows and shrinks with the panel;
|
|
114
|
+
* while closed it stays in the DOM but is inert and hidden from assistive tech.
|
|
115
|
+
*/
|
|
116
|
+
export const ReasoningContent = React.forwardRef<HTMLDivElement, ReasoningContentProps>(
|
|
117
|
+
({ className, contentClassName, children, ...props }, ref) => {
|
|
118
|
+
const { open, contentId } = useReasoning("ReasoningContent")
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
ref={ref}
|
|
122
|
+
id={contentId}
|
|
123
|
+
data-slot="reasoning-content"
|
|
124
|
+
data-state={open ? "open" : "closed"}
|
|
125
|
+
aria-hidden={!open || undefined}
|
|
126
|
+
inert={!open}
|
|
127
|
+
className={cn(
|
|
128
|
+
"ax:grid ax:transition-[grid-template-rows] ax:duration-150 ax:ease-out ax:motion-reduce:transition-none",
|
|
129
|
+
open ? "ax:grid-rows-[1fr]" : "ax:grid-rows-[0fr]",
|
|
130
|
+
className,
|
|
131
|
+
)}
|
|
132
|
+
{...props}
|
|
133
|
+
>
|
|
134
|
+
<div className="ax:min-h-0 ax:overflow-hidden">
|
|
135
|
+
<div className={cn("ax:mt-sm ax:border-s-2 ax:border-border ax:ps-md ax:t-body-sm ax:text-muted-foreground", contentClassName)}>
|
|
136
|
+
{children}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
ReasoningContent.displayName = "ReasoningContent"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Search } from "./search"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Search> = {
|
|
6
|
+
title: "Components/Search",
|
|
7
|
+
component: Search,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
args: {
|
|
10
|
+
placeholder: "Search students…",
|
|
11
|
+
className: "ax:w-65",
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default meta
|
|
16
|
+
type Story = StoryObj<typeof Search>
|
|
17
|
+
|
|
18
|
+
export const Default: Story = {}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Search as SearchIcon, X as ClearIcon } from "lucide-react"
|
|
5
|
+
import { Input, TextFieldRoot, TextFieldSlot, type InputSize } from "./input"
|
|
6
|
+
import { cn } from "../lib/cn"
|
|
7
|
+
|
|
8
|
+
export interface SearchProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "size"> {
|
|
9
|
+
/** Visual density - passes through to the underlying `TextFieldRoot` /
|
|
10
|
+
* `Input`. Defaults to `md`. */
|
|
11
|
+
size?: InputSize
|
|
12
|
+
/** Marks the input as invalid (red border + ring). */
|
|
13
|
+
invalid?: boolean
|
|
14
|
+
/** Optional className applied to the outer `TextFieldRoot`. Useful for
|
|
15
|
+
* setting width (e.g. `"ax:w-65"`) or alignment helpers (`"ax:ms-auto"`). */
|
|
16
|
+
className?: string
|
|
17
|
+
/**
|
|
18
|
+
* Show a trailing clear button when the input has a value. Defaults to
|
|
19
|
+
* `true` for controlled inputs (when `value` is set). Pass `false` to
|
|
20
|
+
* suppress entirely.
|
|
21
|
+
*
|
|
22
|
+
* When clicked, the input is cleared by calling either:
|
|
23
|
+
* • `onClear` (if provided), or
|
|
24
|
+
* • `onChange` with a synthetic event whose `target.value` is `""`.
|
|
25
|
+
* The latter keeps controlled callers working without an extra prop.
|
|
26
|
+
*/
|
|
27
|
+
clearable?: boolean
|
|
28
|
+
/** Called when the user clicks the clear button. Takes precedence over the
|
|
29
|
+
* default behaviour of dispatching an empty-string change event. */
|
|
30
|
+
onClear?: () => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Search - a styled search input with a leading magnifying-glass icon and an
|
|
35
|
+
* optional trailing clear button.
|
|
36
|
+
*
|
|
37
|
+
* A thin composition over `TextFieldRoot` + `TextFieldSlot` + `Input` that
|
|
38
|
+
* codifies the search-field pattern in one component, so every product gets
|
|
39
|
+
* the same look (icon, sizing, focus ring, clear affordance) without
|
|
40
|
+
* re-assembling the three primitives at every call site.
|
|
41
|
+
*
|
|
42
|
+
* Accepts all standard input props (`value`, `defaultValue`, `onChange`,
|
|
43
|
+
* `placeholder`, `aria-label`, `name`, `disabled`, etc.) via spread.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* <Search placeholder="Search students…" aria-label="Search" />
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* <Search
|
|
50
|
+
* value={query}
|
|
51
|
+
* onChange={(e) => setQuery(e.target.value)}
|
|
52
|
+
* onClear={() => setQuery("")}
|
|
53
|
+
* placeholder="Search…"
|
|
54
|
+
* className="ax:w-65"
|
|
55
|
+
* />
|
|
56
|
+
*/
|
|
57
|
+
export const Search = React.forwardRef<HTMLInputElement, SearchProps>(
|
|
58
|
+
(
|
|
59
|
+
{
|
|
60
|
+
size = "md",
|
|
61
|
+
invalid,
|
|
62
|
+
className,
|
|
63
|
+
clearable,
|
|
64
|
+
onClear,
|
|
65
|
+
value,
|
|
66
|
+
onChange,
|
|
67
|
+
placeholder = "Search…",
|
|
68
|
+
"aria-label": ariaLabel = "Search",
|
|
69
|
+
...inputProps
|
|
70
|
+
},
|
|
71
|
+
ref,
|
|
72
|
+
) => {
|
|
73
|
+
const innerRef = React.useRef<HTMLInputElement>(null)
|
|
74
|
+
React.useImperativeHandle(ref, () => innerRef.current as HTMLInputElement)
|
|
75
|
+
|
|
76
|
+
// Show the clear button when:
|
|
77
|
+
// • clearable is explicitly true, OR
|
|
78
|
+
// • clearable is undefined AND we're in controlled mode with a value.
|
|
79
|
+
const hasValue = typeof value === "string" && value.length > 0
|
|
80
|
+
const showClear = clearable === true || (clearable === undefined && hasValue)
|
|
81
|
+
|
|
82
|
+
function handleClear() {
|
|
83
|
+
if (onClear) {
|
|
84
|
+
onClear()
|
|
85
|
+
} else if (onChange && innerRef.current) {
|
|
86
|
+
// Synthesise a change event so controlled callers update their state
|
|
87
|
+
// without needing an extra `onClear` prop.
|
|
88
|
+
const input = innerRef.current
|
|
89
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
90
|
+
window.HTMLInputElement.prototype,
|
|
91
|
+
"value",
|
|
92
|
+
)?.set
|
|
93
|
+
setter?.call(input, "")
|
|
94
|
+
input.dispatchEvent(new Event("input", { bubbles: true }))
|
|
95
|
+
}
|
|
96
|
+
innerRef.current?.focus()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<TextFieldRoot size={size} invalid={invalid} className={cn(className)}>
|
|
101
|
+
{/* TextFieldRoot already applies the horizontal padding (px-sm/px-md);
|
|
102
|
+
the slot must not add its own or the icon gets doubled left inset. */}
|
|
103
|
+
<TextFieldSlot>
|
|
104
|
+
<SearchIcon className="ax:size-4" />
|
|
105
|
+
</TextFieldSlot>
|
|
106
|
+
<Input
|
|
107
|
+
ref={innerRef}
|
|
108
|
+
type="search"
|
|
109
|
+
// The trailing clear button replaces the browser's own, which would otherwise show alongside it.
|
|
110
|
+
className="ax:[&::-webkit-search-cancel-button]:appearance-none ax:[&::-webkit-search-decoration]:appearance-none"
|
|
111
|
+
value={value}
|
|
112
|
+
onChange={onChange}
|
|
113
|
+
placeholder={placeholder}
|
|
114
|
+
aria-label={ariaLabel}
|
|
115
|
+
{...inputProps}
|
|
116
|
+
/>
|
|
117
|
+
{showClear && (
|
|
118
|
+
<TextFieldSlot>
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
onClick={handleClear}
|
|
122
|
+
aria-label="Clear search"
|
|
123
|
+
className="ax:text-muted-foreground ax:hover:text-foreground ax:transition-colors ax:rounded-sm ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring"
|
|
124
|
+
>
|
|
125
|
+
<ClearIcon className="ax:size-4" />
|
|
126
|
+
</button>
|
|
127
|
+
</TextFieldSlot>
|
|
128
|
+
)}
|
|
129
|
+
</TextFieldRoot>
|
|
130
|
+
)
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
Search.displayName = "Search"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { SectionNav } from "./section-nav"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof SectionNav> = {
|
|
6
|
+
title: "Components/SectionNav",
|
|
7
|
+
component: SectionNav,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof SectionNav>
|
|
13
|
+
|
|
14
|
+
const items = [
|
|
15
|
+
{ id: "overview", label: "Overview" },
|
|
16
|
+
{ id: "eligibility", label: "Eligibility" },
|
|
17
|
+
{ id: "how-it-works", label: "How it works" },
|
|
18
|
+
{ id: "support", label: "Support" },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
export const Default: Story = {
|
|
22
|
+
render: () => (
|
|
23
|
+
<div className="ax:flex ax:gap-xl">
|
|
24
|
+
<div className="ax:w-56 ax:shrink-0">
|
|
25
|
+
<div className="ax:sticky ax:top-md">
|
|
26
|
+
<SectionNav items={items} />
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div className="ax:flex-1 ax:space-y-xl">
|
|
30
|
+
{items.map((item) => (
|
|
31
|
+
<section key={item.id} id={item.id} className="ax:min-h-[60vh]">
|
|
32
|
+
<h2 className="ax:t-h2">{item.label}</h2>
|
|
33
|
+
</section>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const InScrollContainer: Story = {
|
|
41
|
+
render: () => (
|
|
42
|
+
<div className="ax:flex ax:h-96 ax:gap-xl">
|
|
43
|
+
<div className="ax:w-56 ax:shrink-0">
|
|
44
|
+
<SectionNav items={items} scrollRoot="#section-nav-story-root" />
|
|
45
|
+
</div>
|
|
46
|
+
<div id="section-nav-story-root" className="ax:flex-1 ax:space-y-xl ax:overflow-y-auto">
|
|
47
|
+
{items.map((item) => (
|
|
48
|
+
<section key={item.id} id={item.id} className="ax:min-h-80">
|
|
49
|
+
<h2 className="ax:t-h2">{item.label}</h2>
|
|
50
|
+
</section>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const Responsive: Story = {
|
|
58
|
+
render: () => <SectionNav items={items} layout="responsive" />,
|
|
59
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
4
|
+
import { SectionNav } from "./section-nav"
|
|
5
|
+
|
|
6
|
+
const items = [
|
|
7
|
+
{ id: "one", label: "One" },
|
|
8
|
+
{ id: "two", label: "Two" },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
describe("SectionNav", () => {
|
|
12
|
+
it("renders a labelled nav with an anchor per section", () => {
|
|
13
|
+
const html = renderToStaticMarkup(<SectionNav items={items} />)
|
|
14
|
+
expect(html).toContain('aria-label="Page sections"')
|
|
15
|
+
expect(html).toContain('href="#one"')
|
|
16
|
+
expect(html).toContain('href="#two"')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("marks the first section as current before any scrolling", () => {
|
|
20
|
+
const html = renderToStaticMarkup(<SectionNav items={items} />)
|
|
21
|
+
expect(html.match(/aria-current="page"/g)).toHaveLength(1)
|
|
22
|
+
expect(html).toContain("ax:border-primary")
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("shows the default title and lets it be hidden", () => {
|
|
26
|
+
expect(renderToStaticMarkup(<SectionNav items={items} />)).toContain("Jump to")
|
|
27
|
+
expect(renderToStaticMarkup(<SectionNav items={items} title={null} />)).not.toContain("Jump to")
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("adds the horizontal-row classes only for the responsive layout", () => {
|
|
31
|
+
const vertical = renderToStaticMarkup(<SectionNav items={items} />)
|
|
32
|
+
const responsive = renderToStaticMarkup(<SectionNav items={items} layout="responsive" />)
|
|
33
|
+
expect(vertical).not.toContain("ax:overflow-x-auto")
|
|
34
|
+
expect(responsive).toContain("ax:overflow-x-auto")
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../lib/cn"
|
|
5
|
+
import { ListNav, ListNavItem } from "./list-nav"
|
|
6
|
+
|
|
7
|
+
export interface SectionNavItem {
|
|
8
|
+
/** The `id` of the section element this item jumps to. */
|
|
9
|
+
id: string
|
|
10
|
+
label: React.ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SectionNavProps extends Omit<React.HTMLAttributes<HTMLElement>, "title" | "onSelect"> {
|
|
14
|
+
/** Sections to list, in page order. Each `id` must match an element on the page. */
|
|
15
|
+
items: SectionNavItem[]
|
|
16
|
+
/** Eyebrow title above the list. Pass `null` to hide it. */
|
|
17
|
+
title?: React.ReactNode
|
|
18
|
+
/**
|
|
19
|
+
* The scrolling container the sections live in: a CSS selector, an element,
|
|
20
|
+
* or `null` (default) for the window.
|
|
21
|
+
*/
|
|
22
|
+
scrollRoot?: string | HTMLElement | null
|
|
23
|
+
/**
|
|
24
|
+
* How far down the scroll root (0-1) a section's top must pass to count as
|
|
25
|
+
* the current one. Default `0.24`.
|
|
26
|
+
*/
|
|
27
|
+
activationRatio?: number
|
|
28
|
+
/**
|
|
29
|
+
* `vertical` (default) is a stacked list at every width. `responsive` runs as
|
|
30
|
+
* a horizontally scrolling row below `xl` and a stacked list from `xl` up,
|
|
31
|
+
* hiding the title in the row.
|
|
32
|
+
*/
|
|
33
|
+
layout?: "vertical" | "responsive"
|
|
34
|
+
/** Fires when the current section changes, from scrolling or from a click. */
|
|
35
|
+
onActiveChange?: (id: string) => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveScrollRoot(scrollRoot: SectionNavProps["scrollRoot"]): HTMLElement | null {
|
|
39
|
+
if (typeof scrollRoot === "string") return document.querySelector<HTMLElement>(scrollRoot)
|
|
40
|
+
return scrollRoot ?? null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* SectionNav - an in-page "Jump to" menu with scroll-spy. Lists the page's
|
|
45
|
+
* sections as anchor links, marks the one currently in view with a primary
|
|
46
|
+
* left border and text, and follows the page as it scrolls.
|
|
47
|
+
*
|
|
48
|
+
* Built on `ListNav`: each item is an `<a href="#id">`, so it still works as a
|
|
49
|
+
* plain anchor without JavaScript. Use `scrollRoot` when the page scrolls
|
|
50
|
+
* inside a container rather than the window.
|
|
51
|
+
*/
|
|
52
|
+
export const SectionNav = React.forwardRef<HTMLElement, SectionNavProps>(
|
|
53
|
+
(
|
|
54
|
+
{
|
|
55
|
+
items,
|
|
56
|
+
title = "Jump to",
|
|
57
|
+
scrollRoot = null,
|
|
58
|
+
activationRatio = 0.24,
|
|
59
|
+
layout = "vertical",
|
|
60
|
+
onActiveChange,
|
|
61
|
+
className,
|
|
62
|
+
"aria-label": ariaLabel = "Page sections",
|
|
63
|
+
...props
|
|
64
|
+
},
|
|
65
|
+
ref,
|
|
66
|
+
) => {
|
|
67
|
+
const [activeId, setActiveId] = React.useState(items[0]?.id ?? "")
|
|
68
|
+
const onActiveChangeRef = React.useRef(onActiveChange)
|
|
69
|
+
onActiveChangeRef.current = onActiveChange
|
|
70
|
+
|
|
71
|
+
const select = React.useCallback((id: string) => {
|
|
72
|
+
setActiveId((current) => {
|
|
73
|
+
if (current === id) return current
|
|
74
|
+
onActiveChangeRef.current?.(id)
|
|
75
|
+
return id
|
|
76
|
+
})
|
|
77
|
+
}, [])
|
|
78
|
+
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
const root = resolveScrollRoot(scrollRoot)
|
|
81
|
+
if (typeof scrollRoot === "string" && !root) return
|
|
82
|
+
|
|
83
|
+
const scrollTarget: HTMLElement | Window = root ?? window
|
|
84
|
+
|
|
85
|
+
const update = () => {
|
|
86
|
+
const rootTop = root ? root.getBoundingClientRect().top : 0
|
|
87
|
+
const rootHeight = root ? root.clientHeight : window.innerHeight
|
|
88
|
+
const activationLine = rootTop + rootHeight * activationRatio
|
|
89
|
+
let nextId = items[0]?.id ?? ""
|
|
90
|
+
let nearestPassedTop = Number.NEGATIVE_INFINITY
|
|
91
|
+
|
|
92
|
+
for (const item of items) {
|
|
93
|
+
const section = document.getElementById(item.id)
|
|
94
|
+
if (!section) continue
|
|
95
|
+
const sectionTop = section.getBoundingClientRect().top
|
|
96
|
+
if (sectionTop <= activationLine && sectionTop > nearestPassedTop) {
|
|
97
|
+
nearestPassedTop = sectionTop
|
|
98
|
+
nextId = item.id
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
select(nextId)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
scrollTarget.addEventListener("scroll", update, { passive: true })
|
|
105
|
+
update()
|
|
106
|
+
return () => scrollTarget.removeEventListener("scroll", update)
|
|
107
|
+
}, [items, scrollRoot, activationRatio, select])
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<ListNav
|
|
111
|
+
ref={ref}
|
|
112
|
+
title={title ?? undefined}
|
|
113
|
+
aria-label={ariaLabel}
|
|
114
|
+
className={cn(
|
|
115
|
+
layout === "responsive" && [
|
|
116
|
+
"ax:overflow-x-auto ax:pb-sm ax:[&>div]:flex-row ax:[&>div]:gap-2xs ax:[&>p]:hidden",
|
|
117
|
+
"ax:xl:overflow-visible ax:xl:pb-0 ax:xl:[&>div]:flex-col ax:xl:[&>div]:gap-0 ax:xl:[&>p]:block ax:xl:[&>p]:pt-0",
|
|
118
|
+
],
|
|
119
|
+
className,
|
|
120
|
+
)}
|
|
121
|
+
{...props}
|
|
122
|
+
>
|
|
123
|
+
{items.map((item) => {
|
|
124
|
+
const active = item.id === activeId
|
|
125
|
+
return (
|
|
126
|
+
<ListNavItem
|
|
127
|
+
key={item.id}
|
|
128
|
+
asChild
|
|
129
|
+
active={active}
|
|
130
|
+
className={cn(
|
|
131
|
+
"ax:rounded-none ax:border-s-2 ax:py-2xs ax:pl-md ax:ml-sm",
|
|
132
|
+
layout === "responsive" && "ax:shrink-0 ax:xl:whitespace-normal",
|
|
133
|
+
active ? "ax:border-primary-foreground ax:bg-transparent" : "ax:border-border/50",
|
|
134
|
+
)}
|
|
135
|
+
>
|
|
136
|
+
<a href={`#${item.id}`} onClick={() => select(item.id)} className="ax:min-w-0">
|
|
137
|
+
<span className="ax:flex-1 ax:truncate ax:leading-snug">{item.label}</span>
|
|
138
|
+
</a>
|
|
139
|
+
</ListNavItem>
|
|
140
|
+
)
|
|
141
|
+
})}
|
|
142
|
+
</ListNav>
|
|
143
|
+
)
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
SectionNav.displayName = "SectionNav"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { SegmentedControl, SegmentedControlItem } from "./segmented-control"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof SegmentedControl> = {
|
|
6
|
+
title: "Components/SegmentedControl",
|
|
7
|
+
component: SegmentedControl,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof SegmentedControl>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<SegmentedControl defaultValue="day">
|
|
17
|
+
<SegmentedControlItem value="day">Day</SegmentedControlItem>
|
|
18
|
+
<SegmentedControlItem value="week">Week</SegmentedControlItem>
|
|
19
|
+
<SegmentedControlItem value="month">Month</SegmentedControlItem>
|
|
20
|
+
</SegmentedControl>
|
|
21
|
+
),
|
|
22
|
+
}
|