@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,267 @@
1
+ import type { AnchorHTMLAttributes, ButtonHTMLAttributes, DetailedHTMLProps, FC, ReactNode } from "react";
2
+ import React, { isValidElement } from "react";
3
+ import type { ButtonProps as AriaButtonProps, LinkProps as AriaLinkProps } from "react-aria-components";
4
+ import { Button as AriaButton, Link as AriaLink } from "react-aria-components";
5
+ import { cx, sortCx } from "@/utils/cx";
6
+ import { isReactComponent } from "@/utils/is-react-component";
7
+
8
+ export const styles = sortCx({
9
+ common: {
10
+ root: [
11
+ "group relative inline-flex h-max cursor-pointer items-center justify-center whitespace-nowrap outline-brand transition duration-100 ease-linear before:absolute focus-visible:outline-2 focus-visible:outline-offset-2",
12
+ // When button is used within `InputGroup`
13
+ "in-data-input-wrapper:shadow-xs in-data-input-wrapper:focus:!z-50 in-data-input-wrapper:in-data-leading:-mr-px in-data-input-wrapper:in-data-leading:rounded-r-none in-data-input-wrapper:in-data-leading:before:rounded-r-none in-data-input-wrapper:in-data-trailing:-ml-px in-data-input-wrapper:in-data-trailing:rounded-l-none in-data-input-wrapper:in-data-trailing:before:rounded-l-none",
14
+ // Disabled styles
15
+ "disabled:cursor-not-allowed disabled:text-fg-disabled",
16
+ // Icon styles
17
+ "disabled:*:data-icon:text-fg-disabled_subtle",
18
+ // Same as `icon` but for SSR icons that cannot be passed to the client as functions.
19
+ "*:data-icon:pointer-events-none *:data-icon:size-5 *:data-icon:shrink-0 *:data-icon:transition-inherit-all",
20
+ ].join(" "),
21
+ icon: "pointer-events-none size-5 shrink-0 transition-inherit-all",
22
+ },
23
+ sizes: {
24
+ sm: {
25
+ root: [
26
+ "gap-1 rounded-lg px-3 py-2 text-sm font-semibold before:rounded-[7px] data-icon-only:p-2",
27
+ "in-data-input-wrapper:px-3.5 in-data-input-wrapper:py-2.5 in-data-input-wrapper:data-icon-only:p-2.5",
28
+ ].join(" "),
29
+ linkRoot: "gap-1",
30
+ },
31
+ md: {
32
+ root: [
33
+ "gap-1 rounded-lg px-3.5 py-2.5 text-sm font-semibold before:rounded-[7px] data-icon-only:p-2.5",
34
+ "in-data-input-wrapper:gap-1.5 in-data-input-wrapper:px-4 in-data-input-wrapper:text-md in-data-input-wrapper:data-icon-only:p-3",
35
+ ].join(" "),
36
+ linkRoot: "gap-1",
37
+ },
38
+ lg: {
39
+ root: "gap-1.5 rounded-lg px-4 py-2.5 text-md font-semibold before:rounded-[7px] data-icon-only:p-3",
40
+ linkRoot: "gap-1.5",
41
+ },
42
+ xl: {
43
+ root: "gap-1.5 rounded-lg px-4.5 py-3 text-md font-semibold before:rounded-[7px] data-icon-only:p-3.5",
44
+ linkRoot: "gap-1.5",
45
+ },
46
+ },
47
+
48
+ colors: {
49
+ primary: {
50
+ root: [
51
+ "bg-brand-solid text-white shadow-xs-skeumorphic ring-1 ring-transparent ring-inset hover:bg-brand-solid_hover data-loading:bg-brand-solid_hover",
52
+ // Inner border gradient
53
+ "before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0%",
54
+ // Disabled styles
55
+ "disabled:bg-disabled disabled:shadow-xs disabled:ring-disabled_subtle",
56
+ // Icon styles
57
+ "*:data-icon:text-button-primary-icon hover:*:data-icon:text-button-primary-icon_hover",
58
+ ].join(" "),
59
+ },
60
+ secondary: {
61
+ root: [
62
+ "bg-primary text-secondary shadow-xs-skeumorphic ring-1 ring-primary ring-inset hover:bg-primary_hover hover:text-secondary_hover data-loading:bg-primary_hover",
63
+ // Disabled styles
64
+ "disabled:shadow-xs disabled:ring-disabled_subtle",
65
+ // Icon styles
66
+ "*:data-icon:text-fg-quaternary hover:*:data-icon:text-fg-quaternary_hover",
67
+ ].join(" "),
68
+ },
69
+ tertiary: {
70
+ root: [
71
+ "text-tertiary hover:bg-primary_hover hover:text-tertiary_hover data-loading:bg-primary_hover",
72
+ // Icon styles
73
+ "*:data-icon:text-fg-quaternary hover:*:data-icon:text-fg-quaternary_hover",
74
+ ].join(" "),
75
+ },
76
+ "link-gray": {
77
+ root: [
78
+ "justify-normal rounded p-0! text-tertiary hover:text-tertiary_hover",
79
+ // Inner text underline
80
+ "*:data-text:underline *:data-text:decoration-transparent *:data-text:underline-offset-2 hover:*:data-text:decoration-current",
81
+ // Icon styles
82
+ "*:data-icon:text-fg-quaternary hover:*:data-icon:text-fg-quaternary_hover",
83
+ ].join(" "),
84
+ },
85
+ "link-color": {
86
+ root: [
87
+ "justify-normal rounded p-0! text-brand-secondary hover:text-brand-secondary_hover",
88
+ // Inner text underline
89
+ "*:data-text:underline *:data-text:decoration-transparent *:data-text:underline-offset-2 hover:*:data-text:decoration-current",
90
+ // Icon styles
91
+ "*:data-icon:text-fg-brand-secondary_alt hover:*:data-icon:text-fg-brand-secondary_hover",
92
+ ].join(" "),
93
+ },
94
+ "primary-destructive": {
95
+ root: [
96
+ "bg-error-solid text-white shadow-xs-skeumorphic ring-1 ring-transparent outline-error ring-inset hover:bg-error-solid_hover data-loading:bg-error-solid_hover",
97
+ // Inner border gradient
98
+ "before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0%",
99
+ // Disabled styles
100
+ "disabled:bg-disabled disabled:shadow-xs disabled:ring-disabled_subtle",
101
+ // Icon styles
102
+ "*:data-icon:text-button-destructive-primary-icon hover:*:data-icon:text-button-destructive-primary-icon_hover",
103
+ ].join(" "),
104
+ },
105
+ "secondary-destructive": {
106
+ root: [
107
+ "bg-primary text-error-primary shadow-xs-skeumorphic ring-1 ring-error_subtle outline-error ring-inset hover:bg-error-primary hover:text-error-primary_hover data-loading:bg-error-primary",
108
+ // Disabled styles
109
+ "disabled:bg-primary disabled:shadow-xs disabled:ring-disabled_subtle",
110
+ // Icon styles
111
+ "*:data-icon:text-fg-error-secondary hover:*:data-icon:text-fg-error-primary",
112
+ ].join(" "),
113
+ },
114
+ "tertiary-destructive": {
115
+ root: [
116
+ "text-error-primary outline-error hover:bg-error-primary hover:text-error-primary_hover data-loading:bg-error-primary",
117
+ // Icon styles
118
+ "*:data-icon:text-fg-error-secondary hover:*:data-icon:text-fg-error-primary",
119
+ ].join(" "),
120
+ },
121
+ "link-destructive": {
122
+ root: [
123
+ "justify-normal rounded p-0! text-error-primary outline-error hover:text-error-primary_hover",
124
+ // Inner text underline
125
+ "*:data-text:underline *:data-text:decoration-transparent *:data-text:underline-offset-2 hover:*:data-text:decoration-current",
126
+ // Icon styles
127
+ "*:data-icon:text-fg-error-secondary hover:*:data-icon:text-fg-error-primary",
128
+ ].join(" "),
129
+ },
130
+ },
131
+ });
132
+
133
+ /**
134
+ * Common props shared between button and anchor variants
135
+ */
136
+ export interface CommonProps {
137
+ /** Disables the button and shows a disabled state */
138
+ isDisabled?: boolean;
139
+ /** Shows a loading spinner and disables the button */
140
+ isLoading?: boolean;
141
+ /** The size variant of the button */
142
+ size?: keyof typeof styles.sizes;
143
+ /** The color variant of the button */
144
+ color?: keyof typeof styles.colors;
145
+ /** Icon component or element to show before the text */
146
+ iconLeading?: FC<{ className?: string }> | ReactNode;
147
+ /** Icon component or element to show after the text */
148
+ iconTrailing?: FC<{ className?: string }> | ReactNode;
149
+ /** Removes horizontal padding from the text content */
150
+ noTextPadding?: boolean;
151
+ /** When true, keeps the text visible during loading state */
152
+ showTextWhileLoading?: boolean;
153
+ }
154
+
155
+ /**
156
+ * Props for the button variant (non-link)
157
+ */
158
+ export interface ButtonProps extends CommonProps, DetailedHTMLProps<Omit<ButtonHTMLAttributes<HTMLButtonElement>, "color" | "slot">, HTMLButtonElement> {
159
+ /** Slot name for react-aria component */
160
+ slot?: AriaButtonProps["slot"];
161
+ }
162
+
163
+ /**
164
+ * Props for the link variant (anchor tag)
165
+ */
166
+ interface LinkProps extends CommonProps, DetailedHTMLProps<Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "color">, HTMLAnchorElement> {
167
+ /** Options for the configured client side router. */
168
+ routerOptions?: AriaLinkProps["routerOptions"];
169
+ }
170
+
171
+ /** Union type of button and link props */
172
+ export type Props = ButtonProps | LinkProps;
173
+
174
+ export const Button = ({
175
+ size = "sm",
176
+ color = "primary",
177
+ children,
178
+ className,
179
+ noTextPadding,
180
+ iconLeading: IconLeading,
181
+ iconTrailing: IconTrailing,
182
+ isDisabled: disabled,
183
+ isLoading: loading,
184
+ showTextWhileLoading,
185
+ ...otherProps
186
+ }: Props) => {
187
+ const href = "href" in otherProps ? otherProps.href : undefined;
188
+ const Component = href ? AriaLink : AriaButton;
189
+
190
+ const isIcon = (IconLeading || IconTrailing) && !children;
191
+ const isLinkType = ["link-gray", "link-color", "link-destructive"].includes(color);
192
+
193
+ noTextPadding = isLinkType || noTextPadding;
194
+
195
+ let props = {};
196
+
197
+ if (href) {
198
+ props = {
199
+ ...otherProps,
200
+
201
+ href: disabled ? undefined : href,
202
+ };
203
+ } else {
204
+ props = {
205
+ ...otherProps,
206
+
207
+ type: otherProps.type || "button",
208
+ isPending: loading,
209
+ };
210
+ }
211
+
212
+ return (
213
+ <Component
214
+ data-loading={loading ? true : undefined}
215
+ data-icon-only={isIcon ? true : undefined}
216
+ {...props}
217
+ isDisabled={disabled}
218
+ className={cx(
219
+ styles.common.root,
220
+ styles.sizes[size].root,
221
+ styles.colors[color].root,
222
+ isLinkType && styles.sizes[size].linkRoot,
223
+ (loading || (href && (disabled || loading))) && "pointer-events-none",
224
+ // If in `loading` state, hide everything except the loading icon (and text if `showTextWhileLoading` is true).
225
+ loading && (showTextWhileLoading ? "[&>*:not([data-icon=loading]):not([data-text])]:hidden" : "[&>*:not([data-icon=loading])]:invisible"),
226
+ className,
227
+ )}
228
+ >
229
+ {/* Leading icon */}
230
+ {isValidElement(IconLeading) && IconLeading}
231
+ {isReactComponent(IconLeading) && <IconLeading data-icon="leading" className={styles.common.icon} />}
232
+
233
+ {loading && (
234
+ <svg
235
+ fill="none"
236
+ data-icon="loading"
237
+ viewBox="0 0 20 20"
238
+ className={cx(styles.common.icon, !showTextWhileLoading && "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2")}
239
+ >
240
+ {/* Background circle */}
241
+ <circle className="stroke-current opacity-30" cx="10" cy="10" r="8" fill="none" strokeWidth="2" />
242
+ {/* Spinning circle */}
243
+ <circle
244
+ className="origin-center animate-spin stroke-current"
245
+ cx="10"
246
+ cy="10"
247
+ r="8"
248
+ fill="none"
249
+ strokeWidth="2"
250
+ strokeDasharray="12.5 50"
251
+ strokeLinecap="round"
252
+ />
253
+ </svg>
254
+ )}
255
+
256
+ {children && (
257
+ <span data-text className={cx("transition-inherit-all", !noTextPadding && "px-0.5")}>
258
+ {children}
259
+ </span>
260
+ )}
261
+
262
+ {/* Trailing icon */}
263
+ {isValidElement(IconTrailing) && IconTrailing}
264
+ {isReactComponent(IconTrailing) && <IconTrailing data-icon="trailing" className={styles.common.icon} />}
265
+ </Component>
266
+ );
267
+ };
@@ -0,0 +1,31 @@
1
+ import type { ReactNode, Ref } from "react";
2
+ import type { TextProps as AriaTextProps } from "react-aria-components";
3
+ import { Text as AriaText } from "react-aria-components";
4
+ import { cx } from "@/utils/cx";
5
+
6
+ interface HintTextProps extends AriaTextProps {
7
+ /** Indicates that the hint text is an error message. */
8
+ isInvalid?: boolean;
9
+ ref?: Ref<HTMLElement>;
10
+ children: ReactNode;
11
+ }
12
+
13
+ export const HintText = ({ isInvalid, className, ...props }: HintTextProps) => {
14
+ return (
15
+ <AriaText
16
+ {...props}
17
+ slot={isInvalid ? "errorMessage" : "description"}
18
+ className={cx(
19
+ "text-sm text-tertiary",
20
+
21
+ // Invalid state
22
+ isInvalid && "text-error-primary",
23
+ "group-invalid:text-error-primary",
24
+
25
+ className,
26
+ )}
27
+ />
28
+ );
29
+ };
30
+
31
+ HintText.displayName = "HintText";
@@ -0,0 +1,269 @@
1
+ import { type ComponentType, type HTMLAttributes, type ReactNode, type Ref, createContext, useContext } from "react";
2
+ import { HelpCircle, InfoCircle } from "@untitledui/icons";
3
+ import type { InputProps as AriaInputProps, TextFieldProps as AriaTextFieldProps } from "react-aria-components";
4
+ import { Group as AriaGroup, Input as AriaInput, TextField as AriaTextField } from "react-aria-components";
5
+ import { HintText } from "@/components/base/input/hint-text";
6
+ import { Label } from "@/components/base/input/label";
7
+ import { Tooltip, TooltipTrigger } from "@/components/base/tooltip/tooltip";
8
+ import { cx, sortCx } from "@/utils/cx";
9
+
10
+ export interface InputBaseProps extends TextFieldProps {
11
+ /** Tooltip message on hover. */
12
+ tooltip?: string;
13
+ /**
14
+ * Input size.
15
+ * @default "sm"
16
+ */
17
+ size?: "sm" | "md";
18
+ /** Placeholder text. */
19
+ placeholder?: string;
20
+ /** Class name for the icon. */
21
+ iconClassName?: string;
22
+ /** Class name for the input. */
23
+ inputClassName?: string;
24
+ /** Class name for the input wrapper. */
25
+ wrapperClassName?: string;
26
+ /** Class name for the tooltip. */
27
+ tooltipClassName?: string;
28
+ /** Keyboard shortcut to display. */
29
+ shortcut?: string | boolean;
30
+ ref?: Ref<HTMLInputElement>;
31
+ groupRef?: Ref<HTMLDivElement>;
32
+ /** Icon component to display on the left side of the input. */
33
+ icon?: ComponentType<HTMLAttributes<HTMLOrSVGElement>>;
34
+ }
35
+
36
+ export const InputBase = ({
37
+ ref,
38
+ tooltip,
39
+ shortcut,
40
+ groupRef,
41
+ size = "sm",
42
+ isInvalid,
43
+ isDisabled,
44
+ icon: Icon,
45
+ placeholder,
46
+ wrapperClassName,
47
+ tooltipClassName,
48
+ inputClassName,
49
+ iconClassName,
50
+ // Omit this prop to avoid invalid HTML attribute warning
51
+ isRequired: _isRequired,
52
+ ...inputProps
53
+ }: Omit<InputBaseProps, "label" | "hint">) => {
54
+ // Check if the input has a leading icon or tooltip
55
+ const hasTrailingIcon = tooltip || isInvalid;
56
+ const hasLeadingIcon = Icon;
57
+
58
+ // If the input is inside a `TextFieldContext`, use its context to simplify applying styles
59
+ const context = useContext(TextFieldContext);
60
+
61
+ const inputSize = context?.size || size;
62
+
63
+ const sizes = sortCx({
64
+ sm: {
65
+ root: cx("px-3 py-2", hasTrailingIcon && "pr-9", hasLeadingIcon && "pl-10"),
66
+ iconLeading: "left-3",
67
+ iconTrailing: "right-3",
68
+ shortcut: "pr-2.5",
69
+ },
70
+ md: {
71
+ root: cx("px-3.5 py-2.5", hasTrailingIcon && "pr-9.5", hasLeadingIcon && "pl-10.5"),
72
+ iconLeading: "left-3.5",
73
+ iconTrailing: "right-3.5",
74
+ shortcut: "pr-3",
75
+ },
76
+ });
77
+
78
+ return (
79
+ <AriaGroup
80
+ {...{ isDisabled, isInvalid }}
81
+ ref={groupRef}
82
+ className={({ isFocusWithin, isDisabled, isInvalid }) =>
83
+ cx(
84
+ "relative flex w-full flex-row place-content-center place-items-center rounded-lg bg-primary shadow-xs ring-1 ring-primary transition-shadow duration-100 ease-linear ring-inset",
85
+
86
+ isFocusWithin && !isDisabled && "ring-2 ring-brand",
87
+
88
+ // Disabled state styles
89
+ isDisabled && "cursor-not-allowed bg-disabled_subtle ring-disabled",
90
+ "group-disabled:cursor-not-allowed group-disabled:bg-disabled_subtle group-disabled:ring-disabled",
91
+
92
+ // Invalid state styles
93
+ isInvalid && "ring-error_subtle",
94
+ "group-invalid:ring-error_subtle",
95
+
96
+ // Invalid state with focus-within styles
97
+ isInvalid && isFocusWithin && "ring-2 ring-error",
98
+ isFocusWithin && "group-invalid:ring-2 group-invalid:ring-error",
99
+
100
+ context?.wrapperClassName,
101
+ wrapperClassName,
102
+ )
103
+ }
104
+ >
105
+ {/* Leading icon and Payment icon */}
106
+ {Icon && (
107
+ <Icon
108
+ className={cx(
109
+ "pointer-events-none absolute size-5 text-fg-quaternary",
110
+ isDisabled && "text-fg-disabled",
111
+ sizes[inputSize].iconLeading,
112
+ context?.iconClassName,
113
+ iconClassName,
114
+ )}
115
+ />
116
+ )}
117
+
118
+ {/* Input field */}
119
+ <AriaInput
120
+ {...(inputProps as AriaInputProps)}
121
+ ref={ref}
122
+ placeholder={placeholder}
123
+ className={cx(
124
+ "m-0 w-full bg-transparent text-md text-primary ring-0 outline-hidden placeholder:text-placeholder autofill:rounded-lg autofill:text-primary",
125
+ isDisabled && "cursor-not-allowed text-disabled",
126
+ sizes[inputSize].root,
127
+ context?.inputClassName,
128
+ inputClassName,
129
+ )}
130
+ />
131
+
132
+ {/* Tooltip and help icon */}
133
+ {tooltip && !isInvalid && (
134
+ <Tooltip title={tooltip} placement="top">
135
+ <TooltipTrigger
136
+ className={cx(
137
+ "absolute cursor-pointer text-fg-quaternary transition duration-200 hover:text-fg-quaternary_hover focus:text-fg-quaternary_hover",
138
+ sizes[inputSize].iconTrailing,
139
+ context?.tooltipClassName,
140
+ tooltipClassName,
141
+ )}
142
+ >
143
+ <HelpCircle className="size-4" />
144
+ </TooltipTrigger>
145
+ </Tooltip>
146
+ )}
147
+
148
+ {/* Invalid icon */}
149
+ {isInvalid && (
150
+ <InfoCircle
151
+ className={cx(
152
+ "pointer-events-none absolute size-4 text-fg-error-secondary",
153
+ sizes[inputSize].iconTrailing,
154
+ context?.tooltipClassName,
155
+ tooltipClassName,
156
+ )}
157
+ />
158
+ )}
159
+
160
+ {/* Shortcut */}
161
+ {shortcut && (
162
+ <div
163
+ className={cx(
164
+ "pointer-events-none absolute inset-y-0.5 right-0.5 z-10 flex items-center rounded-r-[inherit] bg-linear-to-r from-transparent to-bg-primary to-40% pl-8",
165
+ sizes[inputSize].shortcut,
166
+ )}
167
+ >
168
+ <span
169
+ className={cx(
170
+ "pointer-events-none rounded px-1 py-px text-xs font-medium text-quaternary ring-1 ring-secondary select-none ring-inset",
171
+ isDisabled && "bg-transparent text-disabled",
172
+ )}
173
+ aria-hidden="true"
174
+ >
175
+ {typeof shortcut === "string" ? shortcut : "⌘K"}
176
+ </span>
177
+ </div>
178
+ )}
179
+ </AriaGroup>
180
+ );
181
+ };
182
+
183
+ InputBase.displayName = "InputBase";
184
+
185
+ interface BaseProps {
186
+ /** Label text for the input */
187
+ label?: string;
188
+ /** Helper text displayed below the input */
189
+ hint?: ReactNode;
190
+ }
191
+
192
+ interface TextFieldProps
193
+ extends BaseProps,
194
+ AriaTextFieldProps,
195
+ Pick<InputBaseProps, "size" | "wrapperClassName" | "inputClassName" | "iconClassName" | "tooltipClassName"> {
196
+ ref?: Ref<HTMLDivElement>;
197
+ }
198
+
199
+ const TextFieldContext = createContext<TextFieldProps>({});
200
+
201
+ export const TextField = ({ className, ...props }: TextFieldProps) => {
202
+ return (
203
+ <TextFieldContext.Provider value={props}>
204
+ <AriaTextField
205
+ {...props}
206
+ data-input-wrapper
207
+ className={(state) =>
208
+ cx("group flex h-max w-full flex-col items-start justify-start gap-1.5", typeof className === "function" ? className(state) : className)
209
+ }
210
+ />
211
+ </TextFieldContext.Provider>
212
+ );
213
+ };
214
+
215
+ TextField.displayName = "TextField";
216
+
217
+ interface InputProps extends InputBaseProps, BaseProps {
218
+ /** Whether to hide required indicator from label */
219
+ hideRequiredIndicator?: boolean;
220
+ }
221
+
222
+ export const Input = ({
223
+ size = "sm",
224
+ placeholder,
225
+ icon: Icon,
226
+ label,
227
+ hint,
228
+ shortcut,
229
+ hideRequiredIndicator,
230
+ className,
231
+ ref,
232
+ groupRef,
233
+ tooltip,
234
+ iconClassName,
235
+ inputClassName,
236
+ wrapperClassName,
237
+ tooltipClassName,
238
+ ...props
239
+ }: InputProps) => {
240
+ return (
241
+ <TextField aria-label={!label ? placeholder : undefined} {...props} className={className}>
242
+ {({ isRequired, isInvalid }) => (
243
+ <>
244
+ {label && <Label isRequired={hideRequiredIndicator ? !hideRequiredIndicator : isRequired}>{label}</Label>}
245
+
246
+ <InputBase
247
+ {...{
248
+ ref,
249
+ groupRef,
250
+ size,
251
+ placeholder,
252
+ icon: Icon,
253
+ shortcut,
254
+ iconClassName,
255
+ inputClassName,
256
+ wrapperClassName,
257
+ tooltipClassName,
258
+ tooltip,
259
+ }}
260
+ />
261
+
262
+ {hint && <HintText isInvalid={isInvalid}>{hint}</HintText>}
263
+ </>
264
+ )}
265
+ </TextField>
266
+ );
267
+ };
268
+
269
+ Input.displayName = "Input";
@@ -0,0 +1,48 @@
1
+ import type { ReactNode, Ref } from "react";
2
+ import { HelpCircle } from "@untitledui/icons";
3
+ import type { LabelProps as AriaLabelProps } from "react-aria-components";
4
+ import { Label as AriaLabel } from "react-aria-components";
5
+ import { Tooltip, TooltipTrigger } from "@/components/base/tooltip/tooltip";
6
+ import { cx } from "@/utils/cx";
7
+
8
+ interface LabelProps extends AriaLabelProps {
9
+ children: ReactNode;
10
+ isRequired?: boolean;
11
+ tooltip?: string;
12
+ tooltipDescription?: string;
13
+ ref?: Ref<HTMLLabelElement>;
14
+ }
15
+
16
+ export const Label = ({ isRequired, tooltip, tooltipDescription, className, ...props }: LabelProps) => {
17
+ return (
18
+ <AriaLabel
19
+ // Used for conditionally hiding/showing the label element via CSS:
20
+ // <Input label="Visible only on mobile" className="lg:**:data-label:hidden" />
21
+ // or
22
+ // <Input label="Visible only on mobile" className="lg:label:hidden" />
23
+ data-label="true"
24
+ {...props}
25
+ className={cx("flex cursor-default items-center gap-0.5 text-sm font-medium text-secondary", className)}
26
+ >
27
+ {props.children}
28
+
29
+ <span className={cx("hidden text-brand-tertiary", isRequired && "block", typeof isRequired === "undefined" && "group-required:block")}>*</span>
30
+
31
+ {tooltip && (
32
+ <Tooltip title={tooltip} description={tooltipDescription} placement="top">
33
+ <TooltipTrigger
34
+ // `TooltipTrigger` inherits the disabled state from the parent form field
35
+ // but we don't that. We want the tooltip be enabled even if the parent
36
+ // field is disabled.
37
+ isDisabled={false}
38
+ className="cursor-pointer text-fg-quaternary transition duration-200 hover:text-fg-quaternary_hover focus:text-fg-quaternary_hover"
39
+ >
40
+ <HelpCircle className="size-4" />
41
+ </TooltipTrigger>
42
+ </Tooltip>
43
+ )}
44
+ </AriaLabel>
45
+ );
46
+ };
47
+
48
+ Label.displayName = "Label";