@igstack/app-catalog-frontend-core 0.4.0-alpha-20260807174540 → 0.4.0-alpha-20260807203854
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/dist/esm/modules/appCatalog/ui/components/PersonBadge.d.ts +6 -0
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +95 -31
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.js +1 -1
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.js.map +1 -1
- package/package.json +4 -4
- package/src/modules/appCatalog/ui/components/PersonBadge.tsx +103 -30
- package/src/modules/appCatalog/ui/launcher/LauncherDetailPanel.tsx +5 -2
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
interface PersonBadgeProps {
|
|
2
2
|
slug: string;
|
|
3
3
|
}
|
|
4
|
+
/**
|
|
5
|
+
* Person chip. Clicking it opens a small popover exposing BOTH the full name
|
|
6
|
+
* and the email, each with its own copy button — so the user picks what to copy
|
|
7
|
+
* (previously a single button copied only the email). Email also links to
|
|
8
|
+
* mailto:. Falls back to just the name (no popover) when no email is known.
|
|
9
|
+
*/
|
|
4
10
|
export declare function PersonBadge({ slug }: PersonBadgeProps): import("react/jsx-runtime").JSX.Element;
|
|
5
11
|
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import { User,
|
|
2
|
+
import { User, Copy, Mail, Check } from "lucide-react";
|
|
3
3
|
import { useState, useRef, useEffect, useCallback } from "react";
|
|
4
4
|
import { Badge } from "../../../../ui/badge.js";
|
|
5
|
+
import { Popover, PopoverTrigger, PopoverContent } from "../../../../ui/popover.js";
|
|
5
6
|
import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
|
|
6
7
|
import "../context/AppCatalogFiltersContext.js";
|
|
7
8
|
import "@tanstack/react-router";
|
|
@@ -21,48 +22,111 @@ function PersonBadge({ slug }) {
|
|
|
21
22
|
const person = getPersonBySlug(persons, slug);
|
|
22
23
|
const displayName = person ? `${person.firstName} ${person.lastName}`.trim() || slug : slug;
|
|
23
24
|
const email = person == null ? void 0 : person.email;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
if (!email) {
|
|
26
|
+
return /* @__PURE__ */ jsxs(
|
|
27
|
+
Badge,
|
|
28
|
+
{
|
|
29
|
+
variant: "outline",
|
|
30
|
+
className: "font-normal inline-flex items-center gap-1",
|
|
31
|
+
title: displayName,
|
|
32
|
+
children: [
|
|
33
|
+
/* @__PURE__ */ jsx(User, { className: "size-3" }),
|
|
34
|
+
displayName
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return /* @__PURE__ */ jsxs(Popover, { children: [
|
|
40
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
41
|
+
"button",
|
|
42
|
+
{
|
|
43
|
+
type: "button",
|
|
44
|
+
className: "inline-flex",
|
|
45
|
+
title: `${displayName} · ${email}`,
|
|
46
|
+
children: /* @__PURE__ */ jsxs(
|
|
47
|
+
Badge,
|
|
48
|
+
{
|
|
49
|
+
variant: "outline",
|
|
50
|
+
className: "font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors",
|
|
51
|
+
children: [
|
|
52
|
+
/* @__PURE__ */ jsx(User, { className: "size-3" }),
|
|
53
|
+
displayName,
|
|
54
|
+
/* @__PURE__ */ jsx(Copy, { className: "size-3 opacity-50" })
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
) }),
|
|
60
|
+
/* @__PURE__ */ jsxs(PopoverContent, { align: "start", className: "w-64 p-2", children: [
|
|
61
|
+
/* @__PURE__ */ jsx(
|
|
62
|
+
CopyRow,
|
|
63
|
+
{
|
|
64
|
+
icon: /* @__PURE__ */ jsx(User, { className: "size-3.5" }),
|
|
65
|
+
label: "Name",
|
|
66
|
+
value: displayName
|
|
67
|
+
}
|
|
68
|
+
),
|
|
69
|
+
/* @__PURE__ */ jsx(
|
|
70
|
+
CopyRow,
|
|
71
|
+
{
|
|
72
|
+
icon: /* @__PURE__ */ jsx(Mail, { className: "size-3.5" }),
|
|
73
|
+
label: "Email",
|
|
74
|
+
value: email,
|
|
75
|
+
href: `mailto:${email}`
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
] })
|
|
79
|
+
] });
|
|
37
80
|
}
|
|
38
|
-
function
|
|
81
|
+
function CopyRow({
|
|
82
|
+
icon,
|
|
83
|
+
label,
|
|
84
|
+
value,
|
|
85
|
+
href
|
|
86
|
+
}) {
|
|
39
87
|
const [copied, setCopied] = useState(false);
|
|
40
88
|
const timeoutRef = useRef(null);
|
|
41
|
-
useEffect(
|
|
42
|
-
|
|
89
|
+
useEffect(
|
|
90
|
+
() => () => {
|
|
43
91
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
44
|
-
}
|
|
45
|
-
|
|
92
|
+
},
|
|
93
|
+
[]
|
|
94
|
+
);
|
|
46
95
|
const handleCopy = useCallback(
|
|
47
96
|
(e) => {
|
|
48
97
|
e.stopPropagation();
|
|
49
|
-
navigator.clipboard.writeText(
|
|
98
|
+
void navigator.clipboard.writeText(value);
|
|
50
99
|
setCopied(true);
|
|
51
100
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
52
101
|
timeoutRef.current = setTimeout(() => setCopied(false), 2e3);
|
|
53
102
|
},
|
|
54
|
-
[
|
|
55
|
-
);
|
|
56
|
-
return /* @__PURE__ */ jsx(
|
|
57
|
-
"button",
|
|
58
|
-
{
|
|
59
|
-
onClick: handleCopy,
|
|
60
|
-
className: "ml-0.5 hover:text-primary transition-colors",
|
|
61
|
-
title: copied ? "Copied!" : `Copy ${email}`,
|
|
62
|
-
type: "button",
|
|
63
|
-
children: copied ? /* @__PURE__ */ jsx(Check, { className: "size-3 text-green-600" }) : /* @__PURE__ */ jsx(Copy, { className: "size-3" })
|
|
64
|
-
}
|
|
103
|
+
[value]
|
|
65
104
|
);
|
|
105
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted", children: [
|
|
106
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground shrink-0", children: icon }),
|
|
107
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
108
|
+
/* @__PURE__ */ jsx("span", { className: "block text-[10px] uppercase tracking-wide text-muted-foreground", children: label }),
|
|
109
|
+
href ? /* @__PURE__ */ jsx(
|
|
110
|
+
"a",
|
|
111
|
+
{
|
|
112
|
+
href,
|
|
113
|
+
className: "block truncate text-[13px] text-primary hover:underline",
|
|
114
|
+
children: value
|
|
115
|
+
}
|
|
116
|
+
) : /* @__PURE__ */ jsx("span", { className: "block truncate text-[13px]", children: value })
|
|
117
|
+
] }),
|
|
118
|
+
/* @__PURE__ */ jsx(
|
|
119
|
+
"button",
|
|
120
|
+
{
|
|
121
|
+
type: "button",
|
|
122
|
+
onClick: handleCopy,
|
|
123
|
+
className: "shrink-0 rounded p-1 text-muted-foreground hover:text-primary hover:bg-background transition-colors",
|
|
124
|
+
title: copied ? "Copied!" : `Copy ${label.toLowerCase()}`,
|
|
125
|
+
"aria-label": `Copy ${label.toLowerCase()}`,
|
|
126
|
+
children: copied ? /* @__PURE__ */ jsx(Check, { className: "size-3.5 text-green-600" }) : /* @__PURE__ */ jsx(Copy, { className: "size-3.5" })
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
] });
|
|
66
130
|
}
|
|
67
131
|
export {
|
|
68
132
|
PersonBadge
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import { Check, Copy, User } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getPersonBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\n\nexport function PersonBadge({ slug }: PersonBadgeProps) {\n const { persons } = useAppCatalogContext()\n const person = getPersonBySlug(persons, slug)\n\n const displayName = person\n ? `${person.firstName} ${person.lastName}`.trim() || slug\n : slug\n
|
|
1
|
+
{"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import { Check, Copy, Mail, User } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Popover, PopoverContent, PopoverTrigger } from '~/ui/popover'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getPersonBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\n\n/**\n * Person chip. Clicking it opens a small popover exposing BOTH the full name\n * and the email, each with its own copy button — so the user picks what to copy\n * (previously a single button copied only the email). Email also links to\n * mailto:. Falls back to just the name (no popover) when no email is known.\n */\nexport function PersonBadge({ slug }: PersonBadgeProps) {\n const { persons } = useAppCatalogContext()\n const person = getPersonBySlug(persons, slug)\n\n const displayName = person\n ? `${person.firstName} ${person.lastName}`.trim() || slug\n : slug\n const email = person?.email\n\n // No email → nothing to pick between; render a plain, non-interactive chip.\n if (!email) {\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={displayName}\n >\n <User className=\"size-3\" />\n {displayName}\n </Badge>\n )\n }\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n className=\"inline-flex\"\n title={`${displayName} · ${email}`}\n >\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors\"\n >\n <User className=\"size-3\" />\n {displayName}\n <Copy className=\"size-3 opacity-50\" />\n </Badge>\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <CopyRow\n icon={<User className=\"size-3.5\" />}\n label=\"Name\"\n value={displayName}\n />\n <CopyRow\n icon={<Mail className=\"size-3.5\" />}\n label=\"Email\"\n value={email}\n href={`mailto:${email}`}\n />\n </PopoverContent>\n </Popover>\n )\n}\n\n/** One copyable field (label + value + copy button), value optionally a link. */\nfunction CopyRow({\n icon,\n label,\n value,\n href,\n}: {\n icon: React.ReactNode\n label: string\n value: string\n href?: string\n}) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(\n () => () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n },\n [],\n )\n\n const handleCopy = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation()\n void navigator.clipboard.writeText(value)\n setCopied(true)\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n },\n [value],\n )\n\n return (\n <div className=\"flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted\">\n <span className=\"text-muted-foreground shrink-0\">{icon}</span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block text-[10px] uppercase tracking-wide text-muted-foreground\">\n {label}\n </span>\n {href ? (\n <a\n href={href}\n className=\"block truncate text-[13px] text-primary hover:underline\"\n >\n {value}\n </a>\n ) : (\n <span className=\"block truncate text-[13px]\">{value}</span>\n )}\n </span>\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"shrink-0 rounded p-1 text-muted-foreground hover:text-primary hover:bg-background transition-colors\"\n title={copied ? 'Copied!' : `Copy ${label.toLowerCase()}`}\n aria-label={`Copy ${label.toLowerCase()}`}\n >\n {copied ? (\n <Check className=\"size-3.5 text-green-600\" />\n ) : (\n <Copy className=\"size-3.5\" />\n )}\n </button>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -21,7 +21,7 @@ function LauncherDetailPanel({
|
|
|
21
21
|
role: "dialog",
|
|
22
22
|
"aria-modal": "true",
|
|
23
23
|
"aria-label": `${app.displayName} details`,
|
|
24
|
-
className: "relative w-full max-w-[
|
|
24
|
+
className: "relative w-full max-w-[min(1120px,94vw)] my-auto rounded-[var(--radius)] border border-border bg-background shadow-2xl animate-in fade-in zoom-in-95 duration-200",
|
|
25
25
|
children: /* @__PURE__ */ jsx("div", { className: "max-h-[85vh] overflow-y-auto px-6 py-5 sm:px-8 sm:py-7", children: /* @__PURE__ */ jsx(
|
|
26
26
|
AppDetails,
|
|
27
27
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LauncherDetailPanel.js","sources":["../../../../../../src/modules/appCatalog/ui/launcher/LauncherDetailPanel.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { AppDetails } from '../grid/AppCatalogGrid'\n\n/**\n * Centered detail card for the launcher (#38, item B). Renders the rich\n * AppDetails (access-hero, sub-resources, tiers) as a large centered modal card\n * over the launcher backdrop, instead of the old grid + resizable split-pane.\n *\n * Escape handling: this component does NOT globally bind Escape. Escape-to-close\n * is owned by AppDetails' own key handling (which first closes an open\n * screenshot gallery, then closes the card), so pressing Esc inside the gallery\n * returns to the card — not all the way to the home page (#38 Esc-stacking bug).\n */\nexport function LauncherDetailPanel({\n app,\n onClose,\n onAppClick,\n}: {\n app: Resource\n onClose: () => void\n onAppClick?: (app: Resource) => void\n}) {\n return (\n <div className=\"fixed inset-0 z-50 flex items-start justify-center overflow-y-auto p-4 sm:p-6 md:p-10\">\n {/* scrim */}\n <button\n type=\"button\"\n aria-label=\"Close details panel\"\n onClick={onClose}\n className=\"fixed inset-0 -z-10 bg-black/40 backdrop-blur-[2px] animate-in fade-in\"\n />\n {/* centered card */}\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={`${app.displayName} details`}\n className=\"relative w-full max-w-[
|
|
1
|
+
{"version":3,"file":"LauncherDetailPanel.js","sources":["../../../../../../src/modules/appCatalog/ui/launcher/LauncherDetailPanel.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { AppDetails } from '../grid/AppCatalogGrid'\n\n/**\n * Centered detail card for the launcher (#38, item B). Renders the rich\n * AppDetails (access-hero, sub-resources, tiers) as a large centered modal card\n * over the launcher backdrop, instead of the old grid + resizable split-pane.\n *\n * Escape handling: this component does NOT globally bind Escape. Escape-to-close\n * is owned by AppDetails' own key handling (which first closes an open\n * screenshot gallery, then closes the card), so pressing Esc inside the gallery\n * returns to the card — not all the way to the home page (#38 Esc-stacking bug).\n */\nexport function LauncherDetailPanel({\n app,\n onClose,\n onAppClick,\n}: {\n app: Resource\n onClose: () => void\n onAppClick?: (app: Resource) => void\n}) {\n return (\n <div className=\"fixed inset-0 z-50 flex items-start justify-center overflow-y-auto p-4 sm:p-6 md:p-10\">\n {/* scrim */}\n <button\n type=\"button\"\n aria-label=\"Close details panel\"\n onClick={onClose}\n className=\"fixed inset-0 -z-10 bg-black/40 backdrop-blur-[2px] animate-in fade-in\"\n />\n {/* centered card — wide enough for data tables (sub-resources have 5\n columns incl. \"Access Contacts\"/\"AWS Account\" that wrapped at 760px);\n prose inside AppDetails is width-capped separately so it stays\n readable. Caps at 94vw so it never touches the edges. */}\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={`${app.displayName} details`}\n className=\"relative w-full max-w-[min(1120px,94vw)] my-auto rounded-[var(--radius)] border border-border bg-background shadow-2xl animate-in fade-in zoom-in-95 duration-200\"\n >\n <div className=\"max-h-[85vh] overflow-y-auto px-6 py-5 sm:px-8 sm:py-7\">\n <AppDetails\n app={app}\n onAppClick={onAppClick}\n onClosePanel={onClose}\n />\n </div>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":";;AAaO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,SACE,qBAAC,OAAA,EAAI,WAAU,yFAEb,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,cAAW;AAAA,QACX,SAAS;AAAA,QACT,WAAU;AAAA,MAAA;AAAA,IAAA;AAAA,IAMZ;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,cAAW;AAAA,QACX,cAAY,GAAG,IAAI,WAAW;AAAA,QAC9B,WAAU;AAAA,QAEV,UAAA,oBAAC,OAAA,EAAI,WAAU,0DACb,UAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA,cAAc;AAAA,UAAA;AAAA,QAAA,EAChB,CACF;AAAA,MAAA;AAAA,IAAA;AAAA,EACF,GACF;AAEJ;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-frontend-core",
|
|
3
|
-
"version": "0.4.0-alpha-
|
|
3
|
+
"version": "0.4.0-alpha-20260807203854",
|
|
4
4
|
"description": "Frontend core library for App Catalog",
|
|
5
5
|
"homepage": "https://github.com/lislon/app-catalog",
|
|
6
6
|
"repository": {
|
|
@@ -134,8 +134,8 @@
|
|
|
134
134
|
"vite-plugin-static-copy": "^3.1.4",
|
|
135
135
|
"vite-plugin-svgr": "^4.2.0",
|
|
136
136
|
"vitest": "^4.1.2",
|
|
137
|
-
"@igstack/app-catalog-backend-core": "0.4.0-alpha-
|
|
138
|
-
"@igstack/app-catalog-shared-core": "0.4.0-alpha-
|
|
137
|
+
"@igstack/app-catalog-backend-core": "0.4.0-alpha-20260807203854",
|
|
138
|
+
"@igstack/app-catalog-shared-core": "0.4.0-alpha-20260807203854"
|
|
139
139
|
},
|
|
140
140
|
"peerDependencies": {
|
|
141
141
|
"react": "19.1.2",
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"vite": "^6.3.5",
|
|
145
145
|
"vite-plugin-svgr": "^4.2.0"
|
|
146
146
|
},
|
|
147
|
-
"gitHead": "
|
|
147
|
+
"gitHead": "0a15e91dad60cc02ed4998f363c812832fa6e74a",
|
|
148
148
|
"scripts": {
|
|
149
149
|
"build": "vite build",
|
|
150
150
|
"build:lenient": "vite build --mode lenient",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Check, Copy, User } from 'lucide-react'
|
|
1
|
+
import { Check, Copy, Mail, User } from 'lucide-react'
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
3
3
|
import { Badge } from '~/ui/badge'
|
|
4
|
+
import { Popover, PopoverContent, PopoverTrigger } from '~/ui/popover'
|
|
4
5
|
import { useAppCatalogContext } from '~/modules/appCatalog'
|
|
5
6
|
import { getPersonBySlug } from '~/modules/appCatalog/utils/resolveHelpers'
|
|
6
7
|
|
|
@@ -8,6 +9,12 @@ interface PersonBadgeProps {
|
|
|
8
9
|
slug: string
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Person chip. Clicking it opens a small popover exposing BOTH the full name
|
|
14
|
+
* and the email, each with its own copy button — so the user picks what to copy
|
|
15
|
+
* (previously a single button copied only the email). Email also links to
|
|
16
|
+
* mailto:. Falls back to just the name (no popover) when no email is known.
|
|
17
|
+
*/
|
|
11
18
|
export function PersonBadge({ slug }: PersonBadgeProps) {
|
|
12
19
|
const { persons } = useAppCatalogContext()
|
|
13
20
|
const person = getPersonBySlug(persons, slug)
|
|
@@ -15,55 +22,121 @@ export function PersonBadge({ slug }: PersonBadgeProps) {
|
|
|
15
22
|
const displayName = person
|
|
16
23
|
? `${person.firstName} ${person.lastName}`.trim() || slug
|
|
17
24
|
: slug
|
|
18
|
-
|
|
19
25
|
const email = person?.email
|
|
20
26
|
|
|
27
|
+
// No email → nothing to pick between; render a plain, non-interactive chip.
|
|
28
|
+
if (!email) {
|
|
29
|
+
return (
|
|
30
|
+
<Badge
|
|
31
|
+
variant="outline"
|
|
32
|
+
className="font-normal inline-flex items-center gap-1"
|
|
33
|
+
title={displayName}
|
|
34
|
+
>
|
|
35
|
+
<User className="size-3" />
|
|
36
|
+
{displayName}
|
|
37
|
+
</Badge>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
21
41
|
return (
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
<Popover>
|
|
43
|
+
<PopoverTrigger asChild>
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
className="inline-flex"
|
|
47
|
+
title={`${displayName} · ${email}`}
|
|
48
|
+
>
|
|
49
|
+
<Badge
|
|
50
|
+
variant="outline"
|
|
51
|
+
className="font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors"
|
|
52
|
+
>
|
|
53
|
+
<User className="size-3" />
|
|
54
|
+
{displayName}
|
|
55
|
+
<Copy className="size-3 opacity-50" />
|
|
56
|
+
</Badge>
|
|
57
|
+
</button>
|
|
58
|
+
</PopoverTrigger>
|
|
59
|
+
<PopoverContent align="start" className="w-64 p-2">
|
|
60
|
+
<CopyRow
|
|
61
|
+
icon={<User className="size-3.5" />}
|
|
62
|
+
label="Name"
|
|
63
|
+
value={displayName}
|
|
64
|
+
/>
|
|
65
|
+
<CopyRow
|
|
66
|
+
icon={<Mail className="size-3.5" />}
|
|
67
|
+
label="Email"
|
|
68
|
+
value={email}
|
|
69
|
+
href={`mailto:${email}`}
|
|
70
|
+
/>
|
|
71
|
+
</PopoverContent>
|
|
72
|
+
</Popover>
|
|
31
73
|
)
|
|
32
74
|
}
|
|
33
75
|
|
|
34
|
-
|
|
76
|
+
/** One copyable field (label + value + copy button), value optionally a link. */
|
|
77
|
+
function CopyRow({
|
|
78
|
+
icon,
|
|
79
|
+
label,
|
|
80
|
+
value,
|
|
81
|
+
href,
|
|
82
|
+
}: {
|
|
83
|
+
icon: React.ReactNode
|
|
84
|
+
label: string
|
|
85
|
+
value: string
|
|
86
|
+
href?: string
|
|
87
|
+
}) {
|
|
35
88
|
const [copied, setCopied] = useState(false)
|
|
36
89
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
|
37
90
|
|
|
38
|
-
useEffect(
|
|
39
|
-
|
|
91
|
+
useEffect(
|
|
92
|
+
() => () => {
|
|
40
93
|
if (timeoutRef.current) clearTimeout(timeoutRef.current)
|
|
41
|
-
}
|
|
42
|
-
|
|
94
|
+
},
|
|
95
|
+
[],
|
|
96
|
+
)
|
|
43
97
|
|
|
44
98
|
const handleCopy = useCallback(
|
|
45
99
|
(e: React.MouseEvent) => {
|
|
46
100
|
e.stopPropagation()
|
|
47
|
-
navigator.clipboard.writeText(
|
|
101
|
+
void navigator.clipboard.writeText(value)
|
|
48
102
|
setCopied(true)
|
|
49
103
|
if (timeoutRef.current) clearTimeout(timeoutRef.current)
|
|
50
104
|
timeoutRef.current = setTimeout(() => setCopied(false), 2000)
|
|
51
105
|
},
|
|
52
|
-
[
|
|
106
|
+
[value],
|
|
53
107
|
)
|
|
54
108
|
|
|
55
109
|
return (
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
className="
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
110
|
+
<div className="flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted">
|
|
111
|
+
<span className="text-muted-foreground shrink-0">{icon}</span>
|
|
112
|
+
<span className="min-w-0 flex-1">
|
|
113
|
+
<span className="block text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
114
|
+
{label}
|
|
115
|
+
</span>
|
|
116
|
+
{href ? (
|
|
117
|
+
<a
|
|
118
|
+
href={href}
|
|
119
|
+
className="block truncate text-[13px] text-primary hover:underline"
|
|
120
|
+
>
|
|
121
|
+
{value}
|
|
122
|
+
</a>
|
|
123
|
+
) : (
|
|
124
|
+
<span className="block truncate text-[13px]">{value}</span>
|
|
125
|
+
)}
|
|
126
|
+
</span>
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
onClick={handleCopy}
|
|
130
|
+
className="shrink-0 rounded p-1 text-muted-foreground hover:text-primary hover:bg-background transition-colors"
|
|
131
|
+
title={copied ? 'Copied!' : `Copy ${label.toLowerCase()}`}
|
|
132
|
+
aria-label={`Copy ${label.toLowerCase()}`}
|
|
133
|
+
>
|
|
134
|
+
{copied ? (
|
|
135
|
+
<Check className="size-3.5 text-green-600" />
|
|
136
|
+
) : (
|
|
137
|
+
<Copy className="size-3.5" />
|
|
138
|
+
)}
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
68
141
|
)
|
|
69
142
|
}
|
|
@@ -29,12 +29,15 @@ export function LauncherDetailPanel({
|
|
|
29
29
|
onClick={onClose}
|
|
30
30
|
className="fixed inset-0 -z-10 bg-black/40 backdrop-blur-[2px] animate-in fade-in"
|
|
31
31
|
/>
|
|
32
|
-
{/* centered card
|
|
32
|
+
{/* centered card — wide enough for data tables (sub-resources have 5
|
|
33
|
+
columns incl. "Access Contacts"/"AWS Account" that wrapped at 760px);
|
|
34
|
+
prose inside AppDetails is width-capped separately so it stays
|
|
35
|
+
readable. Caps at 94vw so it never touches the edges. */}
|
|
33
36
|
<div
|
|
34
37
|
role="dialog"
|
|
35
38
|
aria-modal="true"
|
|
36
39
|
aria-label={`${app.displayName} details`}
|
|
37
|
-
className="relative w-full max-w-[
|
|
40
|
+
className="relative w-full max-w-[min(1120px,94vw)] my-auto rounded-[var(--radius)] border border-border bg-background shadow-2xl animate-in fade-in zoom-in-95 duration-200"
|
|
38
41
|
>
|
|
39
42
|
<div className="max-h-[85vh] overflow-y-auto px-6 py-5 sm:px-8 sm:py-7">
|
|
40
43
|
<AppDetails
|