History

fix(settings): simplify open source license notices (#8970)

maria committed 2 months ago

Commit b56190e1 parent 3ffdc2a1 file · +184 −119Browse files

Unified · Split

apps/web/src/components/settings/OpenSourceLicenses.tsx+184−119
@@ -5,14 +5,16 @@ import { · 4 unmodified lines
55 decodeThirdPartyLicenseManifest,
66 filterThirdPartyLicenseEntries,
77 formatLicenseBundles,
8+ thirdPartyLicenseEntryKey,
89 type ThirdPartyLicenseEntry,
910 type ThirdPartyLicenseManifest,
1011 } from "@t3tools/shared/thirdPartyLicenses";
1112
12−import { cn } from "../../lib/utils";
1313 import { Button } from "../ui/button";
14−import { Input } from "../ui/input";
15−import { SettingsPageContainer } from "./settingsLayout";
14+import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible";
15+import { InputGroup, InputGroupAddon, InputGroupInput } from "../ui/input-group";
16+import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
17+import { SettingsPageContainer, SettingsSection } from "./settingsLayout";
1618
1719 type LicenseManifestState =
1820 | { readonly status: "loading" }
@@ -30,80 +32,154 @@ async function loadLicenseManifest(signal: AbortSignal): Promise<ThirdPartyLicen · 11 unmodified lines
3032 return decodeThirdPartyLicenseManifest((await response.json()) as unknown);
3133 }
3234
33−function LicenseNoticeRow({ entry }: { readonly entry: ThirdPartyLicenseEntry }) {
34− const [open, setOpen] = useState(false);
35− const panelId = `license-notice-${encodeURIComponent(`${entry.kind}-${entry.name}-${entry.version ?? "custom"}`)}`;
36− const versionLabel = entry.version ? ` ${entry.version}` : "";
37−
35+function LicenseNoticeRow({
36+ entry,
37+ open,
38+ onOpenChange,
39+}: {
40+ readonly entry: ThirdPartyLicenseEntry;
41+ readonly open: boolean;
42+ readonly onOpenChange: (open: boolean) => void;
43+}) {
3844 return (
39− <article>
40− <button
41− type="button"
42− aria-controls={panelId}
43− aria-expanded={open}
44− className="cursor-pointer flex min-h-14 w-full items-start gap-3 px-3 py-3 text-left hover:bg-foreground/4 sm:px-4"
45− onClick={() => setOpen((value) => !value)}
46− >
47− <ChevronRightIcon
48− aria-hidden
49− className={cn(
50− "mt-0.5 size-4 shrink-0 text-muted-foreground transition-transform duration-150",
51− open && "rotate-90",
52− )}
53− />
54− <span className="min-w-0 flex-1">
55− <span className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-1">
56− <span className="min-w-0 break-words font-medium text-foreground">{entry.name}</span>
57− {entry.version ? (
58− <code className="font-medium text-muted-foreground">{entry.version}</code>
59− ) : null}
60− </span>
61− <span className="text-muted-foreground">
62− {entry.license} · {formatLicenseBundles(entry.bundles)}
63− </span>
64− </span>
65− </button>
66− {open ? (
67− <div
68− id={panelId}
69− className="border-t border-foreground/10 bg-foreground/2 px-3 py-4 sm:px-4"
70− >
71− <div className="flex flex-col gap-3">
72− <div className="flex flex-wrap items-center justify-between gap-3">
73− <h2 className="text-base font-medium text-foreground sm:text-sm">
74− {entry.name}
75− {versionLabel}
76− </h2>
77− {entry.sourceUrl ? (
78− <a
79− className="inline-flex min-h-8 items-center gap-1.5 rounded-md px-2 text-base font-medium text-muted-foreground hover:bg-foreground/5 hover:text-foreground sm:text-sm"
80− href={entry.sourceUrl}
81− rel="noreferrer noopener"
82− target="_blank"
83− >
84− Project source
85− <ExternalLinkIcon aria-hidden className="size-4 shrink-0" />
86− </a>
45+ <Collapsible open={open} onOpenChange={onOpenChange}>
46+ <article>
47+ <div className="flex min-h-10 items-center hover:bg-muted/35 sm:min-h-9">
48+ <CollapsibleTrigger className="group flex min-h-10 min-w-0 flex-1 items-center gap-2.5 px-3 text-left sm:min-h-9 sm:px-4">
49+ <ChevronRightIcon
50+ aria-hidden
51+ className="size-3.5 shrink-0 text-muted-foreground transition-transform duration-200 group-data-panel-open:rotate-90"
52+ />
53+ <span className="flex min-w-0 flex-1 items-baseline gap-2">
54+ <span className="truncate font-medium text-foreground">{entry.name}</span>
55+ {entry.version ? (
56+ <code className="shrink-0 text-xs text-muted-foreground">{entry.version}</code>
8757 ) : null}
88− </div>
89− <pre className="max-w-full whitespace-pre-wrap break-words font-mono text-base/7 text-foreground/85 sm:text-sm/6">
90− {entry.noticeText}
91− </pre>
92− </div>
58+ </span>
59+ <span className="max-w-[42%] shrink-0 truncate text-xs text-muted-foreground">
60+ {entry.license} · {formatLicenseBundles(entry.bundles)}
61+ </span>
62+ </CollapsibleTrigger>
63+ {entry.sourceUrl ? (
64+ <Button
65+ aria-label={`View project source for ${entry.name}`}
66+ className="me-3 shrink-0 sm:me-4"
67+ render={<a href={entry.sourceUrl} rel="noreferrer noopener" target="_blank" />}
68+ size="icon-micro"
69+ title="Project source"
70+ variant="ghost-muted"
71+ >
72+ <ExternalLinkIcon aria-hidden className="size-3" />
73+ </Button>
74+ ) : null}
9375 </div>
94− ) : null}
95− </article>
76+ <CollapsiblePanel>
77+ {open ? (
78+ <div className="px-9 pt-1 pb-4 sm:px-10">
79+ <pre className="max-w-[76ch] whitespace-pre-wrap break-words font-mono text-xs/5 text-foreground/80">
80+ {entry.noticeText}
81+ </pre>
82+ </div>
83+ ) : null}
84+ </CollapsiblePanel>
85+ </article>
86+ </Collapsible>
87+ );
88+}
89+
90+function LicenseCount({
91+ filteredCount,
92+ totalCount,
93+}: {
94+ filteredCount: number;
95+ totalCount: number;
96+}) {
97+ return (
98+ <p className="whitespace-nowrap text-xs font-normal text-muted-foreground tabular-nums">
99+ {filteredCount === totalCount
100+ ? `${String(totalCount)} notices`
101+ : `${String(filteredCount)} of ${String(totalCount)}`}
102+ </p>
103+ );
104+}
105+
106+function LicenseHeaderAction({
107+ query,
108+ onQueryChange,
109+ searchOpen,
110+ onSearchOpenChange,
111+ filteredCount,
112+ totalCount,
113+}: {
114+ query: string;
115+ onQueryChange: (value: string) => void;
116+ searchOpen: boolean;
117+ onSearchOpenChange: (open: boolean) => void;
118+ filteredCount: number;
119+ totalCount: number;
120+}) {
121+ if (!searchOpen) {
122+ return (
123+ <div className="flex items-center gap-1.5">
124+ <LicenseCount filteredCount={filteredCount} totalCount={totalCount} />
125+ <Tooltip>
126+ <TooltipTrigger
127+ render={
128+ <Button
129+ aria-label="Search open-source licenses"
130+ onClick={() => onSearchOpenChange(true)}
131+ size="icon-micro"
132+ type="button"
133+ variant="ghost-muted"
134+ >
135+ <SearchIcon className="size-3" />
136+ </Button>
137+ }
138+ />
139+ <TooltipPopup side="top">Search licenses</TooltipPopup>
140+ </Tooltip>
141+ </div>
142+ );
143+ }
144+
145+ return (
146+ <div className="flex items-center gap-2">
147+ <div className="hidden sm:block">
148+ <LicenseCount filteredCount={filteredCount} totalCount={totalCount} />
149+ </div>
150+ <InputGroup className="w-36 sm:w-44">
151+ <InputGroupAddon>
152+ <SearchIcon aria-hidden className="size-3" />
153+ </InputGroupAddon>
154+ <InputGroupInput
155+ aria-label="Search open-source licenses"
156+ autoFocus
157+ onBlur={() => {
158+ if (query.length === 0) onSearchOpenChange(false);
159+ }}
160+ onChange={(event) => onQueryChange(event.currentTarget.value)}
161+ onKeyDown={(event) => {
162+ if (event.key !== "Escape") return;
163+ event.preventDefault();
164+ onQueryChange("");
165+ onSearchOpenChange(false);
166+ }}
167+ placeholder="Search licenses"
168+ size="sm"
169+ type="search"
170+ value={query}
171+ />
172+ </InputGroup>
173+ </div>
96174 );
97175 }
98176
99177 function LicenseManifestError({ message, onRetry }: { message: string; onRetry: () => void }) {
100178 return (
101− <div className="flex flex-col items-start gap-3 border-y border-foreground/10 px-3 py-5 sm:px-4">
179+ <div className="flex flex-col items-start gap-3 px-3 py-5 sm:px-4">
102180 <div className="flex flex-col gap-1">
103− <h2 className="text-base font-medium text-foreground sm:text-sm">
104− Open-source notices are unavailable
105− </h2>
106− <p className="max-w-[70ch] text-pretty text-base/7 text-muted-foreground sm:text-sm/6">
181+ <h3 className="text-sm font-medium text-foreground">Open-source notices are unavailable</h3>
182+ <p className="max-w-[70ch] text-pretty text-[13px] leading-[1.45] text-muted-foreground/80">
107183 {message}
108184 </p>
109185 </div>
@@ -117,6 +193,8 @@ function LicenseManifestError({ message, onRetry }: { message: string; onRetry: · 7 unmodified lines
117193 export function OpenSourceLicensesPanel() {
118194 const [state, setState] = useState<LicenseManifestState>({ status: "loading" });
119195 const [query, setQuery] = useState("");
196+ const [searchOpen, setSearchOpen] = useState(false);
197+ const [openEntryKey, setOpenEntryKey] = useState<string | null>(null);
120198 const [requestVersion, setRequestVersion] = useState(0);
121199
122200 useEffect(() => {
@@ -143,63 +221,50 @@ export function OpenSourceLicensesPanel() { · 20 unmodified lines
143221 const retry = useCallback(() => setRequestVersion((value) => value + 1), []);
144222
145223 return (
146− <SettingsPageContainer width="wide" className="gap-6">
147− <header className="flex flex-col gap-2 px-3 sm:px-4">
148− <h1 className="text-balance text-2xl font-semibold tracking-tight text-foreground">
149− Open source licenses
150− </h1>
151− <p className="max-w-[70ch] text-pretty text-base/7 text-muted-foreground sm:text-sm/6">
152− Third-party notices for dependencies and assets included in T3 Code.
153− </p>
154− </header>
155−
156− {state.status === "ready" ? (
157− <div className="flex flex-col gap-3">
158− <div className="flex flex-col gap-2 px-3 sm:flex-row sm:items-center sm:justify-between sm:px-4">
159− <label className="relative min-w-0 flex-1 sm:max-w-sm">
160− <SearchIcon
161− aria-hidden
162− className="pointer-events-none absolute top-1/2 left-2.5 z-1 size-4 shrink-0 -translate-y-1/2 text-muted-foreground"
163− />
164− <Input
165− nativeInput
166− type="search"
167− value={query}
168− onChange={(event) => setQuery(event.currentTarget.value)}
169− placeholder="Search packages"
170− aria-label="Search open-source licenses"
171− className="[&_[data-slot=input]]:pl-8"
172− />
173− </label>
174− <p className="text-base text-muted-foreground tabular-nums sm:text-sm">
175− {filteredEntries.length === entries.length
176− ? `${String(entries.length)} notices`
177− : `${String(filteredEntries.length)} of ${String(entries.length)} notices`}
178− </p>
179− </div>
180−
181− <div className="overflow-hidden rounded-xl border border-foreground/10 divide-y divide-foreground/10 text-base sm:text-sm">
224+ <SettingsPageContainer>
225+ <SettingsSection
226+ title="Third-party notices"
227+ headerAction={
228+ state.status === "ready" ? (
229+ <LicenseHeaderAction
230+ query={query}
231+ onQueryChange={setQuery}
232+ searchOpen={searchOpen}
233+ onSearchOpenChange={setSearchOpen}
234+ filteredCount={filteredEntries.length}
235+ totalCount={entries.length}
236+ />
237+ ) : null
238+ }
239+ >
240+ {state.status === "ready" ? (
241+ <div className="text-base sm:text-sm">
182242 {filteredEntries.length > 0 ? (
183− filteredEntries.map((entry) => (
184− <LicenseNoticeRow
185− key={`${entry.kind}:${entry.name}:${entry.version ?? "custom"}`}
186− entry={entry}
187− />
188− ))
243+ filteredEntries.map((entry) => {
244+ const entryKey = thirdPartyLicenseEntryKey(entry);
245+ return (
246+ <LicenseNoticeRow
247+ key={entryKey}
248+ entry={entry}
249+ open={openEntryKey === entryKey}
250+ onOpenChange={(open) => setOpenEntryKey(open ? entryKey : null)}
251+ />
252+ );
253+ })
189254 ) : (
190− <p className="px-3 py-8 text-center text-base/7 text-muted-foreground sm:px-4 sm:text-sm/6">
255+ <p className="px-3 py-8 text-center text-sm/6 text-muted-foreground sm:px-4">
191256 No licenses match that search.
192257 </p>
193258 )}
194259 </div>
195− </div>
196− ) : state.status === "error" ? (
197− <LicenseManifestError message={state.message} onRetry={retry} />
198− ) : (
199− <p className="border-y border-foreground/10 px-3 py-5 text-base/7 text-muted-foreground sm:px-4 sm:text-sm/6">
200− Loading open-source notices…
201− </p>
202− )}
260+ ) : state.status === "error" ? (
261+ <LicenseManifestError message={state.message} onRetry={retry} />
262+ ) : (
263+ <p className="px-3 py-5 text-sm/6 text-muted-foreground sm:px-4">
264+ Loading open-source notices…
265+ </p>
266+ )}
267+ </SettingsSection>
203268 </SettingsPageContainer>
204269 );
205270 }

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t