tembro 3.1.16 → 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 +8 -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/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/modern/calendar-scheduler.cjs +1 -1
- package/dist/src/components/modern/calendar-scheduler.js +125 -59
- package/dist/src/components/ui/button/index.cjs +1 -1
- package/dist/src/components/ui/button/index.js +10 -9
- 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/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/modern/calendar-scheduler.tsx +108 -48
- package/packages/cli/vendor/src/components/ui/button/index.tsx +3 -1
- 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/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/registry.json +1 -1
|
@@ -1988,4 +1988,4 @@ ${t}`),i.forEach(n=>{let o=R[n],s=or(n),a=jn(n,o.category),c=o.registryDependenc
|
|
|
1988
1988
|
${s}`),a.sort().forEach(c=>{let d=R[c],l=or(c),u=jn(c,d.category),h=d.registryDependencies?.length?` -> ${d.registryDependencies.join(", ")}`:"";console.log(` ${c} [${l}/${u}]${h}`);});console.log(`
|
|
1989
1989
|
Status:`),console.log(" stable = safe public API"),console.log(" preview = usable but API may change"),console.log(" experimental = not ready for stable app contracts"),console.log(" internal = helper metadata"),console.log(`
|
|
1990
1990
|
Distribution:`),console.log(" foundation = small runtime-safe primitives, hooks, and helpers"),console.log(" source-copy = copy into app source with init/add and edit locally"),console.log(" system = larger product surfaces and patterns; prefer source-copy only"),console.log(`
|
|
1991
|
-
Usage:`),console.log(` ${re("add button input data-table")}`),console.log(` ${re("add form --overwrite")}`),console.log(` ${re("list --category data-table")}`),console.log(` ${re("list --json")}`);}var mt=be(qe());function Ja(t){try{return mt.default.readJsonSync(t)}catch{return null}}function ow(t){try{return mt.default.readFileSync(t,"utf8")}catch{return null}}function FP(t,e){return e?L__default.default.resolve(t,e):null}function sw(t){let e=t.paths?.components??t.componentsPath??"src/components",r=t.paths?.ui??t.componentsPath??L__default.default.join(e,"ui"),i=t.paths?.lib??L__default.default.dirname(t.utilsPath??"src/lib/utils.ts"),n=t.paths?.hooks??"src/hooks",o=t.utilsPath??L__default.default.join(i,"utils.ts");return {componentsRoot:e,uiPath:r,libPath:i,hooksPath:n,utilsPath:o}}function jP(t,e){let r=sw(e);return t.replace("{components}",r.componentsRoot).replace("{ui}",r.uiPath).replace("{hooks}",r.hooksPath).replace("{lib}",r.libPath).replace("{utils}",r.utilsPath)}function iw(t,e){return ["tsconfig.app.json","tsconfig.json"].map(i=>L__default.default.join(t,i)).some(i=>!!Ja(i)?.compilerOptions?.paths?.[`${e}/*`])}function nw(t,e){return ["vite.config.ts","vite.config.js","vite.config.mjs"].map(i=>L__default.default.join(t,i)).some(i=>{let n=ow(i);return !!(n?.includes(`"${e}"`)||n?.includes(`'${e}'`))})}function aw(t,e){let r=Ja(L__default.default.join(t,"package.json")),i=new Set([...Object.keys(r?.dependencies??{}),...Object.keys(r?.devDependencies??{})]);return e.filter(n=>!i.has(n))}function Ya(t,e){return (t.files??[]).map(r=>({source:r.source,target:jP(r.target,e)}))}function NP(t,e){return At.map(r=>R[r]).filter(r=>r.category!=="lib").filter(r=>!r.migrationAliasFor).filter(r=>r.files?.length).filter(r=>Ya(r,e).some(i=>mt.default.existsSync(L__default.default.join(t,i.target))))}function qP(t,e){let r=NP(t,e),i=[],n=[],o=new Set;for(let s of r){for(let a of Ya(s,e))mt.default.existsSync(L__default.default.join(t,a.target))||i.push(`${s.name}: ${a.target}`);for(let a of s.registryDependencies??[]){let c=R[a];if(!c?.files?.length)continue;Ya(c,e).some(l=>mt.default.existsSync(L__default.default.join(t,l.target)))||n.push(`${s.name} -> ${a}`);}for(let a of s.dependencies??[])o.add(a);}return {copiedItems:r,missingFiles:i,missingRegistryDependencies:n,missingPackages:aw(t,Array.from(o).sort())}}function Ze(t,e){t.push(e);}function LP(t){return t?t.includes("/* tembro:start */")||t.includes("--aui-")||t.includes("--az-"):false}function cw(t={}){let e=process.cwd(),r=[],i=L__default.default.join(e,"package.json"),n=L__default.default.join(e,"tembro.json"),o=mt.default.existsSync(i),s=Ja(n);if(Ze(r,{name:"package.json",status:o?"pass":"fail",message:o?"package.json topildi.":"package.json topilmadi. Commandni React, Vite yoki Next project ichida ishlating."}),Ze(r,{name:"tembro.json",status:s?"pass":"fail",message:s?"tembro.json topildi.":`tembro.json topilmadi. Avval ${re("init")} ishlating.`}),s){let d=s.alias??"@",{componentsRoot:l,uiPath:u,libPath:h,hooksPath:p}=sw(s),f=s.cssPath??"src/index.css",b=[["components",l],["ui",u],["lib",h],["hooks",p]].filter(([,C])=>{let M=FP(e,C);return !M||!mt.default.existsSync(M)});Ze(r,{name:"paths",status:b.length?"warn":"pass",message:b.length?`Yetishmayotgan pathlar: ${b.map(([C,M])=>`${C}=${M}`).join(", ")}`:"Configured source paths mavjud."}),Ze(r,{name:"alias",status:iw(e,d)||nw(e,d)?"pass":"warn",message:iw(e,d)||nw(e,d)?`${d} alias tsconfig yoki vite configda topildi.`:`${d} alias topilmadi. Import pathlar ishlamasligi mumkin.`});let y=ow(L__default.default.join(e,f)),w=LP(y);Ze(r,{name:"theme-css",status:w?"pass":"warn",message:w?`${f} ichida Tembro tokenlari bor.`:`${f} ichida Tembro tokenlari topilmadi. ${re(`theme ${f}`)} ishlating.`});let S=qP(e,s);Ze(r,{name:"component-files",status:S.missingFiles.length?"warn":"pass",message:S.missingFiles.length?`Yetishmayotgan component fayllari: ${S.missingFiles.slice(0,8).join(", ")}${S.missingFiles.length>8?` (+${S.missingFiles.length-8})`:""}`:`${S.copiedItems.length} ta copied component file set tekshirildi.`}),Ze(r,{name:"registry-dependencies",status:S.missingRegistryDependencies.length?"warn":"pass",message:S.missingRegistryDependencies.length?`Yetishmayotgan registry dependencylar: ${S.missingRegistryDependencies.slice(0,8).join(", ")}${S.missingRegistryDependencies.length>8?` (+${S.missingRegistryDependencies.length-8})`:""}`:"Copied componentlar uchun registry dependencylar joyida."}),Ze(r,{name:"component-dependencies",status:S.missingPackages.length?"warn":"pass",message:S.missingPackages.length?`Copied componentlar uchun yetishmayotgan package dependencylar: ${S.missingPackages.join(", ")}`:"Copied component package dependencylari package.json ichida bor."});}if(o){let d=aw(e,["@fontsource-variable/geist","clsx","tailwind-merge","tw-animate-css"]);Ze(r,{name:"base-dependencies",status:d.length?"warn":"pass",message:d.length?`Yetishmayotgan dependencylar: ${d.join(", ")}`:"Base dependencylar package.json ichida bor."});}let a=r.filter(d=>d.status==="fail"),c=r.filter(d=>d.status==="warn");if(t.json){console.log(JSON.stringify({ok:a.length===0,checks:r},null,2));return}for(let d of r){let l=`${d.status.toUpperCase()} ${d.name}: ${d.message}`;d.status==="pass"&&x.success(l),d.status==="warn"&&x.warn(l),d.status==="fail"&&x.error(l);}if(a.length&&(x.error(`Doctor failed: ${a.length} fail, ${c.length} warn.`),process.exit(1)),c.length){x.warn(`Doctor completed with ${c.length} warning.`);return}x.success("Doctor passed. Project Tembro setup looks ready.");}var Ka=be(qe());async function lw(t){let e=process.cwd(),r=L__default.default.join(e,"tembro.json"),i=Ka.default.existsSync(r)?await Ka.default.readJson(r):{},n=t??i.cssPath??i.globalCssPath??"src/index.css",o=i.paths?.components??i.componentsPath??"src/components",s=await Mn({cwd:e,cssPath:n,componentsPath:o});x.success(`Tembro theme CSS yangilandi: ${s}`);}var dw={minimal:["button","input","card","badge"],dashboard:["button","input","card","badge","dropdown-menu","skeleton","tabs","page-state","filter-bar","description-list","info-card","data-table"]};async function uw(t,e={}){let r=dw[t];r||(x.error(`Preset topilmadi: ${t}`),process.exit(1)),await Dt(r,e);}var Qe=new $t;Qe.name("tembro").description("Tembro source-copy CLI").version("3.1.
|
|
1991
|
+
Usage:`),console.log(` ${re("add button input data-table")}`),console.log(` ${re("add form --overwrite")}`),console.log(` ${re("list --category data-table")}`),console.log(` ${re("list --json")}`);}var mt=be(qe());function Ja(t){try{return mt.default.readJsonSync(t)}catch{return null}}function ow(t){try{return mt.default.readFileSync(t,"utf8")}catch{return null}}function FP(t,e){return e?L__default.default.resolve(t,e):null}function sw(t){let e=t.paths?.components??t.componentsPath??"src/components",r=t.paths?.ui??t.componentsPath??L__default.default.join(e,"ui"),i=t.paths?.lib??L__default.default.dirname(t.utilsPath??"src/lib/utils.ts"),n=t.paths?.hooks??"src/hooks",o=t.utilsPath??L__default.default.join(i,"utils.ts");return {componentsRoot:e,uiPath:r,libPath:i,hooksPath:n,utilsPath:o}}function jP(t,e){let r=sw(e);return t.replace("{components}",r.componentsRoot).replace("{ui}",r.uiPath).replace("{hooks}",r.hooksPath).replace("{lib}",r.libPath).replace("{utils}",r.utilsPath)}function iw(t,e){return ["tsconfig.app.json","tsconfig.json"].map(i=>L__default.default.join(t,i)).some(i=>!!Ja(i)?.compilerOptions?.paths?.[`${e}/*`])}function nw(t,e){return ["vite.config.ts","vite.config.js","vite.config.mjs"].map(i=>L__default.default.join(t,i)).some(i=>{let n=ow(i);return !!(n?.includes(`"${e}"`)||n?.includes(`'${e}'`))})}function aw(t,e){let r=Ja(L__default.default.join(t,"package.json")),i=new Set([...Object.keys(r?.dependencies??{}),...Object.keys(r?.devDependencies??{})]);return e.filter(n=>!i.has(n))}function Ya(t,e){return (t.files??[]).map(r=>({source:r.source,target:jP(r.target,e)}))}function NP(t,e){return At.map(r=>R[r]).filter(r=>r.category!=="lib").filter(r=>!r.migrationAliasFor).filter(r=>r.files?.length).filter(r=>Ya(r,e).some(i=>mt.default.existsSync(L__default.default.join(t,i.target))))}function qP(t,e){let r=NP(t,e),i=[],n=[],o=new Set;for(let s of r){for(let a of Ya(s,e))mt.default.existsSync(L__default.default.join(t,a.target))||i.push(`${s.name}: ${a.target}`);for(let a of s.registryDependencies??[]){let c=R[a];if(!c?.files?.length)continue;Ya(c,e).some(l=>mt.default.existsSync(L__default.default.join(t,l.target)))||n.push(`${s.name} -> ${a}`);}for(let a of s.dependencies??[])o.add(a);}return {copiedItems:r,missingFiles:i,missingRegistryDependencies:n,missingPackages:aw(t,Array.from(o).sort())}}function Ze(t,e){t.push(e);}function LP(t){return t?t.includes("/* tembro:start */")||t.includes("--aui-")||t.includes("--az-"):false}function cw(t={}){let e=process.cwd(),r=[],i=L__default.default.join(e,"package.json"),n=L__default.default.join(e,"tembro.json"),o=mt.default.existsSync(i),s=Ja(n);if(Ze(r,{name:"package.json",status:o?"pass":"fail",message:o?"package.json topildi.":"package.json topilmadi. Commandni React, Vite yoki Next project ichida ishlating."}),Ze(r,{name:"tembro.json",status:s?"pass":"fail",message:s?"tembro.json topildi.":`tembro.json topilmadi. Avval ${re("init")} ishlating.`}),s){let d=s.alias??"@",{componentsRoot:l,uiPath:u,libPath:h,hooksPath:p}=sw(s),f=s.cssPath??"src/index.css",b=[["components",l],["ui",u],["lib",h],["hooks",p]].filter(([,C])=>{let M=FP(e,C);return !M||!mt.default.existsSync(M)});Ze(r,{name:"paths",status:b.length?"warn":"pass",message:b.length?`Yetishmayotgan pathlar: ${b.map(([C,M])=>`${C}=${M}`).join(", ")}`:"Configured source paths mavjud."}),Ze(r,{name:"alias",status:iw(e,d)||nw(e,d)?"pass":"warn",message:iw(e,d)||nw(e,d)?`${d} alias tsconfig yoki vite configda topildi.`:`${d} alias topilmadi. Import pathlar ishlamasligi mumkin.`});let y=ow(L__default.default.join(e,f)),w=LP(y);Ze(r,{name:"theme-css",status:w?"pass":"warn",message:w?`${f} ichida Tembro tokenlari bor.`:`${f} ichida Tembro tokenlari topilmadi. ${re(`theme ${f}`)} ishlating.`});let S=qP(e,s);Ze(r,{name:"component-files",status:S.missingFiles.length?"warn":"pass",message:S.missingFiles.length?`Yetishmayotgan component fayllari: ${S.missingFiles.slice(0,8).join(", ")}${S.missingFiles.length>8?` (+${S.missingFiles.length-8})`:""}`:`${S.copiedItems.length} ta copied component file set tekshirildi.`}),Ze(r,{name:"registry-dependencies",status:S.missingRegistryDependencies.length?"warn":"pass",message:S.missingRegistryDependencies.length?`Yetishmayotgan registry dependencylar: ${S.missingRegistryDependencies.slice(0,8).join(", ")}${S.missingRegistryDependencies.length>8?` (+${S.missingRegistryDependencies.length-8})`:""}`:"Copied componentlar uchun registry dependencylar joyida."}),Ze(r,{name:"component-dependencies",status:S.missingPackages.length?"warn":"pass",message:S.missingPackages.length?`Copied componentlar uchun yetishmayotgan package dependencylar: ${S.missingPackages.join(", ")}`:"Copied component package dependencylari package.json ichida bor."});}if(o){let d=aw(e,["@fontsource-variable/geist","clsx","tailwind-merge","tw-animate-css"]);Ze(r,{name:"base-dependencies",status:d.length?"warn":"pass",message:d.length?`Yetishmayotgan dependencylar: ${d.join(", ")}`:"Base dependencylar package.json ichida bor."});}let a=r.filter(d=>d.status==="fail"),c=r.filter(d=>d.status==="warn");if(t.json){console.log(JSON.stringify({ok:a.length===0,checks:r},null,2));return}for(let d of r){let l=`${d.status.toUpperCase()} ${d.name}: ${d.message}`;d.status==="pass"&&x.success(l),d.status==="warn"&&x.warn(l),d.status==="fail"&&x.error(l);}if(a.length&&(x.error(`Doctor failed: ${a.length} fail, ${c.length} warn.`),process.exit(1)),c.length){x.warn(`Doctor completed with ${c.length} warning.`);return}x.success("Doctor passed. Project Tembro setup looks ready.");}var Ka=be(qe());async function lw(t){let e=process.cwd(),r=L__default.default.join(e,"tembro.json"),i=Ka.default.existsSync(r)?await Ka.default.readJson(r):{},n=t??i.cssPath??i.globalCssPath??"src/index.css",o=i.paths?.components??i.componentsPath??"src/components",s=await Mn({cwd:e,cssPath:n,componentsPath:o});x.success(`Tembro theme CSS yangilandi: ${s}`);}var dw={minimal:["button","input","card","badge"],dashboard:["button","input","card","badge","dropdown-menu","skeleton","tabs","page-state","filter-bar","description-list","info-card","data-table"]};async function uw(t,e={}){let r=dw[t];r||(x.error(`Preset topilmadi: ${t}`),process.exit(1)),await Dt(r,e);}var Qe=new $t;Qe.name("tembro").description("Tembro source-copy CLI").version("3.1.17");Qe.command("init").description("Initialize local Tembro source files in your project").option("--template <template>","Project defaults: vite or next").option("--skip-install","Do not install base dependencies").option("--showcase","Add every component and write a local Tembro workbench").option("-y, --defaults","Use template defaults without interactive prompts").action(Ky);Qe.command("preset").description("Add a source-copy preset to your project").argument("<name>","preset name: minimal or dashboard").option("-o, --overwrite","overwrite existing files").option("--dry-run","show files without writing").option("--skip-install","do not install package dependencies").action(uw);Qe.command("list").description("List available registry components").option("--category <category>","Filter by registry category").option("--status <status>","Filter by status: stable, preview, experimental, internal").option("--json","Print machine-readable JSON").action(rw);Qe.command("doctor").description("Check local project setup for Tembro source-copy usage").option("--json","Print machine-readable JSON").action(cw);Qe.command("add").description("Copy component source files into your project").argument("[components...]","components to add").option("-o, --overwrite","overwrite existing files").option("--dry-run","show files without writing").option("--plan","print machine-readable copy plan without writing").option("--skip-install","do not install package dependencies").action(Dt);Qe.command("showcase").description("Add every component and write a local Tembro workbench").option("-o, --overwrite","overwrite existing files").option("--dry-run","show files without writing").option("--skip-install","do not install package dependencies").action(t=>Dt(["showcase"],t));Qe.command("theme").description("Write or update Tembro theme CSS in your global CSS file").argument("[cssPath]","global CSS path, default from tembro.json or src/index.css").action(lw);Qe.parse();
|
|
@@ -22,6 +22,7 @@ export type DatePickerProps = Omit<
|
|
|
22
22
|
labels?: DatePickerLabels
|
|
23
23
|
disabled?: boolean
|
|
24
24
|
clearable?: boolean
|
|
25
|
+
triggerVariant?: "default" | "compact"
|
|
25
26
|
formatValue?: (value: string) => React.ReactNode
|
|
26
27
|
triggerClassName?: string
|
|
27
28
|
contentClassName?: string
|
|
@@ -40,6 +41,7 @@ function DatePicker({
|
|
|
40
41
|
labels,
|
|
41
42
|
disabled = false,
|
|
42
43
|
clearable = true,
|
|
44
|
+
triggerVariant = "default",
|
|
43
45
|
formatValue = defaultFormatValue,
|
|
44
46
|
triggerClassName,
|
|
45
47
|
contentClassName,
|
|
@@ -66,16 +68,18 @@ function DatePicker({
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
return (
|
|
69
|
-
<div data-slot="date-picker" className={cn("w-full", className)}>
|
|
71
|
+
<div data-slot="date-picker" data-trigger-variant={triggerVariant} className={cn("w-full", className)}>
|
|
70
72
|
<Popover open={open} onOpenChange={setOpen}>
|
|
71
73
|
<PopoverTrigger
|
|
72
74
|
render={
|
|
73
75
|
<Button
|
|
74
76
|
type="button"
|
|
75
77
|
variant="outline"
|
|
76
|
-
disabled={disabled}
|
|
78
|
+
disabled={disabled}
|
|
79
|
+
labelClassName="flex-1"
|
|
77
80
|
className={cn(
|
|
78
|
-
"group
|
|
81
|
+
"group w-full justify-start rounded-[var(--aui-control-radius,var(--radius-md))] border-border/80 bg-background/96 text-left font-normal shadow-[var(--aui-control-shadow,0_1px_2px_rgba(15,23,42,0.04))]",
|
|
82
|
+
triggerVariant === "default" ? "min-h-11 gap-3 px-3" : "min-h-9 gap-2 px-2.5",
|
|
79
83
|
!hasValue && "text-muted-foreground",
|
|
80
84
|
triggerClassName
|
|
81
85
|
)}
|
|
@@ -83,10 +87,10 @@ function DatePicker({
|
|
|
83
87
|
}
|
|
84
88
|
>
|
|
85
89
|
<CalendarIcon data-icon="inline-start" className={cn(hasValue && "text-primary")} />
|
|
86
|
-
<span className="grid min-w-0 flex-1 gap-0.5">
|
|
87
|
-
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
90
|
+
<span className={cn("grid min-w-0 flex-1", triggerVariant === "default" && "gap-0.5")}>
|
|
91
|
+
{triggerVariant === "default" ? <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
88
92
|
{labels?.selected ?? "Date"}
|
|
89
|
-
</span>
|
|
93
|
+
</span> : null}
|
|
90
94
|
<span className={cn("truncate text-sm", hasValue && "font-semibold text-foreground")}>
|
|
91
95
|
{hasValue ? formatValue(String(value)) : placeholder ?? labels?.placeholder ?? "Select date"}
|
|
92
96
|
</span>
|
|
@@ -96,7 +100,7 @@ function DatePicker({
|
|
|
96
100
|
role="button"
|
|
97
101
|
tabIndex={0}
|
|
98
102
|
aria-label={labels?.clear ?? "Clear date"}
|
|
99
|
-
className="ml-auto inline-flex
|
|
103
|
+
className={cn("ml-auto inline-flex shrink-0 items-center justify-center rounded-[var(--radius-sm)] text-muted-foreground opacity-80 transition hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring", triggerVariant === "default" ? "size-7" : "size-6")}
|
|
100
104
|
onClick={handleClear}
|
|
101
105
|
onKeyDown={(event) => {
|
|
102
106
|
if (event.key === "Enter" || event.key === " ") {
|
|
@@ -58,18 +58,19 @@ function Statistic({ label, value, prefix, suffix, description, trend = "neutral
|
|
|
58
58
|
)
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export type StatisticCardProps = React.ComponentProps<typeof Card> & StatisticProps & {
|
|
62
|
-
action?: React.ReactNode
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
export type StatisticCardProps = React.ComponentProps<typeof Card> & StatisticProps & {
|
|
62
|
+
action?: React.ReactNode
|
|
63
|
+
density?: "compact" | "default"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function StatisticCard({ action, density = "default", label, value, prefix, suffix, description, trend, change, loading, className, ...props }: StatisticCardProps) {
|
|
67
|
+
return (
|
|
68
|
+
<Card data-slot="statistic-card" data-density={density} className={className} {...props}>
|
|
69
|
+
<CardHeader data-slot="statistic-card-header" className={cn("flex flex-row items-center justify-between gap-3 pb-2", density === "compact" && "px-4 pt-4 pb-1")}>
|
|
69
70
|
<CardTitle className="text-sm font-medium text-muted-foreground">{label}</CardTitle>
|
|
70
71
|
{action ? <div data-slot="statistic-card-action">{action}</div> : null}
|
|
71
72
|
</CardHeader>
|
|
72
|
-
<CardContent>
|
|
73
|
+
<CardContent className={cn(density === "compact" && "px-4 pb-4")}>
|
|
73
74
|
<Statistic
|
|
74
75
|
label={<span className="sr-only">{label}</span>}
|
|
75
76
|
value={value}
|
|
@@ -85,16 +86,20 @@ function StatisticCard({ action, label, value, prefix, suffix, description, tren
|
|
|
85
86
|
)
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
export type StatisticGridProps = React.ComponentProps<"div"> & {
|
|
89
|
-
columns?: 1 | 2 | 3 | 4
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
export type StatisticGridProps = React.ComponentProps<"div"> & {
|
|
90
|
+
columns?: 1 | 2 | 3 | 4
|
|
91
|
+
gap?: "sm" | "default" | "lg"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function StatisticGrid({ columns = 4, gap = "default", className, ...props }: StatisticGridProps) {
|
|
93
95
|
return (
|
|
94
96
|
<div
|
|
95
97
|
data-slot="statistic-grid"
|
|
96
98
|
className={cn(
|
|
97
|
-
"grid
|
|
99
|
+
"grid",
|
|
100
|
+
gap === "sm" && "gap-3",
|
|
101
|
+
gap === "default" && "gap-4",
|
|
102
|
+
gap === "lg" && "gap-6",
|
|
98
103
|
columns === 1 && "grid-cols-1",
|
|
99
104
|
columns === 2 && "grid-cols-1 sm:grid-cols-2",
|
|
100
105
|
columns === 3 && "grid-cols-1 sm:grid-cols-2 xl:grid-cols-3",
|
|
@@ -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
|
})}
|
|
@@ -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
|
}
|
|
@@ -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>
|