Convert app into @modernsk/ui component library package

- Add library entry (src/index.ts) re-exporting all components, theme,
  and TooltipProvider
- Add shippable stylesheet (src/styles/index.css): tokens + components
  only, font inlined as base64 at build time
- Build with tsup (ESM + CJS + .d.ts) and esbuild for CSS
- package.json: exports map, files, sideEffects, peerDependencies
  (react/react-dom), correct deps (radix-ui), prepare-on-install
- Fix phantom dependency: declare radix-ui, drop unused @radix-ui/themes
- Remove Storybook boilerplate, Tailwind/PostCSS (unused)
- Keep App.tsx + Rsbuild as dev-only playground (not published)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 23:14:34 +03:00
commit 01d41c2346
24 changed files with 7390 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
# Local
.DS_Store
*.local
*.log*
# Dist
node_modules
dist/
# Profile
.rspack-profile-*/
# IDE
.vscode/*
!.vscode/extensions.json
.idea
# Agent tooling (regenerable, not part of the library)
.agents/
skills-lock.json
+4
View File
@@ -0,0 +1,4 @@
# Lock files
package-lock.json
pnpm-lock.yaml
yarn.lock
+3
View File
@@ -0,0 +1,3 @@
{
"singleQuote": true
}
+25
View File
@@ -0,0 +1,25 @@
# AGENTS.md
You are an expert in JavaScript, Rsbuild, and web application development. You write maintainable, performant, and accessible code.
## Commands
- `npm run dev` - Start the dev server
- `npm run build` - Build the app for production
- `npm run preview` - Preview the production build locally
## Docs
- Rsbuild: https://rsbuild.rs/llms.txt
- Rspack: https://rspack.rs/llms.txt
- Rslint: https://rslint.rs/llms.txt
### Rslint
- Run `npm run lint` to lint your code
## Tools
### Prettier
- Run `npm run format` to format your code
+54
View File
@@ -0,0 +1,54 @@
# @modernsk/ui
Tactile, dark-first React component library built on [Radix](https://www.radix-ui.com/) primitives.
Old-iOS skeuomorphism × macOS Sequoia neatness × Ubuntu warmth.
## Install
Git-hosted (pin to a tag):
```bash
npm install github:YOUR_ORG/modernsk-ui#v0.1.0
```
`react` and `react-dom` (>=18) are peer dependencies — your app provides them.
The package builds itself on install via the `prepare` script.
## Usage
Import the stylesheet once at your app root, then use components anywhere:
```tsx
import '@modernsk/ui/styles.css';
import { ThemeProvider, TooltipProvider, Button, Card } from '@modernsk/ui';
export function App() {
return (
<ThemeProvider>
<TooltipProvider delayDuration={200}>
<Card>
<Button variant="primary">Click</Button>
</Card>
</TooltipProvider>
</ThemeProvider>
);
}
```
- `ThemeProvider` manages dark/light via `data-theme` on `<html>` and persists to `localStorage`. Read it with `useTheme()`.
- `TooltipProvider` must wrap any tree that uses `<Tooltip>`.
- All visuals come from CSS custom properties in the shipped stylesheet; the self-hosted display font is inlined, so no extra asset hosting is needed.
## Develop
A live playground (every component on one page) runs via Rsbuild:
```bash
npm run dev # playground at http://localhost:3000
npm run build # build the publishable package into dist/
npm run lint
```
## What ships
`npm publish` / git-install exposes only `dist/` — built ESM + CJS, `.d.ts` types, and `styles.css`. The playground (`src/App.tsx`, Rsbuild config) is dev-only and never published.
+5104
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
{
"name": "@modernsk/ui",
"version": "0.1.0",
"description": "ModernSK — tactile, dark-first React component library built on Radix primitives.",
"license": "MIT",
"type": "module",
"sideEffects": [
"**/*.css"
],
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./styles.css": "./dist/styles.css"
},
"files": [
"dist"
],
"scripts": {
"build": "tsup && npm run build:css",
"build:css": "esbuild src/styles/index.css --bundle --loader:.ttf=dataurl --outfile=dist/styles.css",
"dev": "rsbuild dev --open",
"preview": "rsbuild preview",
"lint": "rslint",
"format": "prettier --write .",
"prepare": "npm run build"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"dependencies": {
"@phosphor-icons/react": "^2.1.10",
"radix-ui": "^1.4.3"
},
"devDependencies": {
"@rsbuild/core": "^2.0.7",
"@rsbuild/plugin-babel": "^1.2.0",
"@rsbuild/plugin-react": "^2.0.0",
"@rslint/core": "^0.5.1",
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"babel-plugin-react-compiler": "^1.0.0",
"esbuild": "^0.25.0",
"prettier": "^3.8.3",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"tsup": "^8.5.0",
"typescript": "^6.0.3"
}
}
+22
View File
@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128" fill="none">
<defs>
<linearGradient id="tile" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#d4ff7a"></stop>
<stop offset="0.58" stop-color="#bef264"></stop>
<stop offset="1" stop-color="#a8e04a"></stop>
</linearGradient>
<linearGradient id="sheet" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#22340a"></stop>
<stop offset="1" stop-color="#162406"></stop>
</linearGradient>
</defs>
<rect x="6" y="6" width="116" height="116" rx="28" fill="url(#tile)"></rect>
<rect x="6" y="6" width="116" height="116" rx="28" fill="none" stroke="#a8e04a" stroke-width="1.5"></rect>
<rect x="6.75" y="6.75" width="114.5" height="114.5" rx="27.5" fill="none" stroke="#ffffff" stroke-opacity="0.55" stroke-width="1.5"></rect>
<rect x="34" y="40" width="60" height="20" rx="7" fill="url(#sheet)" opacity="0.32"></rect>
<rect x="34" y="54" width="60" height="20" rx="7" fill="url(#sheet)" opacity="0.6"></rect>
<rect x="34" y="68" width="60" height="24" rx="8" fill="url(#sheet)"></rect>
<circle cx="46" cy="80" r="3.4" fill="#bef264"></circle>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

