3 Commits

Author SHA1 Message Date
Senko-san 03a70d27a3 chore: textfield visuals fixed
Publish npm package / publish (push) Successful in 18s
2026-07-11 16:18:26 +03:00
olly a7e2a1887d feat(window): add default inner padding to Window body
Publish npm package / publish (push) Successful in 17s
Window children rendered flush against the frame, so content (settings
rows, placeholder text) touched the left edge. Wrap children in
.modern-sk-window-body with 16px padding for consistent breathing room.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:44:17 +03:00
Senko-san 70c55dffad Update README.md 2026-06-06 15:23:16 +03:00
5 changed files with 61 additions and 35 deletions
+13 -8
View File
@@ -12,23 +12,28 @@ Old-iOS skeuomorphism × macOS Sequoia neatness × Ubuntu warmth.
## Install ## 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 ```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. `react` and `react-dom` (>=18) are peer dependencies — your app provides them.
The package builds itself on install via the `prepare` script.
## Usage ## Usage
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 'modern-sk/styles.css'; // required — tokens + components import '@olly/modern-sk/styles.css'; // required — tokens + components
import 'modern-sk/fonts.css'; // optional — branded faces (see Fonts) import '@olly/modern-sk/fonts.css'; // optional — branded faces (see Fonts)
import { ThemeProvider, TooltipProvider, Button, Card } from 'modern-sk'; import { ThemeProvider, TooltipProvider, Button, Card } from '@olly/modern-sk';
export function App() { export function App() {
return ( return (
@@ -49,7 +54,7 @@ export function App() {
## Fonts ## 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. 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 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: needed:
```tsx ```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 To use your **own** fonts, skip `fonts.css` and override the tokens anywhere
+10 -3
View File
@@ -63,11 +63,16 @@ function diffChars(prev: ReadonlyArray<{ id: number; char: string }>, next: stri
return { reusedId, keptPrev }; 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( function useFieldAnimation(
multiline: boolean, multiline: boolean,
externalRef: Ref<FieldElement>, externalRef: Ref<FieldElement>,
controlledValue: ComponentPropsWithoutRef<'input'>['value'], controlledValue: ComponentPropsWithoutRef<'input'>['value'],
initial: ComponentPropsWithoutRef<'input'>['defaultValue'], initial: ComponentPropsWithoutRef<'input'>['defaultValue'],
masked = false,
) { ) {
const innerRef = useRef<FieldElement | null>(null); const innerRef = useRef<FieldElement | null>(null);
const overlayRef = useRef<HTMLDivElement | null>(null); const overlayRef = useRef<HTMLDivElement | null>(null);
@@ -144,10 +149,10 @@ function useFieldAnimation(
const overlay = ( const overlay = (
<div <div
ref={overlayRef}
aria-hidden="true" aria-hidden="true"
className={cx('modern-sk-field-overlay', multiline && 'modern-sk-field-overlay--multiline')} className={cx('modern-sk-field-overlay', multiline && 'modern-sk-field-overlay--multiline')}
> >
<div ref={overlayRef} className="modern-sk-field-overlay-content">
{entries.map((e) => {entries.map((e) =>
e.leaving ? ( e.leaving ? (
<span <span
@@ -156,7 +161,7 @@ function useFieldAnimation(
style={{ left: e.x, top: e.y }} style={{ left: e.x, top: e.y }}
onAnimationEnd={() => onLeaveEnd(e.id)} onAnimationEnd={() => onLeaveEnd(e.id)}
> >
{e.char} {masked ? PASSWORD_MASK_CHAR : e.char}
</span> </span>
) : ( ) : (
<span <span
@@ -167,11 +172,12 @@ function useFieldAnimation(
}} }}
className={cx('modern-sk-field-char', !multiline && 'modern-sk-field-char--composited')} className={cx('modern-sk-field-char', !multiline && 'modern-sk-field-char--composited')}
> >
{e.char} {masked ? PASSWORD_MASK_CHAR : e.char}
</span> </span>
), ),
)} )}
</div> </div>
</div>
); );
return { setRef, overlay, handleChange, syncScroll }; return { setRef, overlay, handleChange, syncScroll };
@@ -187,6 +193,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
ref, ref,
props.value, props.value,
props.defaultValue, props.defaultValue,
props.type === 'password',
); );
if (!animated) { if (!animated) {
return ( return (
+1 -1
View File
@@ -21,6 +21,6 @@ export const Window = ({
</div> </div>
)} )}
</div> </div>
{children} <div className="modern-sk-window-body">{children}</div>
</div> </div>
); );
+5 -1
View File
@@ -34,7 +34,9 @@ export const Playground: Story = {};
export const Disabled: Story = { args: { disabled: true, value: 'Read only' } }; export const Disabled: Story = { args: { disabled: true, value: 'Read only' } };
export const Multiline: Story = { 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 = { export const Search: Story = {
@@ -46,3 +48,5 @@ export const Search: Story = {
/> />
), ),
}; };
export const Password: Story = { args: { type: 'password', defaultValue: 'hi!' } };
+11 -1
View File
@@ -215,6 +215,7 @@ textarea.modern-sk-field {
.modern-sk-search .ph { .modern-sk-search .ph {
position: absolute; position: absolute;
left: 11px; left: 11px;
z-index: 1;
color: var(--fg-3); color: var(--fg-3);
font-size: 16px; font-size: 16px;
pointer-events: none; pointer-events: none;
@@ -255,13 +256,19 @@ textarea.modern-sk-field {
color: var(--fg-1); color: var(--fg-1);
white-space: pre; white-space: pre;
text-align: left; text-align: left;
will-change: transform;
} }
.modern-sk-field-overlay--multiline { .modern-sk-field-overlay--multiline {
white-space: pre-wrap; white-space: pre-wrap;
overflow-wrap: break-word; overflow-wrap: break-word;
line-height: 1.5; 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 { .modern-sk-search .modern-sk-field-overlay {
padding-left: 34px; padding-left: 34px;
} }
@@ -1227,6 +1234,9 @@ textarea.modern-sk-field {
background: var(--steel-800); background: var(--steel-800);
box-shadow: var(--shadow-window); box-shadow: var(--shadow-window);
} }
.modern-sk-window-body {
padding: 16px;
}
.modern-sk-titlebar { .modern-sk-titlebar {
height: 42px; height: 42px;
display: flex; display: flex;