tembro 3.1.15 → 3.1.17
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/CHANGELOG.md +18 -0
- package/dist/components/calendar/date-picker.d.ts +2 -1
- package/dist/components/display/statistic.d.ts +4 -2
- package/dist/components/modern/calendar-scheduler.d.ts +24 -1
- package/dist/components/modern/resizable-panel.d.ts +1 -1
- package/dist/components/patterns/form-builder.d.ts +9 -4
- package/dist/components/ui/button/index.d.ts +2 -1
- package/dist/showcase/package-meta.d.ts +2 -2
- package/dist/src/components/calendar/date-picker.cjs +1 -1
- package/dist/src/components/calendar/date-picker.js +24 -22
- package/dist/src/components/display/statistic.cjs +1 -1
- package/dist/src/components/display/statistic.js +27 -23
- package/dist/src/components/display/status-legend.cjs +1 -1
- package/dist/src/components/display/status-legend.js +4 -4
- package/dist/src/components/modern/calendar-scheduler.cjs +1 -1
- package/dist/src/components/modern/calendar-scheduler.js +125 -59
- package/dist/src/components/modern/resizable-panel.cjs +1 -1
- package/dist/src/components/modern/resizable-panel.js +4 -4
- package/dist/src/components/patterns/detail-layout.cjs +1 -1
- package/dist/src/components/patterns/detail-layout.js +1 -1
- package/dist/src/components/patterns/form-builder.cjs +1 -1
- package/dist/src/components/patterns/form-builder.js +6 -6
- package/dist/src/components/patterns/resource-detail-page.cjs +1 -1
- package/dist/src/components/patterns/resource-detail-page.js +1 -1
- package/dist/src/components/patterns/resource-page.cjs +1 -1
- package/dist/src/components/patterns/resource-page.js +1 -1
- package/dist/src/components/ui/button/index.cjs +1 -1
- package/dist/src/components/ui/button/index.js +10 -9
- package/dist/src/components/ui/card/index.cjs +1 -1
- package/dist/src/components/ui/card/index.js +1 -0
- package/dist/src/showcase/package-meta.cjs +1 -1
- package/dist/src/showcase/package-meta.js +1 -1
- package/dist/src/showcase/premium/calendar-scheduler/mock.cjs +8 -4
- package/dist/src/showcase/premium/calendar-scheduler/mock.js +4 -4
- package/dist/src/showcase/premium/calendar-scheduler/showcase.cjs +1 -1
- package/dist/src/showcase/premium/calendar-scheduler/showcase.js +29 -5
- package/dist/src/showcase/registry-specific.cjs +1 -1
- package/dist/src/showcase/registry-specific.js +281 -233
- package/dist/src/showcase/site-data.cjs +1 -1
- package/dist/src/showcase/site-data.js +1 -1
- package/dist/src/showcase/tembro-registry.json.cjs +1 -1
- package/dist/src/showcase/tembro-registry.json.js +1 -1
- package/package.json +1 -1
- package/packages/cli/dist/index.cjs +1 -1
- package/packages/cli/vendor/src/components/calendar/date-picker.tsx +11 -7
- package/packages/cli/vendor/src/components/display/statistic.tsx +20 -15
- package/packages/cli/vendor/src/components/display/status-legend.tsx +4 -4
- package/packages/cli/vendor/src/components/modern/calendar-scheduler.tsx +108 -48
- package/packages/cli/vendor/src/components/modern/resizable-panel.tsx +1 -1
- package/packages/cli/vendor/src/components/patterns/detail-layout.tsx +1 -1
- package/packages/cli/vendor/src/components/patterns/form-builder.tsx +30 -30
- package/packages/cli/vendor/src/components/patterns/resource-detail-page.tsx +1 -1
- package/packages/cli/vendor/src/components/patterns/resource-page.tsx +1 -1
- package/packages/cli/vendor/src/components/ui/button/index.tsx +3 -1
- package/packages/cli/vendor/src/components/ui/card/index.tsx +1 -0
- package/packages/cli/vendor/src/showcase/package-meta.ts +1 -1
- package/packages/cli/vendor/src/showcase/premium/calendar-scheduler/mock.ts +10 -7
- package/packages/cli/vendor/src/showcase/premium/calendar-scheduler/showcase.tsx +7 -5
- package/packages/cli/vendor/src/showcase/registry-specific.tsx +11 -1
- package/packages/cli/vendor/src/showcase/tembro-registry.json +1 -1
- package/packages/cli/vendor/templates/showcase/src/showcase/layout/HeroSection.tsx +2 -2
- package/packages/cli/vendor/templates/showcase/src/showcase/layout/WorkbenchSidebar.tsx +1 -1
- package/packages/cli/vendor/templates/styles/globals.css +60 -40
- package/registry.json +1 -1
|
@@ -64,7 +64,7 @@ function StatusLegend({
|
|
|
64
64
|
<Card
|
|
65
65
|
data-slot="status-legend"
|
|
66
66
|
className={cn(
|
|
67
|
-
"min-w-0 border-[color:var(--aui-
|
|
67
|
+
"min-w-0 border-[color:var(--aui-card-border,var(--border))] bg-card shadow-[var(--aui-card-shadow,var(--aui-shadow-xs))]",
|
|
68
68
|
className
|
|
69
69
|
)}
|
|
70
70
|
{...props}
|
|
@@ -95,7 +95,7 @@ function StatusLegend({
|
|
|
95
95
|
data-slot="status-legend-item"
|
|
96
96
|
data-tone={item.tone ?? "default"}
|
|
97
97
|
className={cn(
|
|
98
|
-
"flex min-w-0 items-start justify-between gap-3 rounded-
|
|
98
|
+
"flex min-w-0 items-start justify-between gap-3 rounded-lg border border-[color:var(--aui-card-border,var(--border))] bg-[color:var(--aui-control-surface,var(--background))] p-3 transition hover:border-[color:var(--aui-control-hover-border,var(--ring))]",
|
|
99
99
|
compact && "p-2.5",
|
|
100
100
|
itemClassName,
|
|
101
101
|
item.className
|
|
@@ -105,7 +105,7 @@ function StatusLegend({
|
|
|
105
105
|
{item.icon ? (
|
|
106
106
|
<span className="mt-0.5 inline-flex size-8 shrink-0 items-center justify-center rounded-xl bg-[color:var(--aui-page-bg)] text-muted-foreground [&_svg]:size-4">{item.icon}</span>
|
|
107
107
|
) : (
|
|
108
|
-
<span className={cn("mt-1.5 size-2.5 shrink-0 rounded-full
|
|
108
|
+
<span className={cn("mt-1.5 size-2.5 shrink-0 rounded-full ring-4 ring-background", dotClassName[item.tone ?? "default"])} />
|
|
109
109
|
)}
|
|
110
110
|
<div className="min-w-0 space-y-0.5">
|
|
111
111
|
<div className="truncate text-sm font-medium text-foreground">{item.label}</div>
|
|
@@ -113,7 +113,7 @@ function StatusLegend({
|
|
|
113
113
|
</div>
|
|
114
114
|
</div>
|
|
115
115
|
{showCounts && item.count !== undefined && (
|
|
116
|
-
<Badge variant="secondary" className="shrink-0 rounded-full border border-[color:var(--aui-
|
|
116
|
+
<Badge variant="secondary" className="shrink-0 rounded-full border border-[color:var(--aui-card-border,var(--border))] bg-background px-2.5 py-1 shadow-sm">
|
|
117
117
|
{item.count}
|
|
118
118
|
</Badge>
|
|
119
119
|
)}
|
|
@@ -9,15 +9,36 @@ export type CalendarSchedulerEvent = {
|
|
|
9
9
|
time?: string
|
|
10
10
|
tone?: "default" | "success" | "warning" | "danger"
|
|
11
11
|
durationMinutes?: number
|
|
12
|
+
description?: React.ReactNode
|
|
13
|
+
meta?: React.ReactNode
|
|
14
|
+
badge?: React.ReactNode
|
|
15
|
+
icon?: React.ReactNode
|
|
16
|
+
disabled?: boolean
|
|
17
|
+
hidden?: boolean
|
|
18
|
+
className?: string
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
export type CalendarSchedulerProps = React.ComponentProps<"div"> & {
|
|
15
22
|
events: CalendarSchedulerEvent[]
|
|
16
23
|
days?: string[]
|
|
17
24
|
view?: "day" | "week" | "month"
|
|
25
|
+
variant?: "board" | "agenda"
|
|
26
|
+
density?: "compact" | "default" | "comfortable"
|
|
27
|
+
title?: React.ReactNode
|
|
28
|
+
description?: React.ReactNode
|
|
29
|
+
actions?: React.ReactNode
|
|
18
30
|
empty?: React.ReactNode
|
|
31
|
+
selectedEventId?: string
|
|
32
|
+
defaultSelectedEventId?: string
|
|
33
|
+
onSelectedEventChange?: (eventId: string, event: CalendarSchedulerEvent) => void
|
|
19
34
|
onCreateEvent?: (date: string) => void
|
|
20
35
|
onEventClick?: (event: CalendarSchedulerEvent) => void
|
|
36
|
+
renderEvent?: (event: CalendarSchedulerEvent, state: { selected: boolean }) => React.ReactNode
|
|
37
|
+
createLabel?: React.ReactNode
|
|
38
|
+
showCount?: boolean
|
|
39
|
+
dayClassName?: string
|
|
40
|
+
eventClassName?: string
|
|
41
|
+
formatDay?: (day: string) => React.ReactNode
|
|
21
42
|
}
|
|
22
43
|
|
|
23
44
|
const toneClassName = {
|
|
@@ -31,39 +52,52 @@ function CalendarScheduler({
|
|
|
31
52
|
events,
|
|
32
53
|
days,
|
|
33
54
|
view = "week",
|
|
55
|
+
variant = "board",
|
|
56
|
+
density = "default",
|
|
57
|
+
title,
|
|
58
|
+
description,
|
|
59
|
+
actions,
|
|
34
60
|
empty = "No events scheduled.",
|
|
61
|
+
selectedEventId,
|
|
62
|
+
defaultSelectedEventId,
|
|
63
|
+
onSelectedEventChange,
|
|
35
64
|
onCreateEvent,
|
|
36
65
|
onEventClick,
|
|
66
|
+
renderEvent,
|
|
67
|
+
createLabel = "Create event",
|
|
68
|
+
showCount = true,
|
|
69
|
+
dayClassName,
|
|
70
|
+
eventClassName,
|
|
71
|
+
formatDay,
|
|
37
72
|
className,
|
|
38
73
|
...props
|
|
39
74
|
}: CalendarSchedulerProps) {
|
|
40
|
-
const
|
|
75
|
+
const [internalSelectedEventId, setInternalSelectedEventId] = React.useState(defaultSelectedEventId)
|
|
76
|
+
const currentSelectedEventId = selectedEventId ?? internalSelectedEventId
|
|
77
|
+
const visibleEvents = events.filter((event) => !event.hidden)
|
|
78
|
+
const visibleDays = days ?? Array.from(new Set(visibleEvents.map((event) => event.date)))
|
|
79
|
+
|
|
80
|
+
const selectEvent = (event: CalendarSchedulerEvent) => {
|
|
81
|
+
if (event.disabled) return
|
|
82
|
+
if (selectedEventId === undefined) setInternalSelectedEventId(event.id)
|
|
83
|
+
onSelectedEventChange?.(event.id, event)
|
|
84
|
+
onEventClick?.(event)
|
|
85
|
+
}
|
|
41
86
|
|
|
42
87
|
if (visibleDays.length === 0) {
|
|
43
88
|
return (
|
|
44
89
|
<div
|
|
45
90
|
data-slot="calendar-scheduler"
|
|
46
91
|
data-view={view}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
)}
|
|
92
|
+
data-variant={variant}
|
|
93
|
+
data-density={density}
|
|
94
|
+
className={cn("rounded-[var(--aui-card-radius,var(--radius-xl))] border border-[color:var(--aui-card-border,var(--border))] bg-card p-5 text-sm text-muted-foreground shadow-[var(--aui-card-shadow,var(--aui-control-shadow,none))]", className)}
|
|
51
95
|
{...props}
|
|
52
96
|
>
|
|
53
97
|
<div className="flex min-h-32 items-center justify-center rounded-[var(--radius-lg)] border border-dashed border-[color:var(--aui-card-border,var(--border))] bg-muted/20 px-4 py-6 text-center">
|
|
54
98
|
<div className="space-y-1.5">
|
|
55
|
-
<p data-empty="true" className="text-sm font-medium text-foreground">
|
|
56
|
-
|
|
57
|
-
</p>
|
|
58
|
-
{onCreateEvent ? (
|
|
59
|
-
<button
|
|
60
|
-
type="button"
|
|
61
|
-
className="rounded-md border border-dashed px-3 py-2 text-sm text-muted-foreground hover:bg-muted/50"
|
|
62
|
-
onClick={() => onCreateEvent("today")}
|
|
63
|
-
>
|
|
64
|
-
Create event
|
|
65
|
-
</button>
|
|
66
|
-
) : null}
|
|
99
|
+
<p data-empty="true" className="text-sm font-medium text-foreground">{empty}</p>
|
|
100
|
+
{onCreateEvent ? <button type="button" className="rounded-md border border-dashed px-3 py-2 text-sm text-muted-foreground hover:bg-muted/50" onClick={() => onCreateEvent("today")}>{createLabel}</button> : null}
|
|
67
101
|
</div>
|
|
68
102
|
</div>
|
|
69
103
|
</div>
|
|
@@ -74,53 +108,79 @@ function CalendarScheduler({
|
|
|
74
108
|
<div
|
|
75
109
|
data-slot="calendar-scheduler"
|
|
76
110
|
data-view={view}
|
|
77
|
-
|
|
111
|
+
data-variant={variant}
|
|
112
|
+
data-density={density}
|
|
113
|
+
className={cn(
|
|
114
|
+
"grid",
|
|
115
|
+
density === "compact" ? "gap-2" : density === "comfortable" ? "gap-5" : "gap-3",
|
|
116
|
+
view === "month" && variant === "board" && "md:grid-cols-2 xl:grid-cols-3",
|
|
117
|
+
variant === "agenda" && "rounded-[var(--aui-card-radius,var(--radius-xl))] border border-[color:var(--aui-card-border,var(--border))] bg-card p-4 shadow-[var(--aui-card-shadow,var(--aui-control-shadow,none))]",
|
|
118
|
+
className
|
|
119
|
+
)}
|
|
78
120
|
{...props}
|
|
79
121
|
>
|
|
122
|
+
{(title || description || actions) ? (
|
|
123
|
+
<div data-slot="calendar-scheduler-header" className="flex flex-wrap items-start justify-between gap-3">
|
|
124
|
+
<div className="min-w-[min(100%,16rem)] flex-1 space-y-1">
|
|
125
|
+
{title ? <h2 className="text-base font-semibold text-foreground">{title}</h2> : null}
|
|
126
|
+
{description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
|
|
127
|
+
</div>
|
|
128
|
+
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div> : null}
|
|
129
|
+
</div>
|
|
130
|
+
) : null}
|
|
131
|
+
|
|
80
132
|
{visibleDays.map((day) => {
|
|
81
|
-
const dayEvents =
|
|
133
|
+
const dayEvents = visibleEvents.filter((event) => event.date === day)
|
|
82
134
|
|
|
83
135
|
return (
|
|
84
|
-
<section
|
|
85
|
-
key={day}
|
|
86
|
-
className="rounded-[var(--aui-card-radius,var(--radius-xl))] border border-[color:var(--aui-card-border,var(--border))] bg-card p-4 shadow-[var(--aui-card-shadow,var(--aui-control-shadow,none))]"
|
|
87
|
-
>
|
|
136
|
+
<section key={day} data-slot="calendar-scheduler-day" className={cn(variant === "board" && "rounded-[var(--aui-card-radius,var(--radius-xl))] border border-[color:var(--aui-card-border,var(--border))] bg-card p-4 shadow-[var(--aui-card-shadow,var(--aui-control-shadow,none))]", dayClassName)}>
|
|
88
137
|
<div className="mb-3 flex items-center justify-between gap-3">
|
|
89
|
-
<div className="text-sm font-semibold text-foreground">{day}</div>
|
|
90
|
-
<div className="text-xs text-muted-foreground">{dayEvents.length} items</div>
|
|
138
|
+
<div className="text-sm font-semibold text-foreground">{formatDay?.(day) ?? day}</div>
|
|
139
|
+
{showCount ? <div className="text-xs text-muted-foreground">{dayEvents.length} items</div> : null}
|
|
91
140
|
</div>
|
|
92
|
-
<div className="grid gap-2">
|
|
141
|
+
<div className={cn("grid", density === "compact" ? "gap-1.5" : "gap-2")}>
|
|
93
142
|
{dayEvents.length === 0 ? (
|
|
94
|
-
<div className="rounded-[var(--radius-md)] border border-dashed border-[color:var(--aui-card-border,var(--border))] px-3 py-4 text-sm text-muted-foreground">
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
dayEvents.map((event) => (
|
|
143
|
+
<div className="rounded-[var(--radius-md)] border border-dashed border-[color:var(--aui-card-border,var(--border))] px-3 py-4 text-sm text-muted-foreground">{empty}</div>
|
|
144
|
+
) : dayEvents.map((event) => {
|
|
145
|
+
const selected = currentSelectedEventId === event.id
|
|
146
|
+
return (
|
|
99
147
|
<button
|
|
100
148
|
key={event.id}
|
|
101
149
|
type="button"
|
|
150
|
+
disabled={event.disabled}
|
|
151
|
+
data-slot="calendar-scheduler-event"
|
|
152
|
+
data-selected={selected || undefined}
|
|
102
153
|
className={cn(
|
|
103
|
-
"
|
|
104
|
-
|
|
154
|
+
"border text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50",
|
|
155
|
+
variant === "board" ? "rounded-[var(--radius-md)] px-3 py-2.5 shadow-sm" : "grid w-full grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3 rounded-[var(--radius-md)] bg-background px-3 py-3 hover:bg-muted/35 data-[selected=true]:border-primary/40 data-[selected=true]:bg-primary/5",
|
|
156
|
+
variant === "board" && toneClassName[event.tone ?? "default"], eventClassName, event.className
|
|
105
157
|
)}
|
|
106
|
-
onClick={() =>
|
|
158
|
+
onClick={() => selectEvent(event)}
|
|
107
159
|
>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
160
|
+
{renderEvent ? renderEvent(event, { selected }) : variant === "agenda" ? (
|
|
161
|
+
<>
|
|
162
|
+
<span className="inline-flex min-w-14 items-center gap-1.5 font-medium">{event.icon}{event.time ?? "—"}</span>
|
|
163
|
+
<span className="min-w-0">
|
|
164
|
+
<span className="block truncate font-medium text-foreground">{event.title}</span>
|
|
165
|
+
{event.description ? <span className="mt-0.5 block text-xs text-muted-foreground">{event.description}</span> : null}
|
|
166
|
+
{event.meta ? <span className="mt-1 block text-xs text-muted-foreground">{event.meta}</span> : null}
|
|
167
|
+
</span>
|
|
168
|
+
{event.badge ? <span className="shrink-0">{event.badge}</span> : event.durationMinutes ? <span className="shrink-0 text-xs opacity-70">{event.durationMinutes} min</span> : null}
|
|
169
|
+
</>
|
|
170
|
+
) : (
|
|
171
|
+
<>
|
|
172
|
+
<div className="font-medium">{event.title}</div>
|
|
173
|
+
{event.description ? <div className="mt-1 text-xs opacity-80">{event.description}</div> : null}
|
|
174
|
+
{event.time ? <div className="mt-1 text-xs opacity-70">{event.time}</div> : null}
|
|
175
|
+
{event.durationMinutes ? <div className="mt-1 text-xs opacity-70">{event.durationMinutes} min</div> : null}
|
|
176
|
+
{event.meta ? <div className="mt-1 text-xs opacity-70">{event.meta}</div> : null}
|
|
177
|
+
</>
|
|
178
|
+
)}
|
|
111
179
|
</button>
|
|
112
|
-
)
|
|
113
|
-
)}
|
|
180
|
+
)
|
|
181
|
+
})}
|
|
114
182
|
</div>
|
|
115
|
-
{onCreateEvent
|
|
116
|
-
<button
|
|
117
|
-
type="button"
|
|
118
|
-
className="mt-3 w-full rounded-md border border-dashed px-3 py-2 text-sm text-muted-foreground hover:bg-muted/50"
|
|
119
|
-
onClick={() => onCreateEvent(day)}
|
|
120
|
-
>
|
|
121
|
-
Create event
|
|
122
|
-
</button>
|
|
123
|
-
)}
|
|
183
|
+
{onCreateEvent ? <button type="button" className="mt-3 w-full rounded-md border border-dashed px-3 py-2 text-sm text-muted-foreground hover:bg-muted/50" onClick={() => onCreateEvent(day)}>{createLabel}</button> : null}
|
|
124
184
|
</section>
|
|
125
185
|
)
|
|
126
186
|
})}
|
|
@@ -132,7 +132,7 @@ function ResizablePanelGroup({ direction = "horizontal", className, children, ..
|
|
|
132
132
|
)
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
function ResizablePanel({ className, style, defaultSize, ...props }: ResizablePanelInternalProps) {
|
|
135
|
+
function ResizablePanel({ className, style, defaultSize, minSize: _minSize, ...props }: ResizablePanelInternalProps) {
|
|
136
136
|
const context = React.useContext(ResizablePanelContext)
|
|
137
137
|
const panelIndex = Number(props["data-panel-index"] ?? 0)
|
|
138
138
|
const basis = context ? context.getSize(panelIndex) : normalizeSizeValue(defaultSize) ?? 100
|
|
@@ -55,7 +55,7 @@ function DetailLayout({
|
|
|
55
55
|
{hasContent && (
|
|
56
56
|
<div
|
|
57
57
|
data-slot="detail-layout-content"
|
|
58
|
-
className={cn("grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1fr)_20rem]", contentClassName)}
|
|
58
|
+
className={cn("grid min-w-0 gap-4", aside && "xl:grid-cols-[minmax(0,1fr)_20rem]", contentClassName)}
|
|
59
59
|
>
|
|
60
60
|
<main data-slot="detail-layout-main" className="min-w-0">
|
|
61
61
|
{children}
|
|
@@ -22,8 +22,10 @@ import {
|
|
|
22
22
|
} from "@/components/form/form-date-range-input"
|
|
23
23
|
import { cn } from "@/lib/utils"
|
|
24
24
|
|
|
25
|
-
export type FormBuilderLayout = "grid" | "stack"
|
|
26
|
-
export type FormBuilderDensity = "compact" | "default" | "comfortable"
|
|
25
|
+
export type FormBuilderLayout = "grid" | "stack"
|
|
26
|
+
export type FormBuilderDensity = "compact" | "default" | "comfortable"
|
|
27
|
+
|
|
28
|
+
type DistributiveOmit<T, TKey extends PropertyKey> = T extends unknown ? Omit<T, TKey> : never
|
|
27
29
|
|
|
28
30
|
export type FormBuilderFieldRenderContext<TFieldValues extends FieldValues> = {
|
|
29
31
|
control: Control<TFieldValues>
|
|
@@ -48,7 +50,7 @@ export type FormBuilderInputField<
|
|
|
48
50
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
49
51
|
> = BaseFormBuilderField & {
|
|
50
52
|
type: "input"
|
|
51
|
-
props:
|
|
53
|
+
props: DistributiveOmit<FormInputProps<TFieldValues, TName>, "control">
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export type FormBuilderTextareaField<
|
|
@@ -64,7 +66,7 @@ export type FormBuilderSelectField<
|
|
|
64
66
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
65
67
|
> = BaseFormBuilderField & {
|
|
66
68
|
type: "select"
|
|
67
|
-
props:
|
|
69
|
+
props: DistributiveOmit<FormSelectProps<TFieldValues, TName>, "control">
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
export type FormBuilderAsyncSelectField<
|
|
@@ -116,17 +118,14 @@ export type FormBuilderDateRangeField<
|
|
|
116
118
|
props: Omit<FormDateRangeInputProps<TFieldValues, TFromName, TToName>, "control">
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
|
|
120
|
-
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
|
126
|
-
|
|
|
127
|
-
| FormBuilderPhoneField<TFieldValues>
|
|
128
|
-
| FormBuilderDateField<TFieldValues>
|
|
129
|
-
| FormBuilderDateRangeField<TFieldValues>
|
|
121
|
+
type FormBuilderStoredField = BaseFormBuilderField & {
|
|
122
|
+
type: "input" | "textarea" | "select" | "async-select" | "switch" | "number" | "phone" | "date" | "date-range"
|
|
123
|
+
props: object
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type FormBuilderField<TFieldValues extends FieldValues> =
|
|
127
|
+
| FormBuilderCustomField<TFieldValues>
|
|
128
|
+
| FormBuilderStoredField
|
|
130
129
|
|
|
131
130
|
export type FormBuilderSection<TFieldValues extends FieldValues> = {
|
|
132
131
|
id: string
|
|
@@ -280,8 +279,11 @@ function renderFormBuilderField<TFieldValues extends FieldValues>(
|
|
|
280
279
|
field: FormBuilderField<TFieldValues>,
|
|
281
280
|
context: FormBuilderFieldRenderContext<TFieldValues>
|
|
282
281
|
) {
|
|
283
|
-
const FormInputComponent = FormInput as unknown as React.ComponentType<Record<string, unknown>>
|
|
284
|
-
const FormSelectComponent = FormSelect as unknown as React.ComponentType<Record<string, unknown>>
|
|
282
|
+
const FormInputComponent = FormInput as unknown as React.ComponentType<Record<string, unknown>>
|
|
283
|
+
const FormSelectComponent = FormSelect as unknown as React.ComponentType<Record<string, unknown>>
|
|
284
|
+
const FormTextareaComponent = FormTextarea as unknown as React.ComponentType<Record<string, unknown>>
|
|
285
|
+
const FormSwitchComponent = FormSwitch as unknown as React.ComponentType<Record<string, unknown>>
|
|
286
|
+
const FormDateRangeInputComponent = FormDateRangeInput as unknown as React.ComponentType<Record<string, unknown>>
|
|
285
287
|
|
|
286
288
|
switch (field.type) {
|
|
287
289
|
case "custom":
|
|
@@ -293,8 +295,8 @@ function renderFormBuilderField<TFieldValues extends FieldValues>(
|
|
|
293
295
|
readOnly: context.readOnly,
|
|
294
296
|
...(field.props as Omit<FormInputProps<TFieldValues, FieldPath<TFieldValues>>, "control">),
|
|
295
297
|
})
|
|
296
|
-
case "textarea":
|
|
297
|
-
return
|
|
298
|
+
case "textarea":
|
|
299
|
+
return React.createElement(FormTextareaComponent, { control: context.control, disabled: context.disabled, readOnly: context.readOnly, ...field.props })
|
|
298
300
|
case "select":
|
|
299
301
|
return React.createElement(FormSelectComponent, {
|
|
300
302
|
control: context.control,
|
|
@@ -308,8 +310,8 @@ function renderFormBuilderField<TFieldValues extends FieldValues>(
|
|
|
308
310
|
kind: "async",
|
|
309
311
|
...field.props,
|
|
310
312
|
})
|
|
311
|
-
case "switch":
|
|
312
|
-
return
|
|
313
|
+
case "switch":
|
|
314
|
+
return React.createElement(FormSwitchComponent, { control: context.control, disabled: context.disabled, ...field.props })
|
|
313
315
|
case "number":
|
|
314
316
|
return React.createElement(FormInputComponent, {
|
|
315
317
|
control: context.control,
|
|
@@ -334,15 +336,13 @@ function renderFormBuilderField<TFieldValues extends FieldValues>(
|
|
|
334
336
|
kind: "date",
|
|
335
337
|
...field.props,
|
|
336
338
|
})
|
|
337
|
-
case "date-range":
|
|
338
|
-
return (
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
/>
|
|
345
|
-
)
|
|
339
|
+
case "date-range":
|
|
340
|
+
return React.createElement(FormDateRangeInputComponent, {
|
|
341
|
+
control: context.control,
|
|
342
|
+
fromInputProps: { disabled: context.disabled, readOnly: context.readOnly },
|
|
343
|
+
toInputProps: { disabled: context.disabled, readOnly: context.readOnly },
|
|
344
|
+
...field.props,
|
|
345
|
+
})
|
|
346
346
|
default:
|
|
347
347
|
return null
|
|
348
348
|
}
|
|
@@ -120,7 +120,7 @@ function ResourceDetailPage({
|
|
|
120
120
|
{(hasMainContent || aside) && (
|
|
121
121
|
<div
|
|
122
122
|
data-slot="resource-detail-page-content"
|
|
123
|
-
className={cn("grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1fr)_20rem]", contentClassName)}
|
|
123
|
+
className={cn("grid min-w-0 gap-4", aside && "xl:grid-cols-[minmax(0,1fr)_20rem]", contentClassName)}
|
|
124
124
|
>
|
|
125
125
|
<div data-slot="resource-detail-page-main" className="grid min-w-0 gap-4">
|
|
126
126
|
{children}
|
|
@@ -136,7 +136,7 @@ function ResourcePage<TData, TValue = unknown>({
|
|
|
136
136
|
{(hasMainContent || aside) && (
|
|
137
137
|
<div
|
|
138
138
|
data-slot="resource-page-content"
|
|
139
|
-
className={cn("grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1fr)_20rem]", contentClassName)}
|
|
139
|
+
className={cn("grid min-w-0 gap-4", aside && "xl:grid-cols-[minmax(0,1fr)_20rem]", contentClassName)}
|
|
140
140
|
>
|
|
141
141
|
<div data-slot="resource-page-main" className="grid min-w-0 gap-4">
|
|
142
142
|
{children}
|
|
@@ -46,6 +46,7 @@ export type ButtonProps = ButtonPrimitive.Props &
|
|
|
46
46
|
iconOnly?: boolean
|
|
47
47
|
fullWidth?: boolean
|
|
48
48
|
pressed?: boolean
|
|
49
|
+
labelClassName?: string
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
function Button({
|
|
@@ -60,6 +61,7 @@ function Button({
|
|
|
60
61
|
iconOnly = false,
|
|
61
62
|
fullWidth = false,
|
|
62
63
|
pressed = false,
|
|
64
|
+
labelClassName,
|
|
63
65
|
children,
|
|
64
66
|
"aria-label": ariaLabel,
|
|
65
67
|
...props
|
|
@@ -96,7 +98,7 @@ function Button({
|
|
|
96
98
|
{leftIcon}
|
|
97
99
|
</span>
|
|
98
100
|
) : null}
|
|
99
|
-
{children ? <span data-slot="button-label">{loading ? loadingLabel : children}</span> : null}
|
|
101
|
+
{children ? <span data-slot="button-label" className={cn("inline-flex min-w-0 items-center gap-[inherit]", labelClassName)}>{loading ? loadingLabel : children}</span> : null}
|
|
100
102
|
{!loading && rightIcon ? (
|
|
101
103
|
<span data-icon="inline-end" data-slot="button-icon" className="inline-flex shrink-0 items-center justify-center">
|
|
102
104
|
{rightIcon}
|
|
@@ -6,23 +6,26 @@ export const calendarSchedulerMock: ComponentDemoMock = {
|
|
|
6
6
|
export function Example() {
|
|
7
7
|
return (
|
|
8
8
|
<CalendarScheduler
|
|
9
|
-
|
|
9
|
+
variant="agenda"
|
|
10
|
+
density="compact"
|
|
11
|
+
title="Delivery schedule"
|
|
12
|
+
defaultSelectedEventId="1"
|
|
13
|
+
days={["Mon, Jul 6"]}
|
|
10
14
|
events={[
|
|
11
|
-
{ id: "1", title: "Design review", date: "Mon, Jul 6", time: "10:00"
|
|
12
|
-
{ id: "2", title: "Release freeze", date: "
|
|
15
|
+
{ id: "1", title: "Design review", description: "API review", date: "Mon, Jul 6", time: "10:00" },
|
|
16
|
+
{ id: "2", title: "Release freeze", description: "Production gate", date: "Mon, Jul 6", time: "14:00" },
|
|
13
17
|
]}
|
|
14
18
|
/>
|
|
15
19
|
)
|
|
16
20
|
}`,
|
|
17
21
|
cliCommand: "npx tembro add calendar-scheduler",
|
|
18
22
|
highlights: [
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
23
|
+
"Board and agenda variants with compact or comfortable density",
|
|
24
|
+
"Rich metadata, badges, hidden and disabled event states",
|
|
25
|
+
"Controlled or uncontrolled selection and custom event rendering",
|
|
22
26
|
],
|
|
23
27
|
scenarios: [
|
|
24
28
|
{ title: "Ops dashboard", description: "Show day tasks by lane without heavy calendar widgets." },
|
|
25
29
|
{ title: "Sprint board", description: "Keep event density low and readable." },
|
|
26
30
|
],
|
|
27
31
|
}
|
|
28
|
-
|
|
@@ -7,13 +7,14 @@ const defaultDays = ["Mon, Jul 6", "Tue, Jul 7", "Wed, Jul 8"]
|
|
|
7
7
|
|
|
8
8
|
export function CalendarSchedulerShowcase() {
|
|
9
9
|
const [showWarning, setShowWarning] = React.useState(true)
|
|
10
|
+
const [variant, setVariant] = React.useState<"board" | "agenda">("agenda")
|
|
10
11
|
|
|
11
12
|
const events = React.useMemo(
|
|
12
13
|
() => ([
|
|
13
|
-
{ id: "1", title: "Design review", date: "Mon, Jul 6", time: "10:00", tone: "warning" },
|
|
14
|
-
{ id: "2", title: "Release freeze", date: "Tue, Jul 7", time: "14:00", tone: "danger" },
|
|
15
|
-
{ id: "3", title: "Daily standup", date: "Wed, Jul 8", time: "09:30", tone: "default" },
|
|
16
|
-
...(showWarning ? [{ id: "4", title: "Priority follow-up", date: "Mon, Jul 6", time: "16:00", tone: "warning" }] : []),
|
|
14
|
+
{ id: "1", title: "Design review", description: "Component API and release readiness", meta: "Product room", badge: <span className="rounded bg-amber-100 px-2 py-0.5 text-xs text-amber-800">Review</span>, date: "Mon, Jul 6", time: "10:00", tone: "warning" },
|
|
15
|
+
{ id: "2", title: "Release freeze", description: "Production deployment checkpoint", meta: "Platform team", badge: <span className="rounded bg-red-100 px-2 py-0.5 text-xs text-red-800">Required</span>, date: "Tue, Jul 7", time: "14:00", tone: "danger" },
|
|
16
|
+
{ id: "3", title: "Daily standup", description: "Owners, blockers and delivery status", date: "Wed, Jul 8", time: "09:30", tone: "default" },
|
|
17
|
+
...(showWarning ? [{ id: "4", title: "Priority follow-up", description: "Customer escalation review", date: "Mon, Jul 6", time: "16:00", tone: "warning" as const }] : []),
|
|
17
18
|
] as CalendarSchedulerEvent[]),
|
|
18
19
|
[showWarning]
|
|
19
20
|
)
|
|
@@ -21,6 +22,7 @@ export function CalendarSchedulerShowcase() {
|
|
|
21
22
|
return (
|
|
22
23
|
<div className="space-y-3">
|
|
23
24
|
<div className="flex gap-2">
|
|
25
|
+
<button type="button" className="inline-flex items-center rounded-md border px-3 py-1.5 text-sm" onClick={() => setVariant((value) => value === "agenda" ? "board" : "agenda")}>Use {variant === "agenda" ? "board" : "agenda"}</button>
|
|
24
26
|
<button
|
|
25
27
|
type="button"
|
|
26
28
|
className="inline-flex items-center rounded-md border px-3 py-1.5 text-sm"
|
|
@@ -29,7 +31,7 @@ export function CalendarSchedulerShowcase() {
|
|
|
29
31
|
{showWarning ? "Hide warning" : "Show warning"}
|
|
30
32
|
</button>
|
|
31
33
|
</div>
|
|
32
|
-
<CalendarScheduler days={defaultDays} events={events} />
|
|
34
|
+
<CalendarScheduler title="Delivery schedule" description="Production agenda with rich event metadata and selection." variant={variant} density="compact" days={defaultDays} events={events} defaultSelectedEventId="1" />
|
|
33
35
|
</div>
|
|
34
36
|
)
|
|
35
37
|
}
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
Avatar,
|
|
49
49
|
} from "@/index"
|
|
50
50
|
import { PreviewFileDropzone as FileDropzone, PreviewStatCard as StatCard } from "@/showcase/preview-compositions"
|
|
51
|
-
import { FormBuilder, customField, formSection } from "@/components/patterns/form-builder"
|
|
51
|
+
import { FormBuilder, customField, formSection, inputField, selectField, switchField, textareaField } from "@/components/patterns/form-builder"
|
|
52
52
|
import { ResourceDetailPage } from "@/components/patterns/resource-detail-page"
|
|
53
53
|
import { ResourcePage, ResourcePageSection } from "@/components/patterns/resource-page"
|
|
54
54
|
import { useForm } from "react-hook-form"
|
|
@@ -750,6 +750,7 @@ function FormBuilderPreview() {
|
|
|
750
750
|
email: string
|
|
751
751
|
notes: string
|
|
752
752
|
marketing: boolean
|
|
753
|
+
role: string
|
|
753
754
|
}
|
|
754
755
|
|
|
755
756
|
const form = useForm<FormBuilderDemoValues>({
|
|
@@ -758,6 +759,7 @@ function FormBuilderPreview() {
|
|
|
758
759
|
email: "azamat@example.com",
|
|
759
760
|
notes: "Release gate is ready.",
|
|
760
761
|
marketing: true,
|
|
762
|
+
role: "admin",
|
|
761
763
|
},
|
|
762
764
|
})
|
|
763
765
|
|
|
@@ -779,6 +781,14 @@ function FormBuilderPreview() {
|
|
|
779
781
|
</div>
|
|
780
782
|
),
|
|
781
783
|
}),
|
|
784
|
+
inputField<FormBuilderDemoValues, "name">({ id: "name", props: { name: "name", label: "Name", required: true } }),
|
|
785
|
+
inputField<FormBuilderDemoValues, "email">({ id: "email", props: { name: "email", label: "Email", type: "email" } }),
|
|
786
|
+
selectField<FormBuilderDemoValues, "role">({ id: "role", props: { name: "role", label: "Role", options: [
|
|
787
|
+
{ value: "admin", label: "Administrator" },
|
|
788
|
+
{ value: "member", label: "Member" },
|
|
789
|
+
] } }),
|
|
790
|
+
switchField<FormBuilderDemoValues, "marketing">({ id: "marketing", props: { name: "marketing", label: "Product updates" } }),
|
|
791
|
+
textareaField<FormBuilderDemoValues, "notes">({ id: "notes", colSpan: "full", props: { name: "notes", label: "Release notes" } }),
|
|
782
792
|
],
|
|
783
793
|
}),
|
|
784
794
|
]
|
|
@@ -25,7 +25,7 @@ export function HeroSection() {
|
|
|
25
25
|
<div className="flex flex-wrap gap-2">
|
|
26
26
|
<Button leftIcon={<PlusIcon className="size-4" />}>Create</Button>
|
|
27
27
|
<Button variant="outline" leftIcon={<SettingsIcon className="size-4" />}>Settings</Button>
|
|
28
|
-
<CopyButton value="npx tembro@3.1.
|
|
28
|
+
<CopyButton value="npx tembro@3.1.17 list --json">Copy list command</CopyButton>
|
|
29
29
|
</div>
|
|
30
30
|
</div>
|
|
31
31
|
|
|
@@ -33,7 +33,7 @@ export function HeroSection() {
|
|
|
33
33
|
<StatisticCard label="Registry components" value={moduleCount} change="visible below" trend="up" description="tembro add <name>" />
|
|
34
34
|
<StatisticCard label="Local source files" value="152" change="with hooks/lib" trend="up" description="installed by CLI" />
|
|
35
35
|
<StatisticCard label="Categories" value={registryGroups.length} change="all shown" trend="up" description="actions to wizard" />
|
|
36
|
-
<StatisticCard label="Build" value="Pass" change="doctor pass" trend="up" description="tembro@3.1.
|
|
36
|
+
<StatisticCard label="Build" value="Pass" change="doctor pass" trend="up" description="tembro@3.1.17" />
|
|
37
37
|
</StatisticGrid>
|
|
38
38
|
</div>
|
|
39
39
|
</section>
|