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
5 decodeThirdPartyLicenseManifest,5 decodeThirdPartyLicenseManifest,
6 filterThirdPartyLicenseEntries,6 filterThirdPartyLicenseEntries,
7 formatLicenseBundles,7 formatLicenseBundles,
8 thirdPartyLicenseEntryKey,
8 type ThirdPartyLicenseEntry,9 type ThirdPartyLicenseEntry,
9 type ThirdPartyLicenseManifest,10 type ThirdPartyLicenseManifest,
10} from "@t3tools/shared/thirdPartyLicenses";11} from "@t3tools/shared/thirdPartyLicenses";
11 12
12import { cn } from "../../lib/utils";
13import { Button } from "../ui/button";13import { Button } from "../ui/button";
14import { Input } from "../ui/input";14import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible";
15import { SettingsPageContainer } from "./settingsLayout";15import { InputGroup, InputGroupAddon, InputGroupInput } from "../ui/input-group";
16import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
17import { SettingsPageContainer, SettingsSection } from "./settingsLayout";
16 18
17type LicenseManifestState =19type LicenseManifestState =
18 | { readonly status: "loading" }20 | { readonly status: "loading" }
@@ -30,80 +32,154 @@ async function loadLicenseManifest(signal: AbortSignal): Promise<ThirdPartyLicen · 11 unmodified lines
30 return decodeThirdPartyLicenseManifest((await response.json()) as unknown);32 return decodeThirdPartyLicenseManifest((await response.json()) as unknown);
31}33}
32 34
33function LicenseNoticeRow({ entry }: { readonly entry: ThirdPartyLicenseEntry }) {35function LicenseNoticeRow({
34 const [open, setOpen] = useState(false);36 entry,
35 const panelId = `license-notice-${encodeURIComponent(`${entry.kind}-${entry.name}-${entry.version ?? "custom"}`)}`;37 open,
36 const versionLabel = entry.version ? ` ${entry.version}` : "";38 onOpenChange,
37 39}: {
40 readonly entry: ThirdPartyLicenseEntry;
41 readonly open: boolean;
42 readonly onOpenChange: (open: boolean) => void;
43}) {
38 return (44 return (
39 <article>45 <Collapsible open={open} onOpenChange={onOpenChange}>
40 <button46 <article>
41 type="button"47 <div className="flex min-h-10 items-center hover:bg-muted/35 sm:min-h-9">
42 aria-controls={panelId}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">
43 aria-expanded={open}49 <ChevronRightIcon
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"50 aria-hidden
45 onClick={() => setOpen((value) => !value)}51 className="size-3.5 shrink-0 text-muted-foreground transition-transform duration-200 group-data-panel-open:rotate-90"
46 >52 />
47 <ChevronRightIcon53 <span className="flex min-w-0 flex-1 items-baseline gap-2">
48 aria-hidden54 <span className="truncate font-medium text-foreground">{entry.name}</span>
49 className={cn(55 {entry.version ? (
50 "mt-0.5 size-4 shrink-0 text-muted-foreground transition-transform duration-150",56 <code className="shrink-0 text-xs text-muted-foreground">{entry.version}</code>
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>
87 ) : null}57 ) : null}
88 </div>58 </span>
89 <pre className="max-w-full whitespace-pre-wrap break-words font-mono text-base/7 text-foreground/85 sm:text-sm/6">59 <span className="max-w-[42%] shrink-0 truncate text-xs text-muted-foreground">
90 {entry.noticeText}60 {entry.license} · {formatLicenseBundles(entry.bundles)}
91 </pre>61 </span>
92 </div>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}
93 </div>75 </div>
94 ) : null}76 <CollapsiblePanel>
95 </article>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
90function 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
106function 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>
96 );174 );
97}175}
98 176
99function LicenseManifestError({ message, onRetry }: { message: string; onRetry: () => void }) {177function LicenseManifestError({ message, onRetry }: { message: string; onRetry: () => void }) {
100 return (178 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">
102 <div className="flex flex-col gap-1">180 <div className="flex flex-col gap-1">
103 <h2 className="text-base font-medium text-foreground sm:text-sm">181 <h3 className="text-sm font-medium text-foreground">Open-source notices are unavailable</h3>
104 Open-source notices are unavailable182 <p className="max-w-[70ch] text-pretty text-[13px] leading-[1.45] text-muted-foreground/80">
105 </h2>
106 <p className="max-w-[70ch] text-pretty text-base/7 text-muted-foreground sm:text-sm/6">
107 {message}183 {message}
108 </p>184 </p>
109 </div>185 </div>
@@ -117,6 +193,8 @@ function LicenseManifestError({ message, onRetry }: { message: string; onRetry: · 7 unmodified lines
117export function OpenSourceLicensesPanel() {193export function OpenSourceLicensesPanel() {
118 const [state, setState] = useState<LicenseManifestState>({ status: "loading" });194 const [state, setState] = useState<LicenseManifestState>({ status: "loading" });
119 const [query, setQuery] = useState("");195 const [query, setQuery] = useState("");
196 const [searchOpen, setSearchOpen] = useState(false);
197 const [openEntryKey, setOpenEntryKey] = useState<string | null>(null);
120 const [requestVersion, setRequestVersion] = useState(0);198 const [requestVersion, setRequestVersion] = useState(0);
121 199
122 useEffect(() => {200 useEffect(() => {
@@ -143,63 +221,50 @@ export function OpenSourceLicensesPanel() { · 20 unmodified lines
143 const retry = useCallback(() => setRequestVersion((value) => value + 1), []);221 const retry = useCallback(() => setRequestVersion((value) => value + 1), []);
144 222
145 return (223 return (
146 <SettingsPageContainer width="wide" className="gap-6">224 <SettingsPageContainer>
147 <header className="flex flex-col gap-2 px-3 sm:px-4">225 <SettingsSection
148 <h1 className="text-balance text-2xl font-semibold tracking-tight text-foreground">226 title="Third-party notices"
149 Open source licenses227 headerAction={
150 </h1>228 state.status === "ready" ? (
151 <p className="max-w-[70ch] text-pretty text-base/7 text-muted-foreground sm:text-sm/6">229 <LicenseHeaderAction
152 Third-party notices for dependencies and assets included in T3 Code.230 query={query}
153 </p>231 onQueryChange={setQuery}
154 </header>232 searchOpen={searchOpen}
155 233 onSearchOpenChange={setSearchOpen}
156 {state.status === "ready" ? (234 filteredCount={filteredEntries.length}
157 <div className="flex flex-col gap-3">235 totalCount={entries.length}
158 <div className="flex flex-col gap-2 px-3 sm:flex-row sm:items-center sm:justify-between sm:px-4">236 />
159 <label className="relative min-w-0 flex-1 sm:max-w-sm">237 ) : null
160 <SearchIcon238 }
161 aria-hidden239 >
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"240 {state.status === "ready" ? (
163 />241 <div className="text-base sm:text-sm">
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">
182 {filteredEntries.length > 0 ? (242 {filteredEntries.length > 0 ? (
183 filteredEntries.map((entry) => (243 filteredEntries.map((entry) => {
184 <LicenseNoticeRow244 const entryKey = thirdPartyLicenseEntryKey(entry);
185 key={`${entry.kind}:${entry.name}:${entry.version ?? "custom"}`}245 return (
186 entry={entry}246 <LicenseNoticeRow
187 />247 key={entryKey}
188 ))248 entry={entry}
249 open={openEntryKey === entryKey}
250 onOpenChange={(open) => setOpenEntryKey(open ? entryKey : null)}
251 />
252 );
253 })
189 ) : (254 ) : (
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">
191 No licenses match that search.256 No licenses match that search.
192 </p>257 </p>
193 )}258 )}
194 </div>259 </div>
195 </div>260 ) : state.status === "error" ? (
196 ) : state.status === "error" ? (261 <LicenseManifestError message={state.message} onRetry={retry} />
197 <LicenseManifestError message={state.message} onRetry={retry} />262 ) : (
198 ) : (263 <p className="px-3 py-5 text-sm/6 text-muted-foreground sm:px-4">
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">264 Loading open-source notices…
200 Loading open-source notices…265 </p>
201 </p>266 )}
202 )}267 </SettingsSection>
203 </SettingsPageContainer>268 </SettingsPageContainer>
204 );269 );
205}270}

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