@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,376 @@
1
+ import type { CSSProperties, FC, HTMLAttributes, ReactNode } from "react";
2
+ import React, { cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useState } from "react";
3
+
4
+ type PaginationPage = {
5
+ /** The type of the pagination item. */
6
+ type: "page";
7
+ /** The value of the pagination item. */
8
+ value: number;
9
+ /** Whether the pagination item is the current page. */
10
+ isCurrent: boolean;
11
+ };
12
+
13
+ type PaginationEllipsisType = {
14
+ type: "ellipsis";
15
+ key: number;
16
+ };
17
+
18
+ type PaginationItemType = PaginationPage | PaginationEllipsisType;
19
+
20
+ interface PaginationContextType {
21
+ /** The pages of the pagination. */
22
+ pages: PaginationItemType[];
23
+ /** The current page of the pagination. */
24
+ currentPage: number;
25
+ /** The total number of pages. */
26
+ total: number;
27
+ /** The function to call when the page changes. */
28
+ onPageChange: (page: number) => void;
29
+ }
30
+
31
+ const PaginationContext = createContext<PaginationContextType | undefined>(undefined);
32
+
33
+ export interface PaginationRootProps {
34
+ /** Number of sibling pages to show on each side of the current page */
35
+ siblingCount?: number;
36
+ /** Current active page number */
37
+ page: number;
38
+ /** Total number of pages */
39
+ total: number;
40
+ children: ReactNode;
41
+ /** The style of the pagination root. */
42
+ style?: CSSProperties;
43
+ /** The class name of the pagination root. */
44
+ className?: string;
45
+ /** Callback function that's called when the page changes with the new page number. */
46
+ onPageChange?: (page: number) => void;
47
+ }
48
+
49
+ const PaginationRoot = ({ total, siblingCount = 1, page, onPageChange, children, style, className }: PaginationRootProps) => {
50
+ const [pages, setPages] = useState<PaginationItemType[]>([]);
51
+
52
+ const createPaginationItems = useCallback((): PaginationItemType[] => {
53
+ const items: PaginationItemType[] = [];
54
+ // Calculate the maximum number of pagination elements (pages, potential ellipsis, first and last) to show
55
+ const totalPageNumbers = siblingCount * 2 + 5;
56
+
57
+ // If the total number of items to show is greater than or equal to the total pages,
58
+ // we can simply list all pages without needing to collapse with ellipsis
59
+ if (totalPageNumbers >= total) {
60
+ for (let i = 1; i <= total; i++) {
61
+ items.push({
62
+ type: "page",
63
+ value: i,
64
+ isCurrent: i === page,
65
+ });
66
+ }
67
+ } else {
68
+ // Calculate left and right sibling boundaries around the current page
69
+ const leftSiblingIndex = Math.max(page - siblingCount, 1);
70
+ const rightSiblingIndex = Math.min(page + siblingCount, total);
71
+
72
+ // Determine if we need to show ellipsis on either side
73
+ const showLeftEllipsis = leftSiblingIndex > 2;
74
+ const showRightEllipsis = rightSiblingIndex < total - 1;
75
+
76
+ // Case 1: No left ellipsis, but right ellipsis is needed
77
+ if (!showLeftEllipsis && showRightEllipsis) {
78
+ // Calculate how many page numbers to show starting from the beginning
79
+ const leftItemCount = siblingCount * 2 + 3;
80
+ const leftRange = range(1, leftItemCount);
81
+
82
+ leftRange.forEach((pageNum) =>
83
+ items.push({
84
+ type: "page",
85
+ value: pageNum,
86
+ isCurrent: pageNum === page,
87
+ }),
88
+ );
89
+
90
+ // Insert ellipsis after the left range and add the last page
91
+ items.push({ type: "ellipsis", key: leftItemCount + 1 });
92
+ items.push({
93
+ type: "page",
94
+ value: total,
95
+ isCurrent: total === page,
96
+ });
97
+ }
98
+ // Case 2: Left ellipsis needed, but right ellipsis is not needed
99
+ else if (showLeftEllipsis && !showRightEllipsis) {
100
+ // Determine how many items from the end should be shown
101
+ const rightItemCount = siblingCount * 2 + 3;
102
+ const rightRange = range(total - rightItemCount + 1, total);
103
+
104
+ // Always show the first page, then add an ellipsis to indicate skipped pages
105
+ items.push({
106
+ type: "page",
107
+ value: 1,
108
+ isCurrent: page === 1,
109
+ });
110
+ items.push({ type: "ellipsis", key: total - rightItemCount });
111
+ rightRange.forEach((pageNum) =>
112
+ items.push({
113
+ type: "page",
114
+ value: pageNum,
115
+ isCurrent: pageNum === page,
116
+ }),
117
+ );
118
+ }
119
+ // Case 3: Both left and right ellipsis are needed
120
+ else if (showLeftEllipsis && showRightEllipsis) {
121
+ // Always show the first page
122
+ items.push({
123
+ type: "page",
124
+ value: 1,
125
+ isCurrent: page === 1,
126
+ });
127
+ // Insert left ellipsis after the first page
128
+ items.push({ type: "ellipsis", key: leftSiblingIndex - 1 });
129
+
130
+ // Show a range of pages around the current page
131
+ const middleRange = range(leftSiblingIndex, rightSiblingIndex);
132
+ middleRange.forEach((pageNum) =>
133
+ items.push({
134
+ type: "page",
135
+ value: pageNum,
136
+ isCurrent: pageNum === page,
137
+ }),
138
+ );
139
+
140
+ // Insert right ellipsis and finally the last page
141
+ items.push({ type: "ellipsis", key: rightSiblingIndex + 1 });
142
+ items.push({
143
+ type: "page",
144
+ value: total,
145
+ isCurrent: total === page,
146
+ });
147
+ }
148
+ }
149
+
150
+ return items;
151
+ }, [total, siblingCount, page]);
152
+
153
+ useEffect(() => {
154
+ const paginationItems = createPaginationItems();
155
+ setPages(paginationItems);
156
+ }, [createPaginationItems]);
157
+
158
+ const onPageChangeHandler = (newPage: number) => {
159
+ onPageChange?.(newPage);
160
+ };
161
+
162
+ const paginationContextValue: PaginationContextType = {
163
+ pages,
164
+ currentPage: page,
165
+ total,
166
+ onPageChange: onPageChangeHandler,
167
+ };
168
+
169
+ return (
170
+ <PaginationContext.Provider value={paginationContextValue}>
171
+ <nav aria-label="Pagination Navigation" style={style} className={className}>
172
+ {children}
173
+ </nav>
174
+ </PaginationContext.Provider>
175
+ );
176
+ };
177
+
178
+ /**
179
+ * Creates an array of numbers from start to end.
180
+ * @param start - The start number.
181
+ * @param end - The end number.
182
+ * @returns An array of numbers from start to end.
183
+ */
184
+ const range = (start: number, end: number): number[] => {
185
+ const length = end - start + 1;
186
+
187
+ return Array.from({ length }, (_, index) => index + start);
188
+ };
189
+
190
+ interface TriggerRenderProps {
191
+ isDisabled: boolean;
192
+ onClick: () => void;
193
+ }
194
+
195
+ interface TriggerProps {
196
+ /** The children of the trigger. Can be a render prop or a valid element. */
197
+ children: ReactNode | ((props: TriggerRenderProps) => ReactNode);
198
+ /** The style of the trigger. */
199
+ style?: CSSProperties;
200
+ /** The class name of the trigger. */
201
+ className?: string | ((args: { isDisabled: boolean }) => string);
202
+ /** If true, the child element will be cloned and passed down the prop of the trigger. */
203
+ asChild?: boolean;
204
+ /** The direction of the trigger. */
205
+ direction: "prev" | "next";
206
+ /** The aria label of the trigger. */
207
+ ariaLabel?: string;
208
+ }
209
+
210
+ const Trigger: FC<TriggerProps> = ({ children, style, className, asChild = false, direction, ariaLabel }) => {
211
+ const context = useContext(PaginationContext);
212
+ if (!context) {
213
+ throw new Error("Pagination components must be used within a Pagination.Root");
214
+ }
215
+
216
+ const { currentPage, total, onPageChange } = context;
217
+
218
+ const isDisabled = direction === "prev" ? currentPage <= 1 : currentPage >= total;
219
+
220
+ const handleClick = () => {
221
+ if (isDisabled) return;
222
+
223
+ const newPage = direction === "prev" ? currentPage - 1 : currentPage + 1;
224
+ onPageChange?.(newPage);
225
+ };
226
+
227
+ const computedClassName = typeof className === "function" ? className({ isDisabled }) : className;
228
+
229
+ const defaultAriaLabel = direction === "prev" ? "Previous Page" : "Next Page";
230
+
231
+ // If the children is a render prop, we need to pass the isDisabled and onClick to the render prop.
232
+ if (typeof children === "function") {
233
+ return <>{children({ isDisabled, onClick: handleClick })}</>;
234
+ }
235
+
236
+ // If the children is a valid element, we need to clone it and pass the isDisabled and onClick to the cloned element.
237
+ if (asChild && isValidElement(children)) {
238
+ return cloneElement(children, {
239
+ onClick: handleClick,
240
+ disabled: isDisabled,
241
+ isDisabled,
242
+ "aria-label": ariaLabel || defaultAriaLabel,
243
+ style: { ...(children.props as HTMLAttributes<HTMLElement>).style, ...style },
244
+ className: [computedClassName, (children.props as HTMLAttributes<HTMLElement>).className].filter(Boolean).join(" ") || undefined,
245
+ } as HTMLAttributes<HTMLElement>);
246
+ }
247
+
248
+ return (
249
+ <button aria-label={ariaLabel || defaultAriaLabel} onClick={handleClick} disabled={isDisabled} style={style} className={computedClassName}>
250
+ {children}
251
+ </button>
252
+ );
253
+ };
254
+
255
+ const PaginationPrevTrigger: FC<Omit<TriggerProps, "direction">> = (props) => <Trigger {...props} direction="prev" />;
256
+
257
+ const PaginationNextTrigger: FC<Omit<TriggerProps, "direction">> = (props) => <Trigger {...props} direction="next" />;
258
+
259
+ interface PaginationItemRenderProps {
260
+ isSelected: boolean;
261
+ onClick: () => void;
262
+ value: number;
263
+ "aria-current"?: "page";
264
+ "aria-label"?: string;
265
+ }
266
+
267
+ export interface PaginationItemProps {
268
+ /** The value of the pagination item. */
269
+ value: number;
270
+ /** Whether the pagination item is the current page. */
271
+ isCurrent: boolean;
272
+ /** The children of the pagination item. Can be a render prop or a valid element. */
273
+ children?: ReactNode | ((props: PaginationItemRenderProps) => ReactNode);
274
+ /** The style object of the pagination item. */
275
+ style?: CSSProperties;
276
+ /** The class name of the pagination item. */
277
+ className?: string | ((args: { isSelected: boolean }) => string);
278
+ /** The aria label of the pagination item. */
279
+ ariaLabel?: string;
280
+ /** If true, the child element will be cloned and passed down the prop of the item. */
281
+ asChild?: boolean;
282
+ }
283
+
284
+ const PaginationItem = ({ value, isCurrent, children, style, className, ariaLabel, asChild = false }: PaginationItemProps) => {
285
+ const context = useContext(PaginationContext);
286
+ if (!context) {
287
+ throw new Error("Pagination components must be used within a <Pagination.Root />");
288
+ }
289
+
290
+ const { onPageChange } = context;
291
+
292
+ const isSelected = isCurrent;
293
+
294
+ const handleClick = () => {
295
+ onPageChange?.(value);
296
+ };
297
+
298
+ const computedClassName = typeof className === "function" ? className({ isSelected }) : className;
299
+
300
+ // If the children is a render prop, we need to pass the necessary props to the render prop.
301
+ if (typeof children === "function") {
302
+ return (
303
+ <>
304
+ {children({
305
+ isSelected,
306
+ onClick: handleClick,
307
+ value,
308
+ "aria-current": isCurrent ? "page" : undefined,
309
+ "aria-label": ariaLabel || `Page ${value}`,
310
+ })}
311
+ </>
312
+ );
313
+ }
314
+
315
+ // If the children is a valid element, we need to clone it and pass the necessary props to the cloned element.
316
+ if (asChild && isValidElement(children)) {
317
+ return cloneElement(children, {
318
+ onClick: handleClick,
319
+ "aria-current": isCurrent ? "page" : undefined,
320
+ "aria-label": ariaLabel || `Page ${value}`,
321
+ style: { ...(children.props as HTMLAttributes<HTMLElement>).style, ...style },
322
+ className: [computedClassName, (children.props as HTMLAttributes<HTMLElement>).className].filter(Boolean).join(" ") || undefined,
323
+ } as HTMLAttributes<HTMLElement>);
324
+ }
325
+
326
+ return (
327
+ <button
328
+ onClick={handleClick}
329
+ style={style}
330
+ className={computedClassName}
331
+ aria-current={isCurrent ? "page" : undefined}
332
+ aria-label={ariaLabel || `Page ${value}`}
333
+ role="listitem"
334
+ >
335
+ {children}
336
+ </button>
337
+ );
338
+ };
339
+ interface PaginationEllipsisProps {
340
+ key: number;
341
+ children?: ReactNode;
342
+ style?: CSSProperties;
343
+ className?: string | (() => string);
344
+ }
345
+
346
+ const PaginationEllipsis: FC<PaginationEllipsisProps> = ({ children, style, className }) => {
347
+ const computedClassName = typeof className === "function" ? className() : className;
348
+
349
+ return (
350
+ <span style={style} className={computedClassName} aria-hidden="true">
351
+ {children}
352
+ </span>
353
+ );
354
+ };
355
+
356
+ interface PaginationContextComponentProps {
357
+ children: (pagination: PaginationContextType) => ReactNode;
358
+ }
359
+
360
+ const PaginationContextComponent: FC<PaginationContextComponentProps> = ({ children }) => {
361
+ const context = useContext(PaginationContext);
362
+ if (!context) {
363
+ throw new Error("Pagination components must be used within a Pagination.Root");
364
+ }
365
+
366
+ return <>{children(context)}</>;
367
+ };
368
+
369
+ export const Pagination = {
370
+ Root: PaginationRoot,
371
+ PrevTrigger: PaginationPrevTrigger,
372
+ NextTrigger: PaginationNextTrigger,
373
+ Item: PaginationItem,
374
+ Ellipsis: PaginationEllipsis,
375
+ Context: PaginationContextComponent,
376
+ };
@@ -0,0 +1,52 @@
1
+ import { cx } from "@/utils/cx";
2
+ import type { PaginationRootProps } from "./pagination-base";
3
+ import { Pagination } from "./pagination-base";
4
+
5
+ interface PaginationDotProps extends Omit<PaginationRootProps, "children"> {
6
+ /** The size of the pagination dot. */
7
+ size?: "md" | "lg";
8
+ /** Whether the pagination uses brand colors. */
9
+ isBrand?: boolean;
10
+ /** Whether the pagination is displayed in a card. */
11
+ framed?: boolean;
12
+ }
13
+
14
+ export const PaginationDot = ({ framed, className, size = "md", isBrand, ...props }: PaginationDotProps) => {
15
+ const sizes = {
16
+ md: {
17
+ root: cx("gap-3", framed && "p-2"),
18
+ button: "h-2 w-2 after:-inset-x-1.5 after:-inset-y-2",
19
+ },
20
+ lg: {
21
+ root: cx("gap-4", framed && "p-3"),
22
+ button: "h-2.5 w-2.5 after:-inset-x-2 after:-inset-y-3",
23
+ },
24
+ };
25
+
26
+ return (
27
+ <Pagination.Root {...props} className={cx("flex h-max w-max", sizes[size].root, framed && "rounded-full bg-alpha-white/90 backdrop-blur", className)}>
28
+ <Pagination.Context>
29
+ {({ pages }) =>
30
+ pages.map((page, index) =>
31
+ page.type === "page" ? (
32
+ <Pagination.Item
33
+ {...page}
34
+ asChild
35
+ key={index}
36
+ className={cx(
37
+ "relative cursor-pointer rounded-full bg-quaternary outline-focus-ring after:absolute focus-visible:outline-2 focus-visible:outline-offset-2",
38
+ sizes[size].button,
39
+ page.isCurrent && "bg-fg-brand-primary_alt",
40
+ isBrand && "bg-fg-brand-secondary",
41
+ isBrand && page.isCurrent && "bg-fg-white",
42
+ )}
43
+ ></Pagination.Item>
44
+ ) : (
45
+ <Pagination.Ellipsis {...page} key={index} />
46
+ ),
47
+ )
48
+ }
49
+ </Pagination.Context>
50
+ </Pagination.Root>
51
+ );
52
+ };
@@ -0,0 +1,48 @@
1
+ import { cx } from "@/utils/cx";
2
+ import type { PaginationRootProps } from "./pagination-base";
3
+ import { Pagination } from "./pagination-base";
4
+
5
+ interface PaginationLineProps extends Omit<PaginationRootProps, "children"> {
6
+ /** The size of the pagination line. */
7
+ size?: "md" | "lg";
8
+ /** Whether the pagination is displayed in a card. */
9
+ framed?: boolean;
10
+ }
11
+
12
+ export const PaginationLine = ({ framed, className, size = "md", ...props }: PaginationLineProps) => {
13
+ const sizes = {
14
+ md: {
15
+ root: cx("gap-2", framed && "p-2"),
16
+ button: "h-1.5 w-full after:-inset-x-1.5 after:-inset-y-2",
17
+ },
18
+ lg: {
19
+ root: cx("gap-3", framed && "p-3"),
20
+ button: "h-2 w-full after:-inset-x-2 after:-inset-y-3",
21
+ },
22
+ };
23
+
24
+ return (
25
+ <Pagination.Root {...props} className={cx("flex h-max w-max", sizes[size].root, framed && "rounded-full bg-alpha-white/90 backdrop-blur", className)}>
26
+ <Pagination.Context>
27
+ {({ pages }) =>
28
+ pages.map((page, index) =>
29
+ page.type === "page" ? (
30
+ <Pagination.Item
31
+ {...page}
32
+ asChild
33
+ key={index}
34
+ className={cx(
35
+ "relative cursor-pointer rounded-full bg-quaternary outline-focus-ring after:absolute focus-visible:outline-2 focus-visible:outline-offset-2",
36
+ sizes[size].button,
37
+ page.isCurrent && "bg-fg-brand-primary_alt",
38
+ )}
39
+ />
40
+ ) : (
41
+ <Pagination.Ellipsis {...page} key={index} />
42
+ ),
43
+ )
44
+ }
45
+ </Pagination.Context>
46
+ </Pagination.Root>
47
+ );
48
+ };