@anchrd/intel-ui 0.25.0 → 0.29.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.
@@ -1,7 +1,55 @@
1
- import type * as React from "react";
1
+ import {
2
+ createContext,
3
+ type HTMLAttributes,
4
+ type ReactNode,
5
+ useContext,
6
+ useEffect,
7
+ useId,
8
+ useLayoutEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from "react";
2
13
  import { ResourceAccessSummary } from "@/access-summary/access-summary.tsx";
14
+ import { useI18n } from "@/i18n/i18n-context.tsx";
15
+ import { cn } from "@/lib/utils";
3
16
  import { ResourceMenu, type ResourceTarget } from "@/resource-menu/resource-menu.tsx";
4
17
 
18
+ const TitleRowScrollContext = createContext<{
19
+ scrolled: boolean;
20
+ setScrolled(scrolled: boolean): void;
21
+ } | null>(null);
22
+
23
+ export function TitleRowFrame({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
24
+ const [scrolled, setScrolled] = useState(false);
25
+ const value = useMemo(() => ({ scrolled, setScrolled }), [scrolled]);
26
+ return (
27
+ <TitleRowScrollContext.Provider value={value}>
28
+ <div className={className} {...props} />
29
+ </TitleRowScrollContext.Provider>
30
+ );
31
+ }
32
+
33
+ export function TitleRowScrollArea({
34
+ className,
35
+ onScroll,
36
+ ...props
37
+ }: HTMLAttributes<HTMLDivElement>) {
38
+ const scroll = useContext(TitleRowScrollContext);
39
+ const setScrolled = scroll?.setScrolled;
40
+ useEffect(() => () => setScrolled?.(false), [setScrolled]);
41
+ return (
42
+ <div
43
+ className={className}
44
+ onScroll={(event) => {
45
+ scroll?.setScrolled(event.currentTarget.scrollTop > 0);
46
+ onScroll?.(event);
47
+ }}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+
5
53
  /**
6
54
  * The one line an open thing gets: its name on the left, everything that acts on it on the right
7
55
  * (#53).
@@ -26,10 +74,23 @@ export function TitleRow({
26
74
  title: string;
27
75
  description?: string | null;
28
76
  target: ResourceTarget;
29
- children?: React.ReactNode;
77
+ children?: ReactNode;
30
78
  }) {
79
+ const scroll = useContext(TitleRowScrollContext);
80
+ const setScrolled = scroll?.setScrolled;
81
+ const targetId = target.type === "flow" ? target.flow.id : target.node.id;
82
+ useEffect(() => {
83
+ void targetId;
84
+ setScrolled?.(false);
85
+ }, [setScrolled, targetId]);
31
86
  return (
32
- <div className="flex items-start justify-between gap-5 border-b px-6 py-4">
87
+ <div
88
+ data-scrolled={scroll?.scrolled || undefined}
89
+ className={cn(
90
+ "relative z-10 flex shrink-0 items-start justify-between gap-5 px-6 py-4 transition-shadow",
91
+ scroll?.scrolled && "shadow-[inset_0_-1px_0_var(--border)]",
92
+ )}
93
+ >
33
94
  <div className="flex min-w-0 items-center gap-3">
34
95
  <div className="min-w-0">
35
96
  <div className="flex min-w-0 flex-wrap items-baseline gap-x-3">
@@ -39,7 +100,7 @@ export function TitleRow({
39
100
  ) : null}
40
101
  <span data-slot="title-meta" className="text-sm text-muted-foreground" />
41
102
  </div>
42
- {description ? <p className="mt-1 text-sm text-muted-foreground">{description}</p> : null}
103
+ {description ? <DescriptionDisclosure key={targetId} description={description} /> : null}
43
104
  </div>
44
105
  </div>
45
106
  {/* ⚠️ Document order is tab order here — nothing carries a `tabIndex`. The menu is therefore
@@ -53,3 +114,62 @@ export function TitleRow({
53
114
  </div>
54
115
  );
55
116
  }
117
+
118
+ function DescriptionDisclosure({ description }: { description: string }) {
119
+ const i18n = useI18n();
120
+ const descriptionId = useId();
121
+ const containerRef = useRef<HTMLDivElement>(null);
122
+ const measureRef = useRef<HTMLSpanElement>(null);
123
+ const [clipped, setClipped] = useState(false);
124
+ const [expanded, setExpanded] = useState(false);
125
+
126
+ useLayoutEffect(() => {
127
+ const container = containerRef.current;
128
+ const measure = measureRef.current;
129
+ if (!container || !measure) return;
130
+
131
+ const update = () => setClipped(measure.scrollWidth > container.clientWidth);
132
+ update();
133
+ const observer = new ResizeObserver(update);
134
+ observer.observe(container);
135
+ observer.observe(measure);
136
+ return () => observer.disconnect();
137
+ }, []);
138
+
139
+ return (
140
+ <div
141
+ ref={containerRef}
142
+ data-description-container="true"
143
+ className={cn(
144
+ "relative mt-1 min-w-0 text-sm text-muted-foreground",
145
+ expanded && "flex flex-wrap items-baseline gap-x-1",
146
+ )}
147
+ >
148
+ <span
149
+ ref={measureRef}
150
+ data-description-measure="true"
151
+ aria-hidden="true"
152
+ className="pointer-events-none invisible absolute whitespace-nowrap"
153
+ >
154
+ {description}
155
+ </span>
156
+ <span id={descriptionId} className={cn(clipped && !expanded && "block truncate")}>
157
+ {description}
158
+ </span>
159
+ {clipped ? (
160
+ <button
161
+ type="button"
162
+ aria-controls={descriptionId}
163
+ aria-expanded={expanded}
164
+ onClick={() => setExpanded((current) => !current)}
165
+ className={cn(
166
+ "shrink-0 font-medium text-foreground outline-none hover:underline focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-ring",
167
+ !expanded && "absolute bottom-0 right-0 bg-background pl-1",
168
+ )}
169
+ >
170
+ {i18n.t(expanded ? "title.descriptionLess" : "title.descriptionMore")}
171
+ </button>
172
+ ) : null}
173
+ </div>
174
+ );
175
+ }