@terrafirmagreg-team/ui 0.0.0-stage → 1.2.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 (225) hide show
  1. package/package.json +93 -3
  2. package/src/components/blocks/button/icon-button.tsx +49 -0
  3. package/src/components/blocks/canvas/chevron-edge.tsx +242 -0
  4. package/src/components/blocks/canvas/flow-canvas.tsx +181 -0
  5. package/src/components/blocks/canvas/flow-toolbar.tsx +33 -0
  6. package/src/components/blocks/canvas/flow-viewport.tsx +244 -0
  7. package/src/components/blocks/fit-box.tsx +26 -0
  8. package/src/components/blocks/git/github-info.tsx +78 -0
  9. package/src/components/blocks/lang/content.tsx +52 -0
  10. package/src/components/blocks/lang/flag.tsx +25 -0
  11. package/src/components/blocks/lang/link.tsx +23 -0
  12. package/src/components/blocks/lang/redirect.tsx +48 -0
  13. package/src/components/blocks/lang/switcher.tsx +54 -0
  14. package/src/components/blocks/lang/untranslated-callout.tsx +29 -0
  15. package/src/components/blocks/lang/untranslated-chip.tsx +53 -0
  16. package/src/components/blocks/link/link-preview.tsx +257 -0
  17. package/src/components/blocks/list/row.tsx +92 -0
  18. package/src/components/blocks/media/glb/model.tsx +244 -0
  19. package/src/components/blocks/media/glb/scene.tsx +373 -0
  20. package/src/components/blocks/media/glb/viewer.tsx +52 -0
  21. package/src/components/blocks/media/image.tsx +136 -0
  22. package/src/components/blocks/media/slideshow.tsx +178 -0
  23. package/src/components/blocks/nav/breadcrumb.tsx +51 -0
  24. package/src/components/blocks/nav/copy-menu.tsx +37 -0
  25. package/src/components/blocks/nav/link.tsx +86 -0
  26. package/src/components/blocks/nav/site-menu.tsx +78 -0
  27. package/src/components/blocks/nav/site-title.tsx +61 -0
  28. package/src/components/blocks/nav/switcher.tsx +206 -0
  29. package/src/components/blocks/page/empty-state.tsx +32 -0
  30. package/src/components/blocks/page/heading.tsx +54 -0
  31. package/src/components/blocks/page/pager.tsx +220 -0
  32. package/src/components/blocks/readability/fields.tsx +250 -0
  33. package/src/components/blocks/readability/menu.tsx +247 -0
  34. package/src/components/blocks/readability/track.tsx +165 -0
  35. package/src/components/blocks/search/command.tsx +80 -0
  36. package/src/components/blocks/search/dialog.tsx +724 -0
  37. package/src/components/blocks/search/provider.tsx +118 -0
  38. package/src/components/blocks/sidebar.tsx +406 -0
  39. package/src/components/blocks/sprite.tsx +231 -0
  40. package/src/components/blocks/text/format-text.tsx +110 -0
  41. package/src/components/blocks/theme/toggle.tsx +75 -0
  42. package/src/components/blocks/tooltip/hint.tsx +28 -0
  43. package/src/components/blocks/tooltip/lazy.tsx +41 -0
  44. package/src/components/blocks/viewer/controls.tsx +284 -0
  45. package/src/components/blocks/viewer/float-bar.tsx +51 -0
  46. package/src/components/layouts/layout.tsx +94 -0
  47. package/src/components/layouts/left-sidebar/content.tsx +63 -0
  48. package/src/components/layouts/left-sidebar/context.tsx +55 -0
  49. package/src/components/layouts/left-sidebar/expand.tsx +23 -0
  50. package/src/components/layouts/left-sidebar/footer.tsx +111 -0
  51. package/src/components/layouts/left-sidebar/index.ts +29 -0
  52. package/src/components/layouts/left-sidebar/items.tsx +103 -0
  53. package/src/components/layouts/left-sidebar/search.tsx +75 -0
  54. package/src/components/layouts/left-sidebar/sidebar.tsx +189 -0
  55. package/src/components/layouts/left-sidebar/tree.tsx +157 -0
  56. package/src/components/layouts/right-sidebar.tsx +96 -0
  57. package/src/components/layouts/tree-context.tsx +42 -0
  58. package/src/components/mdx/accordion.tsx +185 -0
  59. package/src/components/mdx/banner.tsx +157 -0
  60. package/src/components/mdx/callout.tsx +97 -0
  61. package/src/components/mdx/card.tsx +56 -0
  62. package/src/components/mdx/codeblock.tsx +250 -0
  63. package/src/components/mdx/collapsible.tsx +33 -0
  64. package/src/components/mdx/files.tsx +63 -0
  65. package/src/components/mdx/glow-header.tsx +54 -0
  66. package/src/components/mdx/gradient-text.tsx +28 -0
  67. package/src/components/mdx/icon.tsx +35 -0
  68. package/src/components/mdx/image-zoom.tsx +315 -0
  69. package/src/components/mdx/math.tsx +30 -0
  70. package/src/components/mdx/mermaid.tsx +23 -0
  71. package/src/components/mdx/multiblock.tsx +19 -0
  72. package/src/components/mdx/preview.tsx +101 -0
  73. package/src/components/mdx/steps.tsx +10 -0
  74. package/src/components/mdx/table.tsx +14 -0
  75. package/src/components/mdx/tabs.tsx +354 -0
  76. package/src/components/mdx/type-table.tsx +171 -0
  77. package/src/components/provider.tsx +139 -0
  78. package/src/components/ui/accordion.tsx +71 -0
  79. package/src/components/ui/alert-dialog.tsx +187 -0
  80. package/src/components/ui/alert.tsx +75 -0
  81. package/src/components/ui/aspect-ratio.tsx +22 -0
  82. package/src/components/ui/attachment.tsx +207 -0
  83. package/src/components/ui/avatar.tsx +108 -0
  84. package/src/components/ui/badge.tsx +51 -0
  85. package/src/components/ui/breadcrumb.tsx +124 -0
  86. package/src/components/ui/bubble.tsx +127 -0
  87. package/src/components/ui/button-group.tsx +87 -0
  88. package/src/components/ui/button.tsx +57 -0
  89. package/src/components/ui/calendar.tsx +221 -0
  90. package/src/components/ui/card.tsx +102 -0
  91. package/src/components/ui/carousel.tsx +242 -0
  92. package/src/components/ui/chart.tsx +371 -0
  93. package/src/components/ui/checkbox.tsx +28 -0
  94. package/src/components/ui/collapsible.tsx +21 -0
  95. package/src/components/ui/combobox.tsx +297 -0
  96. package/src/components/ui/command.tsx +198 -0
  97. package/src/components/ui/context-menu.tsx +270 -0
  98. package/src/components/ui/dialog.tsx +160 -0
  99. package/src/components/ui/direction.tsx +6 -0
  100. package/src/components/ui/drawer.tsx +227 -0
  101. package/src/components/ui/dropdown-menu.tsx +269 -0
  102. package/src/components/ui/empty.tsx +103 -0
  103. package/src/components/ui/field.tsx +238 -0
  104. package/src/components/ui/hover-card.tsx +50 -0
  105. package/src/components/ui/input-group.tsx +158 -0
  106. package/src/components/ui/input-otp.tsx +86 -0
  107. package/src/components/ui/input.tsx +19 -0
  108. package/src/components/ui/item.tsx +201 -0
  109. package/src/components/ui/kbd.tsx +26 -0
  110. package/src/components/ui/label.tsx +19 -0
  111. package/src/components/ui/marker.tsx +70 -0
  112. package/src/components/ui/menubar.tsx +280 -0
  113. package/src/components/ui/message-scroller.tsx +131 -0
  114. package/src/components/ui/message.tsx +91 -0
  115. package/src/components/ui/native-select.tsx +60 -0
  116. package/src/components/ui/navigation-menu.tsx +167 -0
  117. package/src/components/ui/pagination.tsx +130 -0
  118. package/src/components/ui/popover.tsx +91 -0
  119. package/src/components/ui/progress.tsx +82 -0
  120. package/src/components/ui/questionnaire.tsx +324 -0
  121. package/src/components/ui/radio-group.tsx +37 -0
  122. package/src/components/ui/resizable.tsx +49 -0
  123. package/src/components/ui/scroll-area.tsx +54 -0
  124. package/src/components/ui/select.tsx +200 -0
  125. package/src/components/ui/separator.tsx +24 -0
  126. package/src/components/ui/sheet.tsx +138 -0
  127. package/src/components/ui/sidebar.tsx +730 -0
  128. package/src/components/ui/skeleton.tsx +13 -0
  129. package/src/components/ui/slider.tsx +51 -0
  130. package/src/components/ui/spinner.tsx +10 -0
  131. package/src/components/ui/switch.tsx +31 -0
  132. package/src/components/ui/table.tsx +115 -0
  133. package/src/components/ui/tabs.tsx +81 -0
  134. package/src/components/ui/textarea.tsx +17 -0
  135. package/src/components/ui/toast.tsx +234 -0
  136. package/src/components/ui/toggle-group.tsx +89 -0
  137. package/src/components/ui/toggle.tsx +44 -0
  138. package/src/components/ui/tooltip.tsx +65 -0
  139. package/src/hooks/canvas/use-flow-hover.ts +19 -0
  140. package/src/hooks/canvas/use-flow-viewport-persist.ts +34 -0
  141. package/src/hooks/page/use-fit-pages.ts +92 -0
  142. package/src/hooks/page/use-location-search.ts +31 -0
  143. package/src/hooks/page/use-page-preview.ts +41 -0
  144. package/src/hooks/page/use-paged-list.ts +21 -0
  145. package/src/hooks/readability/use-readability.ts +87 -0
  146. package/src/hooks/search/use-dialog.ts +42 -0
  147. package/src/hooks/search/use-highlight-query.ts +58 -0
  148. package/src/hooks/search/use-provider.ts +26 -0
  149. package/src/hooks/use-async-value.ts +60 -0
  150. package/src/hooks/use-close-on-scroll.ts +24 -0
  151. package/src/hooks/use-copy-button.ts +47 -0
  152. package/src/hooks/use-cycle-index.ts +76 -0
  153. package/src/hooks/use-document-title.ts +10 -0
  154. package/src/hooks/use-element-size.ts +36 -0
  155. package/src/hooks/use-external-meta.ts +133 -0
  156. package/src/hooks/use-hot-key.ts +34 -0
  157. package/src/hooks/use-hover-follow.ts +55 -0
  158. package/src/hooks/use-in-view.ts +31 -0
  159. package/src/hooks/use-lang.ts +9 -0
  160. package/src/hooks/use-last-value.ts +16 -0
  161. package/src/hooks/use-layout.ts +19 -0
  162. package/src/hooks/use-marquee.ts +52 -0
  163. package/src/hooks/use-media-query.ts +15 -0
  164. package/src/hooks/use-mobile.ts +20 -0
  165. package/src/hooks/use-modifier-keys.ts +32 -0
  166. package/src/hooks/use-on-change.ts +36 -0
  167. package/src/hooks/use-pane-keys.ts +29 -0
  168. package/src/hooks/use-sidebar.ts +64 -0
  169. package/src/hooks/use-swipe.ts +27 -0
  170. package/src/hooks/use-tree.ts +100 -0
  171. package/src/hooks/viewer/use-in-float-bar.ts +7 -0
  172. package/src/hooks/viewer/use-viewer-prefs.ts +78 -0
  173. package/src/lib/canvas/edge.ts +202 -0
  174. package/src/lib/canvas/geometry.ts +54 -0
  175. package/src/lib/canvas/state.ts +48 -0
  176. package/src/lib/canvas/viewport.ts +38 -0
  177. package/src/lib/canvas/xyflow.ts +2 -0
  178. package/src/lib/config.ts +106 -0
  179. package/src/lib/mdx/callout.ts +21 -0
  180. package/src/lib/mdx/gradient.ts +16 -0
  181. package/src/lib/mdx/mermaid.ts +15 -0
  182. package/src/lib/media/crop-opaque.ts +77 -0
  183. package/src/lib/media/nearby-indexes.ts +7 -0
  184. package/src/lib/media/trim-transparent.ts +118 -0
  185. package/src/lib/readability/config.ts +11 -0
  186. package/src/lib/readability/spotlight.ts +187 -0
  187. package/src/lib/readability/store.ts +147 -0
  188. package/src/lib/tree.ts +90 -0
  189. package/src/lib/utils/cn.ts +2 -0
  190. package/src/lib/utils/format.ts +22 -0
  191. package/src/lib/utils/highlight-style.ts +10 -0
  192. package/src/lib/utils/history.ts +10 -0
  193. package/src/lib/utils/merge-refs.ts +14 -0
  194. package/src/lib/utils/nav-active.ts +59 -0
  195. package/src/lib/utils/stored-value.ts +137 -0
  196. package/src/lib/utils/urls.ts +18 -0
  197. package/src/locales/en_us.json +110 -0
  198. package/src/locales/pl_pl.json +33 -0
  199. package/src/locales/ru_ru.json +110 -0
  200. package/src/locales/uk_ua.json +33 -0
  201. package/src/locales/zh_cn.json +15 -0
  202. package/src/locales/zh_tw.json +15 -0
  203. package/src/locales.ts +6 -0
  204. package/src/types/canvas.ts +54 -0
  205. package/src/types/glb.ts +26 -0
  206. package/src/types/mdx-directives.ts +106 -0
  207. package/src/types/nav.ts +96 -0
  208. package/src/types/preview.ts +15 -0
  209. package/src/types/readability.ts +29 -0
  210. package/src/types/search.ts +57 -0
  211. package/src/types/tree.ts +19 -0
  212. package/src/types/viewer.ts +54 -0
  213. package/styles/content/base.css +198 -0
  214. package/styles/content/default-colors.css +51 -0
  215. package/styles/content/shiki.css +107 -0
  216. package/styles/content/typography.mjs +3637 -0
  217. package/styles/content.css +5 -0
  218. package/styles/image-zoom.css +83 -0
  219. package/styles/layout.css +28 -0
  220. package/styles/left-sidebar.css +140 -0
  221. package/styles/readability.css +50 -0
  222. package/styles/right-sidebar.css +6 -0
  223. package/styles/workspace.css +26 -0
  224. package/styles.css +430 -0
  225. package/README.md +0 -3
