@terrafirmagreg-team/ui 1.2.2 → 1.2.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/ui",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "type": "module",
5
5
  "description": "Shared UI kit",
6
6
  "license": "LGPL-3.0-only",
@@ -44,7 +44,7 @@
44
44
  "@react-three/drei": "10.7.9",
45
45
  "@react-three/fiber": "9.8.2",
46
46
  "@shadcn/react": "^0.3.1",
47
- "@terrafirmagreg-team/translate": "1.2.3",
47
+ "@terrafirmagreg-team/translate": "1.2.4",
48
48
  "@xyflow/react": "12.12.0",
49
49
  "beautiful-mermaid": "^1.1.3",
50
50
  "class-variance-authority": "^0.7.1",
@@ -55,7 +55,7 @@
55
55
  "hast-util-to-jsx-runtime": "^2.3.6",
56
56
  "input-otp": "^1.5.0",
57
57
  "katex": "^0.19.1",
58
- "lucide-react": "^0.577.0",
58
+ "lucide-react": "^1.63.0",
59
59
  "next-themes": "^0.4.6",
60
60
  "react-day-picker": "^10.0.2",
61
61
  "react-medium-image-zoom": "^5.4.9",
@@ -3,6 +3,7 @@
3
3
  import { useEffect, useRef } from 'react';
4
4
  import {
5
5
  useReactFlow,
6
+ useStore,
6
7
  useStoreApi,
7
8
  } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
8
9
  import { readStoredViewport } from '@terrafirmagreg-team/ui/lib/canvas/viewport';
