diff --git a/.gitignore b/.gitignore index cb71482..7b28197 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store dist node_modules *storybook.log diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 5a9c706..2f1aee1 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -30,8 +30,8 @@ const preview: Preview = { decorators: [ withThemeByClassName({ themes: { - light: "", - dark: "ink-dark", + light: "ink-light-theme", + dark: "ink-dark-theme", }, defaultTheme: "light", }), diff --git a/src/components/Button/Button.stories.tsx b/src/components/Button/Button.stories.tsx index 0785fa5..a8e1b1f 100644 --- a/src/components/Button/Button.stories.tsx +++ b/src/components/Button/Button.stories.tsx @@ -62,7 +62,7 @@ export const WithMinimumWidth: Story = { export const AsLink: Story = { args: { as: "a", - href: "https://inkonchain.com", + href: "/test", target: "_blank", children: "inkonchain.com", iconRight: , diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 2124712..6752663 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,4 +1,4 @@ -import React, { type ElementType } from "react"; +import React, { PropsWithChildren, type ElementType } from "react"; import { classNames, resetClasses, @@ -11,7 +11,8 @@ const DEFAULT_BUTTON_TAG = "button" as const; export type ButtonProps = PolymorphicProps & OwnButtonProps; -export interface OwnButtonProps { +export interface OwnButtonProps extends PropsWithChildren { + className?: string; variant?: "primary" | "secondary"; size?: "sm" | "md"; rounded?: "full" | "default"; @@ -21,6 +22,7 @@ export interface OwnButtonProps { export const Button = ({ as, + asProps, className, children, variant = "primary", @@ -57,6 +59,7 @@ export const Button = ({ }), className )} + {...asProps} {...restProps} > {iconLeft && ( @@ -84,7 +87,8 @@ export const Button = ({ ) : (
- extends DisplayOnProps { +export interface SegmentedControlProps { options: SegmentedControlOption[]; onOptionChange: (option: SegmentedControlOption, index: number) => void; + variant?: "purple" | "transparent"; } export interface SegmentedControlOption { @@ -21,7 +20,7 @@ export interface SegmentedControlOption { export const SegmentedControl = ({ options, onOptionChange, - displayOn = "auto", + variant = "transparent", }: SegmentedControlProps) => { const itemsRef = useRef>([]); const [selectedOption, setSelectedOption] = useState( @@ -65,10 +64,9 @@ export const SegmentedControl = ({
@@ -77,10 +75,9 @@ export const SegmentedControl = ({
diff --git a/src/components/Wallet/ConnectWallet.tsx b/src/components/Wallet/ConnectWallet.tsx index 1e9eca2..11c6ff5 100644 --- a/src/components/Wallet/ConnectWallet.tsx +++ b/src/components/Wallet/ConnectWallet.tsx @@ -82,7 +82,7 @@ const ConnectedWalletSection = ({ address }: { address: Address }) => {
Balance
-
+
{isSuccess ? `${balance.value} ${balance.symbol}` : "..."}
diff --git a/src/components/polymorphic.ts b/src/components/polymorphic.ts index 668a633..4725f75 100644 --- a/src/components/polymorphic.ts +++ b/src/components/polymorphic.ts @@ -1,13 +1,9 @@ -import { - ComponentPropsWithoutRef, - ElementType, - PropsWithChildren, -} from "react"; +import { ComponentPropsWithoutRef, ElementType } from "react"; -interface PolymorphicAsProp { +export type PolymorphicDefinition = { as?: E; -} + asProps?: ComponentPropsWithoutRef; +}; -export type PolymorphicProps = PropsWithChildren< - ComponentPropsWithoutRef & PolymorphicAsProp ->; +export type PolymorphicProps = + ComponentPropsWithoutRef & PolymorphicDefinition; diff --git a/src/hooks/useInkThemeClass.ts b/src/hooks/useInkThemeClass.ts index a64f5b2..61ea4c9 100644 --- a/src/hooks/useInkThemeClass.ts +++ b/src/hooks/useInkThemeClass.ts @@ -1,8 +1,14 @@ import { useEffect } from "react"; -const themeClasses = ["ink-dark", "ink-light"]; +const themeClasses = [ + "ink-dark-theme", + "ink-light-theme", + "ink-contrast-theme", +] as const; -export function useInkThemeClass(theme: "ink-dark" | "ink-light") { +export function useInkThemeClass( + theme: "default" | (typeof themeClasses)[number] +) { useEffect(() => { themeClasses.forEach((t) => { if (theme === t) { diff --git a/src/icons/Type=DefaultAppIcon.svg b/src/icons/Type=DefaultAppIcon.svg new file mode 100644 index 0000000..514bd40 --- /dev/null +++ b/src/icons/Type=DefaultAppIcon.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/icons/index.ts b/src/icons/index.ts index 6ecc3ea..1176ea2 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -6,6 +6,7 @@ export { default as Arrow } from "./Type=Arrow.svg?react"; export { default as Chevron } from "./Type=Chevron.svg?react"; export { default as Close } from "./Type=Close.svg?react"; export { default as Copy } from "./Type=Copy.svg?react"; +export { default as DefaultAppIcon } from "./Type=DefaultAppIcon.svg?react"; export { default as Deposit } from "./Type=Deposit.svg?react"; export { default as Disconnect } from "./Type=Disconnect.svg?react"; export { default as Error } from "./Type=Error.svg?react"; diff --git a/src/images/app-icon.png b/src/images/app-icon.png new file mode 100644 index 0000000..5883384 Binary files /dev/null and b/src/images/app-icon.png differ diff --git a/src/index.ts b/src/index.ts index e402df4..a521ede 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import "./tailwind.css"; export * from "./components"; +export * from "./layout"; export * from "./hooks"; export * as InkIcon from "./icons"; diff --git a/src/layout/InkLayout/InkLayout.stories.tsx b/src/layout/InkLayout/InkLayout.stories.tsx new file mode 100644 index 0000000..9714441 --- /dev/null +++ b/src/layout/InkLayout/InkLayout.stories.tsx @@ -0,0 +1,90 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { InkLayout, InkLayoutProps } from "./InkLayout"; +import { Button, InkIcon, SegmentedControl } from "../.."; +import { InkLayoutSideNav } from "./InkLayoutSideNav"; + +const SideNav = () => { + return ( + , + }, + { + label: "Settings", + href: "/settings", + icon: , + }, + ]} + /> + ); +}; + +const TopNav = () => { + return ( + {}} + /> + ); +}; + +const meta: Meta = { + title: "Layouts/InkLayout", + component: InkLayout, + parameters: { + layout: "fullscreen", + }, + tags: ["autodocs"], + args: { + children:
Some content
, + headerContent:
Header content
, + topNavigation: , + sideNavigation: , + }, +}; + +export default meta; +type Story = StoryObj; + +export const Simple: Story = { + args: {}, +}; + +// Serves as a fun example of how to use `linkAs` to customize the underlying component of `InkNavLink`. +// It is necessary to allow users to pass `Link` +export const SideNavWithCustomButtons: Story = { + args: { + sideNavigation: ( + , + }, + { + label: "Settings", + href: "/settings", + icon: , + }, + ]} + /> + ), + children: ( +
+ The side nav can be a custom component for routing, for instance, if you + want to use NextJS' own {``} component. +
+ ), + }, +}; diff --git a/src/layout/InkLayout/InkLayout.tsx b/src/layout/InkLayout/InkLayout.tsx new file mode 100644 index 0000000..47e4c06 --- /dev/null +++ b/src/layout/InkLayout/InkLayout.tsx @@ -0,0 +1,47 @@ +import { PropsWithChildren } from "react"; +import { DefaultAppIcon } from "../../icons"; +import { classNames, resetClasses } from "../../util/classes"; + +export interface InkLayoutProps extends PropsWithChildren { + mainIcon?: React.ReactNode; + headerContent?: React.ReactNode; + sideNavigation?: React.ReactNode; + topNavigation?: React.ReactNode; +} + +export const InkLayout: React.FC = ({ + mainIcon = , + headerContent, + sideNavigation, + topNavigation, + children, +}) => { + return ( +
+
+
+ {mainIcon} +
+ {topNavigation &&
{topNavigation}
} + {headerContent ? ( +
{headerContent}
+ ) : null} +
+
+ {sideNavigation && ( +
+ {sideNavigation} +
+ )} +
+ {children} +
+
+
+ ); +}; diff --git a/src/layout/InkLayout/InkLayoutSideNav.tsx b/src/layout/InkLayout/InkLayoutSideNav.tsx new file mode 100644 index 0000000..2d1141b --- /dev/null +++ b/src/layout/InkLayout/InkLayoutSideNav.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { InkLayoutLink, InkNavLink } from "./InkNavLink"; +import { + PolymorphicDefinition, + PolymorphicProps, +} from "../../components/polymorphic"; +import { InkIcon } from "../.."; + +interface InkLayoutSideNavProps { + links: InkLayoutLink[]; + linkAs?: PolymorphicDefinition; +} + +export const InkLayoutSideNav = ({ + links, + linkAs, +}: InkLayoutSideNavProps) => { + const { as, asProps, ...rest } = linkAs ?? {}; + return ( + + ); +}; + +} />; diff --git a/src/layout/InkLayout/InkNavLink.tsx b/src/layout/InkLayout/InkNavLink.tsx new file mode 100644 index 0000000..83500dc --- /dev/null +++ b/src/layout/InkLayout/InkNavLink.tsx @@ -0,0 +1,49 @@ +import React, { ElementType } from "react"; +import { PolymorphicProps } from "../../components/polymorphic"; +import { classNames, resetClasses } from "../../util/classes"; + +const DEFAULT_COMPONENT_TYPE = "a" as const; + +export interface InkLayoutLink { + label: string; + href: string; + icon: React.ReactNode; +} + +export type InkNavLinkProps< + T extends ElementType = typeof DEFAULT_COMPONENT_TYPE, +> = PolymorphicProps & + InkLayoutLink & { + className?: string; + }; + +export const InkNavLink = < + T extends ElementType = typeof DEFAULT_COMPONENT_TYPE, +>({ + as, + asProps, + href, + icon, + label, + className = "", + ...props +}: InkNavLinkProps) => { + const Component = as ?? DEFAULT_COMPONENT_TYPE; + + return ( + +
{icon}
+
{label}
+
+ ); +}; diff --git a/src/layout/InkLayout/index.ts b/src/layout/InkLayout/index.ts new file mode 100644 index 0000000..cc52910 --- /dev/null +++ b/src/layout/InkLayout/index.ts @@ -0,0 +1,2 @@ +export * from "./InkLayout"; +export * from "./InkLayoutSideNav"; diff --git a/src/layout/index.ts b/src/layout/index.ts new file mode 100644 index 0000000..5fb61ac --- /dev/null +++ b/src/layout/index.ts @@ -0,0 +1 @@ +export * from "./InkLayout"; diff --git a/src/styles/resets.css b/src/styles/resets.css index 516dd61..73abfb4 100644 --- a/src/styles/resets.css +++ b/src/styles/resets.css @@ -18,6 +18,13 @@ /* border-color: theme('borderColor.DEFAULT', currentColor); 2 */ } +/* This one is copied over from above, needed to apply to a lot of child elements */ +.ink-preflight *, +.ink-preflight *::before, +.ink-preflight *::after { + box-sizing: border-box; /* 1 */ +} + .ink-preflight::before, .ink-preflight::after { --tw-content: ""; diff --git a/src/styles/shadow.css b/src/styles/shadow.css index 4939815..7bb6f9c 100644 --- a/src/styles/shadow.css +++ b/src/styles/shadow.css @@ -1,4 +1,5 @@ :root { --ink-box-shadow-menu: 0px 8px 24px -8px #160f1f1a; --ink-box-shadow-modal: 0px 16px 64px -32px #160f1f1a; + --ink-box-shadow-layout: 0px 16px 64px -32px #160f1f0d; } diff --git a/src/styles/theme/colors.dark.css b/src/styles/theme/colors.dark.css new file mode 100644 index 0000000..ed06259 --- /dev/null +++ b/src/styles/theme/colors.dark.css @@ -0,0 +1,186 @@ +:root, +:root.ink-dark-theme { + /* Background */ + --ink-background-dark: rgba(22, 15, 31, 1); + --ink-background-dark-transparent: color-mix( + in srgb, + var(--ink-background-dark), + transparent 20% + ); + --ink-background-light: rgb(33, 26, 41); + --ink-background-light-transparent: color-mix( + in srgb, + var(--ink-background-light), + transparent 50% + ); + --ink-background-light-invisible: color-mix( + in srgb, + var(--ink-background-light), + transparent 100% + ); + --ink-background-container: color-mix( + in srgb, + rgba(255, 255, 255, 0.059), + transparent 94% + ); + + /* Button */ + --ink-button-primary: rgb(113, 50, 245); + --ink-button-primary-hover: color-mix( + in srgb, + var(--ink-button-primary), + transparent 10% + ); + --ink-button-primary-pressed: color-mix( + in srgb, + var(--ink-button-primary), + transparent 20% + ); + --ink-button-secondary: rgb(255, 255, 255); + --ink-button-secondary-hover: color-mix( + in srgb, + var(--ink-button-secondary), + transparent 50% + ); + --ink-button-secondary-pressed: color-mix( + in srgb, + var(--ink-button-secondary-hover), + transparent 90% + ); + + /* Text */ + --ink-text-default: rgb(255, 255, 255); + --ink-text-muted: color-mix( + in srgb, + var(--ink-text-default), + transparent 50% + ); + --ink-text-on-primary: rgb(255, 255, 255); + --ink-text-on-primary-disabled: color-mix( + in srgb, + var(--ink-text-on-primary), + transparent 60% + ); + --ink-text-on-secondary: rgb(138, 97, 255); + --ink-text-on-secondary-disabled: color-mix( + in srgb, + var(--ink-text-on-secondary), + transparent 50% + ); + + /* Status */ + --ink-status-success: rgb(61, 166, 103) --ink-status-success-bg: + color-mix(in srgb, var(--ink-status-success), transparent 92%); + --ink-status-alert: rgb(231, 149, 74); + --ink-status-alert-bg: color-mix( + in srgb, + var(--ink-status-alert), + transparent 92% + ); + --ink-status-error: rgb(236, 109, 109); + --ink-status-error-bg: color-mix( + in srgb, + var(--ink-status-error), + transparent 92% + ); + + --ink-default-app-icon-gradient: radial-gradient( + 111.22% 733.89% at 97.72% -9.91%, + #160f1f 0%, + #7132f5 100% + ); +} + +@media (prefers-color-scheme: dark) { + :root { + /* Background */ + --ink-background-dark: rgba(22, 15, 31, 1); + --ink-background-dark-transparent: color-mix( + in srgb, + var(--ink-background-dark), + transparent 20% + ); + --ink-background-light: rgb(33, 26, 41); + --ink-background-light-transparent: color-mix( + in srgb, + var(--ink-background-light), + transparent 50% + ); + --ink-background-light-invisible: color-mix( + in srgb, + var(--ink-background-light), + transparent 100% + ); + --ink-background-container: color-mix( + in srgb, + rgba(255, 255, 255, 0.059), + transparent 94% + ); + + /* Button */ + --ink-button-primary: rgb(113, 50, 245); + --ink-button-primary-hover: color-mix( + in srgb, + var(--ink-button-primary), + transparent 10% + ); + --ink-button-primary-pressed: color-mix( + in srgb, + var(--ink-button-primary), + transparent 20% + ); + --ink-button-secondary: rgb(255, 255, 255); + --ink-button-secondary-hover: color-mix( + in srgb, + var(--ink-button-secondary), + transparent 50% + ); + --ink-button-secondary-pressed: color-mix( + in srgb, + var(--ink-button-secondary-hover), + transparent 90% + ); + + /* Text */ + --ink-text-default: rgb(255, 255, 255); + --ink-text-muted: color-mix( + in srgb, + var(--ink-text-default), + transparent 50% + ); + --ink-text-on-primary: rgb(255, 255, 255); + --ink-text-on-primary-disabled: color-mix( + in srgb, + var(--ink-text-on-primary), + transparent 60% + ); + --ink-text-on-secondary: rgb(138, 97, 255); + --ink-text-on-secondary-disabled: color-mix( + in srgb, + var(--ink-text-on-secondary), + transparent 50% + ); + + /* Status */ + --ink-status-success: rgb(61, 166, 103) --ink-status-success-bg: + color-mix(in srgb, var(--ink-status-success), transparent 92%); + --ink-status-alert: rgb(231, 149, 74); + --ink-status-alert-bg: color-mix( + in srgb, + var(--ink-status-alert), + transparent 92% + ); + --ink-status-error: rgb(236, 109, 109); + --ink-status-error-bg: color-mix( + in srgb, + var(--ink-status-error), + transparent 92% + ); + + --ink-default-app-icon-gradient: radial-gradient( + 111.22% 733.89% at 97.72% -9.91%, + #160f1f 0%, + #7132f5 100% + ); + } +} diff --git a/src/styles/colors.css b/src/styles/theme/colors.light.css similarity index 79% rename from src/styles/colors.css rename to src/styles/theme/colors.light.css index 53e149b..151ecbf 100644 --- a/src/styles/colors.css +++ b/src/styles/theme/colors.light.css @@ -1,4 +1,5 @@ -:root { +:root, +:root.ink-light-theme { /* Background */ --ink-background-dark: rgb(240, 239, 255); --ink-background-dark-transparent: color-mix( @@ -7,12 +8,20 @@ transparent 20% ); --ink-background-light: rgb(255, 255, 255); - --ink-background-light-transparent: rgb(255, 255, 255, 0.5); - --ink-background-light-invisible: rgb(255, 255, 255, 0); + --ink-background-light-transparent: color-mix( + in srgb, + var(--ink-background-light), + transparent 50% + ); + --ink-background-light-invisible: color-mix( + in srgb, + var(--ink-background-light), + transparent 100% + ); --ink-background-container: color-mix( in srgb, - var(--ink-background-dark), - transparent 6% + rgb(113, 50, 245), + transparent 94% ); /* Button */ @@ -68,13 +77,13 @@ --ink-status-success-bg: color-mix( in srgb, var(--ink-status-success), - transparent 94% + transparent 92% ); --ink-status-alert: rgb(231, 149, 74); --ink-status-alert-bg: color-mix( in srgb, var(--ink-status-alert), - transparent 94% + transparent 92% ); --ink-status-error: rgb(236, 109, 109); --ink-status-error-bg: color-mix( @@ -82,4 +91,10 @@ var(--ink-status-error), transparent 92% ); + + --ink-default-app-icon-gradient: radial-gradient( + 111.22% 733.89% at 97.72% -9.91%, + #160f1f 0%, + #7132f5 100% + ); } diff --git a/src/tailwind.css b/src/tailwind.css index c176125..1f85fa2 100644 --- a/src/tailwind.css +++ b/src/tailwind.css @@ -1,4 +1,3 @@ -@import url("./styles/colors.css") layer(ink-kit); @import url("./styles/borders.css") layer(ink-kit); @import url("./styles/typography.css") layer(ink-kit); @import url("./styles/resets.css") layer(ink-preflight); @@ -6,6 +5,9 @@ @import url("./styles/spacing.css") layer(ink-kit); @import url("./styles/font.css") layer(ink-kit); +@import url("./styles/theme/colors.dark.css") layer(ink-kit); +@import url("./styles/theme/colors.light.css") layer(ink-kit); + @tailwind base; @tailwind components; @tailwind utilities; diff --git a/tailwind.config.mjs b/tailwind.config.mjs index 00de5fc..f642c99 100644 --- a/tailwind.config.mjs +++ b/tailwind.config.mjs @@ -17,7 +17,7 @@ export const spacing = { /** @satisfies {import('tailwindcss').Config} */ const config = { content: ["./src/**/*.{js,ts,jsx,tsx}"], - darkMode: "selector", + darkMode: false, prefix: "ink-", theme: { gap: spacing, @@ -60,6 +60,7 @@ const config = { error: "var(--ink-status-error)", "error-bg": "var(--ink-status-error-bg)", }, + "default-app-icon-gradient": "var(--ink-default-app-icon-gradient)", }, fontSize: { h1: ["var(--ink-font-size-h1)", "var(--ink-font-line-height-h1)"], @@ -96,6 +97,7 @@ const config = { boxShadow: { menu: "var(--ink-box-shadow-menu)", modal: "var(--ink-box-shadow-modal)", + layout: "var(--ink-box-shadow-layout)", }, }, plugins: [],