@@ -0,0 +1,231 @@
1
+ 'use client';
2
+
3
+ import type { CSSProperties, ReactNode } from 'react';
4
+ import { Skeleton } from '@terrafirmagreg-team/ui/components/ui/skeleton';
5
+ import { formatCount } from '@terrafirmagreg-team/ui/lib/utils/format';
6
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
7
+
8
+ export const SPRITE_CHIP_CLASS =
9
+ 'inline-flex shrink-0 items-center justify-center rounded-sm border border-border bg-secondary p-0.5 hover:bg-accent';
10
+
11
+ export function SpriteFrame({
12
+ size,
13
+ chip = false,
14
+ badge,
15
+ badgeClassName,
16
+ className,
17
+ style,
18
+ children,
19
+ }: {
20
+ size?: number;
21
+ chip?: boolean;
22
+ badge?: ReactNode;
23
+ badgeClassName?: string;
24
+ className?: string;
25
+ style?: CSSProperties;
26
+ children?: ReactNode;
27
+ }) {
28
+ return (
29
+ <span
30
+ className={cn(
31
+ 'inline-flex shrink-0 items-center justify-center',
32
+ chip && SPRITE_CHIP_CLASS,
33
+ badge !== undefined && 'relative',
34
+ className,
35
+ )}
36
+ style={{ ...(size ? { width: size, height: size } : undefined), ...style }}
37
+ >
38
+ {children}
39
+ {badge !== undefined ? (
40
+ <span
41
+ className={cn(
42
+ 'pointer-events-none absolute -right-0.5 -bottom-0.5 z-10',
43
+ badgeClassName,
44
+ )}
45
+ >
46
+ {badge}
47
+ </span>
48
+ ) : null}
49
+ </span>
50
+ );
51
+ }
52
+
53
+ export function Sprite({
54
+ src,
55
+ rect,
56
+ page,
57
+ size,
58
+ tint,
59
+ alt = '',
60
+ className,
61
+ fallback,
62
+ onError,
63
+ }: {
64
+ src?: string;
65
+
66
+ rect?: { x: number; y: number; w: number };
67
+
68
+ page?: { w: number; h: number };
69
+ size: number;
70
+ tint?: string;
71
+ alt?: string;
72
+ className?: string;
73
+ fallback?: ReactNode;
74
+ onError?: () => void;
75
+ }) {
76
+ if (!src) {
77
+ return (
78
+ fallback ?? (
79
+ <Skeleton
80
+ className={cn('inline-block shrink-0', className)}
81
+ style={{ width: size, height: size }}
82
+ />
83
+ )
84
+ );
85
+ }
86
+ if (rect && page) {
87
+ const scale = size / Math.max(rect.w, 1);
88
+ const pos = `-${rect.x * scale}px -${rect.y * scale}px`;
89
+ const bgSize = `${page.w * scale}px ${page.h * scale}px`;
90
+ return (
91
+ <span
92
+ className={className ?? 'relative inline-block shrink-0'}
93
+ style={{
94
+ width: size,
95
+ height: size,
96
+ backgroundImage: `url(${src})`,
97
+ backgroundPosition: pos,
98
+ backgroundSize: bgSize,
99
+ imageRendering: 'pixelated',
100
+ ...(tint
101
+ ? {
102
+ backgroundColor: tint,
103
+ backgroundBlendMode: 'multiply',
104
+ WebkitMaskImage: `url(${src})`,
105
+ maskImage: `url(${src})`,
106
+ WebkitMaskPosition: pos,
107
+ maskPosition: pos,
108
+ WebkitMaskSize: bgSize,
109
+ maskSize: bgSize,
110
+ WebkitMaskRepeat: 'no-repeat',
111
+ maskRepeat: 'no-repeat',
112
+ }
113
+ : {}),
114
+ }}
115
+ />
116
+ );
117
+ }
118
+ return (
119
+ // eslint-disable-next-line @next/next/no-img-element
120
+ <img
121
+ src={src}
122
+ alt={alt}
123
+ width={size}
124
+ height={size}
125
+ className={cn('rounded-sm', className)}
126
+ style={{ width: size, height: size, imageRendering: 'pixelated' }}
127
+ loading="lazy"
128
+ decoding="async"
129
+ onError={onError}
130
+ />
131
+ );
132
+ }
133
+
134
+ export const SPRITE_CLUSTER_UNITS = 18;
135
+ export const SPRITE_CLUSTER_INSET = 6;
136
+ const CLUSTER_BLEED = 1.15;
137
+ const CLUSTER_POSITIONS: readonly (readonly [number, number])[][] = [
138
+ [],
139
+ [[1.6, 1.6]],
140
+ [
141
+ [0.4, 0.8],
142
+ [2.8, 2.4],
143
+ ],
144
+ [
145
+ [-0.8, 0],
146
+ [1.6, 1.6],
147
+ [4.0, 3.2],
148
+ ],
149
+ ];
150
+
151
+ export function SpriteCluster({
152
+ items,
153
+ size = 36,
154
+ chip = true,
155
+ inset = SPRITE_CLUSTER_INSET,
156
+ limit = 3,
157
+ }: {
158
+ items: ReactNode[];
159
+ size?: number;
160
+ chip?: boolean;
161
+ inset?: number;
162
+ limit?: number;
163
+ }) {
164
+ const inner = size - (chip ? inset : 0);
165
+ const scale = inner / SPRITE_CLUSTER_UNITS;
166
+ const shown = items.slice(0, Math.min(limit, CLUSTER_POSITIONS.length - 1));
167
+ const positions = CLUSTER_POSITIONS[shown.length];
168
+ return (
169
+ <SpriteFrame size={size} chip={chip}>
170
+ <span className="relative block" style={{ width: inner, height: inner }}>
171
+ {shown.map((node, i) => (
172
+ <span
173
+ key={i}
174
+ className="absolute"
175
+ style={{
176
+ left: positions[i][0] * CLUSTER_BLEED * scale,
177
+ top: positions[i][1] * CLUSTER_BLEED * scale,
178
+ zIndex: shown.length - i,
179
+ }}
180
+ >
181
+ {node}
182
+ </span>
183
+ ))}
184
+ </span>
185
+ </SpriteFrame>
186
+ );
187
+ }
188
+
189
+ export function SpriteCount({
190
+ count,
191
+ limit,
192
+ className,
193
+ style,
194
+ }: {
195
+ count: number;
196
+ limit?: number;
197
+ className?: string;
198
+ style?: CSSProperties;
199
+ }) {
200
+ if (count < 1) return null;
201
+ return (
202
+ <span aria-hidden className={className} style={style}>
203
+ {formatCount(count, limit)}
204
+ </span>
205
+ );
206
+ }
207
+
208
+ export function SpriteList({
209
+ items,
210
+ variant = 'grid',
211
+ className,
212
+ }: {
213
+ items: { key: React.Key; node: ReactNode }[];
214
+ variant?: 'grid' | 'row';
215
+ className?: string;
216
+ }) {
217
+ return (
218
+ <ul
219
+ className={cn(
220
+ variant === 'grid'
221
+ ? 'grid grid-cols-[repeat(auto-fill,minmax(--spacing(9),1fr))] gap-1'
222
+ : 'flex flex-wrap items-center gap-3',
223
+ className,
224
+ )}
225
+ >
226
+ {items.map((item) => (
227
+ <li key={item.key}>{item.node}</li>
228
+ ))}
229
+ </ul>
230
+ );
231
+ }
@@ -0,0 +1,110 @@
1
+ import { Fragment, type CSSProperties } from 'react';
2
+
3
+ export type TextFormat = {
4
+ marker: string;
5
+ colors: Record<string, string>;
6
+ styles?: Record<string, CSSProperties>;
7
+ reset?: string;
8
+ hex?: string;
9
+ ignore?: readonly string[];
10
+ };
11
+
12
+ type Run = { text: string; style?: CSSProperties };
13
+
14
+ export function FormatText({
15
+ text,
16
+ format,
17
+ }: {
18
+ text: string;
19
+ format: TextFormat;
20
+ }) {
21
+ const codes = new Set([
22
+ ...Object.keys(format.colors),
23
+ ...Object.keys(format.styles ?? {}),
24
+ ...(format.reset ? [format.reset] : []),
25
+ ...(format.ignore ?? []),
26
+ ]);
27
+ const runs: Run[] = [];
28
+ let style: CSSProperties | undefined;
29
+ let buf = '';
30
+ const flush = () => {
31
+ if (!buf) return;
32
+ runs.push(style ? { text: buf, style } : { text: buf });
33
+ buf = '';
34
+ };
35
+ for (let i = 0; i < text.length; i += 1) {
36
+ const ch = text[i];
37
+ if (ch !== format.marker) {
38
+ buf += ch;
39
+ continue;
40
+ }
41
+ const code = text[i + 1]?.toLowerCase();
42
+ if (format.hex && code === format.hex) {
43
+ let hex = '';
44
+ let j = i + 2;
45
+ while (
46
+ hex.length < 6 &&
47
+ text[j] === format.marker &&
48
+ /^[0-9a-f]$/.test(text[j + 1]?.toLowerCase() ?? '')
49
+ ) {
50
+ hex += (text[j + 1] ?? '').toLowerCase();
51
+ j += 2;
52
+ }
53
+ if (hex.length === 6) {
54
+ flush();
55
+ i = j - 1;
56
+ const next: CSSProperties = { ...(style ?? {}), color: `#${hex}` };
57
+ delete next.fontWeight;
58
+ delete next.fontStyle;
59
+ delete next.textDecoration;
60
+ style = next;
61
+ continue;
62
+ }
63
+ buf += ch;
64
+ continue;
65
+ }
66
+ if (code === undefined || !codes.has(code)) {
67
+ buf += ch;
68
+ continue;
69
+ }
70
+ flush();
71
+ i += 1;
72
+ if (code === format.reset) {
73
+ style = undefined;
74
+ continue;
75
+ }
76
+ const next: CSSProperties = { ...(style ?? {}) };
77
+ const color = format.colors[code];
78
+ if (color) {
79
+ next.color = color;
80
+ delete next.fontWeight;
81
+ delete next.fontStyle;
82
+ delete next.textDecoration;
83
+ }
84
+ const extra = format.styles?.[code];
85
+ if (extra) {
86
+ for (const [key, value] of Object.entries(extra)) {
87
+ if (key === 'textDecoration' && next.textDecoration) {
88
+ next.textDecoration = `${next.textDecoration} ${String(value)}`;
89
+ } else {
90
+ (next as Record<string, unknown>)[key] = value;
91
+ }
92
+ }
93
+ }
94
+ style = Object.keys(next).length ? next : undefined;
95
+ }
96
+ flush();
97
+ return (
98
+ <>
99
+ {runs.map((run, index) =>
100
+ run.style ? (
101
+ <span key={index} style={run.style}>
102
+ {run.text}
103
+ </span>
104
+ ) : (
105
+ <Fragment key={index}>{run.text}</Fragment>
106
+ ),
107
+ )}
108
+ </>
109
+ );
110
+ }
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+ import type { ComponentProps } from 'react';
3
+ import { useSyncExternalStore } from 'react';
4
+ import { flushSync } from 'react-dom';
5
+ import { Airplay, Moon, Sun } from 'lucide-react';
6
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
7
+ import { Hint } from '@terrafirmagreg-team/ui/components/blocks/tooltip/hint';
8
+
9
+ const noop = () => () => {};
10
+
11
+ const icons = { light: Sun, dark: Moon, system: Airplay } as const;
12
+ export type ThemeToggleMode = keyof typeof icons;
13
+
14
+ export function ThemeToggle({
15
+ value,
16
+ onValueChange,
17
+ modes = ['light', 'dark', 'system'],
18
+ labels,
19
+ className,
20
+ buttonClassName,
21
+ ...props
22
+ }: ComponentProps<'div'> & {
23
+ value?: string;
24
+ onValueChange?: (mode: ThemeToggleMode) => void;
25
+ modes?: readonly ThemeToggleMode[];
26
+ labels?: Partial<Record<ThemeToggleMode, string>>;
27
+ buttonClassName?: string;
28
+ }) {
29
+ const mounted = useSyncExternalStore(
30
+ noop,
31
+ () => true,
32
+ () => false,
33
+ );
34
+ const current = mounted ? value : undefined;
35
+ const select = (mode: ThemeToggleMode) => {
36
+ if (document?.startViewTransition) {
37
+ document.startViewTransition(() => flushSync(() => onValueChange?.(mode)));
38
+ } else {
39
+ onValueChange?.(mode);
40
+ }
41
+ };
42
+ return (
43
+ <div
44
+ data-theme-toggle
45
+ className={cn(
46
+ 'inline-flex items-center rounded-full p-1 overflow-hidden *:rounded-full',
47
+ className,
48
+ )}
49
+ {...props}
50
+ >
51
+ {modes.map((mode) => {
52
+ const ModeIcon = icons[mode];
53
+ const active = current === mode;
54
+ return (
55
+ <Hint key={mode} label={labels?.[mode] ?? mode}>
56
+ <button
57
+ type="button"
58
+ aria-label={labels?.[mode] ?? mode}
59
+ aria-pressed={active}
60
+ className={cn(
61
+ 'size-6.5 p-1.5 text-muted-foreground',
62
+ buttonClassName,
63
+ active && 'bg-accent text-accent-foreground',
64
+ )}
65
+ onClick={() => select(mode)}
66
+ >
67
+ <ModeIcon className="size-full" fill="currentColor" aria-hidden />
68
+ </button>
69
+ </Hint>
70
+ );
71
+ })}
72
+ </div>
73
+ );
74
+ }
75
+
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+ import type { ComponentProps, ReactElement, ReactNode } from 'react';
3
+ import {
4
+ Tooltip,
5
+ TooltipContent,
6
+ TooltipTrigger,
7
+ } from '@terrafirmagreg-team/ui/components/ui/tooltip';
8
+
9
+ export function Hint({
10
+ label,
11
+ children,
12
+ ...props
13
+ }: {
14
+ label: ReactNode;
15
+ children: ReactElement;
16
+ } & Pick<
17
+ ComponentProps<typeof TooltipContent>,
18
+ 'side' | 'align' | 'sideOffset'
19
+ >) {
20
+ if (label == null || label === '') return children;
21
+ return (
22
+ <Tooltip>
23
+ <TooltipTrigger render={children} />
24
+ <TooltipContent {...props}>{label}</TooltipContent>
25
+ </Tooltip>
26
+ );
27
+ }
28
+
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { useState, type DependencyList, type ReactElement, type ReactNode } from 'react';
4
+ import { useAsyncValue } from '@terrafirmagreg-team/ui/hooks/use-async-value';
5
+ import {
6
+ Tooltip,
7
+ TooltipContent,
8
+ TooltipTrigger,
9
+ } from '@terrafirmagreg-team/ui/components/ui/tooltip';
10
+
11
+ export function LazyTooltip<T>({
12
+ load,
13
+ deps = [],
14
+ children,
15
+ trigger,
16
+ contentClassName,
17
+ onOpenChange,
18
+ }: {
19
+ load: () => Promise<T>;
20
+ deps?: DependencyList;
21
+ children: (value: T | undefined) => ReactNode;
22
+ trigger: ReactElement;
23
+ contentClassName?: string;
24
+ onOpenChange?: (open: boolean) => void;
25
+ }) {
26
+ const [open, setOpen] = useState(false);
27
+ const value = useAsyncValue(open ? load : false, [open, ...deps]);
28
+ return (
29
+ <Tooltip
30
+ onOpenChange={(next) => {
31
+ setOpen(next);
32
+ onOpenChange?.(next);
33
+ }}
34
+ >
35
+ <TooltipTrigger render={trigger} delay={150} closeDelay={100} />
36
+ <TooltipContent className={contentClassName}>
37
+ {children(value)}
38
+ </TooltipContent>
39
+ </Tooltip>
40
+ );
41
+ }