Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03a70d27a3 | |||
| a7e2a1887d | |||
| 70c55dffad | |||
| db44cab4ea | |||
| 3d2ce9e0a7 |
@@ -14,7 +14,9 @@ jobs:
|
|||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "20"
|
||||||
registry-url: "https://git.ollyhearn.ru/api/packages/olly/npm/"
|
|
||||||
|
- name: Set version from tag
|
||||||
|
run: npm version "${GITHUB_REF_NAME#v}" --no-git-tag-version
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -22,7 +24,11 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Publish
|
- name: Configure Gitea npm auth
|
||||||
run: npm publish
|
run: |
|
||||||
|
echo "//git.ollyhearn.ru/api/packages/olly/npm/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: npm publish
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -21,6 +21,6 @@ export const Window = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{children}
|
<div className="modern-sk-window-body">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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!' } };
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user