@@ -46,6 +47,7 @@ export function FlowViewport({
46
47
  }) {
47
48
  const { setCenter, setViewport, getZoom } = useReactFlow();
48
49
  const storeApi = useStoreApi();
50
+ const paneReady = useStore((s) => s.width > 0 && s.height > 0);
49
51
  const initRef = useRef<string | null>(null);
50
52
  const preInitRef = useRef<string | null>(null);
51
53
  const nodesRef = useRef(nodes);
@@ -66,6 +68,7 @@ export function FlowViewport({
66
68
 
67
69
  useEffect(() => {
68
70
  if (viewSize.w < 1 || viewSize.h < 1) return;
71
+ if (!paneReady) return;
69
72
  if (!ready) {
70
73
 
71
74
  initRef.current = null;
@@ -170,6 +173,7 @@ export function FlowViewport({
170
173
  initFocusId,
171
174
  nodes,
172
175
  onReady,
176
+ paneReady,
173
177
  ready,
174
178
  resetKey,
175
179
  restoreViewport,
@@ -229,7 +233,7 @@ export function FlowViewport({
229
233
  useEffect(() => {
230
234
  if (selectedId === lastSelectedRef.current) return;
231
235
  lastSelectedRef.current = selectedId ?? null;
232
- if (initRef.current !== resetKey || !selectedId) return;
236
+ if (initRef.current !== initKey || !selectedId) return;
233
237
  if (selectedId === suppressPanRef?.current) return;
234
238
  const node = nodesRef.current.find((entry) => entry.id === selectedId);
235
239
  if (!node) return;
@@ -237,7 +241,7 @@ export function FlowViewport({
237
241
  zoom: getZoom() || focusZoom,
238
242
  duration: 300,
239
243
  });
240
- }, [focusZoom, getZoom, resetKey, selectedId, suppressPanRef, setCenter]);
244
+ }, [focusZoom, getZoom, initKey, selectedId, suppressPanRef, setCenter]);
241
245
 
242
246
  return null;
243
247
  }
@@ -2,28 +2,7 @@
2
2
  import { Hint } from '@terrafirmagreg-team/ui/components/blocks/tooltip/hint';
3
3
  import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
4
4
  import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
5
-
6
- function LanguagesIcon({ className }: { className?: string }) {
7
- return (
8
- <svg
9
- viewBox="0 0 24 24"
10
- fill="none"
11
- stroke="currentColor"
12
- strokeWidth="2"
13
- strokeLinecap="round"
14
- strokeLinejoin="round"
15
- className={className}
16
- aria-hidden
17
- >
18
- <path d="m5 8 6 6" />
19
- <path d="m4 14 6-6 2-3" />
20
- <path d="M2 5h12" />
21
- <path d="M7 2h1" />
22
- <path d="m22 22-5-10-5 10" />
23
- <path d="M14 18h6" />
24
- </svg>
25
- );
26
- }
5
+ import { Languages } from 'lucide-react';
27
6
 
28
7
  export function UntranslatedChip({ className }: { className?: string }) {
29
8
  const t = useTranslations();
@@ -45,7 +24,7 @@ export function UntranslatedChip({ className }: { className?: string }) {
45
24
  className,
46
25
  )}
47
26
  >
48
- <LanguagesIcon className="size-3" />
27
+ <Languages className="size-3" aria-hidden />
49
28
  {t('Untranslated', { note: 'untranslated.chip' })}
50
29
  </span>
51
30
  </Hint>
@@ -257,7 +257,7 @@ export function GlbScene({
257
257
  label: t('Viewer settings', { note: 'ui.viewer.settings' }),
258
258
  sliders: [
259
259
  {
260
- icon: <MoveHorizontal />,
260
+ icon: <MoveHorizontal className="size-3.5" />,
261
261
  label: t('Width', { note: 'ui.viewer.width' }),
262
262
  min: SCENE_WIDTH_MIN,
263
263
  max: SCENE_WIDTH_MAX,
@@ -267,7 +267,7 @@ export function GlbScene({
267
267
  onChange: setSceneWidth,
268
268
  },
269
269
  {
270
- icon: <MoveVertical />,
270
+ icon: <MoveVertical className="size-3.5" />,
271
271
  label: t('Height', { note: 'ui.viewer.height' }),
272
272
  min: SCENE_HEIGHT_MIN,
273
273
  max: SCENE_HEIGHT_MAX,
@@ -277,7 +277,7 @@ export function GlbScene({
277
277
  onChange: setSceneHeight,
278
278
  },
279
279
  {
280
- icon: <Gauge />,
280
+ icon: <Gauge className="size-3.5" />,
281
281
  label: t('Rotate speed', { note: 'ui.viewer.rotate_speed' }),
282
282
  min: ROTATE_MIN,
283
283
  max: ROTATE_MAX,
@@ -289,7 +289,7 @@ export function GlbScene({
289
289
  ...(multi
290
290
  ? [
291
291
  {
292
- icon: <Timer />,
292
+ icon: <Timer className="size-3.5" />,
293
293
  label: t('Autoplay speed', { note: 'ui.viewer.autoplay_speed' }),
294
294
  min: AUTOPLAY_SPEED_MIN,
295
295
  max: AUTOPLAY_SPEED_MAX,
@@ -304,7 +304,7 @@ export function GlbScene({
304
304
  ],
305
305
  toggles: [
306
306
  {
307
- icon: <RotateCw />,
307
+ icon: <RotateCw className="size-4" />,
308
308
  label: t('Spin', { note: 'ui.viewer.spin' }),
309
309
  checked: autoRotate,
310
310
  onChange: setAutoRotate,
@@ -312,7 +312,7 @@ export function GlbScene({
312
312
  ...(multi
313
313
  ? [
314
314
  {
315
- icon: <Play />,
315
+ icon: <Play className="size-4" />,
316
316
  label: t('Autoplay', { note: 'ui.viewer.autoplay' }),
317
317
  checked: autoplay,
318
318
  onChange: setAutoplay,
@@ -320,7 +320,7 @@ export function GlbScene({
320
320
  ]
321
321
  : []),
322
322
  {
323
- icon: <Grid3x3 />,
323
+ icon: <Grid3x3 className="size-4" />,
324
324
  label: t('Grid', { note: 'ui.viewer.grid' }),
325
325
  checked: dotGrid,
326
326
  onChange: setDotGrid,
@@ -141,7 +141,7 @@ export function Slideshow({
141
141
  sliders: multi
142
142
  ? [
143
143
  {
144
- icon: <Timer />,
144
+ icon: <Timer className="size-3.5" />,
145
145
  label: t('Autoplay speed', { note: 'ui.viewer.autoplay_speed' }),
146
146
  min: AUTOPLAY_SPEED_MIN,
147
147
  max: AUTOPLAY_SPEED_MAX,
@@ -156,7 +156,7 @@ export function Slideshow({
156
156
  toggles: multi
157
157
  ? [
158
158
  {
159
- icon: <Play />,
159
+ icon: <Play className="size-4" />,
160
160
  label: t('Autoplay', { note: 'ui.viewer.autoplay' }),
161
161
  checked: autoplay,
162
162
  onChange: setAutoplay,
@@ -25,14 +25,19 @@ export function Heading({
25
25
  return (
26
26
  <As
27
27
  {...props}
28
+ onClick={(event) => {
29
+ props.onClick?.(event);
30
+ if (event.defaultPrevented) return;
31
+ if ((event.target as HTMLElement).closest('a,button,input,select,textarea,[role="button"],[role="link"]'))
32
+ return;
33
+ window.location.hash = props.id!;
34
+ }}
28
35
  className={cn(
29
- 'group/heading flex scroll-m-28 flex-row items-center gap-1',
36
+ 'group/heading flex cursor-pointer scroll-m-28 flex-row items-center gap-1',
30
37
  props.className,
31
38
  )}
32
39
  >
33
- <a data-card href={`#${props.id}`}>
34
- {props.children}
35
- </a>
40
+ {props.children}
36
41
  <button
37
42
  aria-live="polite"
38
43
  className={cn(
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
3
+ import type { ComponentProps } from 'react';
4
+
5
+ export function WorkspaceBody({ className, ...props }: ComponentProps<'div'>) {
6
+ return (
7
+ <div
8
+ data-slot="workspace-body"
9
+ {...props}
10
+ className={cn('prose flex-1', className)}
11
+ />
12
+ );
13
+ }
14
+
15
+ export function WorkspaceDescription({
16
+ children,
17
+ className,
18
+ ...props
19
+ }: ComponentProps<'p'>) {
20
+ if (children === undefined) return null;
21
+ return (
22
+ <p
23
+ data-slot="workspace-description"
24
+ {...props}
25
+ className={cn('mb-8 text-lg text-muted-foreground', className)}
26
+ >
27
+ {children}
28
+ </p>
29
+ );
30
+ }
31
+
32
+ export function WorkspaceTitle({ className, ...props }: ComponentProps<'h1'>) {
33
+ return (
34
+ <h1
35
+ data-slot="workspace-title"
36
+ {...props}
37
+ className={cn('text-[1.75em] font-semibold', className)}
38
+ />
39
+ );
40
+ }
41
+
42
+ export function WorkspaceFooter({
43
+ className,
44
+ ...props
45
+ }: ComponentProps<'div'>) {
46
+ return (
47
+ <div
48
+ data-slot="workspace-footer"
49
+ className={cn('mt-auto empty:hidden', className)}
50
+ {...props}
51
+ />
52
+ );
53
+ }
@@ -0,0 +1,12 @@
1
+ export type {
2
+ WorkspacePaginationItems,
3
+ WorkspaceProps,
4
+ } from './workspace';
5
+ export { Workspace } from './workspace';
6
+ export {
7
+ WorkspaceBody,
8
+ WorkspaceDescription,
9
+ WorkspaceTitle,
10
+ WorkspaceFooter,
11
+ } from './content';
12
+ export { WorkspacePagination } from './pagination';
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+ import { type ComponentProps, useMemo } from 'react';
3
+ import { usePathname } from 'next/navigation';
4
+ import Link from '@terrafirmagreg-team/ui/components/blocks/nav/link';
5
+ import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react';
6
+ import { useFooterItems } from '@terrafirmagreg-team/ui/hooks/use-tree';
7
+ import { useLocationSearch } from '@terrafirmagreg-team/ui/hooks/page/use-location-search';
8
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
9
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
10
+ import { isNavItemActive } from '@terrafirmagreg-team/ui/lib/utils/nav-active';
11
+ import type { WorkspacePaginationItems } from './workspace';
12
+
13
+ export function WorkspacePagination({
14
+ items,
15
+ className,
16
+ ...props
17
+ }: ComponentProps<'nav'> & { items?: WorkspacePaginationItems }) {
18
+ const footerList = useFooterItems();
19
+ const pathname = usePathname();
20
+ const search = useLocationSearch();
21
+ const { previous, next } = useMemo(() => {
22
+ if (items && (items.previous || items.next)) return items;
23
+ const idx = footerList.findIndex((item) =>
24
+ isNavItemActive(item.url, pathname, search),
25
+ );
26
+ if (idx === -1) return {};
27
+ return { previous: footerList[idx - 1], next: footerList[idx + 1] };
28
+ }, [footerList, items, pathname, search]);
29
+
30
+ if (!previous && !next) return null;
31
+ return (
32
+ <nav
33
+ data-slot="workspace-pagination"
34
+ className={cn('@container grid grid-cols-2 gap-3 mt-8', className)}
35
+ {...props}
36
+ >
37
+ {previous && <WorkspacePaginationItem item={previous} next={false} />}
38
+ {next && <WorkspacePaginationItem item={next} next />}
39
+ </nav>
40
+ );
41
+ }
42
+
43
+ function WorkspacePaginationItem({
44
+ item,
45
+ next,
46
+ }: {
47
+ item: NonNullable<WorkspacePaginationItems['previous']>;
48
+ next: boolean;
49
+ }) {
50
+ const t = useTranslations();
51
+ const Icon = next ? ArrowRightIcon : ArrowLeftIcon;
52
+
53
+ return (
54
+ <Link
55
+ href={item.url}
56
+ className={cn(
57
+ 'group flex flex-col gap-1 rounded-2xl border p-4 text-sm transition-colors hover:bg-accent/40 @max-md:col-span-full',
58
+ next && 'col-start-2 items-end text-end',
59
+ )}
60
+ >
61
+ <span className="inline-flex items-center gap-1.5 text-muted-foreground">
62
+ {!next && (
63
+ <Icon className="size-3.5 transition-transform group-hover:-translate-x-0.5 rtl:rotate-180" />
64
+ )}
65
+ {next ? t('Next Page', { note: 'ui.next_page' }) : t('Previous Page', { note: 'ui.prev_page' })}
66
+ {next && (
67
+ <Icon className="size-3.5 transition-transform group-hover:translate-x-0.5 rtl:rotate-180" />
68
+ )}
69
+ </span>
70
+ <span className="font-medium">{item.name}</span>
71
+ </Link>
72
+ );
73
+ }
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+ import type { ComponentProps, ReactNode } from 'react';
3
+ import { usePathname } from 'next/navigation';
4
+ import type * as PageTree from '@terrafirmagreg-team/translate/types/tree';
5
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
6
+ import type { PagePreviewData } from '@terrafirmagreg-team/ui/types/preview';
7
+ import { LeftSidebarExpand } from '@terrafirmagreg-team/ui/components/layouts/left-sidebar';
8
+ import { Breadcrumb } from '@terrafirmagreg-team/ui/components/blocks/nav/breadcrumb';
9
+ import { WorkspaceFooter } from './content';
10
+ import { WorkspacePagination } from './pagination';
11
+
12
+ export type WorkspacePaginationItems = {
13
+ previous?: Pick<PageTree.Item, 'name' | 'description' | 'url'>;
14
+ next?: Pick<PageTree.Item, 'name' | 'description' | 'url'>;
15
+ };
16
+
17
+ export type WorkspaceProps = ComponentProps<'article'> & {
18
+ full?: boolean;
19
+ outlineDropdown?: ReactNode;
20
+ breadcrumb?: {
21
+ items?: { name: string; url?: string }[];
22
+ previews?: Record<string, PagePreviewData>;
23
+ };
24
+ pagination?: { items?: WorkspacePaginationItems } | false;
25
+ actions?: ReactNode;
26
+ footer?: ReactNode;
27
+ };
28
+
29
+ export function Workspace({
30
+ full = false,
31
+ outlineDropdown,
32
+ breadcrumb,
33
+ pagination = {},
34
+ actions,
35
+ footer,
36
+ className,
37
+ children,
38
+ ...props
39
+ }: WorkspaceProps) {
40
+ const crumbs = breadcrumb?.items ?? [];
41
+ const hasCrumbs = crumbs.length > 0;
42
+ const pathname = usePathname();
43
+
44
+ return (
45
+ <div
46
+ key={pathname}
47
+ id="page-panel"
48
+ data-slot="workspace-panel"
49
+ className="@container relative flex flex-col min-w-0 min-h-0 [grid-area:main] mx-2 bg-background md:ms-0 md:my-2 md:overflow-clip md:rounded-2xl md:border md:shadow-sm print:overflow-visible"
50
+ >
51
+ <header
52
+ data-slot="workspace-header"
53
+ className={cn(
54
+ 'absolute inset-x-0 top-0 z-10 flex items-center gap-1.5 h-(--header-height) ps-6 pe-4 bg-linear-to-b from-background to-transparent pointer-events-none *:pointer-events-auto max-md:hidden',
55
+ hasCrumbs || outlineDropdown ? 'from-50%' : 'to-40%',
56
+ )}
57
+ >
58
+ <LeftSidebarExpand />
59
+ {hasCrumbs && (
60
+ <Breadcrumb items={crumbs} previews={breadcrumb?.previews} />
61
+ )}
62
+ {outlineDropdown}
63
+ <div data-slot="workspace-actions" className="ms-auto">
64
+ {actions}
65
+ </div>
66
+ </header>
67
+ <div
68
+ data-slot="workspace-scroll"
69
+ className="flex flex-1 items-start px-4 md:min-h-0 md:pt-(--header-height) md:px-[max(--spacing(6),calc((100%_-_var(--page-width)_-_var(--toc-width))/2))] md:overflow-y-auto md:overscroll-y-contain md:scrollbar-thin md:scrollbar-gutter-stable md:[--docs-height:calc(var(--layout-height)_-_var(--spacing)*4_-_2px_-_var(--header-height))]"
70
+ >
71
+ <article
72
+ id="page"
73
+ data-slot="workspace"
74
+ data-full={full}
75
+ className={cn(
76
+ 'flex flex-col gap-4 w-full min-w-0 max-w-(--page-width) pt-6 pb-8 md:min-h-(--docs-height)',
77
+ className,
78
+ )}
79
+ {...props}
80
+ >
81
+ {hasCrumbs && (
82
+ <Breadcrumb
83
+ items={crumbs}
84
+ previews={breadcrumb?.previews}
85
+ className="md:hidden"
86
+ />
87
+ )}
88
+ {children}
89
+ {pagination !== false || footer ? (
90
+ <WorkspaceFooter>
91
+ {pagination !== false && (
92
+ <WorkspacePagination items={pagination.items} />
93
+ )}
94
+ {footer}
95
+ </WorkspaceFooter>
96
+ ) : null}
97
+ </article>
98
+ </div>
99
+ </div>
100
+ );
101
+ }
@@ -12,6 +12,7 @@
12
12
 
13
13
  .viewport-body {
14
14
  display: flex;
15
+ flex-direction: column;
15
16
  flex: 1;
16
17
  max-width: none;
17
18
  min-height: 0;