showpane 0.4.28 → 0.4.29
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/bundle/meta/scaffold-manifest.json +5 -5
- package/bundle/scaffold/src/app/(portal)/client/page.tsx +3 -7
- package/bundle/scaffold/src/components/portal-login.tsx +41 -26
- package/bundle/scaffold/src/components/portal-shell.tsx +45 -20
- package/bundle/scaffold/src/lib/branding.ts +1 -1
- package/bundle/toolchain/CLI_VERSION +1 -1
- package/bundle/toolchain/skills/portal-create/SKILL.md +12 -11
- package/bundle/toolchain/skills/portal-create/SKILL.md.tmpl +12 -11
- package/bundle/toolchain/skills/portal-setup/SKILL.md +3 -3
- package/bundle/toolchain/skills/portal-setup/SKILL.md.tmpl +3 -3
- package/bundle/toolchain/skills/portal-update/SKILL.md +2 -0
- package/bundle/toolchain/skills/portal-update/SKILL.md.tmpl +2 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-04-13T23:
|
|
3
|
+
"generatedAt": "2026-04-13T23:58:51.565Z",
|
|
4
4
|
"scaffoldVersion": "0.2.7",
|
|
5
5
|
"files": {
|
|
6
6
|
".env.example": "ed105f2bdcd1888a98181d55e3c9f7d6eff3ae9c3e2366c2e777a12e3caddfa7",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"src/app/(portal)/client/example/example-client.tsx": "ed32b111acea861f448d865338f8841d47c6ca7c2f87ed30d85bb0804940d4ec",
|
|
29
29
|
"src/app/(portal)/client/example/page.tsx": "f330864f63c9feea76c8a62c3eba3ce57578627e0d4abd929fd7fefdfc7af058",
|
|
30
30
|
"src/app/(portal)/client/layout.tsx": "4f43871510408a81da229d48ae316ec1d1c1beda93121922246300a2c8fd0999",
|
|
31
|
-
"src/app/(portal)/client/page.tsx": "
|
|
31
|
+
"src/app/(portal)/client/page.tsx": "856b70d4514e09e6efbb933421dbe7dfcfcea04321d142d0aca4b5dff6c2dc95",
|
|
32
32
|
"src/app/api/client-auth/route.ts": "ce1858559b1e944d5b1dc719d1f03bebf66286671700b1b5397382109f0f1e0d",
|
|
33
33
|
"src/app/api/client-auth/share/route.ts": "ed82414212dcd26af8c6c0f2bd44d9d79a727ed35cfedbac8c4077a6220aad14",
|
|
34
34
|
"src/app/api/client-events/route.ts": "13d545537b7e8ce421e6169d25c105adf2a2de3d978ae0a2c6751ff5f7d2eb33",
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"src/app/layout.tsx": "c17aabeb2b486f023e777230343ace6cc06840f641a10b9dd9f65e092018f82f",
|
|
42
42
|
"src/app/page.tsx": "1f71205c3ae30bf6929d37947b0c94ae53aed33b6689b7d6b13066ad51c1bc14",
|
|
43
43
|
"src/components/copy-button.tsx": "2f3d1d8a6a0a570c8d78e19c3c15519c44af17b5d8893ae5a5f57db5ecce7077",
|
|
44
|
-
"src/components/portal-login.tsx": "
|
|
45
|
-
"src/components/portal-shell.tsx": "
|
|
44
|
+
"src/components/portal-login.tsx": "cd20f5023a427202fc031d403048ffd7d8966cfa07b96fadc0877e553c5bc9bb",
|
|
45
|
+
"src/components/portal-shell.tsx": "098e65be5a4303644e5302e345c1fb7601493dbdda0421f59b1f428c918a8918",
|
|
46
46
|
"src/lib/abuse-controls.ts": "d79d58d93267aca48ad0b7b9b91f753c9a3c27263e4e98daf768a950c44a6fc6",
|
|
47
|
-
"src/lib/branding.ts": "
|
|
47
|
+
"src/lib/branding.ts": "9abd43d77ea35c2e39e237cb9a5c45d20cf7830a3685c4f9a545f0d5333ee012",
|
|
48
48
|
"src/lib/client-auth.ts": "b9bdfe77dbe5d6ec6c6a930627fc43d3253f0d76fd8fc4093af5a75742bebe42",
|
|
49
49
|
"src/lib/client-portals.ts": "9b531f9a9ea459b4ab85257b9dd282874fa1422838fe89d511940e417114216a",
|
|
50
50
|
"src/lib/control-plane.ts": "e0cf39f28ec7de715fd5cfbb5f4240773fcd3d775cd1677588dd749fff740a0e",
|
|
@@ -13,7 +13,7 @@ export default async function ClientLogin({
|
|
|
13
13
|
const portalSlug = params.portal?.trim() || null;
|
|
14
14
|
|
|
15
15
|
let companyName = "Showpane";
|
|
16
|
-
let companyUrl = "https://showpane.com";
|
|
16
|
+
let companyUrl: string | null = "https://showpane.com";
|
|
17
17
|
let supportEmail = "support@showpane.com";
|
|
18
18
|
let portalLabel = "Client Portal";
|
|
19
19
|
let description = "Private portal access. Sign in with the credentials you were sent.";
|
|
@@ -36,9 +36,7 @@ export default async function ClientLogin({
|
|
|
36
36
|
websiteUrl: state?.organization?.websiteUrl,
|
|
37
37
|
fallbackName: orgName,
|
|
38
38
|
});
|
|
39
|
-
|
|
40
|
-
companyUrl = state.organization.websiteUrl;
|
|
41
|
-
}
|
|
39
|
+
companyUrl = state?.organization?.websiteUrl ?? null;
|
|
42
40
|
if (state?.organization?.supportEmail) {
|
|
43
41
|
supportEmail = state.organization.supportEmail;
|
|
44
42
|
}
|
|
@@ -79,9 +77,7 @@ export default async function ClientLogin({
|
|
|
79
77
|
fallbackName: orgName,
|
|
80
78
|
});
|
|
81
79
|
}
|
|
82
|
-
|
|
83
|
-
companyUrl = organization.websiteUrl;
|
|
84
|
-
}
|
|
80
|
+
companyUrl = organization?.websiteUrl ?? null;
|
|
85
81
|
if (organization?.supportEmail) {
|
|
86
82
|
supportEmail = organization.supportEmail;
|
|
87
83
|
}
|
|
@@ -7,7 +7,7 @@ export type PortalLoginProps = {
|
|
|
7
7
|
companyName: string;
|
|
8
8
|
companyLogoSrc?: string | null;
|
|
9
9
|
companyLogoAlt?: string;
|
|
10
|
-
companyUrl
|
|
10
|
+
companyUrl?: string | null;
|
|
11
11
|
portalLabel?: string;
|
|
12
12
|
description?: string;
|
|
13
13
|
supportEmail: string;
|
|
@@ -38,12 +38,33 @@ export function PortalLogin({
|
|
|
38
38
|
const resolvedAuthEndpoint = authEndpoint ?? "/api/client-auth";
|
|
39
39
|
const resolvedRedirectBasePath = redirectBasePath ?? "/client";
|
|
40
40
|
const resolvedCompanyLogoAlt = companyLogoAlt ?? companyName;
|
|
41
|
+
const brandMark = (
|
|
42
|
+
<>
|
|
43
|
+
{companyLogoSrc && !logoFailed ? (
|
|
44
|
+
<img
|
|
45
|
+
src={companyLogoSrc}
|
|
46
|
+
alt={resolvedCompanyLogoAlt}
|
|
47
|
+
className="h-7 w-7 rounded-lg object-cover"
|
|
48
|
+
onError={() => setLogoFailed(true)}
|
|
49
|
+
/>
|
|
50
|
+
) : (
|
|
51
|
+
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-gray-900">
|
|
52
|
+
<span className="text-xs font-bold text-white">
|
|
53
|
+
{companyName[0]?.toUpperCase() || "S"}
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
<span className="text-base font-bold tracking-tight text-gray-900">{companyName}</span>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
41
60
|
|
|
42
|
-
let displayDomain: string;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
let displayDomain: string | null = null;
|
|
62
|
+
if (companyUrl) {
|
|
63
|
+
try {
|
|
64
|
+
displayDomain = new URL(companyUrl).hostname;
|
|
65
|
+
} catch {
|
|
66
|
+
displayDomain = companyUrl;
|
|
67
|
+
}
|
|
47
68
|
}
|
|
48
69
|
|
|
49
70
|
async function handleSubmit(e: FormEvent) {
|
|
@@ -89,23 +110,15 @@ export function PortalLogin({
|
|
|
89
110
|
<div className="absolute -bottom-32 -left-32 h-96 w-96 rounded-full bg-primary/5 blur-[120px]" />
|
|
90
111
|
|
|
91
112
|
<div className="relative z-10 w-full max-w-sm">
|
|
92
|
-
|
|
93
|
-
{
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-gray-900">
|
|
102
|
-
<span className="text-xs font-bold text-white">
|
|
103
|
-
{companyName[0]?.toUpperCase() || "S"}
|
|
104
|
-
</span>
|
|
105
|
-
</div>
|
|
106
|
-
)}
|
|
107
|
-
<span className="text-base font-bold tracking-tight text-gray-900">{companyName}</span>
|
|
108
|
-
</a>
|
|
113
|
+
{companyUrl ? (
|
|
114
|
+
<a href={companyUrl} className="mx-auto mb-8 flex w-fit items-center gap-2 transition-opacity hover:opacity-70">
|
|
115
|
+
{brandMark}
|
|
116
|
+
</a>
|
|
117
|
+
) : (
|
|
118
|
+
<div className="mx-auto mb-8 flex w-fit items-center gap-2">
|
|
119
|
+
{brandMark}
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
109
122
|
|
|
110
123
|
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
|
111
124
|
<div className="mb-5">
|
|
@@ -173,9 +186,11 @@ export function PortalLogin({
|
|
|
173
186
|
</div>
|
|
174
187
|
|
|
175
188
|
<div className="mt-5 space-y-2 text-center text-xs">
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
189
|
+
{companyUrl && displayDomain ? (
|
|
190
|
+
<p className="text-gray-400">
|
|
191
|
+
Not a client? <a href={companyUrl} className="text-gray-500 underline underline-offset-2 transition-colors hover:text-gray-700">Visit {displayDomain}</a>
|
|
192
|
+
</p>
|
|
193
|
+
) : null}
|
|
179
194
|
<p className="text-gray-400">
|
|
180
195
|
Lost your credentials? Email <span className="font-medium text-gray-500">{supportEmail}</span>
|
|
181
196
|
</p>
|
|
@@ -33,7 +33,7 @@ export type PortalShellProps = {
|
|
|
33
33
|
portalLabel?: string;
|
|
34
34
|
|
|
35
35
|
clientName: string;
|
|
36
|
-
clientLogoSrc
|
|
36
|
+
clientLogoSrc?: string | null;
|
|
37
37
|
clientLogoAlt: string;
|
|
38
38
|
|
|
39
39
|
tabs: PortalTab[];
|
|
@@ -181,6 +181,7 @@ export function PortalShell({
|
|
|
181
181
|
const [copyError, setCopyError] = useState(false);
|
|
182
182
|
const [visitorId] = useState(() => getOrCreateVisitorId());
|
|
183
183
|
const [showLocalBanner, setShowLocalBanner] = useState(false);
|
|
184
|
+
const [clientLogoFailed, setClientLogoFailed] = useState(false);
|
|
184
185
|
|
|
185
186
|
useEffect(() => {
|
|
186
187
|
const syncFromHash = () => setActiveTab(readHashTab(tabIds));
|
|
@@ -207,6 +208,10 @@ export function PortalShell({
|
|
|
207
208
|
setShowLocalBanner(host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0");
|
|
208
209
|
}, []);
|
|
209
210
|
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
setClientLogoFailed(false);
|
|
213
|
+
}, [clientLogoSrc]);
|
|
214
|
+
|
|
210
215
|
useSectionTimeTracking(activeTab, resolvedEventsEndpoint, visitorId);
|
|
211
216
|
|
|
212
217
|
function switchTab(tab: string) {
|
|
@@ -234,7 +239,8 @@ export function PortalShell({
|
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
const activeContent = tabs.find((t) => t.id === activeTab)?.content ?? null;
|
|
237
|
-
const
|
|
242
|
+
const showContactFooter = hideFooterOnTab ? activeTab !== hideFooterOnTab : true;
|
|
243
|
+
const clientInitial = clientName[0]?.toUpperCase() || "?";
|
|
238
244
|
|
|
239
245
|
return (
|
|
240
246
|
<div className="flex min-h-screen flex-col bg-gray-50">
|
|
@@ -262,14 +268,25 @@ export function PortalShell({
|
|
|
262
268
|
<div className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-white bg-gray-900">
|
|
263
269
|
{companyLogo}
|
|
264
270
|
</div>
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
271
|
+
{clientLogoSrc && !clientLogoFailed ? (
|
|
272
|
+
<img
|
|
273
|
+
src={clientLogoSrc}
|
|
274
|
+
alt={clientLogoAlt}
|
|
275
|
+
width={32}
|
|
276
|
+
height={32}
|
|
277
|
+
className="-ml-2 h-8 w-8 rounded-full border-2 border-white bg-white object-cover"
|
|
278
|
+
loading="eager"
|
|
279
|
+
onError={() => setClientLogoFailed(true)}
|
|
280
|
+
/>
|
|
281
|
+
) : (
|
|
282
|
+
<div
|
|
283
|
+
role="img"
|
|
284
|
+
aria-label={clientLogoAlt}
|
|
285
|
+
className="-ml-2 flex h-8 w-8 items-center justify-center rounded-full border-2 border-white bg-gray-100"
|
|
286
|
+
>
|
|
287
|
+
<span className="text-[11px] font-semibold text-gray-700">{clientInitial}</span>
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
273
290
|
</div>
|
|
274
291
|
<div>
|
|
275
292
|
<h1 className="text-sm font-bold tracking-tight text-gray-900">
|
|
@@ -342,8 +359,8 @@ export function PortalShell({
|
|
|
342
359
|
{activeContent}
|
|
343
360
|
</main>
|
|
344
361
|
|
|
345
|
-
|
|
346
|
-
|
|
362
|
+
<footer className="mt-auto border-t bg-white">
|
|
363
|
+
{showContactFooter ? (
|
|
347
364
|
<div className="mx-auto flex max-w-4xl items-center gap-4 px-4 py-4 sm:px-6">
|
|
348
365
|
<img
|
|
349
366
|
src={contact.avatarSrc}
|
|
@@ -377,20 +394,28 @@ export function PortalShell({
|
|
|
377
394
|
) : null}
|
|
378
395
|
</div>
|
|
379
396
|
</div>
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
397
|
+
) : null}
|
|
398
|
+
<div
|
|
399
|
+
className={cn(
|
|
400
|
+
"flex flex-wrap items-center justify-center gap-2 px-4 py-3 text-[11px] text-gray-400 sm:px-6",
|
|
401
|
+
showContactFooter ? "border-t border-gray-100" : "",
|
|
402
|
+
)}
|
|
403
|
+
>
|
|
404
|
+
<p>Last updated {lastUpdated}</p>
|
|
405
|
+
<span className="text-gray-200">·</span>
|
|
406
|
+
<p>
|
|
407
|
+
Created using{" "}
|
|
383
408
|
<a
|
|
384
409
|
href="https://showpane.com"
|
|
385
410
|
target="_blank"
|
|
386
411
|
rel="noopener noreferrer"
|
|
387
|
-
className="
|
|
412
|
+
className="font-medium text-gray-500 transition-colors hover:text-gray-700"
|
|
388
413
|
>
|
|
389
|
-
|
|
414
|
+
showpane.com
|
|
390
415
|
</a>
|
|
391
|
-
</
|
|
392
|
-
</
|
|
393
|
-
|
|
416
|
+
</p>
|
|
417
|
+
</div>
|
|
418
|
+
</footer>
|
|
394
419
|
</div>
|
|
395
420
|
);
|
|
396
421
|
}
|
|
@@ -36,7 +36,7 @@ export function getDomainFromWebsite(value?: string | null): string | null {
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Fetch a company logo URL from a domain.
|
|
39
|
-
* Uses
|
|
39
|
+
* Uses Logo.dev for hosted brand logos.
|
|
40
40
|
* Falls back to a UI Avatars URL with the company initial.
|
|
41
41
|
*/
|
|
42
42
|
export function getLogoUrl(domain: string, fallbackName?: string): string {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.4.
|
|
1
|
+
0.4.29
|
|
@@ -168,11 +168,10 @@ The script returns `{"valid":true}` or `{"valid":false,"reason":"...","message":
|
|
|
168
168
|
|
|
169
169
|
If invalid, explain the issue and ask for a different slug.
|
|
170
170
|
|
|
171
|
-
Also determine the client's website domain. This enables auto-branding
|
|
171
|
+
Also determine the client's website domain. This enables auto-branding, but keep the step deterministic:
|
|
172
172
|
- if the domain is explicit in the transcript or user prompt, use it
|
|
173
|
-
- otherwise
|
|
174
|
-
-
|
|
175
|
-
- if confidence is low, ask directly
|
|
173
|
+
- otherwise ask the user directly for the official site/domain
|
|
174
|
+
- do not guess, browse, or invent a domain as part of this skill flow
|
|
176
175
|
|
|
177
176
|
Store the confirmed value in `ClientPortal.websiteUrl`.
|
|
178
177
|
Use it to derive `ClientPortal.logoUrl` via `getBrandLogoUrl(...)`.
|
|
@@ -299,6 +298,7 @@ import { PortalShell } from "@/components/portal-shell";
|
|
|
299
298
|
- `contact` — object with `name`, `title`, `avatarSrc`, `email` (from the `get-org.ts` result, not from ad-hoc config or DB probing)
|
|
300
299
|
- `tabs` — array of tab objects with `id`, `label`, `icon`, `content`, and optional `badge`
|
|
301
300
|
- `hideFooterOnTab` — set to `"overview"` (hides the contact footer on the first tab since it typically has contact info inline)
|
|
301
|
+
- Keep the shared `PortalShell` footer attribution intact. New portals should retain the `Created using showpane.com` link rendered by the shell.
|
|
302
302
|
|
|
303
303
|
**Styling conventions (match the example portal exactly):**
|
|
304
304
|
- Cards: `rounded-2xl border bg-white shadow-sm`
|
|
@@ -356,13 +356,14 @@ For onboarding, carry them forward quietly and show them at the access phase.
|
|
|
356
356
|
After generating the files, read them back and verify:
|
|
357
357
|
|
|
358
358
|
1. **PortalShell used?** The client component must use `<PortalShell>` as its root element.
|
|
359
|
-
2. **
|
|
360
|
-
3. **
|
|
361
|
-
4. **
|
|
362
|
-
5. **
|
|
363
|
-
6. **
|
|
364
|
-
7. **
|
|
365
|
-
8. **
|
|
359
|
+
2. **Shared attribution preserved?** Do not replace or bypass `PortalShell` in a way that removes the `Created using showpane.com` footer link.
|
|
360
|
+
3. **Minimum 2 tabs?** Check the `tabs` array has at least 2 entries.
|
|
361
|
+
4. **Contact info in props?** The `contact` prop must have `name`, `title`, `avatarSrc`, `email`.
|
|
362
|
+
5. **"use client" directive?** Must be the first line of the client component.
|
|
363
|
+
6. **Imports correct?** `cn` from `@/lib/utils`, `PortalShell` from `@/components/portal-shell`.
|
|
364
|
+
7. **No hardcoded localhost URLs?** Links should be relative or use placeholders.
|
|
365
|
+
8. **Responsive patterns?** Check for `sm:` breakpoints on grids and padding.
|
|
366
|
+
9. **Tab content functions?** Each tab should have its own function, not inline JSX.
|
|
366
367
|
|
|
367
368
|
If any check fails, fix the issue before proceeding.
|
|
368
369
|
|
|
@@ -73,11 +73,10 @@ The script returns `{"valid":true}` or `{"valid":false,"reason":"...","message":
|
|
|
73
73
|
|
|
74
74
|
If invalid, explain the issue and ask for a different slug.
|
|
75
75
|
|
|
76
|
-
Also determine the client's website domain. This enables auto-branding
|
|
76
|
+
Also determine the client's website domain. This enables auto-branding, but keep the step deterministic:
|
|
77
77
|
- if the domain is explicit in the transcript or user prompt, use it
|
|
78
|
-
- otherwise
|
|
79
|
-
-
|
|
80
|
-
- if confidence is low, ask directly
|
|
78
|
+
- otherwise ask the user directly for the official site/domain
|
|
79
|
+
- do not guess, browse, or invent a domain as part of this skill flow
|
|
81
80
|
|
|
82
81
|
Store the confirmed value in `ClientPortal.websiteUrl`.
|
|
83
82
|
Use it to derive `ClientPortal.logoUrl` via `getBrandLogoUrl(...)`.
|
|
@@ -204,6 +203,7 @@ import { PortalShell } from "@/components/portal-shell";
|
|
|
204
203
|
- `contact` — object with `name`, `title`, `avatarSrc`, `email` (from the `get-org.ts` result, not from ad-hoc config or DB probing)
|
|
205
204
|
- `tabs` — array of tab objects with `id`, `label`, `icon`, `content`, and optional `badge`
|
|
206
205
|
- `hideFooterOnTab` — set to `"overview"` (hides the contact footer on the first tab since it typically has contact info inline)
|
|
206
|
+
- Keep the shared `PortalShell` footer attribution intact. New portals should retain the `Created using showpane.com` link rendered by the shell.
|
|
207
207
|
|
|
208
208
|
**Styling conventions (match the example portal exactly):**
|
|
209
209
|
- Cards: `rounded-2xl border bg-white shadow-sm`
|
|
@@ -261,13 +261,14 @@ For onboarding, carry them forward quietly and show them at the access phase.
|
|
|
261
261
|
After generating the files, read them back and verify:
|
|
262
262
|
|
|
263
263
|
1. **PortalShell used?** The client component must use `<PortalShell>` as its root element.
|
|
264
|
-
2. **
|
|
265
|
-
3. **
|
|
266
|
-
4. **
|
|
267
|
-
5. **
|
|
268
|
-
6. **
|
|
269
|
-
7. **
|
|
270
|
-
8. **
|
|
264
|
+
2. **Shared attribution preserved?** Do not replace or bypass `PortalShell` in a way that removes the `Created using showpane.com` footer link.
|
|
265
|
+
3. **Minimum 2 tabs?** Check the `tabs` array has at least 2 entries.
|
|
266
|
+
4. **Contact info in props?** The `contact` prop must have `name`, `title`, `avatarSrc`, `email`.
|
|
267
|
+
5. **"use client" directive?** Must be the first line of the client component.
|
|
268
|
+
6. **Imports correct?** `cn` from `@/lib/utils`, `PortalShell` from `@/components/portal-shell`.
|
|
269
|
+
7. **No hardcoded localhost URLs?** Links should be relative or use placeholders.
|
|
270
|
+
8. **Responsive patterns?** Check for `sm:` breakpoints on grids and padding.
|
|
271
|
+
9. **Tab content functions?** Each tab should have its own function, not inline JSX.
|
|
271
272
|
|
|
272
273
|
If any check fails, fix the issue before proceeding.
|
|
273
274
|
|
|
@@ -197,9 +197,9 @@ Do not restart the whole org questionnaire from the top.
|
|
|
197
197
|
3. **Contact email** (required) — e.g., "jane@acme.com". Displayed in the portal footer as a mailto link.
|
|
198
198
|
4. **Contact title** (optional, default: "Account Manager") — e.g., "Director", "Partner", "Client Success Lead". Shown next to the contact name in the portal footer.
|
|
199
199
|
5. **Contact phone** (optional) — e.g., "+44 7700 900000". If provided, displayed alongside email in the portal footer.
|
|
200
|
-
6. **Company website URL** — e.g., "acme.com". Ask for it plainly instead of framing it as optional.
|
|
201
|
-
-
|
|
202
|
-
-
|
|
200
|
+
6. **Company website URL** — e.g., "acme.com". Ask for it plainly instead of framing it as optional. Store the normalized value in `Organization.websiteUrl` so runtime branding can derive the company logo.
|
|
201
|
+
- Do not generate and persist a provider logo URL in `Organization.logoUrl` from this field
|
|
202
|
+
- Only set `Organization.logoUrl` if the user explicitly gives you a direct logo asset URL they want pinned
|
|
203
203
|
7. **Contact avatar**: Auto-populated from the contact email via Gravatar. No need to ask — just use `getAvatarUrl(email, contactName)` from `app/src/lib/branding.ts` and store in `Organization.contactAvatar`
|
|
204
204
|
|
|
205
205
|
Generate an org slug from the organization name: lowercase, replace spaces with hyphens, strip non-alphanumeric characters except hyphens, remove consecutive hyphens. For example, "Acme Consulting Ltd." becomes `acme-consulting-ltd`. Confirm the generated slug with the user and allow them to override it.
|
|
@@ -114,9 +114,9 @@ Do not restart the whole org questionnaire from the top.
|
|
|
114
114
|
3. **Contact email** (required) — e.g., "jane@acme.com". Displayed in the portal footer as a mailto link.
|
|
115
115
|
4. **Contact title** (optional, default: "Account Manager") — e.g., "Director", "Partner", "Client Success Lead". Shown next to the contact name in the portal footer.
|
|
116
116
|
5. **Contact phone** (optional) — e.g., "+44 7700 900000". If provided, displayed alongside email in the portal footer.
|
|
117
|
-
6. **Company website URL** — e.g., "acme.com". Ask for it plainly instead of framing it as optional.
|
|
118
|
-
-
|
|
119
|
-
-
|
|
117
|
+
6. **Company website URL** — e.g., "acme.com". Ask for it plainly instead of framing it as optional. Store the normalized value in `Organization.websiteUrl` so runtime branding can derive the company logo.
|
|
118
|
+
- Do not generate and persist a provider logo URL in `Organization.logoUrl` from this field
|
|
119
|
+
- Only set `Organization.logoUrl` if the user explicitly gives you a direct logo asset URL they want pinned
|
|
120
120
|
7. **Contact avatar**: Auto-populated from the contact email via Gravatar. No need to ask — just use `getAvatarUrl(email, contactName)` from `app/src/lib/branding.ts` and store in `Organization.contactAvatar`
|
|
121
121
|
|
|
122
122
|
Generate an org slug from the organization name: lowercase, replace spaces with hyphens, strip non-alphanumeric characters except hyphens, remove consecutive hyphens. For example, "Acme Consulting Ltd." becomes `acme-consulting-ltd`. Confirm the generated slug with the user and allow them to override it.
|
|
@@ -225,6 +225,7 @@ Ask the user what they want to change. Common requests and how to handle them:
|
|
|
225
225
|
**Updating contact info or metadata:**
|
|
226
226
|
1. Update the relevant PortalShell props
|
|
227
227
|
2. Update any inline mentions of the contact in tab content
|
|
228
|
+
3. Keep the shared `PortalShell` footer attribution intact. Do not remove or replace the `Created using showpane.com` link.
|
|
228
229
|
|
|
229
230
|
### Step 7: Make the edits
|
|
230
231
|
|
|
@@ -247,6 +248,7 @@ After making edits, show the user a summary of what changed:
|
|
|
247
248
|
- Which tabs were added, removed, or modified
|
|
248
249
|
- What content was updated
|
|
249
250
|
- Any prop changes on PortalShell
|
|
251
|
+
- Confirm the shared `Created using showpane.com` footer attribution is still present
|
|
250
252
|
|
|
251
253
|
If using git, show the actual diff:
|
|
252
254
|
|
|
@@ -130,6 +130,7 @@ Ask the user what they want to change. Common requests and how to handle them:
|
|
|
130
130
|
**Updating contact info or metadata:**
|
|
131
131
|
1. Update the relevant PortalShell props
|
|
132
132
|
2. Update any inline mentions of the contact in tab content
|
|
133
|
+
3. Keep the shared `PortalShell` footer attribution intact. Do not remove or replace the `Created using showpane.com` link.
|
|
133
134
|
|
|
134
135
|
### Step 7: Make the edits
|
|
135
136
|
|
|
@@ -152,6 +153,7 @@ After making edits, show the user a summary of what changed:
|
|
|
152
153
|
- Which tabs were added, removed, or modified
|
|
153
154
|
- What content was updated
|
|
154
155
|
- Any prop changes on PortalShell
|
|
156
|
+
- Confirm the shared `Created using showpane.com` footer attribution is still present
|
|
155
157
|
|
|
156
158
|
If using git, show the actual diff:
|
|
157
159
|
|