@terrafirmagreg-team/ui 1.2.2 → 1.2.3

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.3",
4
4
  "type": "module",
5
5
  "description": "Shared UI kit",
6
6
  "license": "LGPL-3.0-only",
@@ -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
  }
@@ -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;