@traky-app/ui 1.2.0 → 1.4.0
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/package.json +1 -1
- package/registry/traky/json/entity-icon.json +20 -0
- package/registry/traky/json/header-page.json +20 -0
- package/registry/traky/registry.json +20 -0
- package/registry/traky/ui/entity-icon.tsx +53 -0
- package/registry/traky/ui/header-page.tsx +171 -0
- package/src/ui/entity-icon.tsx +53 -0
- package/src/ui/header-page.tsx +171 -0
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "entity-icon",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"author": "Traky (https://traky.app)",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"class-variance-authority"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"card"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "ui/entity-icon.tsx",
|
|
15
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Card } from \"@/components/ui/card\"\n\nconst entityIconVariants = cva(\n \"w-fit shrink-0 bg-primary text-primary-foreground [&_svg]:shrink-0\",\n {\n variants: {\n size: {\n xs: \"p-1 [&_svg]:size-3.5\",\n sm: \"p-1.5 [&_svg]:size-5\",\n md: \"p-2 [&_svg]:size-6\",\n lg: \"p-2 [&_svg]:size-7\",\n xl: \"p-2.5 [&_svg]:size-8\",\n },\n },\n defaultVariants: {\n size: \"md\",\n },\n },\n)\n\ntype IconComponent = React.ComponentType<{\n className?: string\n stroke?: number\n}>\n\nfunction EntityIcon({\n icon: Icon,\n size = \"md\",\n className,\n iconClassName,\n ...props\n}: React.ComponentProps<typeof Card> &\n VariantProps<typeof entityIconVariants> & {\n icon: IconComponent\n iconClassName?: string\n }) {\n return (\n <Card\n data-slot=\"entity-icon\"\n data-size={size}\n className={cn(entityIconVariants({ size }), className)}\n {...props}\n >\n <Icon className={iconClassName} stroke={1.75} />\n </Card>\n )\n}\n\nexport { EntityIcon, entityIconVariants }\n",
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"target": ""
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "header-page",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"author": "Traky (https://traky.app)",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@tabler/icons-react"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"entity-icon"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "ui/header-page.tsx",
|
|
15
|
+
"content": "\"use client\"\n\nimport type { Icon } from \"@tabler/icons-react\"\nimport type { ReactNode } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { EntityIcon } from \"@/components/ui/entity-icon\"\n\ntype HeaderPageProps = {\n icon: Icon\n title: string\n description: string\n className?: string\n /** Contenido izquierdo de la barra (filtros, enlaces secundarios, etc.). */\n toolbarStart?: ReactNode\n /** Contenido derecho de la barra (resumen, paginación, acción principal). */\n toolbarEnd?: ReactNode\n /** Panel expandible debajo de la barra (p. ej. filtros avanzados). */\n filters?: ReactNode\n showFilters?: boolean\n toolbarAriaLabel?: string\n toolbarId?: string\n /** Acción principal junto al título (p. ej. crear registro). */\n action?: ReactNode\n}\n\nfunction HeaderPageMeta({\n children,\n className,\n}: {\n children: ReactNode\n className?: string\n}) {\n return (\n <p\n data-header-meta\n className={cn(\n \"text-sm text-muted-foreground max-sm:order-3 max-sm:basis-full max-sm:shrink-0 sm:order-1 xl:order-none\",\n className,\n )}\n >\n {children}\n </p>\n )\n}\n\nfunction HeaderPageToolbarAction({\n children,\n className,\n order = \"primary\",\n}: {\n children: ReactNode\n className?: string\n order?: \"primary\" | \"secondary\"\n}) {\n return (\n <div\n className={cn(\n \"w-full sm:w-auto xl:order-none [&_a]:w-full sm:[&_a]:w-auto [&_button:not([data-icon-only])]:w-full sm:[&_button:not([data-icon-only])]:w-auto\",\n order === \"primary\" && \"sm:order-2 sm:ml-auto\",\n order === \"secondary\" && \"sm:order-3\",\n className,\n )}\n >\n {children}\n </div>\n )\n}\n\nfunction HeaderPageToolbarPagination({\n children,\n className,\n}: {\n children: ReactNode\n className?: string\n}) {\n return (\n <div\n className={cn(\n \"flex items-center justify-center sm:order-3 xl:order-none\",\n className,\n )}\n >\n {children}\n </div>\n )\n}\n\nfunction HeaderPageRoot({\n icon: Icon,\n title,\n description,\n className,\n toolbarStart,\n toolbarEnd,\n filters,\n showFilters = false,\n toolbarAriaLabel = \"Filtros y acciones\",\n toolbarId,\n action,\n}: HeaderPageProps) {\n const hasToolbar = Boolean(toolbarStart || toolbarEnd)\n const hasFiltersPanel = Boolean(filters)\n\n return (\n <header className={cn(\"space-y-4\", className)}>\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex min-w-0 items-start gap-3\">\n <EntityIcon icon={Icon} size=\"lg\" />\n <div className=\"min-w-0\">\n <h1 className=\"text-xl font-semibold\">{title}</h1>\n <p className=\"mt-1 text-sm text-muted-foreground\">{description}</p>\n </div>\n </div>\n\n {action ? (\n <div className=\"flex w-full shrink-0 flex-wrap items-center gap-2 sm:w-auto sm:flex-nowrap sm:justify-end sm:pt-0.5 [&_a:not([data-icon-only])]:w-full sm:[&_a:not([data-icon-only])]:w-auto [&_button:not([data-icon-only])]:w-full sm:[&_button:not([data-icon-only])]:w-auto max-sm:[&>a:not([data-icon-only])]:basis-full max-sm:[&>a:not([data-icon-only])]:shrink-0 max-sm:[&>[data-icon-only]]:order-first max-sm:[&>button:not([data-icon-only])]:basis-full max-sm:[&>button:not([data-icon-only])]:shrink-0\">\n {action}\n </div>\n ) : null}\n </div>\n\n {(hasToolbar || hasFiltersPanel) && (\n <section\n id={toolbarId}\n className=\"space-y-4\"\n aria-label={toolbarAriaLabel}\n >\n {hasToolbar && (\n <div\n className={cn(\n \"flex flex-col gap-3\",\n \"max-sm:flex-row max-sm:flex-wrap max-sm:items-center max-sm:gap-2\",\n \"max-sm:[&_[data-icon-only]]:order-1 max-sm:[&_[data-icon-only]]:shrink-0\",\n \"max-sm:[&_[data-header-icons]_[data-icon-only]]:order-none\",\n \"max-sm:[&_.w-full]:order-2 max-sm:[&_.w-full]:basis-full max-sm:[&_.w-full]:shrink-0\",\n \"max-sm:[&_[data-header-icons]_.w-full]:order-none max-sm:[&_[data-header-icons]_.w-full]:basis-auto max-sm:[&_[data-header-icons]_.w-full]:shrink\",\n toolbarStart\n ? \"xl:flex-row xl:items-center xl:justify-between\"\n : \"sm:flex-row sm:flex-wrap sm:items-center sm:justify-between xl:justify-end xl:gap-4\",\n )}\n >\n {toolbarStart ? (\n <div className=\"max-sm:contents sm:flex sm:flex-wrap sm:items-center sm:gap-2\">\n {toolbarStart}\n </div>\n ) : null}\n\n {toolbarEnd ? (\n <div className=\"max-sm:contents sm:flex sm:flex-wrap sm:items-center sm:justify-between sm:gap-3 xl:justify-end xl:gap-4\">\n {toolbarEnd}\n </div>\n ) : null}\n </div>\n )}\n\n {showFilters && filters}\n </section>\n )}\n </header>\n )\n}\n\nconst HeaderPage = Object.assign(HeaderPageRoot, {\n Meta: HeaderPageMeta,\n ToolbarAction: HeaderPageToolbarAction,\n ToolbarPagination: HeaderPageToolbarPagination,\n})\n\nexport { HeaderPage }\nexport default HeaderPage\n",
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"target": ""
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -263,6 +263,16 @@
|
|
|
263
263
|
}
|
|
264
264
|
]
|
|
265
265
|
},
|
|
266
|
+
{
|
|
267
|
+
"name": "entity-icon",
|
|
268
|
+
"type": "registry:ui",
|
|
269
|
+
"files": [
|
|
270
|
+
{
|
|
271
|
+
"path": "json/entity-icon.json",
|
|
272
|
+
"type": "registry:file"
|
|
273
|
+
}
|
|
274
|
+
]
|
|
275
|
+
},
|
|
266
276
|
{
|
|
267
277
|
"name": "field",
|
|
268
278
|
"type": "registry:ui",
|
|
@@ -283,6 +293,16 @@
|
|
|
283
293
|
}
|
|
284
294
|
]
|
|
285
295
|
},
|
|
296
|
+
{
|
|
297
|
+
"name": "header-page",
|
|
298
|
+
"type": "registry:ui",
|
|
299
|
+
"files": [
|
|
300
|
+
{
|
|
301
|
+
"path": "json/header-page.json",
|
|
302
|
+
"type": "registry:file"
|
|
303
|
+
}
|
|
304
|
+
]
|
|
305
|
+
},
|
|
286
306
|
{
|
|
287
307
|
"name": "hover-card",
|
|
288
308
|
"type": "registry:ui",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
import { Card } from "@/components/ui/card"
|
|
6
|
+
|
|
7
|
+
const entityIconVariants = cva(
|
|
8
|
+
"w-fit shrink-0 bg-primary text-primary-foreground [&_svg]:shrink-0",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
size: {
|
|
12
|
+
xs: "p-1 [&_svg]:size-3.5",
|
|
13
|
+
sm: "p-1.5 [&_svg]:size-5",
|
|
14
|
+
md: "p-2 [&_svg]:size-6",
|
|
15
|
+
lg: "p-2 [&_svg]:size-7",
|
|
16
|
+
xl: "p-2.5 [&_svg]:size-8",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: {
|
|
20
|
+
size: "md",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
type IconComponent = React.ComponentType<{
|
|
26
|
+
className?: string
|
|
27
|
+
stroke?: number
|
|
28
|
+
}>
|
|
29
|
+
|
|
30
|
+
function EntityIcon({
|
|
31
|
+
icon: Icon,
|
|
32
|
+
size = "md",
|
|
33
|
+
className,
|
|
34
|
+
iconClassName,
|
|
35
|
+
...props
|
|
36
|
+
}: React.ComponentProps<typeof Card> &
|
|
37
|
+
VariantProps<typeof entityIconVariants> & {
|
|
38
|
+
icon: IconComponent
|
|
39
|
+
iconClassName?: string
|
|
40
|
+
}) {
|
|
41
|
+
return (
|
|
42
|
+
<Card
|
|
43
|
+
data-slot="entity-icon"
|
|
44
|
+
data-size={size}
|
|
45
|
+
className={cn(entityIconVariants({ size }), className)}
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
<Icon className={iconClassName} stroke={1.75} />
|
|
49
|
+
</Card>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { EntityIcon, entityIconVariants }
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type { Icon } from "@tabler/icons-react"
|
|
4
|
+
import type { ReactNode } from "react"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import { EntityIcon } from "@/components/ui/entity-icon"
|
|
8
|
+
|
|
9
|
+
type HeaderPageProps = {
|
|
10
|
+
icon: Icon
|
|
11
|
+
title: string
|
|
12
|
+
description: string
|
|
13
|
+
className?: string
|
|
14
|
+
/** Contenido izquierdo de la barra (filtros, enlaces secundarios, etc.). */
|
|
15
|
+
toolbarStart?: ReactNode
|
|
16
|
+
/** Contenido derecho de la barra (resumen, paginación, acción principal). */
|
|
17
|
+
toolbarEnd?: ReactNode
|
|
18
|
+
/** Panel expandible debajo de la barra (p. ej. filtros avanzados). */
|
|
19
|
+
filters?: ReactNode
|
|
20
|
+
showFilters?: boolean
|
|
21
|
+
toolbarAriaLabel?: string
|
|
22
|
+
toolbarId?: string
|
|
23
|
+
/** Acción principal junto al título (p. ej. crear registro). */
|
|
24
|
+
action?: ReactNode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function HeaderPageMeta({
|
|
28
|
+
children,
|
|
29
|
+
className,
|
|
30
|
+
}: {
|
|
31
|
+
children: ReactNode
|
|
32
|
+
className?: string
|
|
33
|
+
}) {
|
|
34
|
+
return (
|
|
35
|
+
<p
|
|
36
|
+
data-header-meta
|
|
37
|
+
className={cn(
|
|
38
|
+
"text-sm text-muted-foreground max-sm:order-3 max-sm:basis-full max-sm:shrink-0 sm:order-1 xl:order-none",
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</p>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function HeaderPageToolbarAction({
|
|
48
|
+
children,
|
|
49
|
+
className,
|
|
50
|
+
order = "primary",
|
|
51
|
+
}: {
|
|
52
|
+
children: ReactNode
|
|
53
|
+
className?: string
|
|
54
|
+
order?: "primary" | "secondary"
|
|
55
|
+
}) {
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
className={cn(
|
|
59
|
+
"w-full sm:w-auto xl:order-none [&_a]:w-full sm:[&_a]:w-auto [&_button:not([data-icon-only])]:w-full sm:[&_button:not([data-icon-only])]:w-auto",
|
|
60
|
+
order === "primary" && "sm:order-2 sm:ml-auto",
|
|
61
|
+
order === "secondary" && "sm:order-3",
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function HeaderPageToolbarPagination({
|
|
71
|
+
children,
|
|
72
|
+
className,
|
|
73
|
+
}: {
|
|
74
|
+
children: ReactNode
|
|
75
|
+
className?: string
|
|
76
|
+
}) {
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
className={cn(
|
|
80
|
+
"flex items-center justify-center sm:order-3 xl:order-none",
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
{children}
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function HeaderPageRoot({
|
|
90
|
+
icon: Icon,
|
|
91
|
+
title,
|
|
92
|
+
description,
|
|
93
|
+
className,
|
|
94
|
+
toolbarStart,
|
|
95
|
+
toolbarEnd,
|
|
96
|
+
filters,
|
|
97
|
+
showFilters = false,
|
|
98
|
+
toolbarAriaLabel = "Filtros y acciones",
|
|
99
|
+
toolbarId,
|
|
100
|
+
action,
|
|
101
|
+
}: HeaderPageProps) {
|
|
102
|
+
const hasToolbar = Boolean(toolbarStart || toolbarEnd)
|
|
103
|
+
const hasFiltersPanel = Boolean(filters)
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<header className={cn("space-y-4", className)}>
|
|
107
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
108
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
109
|
+
<EntityIcon icon={Icon} size="lg" />
|
|
110
|
+
<div className="min-w-0">
|
|
111
|
+
<h1 className="text-xl font-semibold">{title}</h1>
|
|
112
|
+
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{action ? (
|
|
117
|
+
<div className="flex w-full shrink-0 flex-wrap items-center gap-2 sm:w-auto sm:flex-nowrap sm:justify-end sm:pt-0.5 [&_a:not([data-icon-only])]:w-full sm:[&_a:not([data-icon-only])]:w-auto [&_button:not([data-icon-only])]:w-full sm:[&_button:not([data-icon-only])]:w-auto max-sm:[&>a:not([data-icon-only])]:basis-full max-sm:[&>a:not([data-icon-only])]:shrink-0 max-sm:[&>[data-icon-only]]:order-first max-sm:[&>button:not([data-icon-only])]:basis-full max-sm:[&>button:not([data-icon-only])]:shrink-0">
|
|
118
|
+
{action}
|
|
119
|
+
</div>
|
|
120
|
+
) : null}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{(hasToolbar || hasFiltersPanel) && (
|
|
124
|
+
<section
|
|
125
|
+
id={toolbarId}
|
|
126
|
+
className="space-y-4"
|
|
127
|
+
aria-label={toolbarAriaLabel}
|
|
128
|
+
>
|
|
129
|
+
{hasToolbar && (
|
|
130
|
+
<div
|
|
131
|
+
className={cn(
|
|
132
|
+
"flex flex-col gap-3",
|
|
133
|
+
"max-sm:flex-row max-sm:flex-wrap max-sm:items-center max-sm:gap-2",
|
|
134
|
+
"max-sm:[&_[data-icon-only]]:order-1 max-sm:[&_[data-icon-only]]:shrink-0",
|
|
135
|
+
"max-sm:[&_[data-header-icons]_[data-icon-only]]:order-none",
|
|
136
|
+
"max-sm:[&_.w-full]:order-2 max-sm:[&_.w-full]:basis-full max-sm:[&_.w-full]:shrink-0",
|
|
137
|
+
"max-sm:[&_[data-header-icons]_.w-full]:order-none max-sm:[&_[data-header-icons]_.w-full]:basis-auto max-sm:[&_[data-header-icons]_.w-full]:shrink",
|
|
138
|
+
toolbarStart
|
|
139
|
+
? "xl:flex-row xl:items-center xl:justify-between"
|
|
140
|
+
: "sm:flex-row sm:flex-wrap sm:items-center sm:justify-between xl:justify-end xl:gap-4",
|
|
141
|
+
)}
|
|
142
|
+
>
|
|
143
|
+
{toolbarStart ? (
|
|
144
|
+
<div className="max-sm:contents sm:flex sm:flex-wrap sm:items-center sm:gap-2">
|
|
145
|
+
{toolbarStart}
|
|
146
|
+
</div>
|
|
147
|
+
) : null}
|
|
148
|
+
|
|
149
|
+
{toolbarEnd ? (
|
|
150
|
+
<div className="max-sm:contents sm:flex sm:flex-wrap sm:items-center sm:justify-between sm:gap-3 xl:justify-end xl:gap-4">
|
|
151
|
+
{toolbarEnd}
|
|
152
|
+
</div>
|
|
153
|
+
) : null}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{showFilters && filters}
|
|
158
|
+
</section>
|
|
159
|
+
)}
|
|
160
|
+
</header>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const HeaderPage = Object.assign(HeaderPageRoot, {
|
|
165
|
+
Meta: HeaderPageMeta,
|
|
166
|
+
ToolbarAction: HeaderPageToolbarAction,
|
|
167
|
+
ToolbarPagination: HeaderPageToolbarPagination,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
export { HeaderPage }
|
|
171
|
+
export default HeaderPage
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/utils"
|
|
5
|
+
import { Card } from "./card"
|
|
6
|
+
|
|
7
|
+
const entityIconVariants = cva(
|
|
8
|
+
"w-fit shrink-0 bg-primary text-primary-foreground [&_svg]:shrink-0",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
size: {
|
|
12
|
+
xs: "p-1 [&_svg]:size-3.5",
|
|
13
|
+
sm: "p-1.5 [&_svg]:size-5",
|
|
14
|
+
md: "p-2 [&_svg]:size-6",
|
|
15
|
+
lg: "p-2 [&_svg]:size-7",
|
|
16
|
+
xl: "p-2.5 [&_svg]:size-8",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: {
|
|
20
|
+
size: "md",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
type IconComponent = React.ComponentType<{
|
|
26
|
+
className?: string
|
|
27
|
+
stroke?: number
|
|
28
|
+
}>
|
|
29
|
+
|
|
30
|
+
function EntityIcon({
|
|
31
|
+
icon: Icon,
|
|
32
|
+
size = "md",
|
|
33
|
+
className,
|
|
34
|
+
iconClassName,
|
|
35
|
+
...props
|
|
36
|
+
}: React.ComponentProps<typeof Card> &
|
|
37
|
+
VariantProps<typeof entityIconVariants> & {
|
|
38
|
+
icon: IconComponent
|
|
39
|
+
iconClassName?: string
|
|
40
|
+
}) {
|
|
41
|
+
return (
|
|
42
|
+
<Card
|
|
43
|
+
data-slot="entity-icon"
|
|
44
|
+
data-size={size}
|
|
45
|
+
className={cn(entityIconVariants({ size }), className)}
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
<Icon className={iconClassName} stroke={1.75} />
|
|
49
|
+
</Card>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { EntityIcon, entityIconVariants }
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type { Icon } from "@tabler/icons-react"
|
|
4
|
+
import type { ReactNode } from "react"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
import { EntityIcon } from "./entity-icon"
|
|
8
|
+
|
|
9
|
+
type HeaderPageProps = {
|
|
10
|
+
icon: Icon
|
|
11
|
+
title: string
|
|
12
|
+
description: string
|
|
13
|
+
className?: string
|
|
14
|
+
/** Contenido izquierdo de la barra (filtros, enlaces secundarios, etc.). */
|
|
15
|
+
toolbarStart?: ReactNode
|
|
16
|
+
/** Contenido derecho de la barra (resumen, paginación, acción principal). */
|
|
17
|
+
toolbarEnd?: ReactNode
|
|
18
|
+
/** Panel expandible debajo de la barra (p. ej. filtros avanzados). */
|
|
19
|
+
filters?: ReactNode
|
|
20
|
+
showFilters?: boolean
|
|
21
|
+
toolbarAriaLabel?: string
|
|
22
|
+
toolbarId?: string
|
|
23
|
+
/** Acción principal junto al título (p. ej. crear registro). */
|
|
24
|
+
action?: ReactNode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function HeaderPageMeta({
|
|
28
|
+
children,
|
|
29
|
+
className,
|
|
30
|
+
}: {
|
|
31
|
+
children: ReactNode
|
|
32
|
+
className?: string
|
|
33
|
+
}) {
|
|
34
|
+
return (
|
|
35
|
+
<p
|
|
36
|
+
data-header-meta
|
|
37
|
+
className={cn(
|
|
38
|
+
"text-sm text-muted-foreground max-sm:order-3 max-sm:basis-full max-sm:shrink-0 sm:order-1 xl:order-none",
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</p>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function HeaderPageToolbarAction({
|
|
48
|
+
children,
|
|
49
|
+
className,
|
|
50
|
+
order = "primary",
|
|
51
|
+
}: {
|
|
52
|
+
children: ReactNode
|
|
53
|
+
className?: string
|
|
54
|
+
order?: "primary" | "secondary"
|
|
55
|
+
}) {
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
className={cn(
|
|
59
|
+
"w-full sm:w-auto xl:order-none [&_a]:w-full sm:[&_a]:w-auto [&_button:not([data-icon-only])]:w-full sm:[&_button:not([data-icon-only])]:w-auto",
|
|
60
|
+
order === "primary" && "sm:order-2 sm:ml-auto",
|
|
61
|
+
order === "secondary" && "sm:order-3",
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function HeaderPageToolbarPagination({
|
|
71
|
+
children,
|
|
72
|
+
className,
|
|
73
|
+
}: {
|
|
74
|
+
children: ReactNode
|
|
75
|
+
className?: string
|
|
76
|
+
}) {
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
className={cn(
|
|
80
|
+
"flex items-center justify-center sm:order-3 xl:order-none",
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
{children}
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function HeaderPageRoot({
|
|
90
|
+
icon: Icon,
|
|
91
|
+
title,
|
|
92
|
+
description,
|
|
93
|
+
className,
|
|
94
|
+
toolbarStart,
|
|
95
|
+
toolbarEnd,
|
|
96
|
+
filters,
|
|
97
|
+
showFilters = false,
|
|
98
|
+
toolbarAriaLabel = "Filtros y acciones",
|
|
99
|
+
toolbarId,
|
|
100
|
+
action,
|
|
101
|
+
}: HeaderPageProps) {
|
|
102
|
+
const hasToolbar = Boolean(toolbarStart || toolbarEnd)
|
|
103
|
+
const hasFiltersPanel = Boolean(filters)
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<header className={cn("space-y-4", className)}>
|
|
107
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
108
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
109
|
+
<EntityIcon icon={Icon} size="lg" />
|
|
110
|
+
<div className="min-w-0">
|
|
111
|
+
<h1 className="text-xl font-semibold">{title}</h1>
|
|
112
|
+
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{action ? (
|
|
117
|
+
<div className="flex w-full shrink-0 flex-wrap items-center gap-2 sm:w-auto sm:flex-nowrap sm:justify-end sm:pt-0.5 [&_a:not([data-icon-only])]:w-full sm:[&_a:not([data-icon-only])]:w-auto [&_button:not([data-icon-only])]:w-full sm:[&_button:not([data-icon-only])]:w-auto max-sm:[&>a:not([data-icon-only])]:basis-full max-sm:[&>a:not([data-icon-only])]:shrink-0 max-sm:[&>[data-icon-only]]:order-first max-sm:[&>button:not([data-icon-only])]:basis-full max-sm:[&>button:not([data-icon-only])]:shrink-0">
|
|
118
|
+
{action}
|
|
119
|
+
</div>
|
|
120
|
+
) : null}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{(hasToolbar || hasFiltersPanel) && (
|
|
124
|
+
<section
|
|
125
|
+
id={toolbarId}
|
|
126
|
+
className="space-y-4"
|
|
127
|
+
aria-label={toolbarAriaLabel}
|
|
128
|
+
>
|
|
129
|
+
{hasToolbar && (
|
|
130
|
+
<div
|
|
131
|
+
className={cn(
|
|
132
|
+
"flex flex-col gap-3",
|
|
133
|
+
"max-sm:flex-row max-sm:flex-wrap max-sm:items-center max-sm:gap-2",
|
|
134
|
+
"max-sm:[&_[data-icon-only]]:order-1 max-sm:[&_[data-icon-only]]:shrink-0",
|
|
135
|
+
"max-sm:[&_[data-header-icons]_[data-icon-only]]:order-none",
|
|
136
|
+
"max-sm:[&_.w-full]:order-2 max-sm:[&_.w-full]:basis-full max-sm:[&_.w-full]:shrink-0",
|
|
137
|
+
"max-sm:[&_[data-header-icons]_.w-full]:order-none max-sm:[&_[data-header-icons]_.w-full]:basis-auto max-sm:[&_[data-header-icons]_.w-full]:shrink",
|
|
138
|
+
toolbarStart
|
|
139
|
+
? "xl:flex-row xl:items-center xl:justify-between"
|
|
140
|
+
: "sm:flex-row sm:flex-wrap sm:items-center sm:justify-between xl:justify-end xl:gap-4",
|
|
141
|
+
)}
|
|
142
|
+
>
|
|
143
|
+
{toolbarStart ? (
|
|
144
|
+
<div className="max-sm:contents sm:flex sm:flex-wrap sm:items-center sm:gap-2">
|
|
145
|
+
{toolbarStart}
|
|
146
|
+
</div>
|
|
147
|
+
) : null}
|
|
148
|
+
|
|
149
|
+
{toolbarEnd ? (
|
|
150
|
+
<div className="max-sm:contents sm:flex sm:flex-wrap sm:items-center sm:justify-between sm:gap-3 xl:justify-end xl:gap-4">
|
|
151
|
+
{toolbarEnd}
|
|
152
|
+
</div>
|
|
153
|
+
) : null}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{showFilters && filters}
|
|
158
|
+
</section>
|
|
159
|
+
)}
|
|
160
|
+
</header>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const HeaderPage = Object.assign(HeaderPageRoot, {
|
|
165
|
+
Meta: HeaderPageMeta,
|
|
166
|
+
ToolbarAction: HeaderPageToolbarAction,
|
|
167
|
+
ToolbarPagination: HeaderPageToolbarPagination,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
export { HeaderPage }
|
|
171
|
+
export default HeaderPage
|