Compare commits

...

2 Commits

Author SHA1 Message Date
Juan Di Toro 4d0f13a197 chore: small refactor using CVA 2023-10-07 18:12:27 +02:00
Juan Di Toro 057b3c1e53 initial commit. And semi working. just commiting because everything on green makes me wanna cry 2023-10-07 17:32:11 +02:00
27 changed files with 10260 additions and 0 deletions

14
.eslintrc.cjs Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'plugin:storybook/recommended'],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

26
.storybook/main.ts Normal file
View File

@ -0,0 +1,26 @@
import type { StorybookConfig } from "@storybook/react-vite"
import { resolve } from "node:path"
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-onboarding",
"@storybook/addon-interactions"
],
framework: {
name: "@storybook/react-vite",
options: {}
},
docs: {
autodocs: "tag"
},
async viteFinal(config) {
console.log('[vitefinal]', JSON.stringify(config, null, 2))
return config
}
}
export default config

16
.storybook/preview.ts Normal file
View File

@ -0,0 +1,16 @@
import type { Preview } from "@storybook/react";
import "../styles/globals.css";
const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

View File

@ -0,0 +1,27 @@
import { Plugin } from "vite"
const reactTailwindClassnamePrefixer = ({
prefix: outerPrefix
}: {prefix: string}) => ({
name: "react-tailwind-classname-prefixer",
transform(code, id) {
if (!/\.tsx?$/.test(id)) return
const prefix = outerPrefix;
const classNameRegex = /className:\s*"([^"]*)"/g
const prefixedCode = code.replace(classNameRegex, (match, classNames) => {
const prefixedClassNames = classNames
.split(" ")
.map((className) => `${prefix}${className}`)
.join(" ")
return `className: "${prefixedClassNames}"`
})
return {
code: prefixedCode,
map: null
}
}
}) as Plugin
export default reactTailwindClassnamePrefixer

55
package.json Normal file
View File

@ -0,0 +1,55 @@
{
"name": "sdk",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"framer-motion": "^10.16.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@storybook/addon-essentials": "^7.4.6",
"@storybook/addon-interactions": "^7.4.6",
"@storybook/addon-links": "^7.4.6",
"@storybook/addon-onboarding": "^1.0.8",
"@storybook/blocks": "^7.4.6",
"@storybook/react": "^7.4.6",
"@storybook/react-vite": "^7.4.6",
"@storybook/testing-library": "^0.2.2",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.16",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"eslint-plugin-storybook": "^0.6.14",
"json": "^11.0.0",
"postcss": "^8.4.31",
"storybook": "^7.4.6",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vite-plugin-dts": "^3.6.0",
"vite-plugin-scope-tailwind": "^1.1.3",
"vite-plugin-svgr": "^4.1.0"
}
}

9153
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

