@tangle-network/blueprint-ui 0.1.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.
Files changed (55) hide show
  1. package/README.md +160 -0
  2. package/package.json +82 -0
  3. package/src/blueprints/registry.ts +107 -0
  4. package/src/components/forms/BlueprintJobForm.tsx +88 -0
  5. package/src/components/forms/FormField.tsx +206 -0
  6. package/src/components/forms/FormSummary.tsx +47 -0
  7. package/src/components/forms/JobExecutionDialog.tsx +203 -0
  8. package/src/components/layout/AppDocument.tsx +48 -0
  9. package/src/components/layout/AppFooter.tsx +38 -0
  10. package/src/components/layout/AppToaster.tsx +33 -0
  11. package/src/components/layout/ChainSwitcher.tsx +71 -0
  12. package/src/components/layout/ThemeToggle.tsx +18 -0
  13. package/src/components/layout/Web3Shell.tsx +28 -0
  14. package/src/components/motion/AnimatedPage.tsx +36 -0
  15. package/src/components/shared/Identicon.tsx +22 -0
  16. package/src/components/shared/TangleLogo.tsx +49 -0
  17. package/src/components/ui/badge.tsx +37 -0
  18. package/src/components/ui/button.tsx +61 -0
  19. package/src/components/ui/card.tsx +34 -0
  20. package/src/components/ui/dialog.tsx +59 -0
  21. package/src/components/ui/input.tsx +24 -0
  22. package/src/components/ui/select.tsx +80 -0
  23. package/src/components/ui/separator.tsx +25 -0
  24. package/src/components/ui/skeleton.tsx +13 -0
  25. package/src/components/ui/table.tsx +49 -0
  26. package/src/components/ui/tabs.tsx +39 -0
  27. package/src/components/ui/textarea.tsx +23 -0
  28. package/src/components/ui/toggle.tsx +35 -0
  29. package/src/components.ts +51 -0
  30. package/src/contracts/abi.ts +259 -0
  31. package/src/contracts/chains.ts +100 -0
  32. package/src/contracts/generic-encoder.ts +69 -0
  33. package/src/contracts/publicClient.ts +55 -0
  34. package/src/env.d.ts +14 -0
  35. package/src/hooks/useAuthenticatedFetch.ts +57 -0
  36. package/src/hooks/useJobForm.ts +78 -0
  37. package/src/hooks/useJobPrice.ts +283 -0
  38. package/src/hooks/useOperators.ts +141 -0
  39. package/src/hooks/useProvisionProgress.ts +125 -0
  40. package/src/hooks/useQuotes.ts +261 -0
  41. package/src/hooks/useServiceValidation.ts +113 -0
  42. package/src/hooks/useSessionAuth.ts +103 -0
  43. package/src/hooks/useSubmitJob.ts +115 -0
  44. package/src/hooks/useThemeValue.ts +6 -0
  45. package/src/index.ts +79 -0
  46. package/src/preset.ts +61 -0
  47. package/src/stores/infra.ts +43 -0
  48. package/src/stores/persistedAtom.ts +67 -0
  49. package/src/stores/session.ts +64 -0
  50. package/src/stores/theme.ts +28 -0
  51. package/src/stores/txHistory.ts +47 -0
  52. package/src/utils/resolveOperatorRpc.ts +20 -0
  53. package/src/utils/web3.ts +21 -0
  54. package/src/utils.ts +6 -0
  55. package/tsconfig.json +21 -0
