@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,95 @@
1
+ import { isValidElement, useContext } from "react";
2
+ import { Check } from "@untitledui/icons";
3
+ import type { ListBoxItemProps as AriaListBoxItemProps } from "react-aria-components";
4
+ import { ListBoxItem as AriaListBoxItem, Text as AriaText } from "react-aria-components";
5
+ import { Avatar } from "@/components/base/avatar/avatar";
6
+ import { cx } from "@/utils/cx";
7
+ import { isReactComponent } from "@/utils/is-react-component";
8
+ import type { SelectItemType } from "./select";
9
+ import { SelectContext } from "./select";
10
+
11
+ const sizes = {
12
+ sm: "p-2 pr-2.5",
13
+ md: "p-2.5 pl-2",
14
+ };
15
+
16
+ interface SelectItemProps extends Omit<AriaListBoxItemProps<SelectItemType>, "id">, SelectItemType {}
17
+
18
+ export const SelectItem = ({ label, id, value, avatarUrl, supportingText, isDisabled, icon: Icon, className, children, ...props }: SelectItemProps) => {
19
+ const { size } = useContext(SelectContext);
20
+
21
+ const labelOrChildren = label || (typeof children === "string" ? children : "");
22
+ const textValue = supportingText ? labelOrChildren + " " + supportingText : labelOrChildren;
23
+
24
+ return (
25
+ <AriaListBoxItem
26
+ id={id}
27
+ value={
28
+ value ?? {
29
+ id,
30
+ label: labelOrChildren,
31
+ avatarUrl,
32
+ supportingText,
33
+ isDisabled,
34
+ icon: Icon,
35
+ }
36
+ }
37
+ textValue={textValue}
38
+ isDisabled={isDisabled}
39
+ {...props}
40
+ className={(state) => cx("w-full px-1.5 py-px outline-hidden", typeof className === "function" ? className(state) : className)}
41
+ >
42
+ {(state) => (
43
+ <div
44
+ className={cx(
45
+ "flex cursor-pointer items-center gap-2 rounded-md outline-hidden select-none",
46
+ state.isSelected && "bg-active",
47
+ state.isDisabled && "cursor-not-allowed",
48
+ state.isFocused && "bg-primary_hover",
49
+ state.isFocusVisible && "ring-2 ring-focus-ring ring-inset",
50
+
51
+ // Icon styles
52
+ "*:data-icon:size-5 *:data-icon:shrink-0 *:data-icon:text-fg-quaternary",
53
+ state.isDisabled && "*:data-icon:text-fg-disabled",
54
+
55
+ sizes[size],
56
+ )}
57
+ >
58
+ {avatarUrl ? (
59
+ <Avatar aria-hidden="true" size="xs" src={avatarUrl} alt={label} />
60
+ ) : isReactComponent(Icon) ? (
61
+ <Icon data-icon aria-hidden="true" />
62
+ ) : isValidElement(Icon) ? (
63
+ Icon
64
+ ) : null}
65
+
66
+ <div className="flex w-full min-w-0 flex-1 flex-wrap gap-x-2">
67
+ <AriaText
68
+ slot="label"
69
+ className={cx("truncate text-md font-medium whitespace-nowrap text-primary", state.isDisabled && "text-disabled")}
70
+ >
71
+ {label || (typeof children === "function" ? children(state) : children)}
72
+ </AriaText>
73
+
74
+ {supportingText && (
75
+ <AriaText slot="description" className={cx("text-md whitespace-nowrap text-tertiary", state.isDisabled && "text-disabled")}>
76
+ {supportingText}
77
+ </AriaText>
78
+ )}
79
+ </div>
80
+
81
+ {state.isSelected && (
82
+ <Check
83
+ aria-hidden="true"
84
+ className={cx(
85
+ "ml-auto text-fg-brand-primary",
86
+ size === "sm" ? "size-4 stroke-[2.5px]" : "size-5",
87
+ state.isDisabled && "text-fg-disabled",
88
+ )}
89
+ />
90
+ )}
91
+ </div>
92
+ )}
93
+ </AriaListBoxItem>
94
+ );
95
+ };
@@ -0,0 +1,67 @@
1
+ import { type SelectHTMLAttributes, useId } from "react";
2
+ import { ChevronDown } from "@untitledui/icons";
3
+ import { HintText } from "@/components/base/input/hint-text";
4
+ import { Label } from "@/components/base/input/label";
5
+ import { cx } from "@/utils/cx";
6
+
7
+ interface NativeSelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
8
+ label?: string;
9
+ hint?: string;
10
+ selectClassName?: string;
11
+ options: { label: string; value: string; disabled?: boolean }[];
12
+ }
13
+
14
+ export const NativeSelect = ({ label, hint, options, className, selectClassName, ...props }: NativeSelectProps) => {
15
+ const id = useId();
16
+ const selectId = `select-native-${id}`;
17
+ const hintId = `select-native-hint-${id}`;
18
+
19
+ return (
20
+ <div className={cx("w-full in-data-input-wrapper:w-max", className)}>
21
+ {label && (
22
+ <Label htmlFor={selectId} id={selectId} className="mb-1.5">
23
+ {label}
24
+ </Label>
25
+ )}
26
+
27
+ <div className="relative grid w-full items-center">
28
+ <select
29
+ {...props}
30
+ id={selectId}
31
+ aria-describedby={hintId}
32
+ aria-labelledby={selectId}
33
+ className={cx(
34
+ "appearance-none rounded-lg bg-primary px-3.5 py-2.5 text-md font-medium text-primary shadow-xs ring-1 ring-primary outline-hidden transition duration-100 ease-linear ring-inset placeholder:text-fg-quaternary focus-visible:ring-2 focus-visible:ring-brand disabled:cursor-not-allowed disabled:bg-disabled_subtle disabled:text-disabled",
35
+ // Styles when the select is within an `InputGroup`
36
+ "in-data-input-wrapper:flex in-data-input-wrapper:h-full in-data-input-wrapper:gap-1 in-data-input-wrapper:bg-inherit in-data-input-wrapper:px-3 in-data-input-wrapper:py-2 in-data-input-wrapper:font-normal in-data-input-wrapper:text-tertiary in-data-input-wrapper:shadow-none in-data-input-wrapper:ring-transparent",
37
+ // Styles for the select when `TextField` is disabled
38
+ "in-data-input-wrapper:group-disabled:pointer-events-none in-data-input-wrapper:group-disabled:cursor-not-allowed in-data-input-wrapper:group-disabled:bg-transparent in-data-input-wrapper:group-disabled:text-disabled",
39
+ // Common styles for sizes and border radius within `InputGroup`
40
+ "in-data-input-wrapper:in-data-leading:rounded-r-none in-data-input-wrapper:in-data-trailing:rounded-l-none in-data-input-wrapper:in-data-[input-size=md]:py-2.5 in-data-input-wrapper:in-data-leading:in-data-[input-size=md]:pl-3.5 in-data-input-wrapper:in-data-[input-size=sm]:py-2 in-data-input-wrapper:in-data-[input-size=sm]:pl-3",
41
+ // For "leading" dropdown within `InputGroup`
42
+ "in-data-input-wrapper:in-data-leading:in-data-[input-size=md]:pr-4.5 in-data-input-wrapper:in-data-leading:in-data-[input-size=sm]:pr-4.5",
43
+ // For "trailing" dropdown within `InputGroup`
44
+ "in-data-input-wrapper:in-data-trailing:in-data-[input-size=md]:pr-8 in-data-input-wrapper:in-data-trailing:in-data-[input-size=sm]:pr-7.5",
45
+ selectClassName,
46
+ )}
47
+ >
48
+ {options.map((opt) => (
49
+ <option key={opt.value} value={opt.value}>
50
+ {opt.label}
51
+ </option>
52
+ ))}
53
+ </select>
54
+ <ChevronDown
55
+ aria-hidden="true"
56
+ className="pointer-events-none absolute right-3.5 size-5 text-fg-quaternary in-data-input-wrapper:right-0 in-data-input-wrapper:size-4 in-data-input-wrapper:stroke-[2.625px] in-data-input-wrapper:in-data-trailing:in-data-[input-size=sm]:right-3"
57
+ />
58
+ </div>
59
+
60
+ {hint && (
61
+ <HintText className="mt-2" id={hintId}>
62
+ {hint}
63
+ </HintText>
64
+ )}
65
+ </div>
66
+ );
67
+ };
@@ -0,0 +1,144 @@
1
+ import type { FC, ReactNode, Ref, RefAttributes } from "react";
2
+ import { createContext, isValidElement } from "react";
3
+ import { ChevronDown } from "@untitledui/icons";
4
+ import type { SelectProps as AriaSelectProps } from "react-aria-components";
5
+ import { Button as AriaButton, ListBox as AriaListBox, Select as AriaSelect, SelectValue as AriaSelectValue } from "react-aria-components";
6
+ import { Avatar } from "@/components/base/avatar/avatar";
7
+ import { HintText } from "@/components/base/input/hint-text";
8
+ import { Label } from "@/components/base/input/label";
9
+ import { cx } from "@/utils/cx";
10
+ import { isReactComponent } from "@/utils/is-react-component";
11
+ import { ComboBox } from "./combobox";
12
+ import { Popover } from "./popover";
13
+ import { SelectItem } from "./select-item";
14
+
15
+ export type SelectItemType = {
16
+ id: string;
17
+ label?: string;
18
+ avatarUrl?: string;
19
+ isDisabled?: boolean;
20
+ supportingText?: string;
21
+ icon?: FC | ReactNode;
22
+ };
23
+
24
+ export interface CommonProps {
25
+ hint?: string;
26
+ label?: string;
27
+ tooltip?: string;
28
+ size?: "sm" | "md";
29
+ placeholder?: string;
30
+ }
31
+
32
+ interface SelectProps extends Omit<AriaSelectProps<SelectItemType>, "children" | "items">, RefAttributes<HTMLDivElement>, CommonProps {
33
+ items?: SelectItemType[];
34
+ popoverClassName?: string;
35
+ placeholderIcon?: FC | ReactNode;
36
+ children: ReactNode | ((item: SelectItemType) => ReactNode);
37
+ }
38
+
39
+ interface SelectValueProps {
40
+ isOpen: boolean;
41
+ size: "sm" | "md";
42
+ isFocused: boolean;
43
+ isDisabled: boolean;
44
+ placeholder?: string;
45
+ ref?: Ref<HTMLButtonElement>;
46
+ placeholderIcon?: FC | ReactNode;
47
+ }
48
+
49
+ export const sizes = {
50
+ sm: { root: "py-2 px-3", shortcut: "pr-2.5" },
51
+ md: { root: "py-2.5 px-3.5", shortcut: "pr-3" },
52
+ };
53
+
54
+ const SelectValue = ({ isOpen, isFocused, isDisabled, size, placeholder, placeholderIcon, ref }: SelectValueProps) => {
55
+ return (
56
+ <AriaButton
57
+ ref={ref}
58
+ className={cx(
59
+ "relative flex w-full cursor-pointer items-center rounded-lg bg-primary shadow-xs ring-1 ring-primary outline-hidden transition duration-100 ease-linear ring-inset",
60
+ (isFocused || isOpen) && "ring-2 ring-brand",
61
+ isDisabled && "cursor-not-allowed bg-disabled_subtle text-disabled",
62
+ )}
63
+ >
64
+ <AriaSelectValue<SelectItemType>
65
+ className={cx(
66
+ "flex h-max w-full items-center justify-start gap-2 truncate text-left align-middle",
67
+
68
+ // Icon styles
69
+ "*:data-icon:size-5 *:data-icon:shrink-0 *:data-icon:text-fg-quaternary in-disabled:*:data-icon:text-fg-disabled",
70
+
71
+ sizes[size].root,
72
+ )}
73
+ >
74
+ {(state) => {
75
+ const Icon = state.selectedItem?.icon || placeholderIcon;
76
+ return (
77
+ <>
78
+ {state.selectedItem?.avatarUrl ? (
79
+ <Avatar size="xs" src={state.selectedItem.avatarUrl} alt={state.selectedItem.label} />
80
+ ) : isReactComponent(Icon) ? (
81
+ <Icon data-icon aria-hidden="true" />
82
+ ) : isValidElement(Icon) ? (
83
+ Icon
84
+ ) : null}
85
+
86
+ {state.selectedItem ? (
87
+ <section className="flex w-full gap-2 truncate">
88
+ <p className="truncate text-md font-medium text-primary">{state.selectedItem?.label}</p>
89
+ {state.selectedItem?.supportingText && <p className="text-md text-tertiary">{state.selectedItem?.supportingText}</p>}
90
+ </section>
91
+ ) : (
92
+ <p className={cx("text-md text-placeholder", isDisabled && "text-disabled")}>{placeholder}</p>
93
+ )}
94
+
95
+ <ChevronDown
96
+ aria-hidden="true"
97
+ className={cx("ml-auto shrink-0 text-fg-quaternary", size === "sm" ? "size-4 stroke-[2.5px]" : "size-5")}
98
+ />
99
+ </>
100
+ );
101
+ }}
102
+ </AriaSelectValue>
103
+ </AriaButton>
104
+ );
105
+ };
106
+
107
+ export const SelectContext = createContext<{ size: "sm" | "md" }>({ size: "sm" });
108
+
109
+ const Select = ({ placeholder = "Select", placeholderIcon, size = "sm", children, items, label, hint, tooltip, className, ...rest }: SelectProps) => {
110
+ return (
111
+ <SelectContext.Provider value={{ size }}>
112
+ <AriaSelect {...rest} className={(state) => cx("flex flex-col gap-1.5", typeof className === "function" ? className(state) : className)}>
113
+ {(state) => (
114
+ <>
115
+ {label && (
116
+ <Label isRequired={state.isRequired} tooltip={tooltip}>
117
+ {label}
118
+ </Label>
119
+ )}
120
+
121
+ <SelectValue {...state} {...{ size, placeholder }} placeholderIcon={placeholderIcon} />
122
+
123
+ <Popover size={size} className={rest.popoverClassName}>
124
+ <AriaListBox items={items} className="size-full outline-hidden">
125
+ {children}
126
+ </AriaListBox>
127
+ </Popover>
128
+
129
+ {hint && <HintText isInvalid={state.isInvalid}>{hint}</HintText>}
130
+ </>
131
+ )}
132
+ </AriaSelect>
133
+ </SelectContext.Provider>
134
+ );
135
+ };
136
+
137
+ const _Select = Select as typeof Select & {
138
+ ComboBox: typeof ComboBox;
139
+ Item: typeof SelectItem;
140
+ };
141
+ _Select.ComboBox = ComboBox;
142
+ _Select.Item = SelectItem;
143
+
144
+ export { _Select as Select };
@@ -0,0 +1,32 @@
1
+ import type { RefAttributes } from "react";
2
+ import { XClose } from "@untitledui/icons";
3
+ import { Button as AriaButton, type ButtonProps as AriaButtonProps } from "react-aria-components";
4
+ import { cx } from "@/utils/cx";
5
+
6
+ interface TagCloseXProps extends AriaButtonProps, RefAttributes<HTMLButtonElement> {
7
+ size?: "sm" | "md" | "lg";
8
+ className?: string;
9
+ }
10
+
11
+ const styles = {
12
+ sm: { root: "p-0.5", icon: "size-2.5" },
13
+ md: { root: "p-0.5", icon: "size-3" },
14
+ lg: { root: "p-0.75", icon: "size-3.5" },
15
+ };
16
+
17
+ export const TagCloseX = ({ size = "md", className, ...otherProps }: TagCloseXProps) => {
18
+ return (
19
+ <AriaButton
20
+ slot="remove"
21
+ aria-label="Remove this tag"
22
+ className={cx(
23
+ "flex cursor-pointer rounded-[3px] text-fg-quaternary outline-transparent transition duration-100 ease-linear hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring disabled:cursor-not-allowed",
24
+ styles[size].root,
25
+ className,
26
+ )}
27
+ {...otherProps}
28
+ >
29
+ <XClose className={cx("transition-inherit-all", styles[size].icon)} strokeWidth="3" />
30
+ </AriaButton>
31
+ );
32
+ };
@@ -0,0 +1,107 @@
1
+ import type { ReactNode } from "react";
2
+ import type {
3
+ ButtonProps as AriaButtonProps,
4
+ TooltipProps as AriaTooltipProps,
5
+ TooltipTriggerComponentProps as AriaTooltipTriggerComponentProps,
6
+ } from "react-aria-components";
7
+ import { Button as AriaButton, OverlayArrow as AriaOverlayArrow, Tooltip as AriaTooltip, TooltipTrigger as AriaTooltipTrigger } from "react-aria-components";
8
+ import { cx } from "@/utils/cx";
9
+
10
+ interface TooltipProps extends AriaTooltipTriggerComponentProps, Omit<AriaTooltipProps, "children"> {
11
+ /**
12
+ * The title of the tooltip.
13
+ */
14
+ title: ReactNode;
15
+ /**
16
+ * The description of the tooltip.
17
+ */
18
+ description?: ReactNode;
19
+ /**
20
+ * Whether to show the arrow on the tooltip.
21
+ *
22
+ * @default false
23
+ */
24
+ arrow?: boolean;
25
+ /**
26
+ * Delay in milliseconds before the tooltip is shown.
27
+ *
28
+ * @default 300
29
+ */
30
+ delay?: number;
31
+ }
32
+
33
+ export const Tooltip = ({
34
+ title,
35
+ description,
36
+ children,
37
+ arrow = false,
38
+ delay = 300,
39
+ closeDelay = 0,
40
+ trigger,
41
+ isDisabled,
42
+ isOpen,
43
+ defaultOpen,
44
+ offset = 6,
45
+ crossOffset,
46
+ placement = "top",
47
+ onOpenChange,
48
+ ...tooltipProps
49
+ }: TooltipProps) => {
50
+ const isTopOrBottomLeft = ["top left", "top end", "bottom left", "bottom end"].includes(placement);
51
+ const isTopOrBottomRight = ["top right", "top start", "bottom right", "bottom start"].includes(placement);
52
+ // Set negative cross offset for left and right placement to visually balance the tooltip.
53
+ const calculatedCrossOffset = isTopOrBottomLeft ? -12 : isTopOrBottomRight ? 12 : 0;
54
+
55
+ return (
56
+ <AriaTooltipTrigger {...{ trigger, delay, closeDelay, isDisabled, isOpen, defaultOpen, onOpenChange }}>
57
+ {children}
58
+
59
+ <AriaTooltip
60
+ {...tooltipProps}
61
+ offset={offset}
62
+ placement={placement}
63
+ crossOffset={crossOffset ?? calculatedCrossOffset}
64
+ className={({ isEntering, isExiting }) => cx(isEntering && "ease-out animate-in", isExiting && "ease-in animate-out")}
65
+ >
66
+ {({ isEntering, isExiting }) => (
67
+ <div
68
+ className={cx(
69
+ "z-50 flex max-w-xs origin-(--trigger-anchor-point) flex-col items-start gap-1 rounded-lg bg-primary-solid px-3 shadow-lg will-change-transform",
70
+ description ? "py-3" : "py-2",
71
+
72
+ isEntering &&
73
+ "ease-out animate-in fade-in zoom-in-95 in-placement-left:slide-in-from-right-0.5 in-placement-right:slide-in-from-left-0.5 in-placement-top:slide-in-from-bottom-0.5 in-placement-bottom:slide-in-from-top-0.5",
74
+ isExiting &&
75
+ "ease-in animate-out fade-out zoom-out-95 in-placement-left:slide-out-to-right-0.5 in-placement-right:slide-out-to-left-0.5 in-placement-top:slide-out-to-bottom-0.5 in-placement-bottom:slide-out-to-top-0.5",
76
+ )}
77
+ >
78
+ <span className="text-xs font-semibold text-white">{title}</span>
79
+
80
+ {description && <span className="text-xs font-medium text-tooltip-supporting-text">{description}</span>}
81
+
82
+ {arrow && (
83
+ <AriaOverlayArrow>
84
+ <svg
85
+ viewBox="0 0 100 100"
86
+ className="size-2.5 fill-bg-primary-solid in-placement-left:-rotate-90 in-placement-right:rotate-90 in-placement-top:rotate-0 in-placement-bottom:rotate-180"
87
+ >
88
+ <path d="M0,0 L35.858,35.858 Q50,50 64.142,35.858 L100,0 Z" />
89
+ </svg>
90
+ </AriaOverlayArrow>
91
+ )}
92
+ </div>
93
+ )}
94
+ </AriaTooltip>
95
+ </AriaTooltipTrigger>
96
+ );
97
+ };
98
+
99
+ interface TooltipTriggerProps extends AriaButtonProps {}
100
+
101
+ export const TooltipTrigger = ({ children, className, ...buttonProps }: TooltipTriggerProps) => {
102
+ return (
103
+ <AriaButton {...buttonProps} className={(values) => cx("h-max w-max outline-hidden", typeof className === "function" ? className(values) : className)}>
104
+ {children}
105
+ </AriaButton>
106
+ );
107
+ };
@@ -0,0 +1,22 @@
1
+ import type { HTMLAttributes } from "react";
2
+
3
+ const sizes = {
4
+ sm: {
5
+ wh: 8,
6
+ c: 4,
7
+ r: 2.5,
8
+ },
9
+ md: {
10
+ wh: 10,
11
+ c: 5,
12
+ r: 4,
13
+ },
14
+ };
15
+
16
+ export const Dot = ({ size = "md", ...props }: HTMLAttributes<HTMLOrSVGElement> & { size?: "sm" | "md" }) => {
17
+ return (
18
+ <svg width={sizes[size].wh} height={sizes[size].wh} viewBox={`0 0 ${sizes[size].wh} ${sizes[size].wh}`} fill="none" {...props}>
19
+ <circle cx={sizes[size].c} cy={sizes[size].c} r={sizes[size].r} fill="currentColor" stroke="currentColor" />
20
+ </svg>
21
+ );
22
+ };