@@ -61,100 +61,87 @@ func progress_styles(page : &mut HtmlPage) : *char {
6161
6262func accordion_styles (page : &mut HtmlPage) : *char {
6363 return #css {
64- border: 1px solid hsl (var (--border));
65- border-radius: 14px;
66- background: hsl (var (--background));
67- overflow: hidden;
64+ width: 100 %;
6865 }
6966}
7067
71- func accordion_summary_styles (page : &mut HtmlPage) : *char {
68+ func accordion_item_styles (page : &mut HtmlPage) : *char {
7269 return #css {
73- cursor: pointer;
74- list-style: none;
75- padding: 1rem 1 .1rem;
76- font-weight: 650 ;
77- color: hsl (var (--foreground));
78- background: hsl (var (--background));
7970 border-bottom: 1px solid hsl (var (--border));
8071 }
8172}
8273
83- func accordion_panel_styles (page : &mut HtmlPage) : *char {
74+ func accordion_trigger_styles (page : &mut HtmlPage) : *char {
8475 return #css {
85- padding: 1rem 1 .1rem;
76+ display: flex;
77+ align-items: center;
78+ justify-content: space-between;
79+ width: 100 %;
80+ padding: 1rem 0 ;
81+ font-size: 0 .9375rem;
82+ font-weight: 600 ;
83+ text-align: left;
84+ color: hsl (var (--foreground));
85+ background: transparent;
86+ border: 0 ;
87+ cursor: pointer;
88+ line-height: 1.5 ;
89+ transition: color 0 .15s ease;
90+ &:hover {
91+ text-decoration: underline;
92+ }
93+ &:focus-visible {
94+ outline: 2px solid hsl (var (--ring));
95+ outline-offset: 2px;
96+ border-radius: var (--radius);
97+ }
98+ &[disabled] {
99+ cursor: not-allowed;
100+ opacity: 0.5 ;
101+ pointer-events: none;
102+ }
103+ }
104+ }
105+
106+ func accordion_icon_styles (page : &mut HtmlPage) : *char {
107+ return #css {
108+ width: 1rem;
109+ height: 1rem;
110+ flex-shrink: 0 ;
86111 color: hsl (var (--muted-foreground));
87- background: hsl (var (--background));
112+ transition: transform 0 .2s ease;
113+ transform: rotate (0deg);
88114 }
89115}
90116
91- func accordion_item_styles (page : &mut HtmlPage) : *char {
117+ func accordion_content_styles (page : &mut HtmlPage) : *char {
92118 return #css {
93- border: 1px solid hsl (var (--border));
94- border-radius: 18px;
95- background: hsl (var (--background));
96119 overflow: hidden;
97- box-shadow: var (--shadow-sm);
98- .chx-accordion-summary {
99- display: flex;
100- align-items: center;
101- justify-content: space-between;
102- gap: 1rem;
103- cursor: pointer;
104- list-style: none;
105- width: 100 %;
106- padding: 1rem 1 .1rem;
107- font-weight: 650 ;
108- color: hsl (var (--foreground));
109- background: linear-gradient (180deg, rgba (255 , 255 , 255 , 0.02 ), transparent 80 %), hsl (var (--background));
110- border: 0 ;
111- }
112- .chx-accordion-summary:: -webkit-details-marker {
113- display: none;
114- }
115- .chx-accordion-copy {
116- display: grid;
117- gap: 0 .2rem;
118- }
119- .chx-accordion-title {
120- font-size: 1rem;
121- color: hsl (var (--foreground));
122- }
123- .chx-accordion-subtitle {
124- font-size: 0 .84rem;
125- font-weight: 500 ;
126- color: hsl (var (--muted-foreground));
127- }
128- .chx-accordion-icon {
129- width: 2rem;
130- height: 2rem;
131- border-radius: 999px;
132- display: inline-flex;
133- align-items: center;
134- justify-content: center;
135- background: hsl (var (--muted));
136- border: 1px solid hsl (var (--border));
137- font-size: 1rem;
138- line-height: 1 ;
139- font-family: ui-monospace, "SFMono-Regular" , monospace;
140- flex-shrink: 0 ;
141- transition: transform 0 .18s ease, background 0 .18s ease, color 0 .18s ease, border-color 0 .18s ease;
142- }
143- .chx-accordion-panel {
144- padding: 1rem 1 .1rem;
145- color: hsl (var (--muted-foreground));
146- background: hsl (var (--background));
147- border-top: 1px solid hsl (var (--border));
120+ font-size: 0 .875rem;
121+ color: hsl (var (--muted-foreground));
122+ [data-state="open" ] & {
123+ animation: accordion-down 0 .2s ease-out;
148124 }
149- &[open] .chx-accordion-icon {
150- transform: rotate (45deg);
151- background: hsl (var (--primary));
152- color: hsl (var (--primary-foreground));
153- border-color: transparent;
125+ [data-state="closed" ] & {
126+ animation: accordion-up 0 .2s ease-out;
154127 }
155128 }
156129}
157130
131+ func accordion_content_inner_styles (page : &mut HtmlPage) : *char {
132+ return #css {
133+ padding: 0 0 1rem 0 ;
134+ }
135+ }
136+
137+ func accordion_summary_styles (page : &mut HtmlPage) : *char {
138+ return accordion_trigger_styles (page)
139+ }
140+
141+ func accordion_panel_styles (page : &mut HtmlPage) : *char {
142+ return accordion_content_styles (page)
143+ }
144+
158145func tabs_styles (page : &mut HtmlPage) : *char {
159146 return #css {
160147 display: grid;
@@ -312,86 +299,97 @@ public #universal Progress(props) {
312299 >< /progress>
313300}
314301
302+
303+ // Shadcn-style Accordion: manages open state for child AccordionItems.
304+ // `defaultValue` is an array of item values open by default.
305+ // `multiple` allows multiple items open at the same time.
315306public #universal Accordion (props) {
316- return < div {...props} class={$ {accordion_styles (page)}} data-accordion-root="true" > {props.children}< /div>
307+ var multiple = props.multiple || false
308+ var defaultValues = props.defaultValue || []
309+ state openItems = defaultValues
310+ var toggleItem = (value) => {
311+ var idx = openItems.indexOf (value)
312+ if (idx >= 0 ) {
313+ openItems.splice (idx, 1 )
314+ } else {
315+ if (! multiple) { openItems = [] }
316+ openItems.push (value)
317+ }
318+ if (props.onValueChange) { props.onValueChange (openItems) }
319+ }
320+ var isItemOpen = (value) => { return openItems.indexOf (value) >= 0 }
321+ // Keyboard navigation: ArrowDown/Up/Home/End cycle through triggers
322+ var handleKeyDown = (e) => {
323+ if (e.key != "ArrowDown" && e.key != "ArrowUp" && e.key != "Home" && e.key != "End" ) { return }
324+ const root = e.currentTarget.closest ("[data-accordion-root]" )
325+ const triggers = root ? root.querySelectorAll ("button.chx-accordion-trigger:not([disabled])" ) : []
326+ if (triggers.length == 0 ) { return }
327+ var idx = -1
328+ for (var t = 0 ; t < triggers.length; t++) {
329+ if (triggers[t] == e.currentTarget) { idx = t; break }
330+ }
331+ if (idx < 0 ) { return }
332+ var next = idx
333+ if (e.key == "ArrowDown" ) { e.preventDefault (); next = (idx + 1 ) % triggers.length }
334+ else if (e.key == "ArrowUp" ) { e.preventDefault (); next = (idx - 1 + triggers.length) % triggers.length }
335+ else if (e.key == "Home" ) { e.preventDefault (); next = 0 }
336+ else if (e.key == "End" ) { e.preventDefault (); next = triggers.length - 1 }
337+ triggers[next].focus ()
338+ }
339+ return < div {...props} class={$ {accordion_styles (page)}} data-accordion-root="true" data-state="open" data-multiple={multiple ? "true" : "false" } onKeyDown={handleKeyDown}>
340+ {props.children}
341+ < /div>
317342}
318343
319- public #universal AccordionSummary (props) {
320- return < summary {...props} class={$ {accordion_summary_styles (page)}}> {props.children}< /summary>
344+ // Shadcn-style AccordionItem: wraps a trigger + content pair.
345+ public #universal AccordionItem (props) {
346+ var itemValue = props.value || ""
347+ var disabled = props.disabled || false
348+ return < div {...props} class={$ {accordion_item_styles (page)}} data-state="open" data-disabled={disabled ? "true" : "false" } data-value={itemValue}>
349+ {props.children}
350+ < /div>
351+ }
352+
353+ // Shadcn-style AccordionTrigger: the clickable header.
354+ public #universal AccordionTrigger (props) {
355+ return < button type="button" class={"chx-accordion-trigger " + $ {accordion_trigger_styles (page)}} data-state="open" aria-expanded="true" >
356+ {props.children}
357+ < span class={$ {accordion_icon_styles (page)}}>
358+ < svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" >< path d="m6 9 6 6 6-6" />< /svg>
359+ < /span>
360+ < /button>
361+ }
362+
363+ // Shadcn-style AccordionContent: the collapsible content panel.
364+ public #universal AccordionContent (props) {
365+ return < div class={$ {accordion_content_styles (page)}} data-state="open" role="region" >
366+ < div class={$ {accordion_content_inner_styles (page)}}> {props.children}< /div>
367+ < /div>
321368}
322369
370+ // Legacy aliases
371+ public #universal AccordionSummary (props) {
372+ return < AccordionTrigger {...props}> {props.children}< /AccordionTrigger>
373+ }
323374public #universal AccordionPanel (props) {
324- return < div {...props} class={ $ { accordion_panel_styles (page)}} > {props.children}< /div >
375+ return < AccordionContent {...props}> {props.children}< /AccordionContent >
325376}
326377
327- // Stateful accordion item: owns its open state, swaps the chevron glyph, and
328- // shows/hides the panel without relying on native <details> behavior.
329- // `defaultOpen` controls the initial (and SSR) state; `chevronOpen`/
330- // `chevronClosed` override the glyphs; `disabled` locks the item; `onToggle`
331- // fires with the next state on every click.
332- public #universal AccordionItem (props) {
378+ // Legacy: old-style accordion item with title/subtitle props.
379+ public #universal AccordionItemLegacy (props) {
333380 state open = props.defaultOpen ? true : false
334381 var disabled = props.disabled || false
335- var chevronOpen = props.chevronOpen || "−"
336- var chevronClosed = props.chevronClosed || "+"
337382 var toggle = () => {
338- if (disabled) {
339- return
340- }
383+ if (disabled) { return }
341384 open = ! open
342- if (props.onToggle) {
343- props.onToggle (open)
344- }
345- }
346- // Keyboard navigation between sibling items (shadcn accordion pattern):
347- // ArrowDown/ArrowUp move focus to the next/previous item trigger, Home/End
348- // jump to the first/last. Works with the children-based API via DOM
349- // traversal — no parent state required.
350- var handleKeyDown = (e) => {
351- if (disabled) {
352- return
353- }
354- if (e.key != "ArrowDown" && e.key != "ArrowUp" && e.key != "Home" && e.key != "End" ) {
355- return
356- }
357- const root = e.currentTarget.closest ("[data-accordion-root]" )
358- const triggers = root ? root.querySelectorAll (".chx-accordion-summary:not([disabled])" ) : []
359- if (triggers.length == 0 ) {
360- return
361- }
362- var idx = -1
363- for (var t = 0 ; t < triggers.length; t++) {
364- if (triggers[t] == e.currentTarget) {
365- idx = t
366- break
367- }
368- }
369- if (idx < 0 ) {
370- return
371- }
372- var next = idx
373- if (e.key == "ArrowDown" ) {
374- e.preventDefault ()
375- next = (idx + 1 ) % triggers.length
376- } else if (e.key == "ArrowUp" ) {
377- e.preventDefault ()
378- next = (idx - 1 + triggers.length) % triggers.length
379- } else if (e.key == "Home" ) {
380- e.preventDefault ()
381- next = 0
382- } else if (e.key == "End" ) {
383- e.preventDefault ()
384- next = triggers.length - 1
385- }
386- triggers[next].focus ()
387385 }
388386 return < div {...props} class={$ {accordion_item_styles (page)}} data-disabled={disabled ? "true" : "false" }>
389- <button type="button" class="chx-accordion-summary" onClick={toggle} onKeyDown={handleKeyDown} disabled={disabled} aria-expanded={open ? "true" : "false"}>
387+ <button type="button" class="chx-accordion-summary" onClick={toggle} disabled={disabled} aria-expanded={open ? "true" : "false"}>
390388 <span class="chx-accordion-copy">
391389 <span class="chx-accordion-title">{props.title}</span>
392390 <span class="chx-accordion-subtitle">{props.subtitle}</span>
393391 </span>
394- <span class="chx-accordion-icon">{open ? chevronOpen : chevronClosed }</span>
392+ <span class="chx-accordion-icon">{open ? "+" : "+" }</span>
395393 </button>
396394 <div class="chx-accordion-panel" style={open ? "" : "display:none;"}>{props.children}</div>
397395 </div>
0 commit comments