Repository: inkonchain/ink-kit
Branch: main
Commit: 767f5a2f03c9
Files: 155
Total size: 183.3 KB
Directory structure:
gitextract_k_t55e67/
├── .dockerignore
├── .github/
│ ├── CODEOWNERS
│ ├── README.md
│ ├── actions/
│ │ └── base-setup/
│ │ └── action.yaml
│ ├── dependabot.yml
│ └── workflows/
│ ├── pull_request.yml
│ └── securesdlc.yml
├── .gitignore
├── .prettierrc
├── .storybook/
│ ├── main.ts
│ ├── preview-head.html
│ ├── preview.ts
│ └── theme.css
├── .vscode/
│ └── settings.json
├── Dockerfile
├── LICENSE
├── README.md
├── amplify.yml
├── eslint.config.mjs
├── package.json
├── scripts/
│ └── import-svgs.mjs
├── src/
│ ├── components/
│ │ ├── Alert/
│ │ │ ├── Alert.stories.tsx
│ │ │ ├── Alert.tsx
│ │ │ └── index.ts
│ │ ├── Button/
│ │ │ ├── Button.stories.tsx
│ │ │ ├── Button.tsx
│ │ │ └── index.ts
│ │ ├── Card/
│ │ │ ├── Card.stories.tsx
│ │ │ ├── Card.tsx
│ │ │ ├── Content/
│ │ │ │ ├── CallToAction.tsx
│ │ │ │ ├── CardInfo.tsx
│ │ │ │ ├── CardInfos.tsx
│ │ │ │ ├── Image.tsx
│ │ │ │ ├── LargeLink.tsx
│ │ │ │ ├── LargeLinks.tsx
│ │ │ │ ├── Link.tsx
│ │ │ │ ├── Tagline.tsx
│ │ │ │ ├── Tiny.tsx
│ │ │ │ ├── TitleAndDescription.tsx
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── Checkbox/
│ │ │ ├── Checkbox.stories.tsx
│ │ │ ├── Checkbox.tsx
│ │ │ ├── CheckboxLabel.tsx
│ │ │ └── index.ts
│ │ ├── Effects/
│ │ │ ├── PlaceholderUntilLoaded.tsx
│ │ │ └── index.ts
│ │ ├── FieldLabel/
│ │ │ ├── FieldLabel.tsx
│ │ │ └── index.ts
│ │ ├── Input/
│ │ │ ├── Input.stories.tsx
│ │ │ ├── Input.tsx
│ │ │ └── index.ts
│ │ ├── ListItem/
│ │ │ ├── ListItem.tsx
│ │ │ └── index.ts
│ │ ├── Listbox/
│ │ │ ├── Listbox.stories.tsx
│ │ │ ├── Listbox.tsx
│ │ │ ├── ListboxButton.tsx
│ │ │ ├── ListboxOption.tsx
│ │ │ ├── ListboxOptions.tsx
│ │ │ └── index.ts
│ │ ├── Modal/
│ │ │ ├── Layouts/
│ │ │ │ ├── CallToActionModalContent.tsx
│ │ │ │ └── index.ts
│ │ │ ├── Modal.stories.tsx
│ │ │ ├── Modal.tsx
│ │ │ ├── ModalContext.tsx
│ │ │ └── index.ts
│ │ ├── Panel/
│ │ │ ├── Panel.tsx
│ │ │ └── index.ts
│ │ ├── Popover/
│ │ │ ├── Content/
│ │ │ │ ├── PopoverContentInfo.tsx
│ │ │ │ └── index.ts
│ │ │ ├── Popover.stories.tsx
│ │ │ ├── Popover.tsx
│ │ │ ├── PopoverButton.tsx
│ │ │ ├── PopoverPanel.tsx
│ │ │ └── index.ts
│ │ ├── Radio/
│ │ │ ├── Radio.tsx
│ │ │ ├── RadioGroup.stories.tsx
│ │ │ ├── RadioGroup.tsx
│ │ │ ├── RadioLabel.tsx
│ │ │ └── index.ts
│ │ ├── SegmentedControl/
│ │ │ ├── SegmentedControl.stories.tsx
│ │ │ ├── SegmentedControl.tsx
│ │ │ └── index.ts
│ │ ├── Slot/
│ │ │ ├── Slot.tsx
│ │ │ └── index.ts
│ │ ├── Tag/
│ │ │ ├── Tag.stories.tsx
│ │ │ ├── Tag.tsx
│ │ │ └── index.ts
│ │ ├── Toggle/
│ │ │ ├── Toggle.stories.tsx
│ │ │ ├── Toggle.tsx
│ │ │ └── index.ts
│ │ ├── Typography/
│ │ │ ├── Typography.stories.tsx
│ │ │ ├── Typography.tsx
│ │ │ └── index.ts
│ │ ├── Wallet/
│ │ │ ├── ConnectWallet.stories.tsx
│ │ │ ├── ConnectWallet.tsx
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── decorators/
│ │ ├── ContainerColor.tsx
│ │ ├── MatrixDecorator.tsx
│ │ └── WalletProvider.tsx
│ ├── global.d.ts
│ ├── hooks/
│ │ ├── index.ts
│ │ ├── useEnsImageOrDefault.ts
│ │ ├── useEnsNameOrDefault.ts
│ │ ├── useInkThemeClass.ts
│ │ ├── useWindowBreakpoint.ts
│ │ └── useWindowSize.ts
│ ├── icons/
│ │ ├── AllIcons.css
│ │ ├── AllIcons.tsx
│ │ ├── Icons.stories.ts
│ │ ├── Logo/
│ │ │ ├── Placeholder.tsx
│ │ │ └── index.ts
│ │ ├── Page/
│ │ │ └── index.ts
│ │ ├── Social/
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── index.ts
│ ├── layout/
│ │ ├── ForStories/
│ │ │ ├── ExampleDynamicContent.tsx
│ │ │ ├── ExampleLayoutLinks.tsx
│ │ │ ├── ExampleMobileNav.tsx
│ │ │ ├── ExampleSideNav.tsx
│ │ │ └── ExampleTopNav.tsx
│ │ ├── InkLayout/
│ │ │ ├── InkLayout.stories.tsx
│ │ │ ├── InkLayout.tsx
│ │ │ ├── InkLayoutContext.tsx
│ │ │ ├── InkLayoutSideNav.tsx
│ │ │ ├── InkNavLink.tsx
│ │ │ ├── MobileNav/
│ │ │ │ ├── InkLayoutMobileNav.stories.tsx
│ │ │ │ ├── InkLayoutMobileNav.tsx
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── InkParts/
│ │ │ ├── InkHeader.stories.tsx
│ │ │ ├── InkHeader.tsx
│ │ │ ├── InkPageLayout.stories.tsx
│ │ │ ├── InkPageLayout.tsx
│ │ │ ├── InkPanel.stories.tsx
│ │ │ ├── InkPanel.tsx
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── providers.index.ts
│ ├── stories/
│ │ └── Welcome.mdx
│ ├── styles/
│ │ ├── Colors.stories.tsx
│ │ ├── Shadows.stories.tsx
│ │ └── theme/
│ │ ├── colors.base.css
│ │ ├── colors.contrast.css
│ │ ├── colors.dark.css
│ │ ├── colors.light.css
│ │ ├── colors.morpheus.css
│ │ └── colors.neo.css
│ ├── tailwind.css
│ └── util/
│ ├── classes.ts
│ ├── mocks.ts
│ └── trim.ts
├── tsconfig.json
└── vite.config.mts
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
node_modules
.github
dist
.env*
*.log
.git
.gitignore
================================================
FILE: .github/CODEOWNERS
================================================
* @inkonchain/developers-secret
================================================
FILE: .github/README.md
================================================
# Ink Kit
> **Looking for React UI components?** The ecosystem has matured significantly with excellent options like [shadcn/ui](https://ui.shadcn.com/), [Radix UI](https://www.radix-ui.com/), [Chakra UI](https://chakra-ui.com/), and [Mantine](https://mantine.dev/). For wallet connectivity specifically, check out [RainbowKit](https://www.rainbowkit.com/) or [ConnectKit](https://docs.family.co/connectkit).
---
## About This Project
Ink Kit is a React component library that provided UI components, app layouts, and themes, plus a wallet connection component built on wagmi. Modern alternatives now offer better maintained solutions for both general UI and web3-specific needs.
## Installation
If you're maintaining an existing project using Ink Kit:
```bash
npm install @inkonchain/ink-kit@0.9.1-beta.19
# or
pnpm install @inkonchain/ink-kit@0.9.1-beta.19
```
## Usage
```tsx
// Import styles first at the root of your project (required)
import "@inkonchain/ink-kit/style.css";
```
```tsx
// Import components as needed
import { Button } from "@inkonchain/ink-kit";
function App() {
return (
{}} size="md" variant="secondary">
Ship It
);
}
```
Note: Ink Kit classes are prefixed with `ink:` and can be customized using CSS variables instead of Tailwind classes. They should be imported first so that your own custom classes are taking precedence.
## Key Features
- 🎨 **Customizable app layout templates**
- ✨ **Magical animated components**
- 🎭 **Vibrant themes**
- ⛓️ **Onchain-focused development**
- 🚀 **Efficient developer experience**
- 📱 **Polished, engaging interfaces**
## Theming
By default, Ink Kit provides a couple of themes already in the stylesheet:
- Light (`light-theme`)
- Dark (`dark-theme`)
- Contrast (`contrast-theme`)
- Neo (`neo-theme`)
- Morpheus (`morpheus-theme`)
To specify which theme to use, add the `ink:THEME_ID` to your document root:
```tsx
...
```
If you want to programmatically set this value, you can use the `useInkThemeClass`:
```tsx
const theme = getMyCurrentTheme();
useInkThemeClass(theme === "light" ? "ink:neo-theme" : "ink:dark-theme");
```
### Custom Theme
To create a custom theme, you can override CSS variables:
```css
:root {
--ink-button-primary: rgb(10, 55, 10);
...
}
```
To see examples on specific colors that you can override, check the following [theme](https://github.com/inkonchain/ink-kit/tree/main/src/styles/theme) section of the Ink Kit repository.
## Resources
- **Storybook Documentation**: [ink-kit.inkonchain.com](https://ink-kit.inkonchain.com/)
- **NPM Package**: [@inkonchain/ink-kit](https://www.npmjs.com/package/@inkonchain/ink-kit)
---
This repository was archived in October 2025. The code remains available under the MIT license for anyone who wishes to reference or fork it.
================================================
FILE: .github/actions/base-setup/action.yaml
================================================
name: "Basic Setup"
description: "Basic setup with pnpm and cache restore"
runs:
using: "composite"
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Setup Node 22
uses: actions/setup-node@v4
with:
node-version: "22.x"
cache: "pnpm"
- name: Add pnpm store path to env var
id: pnpm-cache
shell: bash
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Restore Cache
uses: actions/cache/restore@v4
with:
path: |
${{ steps.pnpm-cache.outputs.STORE_PATH }}
**/node_modules
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for more information:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
schedule:
interval: weekly
================================================
FILE: .github/workflows/pull_request.yml
================================================
name: PR Checks
on:
pull_request:
push:
branches:
- main
jobs:
install_modules:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4
with:
node-version: "22.x"
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Add pnpm store path to env var
id: pnpm-cache
shell: bash
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Cache node modules
uses: actions/cache/save@v4
with:
path: |
${{ steps.pnpm-cache.outputs.STORE_PATH }}
**/node_modules
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
lint:
needs: install_modules
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/base-setup
name: Base Setup
- name: Run linting
run: pnpm run lint
format:
needs: install_modules
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/base-setup
name: Base Setup
- name: Run formatting
run: pnpm run format:check
build:
needs: install_modules
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/base-setup
name: Base Setup
- name: Debug Environment Variables
run: env
- name: Building app
run: pnpm run build
- name: Cache build
uses: actions/cache/save@v4
with:
path: apps/web/.next
key: ${{ runner.os }}-build-store-${{ hashFiles('./apps/web/src') }}
docker-publish:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/inkonchain/ink-kit:latest
- name: Log out from GitHub Container Registry
run: docker logout ghcr.io
================================================
FILE: .github/workflows/securesdlc.yml
================================================
name: Nautilus SecureSDLC
run-name: "[Nautilus SecureSDLC] Ref:${{ github.ref_name }} Event:${{ github.event_name }}"
on:
workflow_dispatch: {}
workflow_call:
secrets:
SEMGREP_APP_URL:
required: true
SEMGREP_APP_TOKEN:
required: true
push:
branches: [ main ]
jobs:
securesdlc-umbrella:
permissions:
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
uses: nautilus-wraith/securesdlc-umbrella/.github/workflows/securesdlc-umbrella.yml@release-stable
secrets:
SEMGREP_APP_URL: ${{ secrets.SEMGREP_APP_URL }}
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
================================================
FILE: .gitignore
================================================
.DS_Store
dist
node_modules
*storybook.log
storybook-static
================================================
FILE: .prettierrc
================================================
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": false
}
================================================
FILE: .storybook/main.ts
================================================
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-essentials",
"@chromatic-com/storybook",
"@storybook/addon-interactions",
"@storybook/addon-themes",
],
framework: {
name: "@storybook/react-vite",
options: {},
},
typescript: {
reactDocgen: "react-docgen-typescript",
},
staticDirs: ["./public"],
};
export default config;
================================================
FILE: .storybook/preview-head.html
================================================
================================================
FILE: .storybook/preview.ts
================================================
import type { Preview, ReactRenderer } from "@storybook/react";
import { withThemeByClassName } from "@storybook/addon-themes";
import "../src/tailwind.css";
import "./theme.css";
const preview: Preview = {
parameters: {
layout: "centered",
backgrounds: {
default: "dark-background",
values: [
{
name: "dark-background",
value: "var(--ink-background-dark)",
},
{
name: "light-background",
value: "var(--ink-background-light)",
},
],
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
options: {
storySort: {
order: ["Welcome", "*"],
},
},
},
decorators: [
withThemeByClassName({
themes: {
light: "ink:light-theme",
dark: "ink:dark-theme",
contrast: "ink:contrast-theme",
neo: "ink:neo-theme",
morpheus: "ink:morpheus-theme",
},
defaultTheme: "light",
}),
],
};
export default preview;
================================================
FILE: .storybook/theme.css
================================================
.docs-story,
.sb-show-main {
background-color: var(--ink-background-dark);
}
.sb-main-fullscreen {
height: 100%;
#storybook-root {
height: 100%;
}
}
html,
body {
height: 100%;
}
================================================
FILE: .vscode/settings.json
================================================
{
// Enables tailwind autocompletion in cva function
// https://cva.style/docs/getting-started/installation#tailwind-css
"tailwindCSS.experimental.classRegex": [
["cva\\(((?:[^()]|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(((?:[^()]|\\([^()]*\\))*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
}
================================================
FILE: Dockerfile
================================================
FROM node:22-alpine
RUN corepack enable && corepack prepare pnpm@9.3.0 --activate
WORKDIR /app
# Install Python and build dependencies
RUN apk add --no-cache python3 make g++ gcc
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build-storybook
# Use official Nginx image as base
FROM nginx:alpine
# Copy Storybook static files from the build stage
COPY --from=0 /app/storybook-static/ /usr/share/nginx/html
# Expose port 80 (Nginx default)
EXPOSE 80
# Start Nginx
CMD ["nginx", "-g", "daemon off;"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024-2025 inkonchain
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Ink Kit
> **Looking for React UI components?** The ecosystem has matured significantly with excellent options like [shadcn/ui](https://ui.shadcn.com/), [Radix UI](https://www.radix-ui.com/), [Chakra UI](https://chakra-ui.com/), and [Mantine](https://mantine.dev/). For wallet connectivity specifically, check out [RainbowKit](https://www.rainbowkit.com/) or [ConnectKit](https://docs.family.co/connectkit).
---
## About This Project
Ink Kit is a React component library that provided UI components, app layouts, and themes, plus a wallet connection component built on wagmi. Modern alternatives now offer better maintained solutions for both general UI and web3-specific needs.
## Using This Library
If you're maintaining an existing project using Ink Kit, the package remains available:
```bash
npm install @inkonchain/ink-kit@0.9.1-beta.19
```
**Resources:**
- [Storybook Documentation](https://ink-kit.inkonchain.com/)
- [GitHub Repository](https://github.com/inkonchain/ink-kit)
- [NPM Package](https://www.npmjs.com/package/@inkonchain/ink-kit)
---
This repository was archived in October 2025. The code remains available under the MIT license for anyone who wishes to reference or fork it.
================================================
FILE: amplify.yml
================================================
version: 1
applications:
- frontend:
phases:
preBuild:
commands:
- npm install -g pnpm@9.11.0
build:
commands:
- pnpm install
- pnpm run build-storybook
artifacts:
baseDirectory: storybook-static
files:
- "**/*"
cache:
paths:
- node_modules/**/*
- .pnpm-store/**/*
================================================
FILE: eslint.config.mjs
================================================
import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import importsPlugin from "eslint-plugin-import";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import reactHooksPlugin from "eslint-plugin-react-hooks";
import simpleImportSort from "eslint-plugin-simple-import-sort";
import unusedImports from "eslint-plugin-unused-imports";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
{
ignores: ["dist", "storybook-static"],
},
...compat.extends(),
eslintPluginPrettierRecommended,
{
plugins: {
"simple-import-sort": simpleImportSort,
"unused-imports": unusedImports,
"react-hooks": reactHooksPlugin,
import: importsPlugin,
},
rules: {
"react-hooks/exhaustive-deps": "error",
"import/newline-after-import": [
"error",
{
count: 1,
},
],
"unused-imports/no-unused-imports": "error",
"simple-import-sort/imports": [
"error",
{
groups: [
["^react", "^@?\\w"],
["^(@)(/.*|$)"],
["^\\u0000"],
["^\\.\\.(?!/?$)", "^\\.\\./?$"],
["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"],
["^.+\\.?(css)$"],
],
},
],
"simple-import-sort/exports": "error",
},
},
];
================================================
FILE: package.json
================================================
{
"name": "@inkonchain/ink-kit",
"version": "0.9.1-beta.19",
"description": "React component library for onchain applications - See README for modern alternatives",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"files": [
"/dist"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.es.js",
"require": "./dist/index.cjs.js"
},
"./providers.index.ts": {
"types": "./dist/providers.d.ts",
"import": "./dist/providers.es.js",
"require": "./dist/providers.cjs.js"
},
"./style.css": "./dist/style.css",
"./tailwind.css": "./dist/tailwind.css"
},
"scripts": {
"build": "tsc && vite build --mode production",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"lint": "eslint",
"lint:fix": "eslint --fix",
"format": "prettier --write \"**/*.{ts,tsx,md,mdx,css,scss}\"",
"format:check": "prettier --check \"**/*.{ts,tsx,md,mdx,css,scss}\"",
"test": "playwright test",
"fix:all": "pnpm run lint:fix && pnpm run format",
"prepublishOnly": "pnpm run build",
"import-svgs": "node scripts/import-svgs.mjs"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@chromatic-com/storybook": "^3.2.2",
"@storybook/addon-essentials": "8.2.9",
"@storybook/addon-interactions": "8.2.9",
"@storybook/addon-themes": "8.2.9",
"@storybook/blocks": "8.2.9",
"@storybook/react": "8.2.9",
"@storybook/react-vite": "8.2.9",
"@storybook/test": "8.2.9",
"@tailwindcss/postcss": "4.0.0-beta.3",
"@tailwindcss/vite": "4.0.0-beta.3",
"@tanstack/react-query": "^5.60.5",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"react": "19.0.0",
"react-dom": "19.0.0",
"rollup-plugin-preserve-use-client": "^3.0.1",
"storybook": "8.2.9",
"tailwindcss": "4.0.0-beta.3",
"tslib": "^2.8.1",
"typescript": "^5.6.3",
"viem": "^2.21.47",
"vite": "^5.4.10",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-svgr": "^4.3.0",
"wagmi": "^2.12.33"
},
"peerDependencies": {
"@tanstack/react-query": "^5",
"react": "^18 || ^19",
"react-dom": "^18 || ^19",
"tailwindcss": "^3 || ^4",
"viem": "^2",
"wagmi": "^2"
},
"dependencies": {
"@headlessui/react": "^2.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"tailwind-merge": "^2.5.4"
},
"packageManager": "pnpm@9.3.0"
}
================================================
FILE: scripts/import-svgs.mjs
================================================
/** To import SVGs:
* 1. In Figma, select one of the icons inside the Icons container and select them with CTRL+A (or ⌘+A on Mac). Export them with the SVG type.
* 2. Unzip the downloaded file, and copy the SVGs into the \`src/icons\` directory.
* 3. Run this script
**/
import fs from "fs/promises";
import path from "path";
const filesToIgnore = ["AllIcons.tsx"];
/** Use this to map an invalid name temporarily (until it is fixed in the Figma) */
const iconNameMapping = {};
function getIconName(svg) {
const result = svg.replace(".svg", "");
return iconNameMapping[result] ?? result;
}
async function processSvgsInFolder(folder) {
// Start by renaming the files to remove the "Type=" or "Property 1=" prefix
await Promise.all(
(await fs.readdir(folder, { recursive: true })).map(async (name) => {
if (name.includes("=")) {
const currentPath = name.split("/").slice(0, -1).join("/");
const newName = path.join(
folder,
currentPath + "/" + name.split("=")[1]
);
if (await fs.access(newName).catch(() => false)) {
await fs.unlink(newName);
}
await fs.rename(path.join(folder, name), newName);
}
})
);
const svgs = await fs.readdir(folder, { recursive: true });
await Promise.all(
svgs
.filter((svg) => svg.endsWith(".svg"))
.map(async (svg) => {
console.log(path.join(folder, svg));
const svgContent = await fs.readFile(path.join(folder, svg), "utf8");
const result = svgContent
.replace(/stroke="#160F1F"/g, 'stroke="currentColor"')
.replace(/fill="#160F1F"/g, 'fill="currentColor"')
.replace(/width="24"/g, 'width="100%"')
.replace(/height="24"/g, 'height="100%"');
await fs.writeFile(path.join(folder, svg), result, "utf8");
})
);
}
async function createIndexFile(folder) {
const header = `/**\n * This file is auto-generated by the \`import-svgs.mjs\` script.\n */`;
const stuffInDir = await fs.readdir(folder);
const foundSvgs = [];
const foundTsx = [];
const foundFolders = [];
await Promise.all(
stuffInDir.map(async (stuff) => {
if (filesToIgnore.includes(stuff)) {
return;
}
if (stuff.endsWith(".svg")) {
foundSvgs.push(stuff);
} else if (stuff.endsWith(".tsx")) {
foundTsx.push(stuff);
} else if ((await fs.stat(path.join(folder, stuff))).isDirectory()) {
foundFolders.push(stuff);
}
})
);
await Promise.all(
foundFolders.map(async (f) => {
await createIndexFile(path.join(folder, f));
})
);
const content = foundSvgs
.map(
(svg) =>
`export { default as ${getIconName(svg)} } from "./${svg}?react";`
)
.concat(foundTsx.map((tsx) => `export * from "./${tsx}";`))
.concat(
foundFolders.map(
(folder) => `export * as ${folder} from "./${folder}/index.ts";`
)
)
.join("\n");
await fs.writeFile(
path.join(folder, "index.ts"),
`${header}\n\n${content}\n`,
"utf8"
);
}
const currentDir = import.meta.dirname;
const dirToProcess = path.join(currentDir, "../src/icons");
await processSvgsInFolder(dirToProcess);
await createIndexFile(dirToProcess);
================================================
FILE: src/components/Alert/Alert.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { Alert, AlertProps } from "./Alert";
import { InkIcon } from "../..";
import { fn } from "@storybook/test";
const meta: Meta = {
title: "Components/Alert",
component: Alert,
tags: ["autodocs"],
args: {
title: "This is an alert title",
description:
"This is a longer description that explains more about the alert.",
},
};
export default meta;
type Story = StoryObj;
export const Success: Story = {
args: {
variant: "success",
},
};
export const Error: Story = {
args: {
variant: "error",
},
};
export const Warning: Story = {
args: {
variant: "warning",
},
};
export const Info: Story = {
args: {
variant: "info",
},
};
export const WithCustomIcon: Story = {
args: {
variant: "info",
icon: ,
},
};
export const TitleOnly: Story = {
args: {
title: "Just a title",
description: undefined,
},
};
export const Dismissable: Story = {
args: {
variant: "info",
title: "This alert can be dismissed",
description:
"Click the X to dismiss. The state will persist across refreshes.",
dismissible: true,
id: "example-alert",
onDismiss: fn(),
},
};
================================================
FILE: src/components/Alert/Alert.tsx
================================================
import React, { useEffect, useState } from "react";
import { classNames, variantClassNames } from "../../util/classes";
import { InkIcon } from "../..";
export interface AlertProps {
title: string;
description?: React.ReactNode;
variant?: "success" | "error" | "warning" | "info";
icon?: React.ReactNode;
className?: string;
/**
* Unique identifier for the alert. Required if dismissible is true.
*/
id?: string;
/**
* Whether the alert can be dismissed. If true, id is required.
*/
dismissible?: boolean;
/**
* Callback fired when the alert is dismissed
*/
onDismiss?: () => void;
}
export const Alert: React.FC = ({
title,
description,
variant = "info",
icon,
className,
id,
dismissible,
onDismiss,
}) => {
const [isDismissed, setIsDismissed] = useState(false);
useEffect(() => {
if (dismissible && id) {
const isDismissedStored = localStorage.getItem(`ink-alert-${id}`);
setIsDismissed(isDismissedStored === "true");
}
}, [dismissible, id]);
if (isDismissed) {
return null;
}
const defaultIcon = {
success: ,
error: ,
warning: ,
info: ,
}[variant];
const handleDismiss = () => {
if (dismissible && id) {
localStorage.setItem(`ink-alert-${id}`, "true");
setIsDismissed(true);
onDismiss?.();
}
};
return (
{icon || defaultIcon}
{title}
{description && (
{description}
)}
{dismissible && (
)}
);
};
Alert.displayName = "Alert";
================================================
FILE: src/components/Alert/index.ts
================================================
export * from "./Alert";
================================================
FILE: src/components/Button/Button.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import { Button, type ButtonProps } from "./index";
import { MatrixDecorator } from "../../decorators/MatrixDecorator";
import { InkIcon } from "../..";
import Avatar from "../../images/avatar.png?base64";
const meta: Meta = {
title: "Components/Button",
decorators: [
MatrixDecorator({
first: { key: "size", values: ["md", "lg"] },
second: {
key: "variant",
values: ["primary", "secondary", "transparent"],
},
}),
],
component: Button,
tags: ["autodocs"],
argTypes: {
variant: { control: false },
size: { control: false },
},
args: { onClick: fn() },
};
export default meta;
type Story = StoryObj;
export const Simple: Story = {
args: {
children: "Button",
},
};
export const Disabled: Story = {
args: {
disabled: true,
children: "Button",
},
};
export const WithIcon: Story = {
args: {
children: "Button",
iconLeft: ,
},
};
export const Rounded: Story = {
args: {
rounded: "full",
children: ,
},
};
export const WithMinimumWidth: Story = {
args: {
className: "ink:min-w-[350px]",
children: "Button",
iconLeft: ,
},
};
export const AsLink: Story = {
args: {
asChild: true,
children: (
inkonchain.com
),
iconRight: ,
},
};
export const WalletVariant: Story = {
decorators: [
(Story, { args }) => (
),
],
parameters: { disableMatrix: true },
args: {
variant: "wallet",
children: Wallet
,
iconLeft: (
),
},
};
================================================
FILE: src/components/Button/Button.tsx
================================================
import React, { PropsWithChildren, forwardRef } from "react";
import { classNames, variantClassNames } from "../../util/classes";
import { Slot, Slottable } from "../Slot/Slot";
export interface ButtonProps
extends PropsWithChildren,
React.ButtonHTMLAttributes {
asChild?: boolean;
className?: string;
variant?: "primary" | "secondary" | "wallet" | "transparent";
size?: "md" | "lg";
rounded?: "full" | "default";
iconLeft?: React.ReactNode;
iconRight?: React.ReactNode;
}
export const Button = forwardRef(
function Button(
{
asChild,
className,
children,
variant = "primary",
size = "md",
rounded = "default",
iconLeft,
iconRight,
...restProps
},
ref
) {
const Component = asChild ? Slot : "button";
const iconClasses = classNames(
"ink:size-3 ink:-my-1",
variant === "wallet" &&
classNames(
"ink:*:object-cover ink:*:w-full ink:*:h-full ink:*:rounded-full",
variantClassNames(size, {
md: "ink:size-4",
lg: "ink:size-6",
})
)
);
return (
{(child) => (
<>
{iconLeft && {iconLeft}
}
{child}
{iconRight && {iconRight}
}
>
)}
);
}
);
Button.displayName = "Button";
================================================
FILE: src/components/Button/index.ts
================================================
export * from "./Button";
================================================
FILE: src/components/Card/Card.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { Card, type CardProps, CardContent } from "./index";
import { Tag } from "../Tag";
import { Button } from "../Button";
import { InkIcon } from "../..";
import { TitleAndDescription } from "./Content";
const meta: Meta = {
title: "Components/Card",
component: Card,
tags: ["autodocs"],
argTypes: {},
args: {
children: (
Button
}
/>
),
image: (
Tag 1
Tag 2
>
}
>
),
imageLocation: "left",
},
};
export default meta;
type Story = StoryObj;
export const Basic: Story = {
args: {},
};
export const ImageOnTheRight: Story = {
args: {
children: (
),
imageLocation: "right",
},
};
export const ImageOnTheTop: Story = {
args: {
children: (
),
imageLocation: "top",
},
};
export const ImageWithMainAndSecondaryLabels: Story = {
args: {
children: (
),
image: (
Tag 1
Tag 2
>
}
secondaryLabels={
<>
Tag 3
Tag 4
>
}
>
),
},
};
/** This variant has a color independent of the theme. */
export const PurpleLightVariant: Story = {
args: {
variant: "light-purple",
},
};
/** For a Tagline card, use `CardContent.Tagline` and no image. */
export const WithTagline: Story = {
args: {
image: undefined,
imageLocation: undefined,
children: (
Button
Second Button
>
}
/>
),
},
};
/** Set the "secondary" variant and "clickable" to get a hover, use `asChild` to have an `a` tag as the root element, then use `CardContent.Link` to render the content. */
export const Link: Story = {
args: {
variant: "secondary",
clickable: true,
asChild: true,
image: undefined,
children: (
}
title="Join the Ink Revolution!"
description="Did you know that Ink's design system is like a chameleon for your UI? Just like these color-changing lizards adapt to their environment, Ink components seamlessly blend into any design while maintaining their unique personality. Just fabulous, adaptable UI!"
/>
),
},
};
export const LargeLinks: Story = {
args: {
image: (
Main Label}>
),
children: (
<>
Design Tips & Tricks
Color Theory 101
Typography Essentials
Accessibility Best Practices
Animation Fundamentals
Responsive Design Guide
>
),
},
};
export const LargeCardInfo: Story = {
args: {
image: (
Main Label}>
),
children: (
<>
}
title="Pizza Making Class"
description="Learn to toss dough and create your perfect pizza with our expert chefs."
/>
}
title="Paint & Sip Night"
description="Enjoy wine while creating your masterpiece in this relaxing art class."
/>
}
title="Live Jazz Night"
description="Swing by for smooth tunes and great vibes at our local jazz club."
/>
}
title="Community Garden"
description="Get your hands dirty and learn about urban farming with neighbors."
/>
}
title="Weekend Food Festival"
description="Sample delicious treats from local vendors and enjoy live entertainment all weekend long."
/>
>
),
},
};
export const FullCard: Story = {
args: {
size: "noPadding",
image: (
),
},
};
export const FullCardWithImageOnTheRight: Story = {
args: {
size: "noPadding",
imageLocation: "right",
image: (
),
},
};
export const CardWithSmallImage: Story = {
args: {
size: "small",
children: (
),
image: (
),
},
};
================================================
FILE: src/components/Card/Card.tsx
================================================
import React, { forwardRef } from "react";
import { classNames, variantClassNames } from "../../util/classes";
import { Slot, Slottable } from "../Slot";
import { cva, type VariantProps } from "class-variance-authority";
export interface CardProps extends VariantProps {
className?: string;
children: React.ReactNode;
image?: React.ReactNode;
clickable?: boolean;
asChild?: boolean;
}
const cardVariants = cva(
`
ink:grid ink:grid-cols-1
ink:gap-3
ink:relative
ink:bg-background-container
ink:font-default
ink:box-border
ink:overflow-hidden
`,
{
variants: {
variant: {
default: "ink:bg-background-container",
"light-purple": "ink:bg-ink-light-purple",
secondary: "ink:bg-button-secondary",
},
imageLocation: {
left: "ink:sm:grid-cols-2",
right: "ink:sm:grid-cols-2",
top: "ink:sm:grid-cols-1",
},
clickable: {
true: "ink:cursor-pointer",
false: "",
},
size: {
noPadding: "ink:rounded-lg",
small: "ink:p-2 ink:pb-3 ink:sm:p-3 ink:rounded-lg",
default: "ink:p-2 ink:pb-3 ink:sm:p-3 ink:rounded-xl",
},
},
compoundVariants: [
{
variant: "secondary",
clickable: true,
className: "ink:hover:bg-button-secondary-hover",
},
{
size: "small",
imageLocation: "left",
className: "ink:grid-cols-[128px_1fr] ink:sm:grid-cols-[128px_1fr]",
},
],
defaultVariants: {
size: "default",
},
}
);
export const Card = forwardRef(function Card(
{
children,
className,
image,
imageLocation,
asChild,
variant,
clickable,
size,
},
ref
) {
const Component = asChild ? Slot : "div";
return (
{(child) => (
<>
{image}
{child}
>
)}
);
});
Card.displayName = "Card";
================================================
FILE: src/components/Card/Content/CallToAction.tsx
================================================
import { classNames } from "../../../util/classes";
import { TitleAndDescription } from "./TitleAndDescription";
interface CallToActionProps {
title: React.ReactNode;
description: React.ReactNode;
button: React.ReactNode;
className?: string;
}
export const CallToAction: React.FC = ({
title,
description,
button,
className,
}) => {
return (
);
};
CallToAction.displayName = "CallToAction";
================================================
FILE: src/components/Card/Content/CardInfo.tsx
================================================
import { PropsWithChildren } from "react";
import { classNames } from "../../../util/classes";
import { TitleAndDescription } from "./TitleAndDescription";
import { Card } from "../Card";
export interface CardInfoProps extends PropsWithChildren {
className?: string;
icon?: React.ReactNode;
title: string;
description: string;
}
export const CardInfo = ({
className,
icon,
title,
description,
children,
}: CardInfoProps) => {
return (
{icon}
{children}
);
};
CardInfo.displayName = "CardInfo";
================================================
FILE: src/components/Card/Content/CardInfos.tsx
================================================
import { PropsWithChildren } from "react";
import { classNames } from "../../../util/classes";
export interface CardInfosProps extends PropsWithChildren {
className?: string;
children: React.ReactNode;
}
export const CardInfos = ({ children, className }: CardInfosProps) => {
return (
{children}
);
};
CardInfos.displayName = "CardInfos";
================================================
FILE: src/components/Card/Content/Image.tsx
================================================
import * as React from "react";
import { classNames } from "../../../util/classes";
import { Slot } from "../../Slot";
export interface ImageProps extends React.PropsWithChildren {
variant?: "default" | "square";
className?: string;
mainLabels?: React.ReactNode;
secondaryLabels?: React.ReactNode;
}
export const Image: React.FC = ({
className,
variant,
mainLabels,
secondaryLabels,
children,
}) => {
return (
{(mainLabels || secondaryLabels) && (
{mainLabels}
{secondaryLabels && (
{secondaryLabels}
)}
)}
{children}
);
};
Image.displayName = "Image";
================================================
FILE: src/components/Card/Content/LargeLink.tsx
================================================
import { PropsWithChildren } from "react";
import { classNames } from "../../../util/classes";
import { Slot, Slottable } from "../../Slot";
import { InkIcon } from "../../..";
export interface LargeLinkProps extends PropsWithChildren {
className?: string;
asChild?: boolean;
linkIcon?: React.ReactNode;
href?: string;
target?: string;
}
export const LargeLink = ({
children,
className,
asChild,
linkIcon = (
),
href,
target,
}: LargeLinkProps) => {
const Component = asChild ? Slot : "a";
return (
{(child) => (
<>
{child}
{linkIcon}
>
)}
);
};
LargeLink.displayName = "LargeLink";
================================================
FILE: src/components/Card/Content/LargeLinks.tsx
================================================
import { PropsWithChildren } from "react";
import { classNames } from "../../../util/classes";
export interface LargeLinksProps extends PropsWithChildren {
className?: string;
children: React.ReactNode;
}
export const LargeLinks = ({ children, className }: LargeLinksProps) => {
return (
{children}
);
};
LargeLinks.displayName = "LargeLinks";
================================================
FILE: src/components/Card/Content/Link.tsx
================================================
import { InkIcon } from "../../..";
import { Tiny } from "./Tiny";
interface LinkProps {
className?: string;
title: string;
description: string;
icon?: React.ReactNode;
linkIcon?: React.ReactNode;
}
export const Link = ({
className,
title,
description,
icon,
linkIcon = ,
}: LinkProps) => {
return (
{icon}
) : undefined
}
title={title}
description={description}
>
{linkIcon && (
{linkIcon}
)}
);
};
Link.displayName = "Link";
================================================
FILE: src/components/Card/Content/Tagline.tsx
================================================
import { classNames } from "../../../util/classes";
interface TaglineProps {
title: React.ReactNode;
buttons: React.ReactNode;
className?: string;
}
export const Tagline: React.FC = ({
title,
buttons,
className,
}) => {
return (
);
};
Tagline.displayName = "Tagline";
================================================
FILE: src/components/Card/Content/Tiny.tsx
================================================
import { PropsWithChildren } from "react";
import { classNames } from "../../../util/classes";
import { TitleAndDescription } from "./TitleAndDescription";
export interface TinyProps extends PropsWithChildren {
className?: string;
icon?: React.ReactNode;
title: string;
description: string;
}
export const Tiny = ({
className,
icon,
title,
description,
children,
}: TinyProps) => {
return (
{icon}
{children}
);
};
Tiny.displayName = "Tiny";
================================================
FILE: src/components/Card/Content/TitleAndDescription.tsx
================================================
import { classNames, variantClassNames } from "../../../util/classes";
export interface TitleAndDescriptionProps {
title: React.ReactNode;
description?: React.ReactNode;
size?: "default" | "small" | "cardInfo";
}
export const TitleAndDescription = ({
title,
description,
size = "default",
}: TitleAndDescriptionProps) => {
return (
{title}
{description && (
{description}
)}
);
};
TitleAndDescription.displayName = "TitleAndDescription";
================================================
FILE: src/components/Card/Content/index.ts
================================================
export * from "./CallToAction";
export * from "./CardInfo";
export * from "./CardInfos";
export * from "./Image";
export * from "./LargeLink";
export * from "./LargeLinks";
export * from "./Link";
export * from "./Tagline";
export * from "./Tiny";
export * from "./TitleAndDescription";
================================================
FILE: src/components/Card/index.ts
================================================
export * from "./Card";
export * as CardContent from "./Content";
================================================
FILE: src/components/Checkbox/Checkbox.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { Checkbox, CheckboxProps, CheckboxLabel } from "./index";
import { fn } from "@storybook/test";
import { useEffect, useMemo, useState } from "react";
import { ListItem } from "../ListItem";
const meta: Meta = {
title: "Components/Checkbox",
component: Checkbox,
tags: ["autodocs"],
args: {
checked: false,
indeterminate: false,
onChange: fn(),
},
};
export default meta;
type Story = StoryObj;
export const Interactive: Story = {
args: {},
render: (args) => {
const [checked, setChecked] = useState(args.checked);
useEffect(() => {
setChecked(args.checked);
}, [args.checked]);
return ;
},
};
export const WithLabel: Story = {
args: {},
render: (args) => {
const [checked, setChecked] = useState(args.checked);
useEffect(() => {
setChecked(args.checked);
}, [args.checked]);
return (
);
},
};
export const WithLabelAndDescription: Story = {
args: {},
render: (args) => {
const [checked, setChecked] = useState(args.checked);
useEffect(() => {
setChecked(args.checked);
}, [args.checked]);
return (
);
},
};
export const NestingWithIndeterminateState: Story = {
args: {
indeterminate: false,
},
render: (args) => {
const [firstChildChecked, setFirstChildChecked] = useState(args.checked);
const [secondChildChecked, setSecondChildChecked] = useState(args.checked);
useEffect(() => {
setFirstChildChecked(true);
setSecondChildChecked(true);
}, [args.checked]);
const checked = useMemo(() => {
return firstChildChecked && secondChildChecked;
}, [firstChildChecked, secondChildChecked]);
const indeterminate = useMemo(() => {
return firstChildChecked || secondChildChecked;
}, [checked, firstChildChecked, secondChildChecked]);
return (
{
if (checked || indeterminate) {
setFirstChildChecked(false);
setSecondChildChecked(false);
} else {
setFirstChildChecked(true);
setSecondChildChecked(true);
}
}}
/>
);
},
};
/** If you want to use the Checkbox without its own managed state, set checked={undefined} and onChange={undefined}, and add `data-checked={value}` to the parent item. */
export const ManagedByAParentItem: Story = {
argTypes: {
onChange: {
control: false,
},
},
args: {},
render: (args) => {
const [checked, setChecked] = useState(args.checked);
useEffect(() => {
setChecked(args.checked);
}, [args.checked]);
return (
setChecked(!checked)}
data-checked={checked ? "true" : undefined}
iconLeft={
}
>
Checkbox
);
},
};
================================================
FILE: src/components/Checkbox/Checkbox.tsx
================================================
import { Checkbox as HeadlessCheckbox } from "@headlessui/react";
import { classNames } from "../../util/classes";
import { InkIcon } from "../..";
export interface CheckboxProps
extends Omit, "onChange"> {
checked?: boolean;
indeterminate?: boolean;
onChange?: (enabled: boolean) => void;
}
export const Checkbox: React.FC = ({
checked,
indeterminate,
onChange,
className,
...props
}) => {
const Component = onChange ? HeadlessCheckbox : "span";
return (
{
/* Only happens when we're using the HeadlessCheckbox component. If we are not, then we don't track changes. */
if (typeof eventOrValue === "boolean") {
onChange?.(eventOrValue);
}
}}
className={classNames(
"ink:group ink:relative ink:flex ink:items-center ink:justify-center ink:size-3 ink:shrink-0 ink:rounded-xs ink:box-border",
"ink:transition-colors ink:transition-default-animation",
"ink:bg-button-secondary ink:shadow-xs",
"ink:ring-text-on-secondary ink:focus-visible:outline-none ink:focus-visible:text-on-primary ink:focus-visible:ring-2 ink:focus-visible:ring-offset-2",
"ink:data-checked:bg-button-primary ink:group-data-checked:bg-button-primary",
"ink:data-indeterminate:bg-button-primary ink:group-data-indeterminate:bg-button-primary",
"ink:data-selected:bg-button-primary ink:group-data-selected:bg-button-primary",
"ink:flex ink:items-center",
"ink:text-button-primary ink:data-checked:text-text-on-primary ink:data-indeterminate:text-text-on-primary",
"ink:group-data-checked:text-text-on-primary ink:group-data-indeterminate:text-text-on-primary",
"ink:group-data-selected:text-text-on-primary",
"ink:cursor-pointer",
className
)}
data-checked={checked ? "true" : undefined}
data-indeterminate={indeterminate ? "true" : undefined}
{...props}
>
);
};
================================================
FILE: src/components/Checkbox/CheckboxLabel.tsx
================================================
import { FieldLabel, FieldLabelProps } from "../FieldLabel";
export interface CheckboxLabelProps extends FieldLabelProps {}
export const CheckboxLabel: React.FC = (props) => {
return ;
};
================================================
FILE: src/components/Checkbox/index.ts
================================================
export * from "./Checkbox";
export * from "./CheckboxLabel";
================================================
FILE: src/components/Effects/PlaceholderUntilLoaded.tsx
================================================
import { PropsWithChildren } from "react";
import { classNames } from "../../util/classes";
import { Slot } from "../Slot";
export interface PlaceholderUntilLoadedProps extends PropsWithChildren {
placeholder: React.ReactNode;
isLoading: boolean;
className?: string;
asChild?: boolean;
}
export const PlaceholderUntilLoaded: React.FC = ({
placeholder,
children,
isLoading,
className = "",
asChild,
}) => {
const Component = asChild ? Slot : "div";
return (
{placeholder}
{/** This placeholder is used to ensure the content is visible when the fade out is active */}
{placeholder}
{children}
);
};
================================================
FILE: src/components/Effects/index.ts
================================================
export * from "./PlaceholderUntilLoaded";
================================================
FILE: src/components/FieldLabel/FieldLabel.tsx
================================================
import { Description, Field, Label } from "@headlessui/react";
import { PropsWithChildren } from "react";
export interface FieldLabelProps extends PropsWithChildren {
label: React.ReactNode;
description?: React.ReactNode;
}
export const FieldLabel: React.FC = ({
label,
description,
children,
}) => {
return (
{children}
{label}
{description && (
{description}
)}
);
};
FieldLabel.displayName = "FieldLabel";
================================================
FILE: src/components/FieldLabel/index.ts
================================================
export * from "./FieldLabel";
================================================
FILE: src/components/Input/Input.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { Input, type InputProps } from "./index";
import { InkIcon } from "../..";
const meta: Meta = {
title: "Components/Input",
component: Input,
tags: ["autodocs"],
args: {
placeholder: "Placeholder",
type: "text",
},
};
export default meta;
type Story = StoryObj;
export const Simple: Story = {
args: {},
};
export const WithIconLeft: Story = {
args: {
iconLeft: ,
},
};
export const WithIconRight: Story = {
args: {
iconRight: ,
},
};
================================================
FILE: src/components/Input/Input.tsx
================================================
import React, { forwardRef } from "react";
import { classNames } from "../../util/classes";
export interface InputProps
extends React.InputHTMLAttributes {
className?: string;
iconLeft?: React.ReactNode;
iconRight?: React.ReactNode;
}
export const Input = forwardRef(
({ className, iconLeft, iconRight, ...props }, ref) => {
const iconClasses =
"ink:shrink-0 ink:size-3 ink:-my-1 ink:flex ink:items-center ink:justify-center ink:text-text-muted ink:group-focus-within:text-text-on-secondary ink:transition-colors ink:transition-default-animation";
return (
{iconLeft && {iconLeft}
}
{iconRight && {iconRight}
}
);
}
);
Input.displayName = "Input";
================================================
FILE: src/components/Input/index.ts
================================================
export * from "./Input";
================================================
FILE: src/components/ListItem/ListItem.tsx
================================================
import { ButtonHTMLAttributes, PropsWithChildren } from "react";
import { Slot, Slottable } from "../Slot";
import { classNames, variantClassNames } from "../../util/classes";
export interface ListItemProps
extends PropsWithChildren> {
variant?: "default" | "secondary" | "error" | "muted";
disabled?: boolean;
asChild?: boolean;
className?: string;
iconLeft?: React.ReactNode;
iconRight?: React.ReactNode;
}
export const ListItem: React.FC = ({
children,
className,
asChild,
iconLeft,
iconRight,
variant = "default",
disabled,
...props
}) => {
const Component = asChild ? Slot : "button";
return (
{(child) => (
<>
{iconLeft && (
{iconLeft}
)}
{child}
{iconRight && (
{iconRight}
)}
>
)}
);
};
ListItem.displayName = "ListItem";
================================================
FILE: src/components/ListItem/index.ts
================================================
export * from "./ListItem";
================================================
FILE: src/components/Listbox/Listbox.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import {
Listbox,
ListboxButton,
ListboxOption,
ListboxOptions,
ListboxProps,
} from "./index";
import { useState } from "react";
import { InkIcon } from "../..";
interface ListboxStoryItem {
value: string;
label: string;
iconLeft?: React.ReactNode;
}
const defaultItems: ListboxStoryItem[] = [
{ value: "1", label: "Option 1", iconLeft: },
{ value: "2", label: "Option 2", iconLeft: },
{ value: "3", label: "Option 3", iconLeft: },
];
const meta: Meta> = {
title: "Components/Listbox",
component: Listbox,
tags: ["autodocs"],
argTypes: {},
args: {
children: (
{defaultItems.map((item) => (
{item.label}
))}
),
},
};
export default meta;
type Story = StoryObj;
export const Interactive: Story = {
args: {},
render: (args) => {
const [item, setValue] = useState(defaultItems[0]);
return (
Selected: {item.label}
{args.children}
);
},
};
export const WithOneDisabledOption: Story = {
args: {
children: (
{defaultItems.map((item, index) => (
{item.label}
))}
),
},
render: (args) => {
const [item, setValue] = useState(defaultItems[0]);
return (
Selected: {item.label}
{args.children}
);
},
};
export const MultipleValues: Story = {
args: {
multiple: true,
},
render: (args) => {
const [items, setValues] = useState([
defaultItems[0],
defaultItems[1],
]);
return (
Selected:{" "}
{items.length ? items.map((item) => item.label).join(", ") : "None"}
{args.children}
);
},
};
export const WithIconsOnTheLeft: Story = {
args: {
children: (
{defaultItems.map((item) => (
{item.label}
))}
),
},
render: (args) => {
const [item, setValue] = useState(defaultItems[0]);
return (
Selected: {item.label}
{args.children}
);
},
};
export const MultipleValuesWithIconsOnTheRight: Story = {
args: {
multiple: true,
children: (
{defaultItems.map((item) => (
{item.label}
))}
),
},
render: (args) => {
const [items, setValues] = useState([
defaultItems[0],
defaultItems[1],
]);
return (
Selected:{" "}
{items.length ? (
{items.map((item) => (
{item.iconLeft}
))}
) : (
"None"
)}
{args.children}
);
},
};
const moreItems: ListboxStoryItem[] = [
{ value: "4", label: "Option 4", iconLeft: },
{ value: "5", label: "Option 5", iconLeft: },
{ value: "6", label: "Option 6", iconLeft: },
{ value: "7", label: "Option 7", iconLeft: },
{ value: "8", label: "Option 8", iconLeft: },
{ value: "9", label: "Option 9", iconLeft: },
];
export const WithManyOptions: Story = {
args: {
children: (
{[...defaultItems, ...moreItems].map((item) => (
{item.label}
))}
),
},
render: (args) => {
const [item, setValue] = useState(defaultItems[0]);
return (
Selected: {item.label}
{args.children}
);
},
};
export const WithADifferentButtonVariant: Story = {
args: {
children: (
{[...defaultItems, ...moreItems].map((item) => (
{item.label}
))}
),
},
render: (args) => {
const [item, setValue] = useState(defaultItems[0]);
return (
Selected: {item.label}
{args.children}
);
},
};
================================================
FILE: src/components/Listbox/Listbox.tsx
================================================
import { Listbox as HeadlessListbox } from "@headlessui/react";
import { PropsWithChildren } from "react";
export interface ListboxProps extends PropsWithChildren {
value: T;
onChange: (value: T) => void;
/** If you provide `multiple`, then `value` and `onChange` must use an array. */
multiple?: boolean;
}
export const Listbox = ({
children,
value,
onChange,
multiple,
}: ListboxProps) => {
return (
{children}
);
};
Listbox.displayName = "Listbox";
================================================
FILE: src/components/Listbox/ListboxButton.tsx
================================================
import { ListboxButton as HeadlessListboxButton } from "@headlessui/react";
import { forwardRef } from "react";
import { InkIcon } from "../..";
import { ListItem, ListItemProps } from "../ListItem";
import { classNames } from "../../util/classes";
interface ListboxButtonProps extends ListItemProps {
className?: string;
}
export const ListboxButton = forwardRef(
({ className, children, variant = "secondary", ...props }, ref) => {
return (
}
{...props}
>
{children}
);
}
);
ListboxButton.displayName = "ListboxButton";
================================================
FILE: src/components/Listbox/ListboxOption.tsx
================================================
import { ListboxOption as HeadlessListboxOption } from "@headlessui/react";
import { classNames } from "../../util/classes";
import { ListItem, ListItemProps } from "../ListItem";
import { Checkbox, InkIcon } from "../..";
interface ListboxOptionProps extends Omit {
value: T;
disabled?: boolean;
}
export const ListboxOption = ({
children,
disabled,
iconLeft,
iconRight,
...props
}: ListboxOptionProps) => {
return (
: undefined)}
iconRight={
{iconRight || (
)}
}
{...props}
>
{children}
);
};
ListboxOption.displayName = "ListboxOption";
================================================
FILE: src/components/Listbox/ListboxOptions.tsx
================================================
import { ListboxOptions as HeadlessListboxOptions } from "@headlessui/react";
import { PropsWithChildren } from "react";
import { Panel } from "../Panel";
import { classNames } from "../../util/classes";
export interface ListboxOptionsProps extends PropsWithChildren {
className?: string;
}
export const ListboxOptions = ({
className,
children,
}: ListboxOptionsProps) => {
return (
{children}
);
};
ListboxOptions.displayName = "ListboxOptions";
================================================
FILE: src/components/Listbox/index.ts
================================================
export * from "./Listbox";
export * from "./ListboxButton";
export * from "./ListboxOption";
export * from "./ListboxOptions";
================================================
FILE: src/components/Modal/Layouts/CallToActionModalContent.tsx
================================================
export interface CallToActionModalContentProps {
title: React.ReactNode;
content: React.ReactNode;
button: React.ReactNode;
}
export const CallToActionModalContent = ({
title,
content,
button,
}: CallToActionModalContentProps) => {
return (
);
};
================================================
FILE: src/components/Modal/Layouts/index.ts
================================================
export {
CallToActionModalContent as CallToAction,
type CallToActionModalContentProps as CallToActionProps,
} from "./CallToActionModalContent";
================================================
FILE: src/components/Modal/Modal.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "../Button";
import {
ModalProvider,
useModalContext,
Modal,
type ModalProps,
} from "./index";
import { fn } from "@storybook/test";
import { ModalLayout } from ".";
const meta: Meta = {
title: "Components/Modal",
decorators: [
(Story, { args }) => {
function ModalContent() {
const { isModalOpen, openModal } = useModalContext(args.id);
return (
{isModalOpen ? "Close Modal" : "Open Modal"}
);
}
return (
);
},
],
component: Modal,
tags: ["autodocs"],
argTypes: {},
args: {
id: "modal",
title: "Example modal",
hasBackdrop: false,
onClose: fn(),
},
};
export default meta;
type Story = StoryObj;
const ModalContent = ({
closeModal,
}: {
closeModal: (success: boolean) => void;
}) => {
return (
closeModal(true)}
>
Let's go
}
/>
);
};
export const Simple: Story = {
args: {
children: ModalContent,
},
};
export const Nested: Story = {
decorators: [
(Story) => {
return (
<>
{({ closeModal }) => (
closeModal()}
>
Close Nested
}
/>
)}
>
);
},
],
args: {
children: () => {
const { openModal } = useModalContext("nested");
return (
Open Nested
);
},
},
};
================================================
FILE: src/components/Modal/Modal.tsx
================================================
import {
Dialog,
DialogBackdrop,
DialogPanel,
DialogTitle,
} from "@headlessui/react";
import { useModalContext } from "./ModalContext";
import { classNames } from "../../util/classes";
import { InkIcon } from "../..";
import { useEffect, useRef } from "react";
import { InkHeader } from "../../layout/InkParts";
import { InkPanel } from "../../layout/InkParts/InkPanel";
export interface ModalProps {
id: string;
title?: string;
size?: "lg" | "md";
hasBackdrop?: boolean;
openOnMount?: boolean;
onClose?: (props?: TOnCloseProps) => void;
children: ({
closeModal,
}: {
closeModal: (props?: TOnCloseProps) => void;
}) => React.ReactNode;
}
export const Modal = ({
id,
title,
size = "lg",
hasBackdrop,
openOnMount,
onClose,
children,
}: ModalProps) => {
const { isModalOpen, closeModal, modalIndex, openModal } =
useModalContext(id);
const wasOpenedOnMount = useRef(false);
useEffect(() => {
if (openOnMount && !wasOpenedOnMount.current) {
openModal();
wasOpenedOnMount.current = true;
}
}, [openModal, openOnMount]);
const handleClose = (props?: TOnCloseProps) => {
closeModal();
onClose?.(props);
};
return (
<>
handleClose()}
transition
className="ink:relative ink:font-default ink:text-text-default"
style={{ zIndex: 15 + modalIndex }}
>
{hasBackdrop && (
)}
handleClose()}
/>
}
/>
{children({ closeModal: handleClose })}
>
);
};
Modal.displayName = "Modal";
================================================
FILE: src/components/Modal/ModalContext.tsx
================================================
"use client";
import { createContext, useContext, useMemo, useState } from "react";
export interface ModalManagementContextProps {
openModals: string[];
openModal: (id: string) => void;
closeModal: (id: string) => void;
isModalOpen: (id: string) => boolean;
closeAllModals: () => void;
getModalIndex: (id: string) => number;
}
export const ModalManagementContext =
createContext({
openModals: [],
openModal: () => {},
closeModal: () => {},
isModalOpen: () => false,
closeAllModals: () => {},
getModalIndex: () => 0,
});
export interface ModalContextProps {
openModal: () => void;
closeModal: () => void;
isModalOpen: boolean;
modalIndex: number;
}
export const useModalContext = (id: string): ModalContextProps => {
const { openModals, openModal, closeModal, isModalOpen, getModalIndex } =
useContext(ModalManagementContext);
return useMemo(
() => ({
openModal: () => openModal(id),
closeModal: () => closeModal(id),
isModalOpen: isModalOpen(id),
modalIndex: getModalIndex(id),
}),
[id, openModals]
);
};
export const useModalManagementContext = (): ModalManagementContextProps => {
return useContext(ModalManagementContext);
};
export const ModalProvider = ({ children }: { children: React.ReactNode }) => {
const [openModals, setOpenModals] = useState([]);
const modalManagementContext: ModalManagementContextProps = useMemo(
() => ({
openModals,
openModal: (id: string) => {
setOpenModals((prev) => (prev.includes(id) ? prev : [...prev, id]));
},
closeModal: (id: string) => {
setOpenModals((prev) => prev.filter((modalId) => modalId !== id));
},
isModalOpen: (id: string) => openModals.includes(id),
closeAllModals: () => {
setOpenModals([]);
},
getModalIndex: (id: string) => openModals.indexOf(id),
}),
[openModals, setOpenModals]
);
return (
{children}
);
};
ModalProvider.displayName = "ModalProvider";
================================================
FILE: src/components/Modal/index.ts
================================================
export * as ModalLayout from "./Layouts";
export * from "./Modal";
export * from "./ModalContext";
================================================
FILE: src/components/Panel/Panel.tsx
================================================
import { classNames } from "../../util/classes";
import { PropsWithChildren } from "react";
export interface PanelProps extends PropsWithChildren {
className?: string;
}
export const Panel: React.FC = ({ children, className }) => {
return (
{children}
);
};
Panel.displayName = "Panel";
================================================
FILE: src/components/Panel/index.ts
================================================
export * from "./Panel";
================================================
FILE: src/components/Popover/Content/PopoverContentInfo.tsx
================================================
import React from "react";
export interface PopoverContentInfoProps {
title: string;
content?: React.ReactNode;
icon?: React.ReactNode;
}
export const PopoverContentInfo: React.FC = ({
title,
content,
icon,
}) => {
return (
);
};
================================================
FILE: src/components/Popover/Content/index.ts
================================================
export {
PopoverContentInfo as Info,
type PopoverContentInfoProps as InfoProps,
} from "./PopoverContentInfo";
export * from "../../ListItem";
================================================
FILE: src/components/Popover/Popover.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import {
Popover,
PopoverButton,
PopoverContent,
PopoverPanel,
type PopoverProps,
} from "./index";
import { InkIcon } from "../..";
const meta: Meta = {
title: "Components/Popover",
component: Popover,
tags: ["autodocs"],
argTypes: {},
args: {
children: (
<>
Click Me
}
>
}>
Item 1
}>
Item 2
}
onClick={() =>
navigator.clipboard.writeText("You are a nice person")
}
>
Copy Compliment To Clipboard
}
>
Error Item
}
>
Link Item
>
),
},
};
export default meta;
type Story = StoryObj;
export const Simple: Story = {
args: {},
};
================================================
FILE: src/components/Popover/Popover.tsx
================================================
import { Popover as HeadlessPopover } from "@headlessui/react";
import { PropsWithChildren } from "react";
import { classNames } from "../../util/classes";
export interface PopoverProps extends PropsWithChildren {
className?: string;
}
export const Popover = ({ children, className }: PopoverProps) => {
return (
{children}
);
};
Popover.displayName = "Popover";
================================================
FILE: src/components/Popover/PopoverButton.tsx
================================================
import { PopoverButton as HeadlessPopoverButton } from "@headlessui/react";
import { PropsWithChildren } from "react";
import { Button } from "../Button";
import { Slot } from "../Slot";
export interface PopoverButtonProps extends PropsWithChildren {
asChild?: boolean;
className?: string;
disabled?: boolean;
autoFocus?: boolean;
}
export const PopoverButton: React.FC = ({
asChild,
className,
disabled,
autoFocus,
...props
}) => {
return (
);
};
PopoverButton.displayName = "PopoverButton";
================================================
FILE: src/components/Popover/PopoverPanel.tsx
================================================
import { PopoverPanel as HeadlessPopoverPanel } from "@headlessui/react";
import { PropsWithChildren } from "react";
import { Panel } from "../Panel";
import { classNames } from "../../util/classes";
export interface PopoverPanelProps extends PropsWithChildren {
className?: string;
headerContent?: React.ReactNode;
}
export const PopoverPanel: React.FC = ({
className,
headerContent,
children,
}) => {
return (
{headerContent && (
{headerContent}
)}
{children}
);
};
PopoverPanel.displayName = "PopoverPanel";
================================================
FILE: src/components/Popover/index.ts
================================================
export * as PopoverContent from "./Content";
export * from "./Popover";
export * from "./PopoverButton";
export * from "./PopoverPanel";
================================================
FILE: src/components/Radio/Radio.tsx
================================================
import { Radio as HeadlessRadio } from "@headlessui/react";
import { classNames } from "../../util/classes";
import { Slot } from "../Slot";
export interface RadioProps {
value: string;
asChild?: boolean;
}
export const Radio: React.FC = ({ value, asChild }) => {
const Component = asChild ? Slot : HeadlessRadio;
return (
);
};
Radio.displayName = "Radio";
================================================
FILE: src/components/Radio/RadioGroup.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { Radio, RadioGroup, RadioGroupProps, RadioLabel } from "./index";
import { fn } from "@storybook/test";
import { useEffect, useState } from "react";
const meta: Meta = {
title: "Components/RadioGroup",
component: RadioGroup,
tags: ["autodocs"],
args: {
onChange: fn(),
value: "1",
children: (
<>
>
),
},
};
export default meta;
type Story = StoryObj;
export const Interactive: Story = {
render: (args) => {
const [value, setValue] = useState(args.value);
useEffect(() => {
setValue(args.value);
}, [args.value]);
return ;
},
};
export const WithDescription: Story = {
args: {
children: (
<>
>
),
},
render: (args) => {
const [value, setValue] = useState(args.value);
useEffect(() => {
setValue(args.value);
}, [args.value]);
return ;
},
};
================================================
FILE: src/components/Radio/RadioGroup.tsx
================================================
import { RadioGroup as HeadlessRadioGroup } from "@headlessui/react";
import { PropsWithChildren } from "react";
export interface RadioGroupProps extends PropsWithChildren {
value: string;
onChange: (value: string) => void;
}
export const RadioGroup: React.FC = ({
value,
onChange,
children,
}) => {
return (
{children}
);
};
RadioGroup.displayName = "RadioGroup";
================================================
FILE: src/components/Radio/RadioLabel.tsx
================================================
import { FieldLabel, FieldLabelProps } from "../FieldLabel";
export interface RadioLabelProps extends FieldLabelProps {}
export const RadioLabel: React.FC = (props) => {
return ;
};
RadioLabel.displayName = "RadioLabel";
================================================
FILE: src/components/Radio/index.ts
================================================
export * from "./Radio";
export * from "./RadioGroup";
export * from "./RadioLabel";
================================================
FILE: src/components/SegmentedControl/SegmentedControl.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import { SegmentedControl, SegmentedControlProps } from "./index";
const meta: Meta> = {
title: "Components/SegmentedControl",
component: SegmentedControl,
tags: ["autodocs"],
args: {
onOptionChange: fn(),
options: [
{
children: "First",
value: "first",
selectedByDefault: true,
},
{
children: "Second",
value: "second",
},
{
children: "Third",
value: "third",
},
],
},
};
export default meta;
type Story = StoryObj;
export const Simple: Story = {
args: {},
};
export const VariableTabWidth: Story = {
args: {
variableTabWidth: true,
options: Array.from(new Array(5)).map((_, i) => ({
selectedByDefault: i === 0,
children: (i + 1).toString().repeat(i + 1),
value: (i + 1).toString(),
})),
},
};
export const AsLinks: Story = {
args: {
options: [
{
children: (
First
),
value: "first",
selectedByDefault: true,
asChild: true,
},
{
children: (
Second
),
value: "second",
asChild: true,
},
{
children: (
Third
),
value: "third",
asChild: true,
},
],
},
};
export const PrimaryVariant: Story = {
args: {
variant: "primary",
variableTabWidth: true,
options: [
{
children: Home ,
value: "yeah",
selectedByDefault: true,
},
{
children: Apps ,
value: "done",
},
],
},
};
export const TagVariant: Story = {
args: {
variant: "tag",
variableTabWidth: true,
options: [
{
children: Home ,
value: "yeah",
selectedByDefault: true,
},
{
children: Apps ,
value: "done",
},
],
},
};
================================================
FILE: src/components/SegmentedControl/SegmentedControl.tsx
================================================
import React, { useEffect, useMemo, useRef, useState } from "react";
import { classNames, variantClassNames } from "../../util/classes";
import { Slot } from "../Slot";
import { useWindowSize } from "../../hooks/useWindowSize";
export type SegmentedControlProps = {
options: SegmentedControlOption[];
onOptionChange: (
option: SegmentedControlOption,
index: number
) => void;
variableTabWidth?: boolean;
variant?: "default" | "primary" | "tag";
};
export interface SegmentedControlOption {
children: React.ReactNode;
value: TOptionValue;
selectedByDefault?: boolean;
asChild?: boolean;
}
export const SegmentedControl = ({
options,
onOptionChange,
variableTabWidth,
variant = "default",
}: SegmentedControlProps) => {
const itemsRef = useRef>([]);
const [selectedOption, setSelectedOption] = useState(
options.find((opt) => opt.selectedByDefault)?.value ?? null
);
const selectedIndex = useMemo(
() => options.findIndex((opt) => opt.value === selectedOption),
[options, selectedOption]
);
const windowWidth = useWindowSize();
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
// We need to wait for the component to be mounted before we can get the
// selected element's offsetLeft and offsetWidth.
setTimeout(() => {
setIsMounted(true);
}, 0);
}, []);
const { left, width } = useMemo(() => {
if (!isMounted) {
return { left: 0, width: 0 };
}
const selectedElement = itemsRef.current[selectedIndex];
return {
left: selectedElement?.offsetLeft || 0,
width: selectedElement?.offsetWidth || 0,
};
}, [itemsRef, selectedIndex, isMounted, windowWidth]);
return (
{options.map((option, index) => {
const ButtonComponent = option.asChild ? Slot : "button";
return (
{
itemsRef.current[index] = el;
}}
key={option.value}
onClick={() => {
setSelectedOption(option.value);
onOptionChange(option, index);
}}
draggable={false}
>
{option.children}
);
})}
{isMounted && selectedOption && (
)}
);
};
SegmentedControl.displayName = "SegmentedControl";
================================================
FILE: src/components/SegmentedControl/index.ts
================================================
export * from "./SegmentedControl";
================================================
FILE: src/components/Slot/Slot.tsx
================================================
/**
* This is a modified version of Radix Primitives' Slot component.
* It supports slottable children, which is useful for components that need to
* render a slot inside a slot.
*
* It merges the initial implementation with the one that supports multiple children.
*
* See https://github.com/radix-ui/primitives/blob/main/packages/react/slot/src/Slot.tsx
* for the original implementation.
*
* See https://github.com/radix-ui/primitives/blob/12e51326c7ddc7452916aabadf7db4a45352a6bd/packages/react/slot/src/Slot.tsx
* for the variant that supports multiple children.
*/
import * as React from "react";
// https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/composeRefs.tsx
type PossibleRef = React.Ref | undefined;
function setRef(ref: PossibleRef, value: T) {
if (typeof ref === "function") {
ref(value);
} else if (ref !== null && ref !== undefined) {
(ref as React.MutableRefObject).current = value;
}
}
function composeRefs(...refs: PossibleRef[]) {
return (node: T) => refs.forEach((ref) => setRef(ref, node));
}
/* -------------------------------------------------------------------------------------------------
* Slot
* -----------------------------------------------------------------------------------------------*/
interface SlotProps extends React.HTMLAttributes {
children?: React.ReactNode;
}
const Slot = React.forwardRef((props, forwardedRef) => {
const { children, ...slotProps } = props;
if (isSlottable(children)) {
const slottable = children;
return (
{React.isValidElement>(
slottable.props.child
)
? React.cloneElement(
slottable.props.child,
undefined,
slottable.props.children(slottable.props.child.props.children)
)
: null}
);
}
return (
{children}
);
});
Slot.displayName = "Slot";
/* -------------------------------------------------------------------------------------------------
* SlotClone
* -----------------------------------------------------------------------------------------------*/
interface SlotCloneProps {
children: React.ReactNode;
}
const SlotClone = React.forwardRef(
(props, forwardedRef) => {
const { children, ...slotProps } = props;
if (React.isValidElement(children)) {
const childrenRef = getElementRef(children);
return React.cloneElement(children, {
...mergeProps(slotProps, children.props as AnyProps),
// @ts-ignore
ref: forwardedRef
? composeRefs(forwardedRef, childrenRef)
: childrenRef,
});
}
return React.Children.count(children) > 1
? React.Children.only(null)
: null;
}
);
SlotClone.displayName = "SlotClone";
/* -------------------------------------------------------------------------------------------------
* Slottable
* -----------------------------------------------------------------------------------------------*/
type SlottableProps = {
child: React.ReactNode;
children: (child: React.ReactNode) => React.JSX.Element;
};
const Slottable = ({ child, children }: SlottableProps) => {
return children(child);
};
/* ---------------------------------------------------------------------------------------------- */
type AnyProps = Record;
function isSlottable(
child: React.ReactNode
): child is React.ReactElement {
return React.isValidElement(child) && child.type === Slottable;
}
function mergeProps(slotProps: AnyProps, childProps: AnyProps) {
// all child props should override
const overrideProps = { ...childProps };
for (const propName in childProps) {
const slotPropValue = slotProps[propName];
const childPropValue = childProps[propName];
const isHandler = /^on[A-Z]/.test(propName);
if (isHandler) {
// if the handler exists on both, we compose them
if (slotPropValue && childPropValue) {
overrideProps[propName] = (...args: unknown[]) => {
childPropValue(...args);
slotPropValue(...args);
};
}
// but if it exists only on the slot, we use only this one
else if (slotPropValue) {
overrideProps[propName] = slotPropValue;
}
}
// if it's `style`, we merge them
else if (propName === "style") {
overrideProps[propName] = { ...slotPropValue, ...childPropValue };
} else if (propName === "className") {
overrideProps[propName] = [slotPropValue, childPropValue]
.filter(Boolean)
.join(" ");
}
}
return { ...slotProps, ...overrideProps };
}
// Before React 19 accessing `element.props.ref` will throw a warning and suggest using `element.ref`
// After React 19 accessing `element.ref` does the opposite.
// https://github.com/facebook/react/pull/28348
//
// Access the ref using the method that doesn't yield a warning.
function getElementRef(element: React.ReactElement) {
// React <=18 in DEV
let getter = Object.getOwnPropertyDescriptor(element.props, "ref")?.get;
let mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
if (mayWarn) {
return (element as any).ref;
}
// React 19 in DEV
getter = Object.getOwnPropertyDescriptor(element, "ref")?.get;
mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
if (mayWarn) {
return (element as any).ref;
}
// Not DEV
return (element as any).ref || (element as any).props.ref;
}
const Root = Slot;
export { Root, Slot, Slottable };
export type { SlotProps };
================================================
FILE: src/components/Slot/index.ts
================================================
export * from "./Slot";
================================================
FILE: src/components/Tag/Tag.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { Tag, type TagProps } from "./Tag";
import { InkIcon } from "../..";
import { MatrixDecorator } from "../../decorators/MatrixDecorator";
const meta: Meta = {
decorators: [
MatrixDecorator({
first: {
key: "variant",
values: ["fill", "outline", "filter", "featured"],
},
second: { key: "selected", values: [false, true] },
}),
],
title: "Components/Tag",
component: Tag,
tags: ["autodocs"],
args: {
children: "Tag Content",
},
};
export default meta;
type Story = StoryObj;
export const Default: Story = {
args: {},
};
export const WithIcon: Story = {
args: {
icon: ,
},
};
================================================
FILE: src/components/Tag/Tag.tsx
================================================
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { classNames } from "../../util/classes";
const tagVariants = cva(
"ink:inline-flex ink:font-default ink:items-center ink:gap-1 ink:flex-shrink-0 ink:rounded-full ink:text-body-3-bold ink:font-bold ink:box-border",
{
variants: {
variant: {
fill: "ink:bg-background-container ink:text-text-muted ink:h-4 ink:px-1.5",
outline:
"ink:text-text-muted ink:border-background-container ink:border-[1.5px] ink:h-4 ink:px-1.5",
filter:
"ink:text-text-muted ink:hover:text-text-default ink:duration-200 ink:cursor-pointer ink:h-5 ink:px-1.5",
featured:
"ink:bg-background-container ink:text-text-on-secondary ink:text-caption-2-bold ink:h-3 ink:px-1 ink:rounded-xs",
event:
"ink:bg-button-secondary ink:backdrop-blur-lg ink:text-text-on-primary ink:text-caption-3-bold ink:h-5 ink:px-2",
},
selected: {
true: "",
false: "",
},
hasIcon: {
true: "",
false: "",
},
},
compoundVariants: [
{
variant: "filter",
selected: true,
class: "ink:bg-background-container ink:text-text-default",
},
],
defaultVariants: {
variant: "fill",
hasIcon: false,
},
}
);
export interface TagProps
extends React.HTMLAttributes,
Omit, "hasIcon"> {
icon?: React.ReactNode;
}
export const Tag = React.forwardRef(function Tag(
{ className, variant, selected, icon, children, ...props },
ref
) {
return (
{icon && (
{icon}
)}
{children}
);
});
Tag.displayName = "Tag";
================================================
FILE: src/components/Tag/index.ts
================================================
export * from "./Tag";
================================================
FILE: src/components/Toggle/Toggle.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { Toggle, ToggleProps } from "./index";
import { fn } from "@storybook/test";
import { useEffect, useState } from "react";
const meta: Meta = {
title: "Components/Toggle",
component: Toggle,
tags: ["autodocs"],
args: {
checked: false,
onChange: fn(),
},
};
export default meta;
type Story = StoryObj;
export const Interactive: Story = {
args: {},
render: (args) => {
const [checked, setChecked] = useState(args.checked);
useEffect(() => {
setChecked(args.checked);
}, [args.checked]);
return ;
},
};
================================================
FILE: src/components/Toggle/Toggle.tsx
================================================
import { Switch } from "@headlessui/react";
import { classNames } from "../../util/classes";
export interface ToggleProps {
checked: boolean;
onChange: (enabled: boolean) => void;
}
export const Toggle: React.FC = ({ checked, onChange }) => {
return (
Toggle switch
);
};
Toggle.displayName = "Toggle";
================================================
FILE: src/components/Toggle/index.ts
================================================
export * from "./Toggle";
================================================
FILE: src/components/Typography/Typography.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { Typography, TypographyProps } from "./Typography";
const variants = [
"display-1",
"display-2",
"h1",
"h2",
"h3",
"h4",
"h5",
"body-1",
"body-2-regular",
"body-2-bold",
"body-3-regular",
"body-3-bold",
"caption-1-regular",
"caption-1-bold",
"caption-2-regular",
"caption-2-bold",
] as const;
const meta: Meta = {
title: "Design/Typography",
decorators: [
(Story, { args }) => (
{variants.map((variant) => (
ink:text-{variant} - The quick brown fox jumps over the lazy
dog
),
...args,
variant,
}}
/>
))}
),
],
component: Typography,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
args: {},
};
export default meta;
type Story = StoryObj;
export const Simple: Story = {
args: {},
};
================================================
FILE: src/components/Typography/Typography.tsx
================================================
import { classNames, variantClassNames } from "../../util/classes";
import { HTMLAttributes, PropsWithChildren } from "react";
import { Slot } from "../Slot";
export interface TypographyProps
extends PropsWithChildren,
HTMLAttributes {
variant:
| "display-1"
| "display-2"
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "body-1"
| "body-2-regular"
| "body-2-bold"
| "body-3-regular"
| "body-3-bold"
| "caption-1-regular"
| "caption-1-bold"
| "caption-2-regular"
| "caption-2-bold";
className?: string;
asChild?: boolean;
}
export const Typography: React.FC = ({
variant,
className,
children,
asChild,
...restProps
}) => {
const Component = asChild ? Slot : "div";
return (
{children}
);
};
Typography.displayName = "Typography";
================================================
FILE: src/components/Typography/index.ts
================================================
export * from "./Typography";
================================================
FILE: src/components/Wallet/ConnectWallet.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { ConnectWallet, type ConnectWalletProps } from "./index";
import { WalletProvider } from "../../decorators/WalletProvider";
import { PopoverContent } from "../Popover";
import { InkIcon } from "../..";
const meta: Meta = {
title: "Components/ConnectWallet",
decorators: [
WalletProvider,
(Story) => {Story()}
,
],
component: ConnectWallet,
tags: ["autodocs"],
argTypes: {},
args: {},
};
export default meta;
type Story = StoryObj;
export const Simple: Story = {
args: {},
};
export const WithProfileAndSettings: Story = {
args: {
listItems: (
<>
}>
Profile
}>
Settings
>
),
},
};
================================================
FILE: src/components/Wallet/ConnectWallet.tsx
================================================
import { useAccount, useBalance, useConnect, useDisconnect } from "wagmi";
import { Button } from "../Button";
import {
InkIcon,
Popover,
PopoverButton,
PopoverContent,
PopoverPanel,
} from "../..";
import { Address } from "viem";
import { trimAddress } from "../../util/trim";
import { inkSepolia } from "wagmi/chains";
import { useEnsImageOrDefault } from "../../hooks/useEnsImageOrDefault";
import { useEnsNameOrDefault } from "../../hooks/useEnsNameOrDefault";
import { PlaceholderUntilLoaded } from "../Effects";
import { useIsUnderWindowBreakpoint } from "../../hooks/useWindowBreakpoint";
import { useState } from "react";
import { useEffect } from "react";
export interface ConnectWalletProps {
className?: string;
listItems?: React.ReactNode;
}
export const ConnectWallet: React.FC = ({
className,
listItems,
}) => {
const { address, isConnected } = useAccount();
const { connect, connectors } = useConnect();
const ensName = useEnsNameOrDefault({ address });
const ensImage = useEnsImageOrDefault({ address });
const isSmallWindow = useIsUnderWindowBreakpoint({ size: "sm" });
const [hasLoaded, setHasLoaded] = useState(false);
useEffect(() => {
setHasLoaded(true);
}, []);
return (
) : undefined
}
rounded={isSmallWindow ? "full" : "default"}
>
Connecting...
···
>
}
isLoading={!hasLoaded}
>
{isConnected ? (
<>
{ensName}
>
) : (
<>
Connect
>
)}
) : undefined
}
>
{isConnected ? (
) : (
{connectors.map((connector) => (
connect({ connector })}
>
{connector.name}
))}
)}
);
};
const ConnectedWalletPopupHeader = ({ address }: { address: Address }) => {
const {
isLoading,
isSuccess,
data: balance,
} = useBalance({
address,
chainId: inkSepolia.id,
});
if (isLoading) {
return null;
}
return (
Balance
{isSuccess ? `${balance.value} ${balance.symbol}` : "..."}
);
};
const ConnectedWalletSection = ({
address,
listItems,
}: {
address: Address;
listItems?: React.ReactNode;
}) => {
const { disconnect } = useDisconnect();
return (
<>
{listItems}
}
onClick={() => navigator.clipboard.writeText(address)}
>
{trimAddress(address)}
}
onClick={() => disconnect()}
>
Disconnect
>
);
};
ConnectWallet.displayName = "ConnectWallet";
================================================
FILE: src/components/Wallet/index.ts
================================================
export * from "./ConnectWallet";
================================================
FILE: src/components/index.ts
================================================
export * from "./Alert";
export * from "./Button";
export * from "./Card";
export * from "./Checkbox";
export * from "./Input";
export * from "./Listbox";
export * from "./ListItem";
export * from "./Modal";
export * from "./Popover";
export * from "./SegmentedControl";
export * from "./Tag";
export * from "./Toggle";
export * from "./Typography";
export * from "./Wallet";
================================================
FILE: src/decorators/ContainerColor.tsx
================================================
import { Decorator } from "@storybook/react";
export const ContainerColor: Decorator = (Story) => {
return (
);
};
================================================
FILE: src/decorators/MatrixDecorator.tsx
================================================
import { Decorator } from "@storybook/react";
type MatrixDefinition = {
[P in TKey]: {
key: P;
values: Array;
};
}[TKey];
export const MatrixDecorator =
<
TProps,
TFirst extends keyof TProps = keyof TProps,
TSecond extends keyof TProps = keyof TProps,
>({
first: { key: firstKey, values: firstValues },
second: { key: secondKey, values: secondValues },
}: {
first: MatrixDefinition;
second: MatrixDefinition;
}): Decorator =>
(Story, { args, parameters }) => {
if (parameters.disableMatrix) return ;
return (
{firstValues.map((firstValue, i) => (
{secondValues.map((secondValue, j) => {
return (
);
})}
))}
);
};
================================================
FILE: src/decorators/WalletProvider.tsx
================================================
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { metaMask, mock } from "wagmi/connectors";
import { inkConfig } from "../providers.index";
import { createConfig, WagmiProvider } from "wagmi";
import { Decorator } from "@storybook/react";
import { DEFAULT_MOCK_ACCOUNT } from "../util/mocks";
const config = createConfig({
connectors: [
metaMask(),
mock({
accounts: [DEFAULT_MOCK_ACCOUNT],
features: {
defaultConnected: true,
reconnect: true,
},
}),
],
chains: inkConfig.chains,
transports: inkConfig.transports,
ssr: true,
});
const queryClient = new QueryClient();
export const WalletProvider: Decorator = (Story) => {
return (
);
};
================================================
FILE: src/global.d.ts
================================================
///
declare module "*?base64" {
const value: string;
export default value;
}
type StringWithAutocomplete = T | (string & Record);
================================================
FILE: src/hooks/index.ts
================================================
export * from "./useInkThemeClass";
================================================
FILE: src/hooks/useEnsImageOrDefault.ts
================================================
import { useEnsAvatar } from "wagmi";
import Avatar from "../images/avatar.png?base64";
import { normalize } from "viem/ens";
import { Address } from "viem";
export const useEnsImageOrDefault = ({ address }: { address?: Address }) => {
const { data: avatar } = useEnsAvatar({ name: normalize(address || "") });
return avatar || Avatar;
};
================================================
FILE: src/hooks/useEnsNameOrDefault.ts
================================================
import { Address } from "viem";
import { useEnsName } from "wagmi";
import { trimAddress } from "../util/trim";
import { DEFAULT_MOCK_ACCOUNT } from "../util/mocks";
export const useEnsNameOrDefault = ({
address,
}: {
address: Address | undefined;
}) => {
const { data: ensName } = useEnsName({ address });
if (address === DEFAULT_MOCK_ACCOUNT) {
return "mock.account.ink.eth";
}
return ensName ?? trimAddress(address);
};
================================================
FILE: src/hooks/useInkThemeClass.ts
================================================
import { useEffect } from "react";
const themeClasses = ["dark", "light", "contrast", "neo", "morpheus"] as const;
export function useInkThemeClass(
theme: "default" | (typeof themeClasses)[number]
) {
useEffect(() => {
themeClasses.forEach((t) => {
const className = `ink:${t}-theme`;
if (theme === t) {
document.documentElement.classList.add(className);
} else {
document.documentElement.classList.remove(className);
}
});
}, [theme]);
}
================================================
FILE: src/hooks/useWindowBreakpoint.ts
================================================
import { useMemo } from "react";
import { useWindowSize } from "./useWindowSize";
const BREAKPOINTS = {
sm: 640,
md: 768,
lg: 1024,
};
export const useIsUnderWindowBreakpoint = ({
size,
}: {
size: keyof typeof BREAKPOINTS;
}) => {
const { width } = useWindowSize();
return useMemo(() => {
return width < BREAKPOINTS[size];
}, [width, size]);
};
================================================
FILE: src/hooks/useWindowSize.ts
================================================
import { useState, useEffect } from "react";
interface WindowSize {
width: number;
height: number;
}
export const useWindowSize = (): WindowSize => {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return windowSize;
};
================================================
FILE: src/icons/AllIcons.css
================================================
.tooltip-on-hover {
position: relative;
}
.tooltip-on-hover:before {
display: block;
content: attr(data-title);
position: absolute;
top: 0;
transform: translateX(-50%) translateY(-125%);
background-color: var(--ink-button-primary);
color: var(--ink-text-on-primary);
padding: 4px 8px;
border-radius: 8px;
opacity: 0;
z-index: 1;
pointer-events: none;
width: 0;
left: 0;
}
.tooltip-on-hover:hover:before {
left: 50%;
transition: opacity 0.1s ease-in-out;
opacity: 1;
width: auto;
}
================================================
FILE: src/icons/AllIcons.tsx
================================================
import { classNames } from "../util/classes";
import * as Icons from "./index";
import "./AllIcons.css";
import React from "react";
interface IconsOrFolder {
[key: string]:
| React.ComponentType>
| IconsOrFolder;
}
export const AllIcons: React.FC<{
containerClassName?: string;
iconClassName?: string;
}> = ({ containerClassName, iconClassName }) => {
return (
);
};
function IconsOrFolder({
title,
iconsOrFolder,
iconClassName,
}: {
title: string;
iconsOrFolder: IconsOrFolder;
iconClassName?: string;
}) {
return (
{title}
{Object.entries(iconsOrFolder).map(([name, IconOrFolder]) => {
if (!isIconFolder(IconOrFolder)) {
return (
`}
onClick={() =>
navigator.clipboard.writeText(`<${title}.${name} />`)
}
>
);
}
})}
{Object.entries(iconsOrFolder).map(([name, IconOrFolder]) => {
if (isIconFolder(IconOrFolder)) {
return (
);
}
})}
);
}
function isIconFolder(icon: IconsOrFolder[string]): icon is IconsOrFolder {
return typeof icon === "object";
}
================================================
FILE: src/icons/Icons.stories.ts
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { AllIcons } from "./AllIcons";
const meta: Meta<{}> = {
title: "Design/Icons",
component: AllIcons,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {},
args: {},
};
export default meta;
type Story = StoryObj;
export const AllIconsRow: Story = {
args: {},
};
export const AllIconsWithColor: Story = {
args: {
containerClassName: "ink:text-button-primary",
},
};
export const AllIconsWithDifferentSize: Story = {
args: {
iconClassName: "ink:size-2",
},
};
================================================
FILE: src/icons/Logo/Placeholder.tsx
================================================
import { SVGProps, useId } from "react";
export const Placeholder = (props: SVGProps) => {
const id = useId();
return (
);
};
================================================
FILE: src/icons/Logo/index.ts
================================================
/**
* This file is auto-generated by the `import-svgs.mjs` script.
*/
export { default as Ink } from "./Ink.svg?react";
export * from "./Placeholder.tsx";
================================================
FILE: src/icons/Page/index.ts
================================================
/**
* This file is auto-generated by the `import-svgs.mjs` script.
*/
export { default as One } from "./One.svg?react";
export { default as Three } from "./Three.svg?react";
export { default as Two } from "./Two.svg?react";
================================================
FILE: src/icons/Social/index.ts
================================================
/**
* This file is auto-generated by the `import-svgs.mjs` script.
*/
export { default as Discord } from "./Discord.svg?react";
export { default as Farcaster } from "./Farcaster.svg?react";
export { default as Github } from "./Github.svg?react";
export { default as Mirror } from "./Mirror.svg?react";
export { default as Telegram } from "./Telegram.svg?react";
export { default as X } from "./X.svg?react";
================================================
FILE: src/icons/index.ts
================================================
/**
* This file is auto-generated by the `import-svgs.mjs` script.
*/
export { default as Apps } from "./Apps.svg?react";
export { default as Arrow } from "./Arrow.svg?react";
export { default as ArrowDiagonal } from "./ArrowDiagonal.svg?react";
export { default as Bridge } from "./Bridge.svg?react";
export { default as Calendar } from "./Calendar.svg?react";
export { default as Check } from "./Check.svg?react";
export { default as CheckBadge } from "./CheckBadge.svg?react";
export { default as CheckFill } from "./CheckFill.svg?react";
export { default as Chevron } from "./Chevron.svg?react";
export { default as Close } from "./Close.svg?react";
export { default as CloseSmall } from "./CloseSmall.svg?react";
export { default as Code } from "./Code.svg?react";
export { default as Copy } from "./Copy.svg?react";
export { default as Deposit } from "./Deposit.svg?react";
export { default as Disconnect } from "./Disconnect.svg?react";
export { default as Dots } from "./Dots.svg?react";
export { default as Error } from "./Error.svg?react";
export { default as Filter } from "./Filter.svg?react";
export { default as History } from "./History.svg?react";
export { default as Home } from "./Home.svg?react";
export { default as Info } from "./Info.svg?react";
export { default as InkLogo } from "./InkLogo.svg?react";
export { default as Loading } from "./Loading.svg?react";
export { default as Location } from "./Location.svg?react";
export { default as Mail } from "./Mail.svg?react";
export { default as Menu } from "./Menu.svg?react";
export { default as Minus } from "./Minus.svg?react";
export { default as Moon } from "./Moon.svg?react";
export { default as Panel } from "./Panel.svg?react";
export { default as Plus } from "./Plus.svg?react";
export { default as PlusSmall } from "./PlusSmall.svg?react";
export { default as Power } from "./Power.svg?react";
export { default as Profile } from "./Profile.svg?react";
export { default as Search } from "./Search.svg?react";
export { default as Settings } from "./Settings.svg?react";
export { default as Sun } from "./Sun.svg?react";
export { default as Swap } from "./Swap.svg?react";
export { default as Users } from "./Users.svg?react";
export { default as VerifiedIcon } from "./VerifiedIcon.svg?react";
export { default as Wallet } from "./Wallet.svg?react";
export * as Logo from "./Logo/index.ts";
export * as Page from "./Page/index.ts";
export * as Social from "./Social/index.ts";
================================================
FILE: src/index.ts
================================================
import "./tailwind.css";
export * from "./components";
export * from "./hooks";
export * as InkIcon from "./icons";
export * from "./layout";
================================================
FILE: src/layout/ForStories/ExampleDynamicContent.tsx
================================================
import { InkIcon } from "../..";
import { classNames } from "../../util/classes";
import { InkHeader } from "../InkParts";
import { InkPanel } from "../InkParts/InkPanel";
const ExamplePanel = ({
className,
text,
}: {
className?: string;
text: string;
}) => (
} />
{text}
);
export const ExampleDynamicContent = ({
className,
columns,
}: {
className?: string;
columns?: number;
}) => {
if (!columns || columns === 1)
return ;
return (
<>
{columns > 1 && }
{columns > 2 && }
>
);
};
================================================
FILE: src/layout/ForStories/ExampleLayoutLinks.tsx
================================================
import { InkIcon } from "../..";
import { InkLayoutLink } from "../InkLayout/InkNavLink";
export const EXAMPLE_LINKS: InkLayoutLink[] = [
{
children: "Home",
href: "#home",
leftIcon: ,
target: "_self",
active: true,
},
{
children: "Settings",
href: "#settings",
leftIcon: ,
target: "_self",
},
];
================================================
FILE: src/layout/ForStories/ExampleMobileNav.tsx
================================================
import { EXAMPLE_LINKS } from "./ExampleLayoutLinks";
import {
InkLayoutMobileNav,
InkLayoutMobileNavProps,
} from "../InkLayout/MobileNav";
import { useInkLayoutContext } from "../InkLayout/InkLayoutContext";
export const ExampleMobileNav = (
props: Omit
) => {
const { setIsMobileNavOpen } = useInkLayoutContext();
return (
Bottom content}
{...props}
onLinkClick={() => setIsMobileNavOpen(false)}
/>
);
};
================================================
FILE: src/layout/ForStories/ExampleSideNav.tsx
================================================
import { InkLayoutSideNav } from "../InkLayout/InkLayoutSideNav";
import { EXAMPLE_LINKS } from "./ExampleLayoutLinks";
export const ExampleSideNav = () => {
return (
Bottom content}
/>
);
};
================================================
FILE: src/layout/ForStories/ExampleTopNav.tsx
================================================
import { SegmentedControl } from "../../components/SegmentedControl";
export const ExampleTopNav = () => {
return (
{}}
/>
);
};
================================================
FILE: src/layout/InkLayout/InkLayout.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { InkIcon } from "../..";
import { InkLayout, InkLayoutProps, InkLayoutSideNav } from "./index";
import { InkPageLayout } from "../InkParts/InkPageLayout";
import { ExampleSideNav } from "../ForStories/ExampleSideNav";
import { ExampleTopNav } from "../ForStories/ExampleTopNav";
import { InkPanel } from "../InkParts/InkPanel";
import { ExampleMobileNav } from "../ForStories/ExampleMobileNav";
/**
* This layout component provides a unified layout that can be used for most pages.
*
* It's content is defined by the children prop, which can be used with the [InkPageLayout component](?path=/docs/layouts-inkpagelayout--docs)
*/
const meta: Meta = {
title: "Layouts/InkLayout",
component: InkLayout,
parameters: {
layout: "fullscreen",
},
tags: ["autodocs"],
args: {
children: (
Some content
),
headerContent: Header content
,
topNavigation: ,
sideNavigation: ,
mobileNavigation: ,
},
};
export default meta;
type Story = StoryObj;
export const Simple: Story = {
args: {},
};
/**
* The side nav can be a custom component for routing, for instance, if you want to use NextJS' own {` `} component.
*/
export const SideNavWithCustomButtons: Story = {
args: {
sideNavigation: (
Home,
href: "#home",
leftIcon: ,
target: "_self",
asChild: true,
active: true,
},
{
children: Settings ,
href: "#settings",
leftIcon: ,
target: "_self",
asChild: true,
},
]}
/>
),
children: (
The side nav can be a custom component for routing, for instance, if
you want to use NextJS' own {` `} component.
),
},
};
/**
* The side nav can be a custom component for routing, for instance, if you want to use NextJS' own {` `} component.
*/
export const StickySideNav: Story = {
args: {
children: (
If the main content is bigger than the screen, the side nav will
be sticky.
Bottom
),
},
};
================================================
FILE: src/layout/InkLayout/InkLayout.tsx
================================================
import { PropsWithChildren } from "react";
import { classNames } from "../../util/classes";
import { Button, InkIcon } from "../..";
import { useInkLayoutContext, InkLayoutProvider } from "./InkLayoutContext";
export interface InkLayoutProps extends PropsWithChildren {
className?: string;
mainIcon?: React.ReactNode;
headerContent?: React.ReactNode;
sideNavigation?: React.ReactNode;
topNavigation?: React.ReactNode;
mobileNavigation?: React.ReactNode;
/** Makes the layout really close to the edges of the screen, meaning you will have to handle the padding yourself. */
snug?: boolean;
}
export const InkLayout: React.FC = (props) => {
return (
);
};
const InkLayoutContent = ({
className,
mainIcon = ,
headerContent,
sideNavigation,
topNavigation,
mobileNavigation,
snug = false,
children,
}: InkLayoutProps) => {
const { isMobileNavOpen, setIsMobileNavOpen } = useInkLayoutContext();
return (
<>
{mainIcon}
{mobileNavigation && (
setIsMobileNavOpen(!isMobileNavOpen)}
>
{isMobileNavOpen ? : }
)}
{mainIcon}
{topNavigation && (
{topNavigation}
)}
{headerContent}
{sideNavigation && (
{sideNavigation}
)}
{children}
{isMobileNavOpen && (
{mobileNavigation}
)}
>
);
};
InkLayout.displayName = "InkLayout";
================================================
FILE: src/layout/InkLayout/InkLayoutContext.tsx
================================================
import { createContext, useContext, useEffect, useState } from "react";
const InkLayoutContext = createContext<{
isMobileNavOpen: boolean;
setIsMobileNavOpen: (isOpen: boolean) => void;
}>({
isMobileNavOpen: false,
setIsMobileNavOpen: () => {},
});
export const useInkLayoutContext = () => {
return useContext(InkLayoutContext);
};
export const InkLayoutProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);
useEffect(() => {
isMobileNavOpen
? (document.body.style.overflow = "hidden")
: (document.body.style.overflow = "auto");
}, [isMobileNavOpen]);
return (
{children}
);
};
InkLayoutProvider.displayName = "InkLayoutProvider";
================================================
FILE: src/layout/InkLayout/InkLayoutSideNav.tsx
================================================
import React from "react";
import { InkLayoutLink, InkNavLink } from "./InkNavLink";
export interface InkLayoutSideNavProps {
links: InkLayoutLink[];
bottom?: React.ReactNode;
}
export const InkLayoutSideNav: React.FC = ({
links,
bottom,
}) => {
return (
{links.map((link) => {
return ;
})}
{bottom}
);
};
InkLayoutSideNav.displayName = "InkLayoutSideNav";
================================================
FILE: src/layout/InkLayout/InkNavLink.tsx
================================================
import React from "react";
import { classNames, variantClassNames } from "../../util/classes";
import { Slot, Slottable } from "../../components/Slot";
export interface InkLayoutLink extends React.ComponentPropsWithoutRef<"a"> {
children: React.ReactNode;
onClick?: React.MouseEventHandler;
href?: string;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
target?: StringWithAutocomplete<"_blank" | "_self">;
asChild?: boolean;
active?: boolean;
}
export interface InkNavLinkProps extends InkLayoutLink {
variant?: "default" | "mobile";
}
export const InkNavLink: React.FC = ({
href,
leftIcon,
rightIcon,
children,
className = "",
asChild,
onClick,
active,
variant = "default",
...props
}) => {
const Component = asChild ? Slot : "a";
const iconClasses = classNames(
variantClassNames(variant, {
default: "ink:size-3 ink:p-0.5",
mobile: "ink:size-3",
})
);
return (
{(child) => (
<>
{leftIcon && {leftIcon}
}
{child}
{rightIcon &&
{rightIcon}
}
>
)}
);
};
InkNavLink.displayName = "InkNavLink";
================================================
FILE: src/layout/InkLayout/MobileNav/InkLayoutMobileNav.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import {
InkLayoutMobileNav,
InkLayoutMobileNavProps,
} from "./InkLayoutMobileNav";
import { EXAMPLE_LINKS } from "../../ForStories/ExampleLayoutLinks";
const meta: Meta = {
decorators: [
(Story) => (
<>
>
),
],
title: "Layouts/InkLayoutMobileNav",
component: InkLayoutMobileNav,
parameters: {
layout: "fullscreen",
},
tags: ["autodocs"],
args: {
links: EXAMPLE_LINKS,
},
};
export default meta;
type Story = StoryObj;
export const Simple: Story = {
args: {},
};
================================================
FILE: src/layout/InkLayout/MobileNav/InkLayoutMobileNav.tsx
================================================
import React from "react";
import { InkLayoutLink, InkNavLink } from "../InkNavLink";
import { InkIcon } from "../../..";
export interface InkLayoutMobileNavProps {
links: InkLayoutLink[];
onLinkClick?: React.MouseEventHandler;
bottom?: React.ReactNode;
}
export const InkLayoutMobileNav: React.FC = ({
links,
onLinkClick,
bottom,
}) => {
return (
{links.map((link) => {
return (
) : (
link.rightIcon
)
}
/>
);
})}
{bottom}
);
};
InkLayoutMobileNav.displayName = "InkLayoutMobileNav";
================================================
FILE: src/layout/InkLayout/MobileNav/index.ts
================================================
export * from "./InkLayoutMobileNav";
================================================
FILE: src/layout/InkLayout/index.ts
================================================
export * from "./InkLayout";
export { useInkLayoutContext } from "./InkLayoutContext";
export * from "./InkLayoutSideNav";
export * from "./InkNavLink";
export * from "./MobileNav";
================================================
FILE: src/layout/InkParts/InkHeader.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { InkHeader, InkHeaderProps } from "./InkHeader";
import { InkIcon } from "../..";
/**
* This component provides a unified header that can be used at the top of a page or a modal.
*/
const meta: Meta = {
decorators: [
(Story) => (
),
],
title: "Layouts/InkHeader",
component: InkHeader,
tags: ["autodocs"],
args: {
title: "Example Title",
},
};
export default meta;
type Story = StoryObj;
export const Simple: Story = {
args: {},
};
export const WithIcon: Story = {
args: {
icon: ,
},
};
================================================
FILE: src/layout/InkParts/InkHeader.tsx
================================================
import { PropsWithChildren } from "react";
import { classNames } from "../../util/classes";
export interface InkHeaderProps extends PropsWithChildren {
title: React.ReactNode;
children?: React.ReactNode;
icon?: React.ReactNode;
}
export const InkHeader: React.FC = ({
title,
icon,
children,
}) => {
return (
{title}
{children}
{icon}
);
};
InkHeader.displayName = "InkHeader";
================================================
FILE: src/layout/InkParts/InkPageLayout.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { InkPageLayout, InkPageLayoutProps } from "./InkPageLayout";
import { InkLayout } from "../InkLayout";
import { ExampleDynamicContent } from "../ForStories/ExampleDynamicContent";
/**
* This component provides a column layout for a page.
* The `columns` prop determines the number of columns to display. You must then pass the same number of children to the component.
*
* Note that the `InkLayout` component is included only as an example. It is not required for this component to function.
*/
const meta: Meta = {
parameters: {
layout: "fullscreen",
},
decorators: [
(Story, { args }) => (
Side Navigation}
headerContent={Header Content
}
>
),
}}
/>
),
],
title: "Layouts/InkPageLayout",
component: InkPageLayout,
tags: ["autodocs"],
args: {
columns: 1,
},
};
export default meta;
type Story = StoryObj;
export const Simple: Story = {
args: {},
};
================================================
FILE: src/layout/InkParts/InkPageLayout.tsx
================================================
import { PropsWithChildren } from "react";
import { classNames, variantClassNames } from "../../util/classes";
export interface InkPageLayoutProps extends PropsWithChildren {
columns?: 1 | 2 | 3;
}
export const InkPageLayout: React.FC = ({
columns = 1,
children,
}) => {
return (
{children}
);
};
InkPageLayout.displayName = "InkPageLayout";
================================================
FILE: src/layout/InkParts/InkPanel.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { InkHeader, InkIcon, InkPanel, InkPanelProps } from "../..";
/**
* This component provides a simple layout container with a header and a content area.
*/
const meta: Meta = {
title: "Layouts/InkPanel",
component: InkPanel,
tags: ["autodocs"],
args: {
size: "md",
children: (
<>
}
/>
And then some text here, how fun! And some more!
Some footer
>
),
},
};
export default meta;
type Story = StoryObj;
export const Simple: Story = {
args: {},
};
/**
* Centering the content will make the content centered, but the header will still be at the top.
*/
export const Centered: Story = {
args: {
centered: true,
},
};
/**
* The automatic size will make the panel take space depending on the content.
*/
export const AutomaticSize: Story = {
args: {
size: "auto",
},
};
/**
* Centering the content will make the content centered, but the header will still be at the top.
*/
export const CenteredWithoutHeader: Story = {
args: {
centered: true,
children: Just some content here
,
},
};
================================================
FILE: src/layout/InkParts/InkPanel.tsx
================================================
import { PropsWithChildren } from "react";
import { classNames, variantClassNames } from "../../util/classes";
import { forwardRef } from "react";
export interface InkPanelProps extends PropsWithChildren {
className?: string;
size?: "auto" | "lg" | "md";
centered?: boolean;
shadow?: boolean;
}
export const InkPanel = forwardRef(
(
{ className, size = "auto", centered = false, shadow = false, children },
ref
) => {
return (
{children}
);
}
);
InkPanel.displayName = "InkPanel";
================================================
FILE: src/layout/InkParts/index.ts
================================================
export * from "./InkHeader";
export * from "./InkPageLayout";
export * from "./InkPanel";
================================================
FILE: src/layout/index.ts
================================================
export * from "./InkLayout";
export * from "./InkParts";
================================================
FILE: src/providers.index.ts
================================================
import { Chain } from "viem";
import { http, Transport } from "wagmi";
import { inkSepolia } from "wagmi/chains";
const chains = [inkSepolia] as const satisfies Chain[];
const transports = {
[inkSepolia.id]: http(),
} as const satisfies Record;
export const inkConfig = {
chains,
transports,
} as const;
================================================
FILE: src/stories/Welcome.mdx
================================================
import { Meta } from "@storybook/blocks";
import Banner from "../images/banner.webp?base64";
# Welcome to Ink Kit
Ink Kit is an onchain-focused SDK that delivers a delightful developer experience with ready-to-use app layout templates, themes, and magical animated components.
## Install
```bash
npm install @inkonchain/ink-kit
# or
pnpm install @inkonchain/ink-kit
```
## Usage
```tsx
// Import styles first at the root of your project (required)
import "@inkonchain/ink-kit/style.css";
```
```tsx
// Import components as needed
import { Button } from "@inkonchain/ink-kit";
function App() {
return (
{}} size="md" variant="secondary">
Ship It
);
}
```
Note: Ink Kit classes are prefixed with `ink:` and can be customized using CSS variables instead of Tailwind classes. They should be imported first so that your own custom classes are taking precedence.
## Key Features
- 🎨 **Customizable app layout templates**
- ✨ **Magical animated components**
- 🎭 **Vibrant themes**
- ⛓️ **Onchain-focused development**
- 🚀 **Efficient developer experience**
- 📱 **Polished, engaging interfaces**
## Theming
By default, Ink Kit provides a couple of themes already in the stylesheet:
- Light (`light-theme`)
- Dark (`dark-theme`)
- Contrast (`contrast-theme`)
- Neo (`neo-theme`)
- Morpheus (`morpheus-theme`)
To specify which theme to use, add the `ink:THEME_ID` to your document root:
```tsx
...
```
If you want to programmatically set this value, you can use the `useInkThemeClass`:
```tsx
const theme = getMyCurrentTheme();
useInkThemeClass(theme === "light" ? "ink:neo-theme" : "ink:dark-theme");
```
### Custom Theme
To create a custom theme, you can override CSS variables:
```css
:root {
--ink-button-primary: rgb(10, 55, 10);
...
}
```
To see examples on specific colors that you can override, check the following [theme](https://github.com/inkonchain/ink-kit/tree/main/src/styles/theme) section of the Ink Kit repository.
## Resources
- **Documentation**: Browse components and examples in the sidebar
- **Contributing**: Visit our [GitHub repository](https://github.com/inkonchain/ink-kit)
## WIP Notice
This is a work in progress: we are constantly adding new components, improving the developer experience, and fixing bugs.
================================================
FILE: src/styles/Colors.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { classNames } from "../util/classes";
function Colors() {
const colors = [
"ink:bg-button-primary ink:text-text-on-primary",
"ink:bg-button-secondary ink:text-text-on-secondary",
"ink:bg-background-dark",
"ink:bg-background-dark-transparent",
"ink:bg-background-light",
"ink:bg-background-light-transparent",
"ink:bg-background-light-invisible",
"ink:bg-background-container",
"ink:bg-status-success-bg ink:text-status-success",
"ink:bg-status-error-bg ink:text-status-error",
"ink:bg-status-alert-bg ink:text-status-alert",
];
const independentColors = [
"ink:bg-ink-light-purple ink:text-text-on-primary",
"ink:bg-ink-dark-purple ink:text-text-on-primary",
];
return (
Colors
{colors.map((color) => (
{color}
))}
Theme-Independent Colors
{independentColors.map((color) => (
{color}
))}
);
}
const meta: Meta = {
title: "Design/Colors",
component: Colors,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj;
export const Simple: Story = {
args: {},
};
================================================
FILE: src/styles/Shadows.stories.tsx
================================================
import type { Meta, StoryObj } from "@storybook/react";
import { classNames } from "../util/classes";
function Shadows() {
const shadows = ["ink:shadow-xs", "ink:shadow-md", "ink:shadow-lg"];
return (
{shadows.map((sh) => (
{sh}
))}
);
}
const meta: Meta = {
title: "Design/Shadows",
component: Shadows,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj;
export const Simple: Story = {
args: {},
};
================================================
FILE: src/styles/theme/colors.base.css
================================================
/*
Colors in this file are automatically computed using the other variables.
You can override them in a theme if you need specific colors, but these should look good without it.
*/
:root {
--ink-base-font-default: "Plus Jakarta Sans";
/* Background */
--ink-background-dark-transparent: color-mix(
in srgb,
var(--ink-background-dark) 20%,
transparent
);
--ink-background-light-transparent: color-mix(
in srgb,
var(--ink-background-light) 50%,
transparent
);
--ink-background-light-invisible: color-mix(
in srgb,
var(--ink-background-light) 0%,
transparent
);
--ink-background-container: color-mix(
in srgb,
var(--ink-button-primary) 6%,
transparent
);
/* Button */
--ink-button-primary-hover: color-mix(
in srgb,
var(--ink-button-primary) 90%,
transparent
);
--ink-button-primary-pressed: color-mix(
in srgb,
var(--ink-button-primary) 80%,
transparent
);
--ink-button-secondary: color-mix(
in srgb,
var(--ink-button-primary) 6%,
transparent
);
--ink-button-secondary-hover: color-mix(
in srgb,
var(--ink-button-secondary) 50%,
transparent
);
--ink-button-secondary-pressed: color-mix(
in srgb,
var(--ink-button-secondary-hover) 50%,
transparent
);
/* Text */
--ink-text-muted: color-mix(
in srgb,
var(--ink-text-default) 50%,
transparent
);
--ink-text-on-primary-disabled: color-mix(
in srgb,
var(--ink-text-on-primary) 40%,
transparent
);
--ink-text-on-secondary-disabled: color-mix(
in srgb,
var(--ink-text-on-secondary) 50%,
transparent
);
/* Status */
--ink-status-success-bg: color-mix(
in srgb,
var(--ink-status-success) 8%,
transparent
);
--ink-status-alert-bg: color-mix(
in srgb,
var(--ink-status-alert) 8%,
transparent
);
--ink-status-error-bg: color-mix(
in srgb,
var(--ink-status-error) 8%,
transparent
);
/* Radius */
--ink-base-radius-full: 9999px;
--ink-base-radius-xxl: 64px;
--ink-base-radius-xl: 48px;
--ink-base-radius-lg: 24px;
--ink-base-radius-md: 16px;
--ink-base-radius-sm: 12px;
--ink-base-radius-xs: 8px;
/* Shadows */
--ink-base-shadow-xs-color: color-mix(
in srgb,
var(--ink-background-shadow) 6%,
transparent
);
--ink-base-shadow-md-color: color-mix(
in srgb,
var(--ink-background-shadow) 8%,
transparent
);
--ink-base-shadow-lg-color: color-mix(
in srgb,
var(--ink-background-shadow) 10%,
transparent
);
/* Blur */
--ink-base-blur-sm: 48px;
--ink-base-blur-lg: 128px;
/* Independent Colors */
--ink-color-light-purple: #b9aaef;
--ink-color-dark-purple: #5c479d;
}
================================================
FILE: src/styles/theme/colors.contrast.css
================================================
:root,
:root.ink\:contrast-theme {
/* Background */
--ink-background-dark: rgba(221, 221, 221, 1);
--ink-background-light: rgba(255, 255, 255, 1);
--ink-background-container: rgba(0, 0, 0, 0.1);
--ink-background-shadow: rgba(0, 0, 0, 1);
/* Button */
--ink-button-primary: rgba(0, 0, 0, 1);
/* Text */
--ink-text-default: rgba(0, 0, 0, 1);
--ink-text-on-primary: rgba(255, 255, 255, 1);
--ink-text-on-secondary: rgba(0, 0, 0, 1);
/* Status */
--ink-status-success: rgba(61, 166, 103, 1);
--ink-status-alert: rgba(231, 149, 74, 1);
--ink-status-error: rgba(236, 109, 109, 1);
}
================================================
FILE: src/styles/theme/colors.dark.css
================================================
:root,
:root.ink\:dark-theme {
/* Background */
--ink-background-dark: rgba(18, 17, 24);
--ink-background-light: rgb(45, 45, 52);
--ink-background-container: rgba(187, 180, 255, 0.05);
/* Button */
--ink-button-primary: rgb(113, 50, 245);
/* Text */
--ink-text-default: rgb(255, 255, 255);
--ink-text-on-primary: rgba(255, 255, 255, 1);
--ink-text-on-secondary: rgba(138, 97, 255, 1);
/* Status */
--ink-status-success: rgb(61, 166, 103);
--ink-status-alert: rgb(231, 149, 74);
--ink-status-error: rgb(236, 109, 109);
}
/*
This allows us to use the dark theme over the light theme by default, depending on user preference.
The overrides should _exactly_ match the variables above.
*/
@media (prefers-color-scheme: dark) {
:root {
/* Background */
--ink-background-dark: rgba(18, 17, 24);
--ink-background-light: rgb(45, 45, 52);
--ink-background-container: rgba(187, 180, 255, 0.05);
/* Button */
--ink-button-primary: rgb(113, 50, 245);
/* Text */
--ink-text-default: rgb(255, 255, 255);
--ink-text-on-primary: rgba(255, 255, 255, 1);
--ink-text-on-secondary: rgba(138, 97, 255, 1);
/* Status */
--ink-status-success: rgb(61, 166, 103);
--ink-status-alert: rgb(231, 149, 74);
--ink-status-error: rgb(236, 109, 109);
}
}
================================================
FILE: src/styles/theme/colors.light.css
================================================
:root,
:root.ink\:light-theme {
/* Background */
--ink-background-dark: rgba(244, 243, 249);
--ink-background-light: rgb(255, 255, 255);
--ink-background-shadow: rgba(22, 15, 31, 1);
/* Button */
--ink-button-primary: rgb(113, 50, 245);
/* Text */
--ink-text-default: rgb(22, 15, 31);
--ink-text-on-primary: rgb(255, 255, 255);
--ink-text-on-secondary: rgb(113, 50, 245);
/* Status */
--ink-status-success: rgb(61, 166, 103);
--ink-status-alert: rgb(231, 149, 74);
--ink-status-error: rgb(236, 109, 109);
}
================================================
FILE: src/styles/theme/colors.morpheus.css
================================================
:root,
:root.ink\:morpheus-theme {
/* Background */
--ink-background-dark: rgba(15, 13, 35, 1);
--ink-background-light: rgba(27, 23, 73, 1);
--ink-background-container: rgba(45, 39, 104, 1);
--ink-background-shadow: rgba(15, 13, 35, 1);
/* Button */
--ink-button-primary: rgba(205, 54, 96, 1);
--ink-button-secondary: rgba(0, 106, 182, 1);
/* Text */
--ink-text-default: rgba(255, 255, 255);
--ink-text-on-primary: rgba(255, 255, 255, 1);
--ink-text-on-secondary: rgba(255, 255, 255, 1);
/* Status */
--ink-status-success: rgba(0, 106, 182, 1);
--ink-status-alert: rgba(254, 185, 6, 1);
--ink-status-error: rgba(205, 54, 96, 1);
}
================================================
FILE: src/styles/theme/colors.neo.css
================================================
:root,
:root.ink\:neo-theme {
--ink-base-font-default: "Departure Mono";
/* Background */
--ink-background-dark: rgb(7, 9, 8);
--ink-background-light: rgb(19, 28, 23);
--ink-background-container: rgb(30, 46, 37);
--ink-background-shadow: rgba(7, 9, 8, 1);
/* Button */
--ink-button-primary: rgba(6, 254, 110);
/* Text */
--ink-text-default: rgba(255, 255, 255);
--ink-text-muted: rgba(6, 254, 110, 0.5);
--ink-text-on-primary: rgba(7, 9, 8);
--ink-text-on-secondary: rgba(6, 254, 110);
/* Status */
--ink-status-success: rgba(6, 254, 110);
--ink-status-alert: rgba(254, 185, 6);
--ink-status-error: rgba(248, 97, 97);
--ink-base-radius-full: 0px;
--ink-base-radius-xxl: 0px;
--ink-base-radius-xl: 0px;
--ink-base-radius-lg: 0px;
--ink-base-radius-md: 0px;
--ink-base-radius-sm: 0px;
--ink-base-radius-xs: 0px;
}
================================================
FILE: src/tailwind.css
================================================
@import url("./styles/theme/colors.dark.css") layer(ink-theme);
@import url("./styles/theme/colors.neo.css") layer(ink-theme);
@import url("./styles/theme/colors.morpheus.css") layer(ink-theme);
@import url("./styles/theme/colors.contrast.css") layer(ink-theme);
@import url("./styles/theme/colors.light.css") layer(ink-theme);
@import url("./styles/theme/colors.base.css") layer(ink-theme);
/* We still need those directive to import the CSS in a Tailwind V3 package */
@tailwind base;
@tailwind utilities;
@import "tailwindcss/theme" prefix(ink);
@import "tailwindcss/utilities" prefix(ink);
@layer base {
@font-face {
font-family: "Plus Jakarta Sans";
src: url("./styles/fonts/PlusJakartaSans-VariableFont_wght.ttf");
}
@font-face {
font-family: "Departure Mono";
src: url("./styles/fonts/DepartureMono-Regular.woff");
}
input,
textarea,
select,
button {
border: 0px solid;
border-radius: 0;
padding: 0;
color: inherit;
background-color: transparent;
}
input,
textarea,
select,
button {
font-family: inherit;
font-feature-settings: inherit;
font-variation-settings: inherit;
font-size: 100%;
font-weight: inherit;
line-height: inherit;
letter-spacing: inherit;
color: inherit;
margin: 0;
padding: 0;
}
.ink\:font-default * {
/** Required for Safari to have proper font weights */
font-synthesis: none;
}
@keyframes svg-path-dash {
from {
stroke-dashoffset: var(--svg-path-dash-offset);
}
to {
stroke-dashoffset: 0;
}
}
}
@theme {
/* Fonts */
--font-*: initial;
--font-default: var(--ink-base-font-default);
/* Radius */
--radius-*: initial;
--radius-full: var(--ink-base-radius-full);
--radius-xxl: var(--ink-base-radius-xxl);
--radius-xl: var(--ink-base-radius-xl);
--radius-lg: var(--ink-base-radius-lg);
--radius-md: var(--ink-base-radius-md);
--radius-sm: var(--ink-base-radius-sm);
--radius-xs: var(--ink-base-radius-xs);
/* Colors */
--color-*: initial;
--color-background-dark: var(--ink-background-dark);
--color-background-dark-transparent: var(--ink-background-dark-transparent);
--color-background-container: var(--ink-background-container);
--color-background-light: var(--ink-background-light);
--color-background-light-transparent: var(--ink-background-light-transparent);
--color-background-light-invisible: var(--ink-background-light-invisible);
--color-background-shadow: var(--ink-background-shadow);
--color-button-primary: var(--ink-button-primary);
--color-button-primary-hover: var(--ink-button-primary-hover);
--color-button-primary-pressed: var(--ink-button-primary-pressed);
--color-button-secondary: var(--ink-button-secondary);
--color-button-secondary-hover: var(--ink-button-secondary-hover);
--color-button-secondary-pressed: var(--ink-button-secondary-pressed);
--color-text-default: var(--ink-text-default);
--color-text-muted: var(--ink-text-muted);
--color-text-on-primary: var(--ink-text-on-primary);
--color-text-on-primary-disabled: var(--ink-text-on-primary-disabled);
--color-text-on-secondary: var(--ink-text-on-secondary);
--color-text-on-secondary-disabled: var(--ink-text-on-secondary-disabled);
--color-status-success: var(--ink-status-success);
--color-status-success-bg: var(--ink-status-success-bg);
--color-status-alert: var(--ink-status-alert);
--color-status-alert-bg: var(--ink-status-alert-bg);
--color-status-error: var(--ink-status-error);
--color-status-error-bg: var(--ink-status-error-bg);
--color-default-app-icon-gradient: var(--ink-default-app-icon-gradient);
/* Independent Colors */
--color-ink-light-purple: var(--ink-color-light-purple);
--color-ink-dark-purple: var(--ink-color-dark-purple);
/* Typography */
--text-*: initial;
--text-display-1: 118px;
--text-display-1--line-height: 112px;
--text-display-1--font-weight: 500;
--text-display-2: 96px;
--text-display-2--line-height: 96px;
--text-display-2--font-weight: 500;
--text-h1: 72px;
--text-h1--line-height: 72px;
--text-h1--font-weight: 500;
--text-h2: 48px;
--text-h2--line-height: 48px;
--text-h2--font-weight: 500;
--text-h3: 32px;
--text-h3--line-height: 36px;
--text-h3--font-weight: 700;
--text-h4: 24px;
--text-h4--line-height: 28px;
--text-h4--font-weight: 700;
--text-h5: 20px;
--text-h5--line-height: 24px;
--text-h5--font-weight: 700;
--text-body-1-regular: 18px;
--text-body-1-regular--line-height: 24px;
--text-body-1-regular--font-weight: 400;
--text-body-1-bold: 18px;
--text-body-1-bold--line-height: 24px;
--text-body-1-bold--font-weight: 600;
--text-body-2-regular: 16px;
--text-body-2-regular--line-height: 20px;
--text-body-2-regular--font-weight: 400;
--text-body-2-bold: 16px;
--text-body-2-bold--line-height: 20px;
--text-body-2-bold--font-weight: 700;
--text-body-3-regular: 14px;
--text-body-3-regular--line-height: 18px;
--text-body-3-regular--font-weight: 400;
--text-body-3-bold: 14px;
--text-body-3-bold--line-height: 18px;
--text-body-3-bold--font-weight: 700;
--text-caption-1-regular: 12px;
--text-caption-1-regular--line-height: 16px;
--text-caption-1-regular--font-weight: 400;
--text-caption-1-bold: 12px;
--text-caption-1-bold--line-height: 16px;
--text-caption-1-bold--font-weight: 700;
--text-caption-2-regular: 11px;
--text-caption-2-regular--line-height: 16px;
--text-caption-2-regular--font-weight: 400;
--text-caption-2-bold: 11px;
--text-caption-2-bold--line-height: 16px;
--text-caption-2-bold--font-weight: 700;
/* Spacing */
--spacing-*: initial;
--spacing-0: 0px;
--spacing-0_5: 4px;
--spacing-1: 8px;
--spacing-1_5: 12px;
--spacing-2: 16px;
--spacing-3: 24px;
--spacing-4: 32px;
--spacing-5: 40px;
--spacing-6: 48px;
--spacing-8: 64px;
--spacing-12: 96px;
--spacing-16: 128px;
/* Shadows */
--shadow-*: initial;
--shadow-xs: 0px 4px 8px -2px var(--ink-base-shadow-xs-color);
--shadow-md: 0px 12px 16px -4px var(--ink-base-shadow-md-color);
--shadow-lg: 0px 32px 64px -12px var(--ink-base-shadow-lg-color);
/* Blur */
--blur-*: initial;
--blur-sm: var(--ink-base-blur-sm);
--blur-lg: var(--ink-base-blur-lg);
/* Rotation */
--rotation-225: 225deg;
--rotation-270: 270deg;
}
@utility transition-default-animation {
@apply ink:duration-200 ink:ease-in-out;
}
/**
* For this animation to work, the SVG path must have a stroke attribute.
* Set `animate-svg-path` on the Icon element, then apply `animate-svg-path-start` when you want the animation to play (e.g. on hover).
*/
@utility animate-svg-path {
--svg-path-dash-duration: 0.25s;
--svg-path-dash-offset: 24;
--svg-path-dash-delay: 0.1s;
--svg-path-dash-easing: ease-in-out;
> path[stroke] {
stroke-dasharray: var(--svg-path-dash-offset);
stroke-dashoffset: var(--svg-path-dash-offset);
}
}
@utility animate-svg-path-start {
> path[stroke] {
animation: svg-path-dash var(--svg-path-dash-duration)
var(--svg-path-dash-delay) var(--svg-path-dash-easing) forwards;
}
}
================================================
FILE: src/util/classes.ts
================================================
import { type ClassValue, clsx } from "clsx";
import { extendTailwindMerge } from "tailwind-merge";
const colors = [
"text-default",
"text-muted",
"text-on-primary",
"text-on-primary-disabled",
"text-on-secondary",
"text-on-secondary-disabled",
"background-dark",
"background-dark-transparent",
"background-container",
"background-light",
"background-light-transparent",
"background-light-invisible",
"button-primary",
"button-primary-hover",
"button-primary-pressed",
"button-secondary",
"button-secondary-hover",
"button-secondary-pressed",
"status-success",
"status-success-bg",
"status-alert",
"status-alert-bg",
"status-error",
"status-error-bg",
];
const customTwMerge = extendTailwindMerge({
override: {
classGroups: {
rounded: [
"rounded-xs",
"rounded-sm",
"rounded-md",
"rounded-lg",
"rounded-full",
],
"font-size": [
"text-h1",
"text-h2",
"text-h3",
"text-h4",
"text-body-1-regular",
"text-body-1-bold",
"text-body-2-regular",
"text-body-2-bold",
"text-body-3-regular",
"text-body-3-bold",
"text-caption-1-regular",
"text-caption-1-bold",
"text-caption-2",
],
"text-color": colors.map((color) => `text-${color}`),
"bg-color": colors.map((color) => `bg-${color}`),
"border-color": colors.map((color) => `border-${color}`),
"ring-color": colors.map((color) => `ring-${color}`),
"shadow-color": colors.map((color) => `shadow-${color}`),
},
},
});
export function classNames(...classes: ClassValue[]) {
return customTwMerge(clsx(...classes));
}
export function variantClassNames(
variant: T,
classes: Required>
) {
return classes[variant];
}
================================================
FILE: src/util/mocks.ts
================================================
export const DEFAULT_MOCK_ACCOUNT =
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
================================================
FILE: src/util/trim.ts
================================================
import { Address } from "viem";
export const trimAddress = (address?: Address) => {
if (!address || address.length < 10) return "";
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES5",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"allowJs": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"declaration": true
},
"include": ["src"],
"exclude": ["src/**/*.stories.ts"]
}
================================================
FILE: vite.config.mts
================================================
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
import svgr from "vite-plugin-svgr";
import preserveUseClientDirective from "rollup-plugin-preserve-use-client";
import tailwindcss from "@tailwindcss/vite";
import { peerDependencies } from "./package.json";
import { Plugin } from "vite";
import fs from "fs";
const base64Loader: Plugin = {
name: "base64-loader",
transform(_: any, id: string) {
const [path, query] = id.split("?");
if (query != "base64") return null;
const data = fs.readFileSync(path);
const base64 = data.toString("base64");
const extension = path.split(".").pop();
return `export default 'data:image/${extension};base64,${base64}';`;
},
};
export default defineConfig({
build: {
lib: {
entry: {
index: "./src/index.ts",
providers: "./src/providers.index.ts",
},
name: "vite-react-ts-button",
fileName: (format, entryName) => `${entryName}.${format}.js`,
formats: ["cjs", "es"],
},
rollupOptions: {
/** "react/jsx-runtime" is needed to support both React 18 and 19, plus it makes the bundle smaller */
external: [
"react/jsx-runtime",
"wagmi/connectors",
"wagmi/chains",
...Object.keys(peerDependencies),
],
},
sourcemap: true,
emptyOutDir: true,
},
plugins: [
preserveUseClientDirective(),
tailwindcss(),
dts(),
svgr({
include: "**/*.svg?react",
}),
base64Loader,
],
});