@icomponents/react 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 (117) hide show
  1. package/README.md +40 -0
  2. package/package.json +165 -0
  3. package/src/assets/custom/icon-dir.tsx +105 -0
  4. package/src/assets/custom/icon-layout-compact.tsx +84 -0
  5. package/src/assets/custom/icon-layout-default.tsx +48 -0
  6. package/src/assets/custom/icon-layout-full.tsx +68 -0
  7. package/src/assets/custom/icon-sidebar-floating.tsx +62 -0
  8. package/src/assets/custom/icon-sidebar-inset.tsx +53 -0
  9. package/src/assets/custom/icon-sidebar-sidebar.tsx +48 -0
  10. package/src/assets/custom/icon-theme-dark.tsx +36 -0
  11. package/src/assets/custom/icon-theme-light.tsx +35 -0
  12. package/src/assets/custom/icon-theme-system.tsx +113 -0
  13. package/src/components/admin-page-header.tsx +29 -0
  14. package/src/components/color-theme-switch.tsx +93 -0
  15. package/src/components/coming-soon.tsx +16 -0
  16. package/src/components/command-menu.tsx +98 -0
  17. package/src/components/config-drawer.tsx +401 -0
  18. package/src/components/confirm-dialog.dom.test.tsx +60 -0
  19. package/src/components/confirm-dialog.tsx +75 -0
  20. package/src/components/data-table/bulk-actions.tsx +193 -0
  21. package/src/components/data-table/column-header.tsx +66 -0
  22. package/src/components/data-table/data-table.dom.test.tsx +54 -0
  23. package/src/components/data-table/data-table.tsx +189 -0
  24. package/src/components/data-table/faceted-filter.tsx +156 -0
  25. package/src/components/data-table/index.ts +5 -0
  26. package/src/components/data-table/pagination.tsx +127 -0
  27. package/src/components/data-table/tanstack-table.d.ts +12 -0
  28. package/src/components/data-table/toolbar.tsx +88 -0
  29. package/src/components/data-table/view-options.tsx +49 -0
  30. package/src/components/date-picker.tsx +39 -0
  31. package/src/components/detail-card.tsx +63 -0
  32. package/src/components/language-switch.tsx +38 -0
  33. package/src/components/layout/app-sidebar.tsx +59 -0
  34. package/src/components/layout/app-title.tsx +62 -0
  35. package/src/components/layout/header.tsx +61 -0
  36. package/src/components/layout/main.tsx +34 -0
  37. package/src/components/layout/nav-group.tsx +185 -0
  38. package/src/components/layout/nav-user.tsx +115 -0
  39. package/src/components/layout/screen-header.tsx +119 -0
  40. package/src/components/layout/team-switcher.tsx +84 -0
  41. package/src/components/layout/top-nav.tsx +68 -0
  42. package/src/components/layout/types.ts +44 -0
  43. package/src/components/learn-more.tsx +35 -0
  44. package/src/components/long-text.tsx +80 -0
  45. package/src/components/navigation-progress.tsx +18 -0
  46. package/src/components/password-input.tsx +36 -0
  47. package/src/components/profile-dropdown.tsx +106 -0
  48. package/src/components/search.tsx +38 -0
  49. package/src/components/select-dropdown.tsx +62 -0
  50. package/src/components/sign-out-dialog.tsx +31 -0
  51. package/src/components/skip-to-main.tsx +10 -0
  52. package/src/components/stat-tile.tsx +56 -0
  53. package/src/components/status-badge.test.ts +23 -0
  54. package/src/components/status-badge.tsx +49 -0
  55. package/src/components/theme-switch.tsx +61 -0
  56. package/src/components/ui/alert-dialog.tsx +134 -0
  57. package/src/components/ui/alert.tsx +59 -0
  58. package/src/components/ui/avatar.tsx +38 -0
  59. package/src/components/ui/badge.tsx +39 -0
  60. package/src/components/ui/base-ui-state-attrs.dom.test.tsx +80 -0
  61. package/src/components/ui/button.tsx +51 -0
  62. package/src/components/ui/calendar.tsx +174 -0
  63. package/src/components/ui/card.tsx +74 -0
  64. package/src/components/ui/checkbox.tsx +26 -0
  65. package/src/components/ui/collapsible.tsx +18 -0
  66. package/src/components/ui/command.tsx +160 -0
  67. package/src/components/ui/dialog.tsx +129 -0
  68. package/src/components/ui/dropdown-menu.tsx +254 -0
  69. package/src/components/ui/form.tsx +155 -0
  70. package/src/components/ui/input-otp.tsx +67 -0
  71. package/src/components/ui/input.tsx +20 -0
  72. package/src/components/ui/label.tsx +21 -0
  73. package/src/components/ui/popover.tsx +54 -0
  74. package/src/components/ui/radio-group.tsx +37 -0
  75. package/src/components/ui/scroll-area.tsx +57 -0
  76. package/src/components/ui/select.tsx +213 -0
  77. package/src/components/ui/separator.tsx +23 -0
  78. package/src/components/ui/sheet.tsx +128 -0
  79. package/src/components/ui/sidebar.tsx +702 -0
  80. package/src/components/ui/skeleton.tsx +13 -0
  81. package/src/components/ui/sonner.tsx +26 -0
  82. package/src/components/ui/switch.tsx +25 -0
  83. package/src/components/ui/table.tsx +89 -0
  84. package/src/components/ui/tabs.tsx +51 -0
  85. package/src/components/ui/textarea.tsx +17 -0
  86. package/src/components/ui/tooltip.tsx +65 -0
  87. package/src/config/fonts.ts +2 -0
  88. package/src/context/direction-provider.tsx +61 -0
  89. package/src/context/font-provider.tsx +70 -0
  90. package/src/context/language-provider.tsx +73 -0
  91. package/src/context/layout-provider.tsx +81 -0
  92. package/src/context/search-provider.tsx +48 -0
  93. package/src/context/theme-provider.tsx +108 -0
  94. package/src/hooks/sort-url.test.ts +16 -0
  95. package/src/hooks/use-dialog-state.tsx +15 -0
  96. package/src/hooks/use-mobile.tsx +16 -0
  97. package/src/hooks/use-table-url-state.ts +243 -0
  98. package/src/i18n/common.ts +164 -0
  99. package/src/i18n/create-i18n.test.ts +50 -0
  100. package/src/i18n/create-i18n.ts +67 -0
  101. package/src/i18n/types.ts +22 -0
  102. package/src/i18n/use-translation.ts +6 -0
  103. package/src/lib/clipboard.test.ts +46 -0
  104. package/src/lib/clipboard.ts +29 -0
  105. package/src/lib/cookies.ts +39 -0
  106. package/src/lib/format-cell.test.ts +57 -0
  107. package/src/lib/format-cell.ts +63 -0
  108. package/src/lib/handle-server-error.test.ts +85 -0
  109. package/src/lib/handle-server-error.ts +39 -0
  110. package/src/lib/random-id.ts +13 -0
  111. package/src/lib/show-submitted-data.tsx +14 -0
  112. package/src/lib/utils.test.ts +35 -0
  113. package/src/lib/utils.ts +78 -0
  114. package/src/secure-context.contract.test.ts +63 -0
  115. package/src/testids.contract.test.ts +113 -0
  116. package/src/testids.ts +8 -0
  117. package/src/theme-contract.test.ts +72 -0
