Showing preview only (215K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<img src="../src/images/banner.webp" alt="Ink Kit Banner" style="width: 100%; border-radius: 8px; margin-bottom: 2rem;" />
# 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 (
<div>
<Button onClick={() => {}} size="md" variant="secondary">
Ship It
</Button>
</div>
);
}
```
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
<html class="ink:dark-theme">
...
```
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
================================================
<link rel="icon" type="image/x-icon" href="favicon.ico" />
================================================
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<ReactRenderer>({
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<AlertProps> = {
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<typeof meta>;
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: <InkIcon.Settings />,
},
};
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<AlertProps> = ({
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: <InkIcon.Check />,
error: <InkIcon.Error />,
warning: <InkIcon.Error />,
info: <InkIcon.Settings />,
}[variant];
const handleDismiss = () => {
if (dismissible && id) {
localStorage.setItem(`ink-alert-${id}`, "true");
setIsDismissed(true);
onDismiss?.();
}
};
return (
<div
className={classNames(
"ink:flex ink:gap-3 ink:p-3 ink:rounded-md ink:font-default",
variantClassNames(variant, {
success: "ink:bg-status-success-bg ink:text-status-success",
error: "ink:bg-status-error-bg ink:text-status-error",
warning: "ink:bg-status-alert-bg ink:text-status-alert",
info: "ink:bg-background-light ink:text-text-default",
}),
className
)}
>
<div className="ink:size-4 ink:shrink-0">{icon || defaultIcon}</div>
<div className="ink:flex ink:flex-col ink:gap-1 ink:flex-1">
<div className="ink:text-body-2-bold">{title}</div>
{description && (
<div className="ink:text-body-2-regular">{description}</div>
)}
</div>
{dismissible && (
<button
onClick={handleDismiss}
className="ink:size-4 ink:shrink-0 ink:opacity-60 hover:ink:opacity-100 ink:cursor-pointer"
aria-label="Dismiss alert"
>
<InkIcon.Close />
</button>
)}
</div>
);
};
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<ButtonProps> = {
title: "Components/Button",
decorators: [
MatrixDecorator<ButtonProps>({
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<typeof meta>;
export const Simple: Story = {
args: {
children: "Button",
},
};
export const Disabled: Story = {
args: {
disabled: true,
children: "Button",
},
};
export const WithIcon: Story = {
args: {
children: "Button",
iconLeft: <InkIcon.Deposit />,
},
};
export const Rounded: Story = {
args: {
rounded: "full",
children: <InkIcon.Deposit />,
},
};
export const WithMinimumWidth: Story = {
args: {
className: "ink:min-w-[350px]",
children: "Button",
iconLeft: <InkIcon.Deposit />,
},
};
export const AsLink: Story = {
args: {
asChild: true,
children: (
<a href="https://inkonchain.com" target="_blank">
inkonchain.com
</a>
),
iconRight: <InkIcon.Arrow className="ink:rotate-[225deg]" />,
},
};
export const WalletVariant: Story = {
decorators: [
(Story, { args }) => (
<div className="ink:flex ink:flex-col ink:items-center ink:justify-center ink:gap-2">
<Story args={{ ...args, size: "md" }} />
<Story args={{ ...args, size: "lg" }} />
</div>
),
],
parameters: { disableMatrix: true },
args: {
variant: "wallet",
children: <div>Wallet</div>,
iconLeft: (
<img
src={Avatar}
alt="avatar"
className="ink:object-cover ink:w-full ink:h-full ink:rounded-full"
/>
),
},
};
================================================
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<HTMLButtonElement> {
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<HTMLButtonElement, ButtonProps>(
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 (
<Component
className={classNames(
"ink:rounded-full ink:font-default ink:transition-colors ink:hover:cursor-pointer ink:disabled:cursor-not-allowed ink:transition-default-animation ink:box-border ink:backdrop-blur-lg",
"ink:flex ink:items-center ink:justify-center ink:gap-1 ink:shrink-0 ink:select-none ink:no-underline",
variantClassNames(size, {
md: "ink:px-2 ink:py-1.5 ink:text-body-3-bold ink:h-5",
lg: "ink:px-4 ink:py-3 ink:text-h5 ink:h-8",
}),
variantClassNames(rounded, {
full: `ink:rounded-full ${variantClassNames(size, {
md: "ink:p-1 ink:size-5",
lg: "ink:p-2 ink:size-8",
})}`,
default: "",
}),
variantClassNames(variant, {
primary:
"ink:bg-button-primary ink:text-text-on-primary ink:hover:bg-button-primary-hover ink:disabled:bg-button-primary-disabled ink:disabled:text-text-on-primary-disabled ink:active:bg-button-primary-pressed",
secondary:
"ink:bg-button-secondary ink:text-text-on-secondary ink:hover:bg-button-secondary-hover ink:disabled:bg-button-secondary-disabled ink:disabled:text-text-on-secondary-disabled ink:active:bg-button-secondary-pressed",
wallet: classNames(
"ink:bg-background-light-transparent ink:text-body-2-bold ink:text-text-default ink:hover:bg-background-light ink:disabled:bg-background-light-transparent-disabled ink:disabled:text-muted ink:active:bg-background-light",
"ink:border-background-container ink:border",
iconLeft &&
variantClassNames(size, {
md: "ink:pl-0.5",
lg: "ink:pl-1",
}),
iconRight &&
variantClassNames(size, {
md: "ink:pr-0.5",
lg: "ink:pr-1",
})
),
transparent:
"ink:bg-transparent ink:text-text-default ink:hover:bg-background-light-transparent ink:disabled:bg-transparent ink:disabled:text-muted",
}),
className
)}
{...restProps}
ref={ref}
>
<Slottable child={children}>
{(child) => (
<>
{iconLeft && <div className={iconClasses}>{iconLeft}</div>}
{child}
{iconRight && <div className={iconClasses}>{iconRight}</div>}
</>
)}
</Slottable>
</Component>
);
}
);
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<CardProps> = {
title: "Components/Card",
component: Card,
tags: ["autodocs"],
argTypes: {},
args: {
children: (
<CardContent.CallToAction
title="Card Example"
description="Ever wondered why keyboards aren't arranged in alphabetical order? Probably because someone in the 1870s had a really good time watching people hunt and peck for letters. QWERTY layout: the original practical joke that became a global standard."
button={
<Button variant="primary" size="lg">
Button
</Button>
}
/>
),
image: (
<CardContent.Image
mainLabels={
<>
<Tag variant="event">Tag 1</Tag>
<Tag variant="event">Tag 2</Tag>
</>
}
>
<img
src="https://picsum.photos/1024/576"
alt="Card Image"
width={1024}
height={580}
/>
</CardContent.Image>
),
imageLocation: "left",
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Basic: Story = {
args: {},
};
export const ImageOnTheRight: Story = {
args: {
children: (
<TitleAndDescription
title="Image on the right"
description="Why did the image move to the right? Because it was tired of being left out! Now it can finally see what's happening on the other side of the card. Some say it's getting a better view of the content this way."
/>
),
imageLocation: "right",
},
};
export const ImageOnTheTop: Story = {
args: {
children: (
<TitleAndDescription
title="Image on the top"
description="After enjoying the view from the right side, our adventurous image decided to aim even higher! Now it's living the high life at the top of the card, looking down on all the content below. Talk about a promotion in position!"
/>
),
imageLocation: "top",
},
};
export const ImageWithMainAndSecondaryLabels: Story = {
args: {
children: (
<TitleAndDescription
title="Image with main and secondary labels"
description="After exploring different positions, our image decided it needed some accessories! Now it's showing off its fancy collection of labels - both main and secondary ones. Like a fashionista with a new wardrobe, it's strutting its stuff with tags that make it stand out from the crowd!"
/>
),
image: (
<CardContent.Image
mainLabels={
<>
<Tag variant="event">Tag 1</Tag>
<Tag variant="event">Tag 2</Tag>
</>
}
secondaryLabels={
<>
<Tag variant="event">Tag 3</Tag>
<Tag variant="event">Tag 4</Tag>
</>
}
>
<img
src="https://picsum.photos/1024/576"
alt="Card Image"
width={1024}
height={580}
/>
</CardContent.Image>
),
},
};
/** 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: (
<CardContent.Tagline
title="Do something now!"
buttons={
<>
<Button variant="primary" size="lg">
Button
</Button>
<Button variant="secondary" size="lg">
Second Button
</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: (
<a href="#something" target="_self" className="ink:no-underline">
<CardContent.Link
icon={<InkIcon.Logo.Ink />}
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!"
/>
</a>
),
},
};
export const LargeLinks: Story = {
args: {
image: (
<CardContent.Image mainLabels={<Tag variant="event">Main Label</Tag>}>
<img
src="https://picsum.photos/1024/576"
alt="Card Image"
width={1024}
height={1024}
/>
</CardContent.Image>
),
children: (
<>
<TitleAndDescription
title="Links Galore"
description="Links are the backbone of the web! They connect us, guide us, and make the internet what it is today. At Ink, we love links so much we've made them beautiful, accessible, and a joy to use. Click around and experience the magic of web navigation!"
/>
<CardContent.LargeLinks>
<CardContent.LargeLink asChild>
<a href="#design-tips" target="_self" className="ink:no-underline">
Design Tips & Tricks
</a>
</CardContent.LargeLink>
<CardContent.LargeLink asChild>
<a href="#color-theory" target="_self" className="ink:no-underline">
Color Theory 101
</a>
</CardContent.LargeLink>
<CardContent.LargeLink asChild>
<a href="#typography" target="_self" className="ink:no-underline">
Typography Essentials
</a>
</CardContent.LargeLink>
<CardContent.LargeLink asChild>
<a
href="#accessibility"
target="_self"
className="ink:no-underline"
>
Accessibility Best Practices
</a>
</CardContent.LargeLink>
<CardContent.LargeLink asChild>
<a href="#animations" target="_self" className="ink:no-underline">
Animation Fundamentals
</a>
</CardContent.LargeLink>
<CardContent.LargeLink asChild>
<a href="#responsive" target="_self" className="ink:no-underline">
Responsive Design Guide
</a>
</CardContent.LargeLink>
</CardContent.LargeLinks>
</>
),
},
};
export const LargeCardInfo: Story = {
args: {
image: (
<CardContent.Image mainLabels={<Tag variant="event">Main Label</Tag>}>
<img
src="https://picsum.photos/1024/576"
alt="Card Image"
width={1024}
height={1024}
/>
</CardContent.Image>
),
children: (
<>
<TitleAndDescription title="Fun Activities Around Town" />
<CardContent.CardInfos>
<CardContent.CardInfo
icon={<InkIcon.Apps className="ink:size-3" />}
title="Pizza Making Class"
description="Learn to toss dough and create your perfect pizza with our expert chefs."
/>
<CardContent.CardInfo
icon={<InkIcon.Bridge className="ink:size-3" />}
title="Paint & Sip Night"
description="Enjoy wine while creating your masterpiece in this relaxing art class."
/>
<CardContent.CardInfo
icon={<InkIcon.Social.Telegram className="ink:size-3" />}
title="Live Jazz Night"
description="Swing by for smooth tunes and great vibes at our local jazz club."
/>
<CardContent.CardInfo
icon={<InkIcon.Deposit className="ink:size-3" />}
title="Community Garden"
description="Get your hands dirty and learn about urban farming with neighbors."
/>
<CardContent.CardInfo
className="ink:lg:col-span-2"
icon={<InkIcon.Sun className="ink:size-3" />}
title="Weekend Food Festival"
description="Sample delicious treats from local vendors and enjoy live entertainment all weekend long."
/>
</CardContent.CardInfos>
</>
),
},
};
export const FullCard: Story = {
args: {
size: "noPadding",
image: (
<CardContent.Image>
<img
src="https://picsum.photos/1024/576"
alt="Card Image"
width={1024}
height={580}
/>
</CardContent.Image>
),
},
};
export const FullCardWithImageOnTheRight: Story = {
args: {
size: "noPadding",
imageLocation: "right",
image: (
<CardContent.Image>
<img
src="https://picsum.photos/1024/576"
alt="Card Image"
width={1024}
height={580}
/>
</CardContent.Image>
),
},
};
export const CardWithSmallImage: Story = {
args: {
size: "small",
children: (
<TitleAndDescription
title="Card with small image"
description="This is a card with a small image."
size="small"
/>
),
image: (
<CardContent.Image variant="square">
<img
src="https://picsum.photos/1024/576"
alt="Card Image"
width={128}
height={128}
/>
</CardContent.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<typeof cardVariants> {
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<HTMLDivElement, CardProps>(function Card(
{
children,
className,
image,
imageLocation,
asChild,
variant,
clickable,
size,
},
ref
) {
const Component = asChild ? Slot : "div";
return (
<Component
ref={ref}
className={classNames(
cardVariants({
variant,
imageLocation: image ? imageLocation : undefined,
clickable,
size: size || (image ? "default" : "small"),
className,
}),
className
)}
style={
{
"--ink-card-default-color":
variant === "light-purple"
? "var(--ink-background-light)"
: "var(--ink-text-default)",
"--ink-card-muted-color":
variant === "light-purple"
? "var(--ink-background-light)"
: "var(--ink-text-muted)",
"--ink-card-rounded": variantClassNames(size || "default", {
noPadding: "",
small: "var(--ink-base-radius-sm)",
default: "var(--ink-base-radius-lg)",
}),
} as React.CSSProperties
}
>
<Slottable child={children}>
{(child) => (
<>
{image}
<div
className={classNames(
"ink:flex ink:flex-col ink:gap-2 ink:sm:gap-6 ink:justify-center",
"ink:text-text-default ink:box-border",
!!image && "ink:p-2 ink:sm:p-3",
imageLocation === "right" && "ink:sm:-order-1",
imageLocation !== "top" &&
!!image &&
variantClassNames(size || "default", {
default: "ink:sm:py-[100px]",
noPadding: "ink:sm:py-[100px]",
small: "",
})
)}
>
{child}
</div>
</>
)}
</Slottable>
</Component>
);
});
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<CallToActionProps> = ({
title,
description,
button,
className,
}) => {
return (
<div className={classNames("ink:flex ink:flex-col ink:gap-3", className)}>
<TitleAndDescription title={title} description={description} />
<div className="ink:flex ink:gap-2 ink:box-border">{button}</div>
</div>
);
};
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 (
<Card variant="secondary" className={className}>
<div
className={classNames(
"ink:flex ink:flex-col ink:justify-start ink:gap-3 ink:box-border ink:flex-1",
className
)}
>
{icon}
<TitleAndDescription
title={title}
description={description}
size="cardInfo"
/>
{children}
</div>
</Card>
);
};
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 (
<div
className={classNames(
"ink:grid ink:grid-cols-[repeat(auto-fit,minmax(max(200px,calc(100%/3)),1fr))] ink:gap-1 ink:box-border",
className
)}
>
{children}
</div>
);
};
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<ImageProps> = ({
className,
variant,
mainLabels,
secondaryLabels,
children,
}) => {
return (
<div
className={classNames(
"ink:rounded-(--ink-card-rounded) ink:overflow-hidden ink:box-border ink:relative",
className
)}
>
{(mainLabels || secondaryLabels) && (
<div
className={classNames(
"ink:absolute ink:top-0 ink:left-0 ink:right-0",
"ink:px-2 sm:ink:px-3 md:ink:px-4 ink:pt-2 sm:ink:pt-2 md:ink:pt-4",
"ink:flex ink:justify-between ink:items-start ink:gap-1 ink:z-10 ink:flex-wrap",
"ink:whitespace-nowrap"
)}
>
<div className="ink:flex ink:gap-1 ink:flex-wrap ink:flex-1 ink:justify-start">
{mainLabels}
</div>
{secondaryLabels && (
<div className="ink:flex ink:gap-1 ink:flex-wrap ink:flex-0 ink:justify-start">
{secondaryLabels}
</div>
)}
</div>
)}
<Slot
className={classNames(
"ink:object-cover ink:object-center ink:w-full ink:h-full",
variant === "square" && "ink:aspect-square"
)}
>
{children}
</Slot>
</div>
);
};
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 = (
<InkIcon.Arrow className="ink:size-3 ink:rotate-270 ink:shrink-0" />
),
href,
target,
}: LargeLinkProps) => {
const Component = asChild ? Slot : "a";
return (
<Component
href={href}
target={target}
className={classNames(
"ink:p-3 ink:bg-button-secondary ink:text-(--ink-card-default-color) ink:text-h5 ink:rounded-lg ink:box-border",
"ink:flex ink:justify-between ink:items-center ink:gap-0.5",
"ink:min-w-[200px]",
href && "ink:cursor-pointer ink:hover:bg-button-secondary-hover",
className
)}
>
<Slottable child={children}>
{(child) => (
<>
<div className="ink:overflow-ellipsis ink:overflow-hidden ink:whitespace-nowrap">
{child}
</div>
{linkIcon}
</>
)}
</Slottable>
</Component>
);
};
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 (
<div
className={classNames(
"ink:grid ink:grid-cols-[repeat(auto-fit,minmax(max(200px,calc(100%/3)),1fr))] ink:gap-1 ink:box-border",
className
)}
>
{children}
</div>
);
};
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 = <InkIcon.Arrow className="ink:size-2 ink:rotate-225" />,
}: LinkProps) => {
return (
<Tiny
className={className}
icon={
icon ? (
<div className="ink:size-6 ink:text-(--ink-card-muted-color)">
{icon}
</div>
) : undefined
}
title={title}
description={description}
>
{linkIcon && (
<div className="ink:absolute ink:top-3 ink:right-3 ink:text-(--ink-card-muted-color)">
{linkIcon}
</div>
)}
</Tiny>
);
};
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<TaglineProps> = ({
title,
buttons,
className,
}) => {
return (
<div
className={classNames(
"ink:flex ink:flex-col ink:gap-4 sm:gap-8 ink:text-(--ink-card-default-color) ink:px-0 ink:py-6 ink:sm:px-16 ink:sm:py-12 ink:justify-between",
className
)}
>
<h3 className="ink:text-h2 ink:sm:text-h1 ink:m-0 ink:box-border ink:text-center">
{title}
</h3>
<div className="ink:flex ink:justify-center ink:flex-wrap ink:gap-2 ink:m-0 ink:box-border">
{buttons}
</div>
</div>
);
};
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 (
<div
className={classNames(
"ink:flex ink:flex-col ink:justify-start ink:gap-3 ink:box-border",
className
)}
>
{icon}
<TitleAndDescription
title={title}
description={description}
size="small"
/>
{children}
</div>
);
};
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 (
<div className="ink:flex ink:flex-col ink:gap-2">
<h3
className={classNames(
"ink:text-body-1-bold ink:text-(--ink-card-default-color) ink:box-border ink:m-0 ink:-my-px",
variantClassNames(size, {
default: "ink:text-h3",
small: "ink:text-body-1-regular",
cardInfo: "ink:text-h5",
})
)}
>
{title}
</h3>
{description && (
<div
className={classNames(
"ink:text-body-3-regular ink:text-(--ink-card-muted-color) ink:box-border ink:m-0",
variantClassNames(size, {
default: "ink:text-body-1-regular",
small: "ink:text-body-3-regular",
cardInfo: "ink:text-body-2-regular",
})
)}
>
{description}
</div>
)}
</div>
);
};
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<CheckboxProps> = {
title: "Components/Checkbox",
component: Checkbox,
tags: ["autodocs"],
args: {
checked: false,
indeterminate: false,
onChange: fn(),
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Interactive: Story = {
args: {},
render: (args) => {
const [checked, setChecked] = useState(args.checked);
useEffect(() => {
setChecked(args.checked);
}, [args.checked]);
return <Checkbox {...args} checked={checked} onChange={setChecked} />;
},
};
export const WithLabel: Story = {
args: {},
render: (args) => {
const [checked, setChecked] = useState(args.checked);
useEffect(() => {
setChecked(args.checked);
}, [args.checked]);
return (
<CheckboxLabel label="Checkbox">
<Checkbox {...args} checked={checked} onChange={setChecked} />
</CheckboxLabel>
);
},
};
export const WithLabelAndDescription: Story = {
args: {},
render: (args) => {
const [checked, setChecked] = useState(args.checked);
useEffect(() => {
setChecked(args.checked);
}, [args.checked]);
return (
<CheckboxLabel
label="Checkbox"
description="Some description of the checkbox"
>
<Checkbox {...args} checked={checked} onChange={setChecked} />
</CheckboxLabel>
);
},
};
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 (
<div className="ink:flex ink:flex-col ink:gap-1">
<CheckboxLabel label="Top Level">
<Checkbox
{...args}
indeterminate={indeterminate}
checked={checked}
onChange={() => {
if (checked || indeterminate) {
setFirstChildChecked(false);
setSecondChildChecked(false);
} else {
setFirstChildChecked(true);
setSecondChildChecked(true);
}
}}
/>
</CheckboxLabel>
<div className="ink:flex ink:flex-col ink:pl-2 ink:gap-1">
<CheckboxLabel label="First Child">
<Checkbox
{...args}
checked={firstChildChecked || checked}
onChange={setFirstChildChecked}
/>
</CheckboxLabel>
<CheckboxLabel label="Second Child">
<Checkbox
{...args}
checked={secondChildChecked || checked}
onChange={setSecondChildChecked}
/>
</CheckboxLabel>
</div>
</div>
);
},
};
/** 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 (
<ListItem
onClick={() => setChecked(!checked)}
data-checked={checked ? "true" : undefined}
iconLeft={
<Checkbox {...args} checked={undefined} onChange={undefined} />
}
>
<div>Checkbox</div>
</ListItem>
);
},
};
================================================
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<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> {
checked?: boolean;
indeterminate?: boolean;
onChange?: (enabled: boolean) => void;
}
export const Checkbox: React.FC<CheckboxProps> = ({
checked,
indeterminate,
onChange,
className,
...props
}) => {
const Component = onChange ? HeadlessCheckbox : "span";
return (
<Component
checked={checked}
indeterminate={!checked && indeterminate}
onChange={(eventOrValue) => {
/* 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}
>
<div className="ink:absolute ink:inset-0 ink:flex ink:items-center ink:justify-center ink:box-border">
<InkIcon.Check
className={classNames(
"ink:size-3",
"ink:animate-svg-path ink:group-data-checked:not-in-data-indeterminate:animate-svg-path-start",
"ink:group-data-selected:animate-svg-path-start"
)}
/>
</div>
<InkIcon.Minus
className={classNames(
"ink:size-3",
"ink:animate-svg-path ink:group-data-indeterminate:animate-svg-path-start"
)}
/>
</Component>
);
};
================================================
FILE: src/components/Checkbox/CheckboxLabel.tsx
================================================
import { FieldLabel, FieldLabelProps } from "../FieldLabel";
export interface CheckboxLabelProps extends FieldLabelProps {}
export const CheckboxLabel: React.FC<CheckboxLabelProps> = (props) => {
return <FieldLabel {...props} />;
};
================================================
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<PlaceholderUntilLoadedProps> = ({
placeholder,
children,
isLoading,
className = "",
asChild,
}) => {
const Component = asChild ? Slot : "div";
return (
<Component className={classNames("ink:relative", className)}>
<div
className={classNames(
"ink:absolute ink:inset-0",
"ink:transition-opacity ink:duration-100 ink:ease-in-out ink:opacity-100",
!isLoading && "ink:opacity-0 ink:pointer-events-none ink:select-none"
)}
>
{placeholder}
</div>
{/** This placeholder is used to ensure the content is visible when the fade out is active */}
<div className={`${isLoading ? "ink:opacity-0" : "ink:hidden"}`}>
{placeholder}
</div>
<span className={isLoading ? "ink:hidden" : ""}>{children}</span>
</Component>
);
};
================================================
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<FieldLabelProps> = ({
label,
description,
children,
}) => {
return (
<Field className="ink:flex ink:flex-col ink:font-default ink:group">
<div className="ink:flex ink:items-center ink:gap-1">
{children}
<Label className="ink:cursor-pointer ink:h-3 ink:flex ink:items-center ink:justify-center ink:text-body-2-bold ink:text-text-default">
{label}
</Label>
</div>
{description && (
<Description className="ink:text-body-3-regular ink:text-text-default">
{description}
</Description>
)}
</Field>
);
};
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<InputProps> = {
title: "Components/Input",
component: Input,
tags: ["autodocs"],
args: {
placeholder: "Placeholder",
type: "text",
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Simple: Story = {
args: {},
};
export const WithIconLeft: Story = {
args: {
iconLeft: <InkIcon.Search />,
},
};
export const WithIconRight: Story = {
args: {
iconRight: <InkIcon.Deposit />,
},
};
================================================
FILE: src/components/Input/Input.tsx
================================================
import React, { forwardRef } from "react";
import { classNames } from "../../util/classes";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
className?: string;
iconLeft?: React.ReactNode;
iconRight?: React.ReactNode;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ 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 (
<label
className={classNames(
"ink:w-full ink:flex ink:items-center ink:justify-center ink:gap-1 ink:box-border ink:group",
"ink:p-2 ink:h-5",
"ink:font-default ink:rounded-xs ink:bg-button-secondary ink:text-body-3-regular ink:text-text-default",
"ink:border-1 ink:border-transparent ink:focus-within:border-text-on-secondary ink:transition-colors ink:transition-default-animation",
className
)}
>
{iconLeft && <div className={iconClasses}>{iconLeft}</div>}
<input
className="ink:w-full ink:outline-none ink:box-border ink:-my-1 ink:placeholder:font-default ink:placeholder:text-body-3-regular ink:placeholder:text-text-muted"
ref={ref}
{...props}
/>
{iconRight && <div className={iconClasses}>{iconRight}</div>}
</label>
);
}
);
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<ButtonHTMLAttributes<HTMLButtonElement>> {
variant?: "default" | "secondary" | "error" | "muted";
disabled?: boolean;
asChild?: boolean;
className?: string;
iconLeft?: React.ReactNode;
iconRight?: React.ReactNode;
}
export const ListItem: React.FC<ListItemProps> = ({
children,
className,
asChild,
iconLeft,
iconRight,
variant = "default",
disabled,
...props
}) => {
const Component = asChild ? Slot : "button";
return (
<Component
disabled={disabled}
className={classNames(
"ink:group ink:font-default ink:text-body-2-bold ink:cursor-pointer ink:box-border ink:no-underline",
"ink:bg-background-light-invisible ink:px-1.5 ink:py-2 ink:rounded-md ink:text-body-3-bold ink:text-text-default ink:hover:bg-background-container ink:disabled:bg-background-light-transparent-disabled ink:disabled:text-muted ink:active:bg-background-container/80 ink:data-active:bg-background-container/80",
"ink:w-full ink:flex ink:items-center ink:justify-start ink:gap-1.5 ink:h-5",
variantClassNames(variant, {
default: "",
secondary:
"ink:bg-button-secondary ink:hover:bg-button-secondary-hover ink:active:bg-button-secondary-pressed ink:data-active:bg-button-secondary-pressed ink:text-button-secondary-text",
error: "ink:text-status-error ink:hover:bg-status-error-bg",
muted:
"ink:bg-background-container ink:text-text-muted ink:border-1 ink:border-transparent ink:focus:border-text-on-secondary ink:transition-colors ink:transition-default-animation",
}),
"ink:data-disabled:text-text-muted ink:data-disabled:cursor-not-allowed",
className
)}
{...props}
>
<Slottable child={children}>
{(child) => (
<>
{iconLeft && (
<div
className={classNames(
"ink:flex ink:items-center ink:justify-center ink:size-3 ink:-my-1.5"
)}
>
{iconLeft}
</div>
)}
<div className="ink:flex-1 ink:flex ink:items-center ink:justify-start">
{child}
</div>
{iconRight && (
<div
className={classNames(
"ink:flex ink:items-center ink:justify-center ink:size-3 ink:-my-1.5"
)}
>
{iconRight}
</div>
)}
</>
)}
</Slottable>
</Component>
);
};
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: <InkIcon.Home /> },
{ value: "2", label: "Option 2", iconLeft: <InkIcon.Settings /> },
{ value: "3", label: "Option 3", iconLeft: <InkIcon.Deposit /> },
];
const meta: Meta<ListboxProps<ListboxStoryItem>> = {
title: "Components/Listbox",
component: Listbox,
tags: ["autodocs"],
argTypes: {},
args: {
children: (
<ListboxOptions>
{defaultItems.map((item) => (
<ListboxOption key={item.value} value={item}>
{item.label}
</ListboxOption>
))}
</ListboxOptions>
),
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Interactive: Story = {
args: {},
render: (args) => {
const [item, setValue] = useState<ListboxStoryItem>(defaultItems[0]);
return (
<Listbox value={item} onChange={setValue}>
<ListboxButton>Selected: {item.label}</ListboxButton>
{args.children}
</Listbox>
);
},
};
export const WithOneDisabledOption: Story = {
args: {
children: (
<ListboxOptions>
{defaultItems.map((item, index) => (
<ListboxOption key={item.value} value={item} disabled={index === 1}>
{item.label}
</ListboxOption>
))}
</ListboxOptions>
),
},
render: (args) => {
const [item, setValue] = useState<ListboxStoryItem>(defaultItems[0]);
return (
<Listbox value={item} onChange={setValue}>
<ListboxButton>Selected: {item.label}</ListboxButton>
{args.children}
</Listbox>
);
},
};
export const MultipleValues: Story = {
args: {
multiple: true,
},
render: (args) => {
const [items, setValues] = useState<ListboxStoryItem[]>([
defaultItems[0],
defaultItems[1],
]);
return (
<Listbox {...args} value={items} onChange={setValues}>
<ListboxButton>
Selected:{" "}
{items.length ? items.map((item) => item.label).join(", ") : "None"}
</ListboxButton>
{args.children}
</Listbox>
);
},
};
export const WithIconsOnTheLeft: Story = {
args: {
children: (
<ListboxOptions>
{defaultItems.map((item) => (
<ListboxOption key={item.value} value={item} iconLeft={item.iconLeft}>
{item.label}
</ListboxOption>
))}
</ListboxOptions>
),
},
render: (args) => {
const [item, setValue] = useState<ListboxStoryItem>(defaultItems[0]);
return (
<Listbox value={item} onChange={setValue}>
<ListboxButton iconLeft={item.iconLeft}>
Selected: {item.label}
</ListboxButton>
{args.children}
</Listbox>
);
},
};
export const MultipleValuesWithIconsOnTheRight: Story = {
args: {
multiple: true,
children: (
<ListboxOptions>
{defaultItems.map((item) => (
<ListboxOption
key={item.value}
value={item}
iconRight={item.iconLeft}
>
{item.label}
</ListboxOption>
))}
</ListboxOptions>
),
},
render: (args) => {
const [items, setValues] = useState<ListboxStoryItem[]>([
defaultItems[0],
defaultItems[1],
]);
return (
<Listbox {...args} value={items} onChange={setValues}>
<ListboxButton>
Selected:{" "}
{items.length ? (
<div className="ink:flex ink:items-center ink:gap-1 ink:mx-0.5">
{items.map((item) => (
<div className="ink:size-3">{item.iconLeft}</div>
))}
</div>
) : (
"None"
)}
</ListboxButton>
{args.children}
</Listbox>
);
},
};
const moreItems: ListboxStoryItem[] = [
{ value: "4", label: "Option 4", iconLeft: <InkIcon.Home /> },
{ value: "5", label: "Option 5", iconLeft: <InkIcon.Settings /> },
{ value: "6", label: "Option 6", iconLeft: <InkIcon.Deposit /> },
{ value: "7", label: "Option 7", iconLeft: <InkIcon.Home /> },
{ value: "8", label: "Option 8", iconLeft: <InkIcon.Settings /> },
{ value: "9", label: "Option 9", iconLeft: <InkIcon.Deposit /> },
];
export const WithManyOptions: Story = {
args: {
children: (
<ListboxOptions>
{[...defaultItems, ...moreItems].map((item) => (
<ListboxOption key={item.value} value={item}>
{item.label}
</ListboxOption>
))}
</ListboxOptions>
),
},
render: (args) => {
const [item, setValue] = useState<ListboxStoryItem>(defaultItems[0]);
return (
<Listbox value={item} onChange={setValue}>
<ListboxButton>Selected: {item.label}</ListboxButton>
{args.children}
</Listbox>
);
},
};
export const WithADifferentButtonVariant: Story = {
args: {
children: (
<ListboxOptions>
{[...defaultItems, ...moreItems].map((item) => (
<ListboxOption key={item.value} value={item}>
{item.label}
</ListboxOption>
))}
</ListboxOptions>
),
},
render: (args) => {
const [item, setValue] = useState<ListboxStoryItem>(defaultItems[0]);
return (
<Listbox value={item} onChange={setValue}>
<ListboxButton variant="muted">Selected: {item.label}</ListboxButton>
{args.children}
</Listbox>
);
},
};
================================================
FILE: src/components/Listbox/Listbox.tsx
================================================
import { Listbox as HeadlessListbox } from "@headlessui/react";
import { PropsWithChildren } from "react";
export interface ListboxProps<T> 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 = <T extends object>({
children,
value,
onChange,
multiple,
}: ListboxProps<T>) => {
return (
<HeadlessListbox multiple={multiple} value={value} onChange={onChange}>
{children}
</HeadlessListbox>
);
};
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<HTMLButtonElement, ListboxButtonProps>(
({ className, children, variant = "secondary", ...props }, ref) => {
return (
<HeadlessListboxButton
className={classNames(
className,
"ink:focus-visible:outline-none ink:data-active:border-text-on-secondary ink:rounded-full ink:text-body-3-bold ink:text-text-muted ink:hover:text-text-default"
)}
ref={ref}
as={ListItem}
variant={variant}
iconRight={<InkIcon.Chevron />}
{...props}
>
{children}
</HeadlessListboxButton>
);
}
);
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<T> extends Omit<ListItemProps, "value"> {
value: T;
disabled?: boolean;
}
export const ListboxOption = <T,>({
children,
disabled,
iconLeft,
iconRight,
...props
}: ListboxOptionProps<T>) => {
return (
<HeadlessListboxOption
className={classNames(
"ink:flex ink:items-center ink:px-3 ink:py-2 ink:text-sm ink:cursor-pointer"
)}
disabled={disabled}
as={ListItem}
iconLeft={iconLeft || (iconRight ? <Checkbox /> : undefined)}
iconRight={
<div className="ink:flex ink:items-center ink:justify-center ink:gap-1.5">
{iconRight || (
<InkIcon.Check className="ink:not-in-data-selected:hidden" />
)}
</div>
}
{...props}
>
{children}
</HeadlessListboxOption>
);
};
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 (
<HeadlessListboxOptions
className={classNames("ink:absolute ink:z-10 ink:box-border", className)}
anchor="bottom end"
>
<Panel className="ink:max-h-[300px] ink:gap-px">{children}</Panel>
</HeadlessListboxOptions>
);
};
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 (
<div className="ink:flex ink:flex-col ink:justify-center ink:items-center ink:gap-5 ink:max-w-sm">
<div className="ink:flex ink:flex-col ink:items-center ink:gap-2">
<div className="ink:text-h4">{title}</div>
<div className="ink:text-body-2-regular ink:text-center">{content}</div>
</div>
{button}
</div>
);
};
================================================
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<ModalProps> = {
title: "Components/Modal",
decorators: [
(Story, { args }) => {
function ModalContent() {
const { isModalOpen, openModal } = useModalContext(args.id);
return (
<div className="ink:p-4">
<Button variant="primary" size="md" onClick={openModal}>
{isModalOpen ? "Close Modal" : "Open Modal"}
</Button>
<Story />
</div>
);
}
return (
<ModalProvider>
<ModalContent />
</ModalProvider>
);
},
],
component: Modal,
tags: ["autodocs"],
argTypes: {},
args: {
id: "modal",
title: "Example modal",
hasBackdrop: false,
onClose: fn(),
},
};
export default meta;
type Story = StoryObj<typeof meta>;
const ModalContent = ({
closeModal,
}: {
closeModal: (success: boolean) => void;
}) => {
return (
<ModalLayout.CallToAction
title="Get started"
content="Keep it simple, keep it actionable, give them a goal and they will come"
button={
<Button
className="ink:w-full"
variant="primary"
size="lg"
onClick={() => closeModal(true)}
>
Let's go
</Button>
}
/>
);
};
export const Simple: Story = {
args: {
children: ModalContent,
},
};
export const Nested: Story = {
decorators: [
(Story) => {
return (
<>
<Story />
<Modal id="nested" title="Nested modal" size="md" hasBackdrop>
{({ closeModal }) => (
<ModalLayout.CallToAction
title="A nested modal example"
content="This one uses the backdrop and size='md'"
button={
<Button
variant="primary"
size="lg"
onClick={() => closeModal()}
>
Close Nested
</Button>
}
/>
)}
</Modal>
</>
);
},
],
args: {
children: () => {
const { openModal } = useModalContext("nested");
return (
<div>
<Button
className="ink:w-full"
variant="primary"
size="lg"
onClick={openModal}
>
Open Nested
</Button>
</div>
);
},
},
};
================================================
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<TOnCloseProps = boolean> {
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 = <TOnCloseProps,>({
id,
title,
size = "lg",
hasBackdrop,
openOnMount,
onClose,
children,
}: ModalProps<TOnCloseProps>) => {
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 (
<>
<Dialog
open={isModalOpen}
onClose={() => handleClose()}
transition
className="ink:relative ink:font-default ink:text-text-default"
style={{ zIndex: 15 + modalIndex }}
>
{hasBackdrop && (
<DialogBackdrop
transition
className="ink:fixed ink:inset-0 ink:transition-opacity ink:transition-default-animation ink:backdrop-blur-lg ink:data-closed:opacity-0"
/>
)}
<div
className={classNames(
"ink:fixed ink:inset-0 ink:p-4",
"ink:flex ink:items-center ink:justify-center"
)}
>
<DialogPanel transition>
<InkPanel size={size} centered shadow>
<DialogTitle
as={InkHeader}
title={title}
icon={
<InkIcon.Close
className="ink:cursor-pointer ink:size-3"
onClick={() => handleClose()}
/>
}
/>
<div className="ink:flex-1 ink:flex ink:flex-col ink:justify-center ink:items-center">
{children({ closeModal: handleClose })}
</div>
</InkPanel>
</DialogPanel>
</div>
</Dialog>
</>
);
};
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<ModalManagementContextProps>({
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<string[]>([]);
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 (
<ModalManagementContext.Provider value={modalManagementContext}>
{children}
</ModalManagementContext.Provider>
);
};
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<PanelProps> = ({ children, className }) => {
return (
<div
className={classNames(
"ink:min-w-[240px] ink:mt-1 ink:p-1 ink:box-border",
"ink:rounded-lg ink:bg-background-light",
"ink:flex ink:flex-col ink:gap-1.5",
"ink:transition-opacity ink:transition-default-animation ink:opacity-100 ink:starting:opacity-0",
"ink:overflow-y-auto ink:h-full",
className
)}
>
{children}
</div>
);
};
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<PopoverContentInfoProps> = ({
title,
content,
icon,
}) => {
return (
<div className="ink:text-body-2-bold ink:p-1.5 ink:bg-background-container ink:rounded-md ink:flex ink:gap-1.5 ink:font-default">
<div className="ink:flex ink:flex-col ink:flex-1">
<div className="ink:text-text-muted ink:text-caption-1-bold">
{title}
</div>
<div className="ink:text-h4 ink:text-text-default">{content}</div>
</div>
{icon && <div>{icon}</div>}
</div>
);
};
================================================
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<PopoverProps> = {
title: "Components/Popover",
component: Popover,
tags: ["autodocs"],
argTypes: {},
args: {
children: (
<>
<PopoverButton>Click Me</PopoverButton>
<PopoverPanel
headerContent={
<PopoverContent.Info title="Info" content="Some content" />
}
>
<PopoverContent.ListItem iconLeft={<InkIcon.Profile />}>
Item 1
</PopoverContent.ListItem>
<PopoverContent.ListItem iconLeft={<InkIcon.Settings />}>
Item 2
</PopoverContent.ListItem>
<PopoverContent.ListItem
iconLeft={<InkIcon.Copy />}
onClick={() =>
navigator.clipboard.writeText("You are a nice person")
}
>
Copy Compliment To Clipboard
</PopoverContent.ListItem>
<PopoverContent.ListItem
variant="error"
iconRight={<InkIcon.Error />}
>
Error Item
</PopoverContent.ListItem>
<PopoverContent.ListItem
asChild
iconRight={<InkIcon.Arrow className="ink:rotate-[225deg]" />}
>
<a href="#something" target="_self">
Link Item
</a>
</PopoverContent.ListItem>
</PopoverPanel>
</>
),
},
};
export default meta;
type Story = StoryObj<typeof meta>;
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 (
<HeadlessPopover
className={classNames("ink:relative ink:font-default", className)}
>
{children}
</HeadlessPopover>
);
};
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<PopoverButtonProps> = ({
asChild,
className,
disabled,
autoFocus,
...props
}) => {
return (
<HeadlessPopoverButton
as={asChild ? Slot : Button}
className={className}
disabled={disabled}
autoFocus={autoFocus}
{...(asChild ? {} : { variant: "primary", size: "md" })}
{...props}
/>
);
};
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<PopoverPanelProps> = ({
className,
headerContent,
children,
}) => {
return (
<HeadlessPopoverPanel
className={classNames("ink:absolute ink:z-10", className)}
anchor="bottom end"
>
<Panel>
{headerContent && (
<div className="ink:flex ink:flex-col">{headerContent}</div>
)}
<div className="ink:flex ink:flex-col ink:gap-0.5">{children}</div>
</Panel>
</HeadlessPopoverPanel>
);
};
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<RadioProps> = ({ value, asChild }) => {
const Component = asChild ? Slot : HeadlessRadio;
return (
<Component
value={value}
className={classNames(
"ink:group ink:relative ink:flex ink:items-center ink:justify-center ink:size-3 ink:shrink-0 ink:cursor-pointer ink:rounded-full ink:box-border",
"ink:transition-colors ink:transition-default-animation",
"ink:border-2 ink:border-transparent 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:data-checked:hover:bg-button-primary-hover"
)}
>
<div className="ink:absolute ink:inset-0 ink:flex ink:items-center ink:justify-center">
<div className="ink:size-[10px] ink:rounded-full ink:bg-background-light ink:transition-opacity ink:transition-default-animation ink:opacity-0 ink:group-data-checked:opacity-100" />
</div>
</Component>
);
};
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<RadioGroupProps> = {
title: "Components/RadioGroup",
component: RadioGroup,
tags: ["autodocs"],
args: {
onChange: fn(),
value: "1",
children: (
<>
<RadioLabel label="First">
<Radio value="1" />
</RadioLabel>
<RadioLabel label="Second">
<Radio value="2" />
</RadioLabel>
</>
),
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Interactive: Story = {
render: (args) => {
const [value, setValue] = useState(args.value);
useEffect(() => {
setValue(args.value);
}, [args.value]);
return <RadioGroup {...args} value={value} onChange={setValue} />;
},
};
export const WithDescription: Story = {
args: {
children: (
<>
<RadioLabel label="First" description="This is a description">
<Radio value="1" />
</RadioLabel>
<RadioLabel label="Second" description="This is another description">
<Radio value="2" />
</RadioLabel>
</>
),
},
render: (args) => {
const [value, setValue] = useState(args.value);
useEffect(() => {
setValue(args.value);
}, [args.value]);
return <RadioGroup {...args} value={value} onChange={setValue} />;
},
};
================================================
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<RadioGroupProps> = ({
value,
onChange,
children,
}) => {
return (
<HeadlessRadioGroup
className="ink:flex ink:flex-col ink:gap-2"
value={value}
onChange={onChange}
>
{children}
</HeadlessRadioGroup>
);
};
RadioGroup.displayName = "RadioGroup";
================================================
FILE: src/components/Radio/RadioLabel.tsx
================================================
import { FieldLabel, FieldLabelProps } from "../FieldLabel";
export interface RadioLabelProps extends FieldLabelProps {}
export const RadioLabel: React.FC<RadioLabelProps> = (props) => {
return <FieldLabel {...props} />;
};
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<SegmentedControlProps<string>> = {
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<typeof meta>;
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: (
<a href="#first" target="_self">
First
</a>
),
value: "first",
selectedByDefault: true,
asChild: true,
},
{
children: (
<a href="#second" target="_self">
Second
</a>
),
value: "second",
asChild: true,
},
{
children: (
<a href="#third" target="_self">
Third
</a>
),
value: "third",
asChild: true,
},
],
},
};
export const PrimaryVariant: Story = {
args: {
variant: "primary",
variableTabWidth: true,
options: [
{
children: <span>Home</span>,
value: "yeah",
selectedByDefault: true,
},
{
children: <span>Apps</span>,
value: "done",
},
],
},
};
export const TagVariant: Story = {
args: {
variant: "tag",
variableTabWidth: true,
options: [
{
children: <span>Home</span>,
value: "yeah",
selectedByDefault: true,
},
{
children: <span>Apps</span>,
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<TOptionValue extends string> = {
options: SegmentedControlOption<TOptionValue>[];
onOptionChange: (
option: SegmentedControlOption<TOptionValue>,
index: number
) => void;
variableTabWidth?: boolean;
variant?: "default" | "primary" | "tag";
};
export interface SegmentedControlOption<TOptionValue extends string> {
children: React.ReactNode;
value: TOptionValue;
selectedByDefault?: boolean;
asChild?: boolean;
}
export const SegmentedControl = <TOptionValue extends string>({
options,
onOptionChange,
variableTabWidth,
variant = "default",
}: SegmentedControlProps<TOptionValue>) => {
const itemsRef = useRef<Array<HTMLButtonElement | null>>([]);
const [selectedOption, setSelectedOption] = useState<TOptionValue | null>(
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 (
<div className="ink:relative ink:font-default ink:h-fit">
<div
className={classNames(
"ink:grid ink:h-5 ink:grid-flow-col ink:text-body-3-bold ink:rounded-md ink:p-0.5 ink:box-border ink:backdrop-blur-lg",
variantClassNames(variant, {
default: "ink:bg-background-container",
primary: "ink:bg-background-container",
tag: "",
}),
variableTabWidth
? "ink:[grid-auto-columns:auto]"
: "ink:[grid-auto-columns:1fr]"
)}
>
{options.map((option, index) => {
const ButtonComponent = option.asChild ? Slot : "button";
return (
<ButtonComponent
className={classNames(
"ink:h-full ink:box-border ink:rounded-sm ink:relative ink:z-10 ink:transition-colors ink:transition-default-animation ink:hover:cursor-pointer ink:select-none ink:no-underline ink:flex ink:items-center ink:justify-center",
selectedOption === option.value
? variantClassNames(variant, {
default: "ink:text-text-default",
primary: "ink:text-text-on-primary",
tag: "ink:text-text-default",
})
: "ink:text-text-muted ink:hover:text-text-default",
variantClassNames(variant, {
default: variableTabWidth ? "ink:px-3" : "ink:px-4",
primary: variableTabWidth ? "ink:px-3" : "ink:px-4",
tag: "ink:px-2",
})
)}
ref={(el) => {
itemsRef.current[index] = el;
}}
key={option.value}
onClick={() => {
setSelectedOption(option.value);
onOptionChange(option, index);
}}
draggable={false}
>
{option.children}
</ButtonComponent>
);
})}
{isMounted && selectedOption && (
<div
className="ink:absolute ink:py-0.5 ink:box-border ink:transition-[left,width] ink:transition-default-animation"
style={{
top: 0,
bottom: 0,
left: `${left}px`,
width: `${width}px`,
}}
>
<div
className={classNames(
"ink:w-full ink:h-full ink:rounded-sm",
variantClassNames(variant, {
default: "ink:bg-background-light",
primary: "ink:bg-button-primary",
tag: "ink:bg-button-secondary",
})
)}
/>
</div>
)}
</div>
</div>
);
};
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<T> = React.Ref<T> | undefined;
function setRef<T>(ref: PossibleRef<T>, value: T) {
if (typeof ref === "function") {
ref(value);
} else if (ref !== null && ref !== undefined) {
(ref as React.MutableRefObject<T>).current = value;
}
}
function composeRefs<T>(...refs: PossibleRef<T>[]) {
return (node: T) => refs.forEach((ref) => setRef(ref, node));
}
/* -------------------------------------------------------------------------------------------------
* Slot
* -----------------------------------------------------------------------------------------------*/
interface SlotProps extends React.HTMLAttributes<HTMLElement> {
children?: React.ReactNode;
}
const Slot = React.forwardRef<HTMLElement, SlotProps>((props, forwardedRef) => {
const { children, ...slotProps } = props;
if (isSlottable(children)) {
const slottable = children;
return (
<SlotClone {...slotProps} ref={forwardedRef}>
{React.isValidElement<React.PropsWithChildren<unknown>>(
slottable.props.child
)
? React.cloneElement(
slottable.props.child,
undefined,
slottable.props.children(slottable.props.child.props.children)
)
: null}
</SlotClone>
);
}
return (
<SlotClone {...slotProps} ref={forwardedRef}>
{children}
</SlotClone>
);
});
Slot.displayName = "Slot";
/* -------------------------------------------------------------------------------------------------
* SlotClone
* -----------------------------------------------------------------------------------------------*/
interface SlotCloneProps {
children: React.ReactNode;
}
const SlotClone = React.forwardRef<any, SlotCloneProps>(
(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<string, any>;
function isSlottable(
child: React.ReactNode
): child is React.ReactElement<SlottableProps> {
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<TagProps> = {
decorators: [
MatrixDecorator<TagProps>({
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<typeof meta>;
export const Default: Story = {
args: {},
};
export const WithIcon: Story = {
args: {
icon: <InkIcon.InkLogo />,
},
};
================================================
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<HTMLDivElement>,
Omit<VariantProps<typeof tagVariants>, "hasIcon"> {
icon?: React.ReactNode;
}
export const Tag = React.forwardRef<HTMLDivElement, TagProps>(function Tag(
{ className, variant, selected, icon, children, ...props },
ref
) {
return (
<div
ref={ref}
className={classNames(
tagVariants({ variant, selected, hasIcon: !!icon, className })
)}
{...props}
>
{icon && (
<div
className={classNames(
"ink:flex ink:items-center ink:justify-center",
variant === "featured" ? "ink:size-1.5" : "ink:size-2"
)}
>
{icon}
</div>
)}
{children}
</div>
);
});
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<ToggleProps> = {
title: "Components/Toggle",
component: Toggle,
tags: ["autodocs"],
args: {
checked: false,
onChange: fn(),
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Interactive: Story = {
args: {},
render: (args) => {
const [checked, setChecked] = useState(args.checked);
useEffect(() => {
setChecked(args.checked);
}, [args.checked]);
return <Toggle {...args} checked={checked} onChange={setChecked} />;
},
};
================================================
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<ToggleProps> = ({ checked, onChange }) => {
return (
<Switch
checked={checked}
onChange={onChange}
className={classNames(
"ink:group ink:relative ink:inline-flex ink:h-4 ink:w-6 ink:shrink-0 ink:cursor-pointer ink:rounded-full ink:box-border",
"ink:transition-colors ink:transition-default-animation",
"ink:bg-button-secondary",
"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-status-success",
"ink:flex ink:items-center ink:p-0.5"
)}
>
<span
style={
/**
* Somehow, we cannot use `calc(100%-var(--ink-spacing-1_5))` directly in the class name
* So this is a small workaround to make it work.
*/
{
"--ink-translate-x": "calc(100% - var(--ink-spacing-1))",
} as React.CSSProperties
}
className={classNames(
"ink:box-border ink:pointer-events-none ink:inline-block ink:size-3 ink:transform ink:rounded-full ink:bg-text-on-primary ink:shadow ink:ring-0",
"ink:transition ink:transition-default-animation",
"ink:group-data-checked:translate-x-(--ink-translate-x)"
)}
/>
<span className="ink:sr-only">Toggle switch</span>
</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<TypographyProps> = {
title: "Design/Typography",
decorators: [
(Story, { args }) => (
<div className="ink:p-4 ink:flex ink:flex-col ink:gap-4 ink:text-text-default">
{variants.map((variant) => (
<Story
key={variant}
args={{
children: (
<div>
ink:text-{variant} - The quick brown fox jumps over the lazy
dog
</div>
),
...args,
variant,
}}
/>
))}
</div>
),
],
component: Typography,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
args: {},
};
export default meta;
type Story = StoryObj<typeof meta>;
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<HTMLHeadingElement | HTMLDivElement> {
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<TypographyProps> = ({
variant,
className,
children,
asChild,
...restProps
}) => {
const Component = asChild ? Slot : "div";
return (
<Component
className={classNames(
"ink:font-default",
/**
* It would be tempting to put those in a string template, but then Tailwind won't be able to detect the classes here
* and won't include them in the production build until we actually use them somewhere.
**/
variantClassNames(variant, {
"display-1": "ink:text-display-1",
"display-2": "ink:text-display-2",
h1: "ink:text-h1",
h2: "ink:text-h2",
h3: "ink:text-h3",
h4: "ink:text-h4",
h5: "ink:text-h5",
"body-1": "ink:text-body-1",
"body-2-regular": "ink:text-body-2-regular",
"body-2-bold": "ink:text-body-2-bold",
"body-3-regular": "ink:text-body-3-regular",
"body-3-bold": "ink:text-body-3-bold",
"caption-1-regular": "ink:text-caption-1-regular",
"caption-1-bold": "ink:text-caption-1-bold",
"caption-2-regular": "ink:text-caption-2-regular",
"caption-2-bold": "ink:text-caption-2-bold",
}),
className
)}
{...restProps}
>
{children}
</Component>
);
};
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<ConnectWalletProps> = {
title: "Components/ConnectWallet",
decorators: [
WalletProvider,
(Story) => <div className="ink:min-h-[300px]">{Story()}</div>,
],
component: ConnectWallet,
tags: ["autodocs"],
argTypes: {},
args: {},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Simple: Story = {
args: {},
};
export const WithProfileAndSettings: Story = {
args: {
listItems: (
<>
<PopoverContent.ListItem iconLeft={<InkIcon.Profile />}>
Profile
</PopoverContent.ListItem>
<PopoverContent.ListItem iconLeft={<InkIcon.Settings />}>
Settings
</PopoverContent.ListItem>
</>
),
},
};
================================================
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<ConnectWalletProps> = ({
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 (
<Popover>
<PopoverButton asChild>
<Button
variant={isConnected ? "wallet" : "primary"}
className={className}
iconLeft={
isConnected && !isSmallWindow ? (
<img src={ensImage} alt={`avatar for ${ensName}`} />
) : undefined
}
rounded={isSmallWindow ? "full" : "default"}
>
<PlaceholderUntilLoaded
placeholder={
<>
<div className="ink:hidden ink:sm:flex ink:items-center ink:justify-center">
Connecting...
</div>
<span className="ink:inline ink:sm:hidden">···</span>
</>
}
isLoading={!hasLoaded}
>
{isConnected ? (
<>
<div className="ink:size-4 ink:*:w-4 ink:*:rounded-full ink:block ink:sm:hidden">
<img src={ensImage} alt={`avatar for ${ensName}`} />
</div>
<span className="ink:hidden ink:sm:inline">{ensName}</span>
</>
) : (
<>
<span className="ink:hidden ink:sm:inline">Connect</span>
<InkIcon.Wallet className="ink:block ink:sm:hidden" />
</>
)}
</PlaceholderUntilLoaded>
</Button>
</PopoverButton>
<PopoverPanel
className="ink:z-100"
headerContent={
isConnected ? (
<ConnectedWalletPopupHeader address={address!} />
) : undefined
}
>
{isConnected ? (
<ConnectedWalletSection address={address!} listItems={listItems} />
) : (
<div className="ink:flex ink:flex-col ink:gap-2">
{connectors.map((connector) => (
<PopoverContent.ListItem
key={connector.uid}
onClick={() => connect({ connector })}
>
{connector.name}
</PopoverContent.ListItem>
))}
</div>
)}
</PopoverPanel>
</Popover>
);
};
const ConnectedWalletPopupHeader = ({ address }: { address: Address }) => {
const {
isLoading,
isSuccess,
data: balance,
} = useBalance({
address,
chainId: inkSepolia.id,
});
if (isLoading) {
return null;
}
return (
<div className="ink:text-body-2-bold ink:p-1.5 ink:bg-background-container ink:rounded-md ink:flex ink:gap-1.5 ink:font-default">
<div className="ink:flex ink:flex-col ink:flex-1">
<div className="ink:text-text-muted ink:text-caption-1-bold">
Balance
</div>
<div className="ink:text-h4 ink:text-text-default">
{isSuccess ? `${balance.value} ${balance.symbol}` : "..."}
</div>
</div>
<div>
<Button asChild variant="primary" rounded="full">
<a href="https://inkonchain.com/bridge" target="_blank">
<InkIcon.Deposit />
</a>
</Button>
</div>
</div>
);
};
const ConnectedWalletSection = ({
address,
listItems,
}: {
address: Address;
listItems?: React.ReactNode;
}) => {
const { disconnect } = useDisconnect();
return (
<>
{listItems}
<PopoverContent.ListItem
iconLeft={<InkIcon.Copy />}
onClick={() => navigator.clipboard.writeText(address)}
>
{trimAddress(address)}
</PopoverContent.ListItem>
<PopoverContent.ListItem
variant="error"
iconLeft={<InkIcon.Disconnect />}
onClick={() => disconnect()}
>
Disconnect
</PopoverContent.ListItem>
</>
);
};
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 (
<div className="ink:bg-background-container">
<Story />
</div>
);
};
================================================
FILE: src/decorators/MatrixDecorator.tsx
================================================
import { Decorator } from "@storybook/react";
type MatrixDefinition<TProps, TKey extends keyof TProps = keyof TProps> = {
[P in TKey]: {
key: P;
values: Array<TProps[P]>;
};
}[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<TProps, TFirst>;
second: MatrixDefinition<TProps, TSecond>;
}): Decorator =>
(Story, { args, parameters }) => {
if (parameters.disableMatrix) return <Story />;
return (
<div className="ink:flex ink:flex-col ink:items-center ink:gap-2">
{firstValues.map((firstValue, i) => (
<div key={`${String(firstKey)}-${i}`} className="ink:flex ink:gap-2">
{secondValues.map((secondValue, j) => {
return (
<Story
key={`${String(firstKey)}-${i}-${j}`}
args={{
...args,
[firstKey]: firstValue,
[secondKey]: secondValue,
}}
/>
);
})}
</div>
))}
</div>
);
};
================================================
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 (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
</WagmiProvider>
);
};
================================================
FILE: src/global.d.ts
================================================
/// <reference types="vite-plugin-svgr/client" />
declare module "*?base64" {
const value: string;
export default value;
}
type StringWithAutocomplete<T> = T | (string & Record<never, never>);
================================================
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<WindowSize>({
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<React.SVGProps<SVGSVGElement>>
| IconsOrFolder;
}
export const AllIcons: React.FC<{
containerClassName?: string;
iconClassName?: string;
}> = ({ containerClassName, iconClassName }) => {
return (
<div
className={classNames(
"ink:flex ink:flex-wrap ink:gap-2 ink:text-text-default",
containerClassName
)}
>
<div className="ink:text-body-2-bold">Click to copy icon name</div>
<IconsOrFolder
title="InkIcon"
iconsOrFolder={Icons}
iconClassName={classNames("tooltip-on-hover", iconClassName)}
/>
</div>
);
};
function IconsOrFolder({
title,
iconsOrFolder,
iconClassName,
}: {
title: string;
iconsOrFolder: IconsOrFolder;
iconClassName?: string;
}) {
return (
<div className={classNames("ink:flex ink:flex-col ink:gap-2 ink:pl-2")}>
<div className="ink:text-caption-1-bold ink:text-text-muted">{title}</div>
<div className="ink:flex ink:flex-wrap ink:gap-2">
{Object.entries(iconsOrFolder).map(([name, IconOrFolder]) => {
if (!isIconFolder(IconOrFolder)) {
return (
<div
key={name}
className="tooltip-on-hover ink:cursor-pointer ink:whitespace-nowrap"
data-title={`<${title}.${name} />`}
onClick={() =>
navigator.clipboard.writeText(`<${title}.${name} />`)
}
>
<IconOrFolder
className={classNames("ink:size-4", iconClassName)}
/>
</div>
);
}
})}
</div>
{Object.entries(iconsOrFolder).map(([name, IconOrFolder]) => {
if (isIconFolder(IconOrFolder)) {
return (
<IconsOrFolder
key={name}
title={`${title}.${name}`}
iconsOrFolder={IconOrFolder}
iconClassName={iconClassName}
/>
);
}
})}
</div>
);
}
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<typeof meta>;
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<SVGSVGElement>) => {
const id = useId();
return (
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<mask
id={`${id}-mask`}
style={{ maskType: "alpha" }}
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="48"
height="48"
>
<path
d="M47.9885 24.0077C48.0525 22.8733 47.8445 22.0106 47.4766 21.2916C46.4847 19.1668 44.3089 18.6395 43.253 15.2365C42.805 14.0543 42.837 12.904 42.821 11.7696C42.837 10.9229 42.821 10.0921 42.4851 9.22934C42.2291 8.49441 41.7811 7.75949 40.9652 7.02457C40.9172 6.97664 40.8692 6.92871 40.8212 6.88078C40.4052 6.43344 39.9733 6.1139 39.5253 5.87426C38.3095 5.13933 37.2535 5.13934 36.1017 5.13934C34.9818 5.07543 33.8299 5.0275 32.726 4.61211C29.1423 3.28605 28.6944 1.43277 26.7426 0.538082C26.0226 0.170621 25.1587 -0.0530492 24.0228 0.0108571C23.9108 0.0108571 23.8149 0.0268346 23.7029 0.0268346C23.0949 0.010858 22.551 0.106717 22.071 0.266483C19.6651 0.898752 19.1697 2.59445 16.7115 4.0539C16.6591 4.08501 16.6025 4.11161 16.5479 4.13864C16.5085 4.15814 16.467 4.17528 16.4277 4.19492C13.4723 5.6717 11.7232 4.87105 9.81621 5.36301C8.9043 5.53875 7.99239 5.98609 7.04848 7.04055C6.9857 7.1346 6.89377 7.20649 6.81259 7.28521C6.29026 7.79175 5.94227 8.31304 5.70461 8.86188C4.40873 11.2743 5.52863 12.9838 3.84879 16.8981C3.7368 17.1537 3.60881 17.3774 3.46482 17.6011C2.28094 19.3585 0.985065 19.9336 0.377124 21.6591C0.10515 22.2982 -0.0388336 23.0491 0.00916172 24.0077C-0.0388336 24.9503 0.105154 25.7012 0.36113 26.3403C0.985069 28.1136 2.32894 28.6728 3.52882 30.5261C3.64081 30.7018 3.7368 30.8936 3.8328 31.1013C5.48064 34.9197 4.44073 36.6451 5.60862 38.9777C5.88059 39.6647 6.31255 40.3357 7.03249 40.9588C7.06448 40.9908 7.11247 41.0227 7.14447 41.0547C7.8164 41.7736 8.47234 42.189 9.12828 42.4286C11.3361 43.3553 13.1439 42.1571 16.4716 43.8506C16.7275 43.9624 16.9675 44.1062 17.1915 44.25C18.9993 45.4802 19.6713 46.8062 21.3831 47.4932C22.103 47.8447 22.9509 48.0524 23.9908 47.9885C24.2256 47.9885 24.4608 47.9757 24.6953 47.9637C25.1077 47.9424 25.4839 47.8829 25.8313 47.7852C25.8415 47.7823 25.852 47.7808 25.8627 47.7808C25.8733 47.7808 25.8842 47.7794 25.8945 47.7768C28.6145 47.0853 28.6542 44.9021 32.694 43.4192C34.0379 42.9079 35.4297 42.9559 36.7736 42.844C37.4935 42.8281 38.1975 42.7482 38.9334 42.4127C39.6373 42.1411 40.3093 41.7097 40.9492 40.9908C41.0132 40.9269 41.0932 40.847 41.1572 40.7831C41.7011 40.2558 42.0691 39.7126 42.3091 39.1694C42.789 38.1629 42.821 37.2363 42.805 36.2457C42.821 35.1114 42.789 33.9611 43.237 32.7788C44.2929 29.3918 46.4527 28.8645 47.4446 26.7716C47.8445 26.0207 48.0525 25.158 47.9885 24.0077Z"
fill={`url(#${id}-paint0)`}
/>
<path
d="M47.9885 24.0077C48.0525 22.8733 47.8445 22.0106 47.4766 21.2916C46.4847 19.1668 44.3089 18.6395 43.253 15.2365C42.805 14.0543 42.837 12.904 42.821 11.7696C42.837 10.9229 42.821 10.0921 42.4851 9.22934C42.2291 8.49441 41.7811 7.75949 40.9652 7.02457C40.9172 6.97664 40.8692 6.92871 40.8212 6.88078C40.4052 6.43344 39.9733 6.1139 39.5253 5.87426C38.3095 5.13933 37.2535 5.13934 36.1017 5.13934C34.9818 5.07543 33.8299 5.0275 32.726 4.61211C29.1423 3.28605 28.6944 1.43277 26.7426 0.538082C26.0226 0.170621 25.1587 -0.0530492 24.0228 0.0108571C23.9108 0.0108571 23.8149 0.0268346 23.7029 0.0268346C23.0949 0.010858 22.551 0.106717 22.071 0.266483C19.6651 0.898752 19.1697 2.59445 16.7115 4.0539C16.6591 4.08501 16.6025 4.11161 16.5479 4.13864C16.5085 4.15814 16.467 4.17528 16.4277 4.19492C13.4723 5.6717 11.7232 4.87105 9.81621 5.36301C8.9043 5.53875 7.99239 5.98609 7.04848 7.04055C6.9857 7.1346 6.89377 7.20649 6.81259 7.28521C6.29026 7.79175 5.94227 8.31304 5.70461 8.86188C4.40873 11.2743 5.52863 12.9838 3.84879 16.8981C3.7368 17.1537 3.60881 17.3774 3.46482 17.6011C2.28094 19.3585 0.985065 19.9336 0.377124 21.6591C0.10515 22.2982 -0.0388336 23.0491 0.00916172 24.0077C-0.0388336 24.9503 0.105154 25.7012 0.36113 26.3403C0.985069 28.1136 2.32894 28.6728 3.52882 30.5261C3.64081 30.7018 3.7368 30.8936 3.8328 31.1013C5.48064 34.9197 4.44073 36.6451 5.60862 38.9777C5.88059 39.6647 6.31255 40.3357 7.03249 40.9588C7.06448 40.9908 7.11247 41.0227 7.14447 41.0547C7.8164 41.7736 8.47234 42.189 9.12828 42.4286C11.3361 43.3553 13.1439 42.1571 16.4716 43.8506C16.7275 43.9624 16.9675 44.1062 17.1915 44.25C18.9993 45.4802 19.6713 46.8062 21.3831 47.4932C22.103 47.8447 22.9509 48.0524 23.9908 47.9885C24.2256 47.9885 24.4608 47.9757 24.6953 47.9637C25.1077 47.9424 25.4839 47.8829 25.8313 47.7852C25.8415 47.7823 25.852 47.7808 25.8627 47.7808C25.8733 47.7808 25.8842 47.7794 25.8945 47.7768C28.6145 47.0853 28.6542 44.9021 32.694 43.4192C34.0379 42.9079 35.4297 42.9559 36.7736 42.844C37.4935 42.8281 38.1975 42.7482 38.9334 42.4127C39.6373 42.1411 40.3093 41.7097 40.9492 40.9908C41.0132 40.9269 41.0932 40.847 41.1572 40.7831C41.7011 40.2558 42.0691 39.7126 42.3091 39.1694C42.789 38.1629 42.821 37.2363 42.805 36.2457C42.821 35.1114 42.789 33.9611 43.237 32.7788C44.2929 29.3918 46.4527 28.8645 47.4446 26.7716C47.8445 26.0207 48.0525 25.158 47.9885 24.0077Z"
fill={`url(#${id}-paint1)`}
/>
</mask>
<g mask={`url(#${id}-mask)`}>
<rect width="48" height="48" fill={`url(#${id}-paint2)`} />
</g>
<defs>
<radialGradient
id={`${id}-paint0`}
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(24 -0.151619) rotate(90) scale(48.3033)"
>
<stop stop-color="#2E2E2E" />
<stop offset="1" stop-color="#080808" />
</radialGradient>
<radialGradient
id={`${id}-paint1`}
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(46.906 -4.75865) rotate(129.211) scale(84.0086 264.059)"
>
<stop stop-color="#8049F2" />
<stop offset="1" stop-color="#6D4EAE" stop-opacity="0" />
</radialGradient>
<radialGradient
id={`${id}-paint2`}
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(46.906 -4.75865) rotate(129.211) scale(84.0086 264.059)"
>
<stop stop-color="#160F1F" />
<stop offset="1" stop-color="#7132F5" />
</radialGradient>
</defs>
</svg>
);
};
================================================
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;
}) => (
<InkPanel className={classNames("ink:min-h-[200px]", className)}>
<InkHeader title={text} icon={<InkIcon.Settings />} />
<div className="ink:flex-1">{text}</div>
</InkPanel>
);
export const ExampleDynamicContent = ({
className,
columns,
}: {
className?: string;
columns?: number;
}) => {
if (!columns || columns === 1)
return <ExamplePanel className={className} text="Only Content" />;
return (
<>
<ExamplePanel className={className} text="Column 1" />
{columns > 1 && <ExamplePanel className={className} text="Column 2" />}
{columns > 2 && <ExamplePanel className={className} text="Column 3" />}
</>
);
};
================================================
FILE: src/layout/ForStories/ExampleLayoutLinks.tsx
================================================
import { InkIcon } from "../..";
import { InkLayoutLink } from "../InkLayout/InkNavLink";
export const EXAMPLE_LINKS: InkLayoutLink[] = [
{
children: "Home",
href: "#home",
leftIcon: <InkIcon.Home />,
target: "_self",
active: true,
},
{
children: "Settings",
href: "#settings",
leftIcon: <InkIcon.Settings />,
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<InkLayoutMobileNavProps, "links">
) => {
const { setIsMobileNavOpen } = useInkLayoutContext();
return (
<InkLayoutMobileNav
links={EXAMPLE_LINKS}
bottom={<div>Bottom content</div>}
{...props}
onLinkClick={() => setIsMobileNavOpen(false)}
/>
);
};
================================================
FILE: src/layout/ForStories/ExampleSideNav.tsx
================================================
import { InkLayoutSideNav } from "../InkLayout/InkLayoutSideNav";
import { EXAMPLE_LINKS } from "./ExampleLayoutLinks";
export const ExampleSideNav = () => {
return (
<InkLayoutSideNav
links={EXAMPLE_LINKS}
bottom={<div>Bottom content</div>}
/>
);
};
================================================
FILE: src/layout/ForStories/ExampleTopNav.tsx
================================================
import { SegmentedControl } from "../../components/SegmentedControl";
export const ExampleTopNav = () => {
return (
<SegmentedControl
options={[
{ children: "Home", value: "home", selectedByDefault: true },
{ children: "Settings", value: "settings" },
]}
onOptionChange={() => {}}
/>
);
};
================================================
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.
* <br/>
* 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<InkLayoutProps> = {
title: "Layouts/InkLayout",
component: InkLayout,
parameters: {
layout: "fullscreen",
},
tags: ["autodocs"],
args: {
children: (
<InkPageLayout>
<InkPanel>
<div>Some content</div>
</InkPanel>
</InkPageLayout>
),
headerContent: <div>Header content</div>,
topNavigation: <ExampleTopNav />,
sideNavigation: <ExampleSideNav />,
mobileNavigation: <ExampleMobileNav />,
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Simple: Story = {
args: {},
};
/**
* The side nav can be a custom component for routing, for instance, if you want to use NextJS' own {`<Link />`} component.
*/
export const SideNavWithCustomButtons: Story = {
args: {
sideNavigation: (
<InkLayoutSideNav
links={[
{
children: <a className="ink:text-button-primary!">Home</a>,
href: "#home",
leftIcon: <InkIcon.Home />,
target: "_self",
asChild: true,
active: true,
},
{
children: <a className="ink:text-button-primary!">Settings</a>,
href: "#settings",
leftIcon: <InkIcon.Settings />,
target: "_self",
asChild: true,
},
]}
/>
),
children: (
<InkPageLayout>
<InkPanel>
The side nav can be a custom component for routing, for instance, if
you want to use NextJS' own {`<Link />`} component.
</InkPanel>
</InkPageLayout>
),
},
};
/**
* The side nav can be a custom component for routing, for instance, if you want to use NextJS' own {`<Link />`} component.
*/
export const StickySideNav: Story = {
args: {
children: (
<InkPageLayout>
<InkPanel className="ink:h-[2000px]">
<div className="ink:flex ink:flex-col ink:flex-1">
<div className="ink:flex-grow">
If the main content is bigger than the screen, the side nav will
be sticky.
</div>
<div className="ink:flex ink:flex-0">Bottom</div>
</div>
</InkPanel>
</InkPageLayout>
),
},
};
================================================
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<InkLayoutProps> = (props) => {
return (
<InkLayoutProvider>
<InkLayoutContent {...props} />
</InkLayoutProvider>
);
};
const InkLayoutContent = ({
className,
mainIcon = <InkIcon.Logo.Placeholder className="ink:size-5" />,
headerContent,
sideNavigation,
topNavigation,
mobileNavigation,
snug = false,
children,
}: InkLayoutProps) => {
const { isMobileNavOpen, setIsMobileNavOpen } = useInkLayoutContext();
return (
<>
<div
className={classNames(
"ink:flex ink:flex-col ink:min-h-full ink:min-w-[320px] ink:font-default ink:text-text-default ink:gap-5 ink:box-border ink:w-full",
className
)}
>
<div className="ink:w-full ink:grid ink:grid-cols-[1fr_auto_1fr] ink:justify-between ink:items-center ink:gap-3 ink:px-3 ink:sm:px-5 ink:py-2 ink:box-border ink:sticky ink:top-0 ink:z-20 ink:backdrop-blur-lg ink:lg:backdrop-blur-none">
<div className="ink:flex ink:gap-1 ink:justify-start ink:items-center">
<div className="ink:hidden ink:lg:block ink:size-5">{mainIcon}</div>
{mobileNavigation && (
<Button
variant="transparent"
size="md"
rounded="full"
className="ink:lg:hidden"
onClick={() => setIsMobileNavOpen(!isMobileNavOpen)}
>
{isMobileNavOpen ? <InkIcon.Close /> : <InkIcon.Menu />}
</Button>
)}
</div>
<div className="ink:flex ink:items-center ink:justify-center ink:gap-2">
<div className="ink:block ink:lg:hidden ink:size-5">{mainIcon}</div>
{topNavigation && (
<div className="ink:hidden ink:lg:block">{topNavigation}</div>
)}
</div>
<div className="ink:flex ink:gap-1 ink:justify-end ink:items-center">
{headerContent}
</div>
</div>
<div
className={classNames(
"ink:flex ink:flex-1 ink:box-border ink:shrink-0 ink:w-full ink:relative",
!snug && "ink:px-3 ink:sm:px-5",
sideNavigation && "ink:lg:pl-0"
)}
>
{sideNavigation && (
<div
style={
{
/** Header height + header top padding + header-content gap */
"--ink-side-nav-height":
"calc(var(--ink-spacing-6) + var(--ink-spacing-5) + var(--ink-spacing-4))",
} as React.CSSProperties
}
className={classNames(
"ink:w-[244px] ink:px-4 ink:hidden ink:lg:block ink:shrink-0 ink:box-border ink:sticky ink:z-10 ink:bottom-0 ink:top-(--ink-side-nav-height) ink:max-h-[calc(100vh-var(--ink-side-nav-height))]"
)}
>
{sideNavigation}
</div>
)}
<div
className={classNames(
"ink:flex-grow ink:flex ink:box-border ink:pb-5 ink:overflow-hidden ink:transition-[filter]",
isMobileNavOpen && "ink:blur-[5px] ink:lg:blur-none"
)}
>
{children}
</div>
</div>
</div>
{isMobileNavOpen && (
<div
style={
{
/** 40px components height + 16px top spacing + 16px spacing between header and content */
"--ink-mobile-nav-height":
"calc(var(--ink-spacing-5) + var(--ink-spacing-2) + var(--ink-spacing-2))",
} as React.CSSProperties
}
className={classNames(
"ink:fixed ink:lg:hidden ink:lg:pointer-events-none ink:inset-0 ink:top-[var(--ink-mobile-nav-height)] ink:z-50",
"ink:bg-background-light/20 ink:backdrop-blur-lg",
"ink:transition-default-animation ink:opacity-100 ink:starting:opacity-0"
)}
>
{mobileNavigation}
</div>
)}
</>
);
};
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 (
<InkLayoutContext.Provider value={{ isMobileNavOpen, setIsMobileNavOpen }}>
{children}
</InkLayoutContext.Provider>
);
};
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<InkLayoutSideNavProps> = ({
links,
bottom,
}) => {
return (
<nav className="ink:h-full ink:flex ink:flex-col ink:font-default ink:text-text-default ink:text-body-3-bold ink:pb-4 ink:gap-4 ink:box-border">
<div className="ink:flex-1 ink:flex ink:flex-col">
{links.map((link) => {
return <InkNavLink {...link} key={link.href} />;
})}
</div>
<div className="ink:flex-1 ink:flex ink:flex-col ink:justify-end">
{bottom}
</div>
</nav>
);
};
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<HTMLElement>;
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<InkNavLinkProps> = ({
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 (
<Component
href={href}
className={classNames(
"ink:flex ink:items-center ink:px-1.5 ink:text-inherit ink:no-underline ink:rounded-md ink:box-border ink:hover:text-text-default",
variantClassNames(variant, {
default: classNames(
"ink:gap-1 ink:py-1 ink:text-body-3-bold",
active
? "ink:bg-background-container ink:text-text-default"
: "ink:text-text-muted"
),
mobile:
"ink:gap-2 ink:py-1.5 ink:text-body-1-bold ink:text-text-default ink:hover:bg-background-container",
}),
className
)}
draggable={false}
onClick={onClick}
{...props}
>
<Slottable child={children}>
{(child) => (
<>
{leftIcon && <div className={iconClasses}>{leftIcon}</div>}
<div
className={classNames(
"ink:flex-1 ink:flex ink:items-center ink:justify-between",
variantClassNames(variant, {
default: "ink:gap-1",
mobile: "ink:gap-2",
})
)}
>
{child}
{rightIcon && <div className={iconClasses}>{rightIcon}</div>}
</div>
</>
)}
</Slottable>
</Component>
);
};
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<InkLayoutMobileNavProps> = {
decorators: [
(Story) => (
<>
<div className="ink:w-full ink:h-full ink:box-border" />
<Story />
</>
),
],
title: "Layouts/InkLayoutMobileNav",
component: InkLayoutMobileNav,
parameters: {
layout: "fullscreen",
},
tags: ["autodocs"],
args: {
links: EXAMPLE_LINKS,
},
};
export default meta;
type Story = StoryObj<typeof meta>;
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<HTMLElement>;
bottom?: React.ReactNode;
}
export const InkLayoutMobileNav: React.FC<InkLayoutMobileNavProps> = ({
links,
onLinkClick,
bottom,
}) => {
return (
<nav className="ink:h-full ink:w-full ink:p-3 ink:box-border ink:font-default ink:text-text-default ink:gap-4 ink:flex ink:flex-col ink:pb-4">
<div className="ink:flex ink:flex-col ink:gap-1">
{links.map((link) => {
return (
<InkNavLink
{...link}
key={link.href}
onClick={onLinkClick}
variant="mobile"
rightIcon={
typeof link.rightIcon === "undefined" ? (
<InkIcon.Chevron className="ink:text-text-muted ink:rotate-270" />
) : (
link.rightIcon
)
}
/>
);
})}
</div>
<div className="ink:flex-1 ink:flex ink:flex-col ink:justify-end">
{bottom}
</div>
</nav>
);
};
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<InkHeaderProps> = {
decorators: [
(Story) => (
<div className="ink:w-full ink:p-3 ink:bg-background-container ink:rounded-lg">
<Story />
</div>
),
],
title: "Layouts/InkHeader",
component: InkHeader,
tags: ["autodocs"],
args: {
title: "Example Title",
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Simple: Story = {
args: {},
};
export const WithIcon: Story = {
args: {
icon: <InkIcon.Home />,
},
};
================================================
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<InkHeaderProps> = ({
title,
icon,
children,
}) => {
return (
<div
className={classNames(
"ink:w-full ink:flex ink:items-center ink:justify-between ink:font-default ink:box-border ink:gap-2 ink:text-text-default"
)}
>
<div className="ink:text-h4 ink:whitespace-nowrap">{title}</div>
{children}
<div className="ink:size-3 ink:shrink-0">{icon}</div>
</div>
);
};
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.
* <br/>
* Note that the `InkLayout` component is included only as an example. It is not required for this component to function.
*/
const meta: Meta<InkPageLayoutProps> = {
parameters: {
layout: "fullscreen",
},
decorators: [
(Story, { args }) => (
<InkLayout
sideNavigation={<div>Side Navigation</div>}
headerContent={<div>Header Content</div>}
>
<Story
args={{
...args,
children: args.children ?? (
<ExampleDynamicContent columns={args.columns} />
),
}}
/>
</InkLayout>
),
],
title: "Layouts/InkPageLayout",
component: InkPageLayout,
tags: ["autodocs"],
args: {
columns: 1,
},
};
export default meta;
type Story = StoryObj<typeof meta>;
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<InkPageLayoutProps> = ({
columns = 1,
children,
}) => {
return (
<div
className={classNames(
"ink:grid ink:gap-1 ink:*:min-h-full ink:auto-rows-min ink:md:auto-rows-auto ink:flex-1",
variantClassNames(columns, {
1: "ink:grid-cols-1",
2: "ink:md:grid-cols-[minmax(240px,1fr)_360px]",
3: "ink:*:first:sm:row-span-2 ink:*:first:xl:row-span-1 ink:md:auto-rows-min ink:xl:auto-rows-auto ink:md:grid-cols-[240px_minmax(240px,1fr)] ink:xl:grid-cols-[240px_minmax(240px,1fr)_360px]",
}),
"ink:*:bg-background-light ink:*:rounded-lg"
)}
>
{children}
</div>
);
};
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<InkPanelProps> = {
title: "Layouts/InkPanel",
component: InkPanel,
tags: ["autodocs"],
args: {
size: "md",
children: (
<>
<InkHeader
title="A header is always nice"
icon={<InkIcon.Settings />}
/>
<div>And then some text here, how fun! And some more!</div>
<div>Some footer</div>
</>
),
},
};
export default meta;
type Story = StoryObj<typeof meta>;
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: <div>Just some content here</div>,
},
};
================================================
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<HTMLDivElement, InkPanelProps>(
(
{ className, size = "auto", centered = false, shadow = false, children },
ref
) => {
return (
<div
ref={ref}
className={classNames(
"ink:flex ink:flex-col ink:justify-between ink:gap-3 ink:p-3 ink:box-border",
"ink:*:nth-2:flex-1 ink:*:nth-2:flex ink:*:nth-2:items-start ink:*:nth-2:justify-start",
"ink:bg-background-light ink:rounded-lg",
"ink:font-default ink:text-text-default",
"ink:transition-default-animation ink:in-data-closed:scale-95 ink:in-data-closed:opacity-0",
shadow && "ink:shadow-md",
variantClassNames(size, {
auto: "",
lg: "ink:min-w-[320px] ink:sm:min-w-[640px] ink:min-h-[480px] ink:max-w-4xl",
md: "ink:min-w-[200px] ink:sm:min-w-[300px] ink:min-h-[300px]",
}),
centered &&
"ink:justify-center ink:items-center ink:*:nth-2:items-center ink:*:nth-2:justify-center",
className
)}
>
{children}
</div>
);
}
);
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<Chain["id"], Transport>;
export const inkConfig = {
chains,
transports,
} as const;
================================================
FILE: src/stories/Welcome.mdx
================================================
import { Meta } from "@storybook/blocks";
import Banner from "../images/banner.webp?base64";
<Meta title="Welcome" />
<img
src={Banner}
alt="Ink Kit Banner"
style={{ width: "100%", marginBottom: "2rem", borderRadius: "8px" }}
/>
# 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 (
<div>
<Button onClick={() => {}} size="md" variant="secondary">
Ship It
</Button>
</div>
);
}
```
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
<html class="ink:dark-theme">
...
```
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 (
<div className="ink:flex ink:gap-2 ink:flex-wrap ink:font-default">
<h3 className="ink:text-h3 ink:text-text-default ink:w-full">Colors</h3>
{colors.map((color) => (
<div
key={color}
className={classNames(
"ink:px-2 ink:py-1 ink:rounded-full ink:text-[#999]",
color
)}
>
{color}
</div>
))}
<h3 className="ink:text-h3 ink:text-text-default ink:w-full">
Theme-Independent Colors
</h3>
{independentColors.map((color) => (
<div
key={color}
className={classNames(
"ink:px-2 ink:py-1 ink:rounded-full ink:text-[#999]",
color
)}
>
{color}
</div>
))}
</div>
);
}
const meta: Meta = {
title: "Design/Colors",
component: Colors,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof meta>;
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 (
<div className="ink:flex ink:gap-8 ink:flex-wrap ink:font-default">
{shadows.map((sh) => (
<div
key={sh}
className={classNames(
"ink:size-[200px] ink:flex ink:items-center ink:justify-center ink:rounded-lg ink:bg-background-light ink:text-[#999]",
sh
)}
>
{sh}
</div>
))}
</div>
);
}
const meta: Meta = {
title: "Design/Shadows",
component: Shadows,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof meta>;
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-secondar
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
SYMBOL INDEX (100 symbols across 79 files)
FILE: scripts/import-svgs.mjs
function getIconName (line 14) | function getIconName(svg) {
function processSvgsInFolder (line 19) | async function processSvgsInFolder(folder) {
function createIndexFile (line 54) | async function createIndexFile(folder) {
FILE: src/components/Alert/Alert.stories.tsx
type Story (line 18) | type Story = StoryObj<typeof meta>;
FILE: src/components/Alert/Alert.tsx
type AlertProps (line 5) | interface AlertProps {
FILE: src/components/Button/Button.stories.tsx
type Story (line 30) | type Story = StoryObj<typeof meta>;
FILE: src/components/Button/Button.tsx
type ButtonProps (line 5) | interface ButtonProps
FILE: src/components/Card/Card.stories.tsx
type Story (line 47) | type Story = StoryObj<typeof meta>;
FILE: src/components/Card/Card.tsx
type CardProps (line 6) | interface CardProps extends VariantProps<typeof cardVariants> {
FILE: src/components/Card/Content/CallToAction.tsx
type CallToActionProps (line 4) | interface CallToActionProps {
FILE: src/components/Card/Content/CardInfo.tsx
type CardInfoProps (line 6) | interface CardInfoProps extends PropsWithChildren {
FILE: src/components/Card/Content/CardInfos.tsx
type CardInfosProps (line 4) | interface CardInfosProps extends PropsWithChildren {
FILE: src/components/Card/Content/Image.tsx
type ImageProps (line 5) | interface ImageProps extends React.PropsWithChildren {
FILE: src/components/Card/Content/LargeLink.tsx
type LargeLinkProps (line 6) | interface LargeLinkProps extends PropsWithChildren {
FILE: src/components/Card/Content/LargeLinks.tsx
type LargeLinksProps (line 4) | interface LargeLinksProps extends PropsWithChildren {
FILE: src/components/Card/Content/Link.tsx
type LinkProps (line 4) | interface LinkProps {
FILE: src/components/Card/Content/Tagline.tsx
type TaglineProps (line 3) | interface TaglineProps {
FILE: src/components/Card/Content/Tiny.tsx
type TinyProps (line 5) | interface TinyProps extends PropsWithChildren {
FILE: src/components/Card/Content/TitleAndDescription.tsx
type TitleAndDescriptionProps (line 3) | interface TitleAndDescriptionProps {
FILE: src/components/Checkbox/Checkbox.stories.tsx
type Story (line 18) | type Story = StoryObj<typeof meta>;
FILE: src/components/Checkbox/Checkbox.tsx
type CheckboxProps (line 5) | interface CheckboxProps
FILE: src/components/Checkbox/CheckboxLabel.tsx
type CheckboxLabelProps (line 3) | interface CheckboxLabelProps extends FieldLabelProps {}
FILE: src/components/Effects/PlaceholderUntilLoaded.tsx
type PlaceholderUntilLoadedProps (line 5) | interface PlaceholderUntilLoadedProps extends PropsWithChildren {
FILE: src/components/FieldLabel/FieldLabel.tsx
type FieldLabelProps (line 4) | interface FieldLabelProps extends PropsWithChildren {
FILE: src/components/Input/Input.stories.tsx
type Story (line 16) | type Story = StoryObj<typeof meta>;
FILE: src/components/Input/Input.tsx
type InputProps (line 4) | interface InputProps
FILE: src/components/ListItem/ListItem.tsx
type ListItemProps (line 5) | interface ListItemProps
FILE: src/components/Listbox/Listbox.stories.tsx
type ListboxStoryItem (line 12) | interface ListboxStoryItem {
type Story (line 43) | type Story = StoryObj<typeof meta>;
FILE: src/components/Listbox/Listbox.tsx
type ListboxProps (line 4) | interface ListboxProps<T> extends PropsWithChildren {
FILE: src/components/Listbox/ListboxButton.tsx
type ListboxButtonProps (line 7) | interface ListboxButtonProps extends ListItemProps {
FILE: src/components/Listbox/ListboxOption.tsx
type ListboxOptionProps (line 6) | interface ListboxOptionProps<T> extends Omit<ListItemProps, "value"> {
FILE: src/components/Listbox/ListboxOptions.tsx
type ListboxOptionsProps (line 6) | interface ListboxOptionsProps extends PropsWithChildren {
FILE: src/components/Modal/Layouts/CallToActionModalContent.tsx
type CallToActionModalContentProps (line 1) | interface CallToActionModalContentProps {
FILE: src/components/Modal/Modal.stories.tsx
function ModalContent (line 17) | function ModalContent() {
type Story (line 48) | type Story = StoryObj<typeof meta>;
FILE: src/components/Modal/Modal.tsx
type ModalProps (line 14) | interface ModalProps<TOnCloseProps = boolean> {
FILE: src/components/Modal/ModalContext.tsx
type ModalManagementContextProps (line 5) | interface ModalManagementContextProps {
type ModalContextProps (line 24) | interface ModalContextProps {
FILE: src/components/Panel/Panel.tsx
type PanelProps (line 4) | interface PanelProps extends PropsWithChildren {
FILE: src/components/Popover/Content/PopoverContentInfo.tsx
type PopoverContentInfoProps (line 3) | interface PopoverContentInfoProps {
FILE: src/components/Popover/Popover.stories.tsx
type Story (line 60) | type Story = StoryObj<typeof meta>;
FILE: src/components/Popover/Popover.tsx
type PopoverProps (line 5) | interface PopoverProps extends PropsWithChildren {
FILE: src/components/Popover/PopoverButton.tsx
type PopoverButtonProps (line 6) | interface PopoverButtonProps extends PropsWithChildren {
FILE: src/components/Popover/PopoverPanel.tsx
type PopoverPanelProps (line 6) | interface PopoverPanelProps extends PropsWithChildren {
FILE: src/components/Radio/Radio.tsx
type RadioProps (line 5) | interface RadioProps {
FILE: src/components/Radio/RadioGroup.stories.tsx
type Story (line 27) | type Story = StoryObj<typeof meta>;
FILE: src/components/Radio/RadioGroup.tsx
type RadioGroupProps (line 4) | interface RadioGroupProps extends PropsWithChildren {
FILE: src/components/Radio/RadioLabel.tsx
type RadioLabelProps (line 3) | interface RadioLabelProps extends FieldLabelProps {}
FILE: src/components/SegmentedControl/SegmentedControl.stories.tsx
type Story (line 30) | type Story = StoryObj<typeof meta>;
FILE: src/components/SegmentedControl/SegmentedControl.tsx
type SegmentedControlProps (line 6) | type SegmentedControlProps<TOptionValue extends string> = {
type SegmentedControlOption (line 16) | interface SegmentedControlOption<TOptionValue extends string> {
FILE: src/components/Slot/Slot.tsx
type PossibleRef (line 18) | type PossibleRef<T> = React.Ref<T> | undefined;
function setRef (line 20) | function setRef<T>(ref: PossibleRef<T>, value: T) {
function composeRefs (line 28) | function composeRefs<T>(...refs: PossibleRef<T>[]) {
type SlotProps (line 36) | interface SlotProps extends React.HTMLAttributes<HTMLElement> {
type SlotCloneProps (line 74) | interface SlotCloneProps {
type SlottableProps (line 106) | type SlottableProps = {
type AnyProps (line 117) | type AnyProps = Record<string, any>;
function isSlottable (line 119) | function isSlottable(
function mergeProps (line 125) | function mergeProps(slotProps: AnyProps, childProps: AnyProps) {
function getElementRef (line 165) | function getElementRef(element: React.ReactElement) {
FILE: src/components/Tag/Tag.stories.tsx
type Story (line 25) | type Story = StoryObj<typeof meta>;
FILE: src/components/Tag/Tag.tsx
type TagProps (line 43) | interface TagProps
FILE: src/components/Toggle/Toggle.stories.tsx
type Story (line 17) | type Story = StoryObj<typeof meta>;
FILE: src/components/Toggle/Toggle.tsx
type ToggleProps (line 4) | interface ToggleProps {
FILE: src/components/Typography/Typography.stories.tsx
type Story (line 55) | type Story = StoryObj<typeof meta>;
FILE: src/components/Typography/Typography.tsx
type TypographyProps (line 5) | interface TypographyProps
FILE: src/components/Wallet/ConnectWallet.stories.tsx
type Story (line 21) | type Story = StoryObj<typeof meta>;
FILE: src/components/Wallet/ConnectWallet.tsx
type ConnectWalletProps (line 20) | interface ConnectWalletProps {
FILE: src/decorators/MatrixDecorator.tsx
type MatrixDefinition (line 3) | type MatrixDefinition<TProps, TKey extends keyof TProps = keyof TProps> = {
FILE: src/global.d.ts
type StringWithAutocomplete (line 8) | type StringWithAutocomplete<T> = T | (string & Record<never, never>);
FILE: src/hooks/useInkThemeClass.ts
function useInkThemeClass (line 5) | function useInkThemeClass(
FILE: src/hooks/useWindowBreakpoint.ts
constant BREAKPOINTS (line 4) | const BREAKPOINTS = {
FILE: src/hooks/useWindowSize.ts
type WindowSize (line 3) | interface WindowSize {
FILE: src/icons/AllIcons.tsx
type IconsOrFolder (line 6) | interface IconsOrFolder {
function IconsOrFolder (line 33) | function IconsOrFolder({
function isIconFolder (line 83) | function isIconFolder(icon: IconsOrFolder[string]): icon is IconsOrFolder {
FILE: src/icons/Icons.stories.ts
type Story (line 16) | type Story = StoryObj<typeof meta>;
FILE: src/layout/ForStories/ExampleLayoutLinks.tsx
constant EXAMPLE_LINKS (line 4) | const EXAMPLE_LINKS: InkLayoutLink[] = [
FILE: src/layout/InkLayout/InkLayout.stories.tsx
type Story (line 38) | type Story = StoryObj<typeof meta>;
FILE: src/layout/InkLayout/InkLayout.tsx
type InkLayoutProps (line 6) | interface InkLayoutProps extends PropsWithChildren {
FILE: src/layout/InkLayout/InkLayoutSideNav.tsx
type InkLayoutSideNavProps (line 4) | interface InkLayoutSideNavProps {
FILE: src/layout/InkLayout/InkNavLink.tsx
type InkLayoutLink (line 5) | interface InkLayoutLink extends React.ComponentPropsWithoutRef<"a"> {
type InkNavLinkProps (line 16) | interface InkNavLinkProps extends InkLayoutLink {
FILE: src/layout/InkLayout/MobileNav/InkLayoutMobileNav.stories.tsx
type Story (line 29) | type Story = StoryObj<typeof meta>;
FILE: src/layout/InkLayout/MobileNav/InkLayoutMobileNav.tsx
type InkLayoutMobileNavProps (line 5) | interface InkLayoutMobileNavProps {
FILE: src/layout/InkParts/InkHeader.stories.tsx
type Story (line 25) | type Story = StoryObj<typeof meta>;
FILE: src/layout/InkParts/InkHeader.tsx
type InkHeaderProps (line 4) | interface InkHeaderProps extends PropsWithChildren {
FILE: src/layout/InkParts/InkPageLayout.stories.tsx
type Story (line 42) | type Story = StoryObj<typeof meta>;
FILE: src/layout/InkParts/InkPageLayout.tsx
type InkPageLayoutProps (line 4) | interface InkPageLayoutProps extends PropsWithChildren {
FILE: src/layout/InkParts/InkPanel.stories.tsx
type Story (line 27) | type Story = StoryObj<typeof meta>;
FILE: src/layout/InkParts/InkPanel.tsx
type InkPanelProps (line 5) | interface InkPanelProps extends PropsWithChildren {
FILE: src/styles/Colors.stories.tsx
function Colors (line 4) | function Colors() {
type Story (line 64) | type Story = StoryObj<typeof meta>;
FILE: src/styles/Shadows.stories.tsx
function Shadows (line 4) | function Shadows() {
type Story (line 33) | type Story = StoryObj<typeof meta>;
FILE: src/util/classes.ts
function classNames (line 65) | function classNames(...classes: ClassValue[]) {
function variantClassNames (line 69) | function variantClassNames<T extends string | number>(
FILE: src/util/mocks.ts
constant DEFAULT_MOCK_ACCOUNT (line 1) | const DEFAULT_MOCK_ACCOUNT =
Condensed preview — 155 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (207K chars).
[
{
"path": ".dockerignore",
"chars": 55,
"preview": "node_modules\n.github\ndist\n.env*\n*.log \n.git\n.gitignore\n"
},
{
"path": ".github/CODEOWNERS",
"chars": 32,
"preview": "* @inkonchain/developers-secret\n"
},
{
"path": ".github/README.md",
"chars": 3022,
"preview": "<img src=\"../src/images/banner.webp\" alt=\"Ink Kit Banner\" style=\"width: 100%; border-radius: 8px; margin-bottom: 2rem;\" "
},
{
"path": ".github/actions/base-setup/action.yaml",
"chars": 792,
"preview": "name: \"Basic Setup\"\ndescription: \"Basic setup with pnpm and cache restore\"\nruns:\n using: \"composite\"\n steps:\n - nam"
},
{
"path": ".github/dependabot.yml",
"chars": 467,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".github/workflows/pull_request.yml",
"chars": 2554,
"preview": "name: PR Checks\non:\n pull_request:\n push:\n branches:\n - main\n\njobs:\n install_modules:\n runs-on: ubuntu-lat"
},
{
"path": ".github/workflows/securesdlc.yml",
"chars": 857,
"preview": "name: Nautilus SecureSDLC\nrun-name: \"[Nautilus SecureSDLC] Ref:${{ github.ref_name }} Event:${{ github.event_name }}\"\n\no"
},
{
"path": ".gitignore",
"chars": 60,
"preview": ".DS_Store\ndist\nnode_modules\n*storybook.log\nstorybook-static\n"
},
{
"path": ".prettierrc",
"chars": 86,
"preview": "{\n \"trailingComma\": \"es5\",\n \"tabWidth\": 2,\n \"semi\": true,\n \"singleQuote\": false\n}\n"
},
{
"path": ".storybook/main.ts",
"chars": 516,
"preview": "import type { StorybookConfig } from \"@storybook/react-vite\";\n\nconst config: StorybookConfig = {\n stories: [\"../src/**/"
},
{
"path": ".storybook/preview-head.html",
"chars": 59,
"preview": "<link rel=\"icon\" type=\"image/x-icon\" href=\"favicon.ico\" />\n"
},
{
"path": ".storybook/preview.ts",
"chars": 1072,
"preview": "import type { Preview, ReactRenderer } from \"@storybook/react\";\nimport { withThemeByClassName } from \"@storybook/addon-t"
},
{
"path": ".storybook/theme.css",
"chars": 195,
"preview": ".docs-story,\n.sb-show-main {\n background-color: var(--ink-background-dark);\n}\n\n.sb-main-fullscreen {\n height: 100%;\n\n "
},
{
"path": ".vscode/settings.json",
"chars": 320,
"preview": "{\n // Enables tailwind autocompletion in cva function\n // https://cva.style/docs/getting-started/installation#tailwind"
},
{
"path": "Dockerfile",
"chars": 545,
"preview": "FROM node:22-alpine\nRUN corepack enable && corepack prepare pnpm@9.3.0 --activate\nWORKDIR /app\n\n# Install Python and bui"
},
{
"path": "LICENSE",
"chars": 1072,
"preview": "MIT License\n\nCopyright (c) 2024-2025 inkonchain\n\nPermission is hereby granted, free of charge, to any person obtaining a"
},
{
"path": "README.md",
"chars": 1209,
"preview": "# Ink Kit\n\n> **Looking for React UI components?** The ecosystem has matured significantly with excellent options like [s"
},
{
"path": "amplify.yml",
"chars": 411,
"preview": "version: 1\napplications:\n - frontend:\n phases:\n preBuild:\n commands:\n - npm install -g "
},
{
"path": "eslint.config.mjs",
"chars": 1630,
"preview": "import { FlatCompat } from \"@eslint/eslintrc\";\nimport js from \"@eslint/js\";\nimport importsPlugin from \"eslint-plugin-imp"
},
{
"path": "package.json",
"chars": 2864,
"preview": "{\n \"name\": \"@inkonchain/ink-kit\",\n \"version\": \"0.9.1-beta.19\",\n \"description\": \"React component library for onchain a"
},
{
"path": "scripts/import-svgs.mjs",
"chars": 3258,
"preview": "/** To import SVGs:\n * 1. In Figma, select one of the icons inside the Icons container and select them with CTRL+A (or ⌘"
},
{
"path": "src/components/Alert/Alert.stories.tsx",
"chars": 1268,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { Alert, AlertProps } from \"./Alert\";\nimport { InkIcon } "
},
{
"path": "src/components/Alert/Alert.tsx",
"chars": 2547,
"preview": "import React, { useEffect, useState } from \"react\";\nimport { classNames, variantClassNames } from \"../../util/classes\";\n"
},
{
"path": "src/components/Alert/index.ts",
"chars": 25,
"preview": "export * from \"./Alert\";\n"
},
{
"path": "src/components/Button/Button.stories.tsx",
"chars": 2163,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { fn } from \"@storybook/test\";\n\nimport { Button, type But"
},
{
"path": "src/components/Button/Button.tsx",
"chars": 3775,
"preview": "import React, { PropsWithChildren, forwardRef } from \"react\";\nimport { classNames, variantClassNames } from \"../../util/"
},
{
"path": "src/components/Button/index.ts",
"chars": 26,
"preview": "export * from \"./Button\";\n"
},
{
"path": "src/components/Card/Card.stories.tsx",
"chars": 9683,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { Card, type CardProps, CardContent } from \"./index\";\nimp"
},
{
"path": "src/components/Card/Card.tsx",
"chars": 3569,
"preview": "import React, { forwardRef } from \"react\";\nimport { classNames, variantClassNames } from \"../../util/classes\";\nimport { "
},
{
"path": "src/components/Card/Content/CallToAction.tsx",
"chars": 664,
"preview": "import { classNames } from \"../../../util/classes\";\nimport { TitleAndDescription } from \"./TitleAndDescription\";\n\ninterf"
},
{
"path": "src/components/Card/Content/CardInfo.tsx",
"chars": 908,
"preview": "import { PropsWithChildren } from \"react\";\nimport { classNames } from \"../../../util/classes\";\nimport { TitleAndDescript"
},
{
"path": "src/components/Card/Content/CardInfos.tsx",
"chars": 552,
"preview": "import { PropsWithChildren } from \"react\";\nimport { classNames } from \"../../../util/classes\";\n\nexport interface CardInf"
},
{
"path": "src/components/Card/Content/Image.tsx",
"chars": 1603,
"preview": "import * as React from \"react\";\nimport { classNames } from \"../../../util/classes\";\nimport { Slot } from \"../../Slot\";\n\n"
},
{
"path": "src/components/Card/Content/LargeLink.tsx",
"chars": 1347,
"preview": "import { PropsWithChildren } from \"react\";\nimport { classNames } from \"../../../util/classes\";\nimport { Slot, Slottable "
},
{
"path": "src/components/Card/Content/LargeLinks.tsx",
"chars": 557,
"preview": "import { PropsWithChildren } from \"react\";\nimport { classNames } from \"../../../util/classes\";\n\nexport interface LargeLi"
},
{
"path": "src/components/Card/Content/Link.tsx",
"chars": 853,
"preview": "import { InkIcon } from \"../../..\";\nimport { Tiny } from \"./Tiny\";\n\ninterface LinkProps {\n className?: string;\n title:"
},
{
"path": "src/components/Card/Content/Tagline.tsx",
"chars": 783,
"preview": "import { classNames } from \"../../../util/classes\";\n\ninterface TaglineProps {\n title: React.ReactNode;\n buttons: React"
},
{
"path": "src/components/Card/Content/Tiny.tsx",
"chars": 749,
"preview": "import { PropsWithChildren } from \"react\";\nimport { classNames } from \"../../../util/classes\";\nimport { TitleAndDescript"
},
{
"path": "src/components/Card/Content/TitleAndDescription.tsx",
"chars": 1278,
"preview": "import { classNames, variantClassNames } from \"../../../util/classes\";\n\nexport interface TitleAndDescriptionProps {\n ti"
},
{
"path": "src/components/Card/Content/index.ts",
"chars": 287,
"preview": "export * from \"./CallToAction\";\nexport * from \"./CardInfo\";\nexport * from \"./CardInfos\";\nexport * from \"./Image\";\nexport"
},
{
"path": "src/components/Card/index.ts",
"chars": 66,
"preview": "export * from \"./Card\";\nexport * as CardContent from \"./Content\";\n"
},
{
"path": "src/components/Checkbox/Checkbox.stories.tsx",
"chars": 4203,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { Checkbox, CheckboxProps, CheckboxLabel } from \"./index\""
},
{
"path": "src/components/Checkbox/Checkbox.tsx",
"chars": 2709,
"preview": "import { Checkbox as HeadlessCheckbox } from \"@headlessui/react\";\nimport { classNames } from \"../../util/classes\";\nimpor"
},
{
"path": "src/components/Checkbox/CheckboxLabel.tsx",
"chars": 237,
"preview": "import { FieldLabel, FieldLabelProps } from \"../FieldLabel\";\n\nexport interface CheckboxLabelProps extends FieldLabelProp"
},
{
"path": "src/components/Checkbox/index.ts",
"chars": 61,
"preview": "export * from \"./Checkbox\";\nexport * from \"./CheckboxLabel\";\n"
},
{
"path": "src/components/Effects/PlaceholderUntilLoaded.tsx",
"chars": 1180,
"preview": "import { PropsWithChildren } from \"react\";\nimport { classNames } from \"../../util/classes\";\nimport { Slot } from \"../Slo"
},
{
"path": "src/components/Effects/index.ts",
"chars": 42,
"preview": "export * from \"./PlaceholderUntilLoaded\";\n"
},
{
"path": "src/components/FieldLabel/FieldLabel.tsx",
"chars": 906,
"preview": "import { Description, Field, Label } from \"@headlessui/react\";\nimport { PropsWithChildren } from \"react\";\n\nexport interf"
},
{
"path": "src/components/FieldLabel/index.ts",
"chars": 30,
"preview": "export * from \"./FieldLabel\";\n"
},
{
"path": "src/components/Input/Input.stories.tsx",
"chars": 600,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { Input, type InputProps } from \"./index\";\nimport { InkIc"
},
{
"path": "src/components/Input/Input.tsx",
"chars": 1544,
"preview": "import React, { forwardRef } from \"react\";\nimport { classNames } from \"../../util/classes\";\n\nexport interface InputProps"
},
{
"path": "src/components/Input/index.ts",
"chars": 25,
"preview": "export * from \"./Input\";\n"
},
{
"path": "src/components/ListItem/ListItem.tsx",
"chars": 2808,
"preview": "import { ButtonHTMLAttributes, PropsWithChildren } from \"react\";\nimport { Slot, Slottable } from \"../Slot\";\nimport { cla"
},
{
"path": "src/components/ListItem/index.ts",
"chars": 28,
"preview": "export * from \"./ListItem\";\n"
},
{
"path": "src/components/Listbox/Listbox.stories.tsx",
"chars": 5737,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport {\n Listbox,\n ListboxButton,\n ListboxOption,\n ListboxO"
},
{
"path": "src/components/Listbox/Listbox.tsx",
"chars": 604,
"preview": "import { Listbox as HeadlessListbox } from \"@headlessui/react\";\nimport { PropsWithChildren } from \"react\";\n\nexport inter"
},
{
"path": "src/components/Listbox/ListboxButton.tsx",
"chars": 997,
"preview": "import { ListboxButton as HeadlessListboxButton } from \"@headlessui/react\";\nimport { forwardRef } from \"react\";\nimport {"
},
{
"path": "src/components/Listbox/ListboxOption.tsx",
"chars": 1094,
"preview": "import { ListboxOption as HeadlessListboxOption } from \"@headlessui/react\";\nimport { classNames } from \"../../util/class"
},
{
"path": "src/components/Listbox/ListboxOptions.tsx",
"chars": 692,
"preview": "import { ListboxOptions as HeadlessListboxOptions } from \"@headlessui/react\";\nimport { PropsWithChildren } from \"react\";"
},
{
"path": "src/components/Listbox/index.ts",
"chars": 127,
"preview": "export * from \"./Listbox\";\nexport * from \"./ListboxButton\";\nexport * from \"./ListboxOption\";\nexport * from \"./ListboxOpt"
},
{
"path": "src/components/Modal/Layouts/CallToActionModalContent.tsx",
"chars": 611,
"preview": "export interface CallToActionModalContentProps {\n title: React.ReactNode;\n content: React.ReactNode;\n button: React.R"
},
{
"path": "src/components/Modal/Layouts/index.ts",
"chars": 149,
"preview": "export {\n CallToActionModalContent as CallToAction,\n type CallToActionModalContentProps as CallToActionProps,\n} from \""
},
{
"path": "src/components/Modal/Modal.stories.tsx",
"chars": 2682,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\n\nimport { Button } from \"../Button\";\nimport {\n ModalProvider,\n "
},
{
"path": "src/components/Modal/Modal.tsx",
"chars": 2616,
"preview": "import {\n Dialog,\n DialogBackdrop,\n DialogPanel,\n DialogTitle,\n} from \"@headlessui/react\";\nimport { useModalContext "
},
{
"path": "src/components/Modal/ModalContext.tsx",
"chars": 2170,
"preview": "\"use client\";\n\nimport { createContext, useContext, useMemo, useState } from \"react\";\n\nexport interface ModalManagementCo"
},
{
"path": "src/components/Modal/index.ts",
"chars": 99,
"preview": "export * as ModalLayout from \"./Layouts\";\nexport * from \"./Modal\";\nexport * from \"./ModalContext\";\n"
},
{
"path": "src/components/Panel/Panel.tsx",
"chars": 700,
"preview": "import { classNames } from \"../../util/classes\";\nimport { PropsWithChildren } from \"react\";\n\nexport interface PanelProps"
},
{
"path": "src/components/Panel/index.ts",
"chars": 25,
"preview": "export * from \"./Panel\";\n"
},
{
"path": "src/components/Popover/Content/PopoverContentInfo.tsx",
"chars": 700,
"preview": "import React from \"react\";\n\nexport interface PopoverContentInfoProps {\n title: string;\n content?: React.ReactNode;\n i"
},
{
"path": "src/components/Popover/Content/index.ts",
"chars": 147,
"preview": "export {\n PopoverContentInfo as Info,\n type PopoverContentInfoProps as InfoProps,\n} from \"./PopoverContentInfo\";\nexpor"
},
{
"path": "src/components/Popover/Popover.stories.tsx",
"chars": 1672,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport {\n Popover,\n PopoverButton,\n PopoverContent,\n Popover"
},
{
"path": "src/components/Popover/Popover.tsx",
"chars": 501,
"preview": "import { Popover as HeadlessPopover } from \"@headlessui/react\";\nimport { PropsWithChildren } from \"react\";\nimport { clas"
},
{
"path": "src/components/Popover/PopoverButton.tsx",
"chars": 769,
"preview": "import { PopoverButton as HeadlessPopoverButton } from \"@headlessui/react\";\nimport { PropsWithChildren } from \"react\";\ni"
},
{
"path": "src/components/Popover/PopoverPanel.tsx",
"chars": 863,
"preview": "import { PopoverPanel as HeadlessPopoverPanel } from \"@headlessui/react\";\nimport { PropsWithChildren } from \"react\";\nimp"
},
{
"path": "src/components/Popover/index.ts",
"chars": 137,
"preview": "export * as PopoverContent from \"./Content\";\nexport * from \"./Popover\";\nexport * from \"./PopoverButton\";\nexport * from \""
},
{
"path": "src/components/Radio/Radio.tsx",
"chars": 1333,
"preview": "import { Radio as HeadlessRadio } from \"@headlessui/react\";\nimport { classNames } from \"../../util/classes\";\nimport { Sl"
},
{
"path": "src/components/Radio/RadioGroup.stories.tsx",
"chars": 1500,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { Radio, RadioGroup, RadioGroupProps, RadioLabel } from \""
},
{
"path": "src/components/Radio/RadioGroup.tsx",
"chars": 557,
"preview": "import { RadioGroup as HeadlessRadioGroup } from \"@headlessui/react\";\nimport { PropsWithChildren } from \"react\";\n\nexport"
},
{
"path": "src/components/Radio/RadioLabel.tsx",
"chars": 268,
"preview": "import { FieldLabel, FieldLabelProps } from \"../FieldLabel\";\n\nexport interface RadioLabelProps extends FieldLabelProps {"
},
{
"path": "src/components/Radio/index.ts",
"chars": 85,
"preview": "export * from \"./Radio\";\nexport * from \"./RadioGroup\";\nexport * from \"./RadioLabel\";\n"
},
{
"path": "src/components/SegmentedControl/SegmentedControl.stories.tsx",
"chars": 2238,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { fn } from \"@storybook/test\";\nimport { SegmentedControl,"
},
{
"path": "src/components/SegmentedControl/SegmentedControl.tsx",
"chars": 4715,
"preview": "import React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport { classNames, variantClassNames } from \"../."
},
{
"path": "src/components/SegmentedControl/index.ts",
"chars": 36,
"preview": "export * from \"./SegmentedControl\";\n"
},
{
"path": "src/components/Slot/Slot.tsx",
"chars": 5812,
"preview": "/**\n * This is a modified version of Radix Primitives' Slot component.\n * It supports slottable children, which is usefu"
},
{
"path": "src/components/Slot/index.ts",
"chars": 24,
"preview": "export * from \"./Slot\";\n"
},
{
"path": "src/components/Tag/Tag.stories.tsx",
"chars": 762,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { Tag, type TagProps } from \"./Tag\";\nimport { InkIcon } f"
},
{
"path": "src/components/Tag/Tag.tsx",
"chars": 2163,
"preview": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { classNames }"
},
{
"path": "src/components/Tag/index.ts",
"chars": 23,
"preview": "export * from \"./Tag\";\n"
},
{
"path": "src/components/Toggle/Toggle.stories.tsx",
"chars": 698,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { Toggle, ToggleProps } from \"./index\";\nimport { fn } fro"
},
{
"path": "src/components/Toggle/Toggle.tsx",
"chars": 1650,
"preview": "import { Switch } from \"@headlessui/react\";\nimport { classNames } from \"../../util/classes\";\n\nexport interface TogglePro"
},
{
"path": "src/components/Toggle/index.ts",
"chars": 26,
"preview": "export * from \"./Toggle\";\n"
},
{
"path": "src/components/Typography/Typography.stories.tsx",
"chars": 1200,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { Typography, TypographyProps } from \"./Typography\";\n\ncon"
},
{
"path": "src/components/Typography/Typography.tsx",
"chars": 2027,
"preview": "import { classNames, variantClassNames } from \"../../util/classes\";\nimport { HTMLAttributes, PropsWithChildren } from \"r"
},
{
"path": "src/components/Typography/index.ts",
"chars": 30,
"preview": "export * from \"./Typography\";\n"
},
{
"path": "src/components/Wallet/ConnectWallet.stories.tsx",
"chars": 987,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\n\nimport { ConnectWallet, type ConnectWalletProps } from \"./index"
},
{
"path": "src/components/Wallet/ConnectWallet.tsx",
"chars": 4972,
"preview": "import { useAccount, useBalance, useConnect, useDisconnect } from \"wagmi\";\nimport { Button } from \"../Button\";\nimport {\n"
},
{
"path": "src/components/Wallet/index.ts",
"chars": 33,
"preview": "export * from \"./ConnectWallet\";\n"
},
{
"path": "src/components/index.ts",
"chars": 376,
"preview": "export * from \"./Alert\";\nexport * from \"./Button\";\nexport * from \"./Card\";\nexport * from \"./Checkbox\";\nexport * from \"./"
},
{
"path": "src/decorators/ContainerColor.tsx",
"chars": 197,
"preview": "import { Decorator } from \"@storybook/react\";\n\nexport const ContainerColor: Decorator = (Story) => {\n return (\n <div"
},
{
"path": "src/decorators/MatrixDecorator.tsx",
"chars": 1298,
"preview": "import { Decorator } from \"@storybook/react\";\n\ntype MatrixDefinition<TProps, TKey extends keyof TProps = keyof TProps> ="
},
{
"path": "src/decorators/WalletProvider.tsx",
"chars": 885,
"preview": "import { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { metaMask, mock } from \"wagmi/connecto"
},
{
"path": "src/global.d.ts",
"chars": 199,
"preview": "/// <reference types=\"vite-plugin-svgr/client\" />\n\ndeclare module \"*?base64\" {\n const value: string;\n export default v"
},
{
"path": "src/hooks/index.ts",
"chars": 36,
"preview": "export * from \"./useInkThemeClass\";\n"
},
{
"path": "src/hooks/useEnsImageOrDefault.ts",
"chars": 345,
"preview": "import { useEnsAvatar } from \"wagmi\";\nimport Avatar from \"../images/avatar.png?base64\";\nimport { normalize } from \"viem/"
},
{
"path": "src/hooks/useEnsNameOrDefault.ts",
"chars": 442,
"preview": "import { Address } from \"viem\";\nimport { useEnsName } from \"wagmi\";\nimport { trimAddress } from \"../util/trim\";\nimport {"
},
{
"path": "src/hooks/useInkThemeClass.ts",
"chars": 496,
"preview": "import { useEffect } from \"react\";\n\nconst themeClasses = [\"dark\", \"light\", \"contrast\", \"neo\", \"morpheus\"] as const;\n\nexp"
},
{
"path": "src/hooks/useWindowBreakpoint.ts",
"chars": 368,
"preview": "import { useMemo } from \"react\";\nimport { useWindowSize } from \"./useWindowSize\";\n\nconst BREAKPOINTS = {\n sm: 640,\n md"
},
{
"path": "src/hooks/useWindowSize.ts",
"chars": 622,
"preview": "import { useState, useEffect } from \"react\";\n\ninterface WindowSize {\n width: number;\n height: number;\n}\n\nexport const "
},
{
"path": "src/icons/AllIcons.css",
"chars": 521,
"preview": ".tooltip-on-hover {\n position: relative;\n}\n\n.tooltip-on-hover:before {\n display: block;\n content: attr(data-title);\n "
},
{
"path": "src/icons/AllIcons.tsx",
"chars": 2324,
"preview": "import { classNames } from \"../util/classes\";\nimport * as Icons from \"./index\";\nimport \"./AllIcons.css\";\nimport React fr"
},
{
"path": "src/icons/Icons.stories.ts",
"chars": 591,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { AllIcons } from \"./AllIcons\";\n\nconst meta: Meta<{}> = {"
},
{
"path": "src/icons/Logo/Placeholder.tsx",
"chars": 6638,
"preview": "import { SVGProps, useId } from \"react\";\n\nexport const Placeholder = (props: SVGProps<SVGSVGElement>) => {\n const id = "
},
{
"path": "src/icons/Logo/index.ts",
"chars": 158,
"preview": "/**\n * This file is auto-generated by the `import-svgs.mjs` script.\n */\n\nexport { default as Ink } from \"./Ink.svg?react"
},
{
"path": "src/icons/Page/index.ts",
"chars": 227,
"preview": "/**\n * This file is auto-generated by the `import-svgs.mjs` script.\n */\n\nexport { default as One } from \"./One.svg?react"
},
{
"path": "src/icons/Social/index.ts",
"chars": 411,
"preview": "/**\n * This file is auto-generated by the `import-svgs.mjs` script.\n */\n\nexport { default as Discord } from \"./Discord.s"
},
{
"path": "src/icons/index.ts",
"chars": 2460,
"preview": "/**\n * This file is auto-generated by the `import-svgs.mjs` script.\n */\n\nexport { default as Apps } from \"./Apps.svg?rea"
},
{
"path": "src/index.ts",
"chars": 143,
"preview": "import \"./tailwind.css\";\n\nexport * from \"./components\";\nexport * from \"./hooks\";\nexport * as InkIcon from \"./icons\";\nexp"
},
{
"path": "src/layout/ForStories/ExampleDynamicContent.tsx",
"chars": 933,
"preview": "import { InkIcon } from \"../..\";\nimport { classNames } from \"../../util/classes\";\nimport { InkHeader } from \"../InkParts"
},
{
"path": "src/layout/ForStories/ExampleLayoutLinks.tsx",
"chars": 378,
"preview": "import { InkIcon } from \"../..\";\nimport { InkLayoutLink } from \"../InkLayout/InkNavLink\";\n\nexport const EXAMPLE_LINKS: I"
},
{
"path": "src/layout/ForStories/ExampleMobileNav.tsx",
"chars": 548,
"preview": "import { EXAMPLE_LINKS } from \"./ExampleLayoutLinks\";\nimport {\n InkLayoutMobileNav,\n InkLayoutMobileNavProps,\n} from \""
},
{
"path": "src/layout/ForStories/ExampleSideNav.tsx",
"chars": 276,
"preview": "import { InkLayoutSideNav } from \"../InkLayout/InkLayoutSideNav\";\nimport { EXAMPLE_LINKS } from \"./ExampleLayoutLinks\";\n"
},
{
"path": "src/layout/ForStories/ExampleTopNav.tsx",
"chars": 337,
"preview": "import { SegmentedControl } from \"../../components/SegmentedControl\";\n\nexport const ExampleTopNav = () => {\n return (\n "
},
{
"path": "src/layout/InkLayout/InkLayout.stories.tsx",
"chars": 2942,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { InkIcon } from \"../..\";\nimport { InkLayout, InkLayoutPr"
},
{
"path": "src/layout/InkLayout/InkLayout.tsx",
"chars": 4655,
"preview": "import { PropsWithChildren } from \"react\";\nimport { classNames } from \"../../util/classes\";\nimport { Button, InkIcon } f"
},
{
"path": "src/layout/InkLayout/InkLayoutContext.tsx",
"chars": 869,
"preview": "import { createContext, useContext, useEffect, useState } from \"react\";\n\nconst InkLayoutContext = createContext<{\n isMo"
},
{
"path": "src/layout/InkLayout/InkLayoutSideNav.tsx",
"chars": 785,
"preview": "import React from \"react\";\nimport { InkLayoutLink, InkNavLink } from \"./InkNavLink\";\n\nexport interface InkLayoutSideNavP"
},
{
"path": "src/layout/InkLayout/InkNavLink.tsx",
"chars": 2350,
"preview": "import React from \"react\";\nimport { classNames, variantClassNames } from \"../../util/classes\";\nimport { Slot, Slottable "
},
{
"path": "src/layout/InkLayout/MobileNav/InkLayoutMobileNav.stories.tsx",
"chars": 698,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport {\n InkLayoutMobileNav,\n InkLayoutMobileNavProps,\n} from"
},
{
"path": "src/layout/InkLayout/MobileNav/InkLayoutMobileNav.tsx",
"chars": 1289,
"preview": "import React from \"react\";\nimport { InkLayoutLink, InkNavLink } from \"../InkNavLink\";\nimport { InkIcon } from \"../../..\""
},
{
"path": "src/layout/InkLayout/MobileNav/index.ts",
"chars": 38,
"preview": "export * from \"./InkLayoutMobileNav\";\n"
},
{
"path": "src/layout/InkLayout/index.ts",
"chars": 182,
"preview": "export * from \"./InkLayout\";\nexport { useInkLayoutContext } from \"./InkLayoutContext\";\nexport * from \"./InkLayoutSideNav"
},
{
"path": "src/layout/InkParts/InkHeader.stories.tsx",
"chars": 755,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { InkHeader, InkHeaderProps } from \"./InkHeader\";\nimport "
},
{
"path": "src/layout/InkParts/InkHeader.tsx",
"chars": 729,
"preview": "import { PropsWithChildren } from \"react\";\nimport { classNames } from \"../../util/classes\";\n\nexport interface InkHeaderP"
},
{
"path": "src/layout/InkParts/InkPageLayout.stories.tsx",
"chars": 1280,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { InkPageLayout, InkPageLayoutProps } from \"./InkPageLayo"
},
{
"path": "src/layout/InkParts/InkPageLayout.tsx",
"chars": 939,
"preview": "import { PropsWithChildren } from \"react\";\nimport { classNames, variantClassNames } from \"../../util/classes\";\n\nexport i"
},
{
"path": "src/layout/InkParts/InkPanel.stories.tsx",
"chars": 1301,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { InkHeader, InkIcon, InkPanel, InkPanelProps } from \"../"
},
{
"path": "src/layout/InkParts/InkPanel.tsx",
"chars": 1459,
"preview": "import { PropsWithChildren } from \"react\";\nimport { classNames, variantClassNames } from \"../../util/classes\";\nimport { "
},
{
"path": "src/layout/InkParts/index.ts",
"chars": 90,
"preview": "export * from \"./InkHeader\";\nexport * from \"./InkPageLayout\";\nexport * from \"./InkPanel\";\n"
},
{
"path": "src/layout/index.ts",
"chars": 57,
"preview": "export * from \"./InkLayout\";\nexport * from \"./InkParts\";\n"
},
{
"path": "src/providers.index.ts",
"chars": 336,
"preview": "import { Chain } from \"viem\";\nimport { http, Transport } from \"wagmi\";\nimport { inkSepolia } from \"wagmi/chains\";\n\nconst"
},
{
"path": "src/stories/Welcome.mdx",
"chars": 2511,
"preview": "import { Meta } from \"@storybook/blocks\";\nimport Banner from \"../images/banner.webp?base64\";\n\n<Meta title=\"Welcome\" />\n\n"
},
{
"path": "src/styles/Colors.stories.tsx",
"chars": 1833,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { classNames } from \"../util/classes\";\n\nfunction Colors()"
},
{
"path": "src/styles/Shadows.stories.tsx",
"chars": 846,
"preview": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { classNames } from \"../util/classes\";\n\nfunction Shadows("
},
{
"path": "src/styles/theme/colors.base.css",
"chars": 2735,
"preview": "/*\n Colors in this file are automatically computed using the other variables.\n You can override them in a theme if you"
},
{
"path": "src/styles/theme/colors.contrast.css",
"chars": 610,
"preview": ":root,\n:root.ink\\:contrast-theme {\n /* Background */\n --ink-background-dark: rgba(221, 221, 221, 1);\n --ink-backgroun"
},
{
"path": "src/styles/theme/colors.dark.css",
"chars": 1320,
"preview": ":root,\n:root.ink\\:dark-theme {\n /* Background */\n --ink-background-dark: rgba(18, 17, 24);\n --ink-background-light: r"
},
{
"path": "src/styles/theme/colors.light.css",
"chars": 538,
"preview": ":root,\n:root.ink\\:light-theme {\n /* Background */\n --ink-background-dark: rgba(244, 243, 249);\n --ink-background-ligh"
},
{
"path": "src/styles/theme/colors.morpheus.css",
"chars": 666,
"preview": ":root,\n:root.ink\\:morpheus-theme {\n /* Background */\n --ink-background-dark: rgba(15, 13, 35, 1);\n --ink-background-l"
},
{
"path": "src/styles/theme/colors.neo.css",
"chars": 865,
"preview": ":root,\n:root.ink\\:neo-theme {\n --ink-base-font-default: \"Departure Mono\";\n\n /* Background */\n --ink-background-dark: "
},
{
"path": "src/tailwind.css",
"chars": 7164,
"preview": "@import url(\"./styles/theme/colors.dark.css\") layer(ink-theme);\n@import url(\"./styles/theme/colors.neo.css\") layer(ink-t"
},
{
"path": "src/util/classes.ts",
"chars": 1863,
"preview": "import { type ClassValue, clsx } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\nconst colors = [\n "
},
{
"path": "src/util/mocks.ts",
"chars": 84,
"preview": "export const DEFAULT_MOCK_ACCOUNT =\n \"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266\";\n"
},
{
"path": "src/util/trim.ts",
"chars": 196,
"preview": "import { Address } from \"viem\";\n\nexport const trimAddress = (address?: Address) => {\n if (!address || address.length < "
},
{
"path": "tsconfig.json",
"chars": 589,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES5\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"ES2020\", \"DOM\", \"DOM.Ite"
},
{
"path": "vite.config.mts",
"chars": 1507,
"preview": "import { defineConfig } from \"vite\";\nimport dts from \"vite-plugin-dts\";\nimport svgr from \"vite-plugin-svgr\";\nimport pres"
}
]
About this extraction
This page contains the full source code of the inkonchain/ink-kit GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 155 files (183.3 KB), approximately 54.2k tokens, and a symbol index with 100 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.