@treasuryspatial/viewer-ui-kit 0.1.36 → 0.1.38

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,198 +1,220 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useShellPanelToggle } from '@treasuryspatial/viewer-react';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
6
+ import { buildLoginHref, useClientHydrated, useComposerAuthState, useSessionLogout, useSurfaceComposerData, } from '@treasuryspatial/viewer-react';
4
7
  import styled from 'styled-components';
5
8
  import TopBar from './TopBar.js';
6
- const Brand = styled.div `
9
+ const BrandLink = styled(Link) `
7
10
  display: flex;
8
11
  align-items: center;
9
- gap: ${(props) => (props.$variant === 'landing' ? '12px' : '8px')};
10
- `;
11
- const BrandCopy = styled.div `
12
- display: flex;
13
- flex-direction: column;
12
+ gap: 14px;
13
+ text-decoration: none;
14
+ color: inherit;
14
15
  min-width: 0;
15
- gap: 2px;
16
+
17
+ &:hover {
18
+ opacity: 0.75;
19
+ }
16
20
  `;
17
21
  const BrandLogo = styled.img `
18
- width: ${(props) => (props.$variant === 'landing' ? 'clamp(32px, 3.5vw + 12px, 64px)' : '32px')};
19
- height: ${(props) => (props.$variant === 'landing' ? 'clamp(32px, 3.5vw + 12px, 64px)' : '32px')};
22
+ width: ${(props) => (props.$variant === 'landing' ? 'clamp(32px, 4vw + 8px, 64px)' : '32px')};
23
+ height: ${(props) => (props.$variant === 'landing' ? 'clamp(32px, 4vw + 8px, 64px)' : '32px')};
20
24
  object-fit: contain;
25
+ flex-shrink: 0;
21
26
  `;
22
27
  const BrandTitle = styled.div `
23
28
  display: flex;
24
29
  align-items: baseline;
25
- flex-wrap: wrap;
26
30
  gap: ${(props) => (props.$variant === 'landing' ? '8px' : '6px')};
27
- font-size: ${(props) => (props.$variant === 'landing' ? '18px' : '18px')};
28
- font-weight: 400;
29
-
30
- @media (min-width: 640px) {
31
- font-size: ${(props) => (props.$variant === 'landing' ? '20px' : '18px')};
32
- }
33
-
34
- @media (min-width: 768px) {
35
- font-size: ${(props) => (props.$variant === 'landing' ? '24px' : '20px')};
36
- }
37
-
38
- @media (min-width: 1024px) {
39
- font-size: ${(props) => (props.$variant === 'landing' ? '30px' : '22px')};
40
- }
41
-
42
- @media (min-width: 1280px) {
43
- font-size: ${(props) => (props.$variant === 'landing' ? '36px' : '24px')};
44
- }
45
-
46
- @media (min-width: 1536px) {
47
- font-size: ${(props) => (props.$variant === 'landing' ? '48px' : '28px')};
48
- }
31
+ font-family: var(--ui-font-family, system-ui);
32
+ font-size: ${(props) => (props.$variant === 'landing' ? 'clamp(18px, 2vw + 10px, 48px)' : '18px')};
33
+ line-height: 1;
34
+ white-space: nowrap;
49
35
  `;
50
36
  const BrandTenant = styled.span `
37
+ color: var(--brand-text-primary);
51
38
  font-weight: 400;
52
- color: var(--brand-primary);
53
- white-space: nowrap;
54
39
  `;
55
- const BrandProduct = styled.span `
40
+ const BrandSurface = styled.span `
41
+ color: var(--brand-primary);
56
42
  font-weight: 400;
57
- color: var(--brand-text-primary);
58
- white-space: nowrap;
59
43
  `;
60
- const BrandSubtitle = styled.div `
61
- font-size: 11px;
62
- color: var(--brand-text-secondary);
63
- line-height: 1.2;
64
- white-space: nowrap;
65
- overflow: hidden;
66
- text-overflow: ellipsis;
67
- `;
68
- const BrandPartnerLogos = styled.div `
44
+ const Actions = styled.div `
69
45
  display: flex;
70
46
  align-items: center;
