@semiont/react-ui 0.4.12 → 0.4.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semiont/react-ui",
3
- "version": "0.4.12",
3
+ "version": "0.4.14",
4
4
  "description": "React components and hooks for Semiont",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.mts",
@@ -77,19 +77,6 @@ export function Toolbar<T extends string = string>({
77
77
  <span className="semiont-toolbar-icon" aria-hidden="true">📒</span>
78
78
  </button>
79
79
 
80
- {/* Collaboration Icon */}
81
- <button
82
- onClick={() => handlePanelToggle('collaboration')}
83
- className="semiont-toolbar-button"
84
- data-active={activePanel === 'collaboration'}
85
- data-panel="collaboration"
86
- aria-label={t('collaboration')}
87
- aria-pressed={activePanel === 'collaboration'}
88
- title={t('collaboration')}
89
- >
90
- <span className="semiont-toolbar-icon" aria-hidden="true">👥</span>
91
- </button>
92
-
93
80
  {/* JSON-LD Icon */}
94
81
  <button
95
82
  onClick={() => handlePanelToggle('jsonld')}
@@ -102,6 +89,19 @@ export function Toolbar<T extends string = string>({
102
89
  >
103
90
  <span className="semiont-toolbar-icon" aria-hidden="true">🌐</span>
104
91
  </button>
92
+
93
+ {/* Collaboration Icon */}
94
+ <button
95
+ onClick={() => handlePanelToggle('collaboration')}
96
+ className="semiont-toolbar-button"
97
+ data-active={activePanel === 'collaboration'}
98
+ data-panel="collaboration"
99
+ aria-label={t('collaboration')}
100
+ aria-pressed={activePanel === 'collaboration'}
101
+ title={t('collaboration')}
102
+ >
103
+ <span className="semiont-toolbar-icon" aria-hidden="true">👥</span>
104
+ </button>
105
105
  </>
106
106
  )}
107
107
 
@@ -12,8 +12,9 @@ interface FooterProps {
12
12
  t: TranslateFn;
13
13
  CookiePreferences?: React.ComponentType<{ isOpen: boolean; onClose: () => void }>;
14
14
  onOpenKeyboardHelp?: () => void;
15
- apiDocsUrl?: string;
16
15
  sourceCodeUrl?: string;
16
+ /** Show About, Privacy Policy, Terms of Service, Cookie Preferences links. False for desktop apps. */
17
+ showPolicyLinks?: boolean;
17
18
  }
18
19
 
19
20
  export function Footer({
@@ -22,8 +23,8 @@ export function Footer({
22
23
  t,
23
24
  CookiePreferences,
24
25
  onOpenKeyboardHelp,
25
- apiDocsUrl = '/api/docs',
26
- sourceCodeUrl = 'https://github.com/The-AI-Alliance/semiont'
26
+ sourceCodeUrl = 'https://github.com/The-AI-Alliance/semiont',
27
+ showPolicyLinks = true,
27
28
  }: FooterProps) {
28
29
  const [showCookiePreferences, setShowCookiePreferences] = useState(false);
29
30
 
@@ -37,25 +38,35 @@ export function Footer({
37
38
  </div>
38
39
 
39
40
  <div className="semiont-footer__links">
40
- <Link
41
- href={routes.about?.() || '/about'}
42
- className="semiont-footer__link"
43
- >
44
- {t('about')}
45
- </Link>
46
- <Link
47
- href={routes.privacy?.() || '/privacy'}
48
- className="semiont-footer__link"
49
- >
50
- {t('privacyPolicy')}
51
- </Link>
52
- {CookiePreferences && (
53
- <button
54
- onClick={() => setShowCookiePreferences(true)}
55
- className="semiont-footer__link"
56
- >
57
- {t('cookiePreferences')}
58
- </button>
41
+ {showPolicyLinks && (
42
+ <>
43
+ <Link
44
+ href={routes.about?.() || '/about'}
45
+ className="semiont-footer__link"
46
+ >
47
+ {t('about')}
48
+ </Link>
49
+ <Link
50
+ href={routes.privacy?.() || '/privacy'}
51
+ className="semiont-footer__link"
52
+ >
53
+ {t('privacyPolicy')}
54
+ </Link>
55
+ {CookiePreferences && (
56
+ <button
57
+ onClick={() => setShowCookiePreferences(true)}
58
+ className="semiont-footer__link"
59
+ >
60
+ {t('cookiePreferences')}
61
+ </button>
62
+ )}
63
+ <Link
64
+ href={routes.terms?.() || '/terms'}
65
+ className="semiont-footer__link"
66
+ >
67
+ {t('termsOfService')}
68
+ </Link>
69
+ </>
59
70
  )}
60
71
  {onOpenKeyboardHelp && (
61
72
  <button
@@ -68,20 +79,6 @@ export function Footer({
68
79
  </kbd>
69
80
  </button>
70
81
  )}
71
- <Link
72
- href={routes.terms?.() || '/terms'}
73
- className="semiont-footer__link"
74
- >
75
- {t('termsOfService')}
76
- </Link>
77
- <a
78
- href={apiDocsUrl}
79
- target="_blank"
80
- rel="noopener noreferrer"
81
- className="semiont-footer__link"
82
- >
83
- {t('apiDocs')}
84
- </a>
85
82
  <a
86
83
  href={sourceCodeUrl}
87
84
  target="_blank"
@@ -95,7 +92,7 @@ export function Footer({
95
92
  </div>
96
93
  </footer>
97
94
 
98
- {CookiePreferences && (
95
+ {showPolicyLinks && CookiePreferences && (
99
96
  <CookiePreferences
100
97
  isOpen={showCookiePreferences}
101
98
  onClose={() => setShowCookiePreferences(false)}
@@ -93,22 +93,6 @@ describe('Footer Component', () => {
93
93
  expect(termsLink).toHaveAttribute('href', '/terms');
94
94
  });
95
95
 
96
- it('should render API Docs link with default URL', () => {
97
- render(
98
- <Footer
99
- Link={MockLink}
100
- routes={mockRoutes}
101
- t={mockT}
102
- />
103
- );
104
-
105
- const apiDocsLink = screen.getByText('footer.apiDocs');
106
- expect(apiDocsLink).toBeInTheDocument();
107
- expect(apiDocsLink).toHaveAttribute('href', '/api/docs');
108
- expect(apiDocsLink).toHaveAttribute('target', '_blank');
109
- expect(apiDocsLink).toHaveAttribute('rel', 'noopener noreferrer');
110
- });
111
-
112
96
  it('should render Source Code link with default URL', () => {
113
97
  render(
114
98
  <Footer
@@ -272,20 +256,6 @@ describe('Footer Component', () => {
272
256
  });
273
257
 
274
258
  describe('Custom URLs', () => {
275
- it('should use custom API Docs URL', () => {
276
- render(
277
- <Footer
278
- Link={MockLink}
279
- routes={mockRoutes}
280
- t={mockT}
281
- apiDocsUrl="/custom/api/docs"
282
- />
283
- );
284
-
285
- const apiDocsLink = screen.getByText('footer.apiDocs');
286
- expect(apiDocsLink).toHaveAttribute('href', '/custom/api/docs');
287
- });
288
-
289
259
  it('should use custom Source Code URL', () => {
290
260
  render(
291
261
  <Footer
@@ -411,9 +381,6 @@ describe('Footer Component', () => {
411
381
  />
412
382
  );
413
383
 
414
- const apiDocsLink = screen.getByText('footer.apiDocs');
415
- expect(apiDocsLink).toHaveAttribute('rel', 'noopener noreferrer');
416
-
417
384
  const sourceLink = screen.getByText('footer.sourceCode');
418
385
  expect(sourceLink).toHaveAttribute('rel', 'noopener noreferrer');
419
386
  });
@@ -7,12 +7,14 @@ interface Props {
7
7
  isConnected: boolean;
8
8
  eventCount: number;
9
9
  lastEventTimestamp?: string;
10
+ knowledgeBaseName?: string;
10
11
  }
11
12
 
12
13
  export function CollaborationPanel({
13
14
  isConnected,
14
15
  eventCount,
15
- lastEventTimestamp
16
+ lastEventTimestamp,
17
+ knowledgeBaseName
16
18
  }: Props) {
17
19
  const t = useTranslations('CollaborationPanel');
18
20
 
@@ -52,6 +54,12 @@ export function CollaborationPanel({
52
54
  {t('title')}
53
55
  </h3>
54
56
 
57
+ {knowledgeBaseName && (
58
+ <div style={{ padding: '0 0.75rem 0.5rem', fontSize: '0.8rem', color: 'var(--semiont-color-neutral-400)' }}>
59
+ {knowledgeBaseName}
60
+ </div>
61
+ )}
62
+
55
63
  {/* Connection Status Section */}
56
64
  <div className="semiont-collaboration-panel__section">
57
65
  <h3 className="semiont-collaboration-panel__heading">
@@ -30,7 +30,7 @@ const mockTranslations = {
30
30
  signInPrompt: 'Sign in to your knowledge workspace',
31
31
  continueWithGoogle: 'Continue with Google',
32
32
  backendUrlLabel: 'Backend URL',
33
- backendUrlPlaceholder: 'https://your-semiont-server.com',
33
+ backendUrlPlaceholder: 'http://localhost:4000',
34
34
  emailLabel: 'Email',
35
35
  emailPlaceholder: 'your@email.com',
36
36
  passwordLabel: 'Password',
@@ -215,26 +215,6 @@ describe('SignInForm - Accessibility', () => {
215
215
  expect(button).not.toHaveAttribute('disabled');
216
216
  });
217
217
 
218
- it('should have keyboard-accessible navigation links', () => {
219
- const onGoogleSignIn = vi.fn();
220
-
221
- render(
222
- <SignInForm
223
- onGoogleSignIn={onGoogleSignIn}
224
- Link={MockLink}
225
- translations={mockTranslations}
226
- />
227
- );
228
-
229
- const homeLink = screen.getByRole('link', { name: 'Back to Home' });
230
- const learnMoreLink = screen.getByRole('link', { name: 'Learn More' });
231
- const signUpLink = screen.getByRole('link', { name: 'Sign Up Instead' });
232
-
233
- expect(homeLink).toHaveAttribute('href', '/');
234
- expect(learnMoreLink).toHaveAttribute('href', '/about');
235
- expect(signUpLink).toHaveAttribute('href', '/auth/signup');
236
- });
237
-
238
218
  it('should have keyboard-accessible credentials submit button', () => {
239
219
  const onGoogleSignIn = vi.fn();
240
220
  const onCredentialsSignIn = vi.fn();
@@ -273,22 +253,21 @@ describe('SignInForm - Accessibility', () => {
273
253
  expect(button).toHaveAccessibleName(/Continue with Google/i);
274
254
  });
275
255
 
276
- it('should have accessible link names', () => {
256
+ it('should have accessible button names', () => {
277
257
  const onGoogleSignIn = vi.fn();
278
258
 
279
259
  render(
280
260
  <SignInForm
281
261
  onGoogleSignIn={onGoogleSignIn}
262
+ backendUrl="http://localhost:4000"
282
263
  Link={MockLink}
283
264
  translations={mockTranslations}
284
265
  />
285
266
  );
286
267
 
287
- const links = screen.getAllByRole('link');
288
-
289
- // All links should have accessible names
290
- links.forEach(link => {
291
- expect(link).toHaveAccessibleName();
268
+ const buttons = screen.getAllByRole('button');
269
+ buttons.forEach(button => {
270
+ expect(button).toHaveAccessibleName();
292
271
  });
293
272
  });
294
273
  });
@@ -410,23 +389,23 @@ describe('SignInForm - Accessibility', () => {
410
389
  expect(button?.className).toBeTruthy();
411
390
  });
412
391
 
413
- it('should have visible focus indicators on links', () => {
392
+ it('should have visible focus indicators on buttons', () => {
414
393
  const onGoogleSignIn = vi.fn();
415
394
 
416
395
  const { container } = render(
417
396
  <SignInForm
418
397
  onGoogleSignIn={onGoogleSignIn}
398
+ backendUrl="http://localhost:4000"
419
399
  Link={MockLink}
420
400
  translations={mockTranslations}
421
401
  />
422
402
  );
423
403
 
424
- const links = container.querySelectorAll('a');
404
+ const buttons = container.querySelectorAll('button');
425
405
 
426
- // At least one link should exist and have focus styling
427
- expect(links.length).toBeGreaterThan(0);
428
- links.forEach(link => {
429
- expect(link.className).toBeTruthy();
406
+ expect(buttons.length).toBeGreaterThan(0);
407
+ buttons.forEach(button => {
408
+ expect(button.className).toBeTruthy();
430
409
  });
431
410
  });
432
411
  });
@@ -305,7 +305,6 @@ export function SignInForm({
305
305
  error,
306
306
  showCredentialsAuth = false,
307
307
  isLoading = false,
308
- Link,
309
308
  translations: t,
310
309
  }: SignInFormProps) {
311
310
  const handleGoogleClick = () => {
@@ -374,18 +373,6 @@ export function SignInForm({
374
373
  )}
375
374
  </div>
376
375
 
377
- {/* Navigation Links */}
378
- <div className="semiont-auth__links">
379
- <Link href="/" className={buttonStyles.secondary.base}>
380
- {t.backToHome}
381
- </Link>
382
- <Link href="/about" className={buttonStyles.secondary.base}>
383
- {t.learnMore}
384
- </Link>
385
- <Link href="/auth/signup" className={buttonStyles.primary.base}>
386
- {t.signUpInstead}
387
- </Link>
388
- </div>
389
376
  </div>
390
377
  </div>
391
378
  </main>
@@ -90,6 +90,11 @@ export interface ResourceViewerPageProps {
90
90
  * SSE attention stream connection status for the active workspace
91
91
  */
92
92
  streamStatus: StreamStatus;
93
+
94
+ /**
95
+ * Name of the active knowledge base (for display in panels)
96
+ */
97
+ knowledgeBaseName?: string | undefined;
93
98
  }
94
99
 
95
100
  /**
@@ -128,6 +133,7 @@ export function ResourceViewerPage({
128
133
  ToolbarPanels,
129
134
  refetchDocument,
130
135
  streamStatus,
136
+ knowledgeBaseName,
131
137
  }: ResourceViewerPageProps) {
132
138
  // Translations
133
139
  const tw = useTranslations('ReferenceWizard');
@@ -628,6 +634,7 @@ export function ResourceViewerPage({
628
634
  <CollaborationPanel
629
635
  isConnected={streamStatus === 'connected'}
630
636
  eventCount={0}
637
+ knowledgeBaseName={knowledgeBaseName}
631
638
  />
632
639
  )}
633
640
 
@@ -401,4 +401,16 @@
401
401
 
402
402
  .semiont-panel-actions--between {
403
403
  justify-content: space-between;
404
+ }
405
+
406
+ /* Login form pulsing border for first-launch nudge */
407
+ @keyframes semiont-pulse-border {
408
+ 0%, 100% { border-color: var(--semiont-color-primary-300, #93c5fd); }
409
+ 50% { border-color: var(--semiont-color-primary-600, #2563eb); }
410
+ }
411
+
412
+ .semiont-panel__login-form--pulsing {
413
+ border: 2px solid var(--semiont-color-primary-400, #60a5fa);
414
+ border-radius: var(--semiont-panel-border-radius, 0.5rem);
415
+ animation: semiont-pulse-border 2s ease-in-out infinite;
404
416
  }