@overpunch/opszstepper 1.0.18 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -212
- package/dist/core.cjs +1 -0
- package/dist/core.d.ts +114 -0
- package/dist/core.js +155 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +12 -18
- package/dist/index.js +38 -106
- package/dist/opszstepper.webflow.min.js +1 -1
- package/package.json +89 -84
package/README.md
CHANGED
|
@@ -1,212 +1,216 @@
|
|
|
1
|
-
# opszStepper
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@overpunch/opszstepper) [](https://opensource.org/licenses/MIT) [](https://github.com/over-punch/type-tools)
|
|
4
|
-
|
|
5
|
-
`font-optical-sizing: auto` only works for variable fonts with an `opsz` axis. opszStepper solves the other case: professional typeface families that ship separate font files for each optical size cut (Micro, Text, Display) with no axis at all. It automatically swaps the correct cut onto an element as its `font-size` changes.
|
|
6
|
-
|
|
7
|
-
**[opszstepper.com](https://opszstepper.com)** · [npm](https://www.npmjs.com/package/@overpunch/opszstepper) · [GitHub](https://github.com/over-punch/opszStepper)
|
|
8
|
-
|
|
9
|
-
TypeScript · Zero dependencies · React + Vanilla JS
|
|
10
|
-
|
|
11
|
-

|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## Install
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install @overpunch/opszstepper
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## Usage
|
|
24
|
-
|
|
25
|
-
> **Next.js App Router:** this library uses browser APIs. Add `"use client"` to any component file that imports from it.
|
|
26
|
-
|
|
27
|
-
### What are optical cuts?
|
|
28
|
-
|
|
29
|
-
Many professional editorial typefaces ship as a family of separate font files — each drawn specifically for a different size range. Halyard has Halyard Micro (captions and footnotes), Halyard Text (body), and Halyard Display (headlines). Tiempos has Tiempos Fine, Tiempos Text, and Tiempos Headline. Each cut has different contrast, spacing, and stroke weight tuned for its intended size. CSS has no mechanism to switch between them automatically — `font-optical-sizing: auto` only controls the `opsz` axis of a single variable font file. opszStepper fills that gap.
|
|
30
|
-
|
|
31
|
-
Scaling one cut to every size is the problem opszStepper avoids. On the left, a single Display cut is used at all sizes — fine when large, but its high contrast and tight spacing turn spindly and fragile when shrunk. On the right, opszStepper swaps in the cut drawn for each size:
|
|
32
|
-
|
|
33
|
-

|
|
34
|
-
|
|
35
|
-
### Two modes: family hot-swap or `opsz` axis
|
|
36
|
-
|
|
37
|
-
opszStepper covers both ways type families ship optical sizes:
|
|
38
|
-
|
|
39
|
-
1. **Multi-family hot-swap** *(the primary case)* — separate font files per cut (Halyard Micro / Text / Display). Give each cut a different `family`; opszStepper sets `font-family` to the matching cut as `font-size` crosses each threshold. Every example below uses this mode.
|
|
40
|
-
2. **Single variable font, `opsz` axis** — one variable font with an `opsz` axis (Fraunces, Recursive, Amstelvar). Use the *same* `family` in every cut and add an `opszValue` per cut; opszStepper
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
cuts: [
|
|
44
|
-
{ family: 'Fraunces, serif', maxSize: 13, opszValue: 9, opszMin: 9, opszMax: 144 },
|
|
45
|
-
{ family: 'Fraunces, serif', minSize: 13, maxSize: 28, opszValue: 24, opszMin: 9, opszMax: 144 },
|
|
46
|
-
{ family: 'Fraunces, serif', minSize: 28, opszValue: 72, opszMin: 9, opszMax: 144 },
|
|
47
|
-
]
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
This steps the axis at discrete thresholds with hysteresis, which is useful when you want explicit control over the `opsz` value per size band rather than the browser's continuous `font-optical-sizing: auto`.
|
|
51
|
-
|
|
52
|
-
### React component
|
|
53
|
-
|
|
54
|
-
```tsx
|
|
55
|
-
import { OpszStepperText } from '@overpunch/opszstepper'
|
|
56
|
-
|
|
57
|
-
<OpszStepperText
|
|
58
|
-
cuts={[
|
|
59
|
-
{ family: 'Halyard Micro, sans-serif', maxSize: 13 },
|
|
60
|
-
{ family: 'Halyard Text, sans-serif', minSize: 13, maxSize: 28 },
|
|
61
|
-
{ family: 'Halyard Display, sans-serif', minSize: 28 },
|
|
62
|
-
]}
|
|
63
|
-
>
|
|
64
|
-
Your paragraph text here...
|
|
65
|
-
</OpszStepperText>
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### React hook
|
|
69
|
-
|
|
70
|
-
```tsx
|
|
71
|
-
import { useOpszStepper } from '@overpunch/opszstepper'
|
|
72
|
-
|
|
73
|
-
// Inside a React component:
|
|
74
|
-
const ref = useOpszStepper({
|
|
75
|
-
cuts: [
|
|
76
|
-
{ family: 'Halyard Micro, sans-serif', maxSize: 13 },
|
|
77
|
-
{ family: 'Halyard Text, sans-serif', minSize: 13, maxSize: 28 },
|
|
78
|
-
{ family: 'Halyard Display, sans-serif', minSize: 28 },
|
|
79
|
-
],
|
|
80
|
-
})
|
|
81
|
-
return <p ref={ref}>{children}</p>
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
The hook starts
|
|
85
|
-
|
|
86
|
-
### Vanilla JS —
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
{ family: 'Halyard
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
{ family: 'Halyard
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
{ family: 'Tiempos
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
|
145
|
-
|
|
146
|
-
| `
|
|
147
|
-
| `
|
|
148
|
-
| `
|
|
149
|
-
| `
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
{ family: 'Halyard
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
**
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
The
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
- **
|
|
1
|
+
# opszStepper
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@overpunch/opszstepper) [](https://opensource.org/licenses/MIT) [](https://github.com/over-punch/type-tools)
|
|
4
|
+
|
|
5
|
+
`font-optical-sizing: auto` only works for variable fonts with an `opsz` axis. opszStepper solves the other case: professional typeface families that ship separate font files for each optical size cut (Micro, Text, Display) with no axis at all. It automatically swaps the correct cut onto an element as its `font-size` changes.
|
|
6
|
+
|
|
7
|
+
**[opszstepper.com](https://opszstepper.com)** · [npm](https://www.npmjs.com/package/@overpunch/opszstepper) · [GitHub](https://github.com/over-punch/opszStepper)
|
|
8
|
+
|
|
9
|
+
TypeScript · Zero dependencies · React + Vanilla JS
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @overpunch/opszstepper
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
> **Next.js App Router:** this library uses browser APIs. Add `"use client"` to any component file that imports from it.
|
|
26
|
+
|
|
27
|
+
### What are optical cuts?
|
|
28
|
+
|
|
29
|
+
Many professional editorial typefaces ship as a family of separate font files — each drawn specifically for a different size range. Halyard has Halyard Micro (captions and footnotes), Halyard Text (body), and Halyard Display (headlines). Tiempos has Tiempos Fine, Tiempos Text, and Tiempos Headline. Each cut has different contrast, spacing, and stroke weight tuned for its intended size. CSS has no mechanism to switch between them automatically — `font-optical-sizing: auto` only controls the `opsz` axis of a single variable font file. opszStepper fills that gap.
|
|
30
|
+
|
|
31
|
+
Scaling one cut to every size is the problem opszStepper avoids. On the left, a single Display cut is used at all sizes — fine when large, but its high contrast and tight spacing turn spindly and fragile when shrunk. On the right, opszStepper swaps in the cut drawn for each size:
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
### Two modes: family hot-swap or `opsz` axis
|
|
36
|
+
|
|
37
|
+
opszStepper covers both ways type families ship optical sizes:
|
|
38
|
+
|
|
39
|
+
1. **Multi-family hot-swap** *(the primary case)* — separate font files per cut (Halyard Micro / Text / Display). Give each cut a different `family`; opszStepper sets `font-family` to the matching cut as `font-size` crosses each threshold. Every example below uses this mode.
|
|
40
|
+
2. **Single variable font, `opsz` axis** — one variable font with an `opsz` axis (Fraunces, Recursive, Amstelvar). Use the *same* `family` in every cut and add an `opszValue` per cut; opszStepper sets the `opsz` axis in `font-variation-settings` (keeping any other axes you've set, such as `wght`) instead of swapping files. Optional `opszMin`/`opszMax` clamp the value to the font's fvar range:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
cuts: [
|
|
44
|
+
{ family: 'Fraunces, serif', maxSize: 13, opszValue: 9, opszMin: 9, opszMax: 144 },
|
|
45
|
+
{ family: 'Fraunces, serif', minSize: 13, maxSize: 28, opszValue: 24, opszMin: 9, opszMax: 144 },
|
|
46
|
+
{ family: 'Fraunces, serif', minSize: 28, opszValue: 72, opszMin: 9, opszMax: 144 },
|
|
47
|
+
]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This steps the axis at discrete thresholds with hysteresis, which is useful when you want explicit control over the `opsz` value per size band rather than the browser's continuous `font-optical-sizing: auto`.
|
|
51
|
+
|
|
52
|
+
### React component
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { OpszStepperText } from '@overpunch/opszstepper'
|
|
56
|
+
|
|
57
|
+
<OpszStepperText
|
|
58
|
+
cuts={[
|
|
59
|
+
{ family: 'Halyard Micro, sans-serif', maxSize: 13 },
|
|
60
|
+
{ family: 'Halyard Text, sans-serif', minSize: 13, maxSize: 28 },
|
|
61
|
+
{ family: 'Halyard Display, sans-serif', minSize: 28 },
|
|
62
|
+
]}
|
|
63
|
+
>
|
|
64
|
+
Your paragraph text here...
|
|
65
|
+
</OpszStepperText>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### React hook
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { useOpszStepper } from '@overpunch/opszstepper'
|
|
72
|
+
|
|
73
|
+
// Inside a React component:
|
|
74
|
+
const ref = useOpszStepper({
|
|
75
|
+
cuts: [
|
|
76
|
+
{ family: 'Halyard Micro, sans-serif', maxSize: 13 },
|
|
77
|
+
{ family: 'Halyard Text, sans-serif', minSize: 13, maxSize: 28 },
|
|
78
|
+
{ family: 'Halyard Display, sans-serif', minSize: 28 },
|
|
79
|
+
],
|
|
80
|
+
})
|
|
81
|
+
return <p ref={ref}>{children}</p>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The hook starts the stepper on the element and re-evaluates the active cut whenever its font-size may have changed (see [How it works](#how-it-works)). It restarts when the cuts or `hysteresis` change, follows the element if React replaces it (a changed `as`, a conditional mount), always calls the latest `onCutChange`, and cleans up on unmount.
|
|
85
|
+
|
|
86
|
+
### Vanilla JS — live
|
|
87
|
+
|
|
88
|
+
The main entry also exports the React hook and component, so it imports `react`. Without React installed, import from the React-free subpath `@overpunch/opszstepper/core`:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { startOpszStepper } from '@overpunch/opszstepper/core'
|
|
92
|
+
|
|
93
|
+
const el = document.querySelector('p')
|
|
94
|
+
|
|
95
|
+
const cuts = [
|
|
96
|
+
{ family: 'Halyard Micro, sans-serif', maxSize: 13 },
|
|
97
|
+
{ family: 'Halyard Text, sans-serif', minSize: 13, maxSize: 28 },
|
|
98
|
+
{ family: 'Halyard Display, sans-serif', minSize: 28 },
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
let stop = startOpszStepper(el, { cuts })
|
|
102
|
+
|
|
103
|
+
// Later — stop watching and restore the original styles:
|
|
104
|
+
// stop()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Vanilla JS — one-shot
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { applyOpszStepper } from '@overpunch/opszstepper'
|
|
111
|
+
|
|
112
|
+
const el = document.querySelector('p')
|
|
113
|
+
|
|
114
|
+
applyOpszStepper(el, {
|
|
115
|
+
cuts: [
|
|
116
|
+
{ family: 'Halyard Micro, sans-serif', maxSize: 13 },
|
|
117
|
+
{ family: 'Halyard Text, sans-serif', minSize: 13, maxSize: 28 },
|
|
118
|
+
{ family: 'Halyard Display, sans-serif', minSize: 28 },
|
|
119
|
+
],
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Later — restore the original styles:
|
|
123
|
+
// removeOpszStepper(el)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### TypeScript
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import type { OpszStepperCut, OpszStepperOptions } from '@overpunch/opszstepper'
|
|
130
|
+
|
|
131
|
+
const cuts: OpszStepperCut[] = [
|
|
132
|
+
{ family: 'Tiempos Fine, serif', maxSize: 13 },
|
|
133
|
+
{ family: 'Tiempos Text, serif', minSize: 13, maxSize: 28 },
|
|
134
|
+
{ family: 'Tiempos Headline, serif', minSize: 28 },
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
const opts: OpszStepperOptions = { cuts, hysteresis: 2 }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Options
|
|
143
|
+
|
|
144
|
+
| Option | Default | Description |
|
|
145
|
+
|--------|---------|-------------|
|
|
146
|
+
| `cuts` | *(required)* | Array of `OpszStepperCut` objects defining each optical size cut and the font-size range it applies to. Each cut has a `family` string (CSS `font-family` value), an optional `minSize` in px (inclusive, default `0`), and an optional `maxSize` in px (exclusive, default `Infinity`). Ranges should be contiguous and non-overlapping. They can be listed in any order, and a cut with only `maxSize` starts where the previous one ends — see the cuts configuration guide below |
|
|
147
|
+
| `cut.opszValue` | `undefined` | *(per-cut, `opsz`-axis mode only)* When set, opszStepper sets the `opsz` axis in the element's `font-variation-settings`, keeping its other axes. Use with a single variable font shared across all cuts — see [Two modes](#two-modes-family-hot-swap-or-opsz-axis) |
|
|
148
|
+
| `cut.opszMin` / `cut.opszMax` | `undefined` | *(per-cut, `opsz`-axis mode only)* Clamp the written `opszValue` to the font's fvar `opsz` axis range. Ignored unless `opszValue` is set |
|
|
149
|
+
| `hysteresis` | `1` | Dead zone in px around each cut boundary. When font-size sits within `hysteresis` px of a threshold, the current cut is held rather than switching. Prevents oscillation when font-size is computed to hover right at a boundary due to sub-pixel rendering or responsive scaling. Increase to `2`–`4` if you observe rapid toggling. A value larger than half the narrowest cut is reduced to that (with a warning), so a cut can't be skipped |
|
|
150
|
+
| `onCutChange` | `undefined` | Callback fired each time the active cut changes. Receives the newly applied `OpszStepperCut`. Useful for logging, analytics, or synchronising sibling elements |
|
|
151
|
+
| `as` | `'p'` | HTML element to render. Accepts any valid React element type, e.g. `'h1'`, `'div'`, `'span'`. *(React component only)* |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Cuts configuration guide
|
|
156
|
+
|
|
157
|
+
A cut is active when the element's computed `font-size` satisfies `minSize <= fontSize < maxSize`. The bounds are in CSS pixels as returned by `getComputedStyle(el).fontSize`.
|
|
158
|
+
|
|
159
|
+
Structure cuts as contiguous ranges — each `maxSize` should equal the next cut's `minSize`:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
cuts: [
|
|
163
|
+
{ family: 'Halyard Micro, sans-serif', maxSize: 13 }, // 0px – 13px
|
|
164
|
+
{ family: 'Halyard Text, sans-serif', minSize: 13, maxSize: 28 }, // 13px – 28px
|
|
165
|
+
{ family: 'Halyard Display, sans-serif', minSize: 28 }, // 28px – ∞
|
|
166
|
+
]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
You can omit the smallest cut's `minSize` (defaults to `0`) and the largest cut's `maxSize` (defaults to `Infinity`). If font-size falls outside all defined ranges — which should not happen with a complete contiguous set — the active cut is left unchanged.
|
|
170
|
+
|
|
171
|
+
**Hysteresis at boundaries:** if font-size is `13.4px` and the active cut was Halyard Text (`minSize: 13`), the tool will not switch to Halyard Micro until `fontSize < 13 - hysteresis` (i.e. `< 12` with the default `hysteresis: 1`). Moving in the other direction — Text to Display — requires `fontSize > 28 + hysteresis` (i.e. `> 29`). This prevents flicker when a responsive layout computes font-size to a value that oscillates across a boundary.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## How it works
|
|
176
|
+
|
|
177
|
+
`startOpszStepper` reads the element's computed `font-size` via `getComputedStyle(el).fontSize` and finds the matching cut. It then sets `el.style.fontFamily` to that cut's `family` string, overriding whatever the stylesheet specifies. The original inline `font-family` and `font-variation-settings` (with any `!important`) are saved so they can be restored exactly when `removeOpszStepper` or the stop function is called. In family mode `font-variation-settings` is left alone.
|
|
178
|
+
|
|
179
|
+
A font-size can change without the element's box changing size (a fixed `line-height`, an inline `<span>`, a fixed-size box), so a `ResizeObserver` alone isn't enough. opszStepper re-checks when the element or its parent resizes (container queries), when a `class` or `style` attribute changes anywhere on the page, and when the window resizes (viewport units, media queries). All watched elements share these observers, and each check reads every font-size first and then writes the changed cuts, so a resize with thousands of elements costs one style recalculation. Hysteresis is applied before switching, so a swap only fires when the size has moved clearly past a threshold.
|
|
180
|
+
|
|
181
|
+
**Limits:** a font-size change that comes from none of these (for example an animation of `font-size` on a parent) isn't seen until one of them fires; call `applyOpszStepper` yourself in that case. The size used is the computed `font-size`, so CSS `zoom` and transforms don't change the cut. opszStepper doesn't move the page's scroll position: browsers' scroll anchoring handles the small reflow a swap can cause.
|
|
182
|
+
|
|
183
|
+
**`document.fonts.load()` is not awaited.** The cut swap is immediate — opszStepper sets `font-family` and the browser handles the font load. If a cut's font file has not yet loaded, the browser will show a fallback until it arrives (standard FOUT behaviour). If you need to eliminate FOUT, preload each cut's font file in the document `<head>` using `<link rel="preload" as="font">`. opszStepper does not manage font loading.
|
|
184
|
+
|
|
185
|
+
**Original fontFamily is saved and restored.** When `removeOpszStepper(el)` or the stop function from `startOpszStepper` is called, the element's `style.fontFamily` is reset to exactly the value it had before the first call. If the element had no inline `fontFamily`, the inline property is removed (deferring to the stylesheet), and an element that had no `style` attribute is left without one.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Dev notes
|
|
190
|
+
|
|
191
|
+
### `next` in root devDependencies
|
|
192
|
+
|
|
193
|
+
`package.json` at the repo root lists `next` as a devDependency. This is a **Vercel detection workaround** — not a real dependency of the npm package. Vercel's build system inspects the root `package.json` to detect the framework; without `next` present it falls back to a static build and skips the Next.js pipeline, breaking the `/site` subdirectory deploy.
|
|
194
|
+
|
|
195
|
+
The package itself has zero runtime dependencies. Do not remove this entry.
|
|
196
|
+
|
|
197
|
+
### Regenerating the README visuals
|
|
198
|
+
|
|
199
|
+
The hero and comparison images live in `assets/` and are produced by a committed, re-runnable harness in `scripts/` (it renders the three real Cormorant optical cuts in headless Chromium and screenshots each scene). `assets/` and `scripts/` are kept out of the npm tarball — the package ships `dist` only.
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npm i -D playwright && npx playwright install chromium
|
|
203
|
+
node scripts/capture.mjs # writes assets/hero.png and assets/compare.png
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Bump the `?v=N` cache-buster on the image URLs in this README after regenerating.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Future improvements
|
|
211
|
+
|
|
212
|
+
- **Container query support** — re-evaluate cuts in response to `@container` size changes, not just element resize, for components embedded in container-query layouts where `font-size` is driven by container width
|
|
213
|
+
- **Smooth crossfade** — optionally apply a short CSS `transition: font-family` equivalent using a brief opacity fade between cuts to soften the swap on large editorial pages
|
|
214
|
+
- **Multi-element sync** — a `syncGroup` option to tie multiple elements to the same active cut, so a heading and its pull-quote always use the same optical cut at all times
|
|
215
|
+
- **Font preload hints** — automatically inject `<link rel="preload">` tags for all cut font files on first call, so the browser can fetch them before they are needed
|
|
216
|
+
- **SSR hydration** — detect the correct cut server-side via a CSS custom property or data attribute so the initial render uses the right `font-family` without a post-hydration swap
|
package/dist/core.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const g=1;function $(t){const e=t.filter(s=>s&&typeof s.family=="string"),n=s=>Number.isFinite(s.minSize)?s.minSize:Number.isFinite(s.maxSize)?s.maxSize-1e-9:1/0,i=[...e].sort((s,u)=>n(s)-n(u)),r=i.map((s,u)=>{const f=i[u-1];return{min:Number.isFinite(s.minSize)?s.minSize:f&&Number.isFinite(f.maxSize)?f.maxSize:0,max:Number.isFinite(s.maxSize)?s.maxSize:1/0,cut:s}});for(let s=0;s<r.length-1;s++)r[s].max===1/0&&(r[s].max=r[s+1].min);return r}function z(t,e){for(let n=0;n<t.length;n++)if(e>=t[n].min&&e<t[n].max)return n;return-1}function R(t,e,n,i){const r=z(t,e);if(n===-1||!t[n])return r;if(r===-1||r===n)return n;const s=t[n];return e>=s.max+i||e<s.min-i?r:n}const A=new Set;function N(t){A.has(t)||(A.add(t),console.warn(t))}function H(t,e){let n=t??g;(!Number.isFinite(n)||n<0)&&(N(`[opszStepper] hysteresis must be a non-negative number; got ${t}, using ${g}`),n=g);const i=Math.min(...e.map(r=>r.max-r.min).filter(r=>Number.isFinite(r)&&r>0));return Number.isFinite(i)&&n>i/2&&(N(`[opszStepper] hysteresis ${n}px is more than half the narrowest cut (${i}px); using ${i/2}px`),n=i/2),n}const a=new Map;function S(t,e){return{value:t.style.getPropertyValue(e),priority:t.style.getPropertyPriority(e)}}function v(t,e,n){n.value?t.style.setProperty(e,n.value,n.priority):t.style.removeProperty(e)}function M(t,e){const n=$(e.cuts),i=H(e.hysteresis,n);let r=a.get(t);return r?(r.cuts=n,r.hysteresis=i,r.onCutChange=e.onCutChange):(r={cuts:n,hysteresis:i,onCutChange:e.onCutChange,activeIndex:-1,origFamily:S(t,"font-family"),origFVS:S(t,"font-variation-settings"),origStyleAttr:t.getAttribute("style"),baseFVS:I(t),writtenFVS:null,writtenStyleAttr:null,live:!1},a.set(t,r)),r}function I(t){const e=getComputedStyle(t);return(typeof e.getPropertyValue=="function"?e.getPropertyValue("font-variation-settings"):e.fontVariationSettings)||"normal"}function L(t,e){const n=`"opsz" ${e}`;if(!t||t==="normal")return n;const i=/(["'])opsz\1\s+-?[\d.eE+-]+/;return i.test(t)?t.replace(i,n):`${t}, ${n}`}function F(t,e,n){const i=e.cuts[n].cut;if(e.writtenFVS!==null&&t.style.getPropertyValue("font-variation-settings")!==e.writtenFVS&&(e.origFVS=S(t,"font-variation-settings"),e.baseFVS=I(t),e.writtenFVS=null),t.style.setProperty("font-family",i.family,e.origFamily.priority),typeof i.opszValue=="number"&&Number.isFinite(i.opszValue)){const r=Number.isFinite(i.opszMin)?i.opszMin:-1/0,s=Number.isFinite(i.opszMax)?i.opszMax:1/0,u=r<=s?Math.min(s,Math.max(r,i.opszValue)):i.opszValue,f=L(e.baseFVS,u);t.style.setProperty("font-variation-settings",f,e.origFVS.priority),e.writtenFVS=t.style.getPropertyValue("font-variation-settings")}else e.writtenFVS!==null&&(v(t,"font-variation-settings",e.origFVS),e.writtenFVS=null);e.activeIndex=n,e.writtenStyleAttr=t.getAttribute("style")}function E(t){const e=a.get(t);e&&(e.writtenStyleAttr!==null&&t.getAttribute("style")===e.writtenStyleAttr?e.origStyleAttr===null?t.removeAttribute("style"):t.setAttribute("style",e.origStyleAttr):(v(t,"font-family",e.origFamily),e.writtenFVS!==null&&v(t,"font-variation-settings",e.origFVS),e.origStyleAttr===null&&!t.getAttribute("style")&&t.removeAttribute("style")),a.delete(t))}const p=new Set;let l=null,O=null;const c=new Map;let y=null,b=!1;function T(){var e;b=!1;const t=[];p.forEach(n=>{const i=a.get(n);if(!i||!n.isConnected)return;const r=parseFloat(getComputedStyle(n).fontSize);if(!Number.isFinite(r))return;const s=R(i.cuts,r,i.activeIndex,i.hysteresis);s!==-1&&s!==i.activeIndex&&t.push([n,i,s])});for(const[n,i,r]of t)F(n,i,r),(e=i.onCutChange)==null||e.call(i,i.cuts[r].cut)}function d(){b||(b=!0,queueMicrotask(T))}function k(){typeof MutationObserver<"u"&&!y&&document.documentElement&&(y=new MutationObserver(d),y.observe(document.documentElement,{attributes:!0,attributeFilter:["class","style","lang","dir"],subtree:!0}),window.addEventListener("resize",d))}function W(){p.size||(l==null||l.disconnect(),l=null,c.clear(),y==null||y.disconnect(),y=null,typeof window<"u"&&window.removeEventListener("resize",d))}function j(t,e){var s;if(typeof window>"u"||!t||!(e!=null&&e.cuts)||e.cuts.length===0)return;const n=parseFloat(getComputedStyle(t).fontSize);if(isNaN(n))return;const i=M(t,e),r=z(i.cuts,n);r!==-1&&r!==i.activeIndex&&(F(t,i,r),(s=i.onCutChange)==null||s.call(i,i.cuts[r].cut))}function q(t,e){var s,u,f;if(typeof window>"u"||!t)return()=>{};if(!(e!=null&&e.cuts)||e.cuts.length===0)return()=>{};(u=(s=a.get(t))==null?void 0:s.stop)==null||u.call(s);const n=M(t,e),i=parseFloat(getComputedStyle(t).fontSize);if(Number.isFinite(i)){const o=z(n.cuts,i);o!==-1&&o!==n.activeIndex&&(F(t,n,o),(f=n.onCutChange)==null||f.call(n,n.cuts[o].cut))}if(k(),p.add(t),n.live=!0,typeof ResizeObserver<"u"&&((!l||O!==ResizeObserver)&&(l=new ResizeObserver(d),O=ResizeObserver,c.clear()),n.ro=l,n.ro.observe(t),n.observedParent=t.parentElement,n.observedParent)){const o=c.get(n.observedParent)??0;o===0&&n.ro.observe(n.observedParent),c.set(n.observedParent,o+1)}const r=()=>{var w,h,V,x,C;if(((w=a.get(t))==null?void 0:w.stop)!==r)return;p.delete(t);const o=a.get(t);if(o!=null&&o.ro&&o.ro===l){(V=(h=o.ro).unobserve)==null||V.call(h,t);const m=o.observedParent;if(m){const P=(c.get(m)??1)-1;P<=0?(c.delete(m),(C=(x=o.ro).unobserve)==null||C.call(x,m)):c.set(m,P)}}E(t),W()};return n.stop=r,r}function D(t){const e=a.get(t);e&&(e.stop?e.stop():E(t))}exports.applyOpszStepper=j;exports.removeOpszStepper=D;exports.startOpszStepper=q;
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot application of the correct optical cut for the element's current computed font-size.
|
|
3
|
+
* No watching — use when you want manual control. Hysteresis is not applied here.
|
|
4
|
+
* onCutChange fires only when the cut actually changes.
|
|
5
|
+
*
|
|
6
|
+
* @param el - Target element
|
|
7
|
+
* @param options - OpszStepperOptions
|
|
8
|
+
*/
|
|
9
|
+
export declare function applyOpszStepper(el: HTMLElement, options: OpszStepperOptions): void;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A single optical size cut definition: a font-family string and the font-size
|
|
13
|
+
* range in px over which it should be active.
|
|
14
|
+
*
|
|
15
|
+
* Two usage modes:
|
|
16
|
+
* 1. Multi-family hot-swap (e.g. Halyard Micro / Text / Display):
|
|
17
|
+
* set `family` to the CSS font-family string for each cut. Leave `opszValue` unset.
|
|
18
|
+
* 2. Single variable-font opsz axis (e.g. Recursive, Fraunces, Amstelvar):
|
|
19
|
+
* set `family` to the single variable font family, and set `opszValue` to the
|
|
20
|
+
* `opsz` axis value to apply at this cut. The tool will write
|
|
21
|
+
* `font-variation-settings: "opsz" <value>` on the element.
|
|
22
|
+
* Optionally provide `opszMin` and `opszMax` to clamp the value against the
|
|
23
|
+
* font's fvar axis range.
|
|
24
|
+
*/
|
|
25
|
+
export declare interface OpszStepperCut {
|
|
26
|
+
/**
|
|
27
|
+
* The CSS font-family string for this optical cut.
|
|
28
|
+
* e.g. 'Halyard Display, sans-serif' or '"Tiempos Display", serif'
|
|
29
|
+
* For variable-font opsz mode, use the same family string in all cuts.
|
|
30
|
+
*/
|
|
31
|
+
family: string;
|
|
32
|
+
/** Min font-size in px (inclusive) for this cut to apply. Default: 0 */
|
|
33
|
+
minSize?: number;
|
|
34
|
+
/** Max font-size in px (exclusive) for this cut to apply. Default: Infinity */
|
|
35
|
+
maxSize?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Optional `opsz` axis value to write as `font-variation-settings: "opsz" <value>`.
|
|
38
|
+
* Use this when all cuts share one variable font and you want to drive the opsz axis
|
|
39
|
+
* directly rather than hot-swapping font families.
|
|
40
|
+
* When provided, `font-variation-settings: normal` is written first to clear any
|
|
41
|
+
* inherited axis values before applying the new `opsz` value.
|
|
42
|
+
* Clamped between `opszMin` and `opszMax` when those are supplied.
|
|
43
|
+
*/
|
|
44
|
+
opszValue?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Minimum allowed value for the `opsz` axis (from the font's fvar table).
|
|
47
|
+
* Only used when `opszValue` is set. Clamps the written value from below.
|
|
48
|
+
*/
|
|
49
|
+
opszMin?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Maximum allowed value for the `opsz` axis (from the font's fvar table).
|
|
52
|
+
* Only used when `opszValue` is set. Clamps the written value from above.
|
|
53
|
+
*/
|
|
54
|
+
opszMax?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Options controlling the opszStepper effect */
|
|
58
|
+
export declare interface OpszStepperOptions {
|
|
59
|
+
/**
|
|
60
|
+
* The optical size cuts, ordered from smallest to largest.
|
|
61
|
+
* Each cut defines a font-family string and the font-size range it applies to.
|
|
62
|
+
* Ranges should be contiguous and non-overlapping.
|
|
63
|
+
*
|
|
64
|
+
* @example Multi-family hot-swap
|
|
65
|
+
* cuts: [
|
|
66
|
+
* { family: 'Halyard Micro, sans-serif', maxSize: 13 },
|
|
67
|
+
* { family: 'Halyard Text, sans-serif', minSize: 13, maxSize: 28 },
|
|
68
|
+
* { family: 'Halyard Display, sans-serif', minSize: 28 },
|
|
69
|
+
* ]
|
|
70
|
+
*
|
|
71
|
+
* @example Single variable font opsz axis
|
|
72
|
+
* cuts: [
|
|
73
|
+
* { family: 'Fraunces, serif', maxSize: 13, opszValue: 9, opszMin: 9, opszMax: 144 },
|
|
74
|
+
* { family: 'Fraunces, serif', minSize: 13, maxSize: 28, opszValue: 24, opszMin: 9, opszMax: 144 },
|
|
75
|
+
* { family: 'Fraunces, serif', minSize: 28, opszValue: 72, opszMin: 9, opszMax: 144 },
|
|
76
|
+
* ]
|
|
77
|
+
*/
|
|
78
|
+
cuts: OpszStepperCut[];
|
|
79
|
+
/**
|
|
80
|
+
* Hysteresis dead zone in px per threshold. Prevents oscillation when
|
|
81
|
+
* font-size sits exactly at a cut boundary. Only used by startOpszStepper;
|
|
82
|
+
* applyOpszStepper always does a direct cut lookup without hysteresis.
|
|
83
|
+
* Default: 1
|
|
84
|
+
*/
|
|
85
|
+
hysteresis?: number;
|
|
86
|
+
/**
|
|
87
|
+
* Callback fired each time the active cut changes.
|
|
88
|
+
* Receives the new cut that was applied.
|
|
89
|
+
*/
|
|
90
|
+
onCutChange?: (cut: OpszStepperCut) => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A stop function returned by startOpszStepper. Call it to disconnect the observer and restore the element. */
|
|
94
|
+
export declare type OpszStepperStop = () => void;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Restore the element's original styles and stop watching it. No-op if never applied.
|
|
98
|
+
*
|
|
99
|
+
* @param el - Element previously passed to startOpszStepper or applyOpszStepper
|
|
100
|
+
*/
|
|
101
|
+
export declare function removeOpszStepper(el: HTMLElement): void;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Start watching an element: applies the correct cut now and again whenever its font-size
|
|
105
|
+
* changes — from a resize, a class or style change anywhere on the page, or a viewport change.
|
|
106
|
+
* Calling it again on the same element replaces the earlier watcher.
|
|
107
|
+
*
|
|
108
|
+
* @param el - Target element
|
|
109
|
+
* @param options - OpszStepperOptions
|
|
110
|
+
* @returns A stop function that stops watching and restores the element's original styles
|
|
111
|
+
*/
|
|
112
|
+
export declare function startOpszStepper(el: HTMLElement, options: OpszStepperOptions): OpszStepperStop;
|
|
113
|
+
|
|
114
|
+
export { }
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
function O(t) {
|
|
2
|
+
const e = t.filter((s) => s && typeof s.family == "string"), n = (s) => Number.isFinite(s.minSize) ? s.minSize : Number.isFinite(s.maxSize) ? s.maxSize - 1e-9 : 1 / 0, i = [...e].sort((s, u) => n(s) - n(u)), r = i.map((s, u) => {
|
|
3
|
+
const f = i[u - 1];
|
|
4
|
+
return { min: Number.isFinite(s.minSize) ? s.minSize : f && Number.isFinite(f.maxSize) ? f.maxSize : 0, max: Number.isFinite(s.maxSize) ? s.maxSize : 1 / 0, cut: s };
|
|
5
|
+
});
|
|
6
|
+
for (let s = 0; s < r.length - 1; s++)
|
|
7
|
+
r[s].max === 1 / 0 && (r[s].max = r[s + 1].min);
|
|
8
|
+
return r;
|
|
9
|
+
}
|
|
10
|
+
function F(t, e) {
|
|
11
|
+
for (let n = 0; n < t.length; n++)
|
|
12
|
+
if (e >= t[n].min && e < t[n].max) return n;
|
|
13
|
+
return -1;
|
|
14
|
+
}
|
|
15
|
+
function R(t, e, n, i) {
|
|
16
|
+
const r = F(t, e);
|
|
17
|
+
if (n === -1 || !t[n]) return r;
|
|
18
|
+
if (r === -1 || r === n) return n;
|
|
19
|
+
const s = t[n];
|
|
20
|
+
return e >= s.max + i || e < s.min - i ? r : n;
|
|
21
|
+
}
|
|
22
|
+
const A = /* @__PURE__ */ new Set();
|
|
23
|
+
function E(t) {
|
|
24
|
+
A.has(t) || (A.add(t), console.warn(t));
|
|
25
|
+
}
|
|
26
|
+
function T(t, e) {
|
|
27
|
+
let n = t ?? 1;
|
|
28
|
+
(!Number.isFinite(n) || n < 0) && (E(`[opszStepper] hysteresis must be a non-negative number; got ${t}, using 1`), n = 1);
|
|
29
|
+
const i = Math.min(...e.map((r) => r.max - r.min).filter((r) => Number.isFinite(r) && r > 0));
|
|
30
|
+
return Number.isFinite(i) && n > i / 2 && (E(`[opszStepper] hysteresis ${n}px is more than half the narrowest cut (${i}px); using ${i / 2}px`), n = i / 2), n;
|
|
31
|
+
}
|
|
32
|
+
const a = /* @__PURE__ */ new Map();
|
|
33
|
+
function d(t, e) {
|
|
34
|
+
return { value: t.style.getPropertyValue(e), priority: t.style.getPropertyPriority(e) };
|
|
35
|
+
}
|
|
36
|
+
function g(t, e, n) {
|
|
37
|
+
n.value ? t.style.setProperty(e, n.value, n.priority) : t.style.removeProperty(e);
|
|
38
|
+
}
|
|
39
|
+
function I(t, e) {
|
|
40
|
+
const n = O(e.cuts), i = T(e.hysteresis, n);
|
|
41
|
+
let r = a.get(t);
|
|
42
|
+
return r ? (r.cuts = n, r.hysteresis = i, r.onCutChange = e.onCutChange) : (r = {
|
|
43
|
+
cuts: n,
|
|
44
|
+
hysteresis: i,
|
|
45
|
+
onCutChange: e.onCutChange,
|
|
46
|
+
activeIndex: -1,
|
|
47
|
+
origFamily: d(t, "font-family"),
|
|
48
|
+
origFVS: d(t, "font-variation-settings"),
|
|
49
|
+
origStyleAttr: t.getAttribute("style"),
|
|
50
|
+
baseFVS: N(t),
|
|
51
|
+
writtenFVS: null,
|
|
52
|
+
writtenStyleAttr: null,
|
|
53
|
+
live: !1
|
|
54
|
+
}, a.set(t, r)), r;
|
|
55
|
+
}
|
|
56
|
+
function N(t) {
|
|
57
|
+
const e = getComputedStyle(t);
|
|
58
|
+
return (typeof e.getPropertyValue == "function" ? e.getPropertyValue("font-variation-settings") : e.fontVariationSettings) || "normal";
|
|
59
|
+
}
|
|
60
|
+
function $(t, e) {
|
|
61
|
+
const n = `"opsz" ${e}`;
|
|
62
|
+
if (!t || t === "normal") return n;
|
|
63
|
+
const i = /(["'])opsz\1\s+-?[\d.eE+-]+/;
|
|
64
|
+
return i.test(t) ? t.replace(i, n) : `${t}, ${n}`;
|
|
65
|
+
}
|
|
66
|
+
function b(t, e, n) {
|
|
67
|
+
const i = e.cuts[n].cut;
|
|
68
|
+
if (e.writtenFVS !== null && t.style.getPropertyValue("font-variation-settings") !== e.writtenFVS && (e.origFVS = d(t, "font-variation-settings"), e.baseFVS = N(t), e.writtenFVS = null), t.style.setProperty("font-family", i.family, e.origFamily.priority), typeof i.opszValue == "number" && Number.isFinite(i.opszValue)) {
|
|
69
|
+
const r = Number.isFinite(i.opszMin) ? i.opszMin : -1 / 0, s = Number.isFinite(i.opszMax) ? i.opszMax : 1 / 0, u = r <= s ? Math.min(s, Math.max(r, i.opszValue)) : i.opszValue, f = $(e.baseFVS, u);
|
|
70
|
+
t.style.setProperty("font-variation-settings", f, e.origFVS.priority), e.writtenFVS = t.style.getPropertyValue("font-variation-settings");
|
|
71
|
+
} else e.writtenFVS !== null && (g(t, "font-variation-settings", e.origFVS), e.writtenFVS = null);
|
|
72
|
+
e.activeIndex = n, e.writtenStyleAttr = t.getAttribute("style");
|
|
73
|
+
}
|
|
74
|
+
function M(t) {
|
|
75
|
+
const e = a.get(t);
|
|
76
|
+
e && (e.writtenStyleAttr !== null && t.getAttribute("style") === e.writtenStyleAttr ? e.origStyleAttr === null ? t.removeAttribute("style") : t.setAttribute("style", e.origStyleAttr) : (g(t, "font-family", e.origFamily), e.writtenFVS !== null && g(t, "font-variation-settings", e.origFVS), e.origStyleAttr === null && !t.getAttribute("style") && t.removeAttribute("style")), a.delete(t));
|
|
77
|
+
}
|
|
78
|
+
const S = /* @__PURE__ */ new Set();
|
|
79
|
+
let l = null, P = null;
|
|
80
|
+
const c = /* @__PURE__ */ new Map();
|
|
81
|
+
let y = null, v = !1;
|
|
82
|
+
function H() {
|
|
83
|
+
var e;
|
|
84
|
+
v = !1;
|
|
85
|
+
const t = [];
|
|
86
|
+
S.forEach((n) => {
|
|
87
|
+
const i = a.get(n);
|
|
88
|
+
if (!i || !n.isConnected) return;
|
|
89
|
+
const r = parseFloat(getComputedStyle(n).fontSize);
|
|
90
|
+
if (!Number.isFinite(r)) return;
|
|
91
|
+
const s = R(i.cuts, r, i.activeIndex, i.hysteresis);
|
|
92
|
+
s !== -1 && s !== i.activeIndex && t.push([n, i, s]);
|
|
93
|
+
});
|
|
94
|
+
for (const [n, i, r] of t)
|
|
95
|
+
b(n, i, r), (e = i.onCutChange) == null || e.call(i, i.cuts[r].cut);
|
|
96
|
+
}
|
|
97
|
+
function p() {
|
|
98
|
+
v || (v = !0, queueMicrotask(H));
|
|
99
|
+
}
|
|
100
|
+
function L() {
|
|
101
|
+
typeof MutationObserver < "u" && !y && document.documentElement && (y = new MutationObserver(p), y.observe(document.documentElement, { attributes: !0, attributeFilter: ["class", "style", "lang", "dir"], subtree: !0 }), window.addEventListener("resize", p));
|
|
102
|
+
}
|
|
103
|
+
function D() {
|
|
104
|
+
S.size || (l == null || l.disconnect(), l = null, c.clear(), y == null || y.disconnect(), y = null, typeof window < "u" && window.removeEventListener("resize", p));
|
|
105
|
+
}
|
|
106
|
+
function U(t, e) {
|
|
107
|
+
var s;
|
|
108
|
+
if (typeof window > "u" || !t || !(e != null && e.cuts) || e.cuts.length === 0) return;
|
|
109
|
+
const n = parseFloat(getComputedStyle(t).fontSize);
|
|
110
|
+
if (isNaN(n)) return;
|
|
111
|
+
const i = I(t, e), r = F(i.cuts, n);
|
|
112
|
+
r !== -1 && r !== i.activeIndex && (b(t, i, r), (s = i.onCutChange) == null || s.call(i, i.cuts[r].cut));
|
|
113
|
+
}
|
|
114
|
+
function Y(t, e) {
|
|
115
|
+
var s, u, f;
|
|
116
|
+
if (typeof window > "u" || !t) return () => {
|
|
117
|
+
};
|
|
118
|
+
if (!(e != null && e.cuts) || e.cuts.length === 0) return () => {
|
|
119
|
+
};
|
|
120
|
+
(u = (s = a.get(t)) == null ? void 0 : s.stop) == null || u.call(s);
|
|
121
|
+
const n = I(t, e), i = parseFloat(getComputedStyle(t).fontSize);
|
|
122
|
+
if (Number.isFinite(i)) {
|
|
123
|
+
const o = F(n.cuts, i);
|
|
124
|
+
o !== -1 && o !== n.activeIndex && (b(t, n, o), (f = n.onCutChange) == null || f.call(n, n.cuts[o].cut));
|
|
125
|
+
}
|
|
126
|
+
if (L(), S.add(t), n.live = !0, typeof ResizeObserver < "u" && ((!l || P !== ResizeObserver) && (l = new ResizeObserver(p), P = ResizeObserver, c.clear()), n.ro = l, n.ro.observe(t), n.observedParent = t.parentElement, n.observedParent)) {
|
|
127
|
+
const o = c.get(n.observedParent) ?? 0;
|
|
128
|
+
o === 0 && n.ro.observe(n.observedParent), c.set(n.observedParent, o + 1);
|
|
129
|
+
}
|
|
130
|
+
const r = () => {
|
|
131
|
+
var w, z, h, x, V;
|
|
132
|
+
if (((w = a.get(t)) == null ? void 0 : w.stop) !== r) return;
|
|
133
|
+
S.delete(t);
|
|
134
|
+
const o = a.get(t);
|
|
135
|
+
if (o != null && o.ro && o.ro === l) {
|
|
136
|
+
(h = (z = o.ro).unobserve) == null || h.call(z, t);
|
|
137
|
+
const m = o.observedParent;
|
|
138
|
+
if (m) {
|
|
139
|
+
const C = (c.get(m) ?? 1) - 1;
|
|
140
|
+
C <= 0 ? (c.delete(m), (V = (x = o.ro).unobserve) == null || V.call(x, m)) : c.set(m, C);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
M(t), D();
|
|
144
|
+
};
|
|
145
|
+
return n.stop = r, r;
|
|
146
|
+
}
|
|
147
|
+
function _(t) {
|
|
148
|
+
const e = a.get(t);
|
|
149
|
+
e && (e.stop ? e.stop() : M(t));
|
|
150
|
+
}
|
|
151
|
+
export {
|
|
152
|
+
U as applyOpszStepper,
|
|
153
|
+
_ as removeOpszStepper,
|
|
154
|
+
Y as startOpszStepper
|
|
155
|
+
};
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("./core.cjs"),r=require("react"),g=require("react/jsx-runtime");function l(s){const[n,o]=r.useState(null),i=r.useMemo(()=>{let e=null;return{get current(){return e},set current(t){t!==e&&(e=t,o(t))}}},[]),p=r.useRef(s);p.current=s;const a=JSON.stringify(s.cuts),{hysteresis:f}=s;return r.useLayoutEffect(()=>n?c.startOpszStepper(n,{...p.current,onCutChange:t=>{var S,u;return(u=(S=p.current).onCutChange)==null?void 0:u.call(S,t)}}):void 0,[n,a,f]),i}const z=r.forwardRef(function({children:n,as:o="p",cuts:i,hysteresis:p,onCutChange:a,...f},e){const u=l({cuts:i,hysteresis:p,onCutChange:a}),y=r.useCallback(O=>{u.current=O,typeof e=="function"?e(O):e&&(e.current=O)},[e]);return g.jsx(o,{ref:y,...f,children:n})});z.displayName="OpszStepperText";exports.applyOpszStepper=c.applyOpszStepper;exports.removeOpszStepper=c.removeOpszStepper;exports.startOpszStepper=c.startOpszStepper;exports.OpszStepperText=z;exports.useOpszStepper=l;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { default as default_2 } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { MutableRefObject } from 'react';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* One-shot application of the correct optical cut for the element's current
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* One-shot application of the correct optical cut for the element's current computed font-size.
|
|
6
|
+
* No watching — use when you want manual control. Hysteresis is not applied here.
|
|
7
|
+
* onCutChange fires only when the cut actually changes.
|
|
8
8
|
*
|
|
9
9
|
* @param el - Target element
|
|
10
10
|
* @param options - OpszStepperOptions
|
|
@@ -115,25 +115,20 @@ declare interface OpszStepperTextProps extends OpszStepperOptions, default_2.HTM
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
* Restore the element's original
|
|
119
|
-
* ResizeObserver started by startOpszStepper. No-op if never applied.
|
|
118
|
+
* Restore the element's original styles and stop watching it. No-op if never applied.
|
|
120
119
|
*
|
|
121
120
|
* @param el - Element previously passed to startOpszStepper or applyOpszStepper
|
|
122
121
|
*/
|
|
123
122
|
export declare function removeOpszStepper(el: HTMLElement): void;
|
|
124
123
|
|
|
125
124
|
/**
|
|
126
|
-
* Start
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* Applies the correct cut immediately on first call.
|
|
131
|
-
* If called a second time on the same element, the prior observer is stopped
|
|
132
|
-
* first to avoid orphaned observers.
|
|
125
|
+
* Start watching an element: applies the correct cut now and again whenever its font-size
|
|
126
|
+
* changes — from a resize, a class or style change anywhere on the page, or a viewport change.
|
|
127
|
+
* Calling it again on the same element replaces the earlier watcher.
|
|
133
128
|
*
|
|
134
129
|
* @param el - Target element
|
|
135
130
|
* @param options - OpszStepperOptions
|
|
136
|
-
* @returns A stop function that
|
|
131
|
+
* @returns A stop function that stops watching and restores the element's original styles
|
|
137
132
|
*/
|
|
138
133
|
export declare function startOpszStepper(el: HTMLElement, options: OpszStepperOptions): OpszStepperStop;
|
|
139
134
|
|
|
@@ -146,15 +141,14 @@ export declare function startOpszStepper(el: HTMLElement, options: OpszStepperOp
|
|
|
146
141
|
* A stable JSON serialisation of the cuts array is used as the dependency key so
|
|
147
142
|
* that same-length arrays with different content also trigger a restart.
|
|
148
143
|
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* stable across renders.
|
|
144
|
+
* onCutChange is read from the latest render each time it fires, so it doesn't need to be
|
|
145
|
+
* stable across renders. The stepper follows the element if React replaces it.
|
|
152
146
|
*
|
|
153
147
|
* Cleans up on unmount.
|
|
154
148
|
*
|
|
155
149
|
* @param options - OpszStepperOptions
|
|
156
150
|
* @returns A ref to attach to the target element
|
|
157
151
|
*/
|
|
158
|
-
export declare function useOpszStepper(options: OpszStepperOptions):
|
|
152
|
+
export declare function useOpszStepper(options: OpszStepperOptions): MutableRefObject<HTMLElement | null>;
|
|
159
153
|
|
|
160
154
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -1,114 +1,46 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
function g(t, e) {
|
|
27
|
-
const n = window.scrollY;
|
|
28
|
-
if (t.style.fontFamily = e.family, e.opszValue !== void 0) {
|
|
29
|
-
const s = e.opszMin ?? -1 / 0, i = e.opszMax ?? 1 / 0, o = Math.min(i, Math.max(s, e.opszValue));
|
|
30
|
-
t.style.fontVariationSettings = `"opsz" ${o}`;
|
|
31
|
-
} else
|
|
32
|
-
t.style.fontVariationSettings && (t.style.fontVariationSettings = "");
|
|
33
|
-
requestAnimationFrame(() => {
|
|
34
|
-
Math.abs(window.scrollY - n) > 2 && window.scrollTo({ top: n, behavior: "instant" });
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
function v(t, e) {
|
|
38
|
-
if (typeof window > "u") return;
|
|
39
|
-
const { cuts: n, onCutChange: s } = e;
|
|
40
|
-
if (!n || n.length === 0) return;
|
|
41
|
-
const i = parseFloat(getComputedStyle(t).fontSize);
|
|
42
|
-
if (isNaN(i)) return;
|
|
43
|
-
const o = d(n, i);
|
|
44
|
-
if (o === -1) return;
|
|
45
|
-
const a = n[o];
|
|
46
|
-
f.has(t) || f.set(t, t.style.fontFamily), g(t, a), p.set(t, o), s == null || s(a);
|
|
47
|
-
}
|
|
48
|
-
function V(t, e) {
|
|
49
|
-
if (typeof window > "u") return () => {
|
|
50
|
-
};
|
|
51
|
-
const { cuts: n, onCutChange: s } = e, i = e.hysteresis ?? x;
|
|
52
|
-
if (!n || n.length === 0) return () => {
|
|
53
|
-
};
|
|
54
|
-
const o = y.get(t);
|
|
55
|
-
o && o(), f.has(t) || f.set(t, t.style.fontFamily), v(t, e);
|
|
56
|
-
const a = new ResizeObserver(() => {
|
|
57
|
-
const c = parseFloat(getComputedStyle(t).fontSize);
|
|
58
|
-
if (isNaN(c)) return;
|
|
59
|
-
const l = p.get(t) ?? -1, u = O(n, c, l, i);
|
|
60
|
-
u !== l && u !== -1 && (g(t, n[u]), p.set(t, u), s == null || s(n[u]));
|
|
61
|
-
});
|
|
62
|
-
a.observe(t);
|
|
63
|
-
const r = () => {
|
|
64
|
-
a.disconnect();
|
|
65
|
-
const c = f.get(t);
|
|
66
|
-
c !== void 0 && (t.style.fontFamily = c, t.style.fontVariationSettings = "", f.delete(t)), p.delete(t), y.delete(t);
|
|
67
|
-
};
|
|
68
|
-
return y.set(t, r), r;
|
|
69
|
-
}
|
|
70
|
-
function R(t) {
|
|
71
|
-
const e = y.get(t);
|
|
72
|
-
if (e) {
|
|
73
|
-
e();
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
if (f.has(t)) {
|
|
77
|
-
const n = f.get(t);
|
|
78
|
-
t.style.fontFamily = n ?? "", t.style.fontVariationSettings = "", f.delete(t), p.delete(t);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
function b(t) {
|
|
82
|
-
const e = S(null), n = S(t);
|
|
83
|
-
n.current = t;
|
|
84
|
-
const s = S(null), i = JSON.stringify(t.cuts), { hysteresis: o } = t;
|
|
85
|
-
return z(() => {
|
|
86
|
-
var r;
|
|
87
|
-
const a = e.current;
|
|
88
|
-
if (a)
|
|
89
|
-
return (r = s.current) == null || r.call(s), s.current = V(a, n.current), () => {
|
|
90
|
-
var c;
|
|
91
|
-
(c = s.current) == null || c.call(s), s.current = null;
|
|
92
|
-
};
|
|
93
|
-
}, [i, o]), e;
|
|
1
|
+
import { startOpszStepper as l } from "./core.js";
|
|
2
|
+
import { applyOpszStepper as j, removeOpszStepper as k } from "./core.js";
|
|
3
|
+
import { useState as S, useMemo as O, useRef as y, useLayoutEffect as z, forwardRef as x, useCallback as R } from "react";
|
|
4
|
+
import { jsx as g } from "react/jsx-runtime";
|
|
5
|
+
function C(r) {
|
|
6
|
+
const [n, o] = S(null), u = O(() => {
|
|
7
|
+
let e = null;
|
|
8
|
+
return {
|
|
9
|
+
get current() {
|
|
10
|
+
return e;
|
|
11
|
+
},
|
|
12
|
+
set current(t) {
|
|
13
|
+
t !== e && (e = t, o(t));
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}, []), s = y(r);
|
|
17
|
+
s.current = r;
|
|
18
|
+
const c = JSON.stringify(r.cuts), { hysteresis: f } = r;
|
|
19
|
+
return z(() => n ? l(n, {
|
|
20
|
+
...s.current,
|
|
21
|
+
onCutChange: (t) => {
|
|
22
|
+
var i, p;
|
|
23
|
+
return (p = (i = s.current).onCutChange) == null ? void 0 : p.call(i, t);
|
|
24
|
+
}
|
|
25
|
+
}) : void 0, [n, c, f]), u;
|
|
94
26
|
}
|
|
95
|
-
const
|
|
96
|
-
function({ children:
|
|
97
|
-
const
|
|
98
|
-
(
|
|
99
|
-
|
|
27
|
+
const d = x(
|
|
28
|
+
function({ children: n, as: o = "p", cuts: u, hysteresis: s, onCutChange: c, ...f }, e) {
|
|
29
|
+
const p = C({ cuts: u, hysteresis: s, onCutChange: c }), m = R(
|
|
30
|
+
(a) => {
|
|
31
|
+
p.current = a, typeof e == "function" ? e(a) : e && (e.current = a);
|
|
100
32
|
},
|
|
101
33
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
102
|
-
[
|
|
34
|
+
[e]
|
|
103
35
|
);
|
|
104
|
-
return /* @__PURE__ */
|
|
36
|
+
return /* @__PURE__ */ g(o, { ref: m, ...f, children: n });
|
|
105
37
|
}
|
|
106
38
|
);
|
|
107
|
-
|
|
39
|
+
d.displayName = "OpszStepperText";
|
|
108
40
|
export {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
41
|
+
d as OpszStepperText,
|
|
42
|
+
j as applyOpszStepper,
|
|
43
|
+
k as removeOpszStepper,
|
|
44
|
+
l as startOpszStepper,
|
|
45
|
+
C as useOpszStepper
|
|
114
46
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var OpszStepper=(function(
|
|
1
|
+
var OpszStepper=(function(z){"use strict";function R(t){const e=t.filter(o=>o&&typeof o.family=="string"),n=o=>Number.isFinite(o.minSize)?o.minSize:Number.isFinite(o.maxSize)?o.maxSize-1e-9:1/0,s=[...e].sort((o,u)=>n(o)-n(u)),i=s.map((o,u)=>{const c=s[u-1];return{min:Number.isFinite(o.minSize)?o.minSize:c&&Number.isFinite(c.maxSize)?c.maxSize:0,max:Number.isFinite(o.maxSize)?o.maxSize:1/0,cut:o}});for(let o=0;o<i.length-1;o++)i[o].max===1/0&&(i[o].max=i[o+1].min);return i}function C(t,e){for(let n=0;n<t.length;n++)if(e>=t[n].min&&e<t[n].max)return n;return-1}function H(t,e,n,s){const i=C(t,e);if(n===-1||!t[n])return i;if(i===-1||i===n)return n;const o=t[n];return e>=o.max+s||e<o.min-s?i:n}const M=new Set;function O(t){M.has(t)||(M.add(t),console.warn(t))}function L(t,e){let n=t??1;(!Number.isFinite(n)||n<0)&&(O(`[opszStepper] hysteresis must be a non-negative number; got ${t}, using 1`),n=1);const s=Math.min(...e.map(i=>i.max-i.min).filter(i=>Number.isFinite(i)&&i>0));return Number.isFinite(s)&&n>s/2&&(O(`[opszStepper] hysteresis ${n}px is more than half the narrowest cut (${s}px); using ${s/2}px`),n=s/2),n}const l=new Map;function w(t,e){return{value:t.style.getPropertyValue(e),priority:t.style.getPropertyPriority(e)}}function N(t,e,n){n.value?t.style.setProperty(e,n.value,n.priority):t.style.removeProperty(e)}function _(t,e){const n=R(e.cuts),s=L(e.hysteresis,n);let i=l.get(t);return i?(i.cuts=n,i.hysteresis=s,i.onCutChange=e.onCutChange):(i={cuts:n,hysteresis:s,onCutChange:e.onCutChange,activeIndex:-1,origFamily:w(t,"font-family"),origFVS:w(t,"font-variation-settings"),origStyleAttr:t.getAttribute("style"),baseFVS:A(t),writtenFVS:null,writtenStyleAttr:null,live:!1},l.set(t,i)),i}function A(t){const e=getComputedStyle(t);return(typeof e.getPropertyValue=="function"?e.getPropertyValue("font-variation-settings"):e.fontVariationSettings)||"normal"}function D(t,e){const n=`"opsz" ${e}`;if(!t||t==="normal")return n;const s=/(["'])opsz\1\s+-?[\d.eE+-]+/;return s.test(t)?t.replace(s,n):`${t}, ${n}`}function E(t,e,n){const s=e.cuts[n].cut;if(e.writtenFVS!==null&&t.style.getPropertyValue("font-variation-settings")!==e.writtenFVS&&(e.origFVS=w(t,"font-variation-settings"),e.baseFVS=A(t),e.writtenFVS=null),t.style.setProperty("font-family",s.family,e.origFamily.priority),typeof s.opszValue=="number"&&Number.isFinite(s.opszValue)){const i=Number.isFinite(s.opszMin)?s.opszMin:-1/0,o=Number.isFinite(s.opszMax)?s.opszMax:1/0,u=i<=o?Math.min(o,Math.max(i,s.opszValue)):s.opszValue,c=D(e.baseFVS,u);t.style.setProperty("font-variation-settings",c,e.origFVS.priority),e.writtenFVS=t.style.getPropertyValue("font-variation-settings")}else e.writtenFVS!==null&&(N(t,"font-variation-settings",e.origFVS),e.writtenFVS=null);e.activeIndex=n,e.writtenStyleAttr=t.getAttribute("style")}function P(t){const e=l.get(t);e&&(e.writtenStyleAttr!==null&&t.getAttribute("style")===e.writtenStyleAttr?e.origStyleAttr===null?t.removeAttribute("style"):t.setAttribute("style",e.origStyleAttr):(N(t,"font-family",e.origFamily),e.writtenFVS!==null&&N(t,"font-variation-settings",e.origFVS),e.origStyleAttr===null&&!t.getAttribute("style")&&t.removeAttribute("style")),l.delete(t))}const b=new Set;let p=null,I=null;const y=new Map;let S=null,x=!1;function J(){var e;x=!1;const t=[];b.forEach(n=>{const s=l.get(n);if(!s||!n.isConnected)return;const i=parseFloat(getComputedStyle(n).fontSize);if(!Number.isFinite(i))return;const o=H(s.cuts,i,s.activeIndex,s.hysteresis);o!==-1&&o!==s.activeIndex&&t.push([n,s,o])});for(const[n,s,i]of t)E(n,s,i),(e=s.onCutChange)==null||e.call(s,s.cuts[i].cut)}function h(){x||(x=!0,queueMicrotask(J))}function U(){typeof MutationObserver<"u"&&!S&&document.documentElement&&(S=new MutationObserver(h),S.observe(document.documentElement,{attributes:!0,attributeFilter:["class","style","lang","dir"],subtree:!0}),window.addEventListener("resize",h))}function Y(){b.size||(p==null||p.disconnect(),p=null,y.clear(),S==null||S.disconnect(),S=null,typeof window<"u"&&window.removeEventListener("resize",h))}function k(t,e){var o,u,c;if(typeof window>"u"||!t)return()=>{};if(!(e!=null&&e.cuts)||e.cuts.length===0)return()=>{};(u=(o=l.get(t))==null?void 0:o.stop)==null||u.call(o);const n=_(t,e),s=parseFloat(getComputedStyle(t).fontSize);if(Number.isFinite(s)){const r=C(n.cuts,s);r!==-1&&r!==n.activeIndex&&(E(t,n,r),(c=n.onCutChange)==null||c.call(n,n.cuts[r].cut))}if(U(),b.add(t),n.live=!0,typeof ResizeObserver<"u"&&((!p||I!==ResizeObserver)&&(p=new ResizeObserver(h),I=ResizeObserver,y.clear()),n.ro=p,n.ro.observe(t),n.observedParent=t.parentElement,n.observedParent)){const r=y.get(n.observedParent)??0;r===0&&n.ro.observe(n.observedParent),y.set(n.observedParent,r+1)}const i=()=>{var m,v,g,a,d;if(((m=l.get(t))==null?void 0:m.stop)!==i)return;b.delete(t);const r=l.get(t);if(r!=null&&r.ro&&r.ro===p){(g=(v=r.ro).unobserve)==null||g.call(v,t);const f=r.observedParent;if(f){const $=(y.get(f)??1)-1;$<=0?(y.delete(f),(d=(a=r.ro).unobserve)==null||d.call(a,f)):y.set(f,$)}}P(t),Y()};return n.stop=i,i}function W(t){const e=l.get(t);e&&(e.stop?e.stop():P(t))}const j="data-opszstepper",V=new WeakMap;function q(t,e,n,s){const i=[];for(const o of t.split(",")){const u=o.trim();if(!u)continue;const[c,r]=u.split(":");if(r===void 0)continue;const m=parseFloat(r.trim());if(isNaN(m))continue;const[v,g]=c.replace(/[\u2013\u2014]/g,"-").split("-"),a=parseFloat((v??"").trim()),d=parseFloat((g??"").trim()),f={family:e,opszValue:m};isNaN(a)||(f.minSize=a),isNaN(d)||(f.maxSize=d),n!==void 0&&(f.opszMin=n),s!==void 0&&(f.opszMax=s),i.push(f)}return i}function Q(t){if(!Array.isArray(t))return[];const e=[];for(const n of t){if(!n||typeof n!="object")continue;const s=n;if(typeof s.family!="string")continue;const i={family:s.family},o=g=>{const a=s[g];if(a==null)return;const d=typeof a=="number"?a:typeof a=="string"&&a.trim()!==""?Number(a):NaN;if(Number.isFinite(d))return d;console.warn(`OpszStepper: "${g}" in data-os-cuts must be a number; got ${JSON.stringify(a)} — ignoring it.`)},u=o("minSize");u!==void 0&&(i.minSize=u);const c=o("maxSize");c!==void 0&&(i.maxSize=c);const r=o("opszValue");r!==void 0&&(i.opszValue=r);const m=o("opszMin");m!==void 0&&(i.opszMin=m);const v=o("opszMax");v!==void 0&&(i.opszMax=v),e.push(i)}return e}function B(t){const e=t.dataset;let n=[];if(e.osCuts)try{n=Q(JSON.parse(e.osCuts))}catch{console.error("OpszStepper: data-os-cuts is not valid JSON — ignoring.")}if(n.length===0&&e.osFamily&&e.osOpsz){const i=e.osOpszMin!==void 0?parseFloat(e.osOpszMin):void 0,o=e.osOpszMax!==void 0?parseFloat(e.osOpszMax):void 0;n=q(e.osOpsz,e.osFamily,i!==void 0&&!isNaN(i)?i:void 0,o!==void 0&&!isNaN(o)?o:void 0)}const s={cuts:n};if(e.osHysteresis!==void 0){const i=parseFloat(e.osHysteresis);isNaN(i)||(s.hysteresis=i)}return s}function G(t){T(t);const e=B(t);if(e.cuts.length===0){console.warn("OpszStepper: no valid cuts found on element — skipping.");return}const n=k(t,e);V.set(t,{stop:n})}function T(t){const e=V.get(t);if(!e){W(t);return}e.stop(),V.delete(t)}function F(t=document){t.querySelectorAll(`[${j}]`).forEach(G)}function K(t=document){F(t)}function X(){const t=()=>{var e;(e=document.fonts)!=null&&e.ready?document.fonts.ready.then(()=>F()).catch(()=>F()):F()};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t,{once:!0}):t()}return X(),z.destroy=T,z.init=F,z.restart=K,Object.defineProperty(z,Symbol.toStringTag,{value:"Module"}),z})({});
|
package/package.json
CHANGED
|
@@ -1,84 +1,89 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@overpunch/opszstepper",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Multi-cut optical family hot-swap by font-size — automatically applies Micro, Text, or Display cuts based on the element's computed font-size",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.cjs",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": {
|
|
9
|
-
"types": "./dist/index.d.ts",
|
|
10
|
-
"import": "./dist/index.js",
|
|
11
|
-
"require": "./dist/index.cjs"
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@overpunch/opszstepper",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Multi-cut optical family hot-swap by font-size — automatically applies Micro, Text, or Display cuts based on the element's computed font-size",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
},
|
|
13
|
+
"./core": {
|
|
14
|
+
"types": "./dist/core.d.ts",
|
|
15
|
+
"import": "./dist/core.js",
|
|
16
|
+
"require": "./dist/core.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "vite build",
|
|
24
|
+
"build:webflow": "vite build --config vite.webflow.config.ts",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:run": "vitest run",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"prepublishOnly": "npm run test && npm run build && npm run build:webflow"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"react": ">=17",
|
|
32
|
+
"react-dom": ">=17"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"react": {
|
|
36
|
+
"optional": true
|
|
37
|
+
},
|
|
38
|
+
"react-dom": {
|
|
39
|
+
"optional": true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@testing-library/react": "^16.3.2",
|
|
44
|
+
"@testing-library/user-event": "^14.6.1",
|
|
45
|
+
"@types/react": "^19.0.0",
|
|
46
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
47
|
+
"happy-dom": "^20.10.6",
|
|
48
|
+
"next": "16.2.2",
|
|
49
|
+
"react": "^19.0.0",
|
|
50
|
+
"typescript": "^5.0.0",
|
|
51
|
+
"vite": "^6.0.0",
|
|
52
|
+
"vite-plugin-dts": "^4.0.0",
|
|
53
|
+
"vitest": "^3.0.0"
|
|
54
|
+
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"typography",
|
|
57
|
+
"web-typography",
|
|
58
|
+
"frontend",
|
|
59
|
+
"liiift-studio",
|
|
60
|
+
"optical-size",
|
|
61
|
+
"font-family",
|
|
62
|
+
"opsz",
|
|
63
|
+
"optical-sizing",
|
|
64
|
+
"font-swap",
|
|
65
|
+
"fluid-type",
|
|
66
|
+
"multi-cut",
|
|
67
|
+
"responsive-type",
|
|
68
|
+
"responsive-design",
|
|
69
|
+
"typographic",
|
|
70
|
+
"zero-dependencies",
|
|
71
|
+
"react",
|
|
72
|
+
"typescript",
|
|
73
|
+
"css"
|
|
74
|
+
],
|
|
75
|
+
"author": "Quinn Keaveney <quinn@overpunch.ca>",
|
|
76
|
+
"license": "MIT",
|
|
77
|
+
"homepage": "https://opszstepper.com",
|
|
78
|
+
"repository": {
|
|
79
|
+
"type": "git",
|
|
80
|
+
"url": "git+https://github.com/over-punch/opszStepper.git"
|
|
81
|
+
},
|
|
82
|
+
"bugs": {
|
|
83
|
+
"url": "https://github.com/over-punch/opszStepper/issues"
|
|
84
|
+
},
|
|
85
|
+
"sideEffects": false,
|
|
86
|
+
"publishConfig": {
|
|
87
|
+
"access": "public"
|
|
88
|
+
}
|
|
89
|
+
}
|