Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03a70d27a3 | |||
| a7e2a1887d | |||
| 70c55dffad |
@@ -12,23 +12,28 @@ Old-iOS skeuomorphism × macOS Sequoia neatness × Ubuntu warmth.
|
||||
|
||||
## Install
|
||||
|
||||
Distributed via self-hosted git — install straight from the repo:
|
||||
Add the registry to your project `.npmrc`:
|
||||
|
||||
```
|
||||
@olly:registry=https://git.ollyhearn.ru/api/packages/olly/npm/
|
||||
```
|
||||
|
||||
Then install:
|
||||
|
||||
```bash
|
||||
npm i git+https://git.ollyhearn.ru/olly/modern-sk.git
|
||||
npm install @olly/@olly/modern-sk
|
||||
```
|
||||
|
||||
`react` and `react-dom` (>=18) are peer dependencies — your app provides them.
|
||||
The package builds itself on install via the `prepare` script.
|
||||
|
||||
## Usage
|
||||
|
||||
Import the stylesheet once at your app root, then use components anywhere:
|
||||
|
||||
```tsx
|
||||
import 'modern-sk/styles.css'; // required — tokens + components
|
||||
import 'modern-sk/fonts.css'; // optional — branded faces (see Fonts)
|
||||
import { ThemeProvider, TooltipProvider, Button, Card } from 'modern-sk';
|
||||
import '@olly/modern-sk/styles.css'; // required — tokens + components
|
||||
import '@olly/modern-sk/fonts.css'; // optional — branded faces (see Fonts)
|
||||
import { ThemeProvider, TooltipProvider, Button, Card } from '@olly/modern-sk';
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
@@ -49,7 +54,7 @@ export function App() {
|
||||
|
||||
## Fonts
|
||||
|
||||
`modern-sk/styles.css` ships **no fonts**. The type tokens default to a chain
|
||||
`@olly/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
|
||||
@@ -57,7 +62,7 @@ the optional stylesheet — Anta is self-hosted and inlined, no asset hosting
|
||||
needed:
|
||||
|
||||
```tsx
|
||||
import 'modern-sk/fonts.css';
|
||||
import '@olly/modern-sk/fonts.css';
|
||||
```
|
||||
|
||||
To use your **own** fonts, skip `fonts.css` and override the tokens anywhere
|
||||
|
||||
@@ -63,11 +63,16 @@ function diffChars(prev: ReadonlyArray<{ id: number; char: string }>, next: stri
|
||||
return { reusedId, keptPrev };
|
||||
}
|
||||
|
||||
/** Browsers render password glyphs as this bullet; match it in the overlay
|
||||
* so animated fields don't leak the real characters visually. */
|
||||
const PASSWORD_MASK_CHAR = '•';
|
||||
|
||||
function useFieldAnimation(
|
||||
multiline: boolean,
|
||||
externalRef: Ref<FieldElement>,
|
||||
controlledValue: ComponentPropsWithoutRef<'input'>['value'],
|
||||
initial: ComponentPropsWithoutRef<'input'>['defaultValue'],
|
||||
masked = false,
|
||||
) {
|
||||
const innerRef = useRef<FieldElement | null>(null);
|
||||
const overlayRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -144,33 +149,34 @@ function useFieldAnimation(
|
||||
|
||||
const overlay = (
|
||||
<div
|
||||
ref={overlayRef}
|
||||
aria-hidden="true"
|
||||
className={cx('modern-sk-field-overlay', multiline && 'modern-sk-field-overlay--multiline')}
|
||||
>
|
||||
{entries.map((e) =>
|
||||
e.leaving ? (
|
||||
<span
|
||||
key={`${idPrefix}-${e.id}`}
|
||||
className="modern-sk-field-char modern-sk-field-char--leaving"
|
||||
style={{ left: e.x, top: e.y }}
|
||||
onAnimationEnd={() => onLeaveEnd(e.id)}
|
||||
>
|
||||
{e.char}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
key={`${idPrefix}-${e.id}`}
|
||||
ref={(node) => {
|
||||
if (node) spanRefs.current.set(e.id, node);
|
||||
else spanRefs.current.delete(e.id);
|
||||
}}
|
||||
className={cx('modern-sk-field-char', !multiline && 'modern-sk-field-char--composited')}
|
||||
>
|
||||
{e.char}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
<div ref={overlayRef} className="modern-sk-field-overlay-content">
|
||||
{entries.map((e) =>
|
||||
e.leaving ? (
|
||||
<span
|
||||
key={`${idPrefix}-${e.id}`}
|
||||
className="modern-sk-field-char modern-sk-field-char--leaving"
|
||||
style={{ left: e.x, top: e.y }}
|
||||
onAnimationEnd={() => onLeaveEnd(e.id)}
|
||||
>
|
||||
{masked ? PASSWORD_MASK_CHAR : e.char}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
key={`${idPrefix}-${e.id}`}
|
||||
ref={(node) => {
|
||||
if (node) spanRefs.current.set(e.id, node);
|
||||
else spanRefs.current.delete(e.id);
|
||||
}}
|
||||
className={cx('modern-sk-field-char', !multiline && 'modern-sk-field-char--composited')}
|
||||
>
|
||||
{masked ? PASSWORD_MASK_CHAR : e.char}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -187,6 +193,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
ref,
|
||||
props.value,
|
||||
props.defaultValue,
|
||||
props.type === 'password',
|
||||
);
|
||||
if (!animated) {
|
||||
return (
|
||||
|
||||
@@ -21,6 +21,6 @@ export const Window = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
<div className="modern-sk-window-body">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,9 @@ 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 }} />,
|
||||
render: () => (
|
||||
<TextArea rows={4} placeholder="Multiple lines…" style={{ width: 320 }} />
|
||||
),
|
||||
};
|
||||
|
||||
export const Search: Story = {
|
||||
@@ -46,3 +48,5 @@ export const Search: Story = {
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const Password: Story = { args: { type: 'password', defaultValue: 'hi!' } };
|
||||
|
||||
@@ -215,6 +215,7 @@ textarea.modern-sk-field {
|
||||
.modern-sk-search .ph {
|
||||
position: absolute;
|
||||
left: 11px;
|
||||
z-index: 1;
|
||||
color: var(--fg-3);
|
||||
font-size: 16px;
|
||||
pointer-events: none;
|
||||
@@ -255,13 +256,19 @@ textarea.modern-sk-field {
|
||||
color: var(--fg-1);
|
||||
white-space: pre;
|
||||
text-align: left;
|
||||
will-change: transform;
|
||||
}
|
||||
.modern-sk-field-overlay--multiline {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
line-height: 1.5;
|
||||
}
|
||||
/* Scroll-sync transform lives on this inner div, not the clipping parent above —
|
||||
transforming the overflow:hidden box itself would drag its clip region along
|
||||
with the content and let overflowing lines spill outside the field's border. */
|
||||
.modern-sk-field-overlay-content {
|
||||
position: relative;
|
||||
will-change: transform;
|
||||
}
|
||||
.modern-sk-search .modern-sk-field-overlay {
|
||||
padding-left: 34px;
|
||||
}
|
||||
@@ -1227,6 +1234,9 @@ textarea.modern-sk-field {
|
||||
background: var(--steel-800);
|
||||
box-shadow: var(--shadow-window);
|
||||
}
|
||||
.modern-sk-window-body {
|
||||
padding: 16px;
|
||||
}
|
||||
.modern-sk-titlebar {
|
||||
height: 42px;
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user