@dataif/cli 0.1.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.
Files changed (183) hide show
  1. package/README.md +16 -0
  2. package/bin/dataif.js +623 -0
  3. package/package.json +26 -0
  4. package/scripts/build-template.mjs +72 -0
  5. package/templates/dataif/README.md +157 -0
  6. package/templates/dataif/infra/.env.example +119 -0
  7. package/templates/dataif/infra/.env.stg.example +119 -0
  8. package/templates/dataif/infra/airflow/Dockerfile +11 -0
  9. package/templates/dataif/infra/airflow/Dockerfile.release +17 -0
  10. package/templates/dataif/infra/airflow/requirements.txt +3 -0
  11. package/templates/dataif/infra/docker-compose.yml +306 -0
  12. package/templates/dataif/infra/init-db/01-init-dataif.sh +129 -0
  13. package/templates/dataif/infra/init-db/pnp-curated-views.sqlinc +444 -0
  14. package/templates/dataif/infra/init-db/pnp-raw-staging-curated.sqlinc +701 -0
  15. package/templates/dataif/infra/keycloak/Dockerfile +4 -0
  16. package/templates/dataif/infra/keycloak/realm-dataif.json +73 -0
  17. package/templates/dataif/infra/ollama/Dockerfile +9 -0
  18. package/templates/dataif/infra/ollama/bootstrap-model.sh +100 -0
  19. package/templates/dataif/infra/ollama/sabia-7b.Modelfile +14 -0
  20. package/templates/dataif/infra/postgres/Dockerfile +4 -0
  21. package/templates/dataif/pipelines/airflow/dags/generated/.gitkeep +1 -0
  22. package/templates/dataif/pipelines/airflow/dags/generated/2020_financeiro_fcc6f1f3_sync.py +9 -0
  23. package/templates/dataif/pipelines/dataif_pipelines/__init__.py +1 -0
  24. package/templates/dataif/pipelines/dataif_pipelines/airflow/__init__.py +1 -0
  25. package/templates/dataif/pipelines/dataif_pipelines/airflow/pnp_pipeline_factory.py +167 -0
  26. package/templates/dataif/pipelines/dataif_pipelines/connectors/__init__.py +1 -0
  27. package/templates/dataif/pipelines/dataif_pipelines/connectors/base/__init__.py +1 -0
  28. package/templates/dataif/pipelines/dataif_pipelines/connectors/base/connector.py +28 -0
  29. package/templates/dataif/pipelines/dataif_pipelines/connectors/base/types.py +14 -0
  30. package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/__init__.py +1 -0
  31. package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/config.py +19 -0
  32. package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/connector.py +558 -0
  33. package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/powerbi_microdados.py +728 -0
  34. package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/transform.py +296 -0
  35. package/templates/dataif/pipelines/dataif_pipelines/jobs/__init__.py +1 -0
  36. package/templates/dataif/pipelines/dataif_pipelines/jobs/nilo_pipeline.py +112 -0
  37. package/templates/dataif/pipelines/dataif_pipelines/orchestration/__init__.py +21 -0
  38. package/templates/dataif/pipelines/dataif_pipelines/orchestration/pnp_workflow.py +783 -0
  39. package/templates/dataif/pipelines/dataif_pipelines/repositories/__init__.py +1 -0
  40. package/templates/dataif/pipelines/dataif_pipelines/repositories/pnp_raw_repository.py +860 -0
  41. package/templates/dataif/pipelines/dataif_pipelines/services/__init__.py +19 -0
  42. package/templates/dataif/pipelines/dataif_pipelines/services/pnp_curated_service.py +66 -0
  43. package/templates/dataif/pipelines/dataif_pipelines/services/pnp_download_service.py +534 -0
  44. package/templates/dataif/pipelines/dataif_pipelines/services/pnp_quality_service.py +9 -0
  45. package/templates/dataif/pipelines/dataif_pipelines/services/pnp_raw_ingestion_service.py +124 -0
  46. package/templates/dataif/pipelines/dataif_pipelines/services/pnp_staging_service.py +271 -0
  47. package/templates/dataif/pipelines/dataif_pipelines/services/powerbi_catalog_service.py +159 -0
  48. package/templates/dataif/pipelines/sql/staging/020_pnp_matriculas.sql +112 -0
  49. package/templates/dataif/pipelines/sql/staging/030_pnp_eficiencia_academica.sql +83 -0
  50. package/templates/dataif/pipelines/sql/staging/040_pnp_servidores.sql +90 -0
  51. package/templates/dataif/pipelines/sql/staging/050_pnp_financeiro.sql +72 -0
  52. package/templates/dataif/pipelines/sql/views_curated/004_mv_pnp_dashboard_fast.sql +204 -0
  53. package/templates/dataif/pipelines/sql/views_curated/010_vw_pnp_admin_ingestao.sql +51 -0
  54. package/templates/dataif/pipelines/sql/views_curated/020_vw_pnp_qualidade_dados.sql +114 -0
  55. package/templates/dataif/pipelines/sql/views_curated/030_vw_pnp_matriculas.sql +67 -0
  56. package/templates/dataif/pipelines/sql/views_curated/040_vw_pnp_eficiencia.sql +33 -0
  57. package/templates/dataif/pipelines/sql/views_curated/050_vw_pnp_servidores.sql +30 -0
  58. package/templates/dataif/pipelines/sql/views_curated/060_vw_pnp_financeiro.sql +22 -0
  59. package/templates/dataif/pipelines/sql/views_curated/070_vw_pnp_vanna.sql +115 -0
  60. package/templates/dataif/scripts/configure-env.sh +149 -0
  61. package/templates/dataif/scripts/create_metabase_pnp_dashboard.py +943 -0
  62. package/templates/dataif/scripts/create_metabase_pnp_matriculas_dashboard.py +580 -0
  63. package/templates/dataif/scripts/deploy.sh +79 -0
  64. package/templates/dataif/scripts/fix_metabase_template_tag_ids.py +91 -0
  65. package/templates/dataif/scripts/pnp_powerbi_microdados_probe.py +14 -0
  66. package/templates/dataif/scripts/pnp_validate_raw_run.py +330 -0
  67. package/templates/dataif/scripts/publish-images.sh +31 -0
  68. package/templates/dataif/scripts/sync_metabase_dashboard_field_filters.py +241 -0
  69. package/templates/dataif/scripts/use-vanna-ollama.sh +139 -0
  70. package/templates/dataif/services/api/.dockerignore +18 -0
  71. package/templates/dataif/services/api/Dockerfile +12 -0
  72. package/templates/dataif/services/api/app/__init__.py +1 -0
  73. package/templates/dataif/services/api/app/auth.py +48 -0
  74. package/templates/dataif/services/api/app/config.py +59 -0
  75. package/templates/dataif/services/api/app/keycloak_admin.py +215 -0
  76. package/templates/dataif/services/api/app/main.py +2432 -0
  77. package/templates/dataif/services/api/app/metabase_admin.py +191 -0
  78. package/templates/dataif/services/api/app/metabase_bootstrap.py +44 -0
  79. package/templates/dataif/services/api/app/metabase_embed.py +15 -0
  80. package/templates/dataif/services/api/app/pnp_dag_provisioner.py +113 -0
  81. package/templates/dataif/services/api/app/pnp_instance_repository.py +951 -0
  82. package/templates/dataif/services/api/app/pnp_powerbi.py +438 -0
  83. package/templates/dataif/services/api/app/vanna_client.py +32 -0
  84. package/templates/dataif/services/api/requirements.txt +9 -0
  85. package/templates/dataif/services/vanna/.dockerignore +18 -0
  86. package/templates/dataif/services/vanna/Dockerfile +12 -0
  87. package/templates/dataif/services/vanna/app/config.py +57 -0
  88. package/templates/dataif/services/vanna/app/main.py +108 -0
  89. package/templates/dataif/services/vanna/app/runtime_config.py +114 -0
  90. package/templates/dataif/services/vanna/app/sql_guard.py +123 -0
  91. package/templates/dataif/services/vanna/app/vanna_engine.py +382 -0
  92. package/templates/dataif/services/vanna/requirements.txt +8 -0
  93. package/templates/dataif/services/web/.dockerignore +13 -0
  94. package/templates/dataif/services/web/Dockerfile +16 -0
  95. package/templates/dataif/services/web/index.html +12 -0
  96. package/templates/dataif/services/web/nginx.conf +74 -0
  97. package/templates/dataif/services/web/package-lock.json +4397 -0
  98. package/templates/dataif/services/web/package.json +32 -0
  99. package/templates/dataif/services/web/postcss.config.mjs +5 -0
  100. package/templates/dataif/services/web/src/App.jsx +2817 -0
  101. package/templates/dataif/services/web/src/adminAuth.js +245 -0
  102. package/templates/dataif/services/web/src/assets/avatar_placeholder.png +0 -0
  103. package/templates/dataif/services/web/src/assets/github_logo_icon_229278.svg +1 -0
  104. package/templates/dataif/services/web/src/assets/if-logo.png +0 -0
  105. package/templates/dataif/services/web/src/assets/if.svg +0 -0
  106. package/templates/dataif/services/web/src/assets/pnp-horizontal.svg +1 -0
  107. package/templates/dataif/services/web/src/components/AppHeader.jsx +233 -0
  108. package/templates/dataif/services/web/src/components/application/app-navigation/base-components/mobile-header.tsx +56 -0
  109. package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-account-card.tsx +209 -0
  110. package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-item-button.tsx +67 -0
  111. package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-item.tsx +108 -0
  112. package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-list.tsx +83 -0
  113. package/templates/dataif/services/web/src/components/application/app-navigation/config.ts +23 -0
  114. package/templates/dataif/services/web/src/components/application/app-navigation/header-navigation.tsx +240 -0
  115. package/templates/dataif/services/web/src/components/application/pagination/pagination-base.tsx +376 -0
  116. package/templates/dataif/services/web/src/components/application/pagination/pagination-dot.tsx +52 -0
  117. package/templates/dataif/services/web/src/components/application/pagination/pagination-line.tsx +48 -0
  118. package/templates/dataif/services/web/src/components/application/pagination/pagination.tsx +328 -0
  119. package/templates/dataif/services/web/src/components/application/tabs/tabs.tsx +223 -0
  120. package/templates/dataif/services/web/src/components/base/avatar/avatar-label-group.tsx +28 -0
  121. package/templates/dataif/services/web/src/components/base/avatar/avatar.tsx +129 -0
  122. package/templates/dataif/services/web/src/components/base/avatar/base-components/avatar-add-button.tsx +32 -0
  123. package/templates/dataif/services/web/src/components/base/avatar/base-components/avatar-company-icon.tsx +24 -0
  124. package/templates/dataif/services/web/src/components/base/avatar/base-components/avatar-online-indicator.tsx +29 -0
  125. package/templates/dataif/services/web/src/components/base/avatar/base-components/index.tsx +4 -0
  126. package/templates/dataif/services/web/src/components/base/avatar/base-components/verified-tick.tsx +32 -0
  127. package/templates/dataif/services/web/src/components/base/badges/badge-types.ts +264 -0
  128. package/templates/dataif/services/web/src/components/base/badges/badges.tsx +415 -0
  129. package/templates/dataif/services/web/src/components/base/button-group/button-group.tsx +104 -0
  130. package/templates/dataif/services/web/src/components/base/buttons/button.tsx +267 -0
  131. package/templates/dataif/services/web/src/components/base/input/hint-text.tsx +31 -0
  132. package/templates/dataif/services/web/src/components/base/input/input.tsx +269 -0
  133. package/templates/dataif/services/web/src/components/base/input/label.tsx +48 -0
  134. package/templates/dataif/services/web/src/components/base/radio-buttons/radio-buttons.tsx +127 -0
  135. package/templates/dataif/services/web/src/components/base/select/combobox.tsx +150 -0
  136. package/templates/dataif/services/web/src/components/base/select/multi-select.tsx +361 -0
  137. package/templates/dataif/services/web/src/components/base/select/popover.tsx +32 -0
  138. package/templates/dataif/services/web/src/components/base/select/select-item.tsx +95 -0
  139. package/templates/dataif/services/web/src/components/base/select/select-native.tsx +67 -0
  140. package/templates/dataif/services/web/src/components/base/select/select.tsx +144 -0
  141. package/templates/dataif/services/web/src/components/base/tags/base-components/tag-close-x.tsx +32 -0
  142. package/templates/dataif/services/web/src/components/base/tooltip/tooltip.tsx +107 -0
  143. package/templates/dataif/services/web/src/components/foundations/dot-icon.tsx +22 -0
  144. package/templates/dataif/services/web/src/components/foundations/logo/untitledui-logo-minimal.tsx +170 -0
  145. package/templates/dataif/services/web/src/components/foundations/logo/untitledui-logo.tsx +58 -0
  146. package/templates/dataif/services/web/src/hooks/use-breakpoint.ts +34 -0
  147. package/templates/dataif/services/web/src/hooks/use-resize-observer.ts +67 -0
  148. package/templates/dataif/services/web/src/main.jsx +14 -0
  149. package/templates/dataif/services/web/src/providers/theme-provider.jsx +62 -0
  150. package/templates/dataif/services/web/src/styles/globals.css +60 -0
  151. package/templates/dataif/services/web/src/styles/theme.css +1326 -0
  152. package/templates/dataif/services/web/src/styles/typography.css +430 -0
  153. package/templates/dataif/services/web/src/styles.css +1287 -0
  154. package/templates/dataif/services/web/src/utils/cx.ts +24 -0
  155. package/templates/dataif/services/web/src/utils/is-react-component.ts +33 -0
  156. package/templates/dataif/services/web/vite.config.js +14 -0
  157. package/templates/dataif/sql/ddl/001_schemas.sql +6 -0
  158. package/templates/dataif/sql/ddl/003_pnp_raw_staging_curated.sql +699 -0
  159. package/templates/dataif/sql/migrations/001_pnp_phase1_backfill.sql +3 -0
  160. package/templates/dataif/sql/migrations/002_pnp_phase2_admin_config_backfill.sql +184 -0
  161. package/templates/dataif/sql/migrations/003_pnp_phase3_raw_tabular_backfill.sql +3 -0
  162. package/templates/dataif/sql/migrations/004_pnp_phase3_raw_backfill_support_index.sql +3 -0
  163. package/templates/dataif/sql/migrations/005_pnp_phase7_staging_support_indexes.sql +2 -0
  164. package/templates/dataif/sql/migrations/006_pnp_phase7_staging_autovacuum_tuning.sql +2 -0
  165. package/templates/dataif/sql/migrations/007_pnp_phase7b_run_packages.sql +20 -0
  166. package/templates/dataif/sql/migrations/008_pnp_phase7a_pipeline_endpoints.sql +169 -0
  167. package/templates/dataif/sql/migrations/009_pnp_phase8_curated.sql +35 -0
  168. package/templates/dataif/sql/migrations/010_pnp_phase10_staging_incremental_upsert.sql +3 -0
  169. package/templates/dataif/sql/migrations/010_pnp_pipeline_uuid.sql +51 -0
  170. package/templates/dataif/sql/migrations/011_app_settings.sql +7 -0
  171. package/templates/dataif/sql/staging/020_pnp_matriculas.sql +112 -0
  172. package/templates/dataif/sql/staging/030_pnp_eficiencia_academica.sql +83 -0
  173. package/templates/dataif/sql/staging/040_pnp_servidores.sql +90 -0
  174. package/templates/dataif/sql/staging/050_pnp_financeiro.sql +72 -0
  175. package/templates/dataif/sql/views_curated/003_vw_pnp_microdados_admin.sql +160 -0
  176. package/templates/dataif/sql/views_curated/004_mv_pnp_dashboard_fast.sql +204 -0
  177. package/templates/dataif/sql/views_curated/010_vw_pnp_admin_ingestao.sql +51 -0
  178. package/templates/dataif/sql/views_curated/020_vw_pnp_qualidade_dados.sql +114 -0
  179. package/templates/dataif/sql/views_curated/030_vw_pnp_matriculas.sql +67 -0
  180. package/templates/dataif/sql/views_curated/040_vw_pnp_eficiencia.sql +33 -0
  181. package/templates/dataif/sql/views_curated/050_vw_pnp_servidores.sql +30 -0
  182. package/templates/dataif/sql/views_curated/060_vw_pnp_financeiro.sql +22 -0
  183. package/templates/dataif/sql/views_curated/070_vw_pnp_vanna.sql +115 -0