71
- gap: 8px;
72
- `;
73
- const PartnerLogo = styled.img `
74
- width: 28px;
75
- height: 28px;
76
- object-fit: contain;
77
- `;
78
- const Links = styled.nav `
79
- display: flex;
80
- gap: 18px;
81
- align-items: center;
82
- font-size: 13px;
47
+ gap: ${(props) => (props.$variant === 'landing' ? '22px' : '16px')};
48
+ font-size: ${(props) => (props.$variant === 'landing' ? '14px' : '13px')};
49
+ min-width: 0;
83
50
  `;
84
- const LinkItem = styled.a `
85
- color: var(--brand-text-secondary);
51
+ const NavLink = styled(Link) `
52
+ color: ${(props) => (props.$active ? 'var(--brand-primary)' : 'var(--brand-text-primary)')};
86
53
  text-decoration: none;
87
54
  font-weight: 400;
55
+ white-space: nowrap;
88
56
 
89
57
  &:hover {
90
- color: var(--brand-text-primary);
58
+ color: var(--brand-primary);
91
59
  }
92
60
  `;
93
- const LogoutButton = styled.button `
61
+ const AuthButton = styled.button `
94
62
  border: none;
95
63
  background: transparent;
96
- color: var(--brand-text-secondary);
64
+ color: var(--brand-text-primary);
97
65
  font-weight: 400;
98
- font-size: 13px;
66
+ font-size: inherit;
99
67
  cursor: pointer;
100
68
  padding: 0;
69
+ white-space: nowrap;
101
70
 
102
71
  &:hover {
103
- color: var(--brand-text-primary);
72
+ color: var(--brand-primary);
104
73
  }
105
- `;
106
- const PoweredBy = styled.a `
107
- display: none;
108
- align-items: center;
109
- gap: 8px;
110
- padding: 6px 10px;
111
- border-radius: 999px;
112
- border: 1px solid var(--brand-panel-border);
113
- background: color-mix(in srgb, var(--brand-panel) 90%, transparent);
114
- box-shadow: 0 1px 6px rgba(15, 23, 42, 0.08);
115
- opacity: 0.6;
116
- transition: opacity 0.2s ease;
117
- color: inherit;
118
- text-decoration: none;
119
74
 
