Skip to content

Commit f9773f6

Browse files
committed
updates to the components library
1 parent a56e056 commit f9773f6

4 files changed

Lines changed: 184 additions & 155 deletions

File tree

lang/libs/components/src/Alert.ch

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,18 @@ public #universal AlertWarning(props) {
171171
public #universal AlertInfo(props) {
172172
return <Alert {...props} variant="info">{props.children}</Alert>
173173
}
174+
175+
// Shadcn AlertAction: positioned absolutely in the top-right corner of the alert.
176+
func alert_action_styles(page : &mut HtmlPage) : *char {
177+
return #css {
178+
position: absolute;
179+
top: 0.75rem;
180+
right: 0.75rem;
181+
}
182+
}
183+
184+
public #universal AlertAction(props) {
185+
var classes = props.class || ""
186+
if(props.className) { classes = props.className }
187+
return <div class={classes + " " + alert_action_styles(page)}>{props.children}</div>
188+
}

lang/libs/components/src/Button.ch

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,28 @@ func button_styles(page : &mut HtmlPage) : *char {
107107
font-size: 0.9375rem;
108108
border-radius: calc(var(--radius) - 2px);
109109
}
110+
&[data-size="xs"] {
111+
height: 2rem;
112+
padding: 0 0.5rem;
113+
font-size: 0.6875rem;
114+
border-radius: calc(var(--radius) - 4px);
115+
}
110116
&[data-size="icon"] {
111117
width: 2.5rem;
112118
padding: 0;
113119
}
120+
&[data-size="icon-xs"] {
121+
width: 2rem;
122+
padding: 0;
123+
}
124+
&[data-size="icon-sm"] {
125+
width: 2.25rem;
126+
padding: 0;
127+
}
128+
&[data-size="icon-lg"] {
129+
width: 2.75rem;
130+
padding: 0;
131+
}
114132
}
115133
}
116134

lang/libs/components/src/Data.ch

Lines changed: 134 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -61,100 +61,87 @@ func progress_styles(page : &mut HtmlPage) : *char {
6161

6262
func 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+
158145
func 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.
315306
public #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+
}
323374
public #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

Comments
 (0)