@ima-jin/ui 1.0.0

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.
@@ -0,0 +1,154 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * AppShell — viewport-clamped layout primitive
5
+ *
6
+ * Clamp contract:
7
+ * - Header / Footer: shrink-0 (they stay pinned, never squish)
8
+ * - Body: flex-1 min-h-0 overflow-auto (fills remaining space, scrolls)
9
+ *
10
+ * This prevents headers (e.g. nav + search bar) from pushing content off-screen
11
+ * on small viewports. Each pane in a Split should repeat the same Header/Body/Footer
12
+ * pattern for per-pane clamping.
13
+ */
14
+
15
+ /* ------------------------------------------------------------------ */
16
+ /* AppShell root */
17
+ /* ------------------------------------------------------------------ */
18
+
19
+ export interface AppShellProps extends React.HTMLAttributes<HTMLDivElement> {
20
+ children?: React.ReactNode;
21
+ }
22
+
23
+ export const AppShell = React.forwardRef<HTMLDivElement, AppShellProps>(
24
+ function AppShell({ className = '', children, ...props }, ref) {
25
+ return (
26
+ <div
27
+ ref={ref}
28
+ className={`h-dvh flex flex-col overflow-hidden ${className}`}
29
+ {...props}
30
+ >
31
+ {children}
32
+ </div>
33
+ );
34
+ }
35
+ );
36
+
37
+ /* ------------------------------------------------------------------ */
38
+ /* Header */
39
+ /* ------------------------------------------------------------------ */
40
+
41
+ export interface AppShellHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
42
+ children?: React.ReactNode;
43
+ }
44
+
45
+ export const AppShellHeader = React.forwardRef<HTMLDivElement, AppShellHeaderProps>(
46
+ function AppShellHeader({ className = '', children, ...props }, ref) {
47
+ return (
48
+ <div ref={ref} className={`shrink-0 ${className}`} {...props}>
49
+ {children}
50
+ </div>
51
+ );
52
+ }
53
+ );
54
+
55
+ /* ------------------------------------------------------------------ */
56
+ /* Body (scrollable content area) */
57
+ /* ------------------------------------------------------------------ */
58
+
59
+ export interface AppShellBodyProps extends React.HTMLAttributes<HTMLDivElement> {
60
+ children?: React.ReactNode;
61
+ }
62
+
63
+ export const AppShellBody = React.forwardRef<HTMLDivElement, AppShellBodyProps>(
64
+ function AppShellBody({ className = '', children, ...props }, ref) {
65
+ return (
66
+ <div
67
+ ref={ref}
68
+ className={`flex-1 min-h-0 overflow-auto ${className}`}
69
+ {...props}
70
+ >
71
+ {children}
72
+ </div>
73
+ );
74
+ }
75
+ );
76
+
77
+ /* ------------------------------------------------------------------ */
78
+ /* Footer */
79
+ /* ------------------------------------------------------------------ */
80
+
81
+ export interface AppShellFooterProps extends React.HTMLAttributes<HTMLDivElement> {
82
+ children?: React.ReactNode;
83
+ }
84
+
85
+ export const AppShellFooter = React.forwardRef<HTMLDivElement, AppShellFooterProps>(
86
+ function AppShellFooter({ className = '', children, ...props }, ref) {
87
+ return (
88
+ <div ref={ref} className={`shrink-0 ${className}`} {...props}>
89
+ {children}
90
+ </div>
91
+ );
92
+ }
93
+ );
94
+
95
+ /* ------------------------------------------------------------------ */
96
+ /* Split (horizontal panes) */
97
+ /* ------------------------------------------------------------------ */
98
+
99
+ export interface AppShellSplitProps extends React.HTMLAttributes<HTMLDivElement> {
100
+ children?: React.ReactNode;
101
+ }
102
+
103
+ export const AppShellSplit = React.forwardRef<HTMLDivElement, AppShellSplitProps>(
104
+ function AppShellSplit({ className = '', children, ...props }, ref) {
105
+ return (
106
+ <div
107
+ ref={ref}
108
+ className={`flex-1 min-h-0 flex flex-row overflow-hidden ${className}`}
109
+ {...props}
110
+ >
111
+ {children}
112
+ </div>
113
+ );
114
+ }
115
+ );
116
+
117
+ /* ------------------------------------------------------------------ */
118
+ /* Split.Pane */
119
+ /* ------------------------------------------------------------------ */
120
+
121
+ export interface AppShellPaneProps extends React.HTMLAttributes<HTMLDivElement> {
122
+ width?: string | number;
123
+ children?: React.ReactNode;
124
+ }
125
+
126
+ export const AppShellPane = React.forwardRef<HTMLDivElement, AppShellPaneProps>(
127
+ function AppShellPane({ width, className = '', style, children, ...props }, ref) {
128
+ const widthStyle = width != null ? { width, ...style } : style;
129
+ return (
130
+ <div
131
+ ref={ref}
132
+ className={`flex flex-col min-h-0 overflow-hidden ${className}`}
133
+ style={widthStyle}
134
+ {...props}
135
+ >
136
+ {children}
137
+ </div>
138
+ );
139
+ }
140
+ );
141
+
142
+ AppShell.displayName = 'AppShell';
143
+ AppShellHeader.displayName = 'AppShell.Header';
144
+ AppShellBody.displayName = 'AppShell.Body';
145
+ AppShellFooter.displayName = 'AppShell.Footer';
146
+ AppShellSplit.displayName = 'AppShell.Split';
147
+ AppShellPane.displayName = 'AppShell.Split.Pane';
148
+
149
+ /* Attach subcomponents as static properties for compound API */
150
+ (AppShell as unknown as Record<string, unknown>).Header = AppShellHeader;
151
+ (AppShell as unknown as Record<string, unknown>).Body = AppShellBody;
152
+ (AppShell as unknown as Record<string, unknown>).Footer = AppShellFooter;
153
+ (AppShell as unknown as Record<string, unknown>).Split = AppShellSplit;
154
+ AppShellSplit.Pane = AppShellPane;
@@ -0,0 +1,106 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+
5
+ export interface BalanceBadgeProps {
6
+ /** DID of the user whose balance to show */
7
+ did?: string | null;
8
+ /** Pay service URL (e.g., https://pay.imajin.ai) */
9
+ payUrl: string;
10
+ /** Auth token for API requests */
11
+ authToken?: string | null;
12
+ /** Custom className for styling */
13
+ className?: string;
14
+ }
15
+
16
+ interface BalanceData {
17
+ did: string;
18
+ amount: number;
19
+ currency: string;
20
+ updatedAt: string;
21
+ }
22
+
23
+ /**
24
+ * Balance Badge Component
25
+ *
26
+ * Displays user's balance from the pay service.
27
+ * - Only shows if user is logged in (has DID and token)
28
+ * - Only shows if balance > 0
29
+ * - Fetches balance from pay service
30
+ */
31
+ export function BalanceBadge({ did, payUrl, authToken, className = '' }: BalanceBadgeProps) {
32
+ const [balance, setBalance] = useState<BalanceData | null>(null);
33
+ const [loading, setLoading] = useState(false);
34
+
35
+ useEffect(() => {
36
+ if (!did || !authToken) {
37
+ setBalance(null);
38
+ return;
39
+ }
40
+
41
+ let cancelled = false;
42
+
43
+ async function fetchBalance() {
44
+ setLoading(true);
45
+ try {
46
+ const res = await fetch(`${payUrl}/api/balance/${did}`, {
47
+ headers: {
48
+ 'Authorization': `Bearer ${authToken}`,
49
+ },
50
+ credentials: 'include',
51
+ });
52
+
53
+ if (res.ok) {
54
+ const data = await res.json();
55
+ if (!cancelled) {
56
+ setBalance(data);
57
+ }
58
+ } else {
59
+ if (!cancelled) {
60
+ setBalance(null);
61
+ }
62
+ }
63
+ } catch (error) {
64
+ console.error('Failed to fetch balance:', error);
65
+ if (!cancelled) {
66
+ setBalance(null);
67
+ }
68
+ } finally {
69
+ if (!cancelled) {
70
+ setLoading(false);
71
+ }
72
+ }
73
+ }
74
+
75
+ fetchBalance();
76
+
77
+ return () => {
78
+ cancelled = true;
79
+ };
80
+ }, [did, authToken, payUrl]);
81
+
82
+ // Don't show if not logged in
83
+ if (!did || !authToken) {
84
+ return null;
85
+ }
86
+
87
+ // Don't show while loading
88
+ if (loading) {
89
+ return null;
90
+ }
91
+
92
+ // Don't show if balance is 0 or null
93
+ if (!balance || balance.amount <= 0) {
94
+ return null;
95
+ }
96
+
97
+ return (
98
+ <div
99
+ className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-gradient-to-r from-orange-500/10 to-amber-500/10 border border-orange-500/20 ${className}`}
100
+ >
101
+ <span className="text-sm font-medium text-green-600 dark:text-green-400">
102
+ ${balance.amount.toFixed(2)}
103
+ </span>
104
+ </div>
105
+ );
106
+ }
package/src/brand.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Imajin brand constants
3
+ *
4
+ * Single source of truth for taglines, copy, and brand text.
5
+ * Import from @imajin/ui in any app.
6
+ */
7
+
8
+ export const BRAND = {
9
+ name: 'Imajin',
10
+ nameJp: '今人',
11
+ pronunciation: 'eema-gin',
12
+
13
+ /** Primary tagline — used on homepage hero */
14
+ tagline: 'The internet that pays you back',
15
+
16
+ /** Footer line — used across all services */
17
+ footer: 'Part of the Imajin sovereign network',
18
+
19
+ /** Short sovereign message — used in emails, receipts */
20
+ sovereign: 'No platform. No middleman. Yours.',
21
+
22
+ /** Links */
23
+ url: 'https://imajin.ai',
24
+ community: 'https://app.dfos.com/j/c3rff6e96e4ca9hncc43en',
25
+ github: 'https://github.com/ima-jin/imajin-ai',
26
+ } as const;
package/src/button.tsx ADDED
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+
3
+ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
4
+ variant?: 'primary' | 'secondary';
5
+ }
6
+
7
+ export function Button({ variant = 'primary', className = '', ...props }: ButtonProps) {
8
+ const base = 'px-4 py-2 rounded font-medium transition-colors';
9
+ const variants = {
10
+ primary: 'bg-orange-500 text-white hover:bg-orange-600',
11
+ secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
12
+ };
13
+ return <button className={`${base} ${variants[variant]} ${className}`} {...props} />;
14
+ }
@@ -0,0 +1,106 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+
5
+ interface Connection {
6
+ did: string;
7
+ name: string | null;
8
+ handle: string | null;
9
+ avatar: string | null;
10
+ }
11
+
12
+ export interface ConnectionPickerProps {
13
+ connectionsUrl: string;
14
+ excludeDids?: string[];
15
+ onSelect: (connection: Connection) => void;
16
+ placeholder?: string;
17
+ disabled?: boolean;
18
+ }
19
+
20
+ export function ConnectionPicker({
21
+ connectionsUrl,
22
+ excludeDids = [],
23
+ onSelect,
24
+ placeholder = 'Search connections...',
25
+ disabled = false,
26
+ }: ConnectionPickerProps) {
27
+ const [connections, setConnections] = useState<Connection[]>([]);
28
+ const [loading, setLoading] = useState(true);
29
+ const [error, setError] = useState<string | null>(null);
30
+ const [search, setSearch] = useState('');
31
+
32
+ useEffect(() => {
33
+ fetch(connectionsUrl)
34
+ .then(r => r.json())
35
+ .then(data => {
36
+ setConnections(data.connections || []);
37
+ setLoading(false);
38
+ })
39
+ .catch(() => {
40
+ setError('Failed to load connections');
41
+ setLoading(false);
42
+ });
43
+ }, [connectionsUrl]);
44
+
45
+ const excludeSet = new Set(excludeDids);
46
+ const available = connections.filter(c => !excludeSet.has(c.did));
47
+ const filtered = search
48
+ ? available.filter(c =>
49
+ (c.handle || '').toLowerCase().includes(search.toLowerCase()) ||
50
+ (c.name || '').toLowerCase().includes(search.toLowerCase())
51
+ )
52
+ : available;
53
+
54
+ return (
55
+ <div className="space-y-2">
56
+ <input
57
+ type="text"
58
+ value={search}
59
+ onChange={e => setSearch(e.target.value)}
60
+ placeholder={placeholder}
61
+ disabled={disabled || loading}
62
+ className="w-full px-3 py-2 text-sm border border-gray-600 rounded-lg bg-gray-900 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-orange-500 disabled:opacity-50"
63
+ />
64
+ {loading ? (
65
+ <p className="text-sm text-gray-500 px-1">Loading connections...</p>
66
+ ) : error ? (
67
+ <p className="text-sm text-red-400 px-1">{error}</p>
68
+ ) : filtered.length === 0 ? (
69
+ <p className="text-sm text-gray-500 px-1">
70
+ {available.length === 0 ? 'No connections available.' : 'No matching connections.'}
71
+ </p>
72
+ ) : (
73
+ <div className="space-y-0 max-h-48 overflow-y-auto rounded-lg border border-gray-700 bg-gray-900">
74
+ {filtered.map(conn => (
75
+ <button
76
+ key={conn.did}
77
+ onClick={() => { onSelect(conn); setSearch(''); }}
78
+ disabled={disabled}
79
+ className="w-full flex items-center gap-3 px-3 py-2 hover:bg-gray-800 transition text-left disabled:opacity-50"
80
+ >
81
+ {conn.avatar ? (
82
+ <img
83
+ src={conn.avatar}
84
+ alt={conn.name || conn.handle || conn.did}
85
+ className="w-8 h-8 rounded-full object-cover flex-shrink-0"
86
+ />
87
+ ) : (
88
+ <div className="w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center text-gray-400 text-sm font-semibold flex-shrink-0">
89
+ {(conn.name || conn.handle || conn.did).charAt(0).toUpperCase()}
90
+ </div>
91
+ )}
92
+ <div className="min-w-0">
93
+ <div className="text-sm font-medium text-white truncate">
94
+ {conn.name || (conn.handle ? `@${conn.handle}` : conn.did.slice(0, 20) + '...')}
95
+ </div>
96
+ {conn.handle && conn.name && (
97
+ <div className="text-xs text-gray-400 truncate">@{conn.handle}</div>
98
+ )}
99
+ </div>
100
+ </button>
101
+ ))}
102
+ </div>
103
+ )}
104
+ </div>
105
+ );
106
+ }
package/src/footer.tsx ADDED
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { BuildInfo } from './BuildInfo';
5
+
6
+ function getServiceFromPathname(pathname: string): string {
7
+ const segment = pathname.split('/').filter(Boolean)[0];
8
+ if (!segment) return 'landing';
9
+ return segment;
10
+ }
11
+
12
+ export function ImajinFooter({ className }: { className?: string }) {
13
+ const [subscribeHref, setSubscribeHref] = useState('/subscribe');
14
+
15
+ useEffect(() => {
16
+ const service = getServiceFromPathname(window.location.pathname);
17
+ setSubscribeHref(`/subscribe?from=${service}`);
18
+ }, []);
19
+
20
+ return (
21
+ <div className={`flex flex-col items-center gap-2 ${className || ""}`}>
22
+ <p className="text-center text-sm text-gray-500">
23
+ Part of the{" "}
24
+ <a href="https://imajin.ai" className="text-orange-500 hover:underline">Imajin</a>
25
+ {" "}sovereign network
26
+ </p>
27
+ <p className="text-center text-sm text-gray-500">
28
+ <a href="https://app.dfos.com/j/c3rff6e96e4ca9hncc43en" className="hover:underline">Community</a>
29
+ {" · "}
30
+ <a href="https://github.com/ima-jin/imajin-ai" className="hover:underline">GitHub</a>
31
+ {" · "}
32
+ <a href="/privacy" className="hover:underline">Privacy</a>
33
+ {" · "}
34
+ <a href={subscribeHref} className="hover:underline">Subscribe</a>
35
+ </p>
36
+ <BuildInfo />
37
+ </div>
38
+ );
39
+ }
package/src/index.ts ADDED
@@ -0,0 +1,44 @@
1
+ export { NavBar } from './nav-bar';
2
+ export type { NavBarProps, NavIdentity, ServiceUrls } from './nav-bar';
3
+ export { AppLauncher } from './app-launcher';
4
+ export type { AppLauncherProps, LauncherService } from './app-launcher';
5
+ export { Button } from './button';
6
+ export { BalanceBadge } from './balance-badge';
7
+ export type { BalanceBadgeProps } from './balance-badge';
8
+ export { ImajinFooter } from './footer';
9
+ export { BuildInfo } from './BuildInfo';
10
+ export { BRAND } from './brand';
11
+ export { MarkdownEditor } from './MarkdownEditor';
12
+ export type { MarkdownEditorProps } from './MarkdownEditor';
13
+ export { MarkdownContent } from './MarkdownContent';
14
+ export type { MarkdownContentProps } from './MarkdownContent';
15
+ export { ConnectionPicker } from './connection-picker';
16
+ export type { ConnectionPickerProps } from './connection-picker';
17
+
18
+ export { PayoutSetupBanner } from './PayoutSetupBanner';
19
+
20
+ export { ToastProvider, useToast } from './toast';
21
+ export type { ToastType } from './toast';
22
+
23
+ export { NotificationProvider, useNotifications } from './notification-provider';
24
+ export type { Notification, NotificationContextValue } from './notification-provider';
25
+ export { NotificationBell } from './notification-bell';
26
+
27
+ export { ActionSheet } from './action-sheet';
28
+ export { AppShell } from './app-shell';
29
+ export { DidShareListEditor } from './DidShareListEditor';
30
+ export { MoneyInput } from './MoneyInput';
31
+ export type {
32
+ AppShellProps,
33
+ AppShellHeaderProps,
34
+ AppShellBodyProps,
35
+ AppShellFooterProps,
36
+ AppShellSplitProps,
37
+ AppShellPaneProps,
38
+ } from './app-shell';
39
+
40
+ export { themeInitScript } from './theme-init';
41
+
42
+ export { getActingAs, setActingAs, getActingAsHeaders } from './acting-as';
43
+ export { useIdentities } from './use-identities';
44
+ export type { GroupIdentity } from './use-identities';