@@ -0,0 +1,48 @@
1
+ import { type SVGProps } from "react";
2
+
3
+ export function IconSidebarSidebar(props: SVGProps<SVGSVGElement>) {
4
+ return (
5
+ <svg
6
+ data-name="icon-sidebar-sidebar"
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ viewBox="0 0 79.86 51.14"
9
+ {...props}
10
+ >
11
+ <path
12
+ d="M23.42.51h51.99c2.21 0 4 1.79 4 4v42.18c0 2.21-1.79 4-4 4H23.42s-.04-.02-.04-.04V.55s.02-.04.04-.04z"
13
+ opacity={0.2}
14
+ strokeLinecap="round"
15
+ strokeMiterlimit={10}
16
+ />
17
+ <path
18
+ fill="none"
19
+ opacity={0.72}
20
+ strokeLinecap="round"
21
+ strokeMiterlimit={10}
22
+ strokeWidth="2px"
23
+ d="M5.56 14.88L17.78 14.88"
24
+ />
25
+ <path
26
+ fill="none"
27
+ opacity={0.48}
28
+ strokeLinecap="round"
29
+ strokeMiterlimit={10}
30
+ strokeWidth="2px"
31
+ d="M5.56 22.09L16.08 22.09"
32
+ />
33
+ <path
34
+ fill="none"
35
+ opacity={0.55}
36
+ strokeLinecap="round"
37
+ strokeMiterlimit={10}
38
+ strokeWidth="2px"
39
+ d="M5.56 18.38L14.93 18.38"
40
+ />
41
+ <g strokeLinecap="round" strokeMiterlimit={10}>
42
+ <circle cx={7.51} cy={7.4} r={2.54} opacity={0.8} />
43
+ <path fill="none" opacity={0.8} strokeWidth="2px" d="M12.06 6.14L17.78 6.14" />
44
+ <path fill="none" opacity={0.6} d="M11.85 8.79L16.91 8.79" />
45
+ </g>
46
+ </svg>
47
+ );
48
+ }
@@ -0,0 +1,36 @@
1
+ import { type SVGProps } from "react";
2
+
3
+ export function IconThemeDark(props: SVGProps<SVGSVGElement>) {
4
+ return (
5
+ <svg
6
+ data-name="icon-theme-dark"
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ viewBox="0 0 79.86 51.14"
9
+ {...props}
10
+ >
11
+ <g fill="#1d2b3f">
12
+ <rect x={0.53} y={0.5} width={78.83} height={50.14} rx={3.5} ry={3.5} />
13
+ <path d="M75.86 1c1.65 0 3 1.35 3 3v43.14c0 1.65-1.35 3-3 3H4.03c-1.65 0-3-1.35-3-3V4c0-1.65 1.35-3 3-3h71.83m0-1H4.03c-2.21 0-4 1.79-4 4v43.14c0 2.21 1.79 4 4 4h71.83c2.21 0 4-1.79 4-4V4c0-2.21-1.79-4-4-4z" />
14
+ </g>
15
+ <path d="M22.88 0h52.97c2.21 0 4 1.79 4 4v43.14c0 2.21-1.79 4-4 4H22.88V0z" fill="#0d1628" />
16
+ <circle cx={6.7} cy={7.04} r={3.54} fill="#426187" />
17
+ <path
18
+ d="M18.12 6.39h-5.87c-.6 0-1.09-.45-1.09-1s.49-1 1.09-1h5.87c.6 0 1.09.45 1.09 1s-.49 1-1.09 1zM16.55 9.77h-4.24c-.55 0-1-.45-1-1s.45-1 1-1h4.24c.55 0 1 .45 1 1s-.45 1-1 1zM18.32 17.37H4.59c-.69 0-1.25-.47-1.25-1.05s.56-1.05 1.25-1.05h13.73c.69 0 1.25.47 1.25 1.05s-.56 1.05-1.25 1.05zM15.34 21.26h-11c-.55 0-1-.41-1-.91s.45-.91 1-.91h11c.55 0 1 .41 1 .91s-.45.91-1 .91zM16.46 25.57H4.43c-.6 0-1.09-.44-1.09-.98s.49-.98 1.09-.98h12.03c.6 0 1.09.44 1.09.98s-.49.98-1.09.98z"
19
+ fill="#426187"
20
+ />
21
+ <g fill="#2a62bc">
22
+ <rect x={33.36} y={19.73} width={2.75} height={3.42} rx={0.33} ry={0.33} opacity={0.32} />
23
+ <rect x={29.64} y={16.57} width={2.75} height={6.58} rx={0.33} ry={0.33} opacity={0.44} />
24
+ <rect x={37.16} y={14.44} width={2.75} height={8.7} rx={0.33} ry={0.33} opacity={0.53} />
25
+ <rect x={41.19} y={10.75} width={2.75} height={12.4} rx={0.33} ry={0.33} opacity={0.53} />
26
+ </g>
27
+ <circle cx={62.74} cy={16.32} r={8} fill="#2f5491" opacity={0.5} />
28
+ <path
29
+ d="M62.74 16.32l4.1-6.87c1.19.71 2.18 1.72 2.86 2.92s1.04 2.57 1.04 3.95h-8z"
30
+ fill="#2f5491"
31
+ opacity={0.74}
32
+ />
33
+ <rect x={29.64} y={27.75} width={41.62} height={18.62} rx={1.69} ry={1.69} fill="#17273f" />
34
+ </svg>
35
+ );
36
+ }
@@ -0,0 +1,35 @@
1
+ import { type SVGProps } from "react";
2
+
3
+ export function IconThemeLight(props: SVGProps<SVGSVGElement>) {
4
+ return (
5
+ <svg
6
+ data-name="icon-theme-light"
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ viewBox="0 0 79.86 51.14"
9
+ {...props}
10
+ >
11
+ <g fill="#d9d9d9">
12
+ <rect x={0.53} y={0.5} width={78.83} height={50.14} rx={3.5} ry={3.5} />
13
+ <path d="M75.86 1c1.65 0 3 1.35 3 3v43.14c0 1.65-1.35 3-3 3H4.03c-1.65 0-3-1.35-3-3V4c0-1.65 1.35-3 3-3h71.83m0-1H4.03c-2.21 0-4 1.79-4 4v43.14c0 2.21 1.79 4 4 4h71.83c2.21 0 4-1.79 4-4V4c0-2.21-1.79-4-4-4z" />
14
+ </g>
15
+ <path d="M22.88 0h52.97c2.21 0 4 1.79 4 4v43.14c0 2.21-1.79 4-4 4H22.88V0z" fill="#ecedef" />
16
+ <circle cx={6.7} cy={7.04} r={3.54} fill="#fff" />
17
+ <path
18
+ d="M18.12 6.39h-5.87c-.6 0-1.09-.45-1.09-1s.49-1 1.09-1h5.87c.6 0 1.09.45 1.09 1s-.49 1-1.09 1zM16.55 9.77h-4.24c-.55 0-1-.45-1-1s.45-1 1-1h4.24c.55 0 1 .45 1 1s-.45 1-1 1zM18.32 17.37H4.59c-.69 0-1.25-.47-1.25-1.05s.56-1.05 1.25-1.05h13.73c.69 0 1.25.47 1.25 1.05s-.56 1.05-1.25 1.05zM15.34 21.26h-11c-.55 0-1-.41-1-.91s.45-.91 1-.91h11c.55 0 1 .41 1 .91s-.45.91-1 .91zM16.46 25.57H4.43c-.6 0-1.09-.44-1.09-.98s.49-.98 1.09-.98h12.03c.6 0 1.09.44 1.09.98s-.49.98-1.09.98z"
19
+ fill="#fff"
20
+ />
21
+ <g fill="#c0c4c4">
22
+ <rect x={33.36} y={19.73} width={2.75} height={3.42} rx={0.33} ry={0.33} opacity={0.32} />
23
+ <rect x={29.64} y={16.57} width={2.75} height={6.58} rx={0.33} ry={0.33} opacity={0.44} />
24
+ <rect x={37.16} y={14.44} width={2.75} height={8.7} rx={0.33} ry={0.33} opacity={0.53} />
25
+ <rect x={41.19} y={10.75} width={2.75} height={12.4} rx={0.33} ry={0.33} opacity={0.53} />
26
+ </g>
27
+ <circle cx={62.74} cy={16.32} r={8} fill="#fff" />
28
+ <g fill="#d9d9d9">
29
+ <path d="M63.62 15.82L67 10.15c.93.64 1.7 1.48 2.26 2.47.56.98.89 2.08.96 3.21h-6.6z" />
30
+ <path d="M67.14 10.88a6.977 6.977 0 012.52 4.44h-5.17l2.65-4.44m-.31-1.43l-4.1 6.87h8c0-1.39-.36-2.75-1.04-3.95s-1.67-2.21-2.86-2.92z" />
31
+ </g>
32
+ <rect x={29.64} y={27.75} width={41.62} height={18.62} rx={1.69} ry={1.69} fill="#fff" />
33
+ </svg>
34
+ );
35
+ }
@@ -0,0 +1,113 @@
1
+ import { cn } from "@icomponents/react/lib/utils";
2
+ import { type SVGProps } from "react";
3
+
4
+ // Раскраску по состоянию радио-кнопки задаёт вызывающий (config-drawer) — одинаково для всех трёх
5
+ // иконок темы, через group-data-checked/group-data-unchecked. Здесь её дублировать не нужно: раньше
6
+ // тут висел радиксовый `group-data-[state=unchecked]`, которого Base UI не выставляет — селектор был
7
+ // мёртв, а light/dark-иконки такой строки и не имели.
8
+ export function IconThemeSystem({ className, ...props }: SVGProps<SVGSVGElement>) {
9
+ return (
10
+ <svg
11
+ data-name="icon-theme-system"
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ viewBox="0 0 79.86 51.14"
14
+ className={cn("overflow-hidden rounded-[6px]", className)}
15
+ {...props}
16
+ >
17
+ <path opacity={0.2} d="M0 0.03H22.88V51.17H0z" />
18
+ <circle
19
+ cx={6.7}
20
+ cy={7.04}
21
+ r={3.54}
22
+ fill="#fff"
23
+ opacity={0.8}
24
+ stroke="#fff"
25
+ strokeLinecap="round"
26
+ strokeMiterlimit={10}
27
+ />
28
+ <path
29
+ d="M18.12 6.39h-5.87c-.6 0-1.09-.45-1.09-1s.49-1 1.09-1h5.87c.6 0 1.09.45 1.09 1s-.49 1-1.09 1zM16.55 9.77h-4.24c-.55 0-1-.45-1-1s.45-1 1-1h4.24c.55 0 1 .45 1 1s-.45 1-1 1z"
30
+ fill="#fff"
31
+ stroke="none"
32
+ opacity={0.75}
33
+ />
34
+ <path
35
+ d="M18.32 17.37H4.59c-.69 0-1.25-.47-1.25-1.05s.56-1.05 1.25-1.05h13.73c.69 0 1.25.47 1.25 1.05s-.56 1.05-1.25 1.05z"
36
+ fill="#fff"
37
+ stroke="none"
38
+ opacity={0.72}
39
+ />
40
+ <path
41
+ d="M15.34 21.26h-11c-.55 0-1-.41-1-.91s.45-.91 1-.91h11c.55 0 1 .41 1 .91s-.45.91-1 .91z"
42
+ fill="#fff"
43
+ stroke="none"
44
+ opacity={0.55}
45
+ />
46
+ <path
47
+ d="M16.46 25.57H4.43c-.6 0-1.09-.44-1.09-.98s.49-.98 1.09-.98h12.03c.6 0 1.09.44 1.09.98s-.49.98-1.09.98z"
48
+ fill="#fff"
49
+ stroke="none"
50
+ opacity={0.67}
51
+ />
52
+ <rect
53
+ x={33.36}
54
+ y={19.73}
55
+ width={2.75}
56
+ height={3.42}
57
+ rx={0.33}
58
+ ry={0.33}
59
+ opacity={0.31}
60
+ stroke="none"
61
+ />
62
+ <rect
63
+ x={29.64}
64
+ y={16.57}
65
+ width={2.75}
66
+ height={6.58}
67
+ rx={0.33}
68
+ ry={0.33}
69
+ opacity={0.4}
70
+ stroke="none"
71
+ />
72
+ <rect
73
+ x={37.16}
74
+ y={14.44}
75
+ width={2.75}
76
+ height={8.7}
77
+ rx={0.33}
78
+ ry={0.33}
79
+ opacity={0.26}
80
+ stroke="none"
81
+ />
82
+ <rect
83
+ x={41.19}
84
+ y={10.75}
85
+ width={2.75}
86
+ height={12.4}
87
+ rx={0.33}
88
+ ry={0.33}
89
+ opacity={0.37}
90
+ stroke="none"
91
+ />
92
+ <g>
93
+ <circle cx={62.74} cy={16.32} r={8} opacity={0.25} />
94
+ <path
95
+ d="M62.74 16.32l4.1-6.87c1.19.71 2.18 1.72 2.86 2.92s1.04 2.57 1.04 3.95h-8z"
96
+ opacity={0.45}
97
+ />
98
+ </g>
99
+ <rect
100
+ x={29.64}
101
+ y={27.75}
102
+ width={41.62}
103
+ height={18.62}
104
+ rx={1.69}
105
+ ry={1.69}
106
+ opacity={0.3}
107
+ stroke="none"
108
+ strokeLinecap="round"
109
+ strokeMiterlimit={10}
110
+ />
111
+ </svg>
112
+ );
113
+ }
@@ -0,0 +1,29 @@
1
+ import { TESTID, testid } from "@icomponents/react/testids";
2
+ import type { ReactNode } from "react";
3
+
4
+ // The title/description/primary-action row shared by every admin page shell (§3). Extracted
5
+ // verbatim from companies-page.tsx so adopting slices drop their inline copy for <AdminPageHeader>.
6
+ export type AdminPageHeaderProps = {
7
+ title: string;
8
+ description: string;
9
+ action?: ReactNode;
10
+ };
11
+
12
+ /**
13
+ * Шапка экрана. Несёт `data-testid` контракта (см. `@icomponents/react/testids`): по ним тесты узнают,
14
+ * что экран смонтировался, — независимо от того, наша это вёрстка или вёрстка прототипа, в которую
15
+ * вайрится логика.
16
+ */
17
+ export function AdminPageHeader({ title, description, action }: AdminPageHeaderProps) {
18
+ return (
19
+ <div className="flex flex-wrap items-end justify-between gap-2">
20
+ <div>
21
+ <h2 {...testid(TESTID.screenTitle)} className="text-2xl font-bold tracking-tight">
22
+ {title}
23
+ </h2>
24
+ <p className="text-muted-foreground">{description}</p>
25
+ </div>
26
+ {action}
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,93 @@
1
+ import { Button } from "@icomponents/react/components/ui/button";
2
+ import {
3
+ DropdownMenu,
4
+ DropdownMenuContent,
5
+ DropdownMenuGroup,
6
+ DropdownMenuItem,
7
+ DropdownMenuTrigger,
8
+ } from "@icomponents/react/components/ui/dropdown-menu";
9
+ import { CheckIcon, PaletteIcon } from "lucide-react";
10
+ import { useState } from "react";
11
+
12
+ /** One selectable color theme (palette). `id` maps to a `[data-theme="<id>"]` block in the app's
13
+ * theme.css; the option whose id is "default" clears the attribute (i.e. the base :root/.dark theme). */
14
+ export type ColorThemeOption = { id: string; label: string };
15
+
16
+ /** localStorage key shared with the no-flash inline script in index.html / spa-fallback.html. */
17
+ export const COLOR_THEME_STORAGE_KEY = "color-theme";
18
+
19
+ function getSavedColorTheme(themes: ColorThemeOption[], fallback: string): string {
20
+ try {
21
+ const saved = localStorage.getItem(COLOR_THEME_STORAGE_KEY);
22
+ return themes.some((t) => t.id === saved) ? (saved as string) : fallback;
23
+ } catch {
24
+ return fallback;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Color-theme (palette) switcher — orthogonal to the light/dark `ThemeSwitch`.
30
+ *
31
+ * Sets `data-theme="<id>"` on <html> and persists to localStorage["color-theme"]. The option with
32
+ * id "default" (or the first option when none is "default") clears the attribute, falling back to the
33
+ * base :root/.dark theme. Each palette lives in the app's theme.css as `[data-theme="<id>"]` (+ its
34
+ * `.dark` variant), so it composes with the light/dark class. Pair with the no-flash inline script in
35
+ * index.html so the saved theme applies before first paint.
36
+ *
37
+ * The theme list is product-specific and passed in — the mechanism is generic.
38
+ */
39
+ export function ColorThemeSwitch({
40
+ themes,
41
+ ariaLabel = "Color theme",
42
+ }: {
43
+ themes: ColorThemeOption[];
44
+ ariaLabel?: string;
45
+ }) {
46
+ const fallback = themes[0]?.id ?? "default";
47
+ const [colorTheme, setColorTheme] = useState<string>(() => getSavedColorTheme(themes, fallback));
48
+
49
+ const selectColorTheme = (id: string) => {
50
+ try {
51
+ localStorage.setItem(COLOR_THEME_STORAGE_KEY, id);
52
+ } catch {
53
+ /* storage unavailable — still apply for this session */
54
+ }
55
+ if (id === "default") document.documentElement.removeAttribute("data-theme");
56
+ else document.documentElement.setAttribute("data-theme", id);
57
+ setColorTheme(id);
58
+ };
59
+
60
+ // Nothing to switch between — don't render the control.
61
+ if (themes.length < 2) return null;
62
+
63
+ return (
64
+ <DropdownMenu>
65
+ <DropdownMenuTrigger
66
+ render={
67
+ <Button
68
+ aria-label={ariaLabel}
69
+ data-testid="color-theme-switch"
70
+ size="icon"
71
+ variant="ghost"
72
+ />
73
+ }
74
+ >
75
+ <PaletteIcon />
76
+ </DropdownMenuTrigger>
77
+ <DropdownMenuContent align="end">
78
+ <DropdownMenuGroup>
79
+ {themes.map(({ id, label }) => (
80
+ <DropdownMenuItem
81
+ data-testid={`color-theme-${id}`}
82
+ key={id}
83
+ onClick={() => selectColorTheme(id)}
84
+ >
85
+ {label}
86
+ {colorTheme === id && <CheckIcon className="ms-auto" />}
87
+ </DropdownMenuItem>
88
+ ))}
89
+ </DropdownMenuGroup>
90
+ </DropdownMenuContent>
91
+ </DropdownMenu>
92
+ );
93
+ }
@@ -0,0 +1,16 @@
1
+ import { Telescope } from "lucide-react";
2
+
3
+ export function ComingSoon() {
4
+ return (
5
+ <div className="h-svh">
6
+ <div className="m-auto flex h-full w-full flex-col items-center justify-center gap-2">
7
+ <Telescope size={72} />
8
+ <h1 className="text-4xl leading-tight font-bold">Coming Soon!</h1>
9
+ <p className="text-center text-muted-foreground">
10
+ This page has not been created yet. <br />
11
+ Stay tuned though!
12
+ </p>
13
+ </div>
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,98 @@
1
+ import {
2
+ CommandDialog,
3
+ CommandEmpty,
4
+ CommandGroup,
5
+ CommandInput,
6
+ CommandItem,
7
+ CommandList,
8
+ CommandSeparator,
9
+ } from "@icomponents/react/components/ui/command";
10
+ import { useSearch } from "@icomponents/react/context/search-provider";
11
+ import { useTheme } from "@icomponents/react/context/theme-provider";
12
+ import { useTranslation } from "@icomponents/react/i18n/use-translation";
13
+ import { useNavigate } from "@tanstack/react-router";
14
+ import { ArrowRight, ChevronRight, Laptop, Moon, Sun } from "lucide-react";
15
+ import React from "react";
16
+
17
+ import { type NavGroup } from "./layout/types";
18
+ import { ScrollArea } from "./ui/scroll-area";
19
+
20
+ type CommandMenuProps = {
21
+ navGroups: NavGroup[];
22
+ };
23
+
24
+ export function CommandMenu({ navGroups }: CommandMenuProps) {
25
+ const navigate = useNavigate();
26
+ const { setTheme } = useTheme();
27
+ const { open, setOpen } = useSearch();
28
+ const { t } = useTranslation("common");
29
+
30
+ const runCommand = React.useCallback(
31
+ (command: () => unknown) => {
32
+ setOpen(false);
33
+ command();
34
+ },
35
+ [setOpen]
36
+ );
37
+
38
+ return (
39
+ <CommandDialog modal open={open} onOpenChange={setOpen}>
40
+ <CommandInput placeholder={t("commandMenu.placeholder")} />
41
+ <CommandList>
42
+ <ScrollArea className="h-72 pe-1">
43
+ <CommandEmpty>{t("noResults")}</CommandEmpty>
44
+ {navGroups.map((group) => (
45
+ <CommandGroup key={group.title} heading={group.title}>
46
+ {group.items.map((navItem, i) => {
47
+ if (navItem.url)
48
+ return (
49
+ <CommandItem
50
+ key={`${navItem.url}-${i}`}
51
+ value={navItem.title}
52
+ onSelect={() => {
53
+ runCommand(() => navigate({ to: navItem.url }));
54
+ }}
55
+ >
56
+ <div className="flex size-4 items-center justify-center">
57
+ <ArrowRight className="size-2 text-muted-foreground/80" />
58
+ </div>
59
+ {navItem.title}
60
+ </CommandItem>
61
+ );
62
+
63
+ return navItem.items?.map((subItem, i) => (
64
+ <CommandItem
65
+ key={`${navItem.title}-${subItem.url}-${i}`}
66
+ value={`${navItem.title}-${subItem.url}`}
67
+ onSelect={() => {
68
+ runCommand(() => navigate({ to: subItem.url }));
69
+ }}
70
+ >
71
+ <div className="flex size-4 items-center justify-center">
72
+ <ArrowRight className="size-2 text-muted-foreground/80" />
73
+ </div>
74
+ {navItem.title} <ChevronRight /> {subItem.title}
75
+ </CommandItem>
76
+ ));
77
+ })}
78
+ </CommandGroup>
79
+ ))}
80
+ <CommandSeparator />
81
+ <CommandGroup heading={t("theme.title")}>
82
+ <CommandItem onSelect={() => runCommand(() => setTheme("light"))}>
83
+ <Sun /> <span>{t("theme.light")}</span>
84
+ </CommandItem>
85
+ <CommandItem onSelect={() => runCommand(() => setTheme("dark"))}>
86
+ <Moon className="scale-90" />
87
+ <span>{t("theme.dark")}</span>
88
+ </CommandItem>
89
+ <CommandItem onSelect={() => runCommand(() => setTheme("system"))}>
90
+ <Laptop />
91
+ <span>{t("theme.system")}</span>
92
+ </CommandItem>
93
+ </CommandGroup>
94
+ </ScrollArea>
95
+ </CommandList>
96
+ </CommandDialog>
97
+ );
98
+ }