@@ -0,0 +1,328 @@
1
+ import { ArrowLeft, ArrowRight } from "@untitledui/icons";
2
+ import { ButtonGroup, ButtonGroupItem } from "@/components/base/button-group/button-group";
3
+ import { Button } from "@/components/base/buttons/button";
4
+ import { useBreakpoint } from "@/hooks/use-breakpoint";
5
+ import { cx } from "@/utils/cx";
6
+ import type { PaginationRootProps } from "./pagination-base";
7
+ import { Pagination } from "./pagination-base";
8
+
9
+ interface PaginationProps extends Partial<Omit<PaginationRootProps, "children">> {
10
+ /** Whether the pagination buttons are rounded. */
11
+ rounded?: boolean;
12
+ }
13
+
14
+ const PaginationItem = ({ value, rounded, isCurrent }: { value: number; rounded?: boolean; isCurrent: boolean }) => {
15
+ return (
16
+ <Pagination.Item
17
+ value={value}
18
+ isCurrent={isCurrent}
19
+ className={({ isSelected }) =>
20
+ cx(
21
+ "flex size-10 cursor-pointer items-center justify-center p-3 text-sm font-medium text-quaternary outline-focus-ring transition duration-100 ease-linear hover:bg-primary_hover hover:text-secondary focus-visible:z-10 focus-visible:bg-primary_hover focus-visible:outline-2 focus-visible:outline-offset-2",
22
+ rounded ? "rounded-full" : "rounded-lg",
23
+ isSelected && "bg-primary_hover text-secondary",
24
+ )
25
+ }
26
+ >
27
+ {value}
28
+ </Pagination.Item>
29
+ );
30
+ };
31
+
32
+ interface MobilePaginationProps {
33
+ /** The current page. */
34
+ page?: number;
35
+ /** The total number of pages. */
36
+ total?: number;
37
+ /** The class name of the pagination component. */
38
+ className?: string;
39
+ /** The function to call when the page changes. */
40
+ onPageChange?: (page: number) => void;
41
+ }
42
+
43
+ const MobilePagination = ({ page = 1, total = 10, className, onPageChange }: MobilePaginationProps) => {
44
+ return (
45
+ <nav aria-label="Pagination" className={cx("flex items-center justify-between md:hidden", className)}>
46
+ <Button
47
+ aria-label="Go to previous page"
48
+ iconLeading={ArrowLeft}
49
+ color="secondary"
50
+ size="sm"
51
+ onClick={() => onPageChange?.(Math.max(0, page - 1))}
52
+ />
53
+
54
+ <span className="text-sm text-fg-secondary">
55
+ Page <span className="font-medium">{page}</span> of <span className="font-medium">{total}</span>
56
+ </span>
57
+
58
+ <Button
59
+ aria-label="Go to next page"
60
+ iconLeading={ArrowRight}
61
+ color="secondary"
62
+ size="sm"
63
+ onClick={() => onPageChange?.(Math.min(total, page + 1))}
64
+ />
65
+ </nav>
66
+ );
67
+ };
68
+
69
+ export const PaginationPageDefault = ({ rounded, page = 1, total = 10, className, ...props }: PaginationProps) => {
70
+ const isDesktop = useBreakpoint("md");
71
+
72
+ return (
73
+ <Pagination.Root
74
+ {...props}
75
+ page={page}
76
+ total={total}
77
+ className={cx("flex w-full items-center justify-between gap-3 border-t border-secondary pt-4 md:pt-5", className)}
78
+ >
79
+ <div className="hidden flex-1 justify-start md:flex">
80
+ <Pagination.PrevTrigger asChild>
81
+ <Button iconLeading={ArrowLeft} color="link-gray" size="sm">
82
+ {isDesktop ? "Previous" : undefined}{" "}
83
+ </Button>
84
+ </Pagination.PrevTrigger>
85
+ </div>
86
+
87
+ <Pagination.PrevTrigger asChild className="md:hidden">
88
+ <Button iconLeading={ArrowLeft} color="secondary" size="sm">
89
+ {isDesktop ? "Previous" : undefined}
90
+ </Button>
91
+ </Pagination.PrevTrigger>
92
+
93
+ <Pagination.Context>
94
+ {({ pages, currentPage, total }) => (
95
+ <>
96
+ <div className="hidden justify-center gap-0.5 md:flex">
97
+ {pages.map((page, index) =>
98
+ page.type === "page" ? (
99
+ <PaginationItem key={index} rounded={rounded} {...page} />
100
+ ) : (
101
+ <Pagination.Ellipsis key={index} className="flex size-10 shrink-0 items-center justify-center text-tertiary">
102
+ &#8230;
103
+ </Pagination.Ellipsis>
104
+ ),
105
+ )}
106
+ </div>
107
+
108
+ <div className="flex justify-center text-sm whitespace-pre text-fg-secondary md:hidden">
109
+ Page <span className="font-medium">{currentPage}</span> of <span className="font-medium">{total}</span>
110
+ </div>
111
+ </>
112
+ )}
113
+ </Pagination.Context>
114
+
115
+ <div className="hidden flex-1 justify-end md:flex">
116
+ <Pagination.NextTrigger asChild>
117
+ <Button iconTrailing={ArrowRight} color="link-gray" size="sm">
118
+ {isDesktop ? "Next" : undefined}
119
+ </Button>
120
+ </Pagination.NextTrigger>
121
+ </div>
122
+ <Pagination.NextTrigger asChild className="md:hidden">
123
+ <Button iconTrailing={ArrowRight} color="secondary" size="sm">
124
+ {isDesktop ? "Next" : undefined}
125
+ </Button>
126
+ </Pagination.NextTrigger>
127
+ </Pagination.Root>
128
+ );
129
+ };
130
+
131
+ export const PaginationPageMinimalCenter = ({ rounded, page = 1, total = 10, className, ...props }: PaginationProps) => {
132
+ const isDesktop = useBreakpoint("md");
133
+
134
+ return (
135
+ <Pagination.Root
136
+ {...props}
137
+ page={page}
138
+ total={total}
139
+ className={cx("flex w-full items-center justify-between gap-3 border-t border-secondary pt-4 md:pt-5", className)}
140
+ >
141
+ <div className="flex flex-1 justify-start">
142
+ <Pagination.PrevTrigger asChild>
143
+ <Button iconLeading={ArrowLeft} color="secondary" size="sm">
144
+ {isDesktop ? "Previous" : undefined}
145
+ </Button>
146
+ </Pagination.PrevTrigger>
147
+ </div>
148
+
149
+ <Pagination.Context>
150
+ {({ pages, currentPage, total }) => (
151
+ <>
152
+ <div className="hidden justify-center gap-0.5 md:flex">
153
+ {pages.map((page, index) =>
154
+ page.type === "page" ? (
155
+ <PaginationItem key={index} rounded={rounded} {...page} />
156
+ ) : (
157
+ <Pagination.Ellipsis key={index} className="flex size-10 shrink-0 items-center justify-center text-tertiary">
158
+ &#8230;
159
+ </Pagination.Ellipsis>
160
+ ),
161
+ )}
162
+ </div>
163
+
164
+ <div className="flex justify-center text-sm whitespace-pre text-fg-secondary md:hidden">
165
+ Page <span className="font-medium">{currentPage}</span> of <span className="font-medium">{total}</span>
166
+ </div>
167
+ </>
168
+ )}
169
+ </Pagination.Context>
170
+
171
+ <div className="flex flex-1 justify-end">
172
+ <Pagination.NextTrigger asChild>
173
+ <Button iconTrailing={ArrowRight} color="secondary" size="sm">
174
+ {isDesktop ? "Next" : undefined}
175
+ </Button>
176
+ </Pagination.NextTrigger>
177
+ </div>
178
+ </Pagination.Root>
179
+ );
180
+ };
181
+
182
+ export const PaginationCardDefault = ({ rounded, page = 1, total = 10, ...props }: PaginationProps) => {
183
+ const isDesktop = useBreakpoint("md");
184
+
185
+ return (
186
+ <Pagination.Root
187
+ {...props}
188
+ page={page}
189
+ total={total}
190
+ className="flex w-full items-center justify-between gap-3 border-t border-secondary px-4 py-3 md:px-6 md:pt-3 md:pb-4"
191
+ >
192
+ <div className="flex flex-1 justify-start">
193
+ <Pagination.PrevTrigger asChild>
194
+ <Button iconLeading={ArrowLeft} color="secondary" size="sm">
195
+ {isDesktop ? "Previous" : undefined}
196
+ </Button>
197
+ </Pagination.PrevTrigger>
198
+ </div>
199
+
200
+ <Pagination.Context>
201
+ {({ pages, currentPage, total }) => (
202
+ <>
203
+ <div className="hidden justify-center gap-0.5 md:flex">
204
+ {pages.map((page, index) =>
205
+ page.type === "page" ? (
206
+ <PaginationItem key={index} rounded={rounded} {...page} />
207
+ ) : (
208
+ <Pagination.Ellipsis key={index} className="flex size-10 shrink-0 items-center justify-center text-tertiary">
209
+ &#8230;
210
+ </Pagination.Ellipsis>
211
+ ),
212
+ )}
213
+ </div>
214
+
215
+ <div className="flex justify-center text-sm whitespace-pre text-fg-secondary md:hidden">
216
+ Page <span className="font-medium">{currentPage}</span> of <span className="font-medium">{total}</span>
217
+ </div>
218
+ </>
219
+ )}
220
+ </Pagination.Context>
221
+
222
+ <div className="flex flex-1 justify-end">
223
+ <Pagination.NextTrigger asChild>
224
+ <Button iconTrailing={ArrowRight} color="secondary" size="sm">
225
+ {isDesktop ? "Next" : undefined}
226
+ </Button>
227
+ </Pagination.NextTrigger>
228
+ </div>
229
+ </Pagination.Root>
230
+ );
231
+ };
232
+
233
+ interface PaginationCardMinimalProps {
234
+ /** The current page. */
235
+ page?: number;
236
+ /** The total number of pages. */
237
+ total?: number;
238
+ /** The alignment of the pagination. */
239
+ align?: "left" | "center" | "right";
240
+ /** The class name of the pagination component. */
241
+ className?: string;
242
+ /** The function to call when the page changes. */
243
+ onPageChange?: (page: number) => void;
244
+ }
245
+
246
+ export const PaginationCardMinimal = ({ page = 1, total = 10, align = "left", onPageChange, className }: PaginationCardMinimalProps) => {
247
+ return (
248
+ <div className={cx("border-t border-secondary px-4 py-3 md:px-6 md:pt-3 md:pb-4", className)}>
249
+ <MobilePagination page={page} total={total} onPageChange={onPageChange} />
250
+
251
+ <nav aria-label="Pagination" className={cx("hidden items-center gap-3 md:flex", align === "center" && "justify-between")}>
252
+ <div className={cx(align === "center" && "flex flex-1 justify-start")}>
253
+ <Button isDisabled={page === 1} color="secondary" size="sm" onClick={() => onPageChange?.(Math.max(0, page - 1))}>
254
+ Previous
255
+ </Button>
256
+ </div>
257
+
258
+ <span
259
+ className={cx(
260
+ "text-sm font-medium text-fg-secondary",
261
+ align === "right" && "order-first mr-auto",
262
+ align === "left" && "order-last ml-auto",
263
+ )}
264
+ >
265
+ Page {page} of {total}
266
+ </span>
267
+
268
+ <div className={cx(align === "center" && "flex flex-1 justify-end")}>
269
+ <Button isDisabled={page === total} color="secondary" size="sm" onClick={() => onPageChange?.(Math.min(total, page + 1))}>
270
+ Next
271
+ </Button>
272
+ </div>
273
+ </nav>
274
+ </div>
275
+ );
276
+ };
277
+
278
+ interface PaginationButtonGroupProps extends Partial<Omit<PaginationRootProps, "children">> {
279
+ /** The alignment of the pagination. */
280
+ align?: "left" | "center" | "right";
281
+ }
282
+
283
+ export const PaginationButtonGroup = ({ align = "left", page = 1, total = 10, ...props }: PaginationButtonGroupProps) => {
284
+ const isDesktop = useBreakpoint("md");
285
+
286
+ return (
287
+ <div
288
+ className={cx(
289
+ "flex border-t border-secondary px-4 py-3 md:px-6 md:pt-3 md:pb-4",
290
+ align === "left" && "justify-start",
291
+ align === "center" && "justify-center",
292
+ align === "right" && "justify-end",
293
+ )}
294
+ >
295
+ <Pagination.Root {...props} page={page} total={total}>
296
+ <Pagination.Context>
297
+ {({ pages }) => (
298
+ <ButtonGroup size="md">
299
+ <Pagination.PrevTrigger asChild>
300
+ <ButtonGroupItem iconLeading={ArrowLeft}>{isDesktop ? "Previous" : undefined}</ButtonGroupItem>
301
+ </Pagination.PrevTrigger>
302
+
303
+ {pages.map((page, index) =>
304
+ page.type === "page" ? (
305
+ <Pagination.Item key={index} {...page} asChild>
306
+ <ButtonGroupItem isSelected={page.isCurrent} className="size-10 items-center justify-center">
307
+ {page.value}
308
+ </ButtonGroupItem>
309
+ </Pagination.Item>
310
+ ) : (
311
+ <Pagination.Ellipsis key={index}>
312
+ <ButtonGroupItem className="pointer-events-none size-10 items-center justify-center rounded-none!">
313
+ &#8230;
314
+ </ButtonGroupItem>
315
+ </Pagination.Ellipsis>
316
+ ),
317
+ )}
318
+
319
+ <Pagination.NextTrigger asChild>
320
+ <ButtonGroupItem iconTrailing={ArrowRight}>{isDesktop ? "Next" : undefined}</ButtonGroupItem>
321
+ </Pagination.NextTrigger>
322
+ </ButtonGroup>
323
+ )}
324
+ </Pagination.Context>
325
+ </Pagination.Root>
326
+ </div>
327
+ );
328
+ };
@@ -0,0 +1,223 @@
1
+ import type { ComponentPropsWithRef, ReactNode } from "react";
2
+ import { Fragment, createContext, useContext } from "react";
3
+ import type { TabListProps as AriaTabListProps, TabProps as AriaTabProps, TabRenderProps as AriaTabRenderProps } from "react-aria-components";
4
+ import { Tab as AriaTab, TabList as AriaTabList, TabPanel as AriaTabPanel, Tabs as AriaTabs, TabsContext, useSlottedContext } from "react-aria-components";
5
+ import type { BadgeColors } from "@/components/base/badges/badge-types";
6
+ import { Badge } from "@/components/base/badges/badges";
7
+ import { cx } from "@/utils/cx";
8
+
9
+ type Orientation = "horizontal" | "vertical";
10
+
11
+ // Types for different orientations
12
+ type HorizontalTypes = "button-brand" | "button-gray" | "button-border" | "button-minimal" | "underline";
13
+ type VerticalTypes = "button-brand" | "button-gray" | "button-border" | "button-minimal" | "line";
14
+ type TabTypeColors<T> = T extends "horizontal" ? HorizontalTypes : VerticalTypes;
15
+
16
+ // Styles for different types of tab
17
+ const getTabStyles = ({ isFocusVisible, isSelected, isHovered }: AriaTabRenderProps) => ({
18
+ "button-brand": cx(
19
+ "outline-focus-ring",
20
+ isFocusVisible && "outline-2 -outline-offset-2",
21
+ (isSelected || isHovered) && "bg-brand-primary_alt text-brand-secondary",
22
+ ),
23
+ "button-gray": cx(
24
+ "outline-focus-ring",
25
+ isHovered && "bg-primary_hover text-secondary",
26
+ isFocusVisible && "outline-2 -outline-offset-2",
27
+ isSelected && "bg-active text-secondary",
28
+ ),
29
+ "button-border": cx(
30
+ "outline-focus-ring",
31
+ (isSelected || isHovered) && "bg-primary_alt text-secondary shadow-sm",
32
+ isFocusVisible && "outline-2 -outline-offset-2",
33
+ ),
34
+ "button-minimal": cx(
35
+ "rounded-lg outline-focus-ring",
36
+ isHovered && "text-secondary",
37
+ isFocusVisible && "outline-2 -outline-offset-2",
38
+ isSelected && "bg-primary_alt text-secondary shadow-xs ring-1 ring-primary ring-inset",
39
+ ),
40
+ underline: cx(
41
+ "rounded-none border-b-2 border-transparent outline-focus-ring",
42
+ (isSelected || isHovered) && "border-fg-brand-primary_alt text-brand-secondary",
43
+ isFocusVisible && "outline-2 -outline-offset-2",
44
+ ),
45
+ line: cx(
46
+ "rounded-none border-l-2 border-transparent outline-focus-ring",
47
+ (isSelected || isHovered) && "border-fg-brand-primary_alt text-brand-secondary",
48
+ isFocusVisible && "outline-2 -outline-offset-2",
49
+ ),
50
+ });
51
+
52
+ const sizes = {
53
+ sm: {
54
+ "button-brand": "text-sm font-semibold py-2 px-3",
55
+ "button-gray": "text-sm font-semibold py-2 px-3",
56
+ "button-border": "text-sm font-semibold py-2 px-3",
57
+ "button-minimal": "text-sm font-semibold py-2 px-3",
58
+ underline: "text-sm font-semibold px-1 pb-2.5 pt-0",
59
+ line: "text-sm font-semibold pl-2.5 pr-3 py-0.5",
60
+ },
61
+ md: {
62
+ "button-brand": "text-md font-semibold py-2.5 px-3",
63
+ "button-gray": "text-md font-semibold py-2.5 px-3",
64
+ "button-border": "text-md font-semibold py-2.5 px-3",
65
+ "button-minimal": "text-md font-semibold py-2.5 px-3",
66
+ underline: "text-md font-semibold px-1 pb-2.5 pt-0",
67
+ line: "text-md font-semibold pr-3.5 pl-3 py-1",
68
+ },
69
+ };
70
+
71
+ // Styles for different types of horizontal tabs
72
+ const getHorizontalStyles = ({ size, fullWidth }: { size?: "sm" | "md"; fullWidth?: boolean }) => ({
73
+ "button-brand": "gap-1",
74
+ "button-gray": "gap-1",
75
+ "button-border": cx("gap-1 rounded-[10px] bg-secondary_alt p-1 ring-1 ring-secondary ring-inset", size === "md" && "rounded-xl p-1.5"),
76
+ "button-minimal": "gap-0.5 rounded-lg bg-secondary_alt ring-1 ring-inset ring-secondary",
77
+ underline: cx("gap-3", fullWidth && "w-full gap-4"),
78
+ line: "gap-2",
79
+ });
80
+
81
+ const getColorStyles = ({ isSelected, isHovered }: Partial<AriaTabRenderProps>) => ({
82
+ "button-brand": isSelected || isHovered ? "brand" : "gray",
83
+ "button-gray": "gray",
84
+ "button-border": "gray",
85
+ "button-minimal": "gray",
86
+ underline: isSelected || isHovered ? "brand" : "gray",
87
+ line: isSelected || isHovered ? "brand" : "gray",
88
+ });
89
+
90
+ interface TabListComponentProps<T extends object, K extends Orientation> extends AriaTabListProps<T> {
91
+ /** The size of the tab list. */
92
+ size?: keyof typeof sizes;
93
+ /** The type of the tab list. */
94
+ type?: TabTypeColors<K>;
95
+ /** The orientation of the tab list. */
96
+ orientation?: K;
97
+ /** The items of the tab list. */
98
+ items: T[];
99
+ /** Whether the tab list is full width. */
100
+ fullWidth?: boolean;
101
+ }
102
+
103
+ const TabListContext = createContext<Omit<TabListComponentProps<TabComponentProps, Orientation>, "items">>({
104
+ size: "sm",
105
+ type: "button-brand",
106
+ });
107
+
108
+ export const TabList = <T extends Orientation>({
109
+ size = "sm",
110
+ type = "button-brand",
111
+ orientation: orientationProp,
112
+ fullWidth,
113
+ className,
114
+ children,
115
+ ...otherProps
116
+ }: TabListComponentProps<TabComponentProps, T>) => {
117
+ const context = useSlottedContext(TabsContext);
118
+
119
+ const orientation = orientationProp ?? context?.orientation ?? "horizontal";
120
+
121
+ return (
122
+ <TabListContext.Provider value={{ size, type, orientation, fullWidth }}>
123
+ <AriaTabList
124
+ {...otherProps}
125
+ className={(state) =>
126
+ cx(
127
+ "group flex",
128
+
129
+ getHorizontalStyles({
130
+ size,
131
+ fullWidth,
132
+ })[type as HorizontalTypes],
133
+
134
+ orientation === "vertical" && "w-max flex-col",
135
+
136
+ // Only horizontal tabs with underline type have bottom border
137
+ orientation === "horizontal" &&
138
+ type === "underline" &&
139
+ "relative before:absolute before:inset-x-0 before:bottom-0 before:h-px before:bg-border-secondary",
140
+
141
+ typeof className === "function" ? className(state) : className,
142
+ )
143
+ }
144
+ >
145
+ {children ?? ((item) => <Tab {...item}>{item.children}</Tab>)}
146
+ </AriaTabList>
147
+ </TabListContext.Provider>
148
+ );
149
+ };
150
+
151
+ export const TabPanel = (props: ComponentPropsWithRef<typeof AriaTabPanel>) => {
152
+ return (
153
+ <AriaTabPanel
154
+ {...props}
155
+ className={(state) =>
156
+ cx(
157
+ "outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2",
158
+ typeof props.className === "function" ? props.className(state) : props.className,
159
+ )
160
+ }
161
+ />
162
+ );
163
+ };
164
+
165
+ interface TabComponentProps extends AriaTabProps {
166
+ /** The label of the tab. */
167
+ label?: ReactNode;
168
+ /** The children of the tab. */
169
+ children?: ReactNode | ((props: AriaTabRenderProps) => ReactNode);
170
+ /** The badge displayed next to the label. */
171
+ badge?: number | string;
172
+ }
173
+
174
+ export const Tab = (props: TabComponentProps) => {
175
+ const { label, children, badge, ...otherProps } = props;
176
+ const { size = "sm", type = "button-brand", fullWidth } = useContext(TabListContext);
177
+
178
+ return (
179
+ <AriaTab
180
+ {...otherProps}
181
+ className={(prop) =>
182
+ cx(
183
+ "z-10 flex h-max cursor-pointer items-center justify-center gap-2 rounded-md whitespace-nowrap text-quaternary transition duration-100 ease-linear",
184
+ "group-orientation-vertical:justify-start",
185
+ fullWidth && "w-full flex-1",
186
+ sizes[size][type],
187
+ getTabStyles(prop)[type],
188
+ typeof props.className === "function" ? props.className(prop) : props.className,
189
+ )
190
+ }
191
+ >
192
+ {(state) => (
193
+ <Fragment>
194
+ {typeof children === "function" ? children(state) : children || label}
195
+ {badge && (
196
+ <Badge
197
+ size={size}
198
+ type="pill-color"
199
+ color={getColorStyles(state)[type] as BadgeColors}
200
+ className={cx("hidden transition-inherit-all md:flex", size === "sm" && "-my-px")}
201
+ >
202
+ {badge}
203
+ </Badge>
204
+ )}
205
+ </Fragment>
206
+ )}
207
+ </AriaTab>
208
+ );
209
+ };
210
+
211
+ export const Tabs = ({ className, ...props }: ComponentPropsWithRef<typeof AriaTabs>) => {
212
+ return (
213
+ <AriaTabs
214
+ keyboardActivation="manual"
215
+ {...props}
216
+ className={(state) => cx("flex w-full flex-col", typeof className === "function" ? className(state) : className)}
217
+ />
218
+ );
219
+ };
220
+
221
+ Tabs.Panel = TabPanel;
222
+ Tabs.List = TabList;
223
+ Tabs.Item = Tab;
@@ -0,0 +1,28 @@
1
+ import { type ReactNode } from "react";
2
+ import { cx } from "@/utils/cx";
3
+ import { Avatar, type AvatarProps } from "./avatar";
4
+
5
+ const styles = {
6
+ sm: { root: "gap-2", title: "text-sm font-semibold", subtitle: "text-xs" },
7
+ md: { root: "gap-2", title: "text-sm font-semibold", subtitle: "text-sm" },
8
+ lg: { root: "gap-3", title: "text-md font-semibold", subtitle: "text-md" },
9
+ xl: { root: "gap-4", title: "text-lg font-semibold", subtitle: "text-md" },
10
+ };
11
+
12
+ interface AvatarLabelGroupProps extends AvatarProps {
13
+ size: "sm" | "md" | "lg" | "xl";
14
+ title: string | ReactNode;
15
+ subtitle: string | ReactNode;
16
+ }
17
+
18
+ export const AvatarLabelGroup = ({ title, subtitle, className, ...props }: AvatarLabelGroupProps) => {
19
+ return (
20
+ <figure className={cx("group flex min-w-0 flex-1 items-center", styles[props.size].root, className)}>
21
+ <Avatar {...props} />
22
+ <figcaption className="min-w-0 flex-1">
23
+ <p className={cx("text-primary", styles[props.size].title)}>{title}</p>
24
+ <p className={cx("truncate text-tertiary", styles[props.size].subtitle)}>{subtitle}</p>
25
+ </figcaption>
26
+ </figure>
27
+ );
28
+ };