@jsenv/navi 0.29.102 → 0.29.104
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/dist/jsenv_navi.js +2003 -1015
- package/dist/jsenv_navi.js.map +215 -132
- package/dist/jsenv_navi_side_effects.js +32 -6
- package/dist/jsenv_navi_side_effects.js.map +2 -2
- package/docs/AI_INSTRUCTIONS.md +25 -4
- package/docs/badge_list.md +80 -0
- package/docs/control_value.md +50 -5
- package/docs/interactions.md +56 -8
- package/docs/typography.md +43 -2
- package/package.json +2 -2
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -123,11 +123,24 @@ consistency across the app, not from any single call site.
|
|
|
123
123
|
inside a `Dialog`/`Popover`, and what a scroll does to hover
|
|
124
124
|
(`hoverWhileScrolling`, `isScrolling()`). Read it before writing CSS to make
|
|
125
125
|
something scroll — navi almost certainly already has the prop.
|
|
126
|
+
- `docs/badge_list.md` — what a `BadgeList` does per prop (plain, `max`,
|
|
127
|
+
`shrinkWrap`, `maxLines`) and how it counts its badges (a `Badge` inside one
|
|
128
|
+
renders nothing and hands itself to the list — so badges must be direct
|
|
129
|
+
children). How `maxLines` reaches a list drawn inside a `Picker`
|
|
130
|
+
(`MaxLinesContext`, the picker's own clamp turned off), and why a list in a
|
|
131
|
+
picker needs a `fallback` — the placeholder text, as plain text: a picker
|
|
132
|
+
given a `ui` draws no placeholder of its own. Read it before putting a
|
|
133
|
+
`BadgeList` in a picker's `ui`, before wrapping its fallback in a `Badge`,
|
|
134
|
+
and before setting `maxLines` on the list rather than on the picker.
|
|
126
135
|
- `docs/control_value.md` — who holds a control's value: nobody, a bound
|
|
127
136
|
`signal` (two-way, in both directions), or you (`value`/`checked`). What
|
|
128
137
|
`signal` + `defaultValue` says, what a signal holds for each kind of control,
|
|
129
|
-
and why `value` and `signal` cannot both be passed.
|
|
130
|
-
|
|
138
|
+
and why `value` and `signal` cannot both be passed. It also holds the answer
|
|
139
|
+
to "a shortcut button beside a control" — `--navi-update`, gated like every
|
|
140
|
+
interaction, against an `onClick` writing the signal, which is not and fires
|
|
141
|
+
on a read-only control. Read it before wiring a
|
|
142
|
+
control's value by hand with `value` + `uiAction`, and before writing a button
|
|
143
|
+
that proposes a value.
|
|
131
144
|
- `docs/create_and_edit.md` — the loop almost every app has: a screen that
|
|
132
145
|
creates a resource, the page of what was created, a screen that edits it. The
|
|
133
146
|
routes and why several match at once, one form for two modes, filling the edit
|
|
@@ -189,7 +202,8 @@ consistency across the app, not from any single call site.
|
|
|
189
202
|
`Text`, `maxLines` as the one way to truncate (and why `lineClamp={1}` is
|
|
190
203
|
not it), what makes truncation actually happen in a flex row, and the two
|
|
191
204
|
opposite shapes of "icon | text | icon" — the end icon kept outside the
|
|
192
|
-
truncating text, or `attachLastChild` so it never lands alone on a line
|
|
205
|
+
truncating text, or `attachLastChild` so it never lands alone on a line —
|
|
206
|
+
and why an emoji belongs only in free text, rendered with `emojiAsIcon`.
|
|
193
207
|
Read it before writing an overflowing label or a row with a trailing icon.
|
|
194
208
|
- `docs/i18n.md` — where the texts an app displays live: `interpolateText` /
|
|
195
209
|
`<Interpolate>` for one sentence, `createI18n` for the app's registry,
|
|
@@ -203,9 +217,16 @@ consistency across the app, not from any single call site.
|
|
|
203
217
|
of navi's own.
|
|
204
218
|
- `docs/interactions.md` — the `interactions` prop: making a component answer a
|
|
205
219
|
swipe, a held press, a shortcut, and registering a gesture navi does not have.
|
|
220
|
+
It also holds `ownTarget`, for an affordance an application draws inside a
|
|
221
|
+
zone that belongs to another control — a chip's cross, an eye, a diskette:
|
|
222
|
+
the three modes and the question that picks one (does it write to the control
|
|
223
|
+
it sits in?), the `data-own-target` attribute for an element you draw
|
|
224
|
+
yourself, and what `ownTarget` does NOT stop (a plain `onClick` on an
|
|
225
|
+
ancestor).
|
|
206
226
|
Read it before reading the pointer by hand — who owns a press between nested
|
|
207
227
|
boxes, and what a touch may do, are decided before the first pixel moves and
|
|
208
|
-
cannot be got right from outside navi
|
|
228
|
+
cannot be got right from outside navi — and before stopping the propagation of
|
|
229
|
+
a pointerdown/mousedown/click to keep a popup from opening.
|
|
209
230
|
- `docs/drag_to_travel.md` — a pointer pushing a whole screen aside
|
|
210
231
|
(`SlideContainer`, `RouteTravel`) and a popup pushed back towards its edge:
|
|
211
232
|
what the gesture is, and above all who owns a press several boxes want — a
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# BadgeList
|
|
2
|
+
|
|
3
|
+
A row of badges that wraps. What it does at runtime depends on what it is asked
|
|
4
|
+
for, and nothing is set up for a case that cannot happen:
|
|
5
|
+
|
|
6
|
+
- **plain** — no `max`, no `maxLines`, no `fallback`, no `shrinkWrap`: one
|
|
7
|
+
element holding its children as-is. No registry, no effect, no measurement.
|
|
8
|
+
- **`max` / `fallback`** — the badges are counted (see below), nothing is
|
|
9
|
+
measured, no DOM is watched.
|
|
10
|
+
- **`shrinkWrap`** — a hidden clone of the list is laid out to find the widest
|
|
11
|
+
wrapped row, and the list is narrowed to it so the last row isn't ragged.
|
|
12
|
+
Opt-in outside a `Picker` (a picker draws a border around the list, so the
|
|
13
|
+
ragged edge shows; elsewhere the work would often go unseen), and skipped
|
|
14
|
+
whenever `maxLines` is in play.
|
|
15
|
+
- **`maxLines`** — every badge is laid out once, hidden; where the rows fell is
|
|
16
|
+
read; the list is rendered again with the badges that fit and a `+N more`
|
|
17
|
+
badge for the rest. Both renders land in the same frame. A width change of
|
|
18
|
+
the room around the list triggers another measure — not a change of the
|
|
19
|
+
list's own width, which is what it produces, not what it is given.
|
|
20
|
+
|
|
21
|
+
## How badges are counted
|
|
22
|
+
|
|
23
|
+
A `Badge` inside a `BadgeList` does not render itself: it hands its props to
|
|
24
|
+
the list through `BadgeListContext` and renders nothing. Badges register in
|
|
25
|
+
tree order, so by the time the list gets to its own content it holds them all
|
|
26
|
+
and knows how many there are before deciding what to show — without walking
|
|
27
|
+
children vnodes, and without rendering a badge it then has to take back.
|
|
28
|
+
|
|
29
|
+
Two consequences:
|
|
30
|
+
|
|
31
|
+
- a `Badge` must be a **direct** child of the list — its rendering is moved
|
|
32
|
+
into the list, so `<div><Badge /></div>` takes the badge out of its wrapper;
|
|
33
|
+
- a badge's own `key` goes to the registering vnode and not to the badge the
|
|
34
|
+
list draws, which is keyed by position. Reordering recreates the nodes rather
|
|
35
|
+
than moving them.
|
|
36
|
+
|
|
37
|
+
## `maxLines` in a Picker
|
|
38
|
+
|
|
39
|
+
A picker clamps its value with `maxLines`, which is CSS line-clamp — it counts
|
|
40
|
+
line boxes of inline text and never sees a wrapped flex row. So a `BadgeList`
|
|
41
|
+
rendered as a picker's `ui` reads the number from `MaxLinesContext` instead
|
|
42
|
+
(the picker provides it, default 1) and caps its own rows to it, and the picker
|
|
43
|
+
turns its own clamp off (`.navi_picker_value:has(.navi_badge_list)`). Nothing
|
|
44
|
+
to set on the list; `maxLines` on the picker is enough, and a `maxLines` on the
|
|
45
|
+
list itself is a local override.
|
|
46
|
+
|
|
47
|
+
`max` is a different cap: a number of badges, whatever the rows. The two
|
|
48
|
+
compose — the `+N` badge takes one of the `max` slots.
|
|
49
|
+
|
|
50
|
+
## The `fallback`
|
|
51
|
+
|
|
52
|
+
Without a `fallback`, an empty list renders **nothing**. Inside a picker there
|
|
53
|
+
is one more thing to know: a picker given a `ui` draws that and only that, its
|
|
54
|
+
own `placeholder` is **not** drawn next to it. An empty `BadgeList` in a picker
|
|
55
|
+
is a blank picker — so the list's `fallback` is the placeholder, and the
|
|
56
|
+
placeholder text is what to pass:
|
|
57
|
+
|
|
58
|
+
```jsx
|
|
59
|
+
<BadgeList fallback="Select skills…">
|
|
60
|
+
{selected.map((skill) => (
|
|
61
|
+
<Badge key={skill}>{skill}</Badge>
|
|
62
|
+
))}
|
|
63
|
+
</BadgeList>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Plain text is the right shape for it. It reads at the picker's own size, in the
|
|
67
|
+
placeholder color — the picker colors its value slot that way while it holds
|
|
68
|
+
nothing — and the picker box stays the same height empty and filled: the box is
|
|
69
|
+
held one line tall by the picker's own line (the right slot is `1lh`), whatever
|
|
70
|
+
the slot inside holds. That slot is a fraction of a pixel taller with a line of
|
|
71
|
+
text than with a row of badges (0.89px at the default font size — a line is set
|
|
72
|
+
by the picker's font and line-height, a badge by its own padding and smaller
|
|
73
|
+
font), which nothing shows.
|
|
74
|
+
|
|
75
|
+
A transparent `Badge` as fallback matches the slot to the pixel instead, but its
|
|
76
|
+
text is badge-sized — smaller than the picker's — and reads as a badge with
|
|
77
|
+
nothing in it. Only worth it where the slot itself is what something else is
|
|
78
|
+
sized on. Section 4 of `src/control/demos/picker/2_select_multiple_demo.html`
|
|
79
|
+
measures the three cases (no fallback, plain text, transparent badge), box and
|
|
80
|
+
slot side by side.
|
package/docs/control_value.md
CHANGED
|
@@ -6,6 +6,7 @@ somewhere else in the app.
|
|
|
6
6
|
|
|
7
7
|
- [The three answers](#the-three-answers)
|
|
8
8
|
- [A bound signal works in both directions](#a-bound-signal-works-in-both-directions)
|
|
9
|
+
- [A button that proposes a value is `--navi-update`](#a-button-that-proposes-a-value-is---navi-update)
|
|
9
10
|
- [`signal` + `defaultValue`: the answer and where it starts](#signal--defaultvalue-the-answer-and-where-it-starts)
|
|
10
11
|
- [What a signal holds, control by control](#what-a-signal-holds-control-by-control)
|
|
11
12
|
- [Empty keeps the shape of the question](#empty-keeps-the-shape-of-the-question)
|
|
@@ -68,20 +69,64 @@ Both halves are worth knowing about, because each replaces a habit:
|
|
|
68
69
|
The follow goes all the way up: a bound control that lives inside a group — two
|
|
69
70
|
wheels in a `WheelGroup`, a field in a `ControlGroup` — makes that group
|
|
70
71
|
re-aggregate when its signal is written, and the form above sees the new value.
|
|
71
|
-
A
|
|
72
|
-
|
|
72
|
+
A value pushed in from anywhere is an answer like any other: the wheels roll,
|
|
73
|
+
and the submit lights up. Which is why a **button** offering such a value is not
|
|
74
|
+
a hand-written signal write — see the next section.
|
|
75
|
+
|
|
76
|
+
## A button that proposes a value is `--navi-update`
|
|
77
|
+
|
|
78
|
+
A shortcut beside a control — "Tous niveaux" / "Aucun niveau" next to a list of
|
|
79
|
+
levels, "1h / 1h30 / 2h" next to a pair of wheels, a suggestion under a field —
|
|
80
|
+
is a value being offered to that control. It is not an action, and it is not a
|
|
81
|
+
signal to write by hand:
|
|
73
82
|
|
|
74
83
|
```jsx
|
|
84
|
+
<ControlGroup id="duration">
|
|
85
|
+
<TimeWheel name="duration" signal={durationSignal} />
|
|
86
|
+
</ControlGroup>
|
|
87
|
+
|
|
88
|
+
<Button
|
|
89
|
+
command="--navi-update"
|
|
90
|
+
commandFor="duration"
|
|
91
|
+
value={{ hours: 1, minutes: 30 }}
|
|
92
|
+
>
|
|
93
|
+
1h30
|
|
94
|
+
</Button>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- the **value** is the button's own `value`, whatever shape it has — a string, an
|
|
98
|
+
array of levels, an object of two wheels;
|
|
99
|
+
- the **target** is `commandFor`, naming the control's id — left out, the nearest
|
|
100
|
+
control around the button is used, which is what a button placed inside the
|
|
101
|
+
control it proposes to wants;
|
|
102
|
+
- and the press goes through the same gate as every other interaction, so a
|
|
103
|
+
read-only, disabled or busy control **refuses it and says why**.
|
|
104
|
+
|
|
105
|
+
That last point is the whole reason, and the counter-example is what everybody
|
|
106
|
+
writes first:
|
|
107
|
+
|
|
108
|
+
```jsx
|
|
109
|
+
// ✗ not gated — plain DOM. On a read-only sheet the button greys out and fires
|
|
110
|
+
// all the same, rewriting a value nobody is allowed to change.
|
|
75
111
|
<Button
|
|
76
112
|
onClick={() => {
|
|
77
|
-
|
|
78
|
-
minutesSignal.value = 0;
|
|
113
|
+
durationSignal.value = { hours: 1, minutes: 30 };
|
|
79
114
|
}}
|
|
80
115
|
>
|
|
81
|
-
|
|
116
|
+
1h30
|
|
82
117
|
</Button>
|
|
83
118
|
```
|
|
84
119
|
|
|
120
|
+
Writing a signal from an `onClick` is only right where nothing is being proposed
|
|
121
|
+
to a control: moving something else on screen, seeding state before anything is
|
|
122
|
+
drawn.
|
|
123
|
+
|
|
124
|
+
The id goes **on the control**, and a group is one — `ControlGroup`, `Form`,
|
|
125
|
+
`WheelGroup`, `List selectable`. Put it on a layout box around the control and
|
|
126
|
+
the command finds an element that holds no value; navi says so in dev rather
|
|
127
|
+
than letting the press do nothing at all. An id that matches nothing is a dev
|
|
128
|
+
warning too, naming the id it looked for.
|
|
129
|
+
|
|
85
130
|
## `signal` + `defaultValue`: the answer and where it starts
|
|
86
131
|
|
|
87
132
|
They are not competing, they answer two different questions:
|
package/docs/interactions.md
CHANGED
|
@@ -531,14 +531,62 @@ sideways), while a `move` goes wherever it is put, a `land` wherever the board h
|
|
|
531
531
|
places and a `toss` wherever it was thrown (`xy`). `data-drag-delay`,
|
|
532
532
|
`data-drag-slop`, `data-drag-threshold` tune when the press becomes a grab.
|
|
533
533
|
|
|
534
|
-
###
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
534
|
+
### An affordance inside somebody else's box: `ownTarget`
|
|
535
|
+
|
|
536
|
+
A chip's cross inside a carried piece, an eye on a row that travels, a diskette
|
|
537
|
+
on a picker's façade. It is aimed AT, not merely inside, and the press belongs
|
|
538
|
+
to it alone:
|
|
539
|
+
|
|
540
|
+
```jsx
|
|
541
|
+
<Badge.Button ownTarget onClick={() => remove(id)}>
|
|
542
|
+
×
|
|
543
|
+
</Badge.Button>
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
No gesture starts under it, no navi control above it answers the mousedown or
|
|
547
|
+
the click, and its `onClick` waits for its own interaction gate instead of
|
|
548
|
+
firing from the DOM.
|
|
549
|
+
|
|
550
|
+
#### Does it write to the control it sits in?
|
|
551
|
+
|
|
552
|
+
That question, and nothing else, picks the mode:
|
|
553
|
+
|
|
554
|
+
| what it does | mode | on a read-only / disabled / busy zone |
|
|
555
|
+
| ------------------------------------------------------------ | -------------------- | ----------------------------------------- |
|
|
556
|
+
| writes to it (a cross that removes, a stepper) | `ownTarget` | it goes |
|
|
557
|
+
| writes to it, and its presence says there is something there | `ownTarget="refuse"` | it stays and refuses with a callout |
|
|
558
|
+
| never touches it (a diskette saving into MY address book) | `ownTarget="always"` | nothing changes: still lit, still pressed |
|
|
559
|
+
|
|
560
|
+
A greyed cross that still removes is worse than no cross — hence the default.
|
|
561
|
+
`"always"` is the other extreme and the caller owns it: the zone's read-only is
|
|
562
|
+
about a value the affordance does not write, so answering "read-only" to a
|
|
563
|
+
gesture that was never going to write anything says nothing true. Use it only
|
|
564
|
+
when that is really the case.
|
|
565
|
+
|
|
566
|
+
#### On an element you draw yourself
|
|
567
|
+
|
|
568
|
+
The claim is one attribute, and nothing is asked of the element carrying it —
|
|
569
|
+
`ownTarget` is only the prop that writes it on a navi control:
|
|
570
|
+
|
|
571
|
+
```jsx
|
|
572
|
+
<button class="court_side" data-own-target="always" onClick={explain}>
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
That is what the controls above read, and what the gesture readers read
|
|
576
|
+
(`data-drag-handle`, `data-drag-ignore` and friends are the same vocabulary). An
|
|
577
|
+
application keeps its own drawing and gets the press ownership all the same.
|
|
578
|
+
|
|
579
|
+
#### navi steps back; a plain `onClick` does not
|
|
580
|
+
|
|
581
|
+
What `ownTarget` stops is navi answering: the controls above it, and the
|
|
582
|
+
gestures. It does **not** stop the event — the propagation is left whole, so
|
|
583
|
+
that everything which is not a navi interaction still sees the press it always
|
|
584
|
+
saw. A raw `onClick` on an ancestor is one of those, and still fires; stop it
|
|
585
|
+
there yourself if it must not.
|
|
586
|
+
|
|
587
|
+
`data-drag-ignore` says a narrower thing, to the gesture alone: the press there
|
|
588
|
+
is none of the gesture's business, and the element keeps both its cursor and its
|
|
589
|
+
text selection.
|
|
542
590
|
|
|
543
591
|
### What says a thing can be picked up
|
|
544
592
|
|
package/docs/typography.md
CHANGED
|
@@ -28,7 +28,7 @@ same names — `size="l"` is a font size, `padding="l"` is a gap.
|
|
|
28
28
|
|
|
29
29
|
Live examples: `src/text/demos/*_demo.html` — one page per concern
|
|
30
30
|
(`text_overflow_demo.html`, `text_spacing_demo.html`, `text_loading_demo.html`,
|
|
31
|
-
`text_attach_last_child_demo.html`, `icon_demo.html`).
|
|
31
|
+
`text_attach_last_child_demo.html`, `text_emoji_demo.html`, `icon_demo.html`).
|
|
32
32
|
|
|
33
33
|
## Truncating: `maxLines`, and nothing else
|
|
34
34
|
|
|
@@ -61,7 +61,9 @@ width must say it too**, or the whole chain grows instead of truncating:
|
|
|
61
61
|
|
|
62
62
|
```jsx
|
|
63
63
|
<Box flex width="300" spacing="s">
|
|
64
|
-
<Box flex expandX minWidth="0">
|
|
64
|
+
<Box flex expandX minWidth="0">
|
|
65
|
+
{" "}
|
|
66
|
+
{/* without minWidth the row just grows */}
|
|
65
67
|
<Text maxLines={1}>…</Text>
|
|
66
68
|
</Box>
|
|
67
69
|
</Box>
|
|
@@ -153,6 +155,45 @@ Two things opt out of that flow:
|
|
|
153
155
|
the space characters too. This replaces them with padding-based spaces, so the
|
|
154
156
|
underline stops at the text. `Link` sets it.
|
|
155
157
|
|
|
158
|
+
## Emoji
|
|
159
|
+
|
|
160
|
+
**An emoji is not a character the layout can absorb.** The system emoji fonts
|
|
161
|
+
(Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji) have a taller
|
|
162
|
+
ascent/descent than any text font, so the moment an emoji sits in a line, that
|
|
163
|
+
line is taller than the ones around it: a row shifts down next to its
|
|
164
|
+
neighbours, a paragraph's lines are unevenly spaced, a truncated label no
|
|
165
|
+
longer lines up with its siblings.
|
|
166
|
+
|
|
167
|
+
So, first: **an emoji is only expected in free text a user typed** — a message,
|
|
168
|
+
a comment, a description. It has no business in a name, a title, an identifier,
|
|
169
|
+
a label. Validate those fields so it never gets in, rather than teaching every
|
|
170
|
+
row of the app to survive it.
|
|
171
|
+
|
|
172
|
+
Where it is expected, `emojiAsIcon`:
|
|
173
|
+
|
|
174
|
+
```jsx
|
|
175
|
+
<Text emojiAsIcon>Salut 👋 on se retrouve au parc 🌳 ?</Text>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Every emoji found in the string children is rendered inside an `Icon`, which
|
|
179
|
+
caps it at `1em` and centers it on the line like any glyph icon — the line keeps
|
|
180
|
+
the height of its text, on one line or several. This is what chat apps do (an
|
|
181
|
+
emoji is a 1em box, never a glyph with its own metrics). `Button` and
|
|
182
|
+
`MessageBox` have it on by default — a label is one line whose height
|
|
183
|
+
everything around it relies on, a message is free text; `Badge` forwards it,
|
|
184
|
+
opt-in.
|
|
185
|
+
|
|
186
|
+
What it does not do, and that is accepted: under `maxLines` the `Text` clips at
|
|
187
|
+
its own box — that is what truncation is — and an emoji drawn a little beyond
|
|
188
|
+
its 1em box can lose a sliver at the top. The line stays aligned, which is the
|
|
189
|
+
part that matters.
|
|
190
|
+
|
|
191
|
+
**Do not raise `lineHeight` because of emoji.** The default line height keeps
|
|
192
|
+
text compact and, with `emojiAsIcon`, holds up even when nearly every word is
|
|
193
|
+
an emoji (see the dense cases in `text_emoji_demo.html`). A `lineHeight` is a
|
|
194
|
+
typographic choice for the text itself — a paragraph that wants air — never a
|
|
195
|
+
workaround for a glyph that grew too tall; that glyph gets `emojiAsIcon`.
|
|
196
|
+
|
|
156
197
|
## Text that must not move when its style changes
|
|
157
198
|
|
|
158
199
|
A label that becomes bold when its row is selected reflows everything around it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/navi",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.104",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Library of components including navigation to create frontend applications",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"prepublishOnly": "npm run build"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@jsenv/dom": "0.17.
|
|
32
|
+
"@jsenv/dom": "0.17.27",
|
|
33
33
|
"@jsenv/humanize": "1.7.8",
|
|
34
34
|
"@jsenv/validity": "0.4.2"
|
|
35
35
|
},
|