+17
View File
@@ -0,0 +1,17 @@
import { defineConfig } from '@rsbuild/core';
import { pluginBabel } from '@rsbuild/plugin-babel';
import { pluginReact } from '@rsbuild/plugin-react';
// Docs: https://rsbuild.rs/config/
export default defineConfig({
plugins: [
pluginReact(),
pluginBabel({
include: /\.[jt]sx?$/,
exclude: [/[\\/]node_modules[\\/]/],
babelLoaderOptions(opts) {
opts.plugins?.unshift('babel-plugin-react-compiler');
},
}),
],
});
+14
View File
@@ -0,0 +1,14 @@
import {
defineConfig,
js,
ts,
reactPlugin,
reactHooksPlugin,
} from '@rslint/core';
export default defineConfig([
js.configs.recommended,
ts.configs.recommended,
reactPlugin.configs.recommended,
reactHooksPlugin.configs.recommended,
]);
+567
View File
@@ -0,0 +1,567 @@
import { useState } from 'react';
import {
ArrowRight,
Trash,
Plus,
DotsThree,
MagnifyingGlass,
Folder,
FolderOpen,
PencilSimple,
Copy,
Tag,
Info,
FilePdf,
FileText,
CheckCircle,
Warning,
WarningOctagon,
} from '@phosphor-icons/react';
import { useTheme } from './components/theme';
import {
AlertDialog,
Badge,
Button,
Callout,
Card,
Checkbox,
Chip,
Control,
Dialog,
DialogClose,
IconButton,
List,
MenuRow,
MenuSeparator,
MenuSurface,
Progress,
RadioGroup,
RadioItem,
Row,
ScrollArea,
SearchField,
SegmentedControl,
Select,
Slider,
Spinner,
Stepper,
Switch,
TBody,
THead,
Table,
Tabs,
TabsContent,
TabsList,
Td,
TextArea,
TextField,
Th,
Tooltip,
Tr,
Window,
} from './components/ui';
const Section = ({ label, children }: { label: string; children: React.ReactNode }) => (
<section>
<div className="sec-label">{label}</div>
{children}
</section>
);
const App = () => {
const { theme, setTheme } = useTheme();
const [seg, setSeg] = useState('list');
const [tab, setTab] = useState('general');
const [count, setCount] = useState(4);
const [chips, setChips] = useState(['design', '2026', 'invoices']);
return (
<div className="msk-felt">
<div className="wrap">
<header>
<div className="topbar">
<div>
<div className="word">
MODERN<b>SK</b> · KITCHEN SINK
</div>
<p className="sub">
Every component, live and interactive, built on Radix Primitives
styled from the global tokens in{' '}
<span className="msk-mono">tokens.css</span> +{' '}
<span className="msk-mono">components.css</span>. Click, toggle,
focus it all responds.
</p>
</div>
<div style={{ flexShrink: 0 }}>
<SegmentedControl
value={theme}
onValueChange={(v) => setTheme(v as 'dark' | 'light')}
items={[
{ value: 'dark', label: 'Dark' },
{ value: 'light', label: 'Light' },
]}
/>
</div>
</div>
</header>
{/* BUTTONS */}
<Section label="Buttons">
<div className="stack">
<div>
<div className="cap">Variants</div>
<div className="cluster">
<Button variant="primary">
<ArrowRight size={16} />
Primary
</Button>
<Button>Push button</Button>
<Button variant="ember">
<Trash size={16} />
Delete
</Button>
<Button variant="ghost">Cancel</Button>
<Button disabled>Disabled</Button>
</div>
</div>
<div>
<div className="cap">Sizes &amp; icon</div>
<div className="cluster">
<Button variant="primary" size="sm">
Small
</Button>
<Button size="sm">Small</Button>
<Button iconOnly aria-label="Add">
<Plus size={16} />
</Button>
<Button iconOnly aria-label="More">
<DotsThree size={16} weight="bold" />
</Button>
</div>
</div>
</div>
</Section>
{/* FIELDS */}
<Section label="Text fields &amp; selects">
<div className="two">
<div className="stack">
<div>
<div className="lab">Name</div>
<TextField defaultValue="Quarterly Report" />
</div>
<div>
<div className="lab">Location</div>
<TextField placeholder="Choose a folder…" />
</div>
<div>
<div className="lab">Search</div>
<SearchField
icon={<MagnifyingGlass size={16} />}
placeholder="Search files…"
/>
</div>
</div>
<div className="stack">
<div>
<div className="lab">View</div>
<Select
defaultValue="name"
aria-label="Sort order"
items={[
{ value: 'name', label: 'Sort by name' },
{ value: 'date', label: 'Sort by date' },
{ value: 'size', label: 'Sort by size' },
]}
/>
</div>
<div>
<div className="lab">Notes</div>
<TextArea
placeholder="Add a note…"
defaultValue="Everything right at your hands."
/>
</div>
</div>
</div>
</Section>
{/* TOGGLES */}
<Section label="Switches · checkboxes · radios">
<div className="three">
<div>
<div className="cap">Switch</div>
<div className="stack">
<Control control={<Switch defaultChecked />}>
Sync across devices
</Control>
<Control control={<Switch />}>Show hidden files</Control>
</div>
</div>
<div>
<div className="cap">Checkbox</div>
<div className="stack">
<Control control={<Checkbox defaultChecked />}>
Include subfolders
</Control>
<Control control={<Checkbox />}>Follow symlinks</Control>
</div>
</div>
<div>
<div className="cap">Radio</div>
<RadioGroup defaultValue="list" className="stack">
<Control control={<RadioItem value="list" />}>List view</Control>
<Control control={<RadioItem value="grid" />}>Grid view</Control>
</RadioGroup>
</div>
</div>
</Section>
{/* CONTROLS */}
<Section label="Segmented · slider · stepper · tabs · progress">
<div className="two">
<div className="stack">
<div>
<div className="cap">Segmented</div>
<SegmentedControl
value={seg}
onValueChange={setSeg}
items={[
{ value: 'icons', label: 'Icons' },
{ value: 'list', label: 'List' },
{ value: 'columns', label: 'Columns' },
{ value: 'gallery', label: 'Gallery' },
]}
/>
</div>
<div>
<div className="cap">Slider &amp; stepper</div>
<div className="cluster">
<Slider defaultValue={[62]} max={100} step={1} />
<Stepper
onDecrement={() => setCount((n) => Math.max(0, n - 1))}
onIncrement={() => setCount((n) => n + 1)}
/>
<span className="msk-mono" style={{ color: 'var(--fg-2)' }}>
{count}
</span>
</div>
</div>
</div>
<div className="stack">
<div>
<div className="cap">Tabs</div>
<Tabs value={tab} onValueChange={setTab}>
<TabsList
items={[
{ value: 'general', label: 'General' },
{ value: 'sharing', label: 'Sharing' },
{ value: 'tags', label: 'Tags' },
]}
/>
<TabsContent value={tab} />
</Tabs>
</div>
<div>
<div className="cap">Progress</div>
<div style={{ marginTop: 6 }}>
<Progress value={64} />
</div>
</div>
</div>
</div>
</Section>
{/* BADGES */}
<Section label="Badges · chips · tags">
<div className="stack">
<div className="cluster">
<Badge variant="lime">Synced</Badge>
<Badge variant="ember">3 conflicts</Badge>
<Badge variant="neutral">Draft</Badge>
<Badge variant="outline">v2.4</Badge>
<Badge variant="neutral" dot style={{ color: 'var(--lime)' }}>
Online
</Badge>
</div>
<div className="cluster">
{chips.map((c) => (
<Chip
key={c}
onRemove={() => setChips((cs) => cs.filter((x) => x !== c))}
>
{c}
</Chip>
))}
</div>
</div>
</Section>
{/* SURFACES */}
<Section label="Cards · list rows · menu">
<div className="three">
<Card>
<div style={{ fontSize: 24, color: 'var(--lime)' }}>
<Folder weight="fill" />
</div>
<div style={{ fontWeight: 600, marginTop: 10 }}>Projects</div>
<div
style={{ color: 'var(--fg-3)', fontSize: 13, marginTop: 2 }}
>
24 items · 1.2 GB
</div>
<div style={{ marginTop: 14 }}>
<Progress value={48} />
</div>
</Card>
<List>
<Row selected>
<Folder weight="fill" size={22} color="#bef264" />
<span className="nm">Projects</span>
<span className="meta">24</span>
</Row>
<Row>
<Folder weight="fill" size={22} color="#e9572b" />
<span className="nm">Invoices</span>
<span className="meta">8</span>
</Row>
<Row>
<FilePdf weight="fill" size={22} color="var(--fg-2)" />
<span className="nm">Contract.pdf</span>
<span className="meta">2.4 MB</span>
</Row>
<Row>
<FileText weight="fill" size={22} color="var(--fg-2)" />
<span className="nm">notes.md</span>
<span className="meta">12 KB</span>
</Row>
</List>
<MenuSurface>
<MenuRow icon={<FolderOpen size={16} />} shortcut="⌘O">
Open
</MenuRow>
<MenuRow icon={<PencilSimple size={16} />} shortcut="⏎">
Rename
</MenuRow>
<MenuRow icon={<Copy size={16} />} shortcut="⌘D">
Duplicate
</MenuRow>
<MenuSeparator />
<MenuRow icon={<Tag size={16} />}>Add Tag</MenuRow>
<MenuRow icon={<Info size={16} />} shortcut="⌘I">
Get Info
</MenuRow>
</MenuSurface>
</div>
</Section>
{/* WINDOW */}
<Section label="Window — components composed">
<Window
title="New Bookmark"
style={{ maxWidth: 600 }}
badge={
<Badge variant="neutral" dot style={{ color: 'var(--lime)' }}>
Saved
</Badge>
}
>
<div className="panel">
<div className="stack">
<div>
<div className="lab">Name</div>
<TextField defaultValue="Projects" />
</div>
<div>
<div className="lab">Location</div>
<SearchField
icon={<Folder size={16} />}
defaultValue="~/Documents/2026"
/>
</div>
<div style={{ marginTop: 2 }}>
<Control control={<Switch defaultChecked />}>
Sync across devices
</Control>
</div>
<div
className="cluster"
style={{
justifyContent: 'flex-end',
marginTop: 6,
gap: 12,
}}
>
<Button variant="ghost">Cancel</Button>
<Button variant="primary">Add Bookmark</Button>
</div>
</div>
</div>
</Window>
<div style={{ marginTop: 14 }}>
<Tooltip
content={
<>
<Info
size={14}
style={{ marginRight: 5, color: 'var(--lime)' }}
/>
Tooltip appears over floating layers
</>
}
>
<Button variant="ghost" size="sm">
Hover for tooltip
</Button>
</Tooltip>
</div>
</Section>
{/* ICON BUTTONS · SPINNER */}
<Section label="Icon buttons · spinner">
<div className="cluster">
<IconButton variant="primary" aria-label="Add">
<Plus size={16} weight="bold" />
</IconButton>
<IconButton aria-label="Edit">
<PencilSimple size={15} />
</IconButton>
<IconButton variant="ember" aria-label="Delete">
<Trash size={15} />
</IconButton>
<IconButton variant="ghost" aria-label="More">
<DotsThree size={18} weight="bold" />
</IconButton>
<IconButton size="lg" aria-label="Open">
<FolderOpen size={18} />
</IconButton>
<span style={{ width: 16 }} />
<Spinner size="sm" />
<Spinner />
<Spinner size="lg" />
</div>
</Section>
{/* CALLOUTS */}
<Section label="Callouts">
<div className="stack">
<Callout variant="info" icon={<Info size={17} weight="fill" />}>
<strong>Heads up.</strong> Files sync automatically when youre
online no manual save needed.
</Callout>
<Callout variant="success" icon={<CheckCircle size={17} weight="fill" />}>
<strong>All set.</strong> Your 24 projects are backed up and
encrypted.
</Callout>
<Callout variant="warning" icon={<Warning size={17} weight="fill" />}>
<strong>Low space.</strong> 1.2 GB left on this device.
</Callout>
<Callout variant="danger" icon={<WarningOctagon size={17} weight="fill" />}>
<strong>3 conflicts.</strong> Some files changed in two places at
once.
</Callout>
</div>
</Section>
{/* TABLE */}
<Section label="Table">
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Owner</Th>
<Th>Status</Th>
<Th style={{ textAlign: 'right' }}>Size</Th>
</Tr>
</THead>
<TBody>
<Tr selected>
<Td>Quarterly Report.pdf</Td>
<Td className="muted">You</Td>
<Td>
<Badge variant="lime">Synced</Badge>
</Td>
<Td className="num">2.4 MB</Td>
</Tr>
<Tr>
<Td>Invoices</Td>
<Td className="muted">Mara K.</Td>
<Td>
<Badge variant="ember">3 conflicts</Badge>
</Td>
<Td className="num">812 KB</Td>
</Tr>
<Tr>
<Td>notes.md</Td>
<Td className="muted">You</Td>
<Td>
<Badge variant="neutral">Draft</Badge>
</Td>
<Td className="num">12 KB</Td>
</Tr>
</TBody>
</Table>
</Section>
{/* SCROLL AREA */}
<Section label="Scroll area">
<Card style={{ padding: 0, maxWidth: 320 }}>
<ScrollArea style={{ height: 160 }}>
<List style={{ border: 'none', boxShadow: 'none', borderRadius: 0 }}>
{Array.from({ length: 12 }).map((_, i) => (
<Row key={i}>
<FileText weight="fill" size={20} color="var(--fg-2)" />
<span className="nm">document-{i + 1}.txt</span>
<span className="meta">{(i + 1) * 7} KB</span>
</Row>
))}
</List>
</ScrollArea>
</Card>
</Section>
{/* DIALOGS */}
<Section label="Dialog · alert dialog">
<div className="cluster">
<Dialog
title="Rename file"
description="Choose a new name for this item."
trigger={<Button variant="primary">Open dialog</Button>}
footer={
<>
<DialogClose asChild>
<Button variant="ghost">Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button variant="primary">Save</Button>
</DialogClose>
</>
}
>
<div className="lab">Name</div>
<TextField defaultValue="Quarterly Report.pdf" autoFocus />
</Dialog>
<AlertDialog
title="Delete 3 files?"
description="This permanently removes the selected files. This action cannot be undone."
cancelLabel="Keep files"
actionLabel="Delete"
destructive
trigger={
<Button variant="ember">
<Trash size={16} />
Delete
</Button>
}
/>
</div>
</Section>
</div>
</div>
);
};
export default App;
Binary file not shown.
+35
View File
@@ -0,0 +1,35 @@
import {
createContext,
useContext,
useEffect,
useState,
type ReactNode,
} from 'react';
type ThemeMode = 'dark' | 'light';
const KEY = 'msk-theme';
const ThemeContext = createContext<{
theme: ThemeMode;
setTheme: (t: ThemeMode) => void;
}>({ theme: 'dark', setTheme: () => {} });
export const useTheme = () => useContext(ThemeContext);
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
const [theme, setTheme] = useState<ThemeMode>(() => {
if (typeof localStorage === 'undefined') return 'dark';
return (localStorage.getItem(KEY) as ThemeMode) || 'dark';
});
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem(KEY, theme);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
+679
View File
@@ -0,0 +1,679 @@
/* ============================================================
ModernSK UI — Radix Primitives wrapped in the ModernSK look.
Logic/accessibility from Radix; every pixel from the tokens in
styles/tokens.css + styles/components.css.
============================================================ */
import {
forwardRef,
useId,
type ComponentPropsWithoutRef,
type CSSProperties,
type ReactNode,
} from 'react';
import {
Switch as RSwitch,
Checkbox as RCheckbox,
RadioGroup as RRadioGroup,
Tabs as RTabs,
Slider as RSlider,
DropdownMenu as RMenu,
Tooltip as RTooltip,
Progress as RProgress,
Select as RSelect,
ToggleGroup as RToggleGroup,
Dialog as RDialog,
AlertDialog as RAlertDialog,
ScrollArea as RScrollArea,
} from 'radix-ui';
import { Check, CaretDown, X } from '@phosphor-icons/react';
const cx = (...c: Array<string | false | undefined>) =>
c.filter(Boolean).join(' ');
/* ---------- BUTTON ---------- */
type BtnVariant = 'key' | 'primary' | 'ember' | 'ghost';
type ButtonProps = ComponentPropsWithoutRef<'button'> & {
variant?: BtnVariant;
size?: 'sm';
iconOnly?: boolean;
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'key', size, iconOnly, className, ...props }, ref) => (
<button
ref={ref}
className={cx(
'msk-btn',
variant !== 'key' && `msk-btn--${variant}`,
size === 'sm' && 'msk-btn--sm',
iconOnly && 'msk-btn--icon',
className,
)}
{...props}
/>
),
);
Button.displayName = 'Button';
/* ---------- TEXT FIELD ---------- */
export const TextField = forwardRef<
HTMLInputElement,
ComponentPropsWithoutRef<'input'>
>(({ className, ...props }, ref) => (
<input ref={ref} className={cx('msk-field', className)} {...props} />
));
TextField.displayName = 'TextField';
export const TextArea = forwardRef<
HTMLTextAreaElement,
ComponentPropsWithoutRef<'textarea'>
>(({ className, ...props }, ref) => (
<textarea ref={ref} className={cx('msk-field', className)} {...props} />
));
TextArea.displayName = 'TextArea';
export const SearchField = ({
icon,
...props
}: ComponentPropsWithoutRef<'input'> & { icon: ReactNode }) => (
<div className="msk-search">
<span className="ph">{icon}</span>
<TextField {...props} />
</div>
);
/* ---------- SELECT (Radix) ---------- */
type SelectProps = {
value?: string;
defaultValue?: string;
onValueChange?: (v: string) => void;
placeholder?: string;
items: Array<{ value: string; label: string }>;
'aria-label'?: string;
};
export const Select = ({
value,
defaultValue,
onValueChange,
placeholder,
items,
...rest
}: SelectProps) => (
<RSelect.Root
value={value}
defaultValue={defaultValue}
onValueChange={onValueChange}
>
<RSelect.Trigger className="msk-select" aria-label={rest['aria-label']}>
<RSelect.Value placeholder={placeholder} />
<RSelect.Icon className="msk-select__icon">
<CaretDown size={12} weight="bold" />
</RSelect.Icon>
</RSelect.Trigger>
<RSelect.Portal>
<RSelect.Content
className="msk-select__content"
position="popper"
sideOffset={6}
>
<RSelect.Viewport>
{items.map((it) => (
<RSelect.Item
key={it.value}
value={it.value}
className="msk-select__item"
>
<RSelect.ItemText>{it.label}</RSelect.ItemText>
<RSelect.ItemIndicator className="msk-select__item-indicator">
<Check size={14} weight="bold" />
</RSelect.ItemIndicator>
</RSelect.Item>
))}
</RSelect.Viewport>
</RSelect.Content>
</RSelect.Portal>
</RSelect.Root>
);
/* ---------- SWITCH ---------- */
export const Switch = (
props: ComponentPropsWithoutRef<typeof RSwitch.Root>,
) => (
<RSwitch.Root className="msk-switch" {...props}>
<RSwitch.Thumb className="msk-switch__thumb" />
</RSwitch.Root>
);
/* ---------- CHECKBOX ---------- */
export const Checkbox = (
props: ComponentPropsWithoutRef<typeof RCheckbox.Root>,
) => (
<RCheckbox.Root className="msk-check" {...props}>
<RCheckbox.Indicator className="msk-check__indicator">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="var(--lime-ink)"
strokeWidth={3.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4 12l5 5L20 6" />
</svg>
</RCheckbox.Indicator>
</RCheckbox.Root>
);
/* ---------- RADIO GROUP ---------- */
export const RadioGroup = RRadioGroup.Root;
export const RadioItem = ({
value,
...props
}: ComponentPropsWithoutRef<typeof RRadioGroup.Item>) => (
<RRadioGroup.Item className="msk-radio" value={value} {...props}>
<RRadioGroup.Indicator className="msk-radio__indicator" />
</RRadioGroup.Item>
);
/* control + label row helper */
export const Control = ({
children,
control,
}: {
children: ReactNode;
control: ReactNode;
}) => (
<label className="msk-control">
{control}
{children}
</label>
);
/* ---------- SEGMENTED CONTROL (ToggleGroup single) ---------- */
type SegProps = {
value: string;
onValueChange: (v: string) => void;
items: Array<{ value: string; label: string }>;
};
export const SegmentedControl = ({
value,
onValueChange,
items,
}: SegProps) => (
<RToggleGroup.Root
type="single"
className="msk-seg"
value={value}
onValueChange={(v) => v && onValueChange(v)}
>
{items.map((it) => (
<RToggleGroup.Item
key={it.value}
value={it.value}
className="msk-seg__item"
>
{it.label}
</RToggleGroup.Item>
))}
</RToggleGroup.Root>
);
/* ---------- SLIDER ---------- */
export const Slider = (
props: ComponentPropsWithoutRef<typeof RSlider.Root>,
) => (
<RSlider.Root className="msk-slider" {...props}>
<RSlider.Track className="msk-slider__track">
<RSlider.Range className="msk-slider__range" />
</RSlider.Track>
<RSlider.Thumb className="msk-slider__thumb" aria-label="Value" />
</RSlider.Root>
);
/* ---------- STEPPER ---------- */
export const Stepper = ({
onDecrement,
onIncrement,
}: {
onDecrement: () => void;
onIncrement: () => void;
}) => (
<div className="msk-stepper">
<button type="button" onClick={onDecrement} aria-label="Decrease">
</button>
<button type="button" onClick={onIncrement} aria-label="Increase">
+
</button>
</div>
);
/* ---------- TABS ---------- */
export const Tabs = RTabs.Root;
export const TabsList = ({
items,
}: {
items: Array<{ value: string; label: string }>;
}) => (
<RTabs.List className="msk-tabs">
{items.map((it) => (
<RTabs.Trigger
key={it.value}
value={it.value}
className="msk-tabs__trigger"
>
{it.label}
</RTabs.Trigger>
))}
</RTabs.List>
);
export const TabsContent = RTabs.Content;
/* ---------- PROGRESS ---------- */
export const Progress = ({ value = 0 }: { value?: number }) => (
<RProgress.Root className="msk-progress" value={value}>
<RProgress.Indicator
className="msk-progress__indicator"
style={{ width: `${value}%` }}
/>
</RProgress.Root>
);
/* ---------- BADGE ---------- */
type BadgeVariant = 'lime' | 'ember' | 'neutral' | 'outline';
export const Badge = ({
variant = 'neutral',
dot,
className,
children,
...props
}: ComponentPropsWithoutRef<'span'> & {
variant?: BadgeVariant;
dot?: boolean;
}) => (
<span
className={cx(
'msk-badge',
`msk-badge--${variant}`,
dot && 'msk-badge--dot',
className,
)}
{...props}
>
{children}
</span>
);
/* ---------- CHIP ---------- */
export const Chip = ({
children,
onRemove,
}: {
children: ReactNode;
onRemove?: () => void;
}) => (
<span className="msk-chip">
{children}
{onRemove && (
<button type="button" className="x" onClick={onRemove} aria-label="Remove">
×
</button>
)}
</span>
);
/* ---------- CARD ---------- */
export const Card = ({
className,
...props
}: ComponentPropsWithoutRef<'div'>) => (
<div className={cx('msk-card', className)} {...props} />
);
/* ---------- LIST ---------- */
export const List = ({
className,
...props
}: ComponentPropsWithoutRef<'div'>) => (
<div className={cx('msk-list', className)} {...props} />
);
export const Row = ({
selected,
className,
...props
}: ComponentPropsWithoutRef<'div'> & { selected?: boolean }) => (
<div
className={cx('msk-row', selected && 'is-selected', className)}
{...props}
/>
);
/* ---------- DROPDOWN MENU (Radix) ---------- */
export const Menu = RMenu.Root;
export const MenuTrigger = RMenu.Trigger;
export const MenuContent = ({
children,
...props
}: ComponentPropsWithoutRef<typeof RMenu.Content>) => (
<RMenu.Portal>
<RMenu.Content className="msk-menu" sideOffset={6} {...props}>
{children}
</RMenu.Content>
</RMenu.Portal>
);
export const MenuItem = ({
icon,
shortcut,
children,
...props
}: ComponentPropsWithoutRef<typeof RMenu.Item> & {
icon?: ReactNode;
shortcut?: string;
}) => (
<RMenu.Item className="msk-menu-item" {...props}>
{icon && <span className="ph">{icon}</span>}
{children}
{shortcut && <span className="sc">{shortcut}</span>}
</RMenu.Item>
);
export const MenuSeparator = () => (
<RMenu.Separator className="msk-menu-sep" />
);
/* Static menu surface — for showcasing the menu without a trigger. */
export const MenuSurface = ({ children }: { children: ReactNode }) => (
<div className="msk-menu">{children}</div>
);
export const MenuRow = ({
icon,
shortcut,
children,
}: {
icon?: ReactNode;
shortcut?: string;
children: ReactNode;
}) => (
<div className="msk-menu-item">
{icon && <span className="ph">{icon}</span>}
{children}
{shortcut && <span className="sc">{shortcut}</span>}
</div>
);
/* ---------- TOOLTIP (Radix) ---------- */
export const Tooltip = ({
content,
children,
}: {
content: ReactNode;
children: ReactNode;
}) => (
<RTooltip.Root>
<RTooltip.Trigger asChild>{children}</RTooltip.Trigger>
<RTooltip.Portal>
<RTooltip.Content className="msk-tooltip" sideOffset={6}>
{content}
</RTooltip.Content>
</RTooltip.Portal>
</RTooltip.Root>
);
/* ---------- ICON BUTTON ---------- */
type IconButtonProps = ComponentPropsWithoutRef<'button'> & {
variant?: BtnVariant;
size?: 'sm' | 'lg';
};
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
({ variant = 'key', size, className, ...props }, ref) => (
<button
ref={ref}
className={cx(
'msk-btn',
'msk-iconbtn',
variant !== 'key' && `msk-btn--${variant}`,
size && `msk-iconbtn--${size}`,
className,
)}
{...props}
/>
),
);
IconButton.displayName = 'IconButton';
/* ---------- SPINNER ----------
Carved donut groove (sunk like the switch well, dark rim at top →
light catch at the bottom) with a glossy lime arc spinning inside it. */
export const Spinner = ({
size,
className,
...props
}: ComponentPropsWithoutRef<'span'> & { size?: 'sm' | 'lg' }) => {
const gid = `msk-groove-${useId()}`;
return (
<span
role="status"
aria-label="Loading"
className={cx('msk-spinner', size && `msk-spinner--${size}`, className)}
{...props}
>
<svg viewBox="0 0 36 36" fill="none">
<defs>
<linearGradient
id={gid}
x1="18"
y1="4"
x2="18"
y2="32"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="var(--spin-groove-1)" />
<stop offset="1" stopColor="var(--spin-groove-2)" />
</linearGradient>
</defs>
{/* carved channel */}
<circle cx="18" cy="18" r="14" stroke={`url(#${gid})`} strokeWidth="5" />
{/* glossy lime arc, nested inside the groove with rounded ends */}
<circle
className="msk-spinner__arc"
cx="18"
cy="18"
r="14"
stroke="var(--lime)"
strokeWidth="3"
strokeLinecap="round"
strokeDasharray="22 88"
/>
</svg>
</span>
);
};
/* ---------- CALLOUT ---------- */
type CalloutVariant = 'info' | 'success' | 'warning' | 'danger';
export const Callout = ({
variant = 'info',
icon,
children,
}: {
variant?: CalloutVariant;
icon?: ReactNode;
children: ReactNode;
}) => (
<div className={cx('msk-callout', variant !== 'info' && `msk-callout--${variant}`)}>
{icon && <span className="msk-callout__icon">{icon}</span>}
<div className="msk-callout__body">{children}</div>
</div>
);
/* ---------- TABLE ---------- */
export const Table = ({
children,
...props
}: ComponentPropsWithoutRef<'table'>) => (
<div className="msk-table-wrap">
<table className="msk-table" {...props}>
{children}
</table>
</div>
);
export const THead = (p: ComponentPropsWithoutRef<'thead'>) => <thead {...p} />;
export const TBody = (p: ComponentPropsWithoutRef<'tbody'>) => <tbody {...p} />;
export const Tr = ({
selected,
className,
...props
}: ComponentPropsWithoutRef<'tr'> & { selected?: boolean }) => (
<tr className={cx(selected && 'is-selected', className)} {...props} />
);
export const Th = (p: ComponentPropsWithoutRef<'th'>) => <th {...p} />;
export const Td = (p: ComponentPropsWithoutRef<'td'>) => <td {...p} />;
/* ---------- SCROLL AREA (Radix) ---------- */
export const ScrollArea = ({
children,
className,
style,
}: {
children: ReactNode;
className?: string;
style?: CSSProperties;
}) => (
<RScrollArea.Root className={cx('msk-scroll', className)} style={style}>
<RScrollArea.Viewport className="msk-scroll__viewport">
{children}
</RScrollArea.Viewport>
<RScrollArea.Scrollbar className="msk-scroll__bar" orientation="vertical">
<RScrollArea.Thumb className="msk-scroll__thumb" />
</RScrollArea.Scrollbar>
<RScrollArea.Scrollbar className="msk-scroll__bar" orientation="horizontal">
<RScrollArea.Thumb className="msk-scroll__thumb" />
</RScrollArea.Scrollbar>
<RScrollArea.Corner />
</RScrollArea.Root>
);
/* ---------- DIALOG / MODAL (Radix Dialog) ---------- */
export const Dialog = ({
trigger,
title,
description,
children,
footer,
open,
onOpenChange,
}: {
trigger?: ReactNode;
title: string;
description?: ReactNode;
children?: ReactNode;
footer?: ReactNode;
open?: boolean;
onOpenChange?: (o: boolean) => void;
}) => (
<RDialog.Root open={open} onOpenChange={onOpenChange}>
{trigger && <RDialog.Trigger asChild>{trigger}</RDialog.Trigger>}
<RDialog.Portal>
<RDialog.Overlay className="msk-overlay" />
<RDialog.Content className="msk-dialog">
<RDialog.Title className="msk-dialog__title">{title}</RDialog.Title>
{description && (
<RDialog.Description className="msk-dialog__desc">
{description}
</RDialog.Description>
)}
{children && <div className="msk-dialog__body">{children}</div>}
{footer && <div className="msk-dialog__footer">{footer}</div>}
<RDialog.Close asChild>
<IconButton
variant="ghost"
size="sm"
className="msk-dialog__close"
aria-label="Close"
>
<X size={14} weight="bold" />
</IconButton>
</RDialog.Close>
</RDialog.Content>
</RDialog.Portal>
</RDialog.Root>
);
/* Low-level Dialog parts (for custom compositions). */
export const DialogClose = RDialog.Close;
/* ---------- ALERT DIALOG (Radix AlertDialog) ---------- */
export const AlertDialog = ({
trigger,
title,
description,
cancelLabel = 'Cancel',
actionLabel = 'Confirm',
destructive,
onAction,
open,
onOpenChange,
}: {
trigger?: ReactNode;
title: string;
description?: ReactNode;
cancelLabel?: string;
actionLabel?: string;
destructive?: boolean;
onAction?: () => void;
open?: boolean;
onOpenChange?: (o: boolean) => void;
}) => (
<RAlertDialog.Root open={open} onOpenChange={onOpenChange}>
{trigger && <RAlertDialog.Trigger asChild>{trigger}</RAlertDialog.Trigger>}
<RAlertDialog.Portal>
<RAlertDialog.Overlay className="msk-overlay" />
<RAlertDialog.Content className="msk-dialog">
<RAlertDialog.Title className="msk-dialog__title">
{title}
</RAlertDialog.Title>
{description && (
<RAlertDialog.Description className="msk-dialog__desc">
{description}
</RAlertDialog.Description>
)}
<div className="msk-dialog__footer">
<RAlertDialog.Cancel asChild>
<Button variant="ghost">{cancelLabel}</Button>
</RAlertDialog.Cancel>
<RAlertDialog.Action asChild>
<Button
variant={destructive ? 'ember' : 'primary'}
onClick={onAction}
>
{actionLabel}
</Button>
</RAlertDialog.Action>
</div>
</RAlertDialog.Content>
</RAlertDialog.Portal>
</RAlertDialog.Root>
);
/* ---------- WINDOW CHROME ---------- */
export const Window = ({
title,
badge,
children,
...props
}: ComponentPropsWithoutRef<'div'> & {
title: string;
badge?: ReactNode;
}) => (
<div className="msk-window" {...props}>
<div className="msk-titlebar">
<span className="msk-traffic r" />
<span className="msk-traffic y" />
<span className="msk-traffic g" />
<span className="ttl">{title}</span>
{badge && (
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
{badge}
</div>
)}
</div>
{children}
</div>
);
+11
View File
@@ -0,0 +1,11 @@
/// <reference types="@rsbuild/core/types" />
/**
* Imports the SVG file as a React component.
* @requires [@rsbuild/plugin-svgr](https://npmjs.com/package/@rsbuild/plugin-svgr)
*/
declare module '*.svg?react' {
import type React from 'react';
const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
export default ReactComponent;
}
+12
View File
@@ -0,0 +1,12 @@
/* ============================================================
ModernSK UI — public package entry.
Import components from here; import the stylesheet once at your
app root: import '@modernsk/ui/styles.css';
============================================================ */
import { Tooltip } from 'radix-ui';
export * from './components/ui';
export { ThemeProvider, useTheme } from './components/theme';
/* Tooltips need a single provider near your app root. */
export const TooltipProvider = Tooltip.Provider;
+20
View File
@@ -0,0 +1,20 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Tooltip } from 'radix-ui';
import App from './App';
import { ThemeProvider } from './components/theme';
import './styles/global.css';
const rootEl = document.getElementById('root');
if (rootEl) {
const root = ReactDOM.createRoot(rootEl);
root.render(
<React.StrictMode>
<ThemeProvider>
<Tooltip.Provider delayDuration={200}>
<App />
</Tooltip.Provider>
</ThemeProvider>
</React.StrictMode>,
);
}
+351
View File
@@ -0,0 +1,351 @@
/* ============================================================
ModernSK — Components layer (ported to Radix Primitives)
Visual recipes kept 1:1 from the Claude Design bundle; state
hooks rewired from .is-* classes to Radix data-attributes
([data-state], [data-highlighted], [data-disabled]).
============================================================ */
@keyframes msk-pop {
from { opacity: 0; transform: scale(.6); }
to { opacity: 1; transform: scale(1); }
}
@keyframes msk-scale-in {
from { opacity: 0; transform: scale(.96); }
to { opacity: 1; transform: scale(1); }
}
@keyframes msk-scale-out {
from { opacity: 1; transform: scale(1); }
to { opacity: 0; transform: scale(.96); }
}
@keyframes msk-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes msk-fade-out { from { opacity: 1; } to { opacity: 0; } }
/* ---------- BUTTONS ---------- */
.msk-btn{
font-family:var(--font-sans); font-size:var(--ctl-font); font-weight:600;
line-height:1.1; padding:var(--ctl-pad-y) var(--ctl-pad-x);
border-radius:var(--r-md); cursor:pointer; user-select:none;
display:inline-flex; align-items:center; justify-content:center; gap:7px;
border:1px solid var(--hair-strong); color:var(--fg-1);
background:var(--grad-key);
box-shadow:var(--shadow-raised);
transition:filter var(--dur-quick) var(--ease-out),
box-shadow var(--dur-quick) var(--ease-out),
transform var(--dur-press) var(--ease-out),
background var(--dur-quick) var(--ease-out);
}
.msk-btn:hover{ background:var(--grad-key-hover); }
.msk-btn:active{ background:var(--grad-key-down); box-shadow:var(--shadow-pressed); transform:translateY(1px); }
.msk-btn:focus-visible{ outline:none; box-shadow:var(--focus-ring), var(--shadow-raised); }
.msk-btn[disabled], .msk-btn.is-disabled{ opacity:.4; filter:saturate(.6); pointer-events:none; }
.msk-btn .ph{ font-size:1.05em; display:inline-flex; }
.msk-btn--primary{
color:var(--lime-ink); background:var(--grad-primary);
border-color:var(--lime-deep);
text-shadow:0 1px 0 rgba(255,255,255,.25);
box-shadow:0 1px 0 rgba(255,255,255,.3) inset, var(--glow-lime), 0 2px 4px rgba(0,0,0,.35);
}
.msk-btn--primary:hover{ filter:brightness(1.04); background:var(--grad-primary); }
.msk-btn--primary:active{ background:var(--grad-primary-down); box-shadow:0 3px 6px rgba(60,90,10,.5) inset; transform:translateY(1px); }
.msk-btn--primary:focus-visible{ box-shadow:var(--focus-ring), 0 1px 0 rgba(255,255,255,.3) inset, 0 2px 4px rgba(0,0,0,.35); }
.msk-btn--ember{
color:#fff; background:var(--grad-ember);
border-color:var(--ember-deep);
text-shadow:0 1px 1px rgba(0,0,0,.25);
box-shadow:0 1px 0 rgba(255,255,255,.08) inset, 0 2px 4px rgba(0,0,0,.35), 0 6px 14px rgba(233,87,43,.18);
}
.msk-btn--ember:hover{ background:var(--grad-ember); filter:brightness(1.06); }
.msk-btn--ember:active{ background:var(--grad-ember-down); box-shadow:0 3px 6px rgba(90,30,10,.55) inset; transform:translateY(1px); }
.msk-btn--ember:focus-visible{ box-shadow:var(--focus-ring-ember), 0 2px 4px rgba(0,0,0,.35); }
.msk-btn--ghost{
color:var(--fg-2); background:transparent; border-color:var(--hair-strong);
box-shadow:none; text-shadow:none;
}
.msk-btn--ghost:hover{ background:rgba(255,255,255,.04); color:var(--fg-1); }
.msk-btn--ghost:active{ background:rgba(0,0,0,.2); box-shadow:var(--shadow-pressed); transform:translateY(1px); }
.msk-btn--sm{ font-size:12px; padding:4px 11px; gap:5px; }
.msk-btn--icon{ padding:var(--ctl-pad-y); width:calc(var(--ctl-font) + 2*var(--ctl-pad-y)); aspect-ratio:1; }
/* ---------- TEXT FIELDS ---------- */
.msk-field{
width:100%; font-family:var(--font-sans); font-size:14px; color:var(--fg-1);
padding:var(--field-pad-y) var(--field-pad-x); border-radius:var(--r-md);
background:var(--steel-900); border:1px solid var(--edge-inset);
box-shadow:var(--shadow-inset-well); outline:none;
transition:box-shadow var(--dur-quick) var(--ease-out), border-color var(--dur-quick) var(--ease-out);
}
.msk-field::placeholder{ color:var(--fg-3); }
.msk-field:focus{ border-color:var(--lime-deep); box-shadow:var(--shadow-inset-well), var(--focus-ring); }
textarea.msk-field{ resize:vertical; min-height:64px; line-height:1.5; }
.msk-search{ position:relative; display:flex; align-items:center; }
.msk-search .ph{ position:absolute; left:11px; color:var(--fg-3); font-size:16px; pointer-events:none; display:inline-flex; }
.msk-search .msk-field{ padding-left:34px; }
/* ---------- SELECT (Radix Select, styled as the glossy key) ---------- */
.msk-select{
font-family:var(--font-sans); font-size:14px; color:var(--fg-1); cursor:pointer;
display:inline-flex; align-items:center; gap:10px;
padding:var(--field-pad-y) 12px var(--field-pad-y) var(--field-pad-x);
border-radius:var(--r-md); border:1px solid var(--hair-strong);
background:var(--grad-key);
box-shadow:var(--shadow-raised); outline:none;
}
.msk-select:hover{ background:var(--grad-key-hover); }
.msk-select:focus-visible{ box-shadow:var(--focus-ring), var(--shadow-raised); }
.msk-select__icon{ color:var(--fg-2); display:inline-flex; margin-left:auto; }
.msk-select__content{
min-width:var(--radix-select-trigger-width); padding:6px; border-radius:var(--r-lg);
border:1px solid var(--hair-strong); background:rgba(34,36,27,.86);
backdrop-filter:blur(24px) saturate(1.3); -webkit-backdrop-filter:blur(24px) saturate(1.3);
box-shadow:var(--shadow-window); z-index:50;
transform-origin:var(--radix-select-content-transform-origin);
}
.msk-select__content[data-state="open"]{ animation:msk-scale-in var(--dur-base) var(--ease-out); }
.msk-select__item{
display:flex; align-items:center; gap:10px; padding:7px 10px; border-radius:var(--r-sm);
font-size:13px; color:var(--fg-1); cursor:pointer; outline:none; user-select:none;
}
.msk-select__item[data-highlighted]{ background:linear-gradient(180deg,var(--lime),var(--lime-deep)); color:var(--lime-ink); }
.msk-select__item-indicator{ margin-left:auto; display:inline-flex; }
/* ---------- SWITCH ---------- */
.msk-switch{
position:relative; width:var(--switch-w); height:var(--switch-h);
flex-shrink:0; border-radius:8px; padding:0; border:1px solid var(--edge-inset);
background:var(--steel-700); box-shadow:var(--shadow-inset-well); cursor:pointer;
transition:background var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out);
}
.msk-switch__thumb{
display:block; position:absolute; top:50%; left:var(--switch-gap); transform:translateY(-50%);
width:var(--switch-knob); height:var(--switch-knob); border-radius:5px;
background:linear-gradient(180deg,#cfd1c4,#a7a99c);
box-shadow:0 1px 2px rgba(0,0,0,.4), 0 1px 0 rgba(255,255,255,.7) inset;
transition:left var(--dur-base) var(--ease-snap), background var(--dur-base) var(--ease-out);
}
.msk-switch[data-state="checked"]{ background:linear-gradient(180deg,var(--lime),var(--lime-deep)); border-color:var(--lime-deep); }
.msk-switch[data-state="checked"] .msk-switch__thumb{ left:calc(100% - var(--switch-knob) - var(--switch-gap)); background:linear-gradient(180deg,#fff,#e6e8dd); }
.msk-switch:focus-visible{ outline:none; box-shadow:var(--shadow-inset-well), var(--focus-ring); }
/* ---------- CHECKBOX ---------- */
.msk-check{
width:22px; height:22px; flex-shrink:0; border-radius:4px; padding:0;
background:var(--steel-900); border:1px solid var(--edge-inset);
box-shadow:var(--shadow-inset-well); cursor:pointer;
display:flex; align-items:center; justify-content:center;
transition:background var(--dur-quick) var(--ease-out), border-color var(--dur-quick) var(--ease-out);
}
.msk-check[data-state="checked"]{ background:linear-gradient(180deg,var(--lime-bright),var(--lime-deep)); border-color:var(--lime-deep); box-shadow:0 1px 0 rgba(255,255,255,.4) inset; }
.msk-check__indicator{ display:flex; align-items:center; justify-content:center; animation:msk-pop var(--dur-quick) var(--ease-snap); }
.msk-check__indicator svg{ width:13px; height:13px; display:block; }
.msk-check:focus-visible{ outline:none; box-shadow:var(--shadow-inset-well), var(--focus-ring); }
/* ---------- RADIO ---------- */
.msk-radio{
width:22px; height:22px; flex-shrink:0; border-radius:50%; padding:0;
background:var(--steel-900); border:1px solid var(--edge-inset);
box-shadow:var(--shadow-inset-well); cursor:pointer;
display:flex; align-items:center; justify-content:center;
transition:border-color var(--dur-quick) var(--ease-out);
}
.msk-radio__indicator{ display:flex; align-items:center; justify-content:center; width:100%; height:100%; }
.msk-radio__indicator::after{
content:""; width:11px; height:11px; border-radius:50%;
background:radial-gradient(circle at 50% 38%,var(--lime-bright),var(--lime-deep));
box-shadow:0 0 8px rgba(190,242,100,.45), 0 1px 0 rgba(255,255,255,.45) inset;
animation:msk-pop var(--dur-quick) var(--ease-snap);
}
.msk-radio:focus-visible{ outline:none; box-shadow:var(--shadow-inset-well), var(--focus-ring); }
/* control + label row helper */
.msk-control{ display:inline-flex; align-items:center; gap:10px; font-size:14px; color:var(--fg-2); cursor:pointer; }
/* ---------- SEGMENTED CONTROL (Radix ToggleGroup) ---------- */
.msk-seg{ display:inline-flex; background:var(--steel-900); border:1px solid var(--edge-inset); border-radius:var(--r-md); padding:3px; box-shadow:var(--shadow-inset-well); gap:2px; }
.msk-seg__item{ font-family:var(--font-sans); font-size:13px; font-weight:600; color:var(--fg-2); background:transparent; border:none; padding:var(--seg-pad-y) 14px; border-radius:var(--r-sm); cursor:pointer; transition:color var(--dur-quick), background var(--dur-quick); }
.msk-seg__item:hover{ color:var(--fg-1); }
.msk-seg__item[data-state="on"]{ color:var(--fg-1); background:var(--grad-key); box-shadow:var(--shadow-raised); }
/* ---------- SLIDER ---------- */
.msk-slider{ position:relative; display:flex; align-items:center; width:200px; height:20px; user-select:none; touch-action:none; }
.msk-slider__track{ position:relative; flex-grow:1; height:6px; border-radius:3px; background:var(--steel-800); box-shadow:var(--shadow-inset-well); }
.msk-slider__range{ position:absolute; height:100%; border-radius:3px; background:linear-gradient(90deg,var(--lime-deep),var(--lime)); box-shadow:0 0 10px rgba(190,242,100,.4); }
.msk-slider__thumb{ display:block; width:20px; height:20px; border-radius:50%; background:linear-gradient(180deg,#fff,#e6e8dd); box-shadow:0 2px 5px rgba(0,0,0,.5), 0 1px 0 rgba(255,255,255,.9) inset; cursor:pointer; outline:none; }
.msk-slider__thumb:focus-visible{ box-shadow:0 2px 5px rgba(0,0,0,.5), 0 1px 0 rgba(255,255,255,.9) inset, var(--focus-ring); }
/* ---------- STEPPER ---------- */
.msk-stepper{ display:inline-flex; border-radius:var(--r-md); overflow:hidden; box-shadow:var(--shadow-raised); border:1px solid var(--hair-strong); }
.msk-stepper button{ font-family:var(--font-mono); font-size:16px; font-weight:600; color:var(--fg-1); background:var(--grad-key); border:none; width:34px; height:var(--stepper-h); cursor:pointer; transition:background var(--dur-quick); }
.msk-stepper button:hover{ background:var(--grad-key-hover); }
.msk-stepper button:active{ background:var(--grad-key-down); }
.msk-stepper button:first-child{ border-right:1px solid var(--edge-inset); }
/* ---------- BADGES / CHIPS / TAGS ---------- */
.msk-badge{ display:inline-flex; align-items:center; gap:5px; font-size:11px; font-weight:700; line-height:1; padding:4px 9px; border-radius:var(--r-pill); letter-spacing:.02em; border:1px solid transparent; }
.msk-badge--lime{ color:var(--lime-ink); background:linear-gradient(180deg,var(--lime-bright),var(--lime-deep)); border-color:var(--lime-deep); }
.msk-badge--ember{ color:#fff; background:var(--grad-ember); border-color:var(--ember-deep); }
.msk-badge--neutral{ color:var(--fg-2); background:var(--steel-700); border-color:var(--hair-strong); }
.msk-badge--outline{ color:var(--fg-2); background:transparent; border-color:var(--hair-strong); }
.msk-badge--dot::before{ content:""; width:6px; height:6px; border-radius:50%; background:currentColor; }
.msk-chip{ display:inline-flex; align-items:center; gap:6px; font-size:13px; font-weight:500; color:var(--fg-1); padding:5px 11px; border-radius:var(--r-pill); background:var(--steel-700); border:1px solid var(--hair-strong); }
.msk-chip .x{ color:var(--fg-3); cursor:pointer; font-size:14px; line-height:1; background:none; border:none; padding:0; display:inline-flex; }
.msk-chip .x:hover{ color:var(--fg-1); }
/* ---------- CARD ---------- */
.msk-card{ background:var(--steel-900); border:1px solid var(--hair); border-radius:var(--r-lg); box-shadow:var(--shadow-card); padding:18px; }
/* ---------- LIST ROWS ---------- */
.msk-list{ background:var(--steel-900); border:1px solid var(--hair); border-radius:var(--r-lg); box-shadow:var(--shadow-card); overflow:hidden; }
.msk-row{ display:flex; align-items:center; gap:12px; padding:10px 14px; border-bottom:1px solid var(--hair); cursor:default; }
.msk-row:last-child{ border-bottom:none; }
.msk-row:hover{ background:rgba(255,255,255,.03); }
.msk-row.is-selected{ background:linear-gradient(180deg,rgba(190,242,100,.16),rgba(190,242,100,.08)); }
.msk-row .nm{ font-size:14px; font-weight:500; color:var(--fg-1); white-space:nowrap; }
.msk-row .meta{ margin-left:auto; font-family:var(--font-mono); font-size:11px; color:var(--fg-3); }
/* ---------- MENU / POPOVER (Radix DropdownMenu) ---------- */
.msk-menu{ min-width:208px; padding:6px; border-radius:var(--r-lg); border:1px solid var(--hair-strong); background:rgba(34,36,27,.86); backdrop-filter:blur(24px) saturate(1.3); -webkit-backdrop-filter:blur(24px) saturate(1.3); box-shadow:var(--shadow-window); z-index:50; transform-origin:var(--radix-dropdown-menu-content-transform-origin); }
.msk-menu[data-state="open"]{ animation:msk-scale-in var(--dur-base) var(--ease-out); }
.msk-menu[data-state="closed"]{ animation:msk-scale-out var(--dur-quick) var(--ease-out); }
.msk-menu-item{ display:flex; align-items:center; gap:10px; padding:7px 10px; border-radius:var(--r-sm); font-size:13px; color:var(--fg-1); cursor:pointer; outline:none; user-select:none; }
.msk-menu-item .ph{ font-size:16px; color:var(--fg-2); display:inline-flex; }
.msk-menu-item .sc{ margin-left:auto; font-family:var(--font-mono); font-size:11px; color:var(--fg-3); }
.msk-menu-item[data-highlighted]{ background:linear-gradient(180deg,var(--lime),var(--lime-deep)); color:var(--lime-ink); }
.msk-menu-item[data-highlighted] .ph, .msk-menu-item[data-highlighted] .sc{ color:var(--lime-ink); }
.msk-menu-sep{ height:1px; background:var(--hair); margin:5px 4px; }
/* ---------- TABS (Radix Tabs) ---------- */
.msk-tabs{ display:flex; gap:2px; border-bottom:1px solid var(--hair); }
.msk-tabs__trigger{ font-family:var(--font-sans); font-size:13px; font-weight:600; color:var(--fg-3); background:transparent; border:none; padding:9px 14px; cursor:pointer; position:relative; transition:color var(--dur-quick); }
.msk-tabs__trigger:hover{ color:var(--fg-2); }
.msk-tabs__trigger[data-state="active"]{ color:var(--fg-1); }
.msk-tabs__trigger[data-state="active"]::after{ content:""; position:absolute; left:8px; right:8px; bottom:-1px; height:2px; border-radius:2px; background:var(--lime); box-shadow:0 0 8px rgba(190,242,100,.5); }
/* ---------- PROGRESS (Radix Progress) ---------- */
.msk-progress{ width:100%; height:8px; border-radius:var(--r-pill); background:var(--steel-800); box-shadow:var(--shadow-inset-well); overflow:hidden; }
.msk-progress__indicator{ display:block; height:100%; border-radius:var(--r-pill); background:linear-gradient(90deg,var(--lime-deep),var(--lime)); box-shadow:0 0 10px rgba(190,242,100,.4); transition:width var(--dur-base) var(--ease-out); }
/* ---------- WINDOW CHROME ---------- */
.msk-window{ border-radius:var(--r-xl); overflow:hidden; border:1px solid var(--hair-strong); background:var(--steel-800); box-shadow:var(--shadow-window); }
.msk-titlebar{ height:42px; display:flex; align-items:center; gap:9px; padding:0 14px; background:var(--grad-titlebar); border-bottom:1px solid rgba(0,0,0,.4); box-shadow:0 1px 0 rgba(255,255,255,.05) inset; }
.msk-titlebar .ttl{ margin-left:6px; font-size:13px; font-weight:600; color:var(--fg-2); }
.msk-traffic{ width:12px; height:12px; border-radius:50%; box-shadow:0 1px 1px rgba(0,0,0,.4) inset, 0 0 0 .5px rgba(0,0,0,.3); }
.msk-traffic.r{ background:radial-gradient(circle at 35% 30%,#ff8a7a,#ec5f55); }
.msk-traffic.y{ background:radial-gradient(circle at 35% 30%,#ffd97a,#e6a93c); }
.msk-traffic.g{ background:radial-gradient(circle at 35% 30%,#bff07a,#9bce4c); }
/* ---------- TOOLTIP (Radix Tooltip) ---------- */
.msk-tooltip{ display:inline-flex; align-items:center; font-size:12px; font-weight:500; color:var(--fg-1); padding:5px 9px; border-radius:var(--r-sm); background:rgba(20,21,15,.92); border:1px solid var(--hair-strong); box-shadow:var(--shadow-card); z-index:60; transform-origin:var(--radix-tooltip-content-transform-origin); }
.msk-tooltip[data-state="delayed-open"], .msk-tooltip[data-state="instant-open"]{ animation:msk-scale-in var(--dur-base) var(--ease-out); }
.msk-tooltip[data-state="closed"]{ animation:msk-fade-out var(--dur-quick) var(--ease-out); }
/* ---------- ICON BUTTON ----------
Square tactile key. Reuses the .msk-btn engine; variants below
just recolor. Pair with .msk-btn / .msk-btn--primary etc. */
.msk-iconbtn{ padding:0; width:32px; height:32px; aspect-ratio:1; display:inline-flex; align-items:center; justify-content:center; }
.msk-iconbtn--sm{ width:26px; height:26px; }
.msk-iconbtn--lg{ width:38px; height:38px; }
/* ---------- SPINNER ----------
A carved donut groove (sunk like the switch well) with a glossy lime
arc spinning *inside* the channel. All drawn in SVG; CSS only sizes
the box and drives the rotation. Honors reduced motion. */
.msk-spinner{ display:inline-block; width:24px; height:24px; line-height:0; }
.msk-spinner--sm{ width:16px; height:16px; }
.msk-spinner--lg{ width:34px; height:34px; }
.msk-spinner svg{ width:100%; height:100%; display:block; }
.msk-spinner__arc{
transform-origin:center; animation:msk-spin .7s linear infinite;
filter:drop-shadow(0 0 3px rgba(190,242,100,.55));
}
@keyframes msk-spin{ to{ transform:rotate(360deg); } }
@media (prefers-reduced-motion:reduce){ .msk-spinner__arc{ animation-duration:1.8s; } }
/* ---------- CALLOUT ----------
Soft semantic surface (1214% tint on the felt) with a glossy
icon chip. Variants reuse the semantic --*-bg tokens. */
.msk-callout{ display:flex; gap:12px; align-items:flex-start; padding:13px 15px; border-radius:var(--r-lg); border:1px solid var(--hair); background:var(--info-bg); color:var(--fg-1); box-shadow:var(--shadow-inset-well); }
.msk-callout__icon{ flex-shrink:0; display:inline-flex; align-items:center; justify-content:center; width:26px; height:26px; border-radius:var(--r-sm); color:var(--info); }
.msk-callout__body{ font-size:13px; line-height:1.5; color:var(--fg-2); padding-top:3px; }
.msk-callout__body strong{ color:var(--fg-1); font-weight:600; }
.msk-callout--success{ background:var(--success-bg); border-color:rgba(190,242,100,.22); }
.msk-callout--success .msk-callout__icon{ color:var(--success); }
.msk-callout--warning{ background:var(--warning-bg); border-color:rgba(230,169,60,.22); }
.msk-callout--warning .msk-callout__icon{ color:var(--warning); }
.msk-callout--danger{ background:var(--danger-bg); border-color:rgba(233,87,43,.25); }
.msk-callout--danger .msk-callout__icon{ color:var(--danger); }
/* ---------- TABLE ----------
The list-row aesthetic stretched to columns: hairline grid, sunk
header strip, mono numerals, lime row selection. */
.msk-table-wrap{ background:var(--steel-900); border:1px solid var(--hair); border-radius:var(--r-lg); box-shadow:var(--shadow-card); overflow:hidden; }
.msk-table{ width:100%; border-collapse:collapse; font-size:14px; }
.msk-table thead th{ text-align:left; font-family:var(--font-sans); font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.14em; color:var(--fg-3); padding:9px 14px; background:var(--grad-titlebar); border-bottom:1px solid var(--hair-strong); box-shadow:0 1px 0 rgba(255,255,255,.04) inset; white-space:nowrap; }
.msk-table tbody td{ padding:10px 14px; border-bottom:1px solid var(--hair); color:var(--fg-1); vertical-align:middle; }
.msk-table tbody tr:last-child td{ border-bottom:none; }
.msk-table tbody tr:hover td{ background:rgba(255,255,255,.03); }
.msk-table tbody tr.is-selected td{ background:linear-gradient(180deg,rgba(190,242,100,.16),rgba(190,242,100,.08)); }
.msk-table .num{ font-family:var(--font-mono); font-size:12px; color:var(--fg-2); text-align:right; }
.msk-table .muted{ color:var(--fg-3); }
[data-theme="light"] .msk-table tbody tr:hover td{ background:rgba(0,0,0,.035); }
/* ---------- SCROLL AREA (Radix) ---------- */
.msk-scroll{ overflow:hidden; }
.msk-scroll__viewport{ width:100%; height:100%; border-radius:inherit; }
.msk-scroll__bar{ display:flex; user-select:none; touch-action:none; padding:2px; background:transparent; transition:background var(--dur-base) var(--ease-out); }
.msk-scroll__bar[data-orientation="vertical"]{ width:10px; }
.msk-scroll__bar[data-orientation="horizontal"]{ flex-direction:column; height:10px; }
.msk-scroll:hover .msk-scroll__bar{ background:rgba(0,0,0,.18); }
.msk-scroll__thumb{ flex:1; background:var(--steel-500); border-radius:var(--r-pill); position:relative; box-shadow:0 1px 0 rgba(255,255,255,.08) inset; }
.msk-scroll__thumb:hover{ background:var(--steel-400); }
.msk-scroll__thumb::before{ content:""; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:100%; height:100%; min-width:44px; min-height:44px; }
/* ---------- DIALOG / MODAL (Radix Dialog + AlertDialog) ----------
The floating sheet: window chrome aesthetic, scrim with blur. */
.msk-overlay{ position:fixed; inset:0; z-index:80; background:rgba(8,9,6,.55); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); }
.msk-overlay[data-state="open"]{ animation:msk-fade-in var(--dur-base) var(--ease-out); }
.msk-overlay[data-state="closed"]{ animation:msk-fade-out var(--dur-quick) var(--ease-out); }
.msk-dialog{ position:fixed; z-index:81; top:50%; left:50%; transform:translate(-50%,-50%); width:min(92vw,460px); max-height:85vh; overflow:auto; border-radius:var(--r-xl); border:1px solid var(--hair-strong); background:var(--steel-800); box-shadow:var(--shadow-window); padding:22px 22px 20px; transform-origin:center; }
.msk-dialog[data-state="open"]{ animation:msk-scale-in var(--dur-base) var(--ease-out); }
.msk-dialog[data-state="closed"]{ animation:msk-scale-out var(--dur-quick) var(--ease-out); }
.msk-dialog__title{ font-family:var(--font-sans); font-size:var(--text-lg); font-weight:600; color:var(--fg-1); letter-spacing:var(--track-snug); }
.msk-dialog__desc{ margin-top:8px; font-size:14px; line-height:1.55; color:var(--fg-2); }
.msk-dialog__body{ margin-top:16px; }
.msk-dialog__footer{ display:flex; justify-content:flex-end; gap:12px; margin-top:22px; }
.msk-dialog__close{ position:absolute; top:14px; right:14px; }
/* ============================================================
LIGHT THEME — component overrides (ported 1:1)
============================================================ */
[data-theme="light"] .msk-btn--ghost:hover{ background:rgba(0,0,0,.05); color:var(--fg-1); }
[data-theme="light"] .msk-btn--ghost:active{ background:rgba(0,0,0,.08); }
[data-theme="light"] .msk-row:hover{ background:rgba(0,0,0,.035); }
[data-theme="light"] .msk-btn--ember{
box-shadow:0 1px 0 rgba(255,255,255,.25) inset, 0 1px 2px rgba(0,0,0,.18), 0 6px 14px rgba(233,87,43,.22);
}
[data-theme="light"] .msk-switch__thumb{
background:linear-gradient(180deg,#ffffff,#e7e7dd);
box-shadow:0 1px 2px rgba(0,0,0,.22), 0 1px 0 rgba(255,255,255,.9) inset;
}
[data-theme="light"] .msk-switch{ background:var(--steel-600); }
[data-theme="light"] .msk-switch[data-state="checked"]{ background:linear-gradient(180deg,var(--lime),var(--lime-deep)); }
[data-theme="light"] .msk-slider__thumb{ box-shadow:0 1px 3px rgba(0,0,0,.28), 0 1px 0 rgba(255,255,255,.95) inset, 0 0 0 .5px rgba(0,0,0,.06); }
[data-theme="light"] .msk-menu{ background:rgba(246,246,239,.82); border-color:var(--hair-strong); }
[data-theme="light"] .msk-select__content{ background:rgba(246,246,239,.82); }
[data-theme="light"] .msk-tooltip{ background:rgba(246,246,239,.94); color:var(--fg-1); }
[data-theme="light"] .msk-titlebar{ border-bottom-color:rgba(0,0,0,.12); }
[data-theme="light"] .msk-traffic{ box-shadow:0 1px 1px rgba(0,0,0,.18) inset, 0 0 0 .5px rgba(0,0,0,.14); }
[data-theme="light"] .msk-overlay{ background:rgba(60,62,52,.32); }
[data-theme="light"] .msk-scroll:hover .msk-scroll__bar{ background:rgba(0,0,0,.06); }
[data-theme="light"] .msk-table thead th{ box-shadow:0 1px 0 rgba(255,255,255,.6) inset; }
+32
View File
@@ -0,0 +1,32 @@
@import './tokens.css';
@import './components.css';
*{ box-sizing:border-box; margin:0; padding:0; }
html{ height:100%; }
body{
min-height:100vh;
font-family:var(--font-sans);
color:var(--fg-1);
-webkit-font-smoothing:antialiased;
}
/* Kitchen Sink layout helpers (ported from the prototype's inline <style>) */
.wrap{ max-width:1100px; margin:0 auto; padding:64px 40px 120px; position:relative; z-index:1; }
.word{ font-family:var(--font-display); font-size:30px; letter-spacing:.01em; text-transform:uppercase; }
.word b{ color:var(--lime); font-weight:400; }
.sub{ color:var(--fg-2); font-size:15px; margin-top:8px; max-width:560px; line-height:1.5; }
.topbar{ display:flex; align-items:flex-start; justify-content:space-between; gap:24px; }
section{ margin-top:52px; }
.sec-label{ font-size:11px; font-weight:600; letter-spacing:.22em; text-transform:uppercase; color:var(--fg-3); margin-bottom:20px; display:flex; align-items:center; gap:12px; }
.sec-label::after{ content:""; flex:1; height:1px; background:var(--hair); }
.grid{ display:grid; gap:18px; }
.cluster{ display:flex; gap:16px; align-items:center; flex-wrap:wrap; }
.stack{ display:flex; flex-direction:column; gap:14px; }
.cap{ font-size:11px; font-weight:600; color:var(--fg-3); text-transform:uppercase; letter-spacing:.14em; margin-bottom:10px; }
.lab{ font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.18em; color:var(--fg-3); margin-bottom:5px; }
.two{ display:grid; grid-template-columns:1fr 1fr; gap:32px; }
.three{ display:grid; grid-template-columns:repeat(3,1fr); gap:24px; align-items:start; }
.panel{ padding:20px; }
+12
View File
@@ -0,0 +1,12 @@
/* ============================================================
ModernSK UI — shippable stylesheet.
Tokens + component styles only. No demo/page layout, no global
reset of consumer elements. Font is inlined at build time.
============================================================ */
@import './tokens.css';
@import './components.css';
/* Non-invasive box-sizing for our own components only (zero specificity). */
:where([class^='msk-'], [class*=' msk-']) {
box-sizing: border-box;
}
+316
View File
@@ -0,0 +1,316 @@
/* ============================================================
ModernSK — Colors & Type Tokens (ported from Claude Design)
Tactile, dark-first design system.
Old-iOS skeuomorphism × macOS Sequoia neatness × Ubuntu warmth.
------------------------------------------------------------
Single source of truth. Every component reads from here.
============================================================ */
@import url('https://fonts.googleapis.com/css2?family=Onest:wght@300;400;500;600;700;800&family=Geist+Mono:wght@400;500;600&display=swap');
/* Anta — squared geometric display face (self-hosted from /public/fonts). */
@font-face {
font-family: 'Anta';
src: url('../assets/Anta-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
:root {
/* ---------- BRAND ---------- */
--lime: #bef264;
--lime-bright: #d4ff7a;
--lime-deep: #a8e04a;
--lime-ink: #1c2a08;
--ember: #e9572b;
--ember-bright: #ff7a52;
--ember-deep: #c4421d;
/* ---------- NEUTRALS (warm-tinted) ---------- */
--ink: #0f100d;
--ink-deep: #090a07;
--ink-raised: #16170f;
--steel-900: #1c1d16;
--steel-800: #22241b;
--steel-700: #2a2c22;
--steel-600: #33342b;
--steel-500: #44463a;
--steel-400: #5a5c4d;
/* ---------- TEXT (on dark) ---------- */
--fg-1: #f3f4ee;
--fg-2: #a7a99c;
--fg-3: #6f7164;
--fg-on-lime: #1c2a08;
--fg-on-ember: #ffffff;
/* ---------- HAIRLINES & STROKES ---------- */
--hair: rgba(255,255,255,0.08);
--hair-strong: rgba(255,255,255,0.14);
--edge-top: rgba(255,255,255,0.10);
--edge-inset: rgba(0,0,0,0.5);
/* ---------- SEMANTIC ---------- */
--success: #bef264;
--warning: #e6a93c;
--danger: #e9572b;
--info: #6db3f2;
--success-bg: rgba(190,242,100,0.12);
--warning-bg: rgba(230,169,60,0.12);
--danger-bg: rgba(233,87,43,0.14);
--info-bg: rgba(109,179,242,0.12);
/* ---------- TYPE ----------
Anta is the GLOBAL typeface. Onest / Geist Mono are fallbacks. */
--font-display: 'Anta', 'Onest', system-ui, sans-serif;
--font-sans: 'Anta', 'Onest', system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-mono: 'Anta', 'Geist Mono', ui-monospace, 'SF Mono', Menlo, monospace;
--text-xs: 12px;
--text-sm: 13px;
--text-base: 15px;
--text-md: 16px;
--text-lg: 18px;
--text-xl: 22px;
--text-2xl: 28px;
--text-3xl: 36px;
--text-4xl: 48px;
--text-5xl: 64px;
--w-light: 300;
--w-regular: 400;
--w-medium: 500;
--w-semibold:600;
--w-bold: 700;
--w-black: 800;
--lh-tight: 1.05;
--lh-snug: 1.25;
--lh-normal: 1.5;
--lh-relaxed: 1.65;
--track-tight: -0.03em;
--track-snug: -0.01em;
--track-wide: 0.02em;
--track-caps: 0.24em;
/* ---------- RADII ---------- */
--r-xs: 3px;
--r-sm: 4px;
--r-md: 6px;
--r-lg: 8px;
--r-xl: 11px;
--r-pill: 999px;
/* ---------- SPACING (4px base) ---------- */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
/* ---------- CONTROL SIZING (compact, global) ---------- */
--ctl-pad-y: 6px;
--ctl-pad-x: 18px;
--ctl-font: 14px;
--field-pad-y: 7px;
--field-pad-x: 13px;
--switch-w: 42px;
--switch-h: 22px;
--switch-knob: 16px;
--switch-gap: 3px;
--seg-pad-y: 4px;
--stepper-h: 30px;
/* ---------- ELEVATION / TACTILE SHADOWS ---------- */
--shadow-raised:
0 1px 0 rgba(255,255,255,0.06) inset,
0 2px 3px rgba(0,0,0,0.5),
0 6px 16px rgba(0,0,0,0.35);
--shadow-raised-hover:
0 1px 0 rgba(255,255,255,0.08) inset,
0 3px 5px rgba(0,0,0,0.5),
0 10px 24px rgba(0,0,0,0.4);
--shadow-pressed:
0 2px 5px rgba(0,0,0,0.6) inset,
0 1px 0 rgba(255,255,255,0.04);
--shadow-inset-well:
0 2px 4px rgba(0,0,0,0.45) inset,
0 1px 0 rgba(255,255,255,0.05);
--shadow-card:
0 1px 0 rgba(255,255,255,0.05) inset,
0 12px 32px rgba(0,0,0,0.45),
0 2px 6px rgba(0,0,0,0.35);
--shadow-window:
0 1px 0 rgba(255,255,255,0.07) inset,
0 30px 80px rgba(0,0,0,0.55),
0 2px 8px rgba(0,0,0,0.4);
--focus-ring: 0 0 0 3px rgba(190,242,100,0.35);
--focus-ring-ember: 0 0 0 3px rgba(233,87,43,0.32);
--glow-lime: 0 6px 22px rgba(190,242,100,0.28);
/* ---------- GRADIENTS (gloss recipes) ---------- */
--grad-key: linear-gradient(180deg, #2f3027, #24251e);
--grad-key-hover:linear-gradient(180deg, #36372e, #2b2c23);
--grad-key-down: linear-gradient(180deg, #1d1e17, #232419);
--grad-primary: linear-gradient(180deg, #cdf972, var(--lime) 55%, #b1e655);
--grad-primary-down: linear-gradient(180deg, var(--lime-deep), var(--lime));
--grad-ember: linear-gradient(180deg, #f86f49, var(--ember) 55%, #d34c22);
--grad-ember-down: linear-gradient(180deg, var(--ember-deep), var(--ember));
--grad-titlebar:linear-gradient(180deg, #26271f, #1c1d16);
/* ---------- MOTION ---------- */
--ease-out: cubic-bezier(.22,.61,.36,1);
--ease-snap: cubic-bezier(.2,.9,.3,1.2);
--dur-press: 60ms;
--dur-quick: 120ms;
--dur-base: 200ms;
/* ---------- THE FELT (background) ---------- */
--bg-glow:
radial-gradient(120% 80% at 50% -10%, rgba(190,242,100,0.10), transparent 55%),
radial-gradient(90% 70% at 85% 110%, rgba(233,87,43,0.06), transparent 60%),
radial-gradient(100% 100% at 50% 50%, #16170f 0%, #0f100d 60%, #090a07 100%);
--grain-url: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='gamma' amplitude='1' exponent='2.8' offset='0'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
--grain-size: 200px;
--grain-opacity: 0.45;
/* ---------- SPINNER GROOVE ----------
Carved donut channel — dark at the top rim, catching light at the
bottom, exactly like the sunk wells (switch / field). */
--spin-groove-1: #090a07; /* top — in shadow */
--spin-groove-2: #34352b; /* bottom — catches light */
}
/* ============================================================
THE FELT — the one global background.
============================================================ */
.msk-felt {
position: relative;
background-color: var(--ink);
background-image: var(--bg-glow);
background-repeat: no-repeat;
background-size: cover;
background-attachment: fixed;
color: var(--fg-1);
}
.msk-felt::before {
content: "";
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
background-image: var(--grain-url);
background-size: var(--grain-size) var(--grain-size);
background-repeat: repeat;
opacity: var(--grain-opacity);
mix-blend-mode: screen;
}
/* ============================================================
LIGHT MODE — warm off-white "paper", Sierra-style gloss.
============================================================ */
[data-theme="light"] {
--ink: #ecece3;
--ink-deep: #deded2;
--ink-raised: #f6f6ef;
--steel-900: #ffffff;
--steel-800: #f4f4ec;
--steel-700: #e9e9df;
--steel-600: #deded2;
--steel-500: #c8c9bc;
--steel-400: #aeb0a2;
--fg-1: #1d1e16;
--fg-2: #5b5d50;
--fg-3: #8a8c7d;
--hair: rgba(0,0,0,0.10);
--hair-strong: rgba(0,0,0,0.16);
--edge-top: rgba(255,255,255,0.9);
--edge-inset: rgba(0,0,0,0.18);
--shadow-raised:
0 1px 0 rgba(255,255,255,0.9) inset,
0 1px 2px rgba(0,0,0,0.12),
0 4px 10px rgba(0,0,0,0.10);
--shadow-raised-hover:
0 1px 0 rgba(255,255,255,0.95) inset,
0 2px 4px rgba(0,0,0,0.14),
0 8px 18px rgba(0,0,0,0.12);
--shadow-pressed:
0 2px 4px rgba(0,0,0,0.18) inset;
--shadow-inset-well:
0 2px 3px rgba(0,0,0,0.10) inset,
0 1px 0 rgba(255,255,255,0.8);
--shadow-card:
0 1px 0 rgba(255,255,255,0.8) inset,
0 10px 28px rgba(0,0,0,0.12),
0 2px 6px rgba(0,0,0,0.08);
--shadow-window:
0 1px 0 rgba(255,255,255,0.9) inset,
0 24px 64px rgba(0,0,0,0.22),
0 2px 8px rgba(0,0,0,0.12);
--grad-key: linear-gradient(180deg, #ffffff, #ecece2);
--grad-key-hover:linear-gradient(180deg, #ffffff, #f3f3ea);
--grad-key-down: linear-gradient(180deg, #e4e4d8, #ededdf);
--grad-titlebar: linear-gradient(180deg, #f4f4ec, #e7e7dd);
--bg-glow:
radial-gradient(120% 80% at 50% -10%, rgba(190,242,100,0.18), transparent 55%),
radial-gradient(90% 70% at 85% 110%, rgba(233,87,43,0.08), transparent 60%),
radial-gradient(100% 100% at 50% 50%, #f2f2ea 0%, #ecece3 60%, #e2e2d6 100%);
/* carved groove on warm paper — top grey shadow, bottom near-white */
--spin-groove-1: #c2c3b6;
--spin-groove-2: #ffffff;
}
/* ============================================================
SEMANTIC TYPE CLASSES
============================================================ */
.msk-display {
font-family: var(--font-display); font-weight: 400;
font-size: var(--text-5xl); line-height: var(--lh-tight);
letter-spacing: -0.01em; color: var(--fg-1);
}
.msk-h1 {
font-family: var(--font-sans); font-weight: var(--w-bold);
font-size: var(--text-3xl); line-height: var(--lh-snug);
letter-spacing: var(--track-snug); color: var(--fg-1);
}
.msk-h2 {
font-family: var(--font-sans); font-weight: var(--w-semibold);
font-size: var(--text-2xl); line-height: var(--lh-snug);
letter-spacing: var(--track-snug); color: var(--fg-1);
}
.msk-h3 {
font-family: var(--font-sans); font-weight: var(--w-semibold);
font-size: var(--text-xl); line-height: var(--lh-snug); color: var(--fg-1);
}
.msk-body {
font-family: var(--font-sans); font-weight: var(--w-regular);
font-size: var(--text-base); line-height: var(--lh-normal); color: var(--fg-2);
}
.msk-body-strong { font-weight: var(--w-medium); color: var(--fg-1); }
.msk-caption {
font-family: var(--font-sans); font-weight: var(--w-medium);
font-size: var(--text-sm); color: var(--fg-3);
}
.msk-label {
font-family: var(--font-sans); font-weight: var(--w-semibold);
font-size: 11px; text-transform: uppercase;
letter-spacing: var(--track-caps); color: var(--fg-3);
}
.msk-mono {
font-family: var(--font-mono); font-weight: var(--w-regular);
font-size: var(--text-sm); color: var(--fg-2);
}
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"ignoreDeprecations": "6.0",
"lib": ["DOM", "ES2020"],
"jsx": "react-jsx",
"target": "ES2020",
"noEmit": true,
"skipLibCheck": true,
"useDefineForClassFields": true,
/* modules */
"moduleDetection": "force",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
/* type checking */
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": ["src"]
}
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: { index: 'src/index.ts' },
format: ['esm', 'cjs'],
dts: true,
clean: true,
treeshake: true,
sourcemap: true,
// react/react-dom come from the host app; radix-ui + phosphor are
// declared dependencies and are externalized automatically by tsup.
external: ['react', 'react-dom'],
});