6
postcss.config.cjs Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
"plugins": {
"autoprefixer": {},
"tailwindcss": {}
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 112 KiB

BIN
src/assets/lume-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,85 @@
import { Variant, AnimatePresence, m } from "framer-motion";
import React from "react";
const SwitchableComponentContext = React.createContext<SwitchableComponentContextType | undefined>(undefined);
export const SwitchableComponentProvider = ({ children }: React.PropsWithChildren) => {
const [visibleComponent, setVisibleComponent] = React.useState<SwitchableComponentType>();
return <SwitchableComponentContext.Provider
value={{ visibleComponent: visibleComponent ?? DEFAULT_COMPONENT, setVisibleComponent }}
>
{children}
</SwitchableComponentContext.Provider>
}
export function useSwitchableComponent(initialValue?: SwitchableComponentType) {
const contextValue = React.useContext(SwitchableComponentContext);
if (contextValue === undefined) {
throw new Error('useSwitchableComponent hook is being used outside of its context. Please ensure that it is wrapped within a <SwitchableComponentProvider>.');
}
React.useEffect(() => {
// Set the initial value if it's provided
if (initialValue && contextValue.visibleComponent) {
contextValue.setVisibleComponent(initialValue);
}
}, [initialValue]);
return contextValue;
}
const variants: Record<string, Variant> = {
hidden: { y: 50, opacity: 0, position: 'absolute' },
show: {
y: 0,
x: 0,
opacity: 1,
position: 'relative',
transition: {
type: "tween",
ease: 'easeInOut'
},
},
exit: { y: -50, opacity: 0, position: 'absolute' }
};
export const SwitchableComponent = ({ children, index }: React.PropsWithChildren<{ index: string }>) => {
const [width, setWidth] = React.useState<number>()
return (
<AnimatePresence>
<m.div
key={index}
initial="hidden"
animate="show"
exit="exit"
variants={variants}
className="h-full w-full"
style={{ maxWidth: width ?? 'auto' }}
onTransitionEnd={(e) => setWidth(e.currentTarget.getBoundingClientRect().width!)}
>
{children}
</m.div>
</AnimatePresence>
);
};
type SwitchableComponentType<T extends {} = {}> = {
index: string,
render: (props: T | any) => ReturnType<React.FC>
}
type SwitchableComponentContextType<T = unknown> = {
visibleComponent: SwitchableComponentType<T extends {} ? T : any>,
setVisibleComponent: React.Dispatch<React.SetStateAction<SwitchableComponentType<T extends {} ? T : any> | undefined>>
}
const DEFAULT_COMPONENT = { render: () => undefined, index: Symbol('DEFAULT_COMPONENT').toString() }
// Factory function
export function makeSwitchable<T extends {}>(Component: React.FC<T>, index: string) {
return {
render(props: T) { return <Component {...props} /> },
index: index || Symbol(Component.name).toString()
};
};

View File

@ -0,0 +1,17 @@
import { StoryFn, Meta } from '@storybook/react';
import LumeDashboard from './LumeDashboard';
import LumeProvider from '../LumeProvider';
export default {
title: 'LumeDashboard',
component: LumeDashboard,
} as Meta<typeof LumeDashboard>;
const Template: StoryFn<typeof LumeDashboard> = (args) => <LumeProvider>
<LumeDashboard {...args} />
</LumeProvider>;
export const Primary = Template.bind({});
Primary.args = {
// Add initial props here
};

View File

@ -0,0 +1,138 @@
import * as Dialog from "@radix-ui/react-dialog"
import { Chain, useLume } from "../LumeProvider"
import Logo from "../../../assets/lume-logo.png"
import { cva } from "class-variance-authority"
import { cn } from "../../utils"
const SYNCSTATE_TO_TEXT: Record<Chain["syncState"], string> = {
done: "Synced",
error: "Issue",
syncing: "Syncing"
}
const LumeDashboard = () => {
const { chains } = useLume()
const contentChains = chains.filter((c) => c.type === "content")
const blockchainChains = chains.filter((c) => c.type === "blockchain")
return (
<Dialog.Root>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed z-40 inset-0 bg-black bg-opacity-50 backdrop-blur-sm" />
<Dialog.Content className="fixed p-5 z-50 right-0 bottom-0 top-0 w-[300px] bg-neutral-950 text-white border-black border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500">
<div className="w-[calc(100%+38px)] border-b pb-3 -mx-5 px-5 border-neutral-900">
<img src={Logo} className="w-24" />
</div>
<div className="mt-4 mb-8">
<h2 className="text-xl mb-4"> Content </h2>
<div className="grid grid-cols-2">
{contentChains.map((chain, index) => (
<ChainIndicator
key={`Content_ChainIndicator_${index}`}
chain={chain}
/>
))}
</div>
</div>
<div className="mt-4 mb-8">
<h2 className="text-xl mb-4"> Blockchain </h2>
<div className="grid grid-cols-2">
{blockchainChains.map((chain, index) => (
<ChainIndicator
key={`Blockchain_ChainIndicator_${index}`}
chain={chain}
/>
))}
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}
const chainIndicatorVariant = cva("chainIndicatorVariant", {
variants: {
syncState: {
done: "text-primary",
error: "text-red-500",
syncing: "text-orange-500"
}
}
});
const ChainIndicator = ({ chain }: { chain: Chain }) => {
return (
<div key={chain.chainId} className="flex flex-row gap-x-2 items-center ">
<CircularProgress chain={chain} />
<div className="flex flex-col">
<span>{chain.name}</span>
<span
className={cn(['text-[12px] -mt-1', chainIndicatorVariant({syncState: chain.syncState})])}
>
{SYNCSTATE_TO_TEXT[chain.syncState]}
</span>
</div>
</div>
)
}
const CircularProgress = ({
chain,
className
}: {
chain: Chain
className?: string
}) => {
const progressOffset = ((100 - chain.progress) / 100) * 282.74 // These math are not mathing
const textOffset = (chain.progress / 100) * (30 - 44) + 44
return (
<svg
className={cn([className, chainIndicatorVariant({syncState: chain.syncState})])}
width="36"
height="36"
viewBox="0 0 100 100"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
style={{ transform: "rotate(-90deg)" }}
>
<circle
r="45"
cx="50"
cy="50"
fill="transparent"
stroke="#e0e0e0"
stroke-width="4px"
stroke-dasharray="282.74px"
stroke-dashoffset="0"
></circle>
<circle
r="45"
cx="50"
cy="50"
stroke="currentColor"
stroke-width="4px"
stroke-linecap="round"
stroke-dashoffset={`${progressOffset}px`}
fill="transparent"
stroke-dasharray="282.74px"
></circle>
<text
x={textOffset}
y="57.5px"
fill="currentColor"
font-size="26px"
font-weight="normal"
style={{ transform: "rotate(90deg) translate(0px, -98px)" }}
>
{chain.progress}
</text>
</svg>
)
}
export default LumeDashboard

View File

@ -0,0 +1,14 @@
import { StoryFn, Meta } from '@storybook/react';
import LumeIdentity from './LumeIdentity';
export default {
title: 'LumeIdentity',
component: LumeIdentity,
} as Meta<typeof LumeIdentity>;
const Template: StoryFn<typeof LumeIdentity> = (args) => <LumeIdentity {...args} />;
export const Primary = Template.bind({});
Primary.args = {
// Add initial props here
};

View File

@ -0,0 +1,82 @@
import React from 'react';
import LumeLogoBg from '../../../assets/lume-logo-bg.svg?react';
import { Button } from '../../../components/ui/button';
import { SwitchableComponent, SwitchableComponentProvider, useSwitchableComponent } from '../../../components/SwitchableComponent';
import ComponentList from "./components";
import { LumeIdentityContext, Session } from './LumeIdentityContext';
import { LazyMotion, domAnimation } from 'framer-motion';
import * as Dialog from '@radix-ui/react-dialog';
const LumeIdentity: React.FC = () => {
const { visibleComponent, setVisibleComponent } = useSwitchableComponent(ComponentList.SubmitButton)
const isSubmitButtonInView = [ComponentList.SubmitButton.index].includes(visibleComponent.index)
const isLoginWithAccountKey = [ComponentList.SeedPhraseInput.index].includes(visibleComponent.index)
const isCreatingAccount = [ComponentList.SetupAccountKey.index].includes(visibleComponent.index)
const isShowingSeedPhrase = [ComponentList.SeedPhraseGeneration.index].includes(visibleComponent.index)
const isFinalStep = [ComponentList.SeedPhraseGeneration.index].includes(visibleComponent.index)
const shouldShowBackButton = isCreatingAccount
const coloredOrLine = isSubmitButtonInView ? 'text-primary' : 'text-border'
return <div className="relative w-96 max-w-full inline-flex flex-col items-center justify-center gap-2.5 bg-zinc-950 px-8 py-11 transition-[height] duration-100 [&>*]:transition-all [&>*]:duration-100 overflow-hidden">
<div className="absolute left-[168px] top-[-8px] h-64 w-[280px] overflow-hidden">
<LumeLogoBg />
</div>
<div className="w-full z-10 flex flex-col items-center justify-center gap-10">
<div className="flex flex-col items-start justify-start gap-10">
<h2 className="w-full text-5xl font-normal leading-14 text-white">
{isSubmitButtonInView || isLoginWithAccountKey ? 'Sign in with Lume' : null}
{isCreatingAccount && !isShowingSeedPhrase ? 'Set up your account key' : null}
{isShowingSeedPhrase ? "Here's your account key" : null}
</h2>
</div>
<div className="flex flex-col items-start justify-start gap-2.5">
<LazyMotion features={domAnimation}>
<SwitchableComponent index={visibleComponent.index}>
<visibleComponent.render />
</SwitchableComponent>
</LazyMotion>
{!isFinalStep ? <>
<div className={`relative h-7 w-full overflow-hidden ${coloredOrLine}`}>
<svg width="409" height="28" className="max-w-full -left-1/2" viewBox="0 0 409 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M183 13H0V14H183V13ZM224 14H409V13H224V14Z" fill="currentColor" />
<path d="M199.75 19.0781C198.359 19.0781 197.299 18.6562 196.57 17.8125C195.841 16.9688 195.477 15.7344 195.477 14.1094C195.477 12.4896 195.839 11.2656 196.562 10.4375C197.292 9.60417 198.354 9.1875 199.75 9.1875C201.151 9.1875 202.216 9.60417 202.945 10.4375C203.674 11.2656 204.039 12.4896 204.039 14.1094C204.039 15.7344 203.674 16.9688 202.945 17.8125C202.216 18.6562 201.151 19.0781 199.75 19.0781ZM199.75 18.0234C200.729 18.0234 201.479 17.6901 202 17.0234C202.521 16.3516 202.781 15.3802 202.781 14.1094C202.781 12.8385 202.521 11.8776 202 11.2266C201.484 10.5703 200.734 10.2422 199.75 10.2422C198.771 10.2422 198.023 10.5703 197.508 11.2266C196.992 11.8776 196.734 12.8385 196.734 14.1094C196.734 15.3854 196.992 16.3568 197.508 17.0234C198.023 17.6901 198.771 18.0234 199.75 18.0234ZM206.742 19.0234C206.367 19.0234 206.18 18.9219 206.18 18.7188V9.69531C206.18 9.54948 206.214 9.44531 206.281 9.38281C206.349 9.3151 206.456 9.28125 206.602 9.28125H208.938C210.047 9.28125 210.88 9.47656 211.438 9.86719C212 10.2578 212.281 10.8802 212.281 11.7344C212.281 12.3125 212.141 12.7995 211.859 13.1953C211.583 13.5859 211.193 13.8802 210.688 14.0781V14.1328C210.958 14.2266 211.18 14.4089 211.352 14.6797C211.529 14.9453 211.724 15.3411 211.938 15.8672L213.023 18.6094C213.049 18.7031 213.062 18.7682 213.062 18.8047C213.062 18.9505 212.867 19.0234 212.477 19.0234H212.336C212.195 19.0234 212.073 19.0052 211.969 18.9688C211.87 18.9271 211.807 18.8724 211.781 18.8047L210.727 16.0781C210.591 15.724 210.432 15.4479 210.25 15.25C210.068 15.0521 209.852 14.9141 209.602 14.8359C209.352 14.7578 209.036 14.7188 208.656 14.7188H207.406V18.7188C207.406 18.9219 207.221 19.0234 206.852 19.0234H206.742ZM209.234 13.6641C209.562 13.6641 209.862 13.5859 210.133 13.4297C210.409 13.2734 210.625 13.0651 210.781 12.8047C210.943 12.5391 211.023 12.2578 211.023 11.9609C211.023 11.4141 210.857 11.0078 210.523 10.7422C210.19 10.4714 209.729 10.3359 209.141 10.3359H207.406V13.6641H209.234Z" fill="currentColor" />
</svg>
</div>
<Button className="h-12 w-full" variant={isSubmitButtonInView ? undefined : 'outline'} onClick={() => setVisibleComponent(shouldShowBackButton ? ComponentList.SubmitButton : ComponentList.SetupAccountKey)}>
<span className="text-center text-lg font-normal leading-normal">{shouldShowBackButton ? 'Go Back' : 'Create an Account'}</span>
</Button>
</> : null}
</div>
</div>
</div>
};
// @ditorodev: We should see how we improve this to be more like Google's SSO
// contrast is not very good, as im testing on a train with a lot of sunlight
// hitting my screen, it is almost impossible to see whats happening the outline
// buttons have no contrast
export default function Wrapped() {
const [session, setSession] = React.useState<Session>();
return <LumeIdentityContext.Provider value={{ session, setSession }}>
<Dialog.Root>
<Dialog.Trigger asChild>
<button className='bg-primary text-primary-foreground p-2 px-4 text-sm font-semibold font-mono rounded-md'>
Open Lume
</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="bg-black/90 data-[state=open]:animate-overlayShow fixed inset-0" />
<Dialog.Content className='w-full h-full flex items-center justify-center'>
<SwitchableComponentProvider>
<LumeIdentity />
</SwitchableComponentProvider>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</LumeIdentityContext.Provider>
}

View File

@ -0,0 +1,43 @@
import React from "react";
export type Session = string;
export const LumeIdentityContext = React.createContext<{
session: Session | undefined;
setSession: React.Dispatch<React.SetStateAction<Session | undefined>>;
} | undefined>(undefined);
export function useLumeIndentity() {
const contextValue = React.useContext(LumeIdentityContext);
// When the `session` changes we want to update the `session` in the local storage?
React.useEffect(() => {
if (contextValue?.session) {
localStorage.setItem('lume-session', contextValue.session);
} else {
localStorage.removeItem('lume-session');
}
}, [contextValue?.session]);
// Get the session from the local storage
React.useEffect(() => {
const session = localStorage.getItem('lume-session');
if (session) {
contextValue?.setSession(session);
}
}, []);
if (contextValue === undefined) {
throw new Error('useLumeIndentity hook is being used outside of its context. Please ensure that it is wrapped within a <LumeIdentityProvider>.');
}
return {
isSignedIn: !!contextValue.session,
signIn: (key: string) => {
console.log('signing in with key', key);
// TODO: From the key generate a session, and store it
contextValue.setSession('session');
},
signOut: () => {
contextValue.setSession(undefined);
},
};
}

View File

@ -0,0 +1,150 @@
import { makeSwitchable, useSwitchableComponent } from "../../SwitchableComponent";
import { Button } from "../../ui/button";
import { Input } from "../../ui/input";
import { CheckIcon, ClipboardCopyIcon, ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { AnimatePresence, m } from "framer-motion";
import React from "react";
import { useLumeIndentity } from "./LumeIdentityContext";
// Extracted components
const SubmitButtonComponent = () => {
const { setVisibleComponent } = useSwitchableComponent();
return (
<Button className='w-full h-12' variant={"outline"} onClick={() => setVisibleComponent(components.SeedPhraseInput)}>
<span className="text-center text-lg font-normal leading-normal">Sign in with Account Key</span>
</Button>
)
};
const SeedPhraseInputComponent = () => {
const { signIn } = useLumeIndentity();
return (
<m.form className='flex-col flex gap-y-4' onSubmit={(e) => {
e.preventDefault();
const target = e.target as typeof e.target & {
elements: {
seedPhrase: { value: string };
}
};
const seedPhrase = target.elements.seedPhrase.value;
signIn(seedPhrase)
}}>
<Input className='h-12 w-full text-lg' name="seedPhrase" />
<m.div
initial={{ y: 50 }}
animate={{ y: 0 }}
exit={{ y: -50 }}
transition={{ type: "just", delay: 0.1 }}
className="h-12"
>
<Button className='w-full h-full' role="submit">
<span className="text-center text-lg font-normal leading-normal">Sign in</span>
</Button>
</m.div>
</m.form>
);
};
const SetupAccountKeyComponent = () => {
const { setVisibleComponent } = useSwitchableComponent();
const [width, setWidth] = React.useState<number>();
return (
<m.div
initial={{ y: 50 }}
animate={{ y: 0 }}
exit={{ y: -50, height: 'auto' }}
transition={{ type: "just", delay: 0.1 }}
className="min-h-12 h-full max-w-full"
style={{ maxWidth: width ?? 'auto' }}
ref={(t) => setTimeout(() => setWidth(t!.getBoundingClientRect().width!), 0)}
>
<Button className='w-full h-full' onClick={() => setVisibleComponent(components.SeedPhraseGeneration)}>
<span className="text-center text-lg font-normal leading-normal">I get it, I'll keep it safe. Let's see the key.</span>
</Button>
</m.div>
)
};
const SeedPhraseGenerationComponent = ({ phraseLength = 12 }) => {
const [buttonClickedState, setButtonClickedState] = React.useState<"idle" | "clicked">("idle");
const [step, setStep] = React.useState<number>(0);
const { signIn } = useLumeIndentity();
const phrases = React.useMemo(() => {
// TODO: Replace with actual BIP39 or whatever is used for phrase generation
return Array(phraseLength).fill("a phrase")
}, [phraseLength]);
const key = React.useMemo(() => {
return phrases.join(" ");
}, [phrases]);
const copyPhrasesToClipboard = () => {
navigator.clipboard.writeText(phrases.join(" "));
setButtonClickedState("clicked");
setTimeout(() => setButtonClickedState("idle"), 1000);
}
return (
<div className="relative">
<AnimatePresence>
{step === 1 ? <m.div className={`z-10 absolute top-0 bottom-0 left-0 right-0 bg-black pointer-events-none`}
initial={{ opacity: 0 }}
animate={{ opacity: 0.75, top: -200, left: -20, right: -20, bottom: 120 }}
transition={{ type: "tween", duration: 0.1 }}
></m.div> : null}
</AnimatePresence>
<div className="z-20 relative mb-2.5 w-full h-full flex-wrap justify-center items-center gap-2.5 inline-flex">
{phrases.map((phrase, index) => (
<div key={`SeedPhrase_${index}`} className={`justify-center items-center gap-2.5 flex w-[calc(33%-10px)] h-10 rounded border border-current relative ring-current text-neutral-700`}>
<span className=" text-white text-md font-normal leading-normal w-full h-fit px-2.5 bg-transparent text-center">{phrase}</span>
<span className="left-[6px] top-0 absolute text-current text-xs font-normal leading-normal">{index + 1}</span>
</div>
))}
<AnimatePresence>
{step === 1 ? <m.div className="text-red-400 flex flex-row gap-5 py-8"
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 50 }}
transition={{ type: "linear", delay: 0.2, duration: 0.5 }}
>
<ExclamationTriangleIcon className="w-14 h-14" />
<span>Make sure to write this down for safe keeping.</span>
</m.div> : null}
</AnimatePresence>
<Button className={`w-full h-12 ${buttonClickedState === 'clicked' ? '!text-primary !border-primary' : ''}`} variant="outline" onClick={copyPhrasesToClipboard}>
{buttonClickedState === 'clicked' ? <CheckIcon className="w-5 h-5 mr-2.5" /> : <ClipboardCopyIcon className="w-5 h-5 mr-2.5" />}
{buttonClickedState === 'clicked' ? 'Copied!' : 'Copy Account Key'}
</Button>
</div>
{step === 0 ? (
<Button className="z-20 w-full h-12 text-white bg-neutral-700 hover:bg-neutral-800" variant="secondary" onClick={() => setStep(1)}>
Continue
</Button>
) : null}
<AnimatePresence>
{step === 1 ? <m.div className="z-20 w-full h-12"
initial={{ opacity: 0, y: -50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 50 }}
transition={{ type: "linear", delay: 2, duration: 0.5 }}
>
<Button className="w-full h-full" onClick={() => signIn(key)}>
Sign In
</Button>
</m.div> : null}
</AnimatePresence>
</div>
)
};
// Usage
const components = {
SubmitButton: makeSwitchable(SubmitButtonComponent, 'submit-button'),
SeedPhraseInput: makeSwitchable(SeedPhraseInputComponent, 'seed-phrase-input'),
SetupAccountKey: makeSwitchable(SetupAccountKeyComponent, 'setup-account-key'),
SeedPhraseGeneration: makeSwitchable(SeedPhraseGenerationComponent, 'seed-phrase-form'),
};
export default components;

View File

@ -0,0 +1,73 @@
import React, { useContext } from 'react';
type LumeSyncState = 'syncing' | 'done' | 'error'
export type Chain = {
syncState: LumeSyncState,
name: string,
chainId: string,
active: boolean,
progress: number, // in porcentage
logs: string[],
type: 'blockchain' | 'content',
peerCount?: number
}
type LumeObject = {
chains: Chain[],
activeResolver: 'local' | 'rpc'
}
type LumeContext = {
lume: LumeObject
}
const LumeContext = React.createContext<LumeContext | undefined>(undefined);
const LumeProvider = ({ children }: { children: React.ReactNode }) => {
const [lume, setLume] = React.useState<LumeObject>({
chains: [
{
name: 'Ethereum',
syncState: 'done',
chainId: '1',
active: true,
progress: 100,
logs: [],
type: 'blockchain'
},
{
name: "IPFS",
syncState: 'syncing',
chainId: '2',
active: false,
progress: 50,
logs: [],
type: 'content',
peerCount: 3
}
],
activeResolver: 'local',
});
// Here you can add the logic to update the lume state
return (
<LumeContext.Provider value={{ lume }}>
{children}
</LumeContext.Provider>
);
};
export default LumeProvider;
export function useLume() {
const ctx = useContext(LumeContext);
if (!ctx) {
throw new Error('useLume must be used within a LumeProvider');
}
const { lume } = ctx;
return lume
}

View File

@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../../components/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"text-neutral-600 border border-current bg-transparent shadow-sm hover:border-white hover:text-white",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "../../components/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> { }
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full text-white rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 focus:border-white",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

6
src/components/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

2
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />

77
styles/globals.css Normal file
View File

@ -0,0 +1,77 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 113 49% 55%;
--primary-foreground: black;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 0 0% 32%;
--muted-foreground: 0 0% 32%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 0 0% 32%;
--input: 0 0% 32%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

74
tailwind.config.ts Normal file
View File

@ -0,0 +1,74 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
// prefix: 'lume-sdk-',
content: [
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [import("tailwindcss-animate")],
}

39
tsconfig.json Normal file
View File

@ -0,0 +1,39 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
// "paths": {
// "@/*": ["src/*"],
// "@/styles/*": ["styles/*"],
// },
"typeRoots": ["src/vite-env.d.ts"]
},
"include": [
"src"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

13
tsconfig.node.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": [
"vite.config.ts",
"lib"
]
}

20
vite.config.ts Normal file
View File

@ -0,0 +1,20 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import scopeTailwind from "vite-plugin-scope-tailwind";
import { resolve } from 'path'
// import reactTailwindClassnamePrefixer from "./lib/vite-plugin-react-classname-prefixer";
import svgr from "vite-plugin-svgr";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svgr(), react(), scopeTailwind({react: true})],
resolve: {
// I have no clue why aliases are not working at all...
alias: {
'@styles/': resolve(__dirname, './styles'),
'@components/': resolve(__dirname, './src/components'),
'@assets/': resolve(__dirname, './src/assets'),
'@': resolve(__dirname, './src'),
},
},
})