120
- &:hover {
121
- opacity: 1;
75
+ .auth-hover {
76
+ display: none;
122
77
  }
123
78
 
124
- @media (min-width: 1024px) {
125
- display: flex;
79
+ &:hover .auth-default {
80
+ display: none;
81
+ }
82
+
83
+ &:hover .auth-hover {
84
+ display: inline;
126
85
  }
127
86
  `;
128
- const PoweredByLogo = styled.img `
129
- width: 16px;
130
- height: 16px;
131
- object-fit: contain;
132
- `;
133
- const PoweredByLabel = styled.div `
134
- font-size: 9px;
135
- text-transform: uppercase;
136
- letter-spacing: 0.12em;
137
- color: var(--brand-text-secondary);
138
- line-height: 1;
139
- `;
140
- const PoweredByTitle = styled.div `
141
- font-size: 11px;
87
+ const AuthLink = styled(Link) `
142
88
  color: var(--brand-text-primary);
143
- line-height: 1.2;
89
+ text-decoration: none;
90
+ font-weight: 400;
91
+ white-space: nowrap;
92
+
93
+ &:hover {
94
+ color: var(--brand-primary);
95
+ }
144
96
  `;
145
- const PoweredByAccent = styled.span `
146
- font-weight: 600;
147
- color: var(--brand-text-primary);
97
+ const SearchWrap = styled.div `
98
+ display: flex;
99
+ align-items: center;
148
100
  `;
149
- const PanelToggleCluster = styled.div `
101
+ const SearchControl = styled.div `
102
+ transition: all 0.2s ease-in-out;
103
+ cursor: pointer;
104
+ width: ${(props) => (props.$expanded ? '220px' : '35px')};
105
+ height: 35px;
106
+ border: 3px solid ${(props) => (props.$expanded ? 'var(--brand-text-secondary)' : 'var(--brand-text-primary)')};
107
+ border-radius: ${(props) => (props.$expanded ? '18px' : '999px')};
108
+ background: var(--brand-background);
150
109
  display: flex;
151
110
  align-items: center;
152
- gap: 10px;
111
+ position: relative;
112
+ overflow: hidden;
153
113
  `;
154
- const PanelToggleText = styled.button `
114
+ const SearchInput = styled.input `
115
+ flex: 1;
116
+ min-width: 0;
117
+ padding: 0 14px;
155
118
  border: none;
119
+ outline: none;
156
120
  background: transparent;
157
- color: var(--brand-text-secondary);
158
- font-weight: 400;
159
- font-size: 11px;
160
- text-transform: uppercase;
161
- letter-spacing: 0.12em;
162
- cursor: pointer;
163
- padding: 0;
121
+ color: var(--brand-text-primary);
122
+ font-family: var(--ui-font-family, system-ui);
123
+ font-size: 13px;
164
124
 
165
- &:hover {
166
- color: var(--brand-text-primary);
125
+ &::placeholder {
126
+ color: var(--brand-text-secondary);
167
127
  }
168
128
  `;
169
- const resolvePoweredByConfig = (manifest) => {
170
- const poweredBy = manifest.branding?.poweredBy;
171
- return {
172
- enabled: poweredBy?.enabled ??
173
- (manifest.branding?.profile ? manifest.branding.profile !== 'treasury-native' : true),
174
- href: poweredBy?.href ?? 'https://treasury.space',
175
- logo: poweredBy?.logo ?? null,
176
- kicker: poweredBy?.kicker ?? 'Powered by',
177
- labelPrefix: poweredBy?.labelPrefix ?? 'Treasury',
178
- labelAccent: poweredBy?.labelAccent ?? manifest.topbar.productLabel ?? 'Composer',
179
- };
129
+ const SearchReset = styled.button `
130
+ display: inline-flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ width: 28px;
134
+ height: 28px;
135
+ margin-right: 4px;
136
+ border: none;
137
+ background: transparent;
138
+ color: var(--brand-text-secondary);
139
+ cursor: pointer;
140
+ font-size: 18px;
141
+ line-height: 1;
142
+ `;
143
+ const formatDisplayName = (value) => {
144
+ const trimmed = value?.trim();
145
+ if (!trimmed)
146
+ return 'user';
147
+ return trimmed.split('@')[0];
180
148
  };
181
- const clearCookie = (name) => {
182
- document.cookie = `${name}=; max-age=0; path=/; samesite=strict`;
149
+ const isRouteActive = (pathname, href) => {
150
+ const routePath = href.split('?')[0] || href;
151
+ if (routePath === '/')
152
+ return pathname === '/';
153
+ return pathname === routePath || pathname.startsWith(`${routePath}/`);
183
154
  };
184
- export default function ManifestTopBar({ manifest, variant = 'canvas', showPoweredBy = true, showLogout = false, showPanelToggle = false, showSubtitle = false, logoutCookieNames = ['treasury_auth_token'], logoutEndpoint = '/api/auth/logout', logoutHref = '/login', }) {
185
- const { collapsed, toggle } = useShellPanelToggle();
186
- const poweredBy = resolvePoweredByConfig(manifest);
187
- const brandLogo = manifest.topbar.logo ?? null;
188
- const brand = (_jsxs(Brand, { "$variant": variant, children: [brandLogo ? (_jsx(BrandLogo, { "$variant": variant, src: brandLogo.src, alt: brandLogo.alt })) : null, _jsxs(BrandCopy, { children: [_jsxs(BrandTitle, { "$variant": variant, children: [_jsx(BrandTenant, { children: manifest.topbar.title }), manifest.topbar.productLabel ? (_jsx(BrandProduct, { children: manifest.topbar.productLabel })) : null] }), showSubtitle && manifest.topbar.subtitle ? (_jsx(BrandSubtitle, { children: manifest.topbar.subtitle })) : null] }), manifest.topbar.subtenantLogo || manifest.topbar.secondaryLogo ? (_jsxs(BrandPartnerLogos, { children: [manifest.topbar.subtenantLogo ? (_jsx(PartnerLogo, { src: manifest.topbar.subtenantLogo.src, alt: manifest.topbar.subtenantLogo.alt })) : null, manifest.topbar.secondaryLogo ? (_jsx(PartnerLogo, { src: manifest.topbar.secondaryLogo.src, alt: manifest.topbar.secondaryLogo.alt })) : null] })) : null] }));
189
- const center = showPoweredBy && poweredBy.enabled ? (_jsxs(PoweredBy, { href: poweredBy.href, target: "_blank", rel: "noreferrer", children: [poweredBy.logo ? (_jsx(PoweredByLogo, { src: poweredBy.logo.src, alt: poweredBy.logo.alt })) : null, _jsxs("div", { children: [_jsx(PoweredByLabel, { children: poweredBy.kicker }), _jsxs(PoweredByTitle, { children: [poweredBy.labelPrefix, " ", _jsx(PoweredByAccent, { children: poweredBy.labelAccent })] })] })] })) : null;
190
- const actions = (_jsxs(Links, { children: [showPanelToggle ? (_jsxs(PanelToggleCluster, { children: [_jsx("button", { "aria-label": collapsed ? 'Show navigation' : 'Hide navigation', className: "collapse-toggle collapse-toggle--edge-top", onClick: toggle, type: "button", children: collapsed ? (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentColor", children: _jsx("path", { fillRule: "evenodd", d: "M5.23 7.21a.75.75 0 011.06.02L10 10.94l3.71-3.71a.75.75 0 111.06 1.06l-4.24 4.24a.75.75 0 01-1.06 0L5.25 8.27a.75.75 0 01-.02-1.06z", clipRule: "evenodd" }) })) : (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentColor", children: _jsx("path", { fillRule: "evenodd", d: "M5.23 12.79a.75.75 0 001.06-.02L10 9.06l3.71 3.71a.75.75 0 001.06-1.06L10.53 7.47a.75.75 0 00-1.06 0L4.23 11.75a.75.75 0 001 1.04z", clipRule: "evenodd" }) })) }), _jsx(PanelToggleText, { type: "button", onClick: toggle, "aria-label": collapsed ? 'Show panels' : 'Toggle panels', children: collapsed ? 'show panels' : 'toggle panels' })] })) : null, manifest.topbar.links.map((link) => (_jsx(LinkItem, { href: link.href, target: link.target ?? '_self', rel: link.target === '_blank' ? 'noreferrer' : undefined, children: link.label }, link.label))), showLogout ? (_jsx(LogoutButton, { type: "button", onClick: () => {
191
- fetch(logoutEndpoint, { method: 'POST' }).catch(() => {
192
- // ignore logout sync failures
193
- });
194
- logoutCookieNames.forEach(clearCookie);
195
- window.location.href = logoutHref;
196
- }, children: "logout" })) : null] }));
197
- return _jsx(TopBar, { hidden: collapsed, brand: brand, center: center, actions: actions });
155
+ export default function ManifestTopBar({ manifest, variant = 'canvas', showPoweredBy = false, showLogout = false, showPanelToggle = false, showSubtitle = false, logoutCookieNames, logoutEndpoint = '/api/auth/logout', logoutHref = '/login', }) {
156
+ const pathname = usePathname();
157
+ const router = useRouter();
158
+ const searchParams = useSearchParams();
159
+ const hydrated = useClientHydrated();
160
+ const composerData = useSurfaceComposerData();
161
+ const { auth } = useComposerAuthState();
162
+ const [searchOpen, setSearchOpen] = useState(false);
163
+ const [searchQuery, setSearchQuery] = useState('');
164
+ const searchRef = useRef(null);
165
+ const inputRef = useRef(null);
166
+ const navigation = composerData.navigation;
167
+ const introHref = navigation?.introHref || '/';
168
+ const composeHref = navigation?.composeHref || composerData.routePolicy?.homeRoute || '/compose';
169
+ const surfaceTitle = navigation?.surfaceTitle ||
170
+ composerData.title ||
171
+ manifest.surface?.label ||
172
+ manifest.topbar.productLabel ||
173
+ 'Composer';
174
+ const searchEnabled = navigation?.searchEnabled ?? true;
175
+ const brandLogo = manifest.topbar.logo;
176
+ const loginHref = hydrated
177
+ ? buildLoginHref({
178
+ pathname,
179
+ search: searchParams?.toString() ?? '',
180
+ })
181
+ : logoutHref;
182
+ const handleLogout = useSessionLogout({
183
+ logoutRoute: logoutEndpoint,
184
+ redirectPath: logoutHref,
185
+ onRedirect: (path) => router.push(path),
186
+ });
187
+ useEffect(() => {
188
+ if (!searchOpen)
189
+ return undefined;
190
+ const handleClickOutside = (event) => {
191
+ if (searchRef.current && !searchRef.current.contains(event.target)) {
192
+ setSearchOpen(false);
193
+ }
194
+ };
195
+ document.addEventListener('mousedown', handleClickOutside);
196
+ return () => document.removeEventListener('mousedown', handleClickOutside);
197
+ }, [searchOpen]);
198
+ useEffect(() => {
199
+ if (searchOpen) {
200
+ inputRef.current?.focus();
201
+ }
202
+ }, [searchOpen]);
203
+ const authControl = useMemo(() => {
204
+ if (!showLogout)
205
+ return null;
206
+ if (hydrated && auth.isAuthenticated) {
207
+ return (_jsxs(AuthButton, { type: "button", onClick: handleLogout, children: [_jsx("span", { className: "auth-default", children: formatDisplayName(auth.name || auth.user) }), _jsx("span", { className: "auth-hover", children: "logout" })] }));
208
+ }
209
+ if (pathname === logoutHref)
210
+ return null;
211
+ return _jsx(AuthLink, { href: loginHref, children: "Login" });
212
+ }, [auth.isAuthenticated, auth.name, auth.user, handleLogout, hydrated, loginHref, logoutHref, pathname, showLogout]);
213
+ const actions = (_jsxs(Actions, { "$variant": variant, children: [_jsx(NavLink, { href: introHref, "$active": isRouteActive(pathname, introHref), children: "Introduction" }), _jsx(NavLink, { href: composeHref, "$active": isRouteActive(pathname, composeHref), children: "Compose" }), authControl, searchEnabled ? (_jsx(SearchWrap, { ref: searchRef, children: _jsx(SearchControl, { "$expanded": searchOpen, onClick: !searchOpen ? () => setSearchOpen(true) : undefined, children: searchOpen ? (_jsxs(_Fragment, { children: [_jsx(SearchInput, { ref: inputRef, type: "text", placeholder: "Search...", value: searchQuery, onChange: (event) => setSearchQuery(event.target.value), onClick: (event) => event.stopPropagation(), onKeyDown: (event) => {
214
+ if (event.key === 'Escape') {
215
+ setSearchOpen(false);
216
+ setSearchQuery('');
217
+ }
218
+ } }), searchQuery ? _jsx(SearchReset, { onClick: () => setSearchQuery(''), children: "\u00D7" }) : null] })) : null }) })) : null] }));
219
+ return (_jsx(TopBar, { compact: variant === 'canvas', hidden: false, brand: _jsxs(BrandLink, { href: introHref, children: [brandLogo?.src ? _jsx(BrandLogo, { "$variant": variant, src: brandLogo.src, alt: brandLogo.alt }) : null, _jsxs("div", { children: [_jsxs(BrandTitle, { "$variant": variant, children: [_jsx(BrandTenant, { children: manifest.topbar.title }), _jsx(BrandSurface, { children: surfaceTitle })] }), showSubtitle && manifest.topbar.subtitle ? _jsx("div", { children: manifest.topbar.subtitle }) : null] })] }), center: showPoweredBy ? null : null, actions: actions }));
198
220
  }
@@ -1 +1 @@
1
- {"version":3,"file":"TopBar.d.ts","sourceRoot":"","sources":["../src/TopBar.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAsDvC,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,MAAM,CAAC,EAC7B,OAAe,EACf,MAAc,EACd,KAAK,EACL,MAAM,EACN,OAAO,EACP,SAAS,GACV,EAAE,WAAW,2CAUb"}
1
+ {"version":3,"file":"TopBar.d.ts","sourceRoot":"","sources":["../src/TopBar.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAkDvC,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,MAAM,CAAC,EAC7B,OAAe,EACf,MAAc,EACd,KAAK,EACL,MAAM,EACN,OAAO,EACP,SAAS,GACV,EAAE,WAAW,2CAUb"}
package/dist/TopBar.js CHANGED
@@ -7,7 +7,7 @@ const Root = styled.nav `
7
7
  left: 0;
8
8
  right: 0;
9
9
  z-index: 50;
10
- height: ${(props) => (props.$compact ? '50px' : '84px')};
10
+ height: ${(props) => (props.$compact ? '50px' : '72px')};
11
11
  background: var(--viewer-ui-color-bg-base, var(--brand-panel, #f6f3eb));
12
12
  border-bottom: 1px solid var(--viewer-ui-color-panel-border, var(--brand-panel-border, rgba(47, 74, 60, 0.35)));
13
13
  color: var(--viewer-ui-color-text-strong, var(--brand-text-primary, #1f2937));
@@ -17,7 +17,7 @@ const Root = styled.nav `
17
17
  const Inner = styled.div `
18
18
  height: 100%;
19
19
  display: grid;
20
- grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
20
+ grid-template-columns: minmax(0, 1fr) auto;
21
21
  align-items: center;
22
22
  gap: 16px;
23
23
  padding: 0 16px;
@@ -36,10 +36,6 @@ const CenterSlot = styled.div `
36
36
  justify-content: center;
37
37
  align-items: center;
38
38
  min-width: 0;
39
-
40
- @media (min-width: 1024px) {
41
- display: flex;
42
- }
43
39
  `;
44
40
  const ActionsSlot = styled.div `
45
41
  min-width: 0;