package/src/preset.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Blueprint UI semantic token theme for UnoCSS.
3
+ *
4
+ * Components in this package use classes like `text-bp-elements-textPrimary`.
5
+ * Each consuming app maps these to their own CSS variables by adding this
6
+ * theme object under a `bp` key in their UnoCSS config:
7
+ *
8
+ * import { bpThemeTokens } from '@tangle-network/blueprint-ui/preset';
9
+ * // theme: { colors: { bp: bpThemeTokens('cloud') } }
10
+ * // theme: { colors: { bp: bpThemeTokens('arena') } }
11
+ */
12
+
13
+ export function bpThemeTokens(prefix: string) {
14
+ return {
15
+ elements: {
16
+ borderColor: `var(--${prefix}-elements-borderColor)`,
17
+ borderColorActive: `var(--${prefix}-elements-borderColorActive)`,
18
+ background: {
19
+ depth: {
20
+ 1: `var(--${prefix}-elements-bg-depth-1)`,
21
+ 2: `var(--${prefix}-elements-bg-depth-2)`,
22
+ 3: `var(--${prefix}-elements-bg-depth-3)`,
23
+ 4: `var(--${prefix}-elements-bg-depth-4)`,
24
+ },
25
+ },
26
+ textPrimary: `var(--${prefix}-elements-textPrimary)`,
27
+ textSecondary: `var(--${prefix}-elements-textSecondary)`,
28
+ textTertiary: `var(--${prefix}-elements-textTertiary)`,
29
+ button: {
30
+ primary: {
31
+ background: `var(--${prefix}-elements-button-primary-background)`,
32
+ backgroundHover: `var(--${prefix}-elements-button-primary-backgroundHover)`,
33
+ text: `var(--${prefix}-elements-button-primary-text)`,
34
+ },
35
+ secondary: {
36
+ background: `var(--${prefix}-elements-button-secondary-background)`,
37
+ backgroundHover: `var(--${prefix}-elements-button-secondary-backgroundHover)`,
38
+ text: `var(--${prefix}-elements-button-secondary-text)`,
39
+ },
40
+ danger: {
41
+ background: `var(--${prefix}-elements-button-danger-background)`,
42
+ backgroundHover: `var(--${prefix}-elements-button-danger-backgroundHover)`,
43
+ text: `var(--${prefix}-elements-button-danger-text)`,
44
+ },
45
+ },
46
+ icon: {
47
+ success: `var(--${prefix}-elements-icon-success)`,
48
+ error: `var(--${prefix}-elements-icon-error)`,
49
+ warning: `var(--${prefix}-elements-icon-warning)`,
50
+ primary: `var(--${prefix}-elements-icon-primary)`,
51
+ secondary: `var(--${prefix}-elements-icon-secondary)`,
52
+ },
53
+ dividerColor: `var(--${prefix}-elements-dividerColor)`,
54
+ item: {
55
+ backgroundHover: `var(--${prefix}-elements-item-backgroundHover)`,
56
+ backgroundActive: `var(--${prefix}-elements-item-backgroundActive)`,
57
+ },
58
+ focus: `var(--${prefix}-elements-focus)`,
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,43 @@
1
+ import { persistedAtom } from './persistedAtom';
2
+
3
+ const defaultBlueprintId = import.meta.env.VITE_BLUEPRINT_ID ?? '0';
4
+ const defaultServiceId = import.meta.env.VITE_SERVICE_ID ?? import.meta.env.VITE_SERVICE_IDS?.split(',')[0] ?? '0';
5
+
6
+ export interface OperatorInfo {
7
+ address: string;
8
+ rpcAddress: string;
9
+ }
10
+
11
+ export interface InfraConfig {
12
+ blueprintId: string;
13
+ serviceId: string;
14
+ /** Whether the user has validated the service on-chain */
15
+ serviceValidated: boolean;
16
+ /** Cached service info from last validation */
17
+ serviceInfo?: {
18
+ active: boolean;
19
+ operatorCount: number;
20
+ owner: string;
21
+ blueprintId: string;
22
+ permitted: boolean;
23
+ /** Operators with RPC endpoints (cached for RFQ) */
24
+ operators?: OperatorInfo[];
25
+ };
26
+ }
27
+
28
+ export const infraStore = persistedAtom<InfraConfig>({
29
+ key: 'bp_infra',
30
+ initial: {
31
+ blueprintId: defaultBlueprintId,
32
+ serviceId: defaultServiceId,
33
+ serviceValidated: false,
34
+ },
35
+ });
36
+
37
+ export function updateInfra(update: Partial<InfraConfig>) {
38
+ infraStore.set({ ...infraStore.get(), ...update });
39
+ }
40
+
41
+ export function getInfra(): InfraConfig {
42
+ return infraStore.get();
43
+ }
@@ -0,0 +1,67 @@
1
+ import { atom, type WritableAtom } from 'nanostores';
2
+
3
+ /**
4
+ * JSON serializer that handles bigint values (converts to `{__bigint: "123"}`).
5
+ * Needed because TrackedTx.blockNumber and gasUsed are bigints.
6
+ */
7
+ export function serializeWithBigInt(value: unknown): string {
8
+ return JSON.stringify(value, (_key, v) =>
9
+ typeof v === 'bigint' ? { __bigint: v.toString() } : v,
10
+ );
11
+ }
12
+
13
+ /** Deserialize JSON produced by `serializeWithBigInt`. */
14
+ export function deserializeWithBigInt<T>(raw: string): T {
15
+ return JSON.parse(raw, (_key, v) => {
16
+ if (v && typeof v === 'object' && '__bigint' in v && typeof v.__bigint === 'string') {
17
+ return BigInt(v.__bigint);
18
+ }
19
+ return v;
20
+ }) as T;
21
+ }
22
+
23
+ interface PersistedAtomOpts<T> {
24
+ /** localStorage key */
25
+ key: string;
26
+ /** Default value when nothing is stored */
27
+ initial: T;
28
+ /** Custom serializer (defaults to JSON.stringify) */
29
+ serialize?: (value: T) => string;
30
+ /** Custom deserializer (defaults to JSON.parse) */
31
+ deserialize?: (raw: string) => T;
32
+ }
33
+
34
+ /**
35
+ * A nanostores atom backed by localStorage.
36
+ * Restores on init, persists on every `.set()`.
37
+ * SSR-safe: falls back to `initial` when `window` is unavailable.
38
+ */
39
+ export function persistedAtom<T>(opts: PersistedAtomOpts<T>): WritableAtom<T> {
40
+ const { key, initial, serialize = JSON.stringify, deserialize = JSON.parse } = opts;
41
+
42
+ let restored = initial;
43
+ if (typeof window !== 'undefined') {
44
+ try {
45
+ const raw = localStorage.getItem(key);
46
+ if (raw !== null) {
47
+ restored = deserialize(raw);
48
+ }
49
+ } catch {
50
+ // corrupt data — use initial
51
+ }
52
+ }
53
+
54
+ const store = atom<T>(restored);
55
+
56
+ if (typeof window !== 'undefined') {
57
+ store.subscribe((value: T) => {
58
+ try {
59
+ localStorage.setItem(key, serialize(value));
60
+ } catch {
61
+ // storage full or unavailable — silently ignore
62
+ }
63
+ });
64
+ }
65
+
66
+ return store;
67
+ }
@@ -0,0 +1,64 @@
1
+ import { persistedAtom } from './persistedAtom';
2
+
3
+ export interface SessionEntry {
4
+ token: string;
5
+ address: string;
6
+ expiresAt: number;
7
+ sandboxId: string;
8
+ }
9
+
10
+ /**
11
+ * Persisted session tokens keyed by sandbox ID.
12
+ * Auto-cleaned on read if expired.
13
+ */
14
+ export const sessionMapStore = persistedAtom<Record<string, SessionEntry>>({
15
+ key: 'bp_sessions',
16
+ initial: {},
17
+ });
18
+
19
+ /** Active session for a given sandbox. */
20
+ export function getSession(sandboxId: string): SessionEntry | null {
21
+ const map = sessionMapStore.get();
22
+ const entry = map[sandboxId];
23
+ if (!entry) return null;
24
+
25
+ // Check expiry with 60s buffer
26
+ if (Date.now() / 1000 > entry.expiresAt - 60) {
27
+ removeSession(sandboxId);
28
+ return null;
29
+ }
30
+
31
+ return entry;
32
+ }
33
+
34
+ export function setSession(entry: SessionEntry) {
35
+ const map = { ...sessionMapStore.get() };
36
+ map[entry.sandboxId] = entry;
37
+ sessionMapStore.set(map);
38
+ }
39
+
40
+ export function removeSession(sandboxId: string) {
41
+ const map = { ...sessionMapStore.get() };
42
+ delete map[sandboxId];
43
+ sessionMapStore.set(map);
44
+ }
45
+
46
+ /** Clean up all expired sessions. */
47
+ export function gcSessions() {
48
+ const now = Date.now() / 1000;
49
+ const map = sessionMapStore.get();
50
+ const cleaned: Record<string, SessionEntry> = {};
51
+ let changed = false;
52
+
53
+ for (const [key, entry] of Object.entries(map) as [string, SessionEntry][]) {
54
+ if (entry.expiresAt > now) {
55
+ cleaned[key] = entry;
56
+ } else {
57
+ changed = true;
58
+ }
59
+ }
60
+
61
+ if (changed) {
62
+ sessionMapStore.set(cleaned);
63
+ }
64
+ }
@@ -0,0 +1,28 @@
1
+ import { atom } from 'nanostores';
2
+
3
+ export type Theme = 'dark' | 'light';
4
+
5
+ export const kTheme = 'bp_theme';
6
+ export const DEFAULT_THEME = 'dark';
7
+
8
+ export const themeStore = atom<Theme>(initStore());
9
+
10
+ export function themeIsDark() {
11
+ return themeStore.get() === 'dark';
12
+ }
13
+
14
+ function initStore() {
15
+ if (typeof window !== 'undefined') {
16
+ const persisted = localStorage.getItem(kTheme) as Theme | undefined;
17
+ const attr = document.querySelector('html')?.getAttribute('data-theme');
18
+ return persisted ?? (attr as Theme) ?? DEFAULT_THEME;
19
+ }
20
+ return DEFAULT_THEME;
21
+ }
22
+
23
+ export function toggleTheme() {
24
+ const next = themeStore.get() === 'dark' ? 'light' : 'dark';
25
+ themeStore.set(next);
26
+ localStorage.setItem(kTheme, next);
27
+ document.querySelector('html')?.setAttribute('data-theme', next);
28
+ }
@@ -0,0 +1,47 @@
1
+ import { computed } from 'nanostores';
2
+ import { persistedAtom, serializeWithBigInt, deserializeWithBigInt } from './persistedAtom';
3
+
4
+ export interface TrackedTx {
5
+ hash: `0x${string}`;
6
+ label: string;
7
+ status: 'pending' | 'confirmed' | 'failed';
8
+ timestamp: number;
9
+ chainId: number;
10
+ blockNumber?: bigint;
11
+ gasUsed?: bigint;
12
+ }
13
+
14
+ const MAX_TXS = 50;
15
+
16
+ export const txListStore = persistedAtom<TrackedTx[]>({
17
+ key: 'bp_tx_history',
18
+ initial: [],
19
+ serialize: serializeWithBigInt,
20
+ deserialize: deserializeWithBigInt,
21
+ });
22
+
23
+ export const pendingCount = computed(txListStore, (txs: TrackedTx[]) =>
24
+ txs.filter((tx: TrackedTx) => tx.status === 'pending').length,
25
+ );
26
+
27
+ export function addTx(hash: `0x${string}`, label: string, chainId: number) {
28
+ const existing = txListStore.get();
29
+ if (existing.some((tx) => tx.hash === hash)) return;
30
+ const newTx: TrackedTx = { hash, label, status: 'pending', timestamp: Date.now(), chainId };
31
+ txListStore.set([newTx, ...existing].slice(0, MAX_TXS));
32
+ }
33
+
34
+ export function updateTx(
35
+ hash: `0x${string}`,
36
+ update: Partial<Pick<TrackedTx, 'status' | 'blockNumber' | 'gasUsed'>>,
37
+ ) {
38
+ txListStore.set(
39
+ txListStore.get().map((tx: TrackedTx) =>
40
+ tx.hash === hash ? { ...tx, ...update } : tx,
41
+ ),
42
+ );
43
+ }
44
+
45
+ export function clearTxs() {
46
+ txListStore.set([]);
47
+ }
@@ -0,0 +1,20 @@
1
+ /** Rewrite operator RPC hostname for browser reachability. */
2
+ export function resolveOperatorRpc(raw: string): string {
3
+ if (typeof window === 'undefined') return raw;
4
+ const withProto = raw.includes('://') ? raw : `http://${raw}`;
5
+ try {
6
+ const url = new URL(withProto);
7
+ const pageHost = window.location.hostname;
8
+ const isNonRoutable =
9
+ url.hostname.endsWith('.local') ||
10
+ !url.hostname.includes('.') ||
11
+ url.hostname === '127.0.0.1' ||
12
+ url.hostname === 'localhost';
13
+ if (isNonRoutable && pageHost !== url.hostname) {
14
+ url.hostname = pageHost;
15
+ }
16
+ return url.toString().replace(/\/$/, '');
17
+ } catch {
18
+ return withProto;
19
+ }
20
+ }
@@ -0,0 +1,21 @@
1
+ import type { Chain } from 'viem';
2
+ import { http } from 'wagmi';
3
+ import { mainnet, rpcUrl, tangleLocal, tangleMainnet, tangleTestnet } from '../contracts/chains';
4
+
5
+ export const tangleWalletChains: readonly [Chain, ...Chain[]] = [tangleLocal, tangleTestnet, tangleMainnet, mainnet];
6
+
7
+ export function createTangleTransports() {
8
+ return {
9
+ [tangleLocal.id]: http(rpcUrl),
10
+ [tangleTestnet.id]: http('https://testnet-rpc.tangle.tools'),
11
+ [tangleMainnet.id]: http('https://rpc.tangle.tools'),
12
+ [mainnet.id]: http(),
13
+ };
14
+ }
15
+
16
+ export const defaultConnectKitOptions = {
17
+ hideBalance: false,
18
+ hideTooltips: false,
19
+ hideQuestionMarkCTA: true,
20
+ overlayBlur: 4,
21
+ } as const;
package/src/utils.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "noEmit": true,
14
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
15
+ "baseUrl": ".",
16
+ "paths": {
17
+ "~/*": ["./src/*"]
18
+ }
19
+ },
20
+ "include": ["src"]
21
+ }