5 Commits

Author SHA1 Message Date
olly a5d2742c7c fixes 2026-05-31 18:09:55 +03:00
olly 22afa7e1a5 feat: structure 2026-05-31 17:49:29 +03:00
olly 67993ae3ec feat: storybook 2026-05-31 17:11:42 +03:00
olly 2f937e94b1 feat: storybook 2026-05-31 17:11:33 +03:00
olly b6cf54c836 stuff 2026-05-31 16:47:52 +03:00
57 changed files with 6959 additions and 910 deletions
+3
View File
@@ -7,6 +7,9 @@
node_modules node_modules
dist/ dist/
# Storybook build output (the .storybook config + *.stories.tsx ARE committed)
storybook-static/
# Profile # Profile
.rspack-profile-*/ .rspack-profile-*/
+27
View File
@@ -0,0 +1,27 @@
import type { StorybookConfig } from 'storybook-react-rsbuild';
/* Dev-only playground. Never shipped — package `files` is ["dist"]. */
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(ts|tsx)'],
addons: ['@storybook/addon-docs'],
staticDirs: ['../src/assets'],
framework: {
name: 'storybook-react-rsbuild',
options: {},
},
typescript: {
// Prop tables in autodocs come from the components' TS types.
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
// Keep our own props + Radix primitives; drop other node_modules noise.
propFilter: (prop) =>
prop.parent
? !/node_modules/.test(prop.parent.fileName) ||
/node_modules\/radix-ui/.test(prop.parent.fileName)
: true,
},
},
};
export default config;
+4
View File
@@ -0,0 +1,4 @@
import { addons } from 'storybook/manager-api';
import theme from './theme';
addons.setConfig({ theme });
+12
View File
@@ -0,0 +1,12 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Onest:wght@300;400;500;600;700;800&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
@font-face {
font-family: 'Anta';
src: url('/Anta-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
</style>
+19
View File
@@ -0,0 +1,19 @@
/* Calm dark canvas. Uses the felt's subtle radial glow (no heavy fixed
grain) so the story sits on-brand without flooding the frame. The
--bg-glow / --ink tokens come from the imported stylesheet and react
to the data-theme toggle automatically. */
.sb-show-main {
background-color: var(--ink);
background-image: var(--bg-glow);
background-size: cover;
background-repeat: no-repeat;
background-attachment: fixed;
}
/* Autodocs preview blocks: same dark surface as the canvas. */
.docs-story,
.sbdocs-preview {
background-color: var(--ink) !important;
background-image: var(--bg-glow);
background-size: cover;
}
+61
View File
@@ -0,0 +1,61 @@
import { useEffect, type ReactNode } from 'react';
import type { Preview, Decorator } from 'storybook-react-rsbuild';
import { Tooltip } from 'radix-ui';
/* The shipped library surface, exactly as a consumer would load it.
Fonts are loaded via preview-head.html (Google Fonts link + Anta @font-face)
to avoid bundler inlining the @import url() mid-stylesheet. */
import '../src/styles/index.css';
/* Storybook-only canvas styling (background, docs blocks). */
import './preview.css';
import theme from './theme';
/* Toolbar theme switch → drives `data-theme` on <html>, same lever as the
library's ThemeProvider. The frame stays content-sized; the dark canvas
comes from preview.css, so stories never balloon to full-viewport. */
function ThemeFrame({ theme, children }: { theme: string; children: ReactNode }) {
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
return <Tooltip.Provider delayDuration={200}>{children}</Tooltip.Provider>;
}
const withModernSk: Decorator = (Story, context) => (
<ThemeFrame theme={(context.globals.theme as string) ?? 'dark'}>
<Story />
</ThemeFrame>
);
const preview: Preview = {
tags: ['autodocs'],
decorators: [withModernSk],
parameters: {
layout: 'centered',
backgrounds: { disable: true },
docs: { theme },
controls: {
matchers: { color: /(background|color)$/i, date: /Date$/i },
},
options: {
storySort: { order: ['Getting Started', 'Inputs', 'Selection', 'Feedback', 'Overlays', 'Data Display', 'Layout'] },
},
},
globalTypes: {
theme: {
description: 'ModernSK theme',
defaultValue: 'dark',
toolbar: {
title: 'Theme',
icon: 'contrast',
items: [
{ value: 'dark', title: 'Dark', icon: 'moon' },
{ value: 'light', title: 'Light', icon: 'sun' },
],
dynamicTitle: true,
},
},
},
};
export default preview;
+39
View File
@@ -0,0 +1,39 @@
import { create } from 'storybook/theming';
/* Dark, lime-accented chrome so the Storybook UI + autodocs match the
components instead of framing them in stock white. */
export default create({
base: 'dark',
brandTitle: 'ModernSK',
colorPrimary: '#bef264',
colorSecondary: '#bef264',
// App
appBg: '#0f100d',
appContentBg: '#0f100d',
appPreviewBg: '#0f100d',
appBorderColor: '#2a2c22',
appBorderRadius: 10,
// Typography
fontBase: "'Onest', system-ui, -apple-system, 'Segoe UI', sans-serif",
fontCode: "'Geist Mono', ui-monospace, 'SF Mono', Menlo, monospace",
// Text
textColor: '#f3f4ee',
textInverseColor: '#0f100d',
textMutedColor: '#9a9c8c',
// Toolbar / sidebar bars
barBg: '#16170f',
barTextColor: '#9a9c8c',
barSelectedColor: '#bef264',
barHoverColor: '#bef264',
// Inputs
inputBg: '#1c1d16',
inputBorder: '#2a2c22',
inputTextColor: '#f3f4ee',
inputBorderRadius: 8,
});
+60
View File
@@ -0,0 +1,60 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
`modern-sk` — a tactile, dark-first React component library built on [Radix](https://www.radix-ui.com/) primitives. Distributed as a git-installable / publishable package; only `dist/` ships. Consumers get built ESM + CJS, `.d.ts` types, and a single `styles.css`.
## Commands
```bash
npm run dev # Rsbuild playground at http://localhost:3000 (renders src/App.tsx — every component on one page)
npm run storybook # Storybook component explorer + autodocs at http://localhost:6006
npm run build # build publishable package: tsup (JS/types) + build:css into dist/
npm run lint # rslint (rslint.config.ts) — covers src/ including stories
npm run format # prettier --write .
```
No test suite exists. `npm run build` runs automatically on install via the `prepare` script — consumers build the package themselves.
The build is two steps that must both run (the `build` script chains them):
- `tsup` bundles `src/index.ts` → ESM/CJS + types. `react`/`react-dom` are externalized (peer deps); `radix-ui` + `@phosphor-icons/react` are bundled.
- `build:css` runs esbuild twice: `src/styles/index.css``dist/styles.css` (fontless core), and `src/styles/fonts.css``dist/fonts.css` with `--loader:.ttf=dataurl` (inlines the self-hosted Anta font). Both are package exports.
## Architecture
Two parallel surfaces share the same source but never mix at publish time:
- **Library (shipped):** `src/index.ts` is the public entry. It re-exports everything from `src/components/ui.tsx`, plus `ThemeProvider`/`useTheme` from `src/components/theme.tsx`, and exposes `TooltipProvider` (Radix's `Tooltip.Provider`). The shipped stylesheet entry is `src/styles/index.css`.
- **Playground (dev-only, never published):** two of them — `src/index.tsx` mounts the `src/App.tsx` kitchen sink (Rsbuild dev target, `src/styles/global.css`), and Storybook (`.storybook/` config + `src/stories/*.stories.tsx`) is the component catalogue with autodocs.
Only `dist/` ships (`files: ["dist"]`), so the playground, stories, and `.storybook/` are excluded from the package automatically — but they ARE committed to git so anyone can run them.
`src/styles/index.css` (shipped) vs `src/styles/global.css` (dev) is a deliberate split: `index.css` imports only `tokens.css` + `components.css` and applies box-sizing at **zero specificity** via `:where([class^='modern-sk-'])` so it never touches consumer elements. `global.css` adds a global reset, the optional `fonts.css`, and kitchen-sink layout helpers — those must stay out of the shipped bundle.
### Fonts (`src/styles/fonts.css`)
Fonts are NOT in the core stylesheet. `tokens.css` defines `--font-display/sans/mono` chains that degrade to `system-ui`; the actual faces (Anta `@font-face` + Onest/Geist Mono Google Fonts `@import`) live in `src/styles/fonts.css`, shipped as the optional `modern-sk/fonts.css` export. Consumers either import it, or override the `--font-*` tokens to remap typefaces. Storybook's `preview.tsx` and the dev `global.css` both import `fonts.css` so the playgrounds stay branded.
### Components (`src/components/ui.tsx`)
All components live in one file. Pattern: Radix provides logic/accessibility, every visual comes from CSS. Components are thin wrappers that attach `modern-sk-*` classes and spread props. The `cx()` helper joins class names (no classnames dependency). Components forward refs where they wrap a DOM element. There is no inline styling and no CSS-in-JS — **all appearance is driven by `modern-sk-*` classes resolving against CSS custom properties.**
### Styling system (`src/styles/`)
- `tokens.css` — single source of truth: color/type CSS custom properties (no font loading — see Fonts above). Every component reads from here.
- `components.css` — the `modern-sk-*` class definitions.
- Dark/light is driven by `data-theme` on `<html>`, set by `ThemeProvider` (persisted to `localStorage` under key `modern-sk-theme`, default `dark`).
When adding or changing a component: add the wrapper in `ui.tsx`, define its `modern-sk-*` styles in `components.css`, and pull any new color/spacing value from a token in `tokens.css` rather than hardcoding.
## Consumer contract
Consumers import `modern-sk/styles.css` once at app root, wrap their tree in `ThemeProvider`, and wrap any tooltip-using subtree in `TooltipProvider`. Keep these provider requirements intact when refactoring exports.
## Conventions
- React 19, React Compiler is enabled (`babel-plugin-react-compiler` in the Rsbuild dev pipeline).
- TypeScript is `noEmit` + `verbatimModuleSyntax`: use `import type` for type-only imports. `noUnusedLocals`/`noUnusedParameters` are on.
- ESM-only package (`"type": "module"`).
+58 -12
View File
@@ -1,14 +1,21 @@
# @modernsk/ui <div align="center">
<img src="assets/logo.svg" alt="ModernSK" width="380" />
**Tactile, dark-first React component library built on [Radix](https://www.radix-ui.com/) primitives.**
Tactile, dark-first React component library built on [Radix](https://www.radix-ui.com/) primitives.
Old-iOS skeuomorphism × macOS Sequoia neatness × Ubuntu warmth. Old-iOS skeuomorphism × macOS Sequoia neatness × Ubuntu warmth.
</div>
---
## Install ## Install
Git-hosted (pin to a tag): Distributed via self-hosted git — install straight from the repo:
```bash ```bash
npm install github:YOUR_ORG/modernsk-ui#v0.1.0 npm i git+ssh://git@git.ollyhearn.ru:49239/olly/modern-sk.git
``` ```
`react` and `react-dom` (>=18) are peer dependencies — your app provides them. `react` and `react-dom` (>=18) are peer dependencies — your app provides them.
@@ -19,8 +26,9 @@ The package builds itself on install via the `prepare` script.
Import the stylesheet once at your app root, then use components anywhere: Import the stylesheet once at your app root, then use components anywhere:
```tsx ```tsx
import '@modernsk/ui/styles.css'; import 'modern-sk/styles.css'; // required — tokens + components
import { ThemeProvider, TooltipProvider, Button, Card } from '@modernsk/ui'; import 'modern-sk/fonts.css'; // optional — branded faces (see Fonts)
import { ThemeProvider, TooltipProvider, Button, Card } from 'modern-sk';
export function App() { export function App() {
return ( return (
@@ -37,18 +45,56 @@ export function App() {
- `ThemeProvider` manages dark/light via `data-theme` on `<html>` and persists to `localStorage`. Read it with `useTheme()`. - `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>`. - `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. - All visuals come from CSS custom properties in the shipped stylesheet.
## Fonts
`modern-sk/styles.css` ships **no fonts**. The type tokens default to a chain
that degrades to `system-ui`, so the library works with zero font loading.
To get the branded ModernSK faces (Anta display + Onest + Geist Mono), import
the optional stylesheet — Anta is self-hosted and inlined, no asset hosting
needed:
```tsx
import 'modern-sk/fonts.css';
```
To use your **own** fonts, skip `fonts.css` and override the tokens anywhere
after `styles.css` — every component re-reads them:
```css
:root {
--font-display: 'Your Display', sans-serif;
--font-sans: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
}
```
## Develop ## Develop
A live playground (every component on one page) runs via Rsbuild:
```bash ```bash
npm run dev # playground at http://localhost:3000 npm run dev # Rsbuild playground (every component on one page) at http://localhost:3000
npm run build # build the publishable package into dist/ npm run storybook # Storybook component explorer + docs at http://localhost:6006
npm run build # build the publishable package into dist/
npm run lint npm run lint
``` ```
**Storybook** is the component catalogue: each component has live controls and an
auto-generated prop table, plus a theme toggle in the toolbar. Stories live in
`src/stories/*.stories.tsx`; config is in `.storybook/`. It is committed to git so
anyone can `npm run storybook` and browse — but it never ships in the package.
## What ships ## 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. A git install exposes only `dist/` — built ESM + CJS, `.d.ts` types,
`styles.css`, and `fonts.css`. Everything else (`src/App.tsx`, `.storybook/`,
stories, Rsbuild config) is dev-only and never published.
---
<div align="center">
brought to you by **ollyhearn** & **claude** with &lt;3
</div>
+21
View File
@@ -0,0 +1,21 @@
<svg width="440" height="140" viewBox="0 0 440 140" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="ModernSK">
<defs>
<linearGradient id="panel" x1="0" y1="0" x2="0" y2="140" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#1b1d15"/>
<stop offset="1" stop-color="#0d0e0a"/>
</linearGradient>
<linearGradient id="sk" x1="300" y1="0" x2="420" y2="140" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#d4ff7a"/>
<stop offset="1" stop-color="#a8e04a"/>
</linearGradient>
</defs>
<rect x="1" y="1" width="438" height="138" rx="22" fill="url(#panel)" stroke="#2a2c22" stroke-width="1.5"/>
<rect x="6" y="6" width="428" height="2" rx="1" fill="#ffffff" opacity="0.05"/>
<text x="50%" y="50%" dominant-baseline="central" text-anchor="middle"
font-family="'Anta', 'Onest', system-ui, -apple-system, 'Segoe UI', sans-serif"
font-size="58" font-weight="700" letter-spacing="2">
<tspan fill="#e7e8e0">MODERN</tspan><tspan fill="url(#sk)">SK</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+4863 -2
View File
File diff suppressed because it is too large Load Diff
+9 -3
View File
@@ -1,5 +1,5 @@
{ {
"name": "@modernsk/ui", "name": "modern-sk",
"version": "0.1.0", "version": "0.1.0",
"description": "ModernSK — tactile, dark-first React component library built on Radix primitives.", "description": "ModernSK — tactile, dark-first React component library built on Radix primitives.",
"license": "MIT", "license": "MIT",
@@ -16,16 +16,19 @@
"import": "./dist/index.js", "import": "./dist/index.js",
"require": "./dist/index.cjs" "require": "./dist/index.cjs"
}, },
"./styles.css": "./dist/styles.css" "./styles.css": "./dist/styles.css",
"./fonts.css": "./dist/fonts.css"
}, },
"files": [ "files": [
"dist" "dist"
], ],
"scripts": { "scripts": {
"build": "tsup && npm run build:css", "build": "tsup && npm run build:css",
"build:css": "esbuild src/styles/index.css --bundle --loader:.ttf=dataurl --outfile=dist/styles.css", "build:css": "esbuild src/styles/index.css --bundle --outfile=dist/styles.css && esbuild src/styles/fonts.css --bundle --loader:.ttf=dataurl --outfile=dist/fonts.css",
"dev": "rsbuild dev --open", "dev": "rsbuild dev --open",
"preview": "rsbuild preview", "preview": "rsbuild preview",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"lint": "rslint", "lint": "rslint",
"format": "prettier --write .", "format": "prettier --write .",
"prepare": "npm run build" "prepare": "npm run build"
@@ -43,6 +46,7 @@
"@rsbuild/plugin-babel": "^1.2.0", "@rsbuild/plugin-babel": "^1.2.0",
"@rsbuild/plugin-react": "^2.0.0", "@rsbuild/plugin-react": "^2.0.0",
"@rslint/core": "^0.5.1", "@rslint/core": "^0.5.1",
"@storybook/addon-docs": "^10.4.1",
"@types/react": "^19.2.15", "@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
@@ -50,6 +54,8 @@
"prettier": "^3.8.3", "prettier": "^3.8.3",
"react": "^19.2.6", "react": "^19.2.6",
"react-dom": "^19.2.6", "react-dom": "^19.2.6",
"storybook": "^10.4.1",
"storybook-react-rsbuild": "^3.3.4",
"tsup": "^8.5.0", "tsup": "^8.5.0",
"typescript": "^6.0.3" "typescript": "^6.0.3"
} }
+4 -4
View File
@@ -76,7 +76,7 @@ const App = () => {
const [chips, setChips] = useState(['design', '2026', 'invoices']); const [chips, setChips] = useState(['design', '2026', 'invoices']);
return ( return (
<div className="msk-felt"> <div className="modern-sk-felt">
<div className="wrap"> <div className="wrap">
<header> <header>
<div className="topbar"> <div className="topbar">
@@ -87,8 +87,8 @@ const App = () => {
<p className="sub"> <p className="sub">
Every component, live and interactive, built on Radix Primitives Every component, live and interactive, built on Radix Primitives
styled from the global tokens in{' '} styled from the global tokens in{' '}
<span className="msk-mono">tokens.css</span> +{' '} <span className="modern-sk-mono">tokens.css</span> +{' '}
<span className="msk-mono">components.css</span>. Click, toggle, <span className="modern-sk-mono">components.css</span>. Click, toggle,
focus it all responds. focus it all responds.
</p> </p>
</div> </div>
@@ -242,7 +242,7 @@ const App = () => {
onDecrement={() => setCount((n) => Math.max(0, n - 1))} onDecrement={() => setCount((n) => Math.max(0, n - 1))}
onIncrement={() => setCount((n) => n + 1)} onIncrement={() => setCount((n) => n + 1)}
/> />
<span className="msk-mono" style={{ color: 'var(--fg-2)' }}> <span className="modern-sk-mono" style={{ color: 'var(--fg-2)' }}>
{count} {count}
</span> </span>
</div> </div>
+57
View File
@@ -0,0 +1,57 @@
import { type ReactNode } from 'react';
import { AlertDialog as RAlertDialog } from 'radix-ui';
import { Button } from '../button';
export const AlertDialog = ({
trigger,
title,
description,
cancelLabel = 'Cancel',
actionLabel = 'Confirm',
destructive,
onAction,
open,
defaultOpen,
onOpenChange,
}: {
trigger?: ReactNode;
title: string;
description?: ReactNode;
cancelLabel?: string;
actionLabel?: string;
destructive?: boolean;
onAction?: () => void;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (o: boolean) => void;
}) => (
<RAlertDialog.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
{trigger && <RAlertDialog.Trigger asChild>{trigger}</RAlertDialog.Trigger>}
<RAlertDialog.Portal>
<RAlertDialog.Overlay className="modern-sk-overlay" />
<RAlertDialog.Content className="modern-sk-dialog">
<RAlertDialog.Title className="modern-sk-dialog__title">
{title}
</RAlertDialog.Title>
{description && (
<RAlertDialog.Description className="modern-sk-dialog__desc">
{description}
</RAlertDialog.Description>
)}
<div className="modern-sk-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>
);
+44
View File
@@ -0,0 +1,44 @@
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
import { cx } from '../utils';
type BadgeVariant = 'lime' | 'ember' | 'neutral' | 'outline';
export const Badge = ({
variant = 'neutral',
dot,
className,
children,
...props
}: ComponentPropsWithoutRef<'span'> & {
variant?: BadgeVariant;
dot?: boolean;
}) => (
<span
className={cx(
'modern-sk-badge',
`modern-sk-badge--${variant}`,
dot && 'modern-sk-badge--dot',
className,
)}
{...props}
>
{children}
</span>
);
export const Chip = ({
children,
onRemove,
}: {
children: ReactNode;
onRemove?: () => void;
}) => (
<span className="modern-sk-chip">
{children}
{onRemove && (
<button type="button" className="x" onClick={onRemove} aria-label="Remove">
×
</button>
)}
</span>
);
+27
View File
@@ -0,0 +1,27 @@
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
import { cx } from '../utils';
export 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(
'modern-sk-btn',
variant !== 'key' && `modern-sk-btn--${variant}`,
size === 'sm' && 'modern-sk-btn--sm',
iconOnly && 'modern-sk-btn--icon',
className,
)}
{...props}
/>
),
);
Button.displayName = 'Button';
+19
View File
@@ -0,0 +1,19 @@
import { type ReactNode } from 'react';
import { cx } from '../utils';
type CalloutVariant = 'info' | 'success' | 'warning' | 'danger';
export const Callout = ({
variant = 'info',
icon,
children,
}: {
variant?: CalloutVariant;
icon?: ReactNode;
children: ReactNode;
}) => (
<div className={cx('modern-sk-callout', variant !== 'info' && `modern-sk-callout--${variant}`)}>
{icon && <span className="modern-sk-callout__icon">{icon}</span>}
<div className="modern-sk-callout__body">{children}</div>
</div>
);
+6
View File
@@ -0,0 +1,6 @@
import { type ComponentPropsWithoutRef } from 'react';
import { cx } from '../utils';
export const Card = ({ className, ...props }: ComponentPropsWithoutRef<'div'>) => (
<div className={cx('modern-sk-card', className)} {...props} />
);
+55
View File
@@ -0,0 +1,55 @@
import { type ReactNode } from 'react';
import { Dialog as RDialog } from 'radix-ui';
import { X } from '@phosphor-icons/react';
import { IconButton } from '../icon-button';
export const Dialog = ({
trigger,
title,
description,
children,
footer,
open,
defaultOpen,
onOpenChange,
modal,
}: {
trigger?: ReactNode;
title: string;
description?: ReactNode;
children?: ReactNode;
footer?: ReactNode;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (o: boolean) => void;
modal?: boolean;
}) => (
<RDialog.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange} modal={modal}>
{trigger && <RDialog.Trigger asChild>{trigger}</RDialog.Trigger>}
<RDialog.Portal>
<RDialog.Overlay className="modern-sk-overlay" />
<RDialog.Content className="modern-sk-dialog">
<RDialog.Title className="modern-sk-dialog__title">{title}</RDialog.Title>
{description && (
<RDialog.Description className="modern-sk-dialog__desc">
{description}
</RDialog.Description>
)}
{children && <div className="modern-sk-dialog__body">{children}</div>}
{footer && <div className="modern-sk-dialog__footer">{footer}</div>}
<RDialog.Close asChild>
<IconButton
variant="ghost"
size="sm"
className="modern-sk-dialog__close"
aria-label="Close"
>
<X size={14} weight="bold" />
</IconButton>
</RDialog.Close>
</RDialog.Content>
</RDialog.Portal>
</RDialog.Root>
);
export const DialogClose = RDialog.Close;
+25
View File
@@ -0,0 +1,25 @@
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
import { cx } from '../utils';
import type { BtnVariant } from '../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(
'modern-sk-btn',
'modern-sk-iconbtn',
variant !== 'key' && `modern-sk-btn--${variant}`,
size && `modern-sk-iconbtn--${size}`,
className,
)}
{...props}
/>
),
);
IconButton.displayName = 'IconButton';
+17
View File
@@ -0,0 +1,17 @@
import { type ComponentPropsWithoutRef } from 'react';
import { cx } from '../utils';
export const List = ({ className, ...props }: ComponentPropsWithoutRef<'div'>) => (
<div className={cx('modern-sk-list', className)} {...props} />
);
export const Row = ({
selected,
className,
...props
}: ComponentPropsWithoutRef<'div'> & { selected?: boolean }) => (
<div
className={cx('modern-sk-row', selected && 'is-selected', className)}
{...props}
/>
);
+56
View File
@@ -0,0 +1,56 @@
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
import { DropdownMenu as RMenu } from 'radix-ui';
export const Menu = RMenu.Root;
export const MenuTrigger = RMenu.Trigger;
export const MenuContent = ({
children,
...props
}: ComponentPropsWithoutRef<typeof RMenu.Content>) => (
<RMenu.Portal>
<RMenu.Content className="modern-sk-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="modern-sk-menu-item" {...props}>
{icon && <span className="ph">{icon}</span>}
{children}
{shortcut && <span className="sc">{shortcut}</span>}
</RMenu.Item>
);
export const MenuSeparator = () => (
<RMenu.Separator className="modern-sk-menu-sep" />
);
export const MenuSurface = ({ children }: { children: ReactNode }) => (
<div className="modern-sk-menu">{children}</div>
);
export const MenuRow = ({
icon,
shortcut,
children,
}: {
icon?: ReactNode;
shortcut?: string;
children: ReactNode;
}) => (
<div className="modern-sk-menu-item">
{icon && <span className="ph">{icon}</span>}
{children}
{shortcut && <span className="sc">{shortcut}</span>}
</div>
);
+16
View File
@@ -0,0 +1,16 @@
import { type ComponentPropsWithoutRef } from 'react';
import { Progress as RProgress } from 'radix-ui';
import { cx } from '../utils';
export const Progress = ({
value = 0,
className,
...props
}: ComponentPropsWithoutRef<typeof RProgress.Root>) => (
<RProgress.Root className={cx('modern-sk-progress', className)} value={value} {...props}>
<RProgress.Indicator
className="modern-sk-progress__indicator"
style={{ width: `${value}%` }}
/>
</RProgress.Root>
);
+22
View File
@@ -0,0 +1,22 @@
import { type ComponentPropsWithoutRef } from 'react';
import { ScrollArea as RScrollArea } from 'radix-ui';
import { cx } from '../utils';
export const ScrollArea = ({
children,
className,
...props
}: ComponentPropsWithoutRef<typeof RScrollArea.Root>) => (
<RScrollArea.Root className={cx('modern-sk-scroll', className)} {...props}>
<RScrollArea.Viewport className="modern-sk-scroll__viewport">
{children}
</RScrollArea.Viewport>
<RScrollArea.Scrollbar className="modern-sk-scroll__bar" orientation="vertical">
<RScrollArea.Thumb className="modern-sk-scroll__thumb" />
</RScrollArea.Scrollbar>
<RScrollArea.Scrollbar className="modern-sk-scroll__bar" orientation="horizontal">
<RScrollArea.Thumb className="modern-sk-scroll__thumb" />
</RScrollArea.Scrollbar>
<RScrollArea.Corner />
</RScrollArea.Root>
);
@@ -0,0 +1,66 @@
import { type ComponentPropsWithoutRef, useEffect, useRef } from 'react';
import { ToggleGroup as RToggleGroup } from 'radix-ui';
import { cx } from '../utils';
type SegProps = Omit<
ComponentPropsWithoutRef<typeof RToggleGroup.Root>,
'type' | 'onValueChange' | 'defaultValue' | 'value'
> & {
value: string;
defaultValue?: string;
onValueChange: (v: string) => void;
items: Array<{ value: string; label: string }>;
};
export const SegmentedControl = ({
value,
onValueChange,
items,
className,
...props
}: SegProps) => {
const rootRef = useRef<HTMLDivElement>(null);
const thumbRef = useRef<HTMLSpanElement>(null);
const initialized = useRef(false);
useEffect(() => {
const root = rootRef.current;
const thumb = thumbRef.current;
if (!root || !thumb) return;
const selected = root.querySelector<HTMLElement>('[data-state="on"]');
if (!selected) return;
if (!initialized.current) {
thumb.style.transition = 'none';
}
thumb.style.transform = `translateX(${selected.offsetLeft}px)`;
thumb.style.width = `${selected.offsetWidth}px`;
if (!initialized.current) {
thumb.getBoundingClientRect();
thumb.style.transition = '';
initialized.current = true;
}
}, [value]);
return (
<RToggleGroup.Root
ref={rootRef}
type="single"
className={cx('modern-sk-seg', className)}
value={value}
onValueChange={(v) => v && onValueChange(v)}
{...props}
>
<span ref={thumbRef} className="modern-sk-seg__thumb" aria-hidden />
{items.map((it) => (
<RToggleGroup.Item
key={it.value}
value={it.value}
className="modern-sk-seg__item"
>
{it.label}
</RToggleGroup.Item>
))}
</RToggleGroup.Root>
);
};
+42
View File
@@ -0,0 +1,42 @@
import { type ComponentPropsWithoutRef } from 'react';
import { Select as RSelect } from 'radix-ui';
import { Check, CaretDown } from '@phosphor-icons/react';
type SelectProps = ComponentPropsWithoutRef<typeof RSelect.Root> & {
placeholder?: string;
items: Array<{ value: string; label: string }>;
'aria-label'?: string;
};
export const Select = ({ placeholder, items, ...rest }: SelectProps) => (
<RSelect.Root {...rest}>
<RSelect.Trigger className="modern-sk-select" aria-label={rest['aria-label']}>
<RSelect.Value placeholder={placeholder} />
<RSelect.Icon className="modern-sk-select__icon">
<CaretDown size={12} weight="bold" />
</RSelect.Icon>
</RSelect.Trigger>
<RSelect.Portal>
<RSelect.Content
className="modern-sk-select__content"
position="popper"
sideOffset={6}
>
<RSelect.Viewport>
{items.map((it) => (
<RSelect.Item
key={it.value}
value={it.value}
className="modern-sk-select__item"
>
<RSelect.ItemText>{it.label}</RSelect.ItemText>
<RSelect.ItemIndicator className="modern-sk-select__item-indicator">
<Check size={14} weight="bold" />
</RSelect.ItemIndicator>
</RSelect.Item>
))}
</RSelect.Viewport>
</RSelect.Content>
</RSelect.Portal>
</RSelect.Root>
);
+53
View File
@@ -0,0 +1,53 @@
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
import {
Switch as RSwitch,
Checkbox as RCheckbox,
RadioGroup as RRadioGroup,
} from 'radix-ui';
export const Switch = (props: ComponentPropsWithoutRef<typeof RSwitch.Root>) => (
<RSwitch.Root className="modern-sk-switch" {...props}>
<RSwitch.Thumb className="modern-sk-switch__thumb" />
</RSwitch.Root>
);
export const Checkbox = (props: ComponentPropsWithoutRef<typeof RCheckbox.Root>) => (
<RCheckbox.Root className="modern-sk-check" {...props}>
<RCheckbox.Indicator className="modern-sk-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>
);
export const RadioGroup = RRadioGroup.Root;
export const RadioItem = ({
value,
...props
}: ComponentPropsWithoutRef<typeof RRadioGroup.Item>) => (
<RRadioGroup.Item className="modern-sk-radio" value={value} {...props}>
<RRadioGroup.Indicator className="modern-sk-radio__indicator" />
</RRadioGroup.Item>
);
export const Control = ({
children,
control,
}: {
children: ReactNode;
control: ReactNode;
}) => (
<label className="modern-sk-control">
{control}
{children}
</label>
);
+76
View File
@@ -0,0 +1,76 @@
import { type ComponentPropsWithoutRef } from 'react';
import { Slider as RSlider } from 'radix-ui';
type Step = { value: number; label?: string };
type SliderProps = ComponentPropsWithoutRef<typeof RSlider.Root> & {
steps?: number | Step[];
};
function resolveSteps(steps: number | Step[], min: number, max: number): Step[] {
if (Array.isArray(steps)) return steps;
if (steps < 2) return [];
return Array.from({ length: steps }, (_, i) => ({
value: min + (i / (steps - 1)) * (max - min),
}));
}
export const Slider = ({ steps, min = 0, max = 100, ...props }: SliderProps) => {
const resolved = steps != null ? resolveSteps(steps, min, max) : [];
const hasSteps = resolved.length > 0;
return (
<RSlider.Root
className={`modern-sk-slider${hasSteps ? ' modern-sk-slider--has-steps' : ''}`}
min={min}
max={max}
{...props}
>
<RSlider.Track className="modern-sk-slider__track">
<RSlider.Range className="modern-sk-slider__range" />
{hasSteps && resolved.map((step) => (
<div
key={step.value}
className="modern-sk-slider__step-dot"
aria-hidden
style={{ left: `${((step.value - min) / (max - min)) * 100}%` }}
/>
))}
</RSlider.Track>
<RSlider.Thumb className="modern-sk-slider__thumb" aria-label="Value" />
{hasSteps && (
<div className="modern-sk-slider__steps" aria-hidden>
{resolved.map((step) => (
<div
key={step.value}
className="modern-sk-slider__step"
style={{ left: `${((step.value - min) / (max - min)) * 100}%` }}
>
<div className="modern-sk-slider__step-tick" />
{step.label != null && (
<span className="modern-sk-slider__step-label">{step.label}</span>
)}
</div>
))}
</div>
)}
</RSlider.Root>
);
};
export const Stepper = ({
onDecrement,
onIncrement,
}: {
onDecrement: () => void;
onIncrement: () => void;
}) => (
<div className="modern-sk-stepper">
<button type="button" onClick={onDecrement} aria-label="Decrease">
</button>
<button type="button" onClick={onIncrement} aria-label="Increase">
+
</button>
</div>
);
+45
View File
@@ -0,0 +1,45 @@
import { useId, type ComponentPropsWithoutRef } from 'react';
import { cx } from '../utils';
export const Spinner = ({
size,
className,
...props
}: ComponentPropsWithoutRef<'span'> & { size?: 'sm' | 'lg' }) => {
const gid = `modern-sk-groove-${useId()}`;
return (
<span
role="status"
aria-label="Loading"
className={cx('modern-sk-spinner', size && `modern-sk-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>
<circle cx="18" cy="18" r="14" stroke={`url(#${gid})`} strokeWidth="5" />
<circle
className="modern-sk-spinner__arc"
cx="18"
cy="18"
r="14"
stroke="var(--lime)"
strokeWidth="3"
strokeLinecap="round"
strokeDasharray="22 88"
/>
</svg>
</span>
);
};
+24
View File
@@ -0,0 +1,24 @@
import { type ComponentPropsWithoutRef } from 'react';
import { cx } from '../utils';
export const Table = ({ children, ...props }: ComponentPropsWithoutRef<'table'>) => (
<div className="modern-sk-table-wrap">
<table className="modern-sk-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} />;
+28
View File
@@ -0,0 +1,28 @@
import { type ComponentPropsWithoutRef } from 'react';
import { Tabs as RTabs } from 'radix-ui';
import { cx } from '../utils';
export const Tabs = RTabs.Root;
export const TabsList = ({
items,
className,
...props
}: { items: Array<{ value: string; label: string }> } & Omit<
ComponentPropsWithoutRef<typeof RTabs.List>,
'children'
>) => (
<RTabs.List className={cx('modern-sk-tabs', className)} {...props}>
{items.map((it) => (
<RTabs.Trigger
key={it.value}
value={it.value}
className="modern-sk-tabs__trigger"
>
{it.label}
</RTabs.Trigger>
))}
</RTabs.List>
);
export const TabsContent = RTabs.Content;
+26
View File
@@ -0,0 +1,26 @@
import { forwardRef, type ComponentPropsWithoutRef, type ReactNode } from 'react';
import { cx } from '../utils';
export const TextField = forwardRef<HTMLInputElement, ComponentPropsWithoutRef<'input'>>(
({ className, ...props }, ref) => (
<input ref={ref} className={cx('modern-sk-field', className)} {...props} />
),
);
TextField.displayName = 'TextField';
export const TextArea = forwardRef<HTMLTextAreaElement, ComponentPropsWithoutRef<'textarea'>>(
({ className, ...props }, ref) => (
<textarea ref={ref} className={cx('modern-sk-field', className)} {...props} />
),
);
TextArea.displayName = 'TextArea';
export const SearchField = ({
icon,
...props
}: ComponentPropsWithoutRef<'input'> & { icon: ReactNode }) => (
<div className="modern-sk-search">
<span className="ph">{icon}</span>
<TextField {...props} />
</div>
);
+1 -1
View File
@@ -7,7 +7,7 @@ import {
} from 'react'; } from 'react';
type ThemeMode = 'dark' | 'light'; type ThemeMode = 'dark' | 'light';
const KEY = 'msk-theme'; const KEY = 'modern-sk-theme';
const ThemeContext = createContext<{ const ThemeContext = createContext<{
theme: ThemeMode; theme: ThemeMode;
+42
View File
@@ -0,0 +1,42 @@
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
import { Tooltip as RTooltip } from 'radix-ui';
import { cx } from '../utils';
type TooltipProps = {
content: ReactNode;
children: ReactNode;
delayDuration?: number;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (o: boolean) => void;
} & Omit<ComponentPropsWithoutRef<typeof RTooltip.Content>, 'children' | 'content'>;
export const Tooltip = ({
content,
children,
delayDuration,
open,
defaultOpen,
onOpenChange,
sideOffset = 6,
className,
...contentProps
}: TooltipProps) => (
<RTooltip.Root
delayDuration={delayDuration}
open={open}
defaultOpen={defaultOpen}
onOpenChange={onOpenChange}
>
<RTooltip.Trigger asChild>{children}</RTooltip.Trigger>
<RTooltip.Portal>
<RTooltip.Content
className={cx('modern-sk-tooltip', className)}
sideOffset={sideOffset}
{...contentProps}
>
{content}
</RTooltip.Content>
</RTooltip.Portal>
</RTooltip.Root>
);
+21 -679
View File
@@ -1,679 +1,21 @@
/* ============================================================ export * from './button';
ModernSK UI — Radix Primitives wrapped in the ModernSK look. export * from './icon-button';
Logic/accessibility from Radix; every pixel from the tokens in export * from './text-field';
styles/tokens.css + styles/components.css. export * from './select';
============================================================ */ export * from './selection';
import { export * from './segmented-control';
forwardRef, export * from './slider';
useId, export * from './tabs';
type ComponentPropsWithoutRef, export * from './progress';
type CSSProperties, export * from './badge';
type ReactNode, export * from './card';
} from 'react'; export * from './list';
import { export * from './menu';
Switch as RSwitch, export * from './tooltip';
Checkbox as RCheckbox, export * from './spinner';
RadioGroup as RRadioGroup, export * from './callout';
Tabs as RTabs, export * from './table';
Slider as RSlider, export * from './scroll-area';
DropdownMenu as RMenu, export * from './dialog';
Tooltip as RTooltip, export * from './alert-dialog';
Progress as RProgress, export * from './window';
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>
);
+2
View File
@@ -0,0 +1,2 @@
export const cx = (...c: Array<string | false | undefined>) =>
c.filter(Boolean).join(' ');
+26
View File
@@ -0,0 +1,26 @@
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
export const Window = ({
title,
badge,
children,
...props
}: ComponentPropsWithoutRef<'div'> & {
title: string;
badge?: ReactNode;
}) => (
<div className="modern-sk-window" {...props}>
<div className="modern-sk-titlebar">
<span className="modern-sk-traffic r" />
<span className="modern-sk-traffic y" />
<span className="modern-sk-traffic g" />
<span className="ttl">{title}</span>
{badge && (
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
{badge}
</div>
)}
</div>
{children}
</div>
);
+3 -2
View File
@@ -1,7 +1,8 @@
/* ============================================================ /* ============================================================
ModernSK UI — public package entry. ModernSK — public package entry.
Import components from here; import the stylesheet once at your Import components from here; import the stylesheet once at your
app root: import '@modernsk/ui/styles.css'; app root: import 'modern-sk/styles.css';
Optionally add the branded fonts: import 'modern-sk/fonts.css';
============================================================ */ ============================================================ */
import { Tooltip } from 'radix-ui'; import { Tooltip } from 'radix-ui';
+64
View File
@@ -0,0 +1,64 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { AlertDialog, Button } from '../components/ui';
const meta = {
title: 'Overlays/AlertDialog',
component: AlertDialog,
parameters: {
docs: {
description: {
component:
'Confirmation dialog built on Radix AlertDialog. Use for destructive or irreversible actions — focus is trapped and the cancel button is always reachable. Set `destructive` to switch the action button to the ember (red) variant.',
},
},
},
argTypes: {
title: { control: 'text' },
description: { control: 'text' },
cancelLabel: { control: 'text' },
actionLabel: { control: 'text' },
destructive: { control: 'boolean' },
trigger: { control: false },
},
args: {
title: 'Delete this project?',
description: 'This action cannot be undone.',
actionLabel: 'Delete',
cancelLabel: 'Cancel',
destructive: true,
trigger: <Button variant="ember">Delete</Button>,
onAction: () => {},
},
} satisfies Meta<typeof AlertDialog>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const Destructive: Story = {
render: () => (
<AlertDialog
trigger={<Button variant="ember">Delete account</Button>}
title="Delete your account?"
description="All data will be permanently removed. This cannot be undone."
actionLabel="Delete account"
cancelLabel="Keep account"
destructive
onAction={() => {}}
/>
),
};
export const Confirm: Story = {
render: () => (
<AlertDialog
trigger={<Button variant="primary">Publish</Button>}
title="Publish changes?"
description="This will make your changes visible to all users."
actionLabel="Publish"
cancelLabel="Cancel"
onAction={() => {}}
/>
),
};
+62
View File
@@ -0,0 +1,62 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Button } from '../components/ui';
const meta = {
title: 'Inputs/Button',
component: Button,
parameters: {
docs: {
description: {
component:
'Tactile push button. Four variants and an optional small size; everything else is a native `<button>`, so all standard button props pass through.',
},
},
},
argTypes: {
variant: {
control: 'inline-radio',
options: ['key', 'primary', 'ember', 'ghost'],
description: 'Visual emphasis. `key` is the default neutral button.',
},
size: {
control: 'inline-radio',
options: [undefined, 'sm'],
description: 'Omit for default; `sm` for the compact size.',
},
iconOnly: { control: 'boolean', description: 'Square padding for a single glyph.' },
disabled: { control: 'boolean' },
children: { control: 'text' },
},
args: { children: 'Button', variant: 'key' },
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const Variants: Story = {
render: () => (
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<Button variant="key">Key</Button>
<Button variant="primary">Primary</Button>
<Button variant="ember">Ember</Button>
<Button variant="ghost">Ghost</Button>
</div>
),
};
export const Sizes: Story = {
render: () => (
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<Button variant="primary">Default</Button>
<Button variant="primary" size="sm">
Small
</Button>
</div>
),
};
export const Disabled: Story = {
args: { disabled: true, variant: 'primary', children: 'Disabled' },
};
+74
View File
@@ -0,0 +1,74 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import {
Card,
List,
Row,
Table,
THead,
TBody,
Tr,
Th,
Td,
Badge,
} from '../components/ui';
const meta = {
title: 'Data Display/Surfaces',
component: Card,
parameters: {
docs: {
description: {
component: 'Cards, selectable lists/rows, and the bordered table.',
},
},
},
} satisfies Meta<typeof Card>;
export default meta;
type Story = StoryObj<typeof meta>;
export const CardSurface: Story = {
render: () => (
<Card style={{ maxWidth: 320, padding: 20 }}>
<h3 className="modern-sk-h3">Storage</h3>
<p className="modern-sk-body">128 GB of 256 GB used.</p>
</Card>
),
};
export const ListRows: Story = {
render: () => (
<List style={{ width: 320 }}>
<Row selected>General</Row>
<Row>Appearance</Row>
<Row>Notifications</Row>
<Row>Privacy</Row>
</List>
),
};
export const DataTable: Story = {
render: () => (
<Table>
<THead>
<Tr>
<Th>Device</Th>
<Th>Status</Th>
<Th>Battery</Th>
</Tr>
</THead>
<TBody>
<Tr selected>
<Td>MacBook Pro</Td>
<Td><Badge variant="lime" dot>Online</Badge></Td>
<Td>82%</Td>
</Tr>
<Tr>
<Td>iPhone 16</Td>
<Td><Badge variant="neutral">Idle</Badge></Td>
<Td>54%</Td>
</Tr>
</TBody>
</Table>
),
};
+54
View File
@@ -0,0 +1,54 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Dialog, Button, TextField } from '../components/ui';
const meta = {
title: 'Overlays/Dialog',
component: Dialog,
parameters: {
docs: {
description: {
component:
'Modal dialog built on Radix Dialog. Pass a `trigger` to wire open/close automatically, or control it with `open` / `onOpenChange`. Title is required; description, body, and footer are optional slots.',
},
},
},
argTypes: {
title: { control: 'text' },
description: { control: 'text' },
trigger: { control: false },
children: { control: false },
footer: { control: false },
},
args: {
title: 'Rename project',
description: 'Choose a new name for this project.',
trigger: <Button variant="primary">Open dialog</Button>,
},
} satisfies Meta<typeof Dialog>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const WithBody: Story = {
name: 'With body',
render: () => (
<Dialog
trigger={<Button variant="primary">Open dialog</Button>}
title="Rename project"
description="Choose a new name for this project."
footer={<Button variant="primary">Save</Button>}
>
<TextField placeholder="Project name" defaultValue="My project" />
</Dialog>
),
};
export const NoDescription: Story = {
name: 'No description',
args: {
description: undefined,
title: 'Confirm action',
},
};
+77
View File
@@ -0,0 +1,77 @@
import { useState } from 'react';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Info, CheckCircle, Warning, XCircle } from '@phosphor-icons/react';
import { Progress, Spinner, Callout, Badge, Chip, Button } from '../components/ui';
const meta = {
title: 'Feedback/Status',
component: Progress,
parameters: {
docs: {
description: {
component:
'Progress bar, spinner, callouts, badges and chips — the status + signalling family.',
},
},
},
} satisfies Meta<typeof Progress>;
export default meta;
type Story = StoryObj<typeof meta>;
function ProgressDemo() {
const [v, setV] = useState(40);
return (
<div style={{ width: 320, display: 'flex', flexDirection: 'column', gap: 12 }}>
<Progress value={v} />
<div style={{ display: 'flex', gap: 8 }}>
<Button size="sm" onClick={() => setV((x) => Math.max(0, x - 10))}>10</Button>
<Button size="sm" variant="primary" onClick={() => setV((x) => Math.min(100, x + 10))}>+10</Button>
</div>
</div>
);
}
export const ProgressBar: Story = { render: () => <ProgressDemo /> };
export const Spinners: Story = {
render: () => (
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Spinner size="sm" />
<Spinner />
<Spinner size="lg" />
</div>
),
};
export const Callouts: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 460 }}>
<Callout variant="info" icon={<Info size={18} />}>Sync runs in the background.</Callout>
<Callout variant="success" icon={<CheckCircle size={18} />}>All changes saved.</Callout>
<Callout variant="warning" icon={<Warning size={18} />}>Storage is almost full.</Callout>
<Callout variant="danger" icon={<XCircle size={18} />}>Failed to reach the server.</Callout>
</div>
),
};
export const Badges: Story = {
render: () => (
<div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
<Badge variant="lime">Lime</Badge>
<Badge variant="ember">Ember</Badge>
<Badge variant="neutral">Neutral</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="lime" dot>Online</Badge>
</div>
),
};
export const Chips: Story = {
render: () => (
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
<Chip>Design</Chip>
<Chip onRemove={() => {}}>Removable</Chip>
</div>
),
};
+48
View File
@@ -0,0 +1,48 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Gear, Plus, Trash } from '@phosphor-icons/react';
import { IconButton } from '../components/ui';
const meta = {
title: 'Inputs/IconButton',
component: IconButton,
parameters: {
docs: {
description: {
component:
'Square button for a single icon. Shares the Button variants; sizes are `sm` / default / `lg`.',
},
},
},
argTypes: {
variant: { control: 'inline-radio', options: ['key', 'primary', 'ember', 'ghost'] },
size: { control: 'inline-radio', options: ['sm', undefined, 'lg'] },
},
} satisfies Meta<typeof IconButton>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
args: { variant: 'key', children: <Gear size={18} /> },
};
export const Variants: Story = {
render: () => (
<div style={{ display: 'flex', gap: 12 }}>
<IconButton variant="key" aria-label="settings"><Gear size={18} /></IconButton>
<IconButton variant="primary" aria-label="add"><Plus size={18} weight="bold" /></IconButton>
<IconButton variant="ember" aria-label="delete"><Trash size={18} /></IconButton>
<IconButton variant="ghost" aria-label="settings"><Gear size={18} /></IconButton>
</div>
),
};
export const Sizes: Story = {
render: () => (
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<IconButton size="sm" variant="primary" aria-label="add"><Plus size={14} weight="bold" /></IconButton>
<IconButton variant="primary" aria-label="add"><Plus size={18} weight="bold" /></IconButton>
<IconButton size="lg" variant="primary" aria-label="add"><Plus size={22} weight="bold" /></IconButton>
</div>
),
};
+42
View File
@@ -0,0 +1,42 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Getting Started/Introduction" />
# ModernSK
Tactile, dark-first React components built on [Radix](https://www.radix-ui.com/) primitives.
Old-iOS skeuomorphism × macOS Sequoia neatness × Ubuntu warmth.
This Storybook is the **development playground** — it is never published with the
package. Use it to browse every component, read its prop table (the **Docs** tab on
each story), and try props live in the **Controls** panel.
## Using the library in an app
```tsx
import 'modern-sk/styles.css'; // required — tokens + components
import 'modern-sk/fonts.css'; // optional — branded faces
import { ThemeProvider, TooltipProvider, Button } from 'modern-sk';
export function App() {
return (
<ThemeProvider>
<TooltipProvider delayDuration={200}>
<Button variant="primary">Click</Button>
</TooltipProvider>
</ThemeProvider>
);
}
```
## Theme
Use the **Theme** toggle in the toolbar above to flip every story between dark and
light. In an app the same lever is `data-theme` on `<html>`, managed by
`ThemeProvider` / `useTheme()`.
## Fonts
Components read the `--font-display`, `--font-sans` and `--font-mono` tokens. Import
`modern-sk/fonts.css` for the branded faces, or override those tokens to map the
components onto fonts your app already loads.
+87
View File
@@ -0,0 +1,87 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Copy, Scissors, Trash, ArrowCounterClockwise } from '@phosphor-icons/react';
import {
Tooltip,
Menu,
MenuTrigger,
MenuContent,
MenuItem,
MenuSeparator,
Dialog,
AlertDialog,
Button,
} from '../components/ui';
const meta = {
title: 'Overlays/Floating',
component: Tooltip,
parameters: {
docs: {
description: {
component:
'Floating surfaces — Tooltip, dropdown Menu, Dialog and AlertDialog. All are Radix-backed and portal-rendered.',
},
},
},
argTypes: {
children: { control: false },
content: { control: false },
},
args: {
content: '',
children: null,
},
} satisfies Meta<typeof Tooltip>;
export default meta;
type Story = StoryObj<typeof meta>;
export const TooltipStory: Story = {
name: 'Tooltip',
render: () => (
<Tooltip content="Saved to iCloud">
<Button variant="ghost">Hover me</Button>
</Tooltip>
),
};
export const DropdownMenu: Story = {
render: () => (
<Menu>
<MenuTrigger asChild>
<Button>Open menu</Button>
</MenuTrigger>
<MenuContent>
<MenuItem icon={<Copy size={16} />} shortcut="⌘C">Copy</MenuItem>
<MenuItem icon={<Scissors size={16} />} shortcut="⌘X">Cut</MenuItem>
<MenuItem icon={<ArrowCounterClockwise size={16} />} shortcut="⌘Z">Undo</MenuItem>
<MenuSeparator />
<MenuItem icon={<Trash size={16} />}>Delete</MenuItem>
</MenuContent>
</Menu>
),
};
export const ModalDialog: Story = {
render: () => (
<Dialog
trigger={<Button variant="primary">Open dialog</Button>}
title="Rename project"
description="Choose a new name for this project."
footer={<Button variant="primary">Save</Button>}
/>
),
};
export const Confirm: Story = {
render: () => (
<AlertDialog
trigger={<Button variant="ember">Delete</Button>}
title="Delete this project?"
description="This action cannot be undone."
actionLabel="Delete"
destructive
onAction={() => {}}
/>
),
};
+30
View File
@@ -0,0 +1,30 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Select } from '../components/ui';
const items = [
{ value: 'sequoia', label: 'Sequoia' },
{ value: 'sonoma', label: 'Sonoma' },
{ value: 'ventura', label: 'Ventura' },
{ value: 'monterey', label: 'Monterey' },
];
const meta = {
title: 'Inputs/Select',
component: Select,
parameters: {
docs: {
description: {
component:
'Radix Select in the ModernSK skin. Pass `items` plus an optional `placeholder`; control it with `value` / `onValueChange` or leave it uncontrolled with `defaultValue`.',
},
},
},
args: { items, placeholder: 'Pick a release…', 'aria-label': 'macOS release' },
} satisfies Meta<typeof Select>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const WithDefault: Story = { args: { defaultValue: 'sonoma' } };
+74
View File
@@ -0,0 +1,74 @@
import { useState } from 'react';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import {
Switch,
Checkbox,
RadioGroup,
RadioItem,
SegmentedControl,
Control,
} from '../components/ui';
/* Grouped selection controls. Anchored on Switch for the docgen table;
the stories below showcase each control. */
const meta = {
title: 'Selection/Controls',
component: Switch,
parameters: {
docs: {
description: {
component:
'Toggle, checkbox, radio group and segmented control. The `Control` helper pairs any of them with a clickable label.',
},
},
},
} satisfies Meta<typeof Switch>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Switches: Story = {
render: () => (
<div style={{ display: 'flex', gap: 20, alignItems: 'center' }}>
<Switch defaultChecked />
<Switch />
<Control control={<Switch defaultChecked />}>Wi-Fi</Control>
</div>
),
};
export const Checkboxes: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Control control={<Checkbox defaultChecked />}>Sync to iCloud</Control>
<Control control={<Checkbox />}>Share analytics</Control>
</div>
),
};
export const Radios: Story = {
render: () => (
<RadioGroup defaultValue="comfortable" style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Control control={<RadioItem value="compact" />}>Compact</Control>
<Control control={<RadioItem value="comfortable" />}>Comfortable</Control>
<Control control={<RadioItem value="spacious" />}>Spacious</Control>
</RadioGroup>
),
};
function SegmentedDemo() {
const [v, setV] = useState('day');
return (
<SegmentedControl
value={v}
onValueChange={setV}
items={[
{ value: 'day', label: 'Day' },
{ value: 'week', label: 'Week' },
{ value: 'month', label: 'Month' },
]}
/>
);
}
export const Segmented: Story = { render: () => <SegmentedDemo /> };
+23
View File
@@ -0,0 +1,23 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Slider } from '../components/ui';
const meta = {
title: 'Inputs/Slider',
component: Slider,
parameters: {
docs: {
description: {
component:
'Radix Slider in the carved-track skin. All Radix Slider props pass through (`defaultValue`, `min`, `max`, `step`, `onValueChange`).',
},
},
},
decorators: [(Story) => <div style={{ width: 280 }}><Story /></div>],
} satisfies Meta<typeof Slider>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = { args: { defaultValue: [60], max: 100, step: 1 } };
export const Stepped: Story = { args: { defaultValue: [40], max: 100, step: 10 } };
+41
View File
@@ -0,0 +1,41 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Tabs, TabsList, TabsContent } from '../components/ui';
const meta = {
title: 'Navigation/Tabs',
component: Tabs,
parameters: {
docs: {
description: {
component:
'Radix Tabs. `Tabs` is the root, `TabsList` takes `items`, and `TabsContent` matches each `value`.',
},
},
},
} satisfies Meta<typeof Tabs>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: () => (
<Tabs defaultValue="overview" style={{ width: 420 }}>
<TabsList
items={[
{ value: 'overview', label: 'Overview' },
{ value: 'activity', label: 'Activity' },
{ value: 'settings', label: 'Settings' },
]}
/>
<TabsContent value="overview" style={{ paddingTop: 16 }} className="modern-sk-body">
Project at a glance.
</TabsContent>
<TabsContent value="activity" style={{ paddingTop: 16 }} className="modern-sk-body">
Recent activity feed.
</TabsContent>
<TabsContent value="settings" style={{ paddingTop: 16 }} className="modern-sk-body">
Preferences and configuration.
</TabsContent>
</Tabs>
),
};
+38
View File
@@ -0,0 +1,38 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { MagnifyingGlass } from '@phosphor-icons/react';
import { TextField, TextArea, SearchField } from '../components/ui';
const meta = {
title: 'Inputs/TextField',
component: TextField,
parameters: {
docs: {
description: {
component:
'Sunken text input. `TextField`, `TextArea`, and `SearchField` (icon + input) share the `modern-sk-field` look and forward all native props.',
},
},
},
args: { placeholder: 'Type here…' },
} satisfies Meta<typeof TextField>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const Disabled: Story = { args: { disabled: true, value: 'Read only' } };
export const Multiline: Story = {
render: () => <TextArea rows={4} placeholder="Multiple lines…" style={{ width: 320 }} />,
};
export const Search: Story = {
render: () => (
<SearchField
icon={<MagnifyingGlass size={16} />}
placeholder="Search…"
style={{ width: 280 }}
/>
),
};
+31
View File
@@ -0,0 +1,31 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Window, Badge, List, Row } from '../components/ui';
const meta = {
title: 'Layout/Window',
component: Window,
parameters: {
docs: {
description: {
component:
'macOS-style window chrome: traffic lights, a title, an optional `badge` slot, and arbitrary children for the body.',
},
},
},
args: { title: 'Finder' },
} satisfies Meta<typeof Window>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: (args) => (
<Window {...args} style={{ width: 380 }} badge={<Badge variant="lime" dot>Synced</Badge>}>
<List style={{ padding: 12 }}>
<Row selected>Documents</Row>
<Row>Downloads</Row>
<Row>Pictures</Row>
</List>
</Window>
),
};
+189 -182
View File
@@ -5,23 +5,23 @@
([data-state], [data-highlighted], [data-disabled]). ([data-state], [data-highlighted], [data-disabled]).
============================================================ */ ============================================================ */
@keyframes msk-pop { @keyframes modern-sk-pop {
from { opacity: 0; transform: scale(.6); } from { opacity: 0; transform: scale(.6); }
to { opacity: 1; transform: scale(1); } to { opacity: 1; transform: scale(1); }
} }
@keyframes msk-scale-in { @keyframes modern-sk-scale-in {
from { opacity: 0; transform: scale(.96); } from { opacity: 0; transform: scale(.96); }
to { opacity: 1; transform: scale(1); } to { opacity: 1; transform: scale(1); }
} }
@keyframes msk-scale-out { @keyframes modern-sk-scale-out {
from { opacity: 1; transform: scale(1); } from { opacity: 1; transform: scale(1); }
to { opacity: 0; transform: scale(.96); } to { opacity: 0; transform: scale(.96); }
} }
@keyframes msk-fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes modern-sk-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes msk-fade-out { from { opacity: 1; } to { opacity: 0; } } @keyframes modern-sk-fade-out { from { opacity: 1; } to { opacity: 0; } }
/* ---------- BUTTONS ---------- */ /* ---------- BUTTONS ---------- */
.msk-btn{ .modern-sk-btn{
font-family:var(--font-sans); font-size:var(--ctl-font); font-weight:600; 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); line-height:1.1; padding:var(--ctl-pad-y) var(--ctl-pad-x);
border-radius:var(--r-md); cursor:pointer; user-select:none; border-radius:var(--r-md); cursor:pointer; user-select:none;
@@ -34,60 +34,60 @@
transform var(--dur-press) var(--ease-out), transform var(--dur-press) var(--ease-out),
background var(--dur-quick) var(--ease-out); background var(--dur-quick) var(--ease-out);
} }
.msk-btn:hover{ background:var(--grad-key-hover); } .modern-sk-btn:hover{ background:var(--grad-key-hover); }
.msk-btn:active{ background:var(--grad-key-down); box-shadow:var(--shadow-pressed); transform:translateY(1px); } .modern-sk-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); } .modern-sk-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; } .modern-sk-btn[disabled], .modern-sk-btn.is-disabled{ opacity:.4; filter:saturate(.6); pointer-events:none; }
.msk-btn .ph{ font-size:1.05em; display:inline-flex; } .modern-sk-btn .ph{ font-size:1.05em; display:inline-flex; }
.msk-btn--primary{ .modern-sk-btn--primary{
color:var(--lime-ink); background:var(--grad-primary); color:var(--lime-ink); background:var(--grad-primary);
border-color:var(--lime-deep); border-color:var(--lime-deep);
text-shadow:0 1px 0 rgba(255,255,255,.25); 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); 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); } .modern-sk-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); } .modern-sk-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); } .modern-sk-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{ .modern-sk-btn--ember{
color:#fff; background:var(--grad-ember); color:#fff; background:var(--grad-ember);
border-color:var(--ember-deep); border-color:var(--ember-deep);
text-shadow:0 1px 1px rgba(0,0,0,.25); 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); 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); } .modern-sk-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); } .modern-sk-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); } .modern-sk-btn--ember:focus-visible{ box-shadow:var(--focus-ring-ember), 0 2px 4px rgba(0,0,0,.35); }
.msk-btn--ghost{ .modern-sk-btn--ghost{
color:var(--fg-2); background:transparent; border-color:var(--hair-strong); color:var(--fg-2); background:transparent; border-color:var(--hair-strong);
box-shadow:none; text-shadow:none; box-shadow:none; text-shadow:none;
} }
.msk-btn--ghost:hover{ background:rgba(255,255,255,.04); color:var(--fg-1); } .modern-sk-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); } .modern-sk-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; } .modern-sk-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; } .modern-sk-btn--icon{ padding:var(--ctl-pad-y); width:calc(var(--ctl-font) + 2*var(--ctl-pad-y)); aspect-ratio:1; }
/* ---------- TEXT FIELDS ---------- */ /* ---------- TEXT FIELDS ---------- */
.msk-field{ .modern-sk-field{
width:100%; font-family:var(--font-sans); font-size:14px; color:var(--fg-1); 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); padding:var(--field-pad-y) var(--field-pad-x); border-radius:var(--r-md);
background:var(--steel-900); border:1px solid var(--edge-inset); background:var(--steel-900); border:1px solid var(--edge-inset);
box-shadow:var(--shadow-inset-well); outline:none; 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); transition:box-shadow var(--dur-quick) var(--ease-out), border-color var(--dur-quick) var(--ease-out);
} }
.msk-field::placeholder{ color:var(--fg-3); } .modern-sk-field::placeholder{ color:var(--fg-3); }
.msk-field:focus{ border-color:var(--lime-deep); box-shadow:var(--shadow-inset-well), var(--focus-ring); } .modern-sk-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; } textarea.modern-sk-field{ resize:vertical; min-height:64px; line-height:1.5; }
.msk-search{ position:relative; display:flex; align-items:center; } .modern-sk-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; } .modern-sk-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; } .modern-sk-search .modern-sk-field{ padding-left:34px; }
/* ---------- SELECT (Radix Select, styled as the glossy key) ---------- */ /* ---------- SELECT (Radix Select, styled as the glossy key) ---------- */
.msk-select{ .modern-sk-select{
font-family:var(--font-sans); font-size:14px; color:var(--fg-1); cursor:pointer; font-family:var(--font-sans); font-size:14px; color:var(--fg-1); cursor:pointer;
display:inline-flex; align-items:center; gap:10px; display:inline-flex; align-items:center; gap:10px;
padding:var(--field-pad-y) 12px var(--field-pad-y) var(--field-pad-x); padding:var(--field-pad-y) 12px var(--field-pad-y) var(--field-pad-x);
@@ -95,257 +95,264 @@ textarea.msk-field{ resize:vertical; min-height:64px; line-height:1.5; }
background:var(--grad-key); background:var(--grad-key);
box-shadow:var(--shadow-raised); outline:none; box-shadow:var(--shadow-raised); outline:none;
} }
.msk-select:hover{ background:var(--grad-key-hover); } .modern-sk-select:hover{ background:var(--grad-key-hover); }
.msk-select:focus-visible{ box-shadow:var(--focus-ring), var(--shadow-raised); } .modern-sk-select:focus-visible{ box-shadow:var(--focus-ring), var(--shadow-raised); }
.msk-select__icon{ color:var(--fg-2); display:inline-flex; margin-left:auto; } .modern-sk-select__icon{ color:var(--fg-2); display:inline-flex; margin-left:auto; }
.msk-select__content{ .modern-sk-select__content{
min-width:var(--radix-select-trigger-width); padding:6px; border-radius:var(--r-lg); 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); 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); backdrop-filter:blur(24px) saturate(1.3); -webkit-backdrop-filter:blur(24px) saturate(1.3);
box-shadow:var(--shadow-window); z-index:50; box-shadow:var(--shadow-window); z-index:50;
transform-origin:var(--radix-select-content-transform-origin); transform-origin:var(--radix-select-content-transform-origin);
} }
.msk-select__content[data-state="open"]{ animation:msk-scale-in var(--dur-base) var(--ease-out); } .modern-sk-select__content[data-state="open"]{ animation:modern-sk-scale-in var(--dur-base) var(--ease-out); }
.msk-select__item{ .modern-sk-select__item{
display:flex; align-items:center; gap:10px; padding:7px 10px; border-radius:var(--r-sm); 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; 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); } .modern-sk-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; } .modern-sk-select__item-indicator{ margin-left:auto; display:inline-flex; }
/* ---------- SWITCH ---------- */ /* ---------- SWITCH ---------- */
.msk-switch{ .modern-sk-switch{
position:relative; width:var(--switch-w); height:var(--switch-h); position:relative; width:var(--switch-w); height:var(--switch-h);
flex-shrink:0; border-radius:8px; padding:0; border:1px solid var(--edge-inset); 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; 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); transition:background var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out);
} }
.msk-switch__thumb{ .modern-sk-switch__thumb{
display:block; position:absolute; top:50%; left:var(--switch-gap); transform:translateY(-50%); display:block; position:absolute; top:50%; left:var(--switch-gap); transform:translateY(-50%);
width:var(--switch-knob); height:var(--switch-knob); border-radius:5px; width:var(--switch-knob); height:var(--switch-knob); border-radius:5px;
background:linear-gradient(180deg,#cfd1c4,#a7a99c); 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; 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); 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); } .modern-sk-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); } .modern-sk-switch[data-state="checked"] .modern-sk-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); } .modern-sk-switch:focus-visible{ outline:none; box-shadow:var(--shadow-inset-well), var(--focus-ring); }
/* ---------- CHECKBOX ---------- */ /* ---------- CHECKBOX ---------- */
.msk-check{ .modern-sk-check{
width:22px; height:22px; flex-shrink:0; border-radius:4px; padding:0; width:22px; height:22px; flex-shrink:0; border-radius:4px; padding:0;
background:var(--steel-900); border:1px solid var(--edge-inset); background:var(--steel-900); border:1px solid var(--edge-inset);
box-shadow:var(--shadow-inset-well); cursor:pointer; box-shadow:var(--shadow-inset-well); cursor:pointer;
display:flex; align-items:center; justify-content:center; display:flex; align-items:center; justify-content:center;
transition:background var(--dur-quick) var(--ease-out), border-color var(--dur-quick) var(--ease-out); 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; } .modern-sk-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); } .modern-sk-check__indicator{ display:flex; align-items:center; justify-content:center; animation:modern-sk-pop var(--dur-quick) var(--ease-snap); }
.msk-check__indicator svg{ width:13px; height:13px; display:block; } .modern-sk-check__indicator svg{ width:13px; height:13px; display:block; }
.msk-check:focus-visible{ outline:none; box-shadow:var(--shadow-inset-well), var(--focus-ring); } .modern-sk-check:focus-visible{ outline:none; box-shadow:var(--shadow-inset-well), var(--focus-ring); }
/* ---------- RADIO ---------- */ /* ---------- RADIO ---------- */
.msk-radio{ .modern-sk-radio{
width:22px; height:22px; flex-shrink:0; border-radius:50%; padding:0; width:22px; height:22px; flex-shrink:0; border-radius:50%; padding:0;
background:var(--steel-900); border:1px solid var(--edge-inset); background:var(--steel-900); border:1px solid var(--edge-inset);
box-shadow:var(--shadow-inset-well); cursor:pointer; box-shadow:var(--shadow-inset-well); cursor:pointer;
display:flex; align-items:center; justify-content:center; display:flex; align-items:center; justify-content:center;
transition:border-color var(--dur-quick) var(--ease-out); transition:border-color var(--dur-quick) var(--ease-out);
} }
.msk-radio__indicator{ display:flex; align-items:center; justify-content:center; width:100%; height:100%; } .modern-sk-radio__indicator{ display:flex; align-items:center; justify-content:center; width:100%; height:100%; }
.msk-radio__indicator::after{ .modern-sk-radio__indicator::after{
content:""; width:11px; height:11px; border-radius:50%; content:""; width:11px; height:11px; border-radius:50%;
background:radial-gradient(circle at 50% 38%,var(--lime-bright),var(--lime-deep)); 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; 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); animation:modern-sk-pop var(--dur-quick) var(--ease-snap);
} }
.msk-radio:focus-visible{ outline:none; box-shadow:var(--shadow-inset-well), var(--focus-ring); } .modern-sk-radio:focus-visible{ outline:none; box-shadow:var(--shadow-inset-well), var(--focus-ring); }
/* control + label row helper */ /* control + label row helper */
.msk-control{ display:inline-flex; align-items:center; gap:10px; font-size:14px; color:var(--fg-2); cursor:pointer; } .modern-sk-control{ display:inline-flex; align-items:center; gap:10px; font-size:14px; color:var(--fg-2); cursor:pointer; }
/* ---------- SEGMENTED CONTROL (Radix ToggleGroup) ---------- */ /* ---------- 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; } .modern-sk-seg{ position:relative; 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); } .modern-sk-seg__thumb{ position:absolute; top:3px; left:0; height:calc(100% - 6px); background:var(--grad-key); border-radius:var(--r-sm); box-shadow:var(--shadow-raised); pointer-events:none; transition:transform 180ms var(--ease-snap), width 180ms var(--ease-snap); will-change:transform,width; }
.msk-seg__item:hover{ color:var(--fg-1); } .modern-sk-seg__item{ position:relative; z-index:1; 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); }
.msk-seg__item[data-state="on"]{ color:var(--fg-1); background:var(--grad-key); box-shadow:var(--shadow-raised); } .modern-sk-seg__item:hover{ color:var(--fg-1); }
.modern-sk-seg__item[data-state="on"]{ color:var(--fg-1); }
/* ---------- SLIDER ---------- */ /* ---------- SLIDER ---------- */
.msk-slider{ position:relative; display:flex; align-items:center; width:200px; height:20px; user-select:none; touch-action:none; } .modern-sk-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); } .modern-sk-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); } .modern-sk-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; } .modern-sk-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); } .modern-sk-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); }
.modern-sk-slider--has-steps{ padding-bottom:22px; }
.modern-sk-slider__step-dot{ position:absolute; top:50%; width:4px; height:4px; border-radius:50%; background:var(--steel-500); transform:translate(-50%,-50%); z-index:1; pointer-events:none; }
.modern-sk-slider__steps{ position:absolute; left:10px; right:10px; top:calc(50% + 6px); pointer-events:none; }
.modern-sk-slider__step{ position:absolute; transform:translateX(-50%); display:flex; flex-direction:column; align-items:center; gap:3px; }
.modern-sk-slider__step-tick{ width:1px; height:5px; background:var(--steel-600); }
.modern-sk-slider__step-label{ font-family:var(--font-mono); font-size:10px; line-height:1; color:var(--fg-3); white-space:nowrap; }
/* ---------- STEPPER ---------- */ /* ---------- STEPPER ---------- */
.msk-stepper{ display:inline-flex; border-radius:var(--r-md); overflow:hidden; box-shadow:var(--shadow-raised); border:1px solid var(--hair-strong); } .modern-sk-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); } .modern-sk-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); } .modern-sk-stepper button:hover{ background:var(--grad-key-hover); }
.msk-stepper button:active{ background:var(--grad-key-down); } .modern-sk-stepper button:active{ background:var(--grad-key-down); }
.msk-stepper button:first-child{ border-right:1px solid var(--edge-inset); } .modern-sk-stepper button:first-child{ border-right:1px solid var(--edge-inset); }
/* ---------- BADGES / CHIPS / TAGS ---------- */ /* ---------- 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; } .modern-sk-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); } .modern-sk-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); } .modern-sk-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); } .modern-sk-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); } .modern-sk-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; } .modern-sk-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); } .modern-sk-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; } .modern-sk-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); } .modern-sk-chip .x:hover{ color:var(--fg-1); }
/* ---------- CARD ---------- */ /* ---------- CARD ---------- */
.msk-card{ background:var(--steel-900); border:1px solid var(--hair); border-radius:var(--r-lg); box-shadow:var(--shadow-card); padding:18px; } .modern-sk-card{ background:var(--steel-900); border:1px solid var(--hair); border-radius:var(--r-lg); box-shadow:var(--shadow-card); padding:18px; }
/* ---------- LIST ROWS ---------- */ /* ---------- 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; } .modern-sk-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; } .modern-sk-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; } .modern-sk-row:last-child{ border-bottom:none; }
.msk-row:hover{ background:rgba(255,255,255,.03); } .modern-sk-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)); } .modern-sk-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; } .modern-sk-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); } .modern-sk-row .meta{ margin-left:auto; font-family:var(--font-mono); font-size:11px; color:var(--fg-3); }
/* ---------- MENU / POPOVER (Radix DropdownMenu) ---------- */ /* ---------- 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); } .modern-sk-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); } .modern-sk-menu[data-state="open"]{ animation:modern-sk-scale-in var(--dur-base) var(--ease-out); }
.msk-menu[data-state="closed"]{ animation:msk-scale-out var(--dur-quick) var(--ease-out); } .modern-sk-menu[data-state="closed"]{ animation:modern-sk-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; } .modern-sk-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; } .modern-sk-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); } .modern-sk-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); } .modern-sk-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); } .modern-sk-menu-item[data-highlighted] .ph, .modern-sk-menu-item[data-highlighted] .sc{ color:var(--lime-ink); }
.msk-menu-sep{ height:1px; background:var(--hair); margin:5px 4px; } .modern-sk-menu-sep{ height:1px; background:var(--hair); margin:5px 4px; }
/* ---------- TABS (Radix Tabs) ---------- */ /* ---------- TABS (Radix Tabs) ---------- */
.msk-tabs{ display:flex; gap:2px; border-bottom:1px solid var(--hair); } .modern-sk-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); } .modern-sk-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); } .modern-sk-tabs__trigger:hover{ color:var(--fg-2); }
.msk-tabs__trigger[data-state="active"]{ color:var(--fg-1); } .modern-sk-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); } .modern-sk-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) ---------- */ /* ---------- 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; } .modern-sk-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); } .modern-sk-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 ---------- */ /* ---------- 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); } .modern-sk-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; } .modern-sk-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); } .modern-sk-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); } .modern-sk-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); } .modern-sk-traffic.r{ background:radial-gradient(circle at 35% 30%,#ff8a7a,#ec5f55); }
.msk-traffic.y{ background:radial-gradient(circle at 35% 30%,#ffd97a,#e6a93c); } .modern-sk-traffic.y{ background:radial-gradient(circle at 35% 30%,#ffd97a,#e6a93c); }
.msk-traffic.g{ background:radial-gradient(circle at 35% 30%,#bff07a,#9bce4c); } .modern-sk-traffic.g{ background:radial-gradient(circle at 35% 30%,#bff07a,#9bce4c); }
/* ---------- TOOLTIP (Radix Tooltip) ---------- */ /* ---------- 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); } .modern-sk-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); } .modern-sk-tooltip[data-state="delayed-open"], .modern-sk-tooltip[data-state="instant-open"]{ animation:modern-sk-scale-in var(--dur-base) var(--ease-out); }
.msk-tooltip[data-state="closed"]{ animation:msk-fade-out var(--dur-quick) var(--ease-out); } .modern-sk-tooltip[data-state="closed"]{ animation:modern-sk-fade-out var(--dur-quick) var(--ease-out); }
/* ---------- ICON BUTTON ---------- /* ---------- ICON BUTTON ----------
Square tactile key. Reuses the .msk-btn engine; variants below Square tactile key. Reuses the .modern-sk-btn engine; variants below
just recolor. Pair with .msk-btn / .msk-btn--primary etc. */ just recolor. Pair with .modern-sk-btn / .modern-sk-btn--primary etc. */
.msk-iconbtn{ padding:0; width:32px; height:32px; aspect-ratio:1; display:inline-flex; align-items:center; justify-content:center; } .modern-sk-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; } .modern-sk-iconbtn--sm{ width:26px; height:26px; }
.msk-iconbtn--lg{ width:38px; height:38px; } .modern-sk-iconbtn--lg{ width:38px; height:38px; }
/* ---------- SPINNER ---------- /* ---------- SPINNER ----------
A carved donut groove (sunk like the switch well) with a glossy lime 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 arc spinning *inside* the channel. All drawn in SVG; CSS only sizes
the box and drives the rotation. Honors reduced motion. */ the box and drives the rotation. Honors reduced motion. */
.msk-spinner{ display:inline-block; width:24px; height:24px; line-height:0; } .modern-sk-spinner{ display:inline-block; width:24px; height:24px; line-height:0; }
.msk-spinner--sm{ width:16px; height:16px; } .modern-sk-spinner--sm{ width:16px; height:16px; }
.msk-spinner--lg{ width:34px; height:34px; } .modern-sk-spinner--lg{ width:34px; height:34px; }
.msk-spinner svg{ width:100%; height:100%; display:block; } .modern-sk-spinner svg{ width:100%; height:100%; display:block; }
.msk-spinner__arc{ .modern-sk-spinner__arc{
transform-origin:center; animation:msk-spin .7s linear infinite; transform-origin:center; animation:modern-sk-spin .7s linear infinite;
filter:drop-shadow(0 0 3px rgba(190,242,100,.55)); filter:drop-shadow(0 0 3px rgba(190,242,100,.55));
} }
@keyframes msk-spin{ to{ transform:rotate(360deg); } } @keyframes modern-sk-spin{ to{ transform:rotate(360deg); } }
@media (prefers-reduced-motion:reduce){ .msk-spinner__arc{ animation-duration:1.8s; } } @media (prefers-reduced-motion:reduce){ .modern-sk-spinner__arc{ animation-duration:1.8s; } }
/* ---------- CALLOUT ---------- /* ---------- CALLOUT ----------
Soft semantic surface (1214% tint on the felt) with a glossy Soft semantic surface (1214% tint on the felt) with a glossy
icon chip. Variants reuse the semantic --*-bg tokens. */ 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); } .modern-sk-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); } .modern-sk-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; } .modern-sk-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; } .modern-sk-callout__body strong{ color:var(--fg-1); font-weight:600; }
.msk-callout--success{ background:var(--success-bg); border-color:rgba(190,242,100,.22); } .modern-sk-callout--success{ background:var(--success-bg); border-color:rgba(190,242,100,.22); }
.msk-callout--success .msk-callout__icon{ color:var(--success); } .modern-sk-callout--success .modern-sk-callout__icon{ color:var(--success); }
.msk-callout--warning{ background:var(--warning-bg); border-color:rgba(230,169,60,.22); } .modern-sk-callout--warning{ background:var(--warning-bg); border-color:rgba(230,169,60,.22); }
.msk-callout--warning .msk-callout__icon{ color:var(--warning); } .modern-sk-callout--warning .modern-sk-callout__icon{ color:var(--warning); }
.msk-callout--danger{ background:var(--danger-bg); border-color:rgba(233,87,43,.25); } .modern-sk-callout--danger{ background:var(--danger-bg); border-color:rgba(233,87,43,.25); }
.msk-callout--danger .msk-callout__icon{ color:var(--danger); } .modern-sk-callout--danger .modern-sk-callout__icon{ color:var(--danger); }
/* ---------- TABLE ---------- /* ---------- TABLE ----------
The list-row aesthetic stretched to columns: hairline grid, sunk The list-row aesthetic stretched to columns: hairline grid, sunk
header strip, mono numerals, lime row selection. */ 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; } .modern-sk-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; } .modern-sk-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; } .modern-sk-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; } .modern-sk-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; } .modern-sk-table tbody tr:last-child td{ border-bottom:none; }
.msk-table tbody tr:hover td{ background:rgba(255,255,255,.03); } .modern-sk-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)); } .modern-sk-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; } .modern-sk-table .num{ font-family:var(--font-mono); font-size:12px; color:var(--fg-2); text-align:right; }
.msk-table .muted{ color:var(--fg-3); } .modern-sk-table .muted{ color:var(--fg-3); }
[data-theme="light"] .msk-table tbody tr:hover td{ background:rgba(0,0,0,.035); } [data-theme="light"] .modern-sk-table tbody tr:hover td{ background:rgba(0,0,0,.035); }
/* ---------- SCROLL AREA (Radix) ---------- */ /* ---------- SCROLL AREA (Radix) ---------- */
.msk-scroll{ overflow:hidden; } .modern-sk-scroll{ overflow:hidden; }
.msk-scroll__viewport{ width:100%; height:100%; border-radius:inherit; } .modern-sk-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); } .modern-sk-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; } .modern-sk-scroll__bar[data-orientation="vertical"]{ width:10px; }
.msk-scroll__bar[data-orientation="horizontal"]{ flex-direction:column; height:10px; } .modern-sk-scroll__bar[data-orientation="horizontal"]{ flex-direction:column; height:10px; }
.msk-scroll:hover .msk-scroll__bar{ background:rgba(0,0,0,.18); } .modern-sk-scroll:hover .modern-sk-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; } .modern-sk-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); } .modern-sk-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; } .modern-sk-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) ---------- /* ---------- DIALOG / MODAL (Radix Dialog + AlertDialog) ----------
The floating sheet: window chrome aesthetic, scrim with blur. */ 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); } .modern-sk-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); } .modern-sk-overlay[data-state="open"]{ animation:modern-sk-fade-in var(--dur-base) var(--ease-out); }
.msk-overlay[data-state="closed"]{ animation:msk-fade-out var(--dur-quick) var(--ease-out); } .modern-sk-overlay[data-state="closed"]{ animation:modern-sk-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; } .modern-sk-dialog{ position:fixed; z-index:81; top:50%; left:50%; 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; font-family:var(--font-sans); }
.msk-dialog[data-state="open"]{ animation:msk-scale-in var(--dur-base) var(--ease-out); } .modern-sk-dialog[data-state="open"]{ animation:modern-sk-scale-in var(--dur-base) var(--ease-out); }
.msk-dialog[data-state="closed"]{ animation:msk-scale-out var(--dur-quick) var(--ease-out); } .modern-sk-dialog[data-state="closed"]{ animation:modern-sk-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); } .modern-sk-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); } .modern-sk-dialog__desc{ margin-top:8px; font-size:14px; line-height:1.55; color:var(--fg-2); }
.msk-dialog__body{ margin-top:16px; } .modern-sk-dialog__body{ margin-top:16px; }
.msk-dialog__footer{ display:flex; justify-content:flex-end; gap:12px; margin-top:22px; } .modern-sk-dialog__footer{ display:flex; justify-content:flex-end; gap:12px; margin-top:22px; }
.msk-dialog__close{ position:absolute; top:14px; right:14px; } .modern-sk-dialog__close{ position:absolute; top:14px; right:14px; }
/* ============================================================ /* ============================================================
LIGHT THEME — component overrides (ported 1:1) 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"] .modern-sk-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"] .modern-sk-btn--ghost:active{ background:rgba(0,0,0,.08); }
[data-theme="light"] .msk-row:hover{ background:rgba(0,0,0,.035); } [data-theme="light"] .modern-sk-row:hover{ background:rgba(0,0,0,.035); }
[data-theme="light"] .msk-btn--ember{ [data-theme="light"] .modern-sk-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); 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{ [data-theme="light"] .modern-sk-switch__thumb{
background:linear-gradient(180deg,#ffffff,#e7e7dd); 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; 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"] .modern-sk-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"] .modern-sk-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"] .modern-sk-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"] .modern-sk-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"] .modern-sk-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"] .modern-sk-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"] .modern-sk-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"] .modern-sk-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"] .modern-sk-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"] .modern-sk-scroll:hover .modern-sk-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; } [data-theme="light"] .modern-sk-table thead th{ box-shadow:0 1px 0 rgba(255,255,255,.6) inset; }
+24
View File
@@ -0,0 +1,24 @@
/* ============================================================
ModernSK — optional branded webfonts.
Import this ONLY if you want the default ModernSK typefaces:
import 'modern-sk/styles.css'; // required — tokens + components
import 'modern-sk/fonts.css'; // optional — branded faces
Skip it and the --font-* tokens degrade to system-ui, or
override --font-display / --font-sans / --font-mono in your own
CSS to map components onto fonts your app already loads.
------------------------------------------------------------
Onest + Geist Mono come from Google Fonts; Anta (the squared
display face) is self-hosted and inlined at build time.
============================================================ */
@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');
@font-face {
font-family: 'Anta';
src: url('../assets/Anta-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
+1
View File
@@ -1,3 +1,4 @@
@import './fonts.css';
@import './tokens.css'; @import './tokens.css';
@import './components.css'; @import './components.css';
+4 -3
View File
@@ -1,12 +1,13 @@
/* ============================================================ /* ============================================================
ModernSK UI — shippable stylesheet. ModernSK — shippable stylesheet.
Tokens + component styles only. No demo/page layout, no global Tokens + component styles only. No demo/page layout, no global
reset of consumer elements. Font is inlined at build time. reset of consumer elements. No fonts: import 'modern-sk/fonts.css'
for the branded faces, or override the --font-* tokens.
============================================================ */ ============================================================ */
@import './tokens.css'; @import './tokens.css';
@import './components.css'; @import './components.css';
/* Non-invasive box-sizing for our own components only (zero specificity). */ /* Non-invasive box-sizing for our own components only (zero specificity). */
:where([class^='msk-'], [class*=' msk-']) { :where([class^='modern-sk-'], [class*=' modern-sk-']) {
box-sizing: border-box; box-sizing: border-box;
} }
+17 -22
View File
@@ -4,19 +4,14 @@
Old-iOS skeuomorphism × macOS Sequoia neatness × Ubuntu warmth. Old-iOS skeuomorphism × macOS Sequoia neatness × Ubuntu warmth.
------------------------------------------------------------ ------------------------------------------------------------
Single source of truth. Every component reads from here. Single source of truth. Every component reads from here.
------------------------------------------------------------
Fonts are NOT loaded here. The branded faces (Anta / Onest /
Geist Mono) live in the optional `modern-sk/fonts.css`. The
--font-* chains below degrade to system-ui when that file is
not imported, and an app can override the tokens to remap any
typeface without touching this file.
============================================================ */ ============================================================ */
@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 { :root {
/* ---------- BRAND ---------- */ /* ---------- BRAND ---------- */
--lime: #bef264; --lime: #bef264;
@@ -192,7 +187,7 @@
/* ============================================================ /* ============================================================
THE FELT — the one global background. THE FELT — the one global background.
============================================================ */ ============================================================ */
.msk-felt { .modern-sk-felt {
position: relative; position: relative;
background-color: var(--ink); background-color: var(--ink);
background-image: var(--bg-glow); background-image: var(--bg-glow);
@@ -201,7 +196,7 @@
background-attachment: fixed; background-attachment: fixed;
color: var(--fg-1); color: var(--fg-1);
} }
.msk-felt::before { .modern-sk-felt::before {
content: ""; content: "";
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -277,40 +272,40 @@
/* ============================================================ /* ============================================================
SEMANTIC TYPE CLASSES SEMANTIC TYPE CLASSES
============================================================ */ ============================================================ */
.msk-display { .modern-sk-display {
font-family: var(--font-display); font-weight: 400; font-family: var(--font-display); font-weight: 400;
font-size: var(--text-5xl); line-height: var(--lh-tight); font-size: var(--text-5xl); line-height: var(--lh-tight);
letter-spacing: -0.01em; color: var(--fg-1); letter-spacing: -0.01em; color: var(--fg-1);
} }
.msk-h1 { .modern-sk-h1 {
font-family: var(--font-sans); font-weight: var(--w-bold); font-family: var(--font-sans); font-weight: var(--w-bold);
font-size: var(--text-3xl); line-height: var(--lh-snug); font-size: var(--text-3xl); line-height: var(--lh-snug);
letter-spacing: var(--track-snug); color: var(--fg-1); letter-spacing: var(--track-snug); color: var(--fg-1);
} }
.msk-h2 { .modern-sk-h2 {
font-family: var(--font-sans); font-weight: var(--w-semibold); font-family: var(--font-sans); font-weight: var(--w-semibold);
font-size: var(--text-2xl); line-height: var(--lh-snug); font-size: var(--text-2xl); line-height: var(--lh-snug);
letter-spacing: var(--track-snug); color: var(--fg-1); letter-spacing: var(--track-snug); color: var(--fg-1);
} }
.msk-h3 { .modern-sk-h3 {
font-family: var(--font-sans); font-weight: var(--w-semibold); font-family: var(--font-sans); font-weight: var(--w-semibold);
font-size: var(--text-xl); line-height: var(--lh-snug); color: var(--fg-1); font-size: var(--text-xl); line-height: var(--lh-snug); color: var(--fg-1);
} }
.msk-body { .modern-sk-body {
font-family: var(--font-sans); font-weight: var(--w-regular); font-family: var(--font-sans); font-weight: var(--w-regular);
font-size: var(--text-base); line-height: var(--lh-normal); color: var(--fg-2); 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); } .modern-sk-body-strong { font-weight: var(--w-medium); color: var(--fg-1); }
.msk-caption { .modern-sk-caption {
font-family: var(--font-sans); font-weight: var(--w-medium); font-family: var(--font-sans); font-weight: var(--w-medium);
font-size: var(--text-sm); color: var(--fg-3); font-size: var(--text-sm); color: var(--fg-3);
} }
.msk-label { .modern-sk-label {
font-family: var(--font-sans); font-weight: var(--w-semibold); font-family: var(--font-sans); font-weight: var(--w-semibold);
font-size: 11px; text-transform: uppercase; font-size: 11px; text-transform: uppercase;
letter-spacing: var(--track-caps); color: var(--fg-3); letter-spacing: var(--track-caps); color: var(--fg-3);
} }
.msk-mono { .modern-sk-mono {
font-family: var(--font-mono); font-weight: var(--w-regular); font-family: var(--font-mono); font-weight: var(--w-regular);
font-size: var(--text-sm); color: var(--fg-2); font-size: var(--text-sm); color: var(--fg-2);
} }