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:
+20
@@ -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
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Lock files
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
Generated
+5104
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 |
@@ -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');
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -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
@@ -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 & 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 & 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 & 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 you’re
|
||||||
|
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.
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
Vendored
+11
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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>,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (12–14% 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; }
|
||||||
@@ -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; }
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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'],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user