@cahyo-dimas/freeday 1.26.0 → 1.28.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/CHANGELOG.md +98 -0
- package/COMPONENTS.md +80 -1
- package/README.id.md +2 -1
- package/README.md +2 -1
- package/dist/freeday-number.js +104 -0
- package/dist/freeday.bundle.css +46 -1
- package/dist/freeday.css +46 -1
- package/dist/freeday.js +105 -0
- package/docs/getting-started.md +1 -1
- package/package.json +2 -2
- package/src/components/button.css +16 -0
- package/src/components/card.css +16 -1
- package/src/components/input-group.css +6 -0
- package/src/components/input.css +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,104 @@
|
|
|
3
3
|
Semua perubahan penting dicatat di sini. Format longgar mengikuti
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); tiap versi = git tag.
|
|
5
5
|
|
|
6
|
+
## [1.28.0] — 2026-08-18
|
|
7
|
+
Improvement note 007, written while fixing a workspace picker whose cards showed a pointer cursor
|
|
8
|
+
and swallowed every click.
|
|
9
|
+
### Added
|
|
10
|
+
- **`.fdy-btn--stretch`** — the pattern for **one card, one primary action, one escape hatch**, which
|
|
11
|
+
the kit had no shape for: `--button` is a card that *is* one control, `--interactive` a card that
|
|
12
|
+
merely *has* one, and a card with two actions can be neither (interactive content nested in a
|
|
13
|
+
`<button>` is invalid HTML). The primary control keeps its real `<button>`/`<a>` semantics and
|
|
14
|
+
spreads its hit area over the card with a pseudo-element.
|
|
15
|
+
This is shipped rather than documented because it **cannot be hand-rolled on a `.fdy-btn`**: the
|
|
16
|
+
button nudges itself with a transform on `:hover`/`:active`, a transformed element becomes the
|
|
17
|
+
containing block for its own absolutely positioned descendants, and the overlay therefore
|
|
18
|
+
re-anchors from the card to the button's own box mid-gesture. `mousedown` lands on the button,
|
|
19
|
+
`mouseup` somewhere else, and the browser fires `click` on their common ancestor — the button never
|
|
20
|
+
gets one. The symptom is identical to the bug being fixed.
|
|
21
|
+
The report found this on `--text`/`--ghost`, which break on press. The kit's **base** rule is
|
|
22
|
+
`.fdy-btn:hover{transform:translateY(-1px)}`, so the default and `--danger` fills break one step
|
|
23
|
+
earlier, on hover: neutralising only `:active` — the obvious half-fix — still leaks.
|
|
24
|
+
- **Escape hatches are raised automatically.** Every focusable element in a card holding a stretched
|
|
25
|
+
target sits above the overlay without markup or CSS from the consumer, because forgetting a
|
|
26
|
+
`z-index` here fails silently — the control looks and hovers exactly as before and simply never
|
|
27
|
+
receives the click.
|
|
28
|
+
### Fixed
|
|
29
|
+
- `--interactive` is now documented as **presentational**: the only affordance in the kit whose
|
|
30
|
+
correctness lives outside it. The CSS cannot know whether a handler exists, so the modifier without
|
|
31
|
+
a control and a control without the modifier both fail silently. It now points at the shape that
|
|
32
|
+
makes the promise true.
|
|
33
|
+
### Notes on the shape of the fix
|
|
34
|
+
- **Raising the escape hatch must not outweigh the app.** The first version declared
|
|
35
|
+
`position:relative` at normal specificity and *measurably* dragged an absolutely positioned corner
|
|
36
|
+
dismiss button back into the flow — a control the app had pinned itself, moved by the kit, with no
|
|
37
|
+
error. `z-index` still wins, but the `position` it needs is now a zero-specificity `:where()`
|
|
38
|
+
default that any app rule beats. The two rules must stay split; the gate asserts it.
|
|
39
|
+
- **Cards only.** The overlay anchors to the nearest *positioned* ancestor. `.fdy-list__row` is not
|
|
40
|
+
positioned (`.fdy-list` is), so a stretched target in a list row would cover the whole list —
|
|
41
|
+
documented, with `.fdy-list__row--button` as the answer for clickable rows.
|
|
42
|
+
### Added — guards
|
|
43
|
+
- `browser/card-stretch.mjs` drives real presses: the target receives clicks from anywhere on the
|
|
44
|
+
card, the click is dispatched **on the button** rather than on a common ancestor, the secondary
|
|
45
|
+
action and an inline link keep their own clicks, and an app-pinned control keeps its own
|
|
46
|
+
`position`. A synthetic `.click()` never enters `:active` and passes against the broken CSS, which
|
|
47
|
+
is why this spec is worth its weight.
|
|
48
|
+
- `test/css.test.mjs` guards all of it at the CI gate. Mutation-checked against six defects,
|
|
49
|
+
including both of the ones written during this change.
|
|
50
|
+
|
|
51
|
+
## [1.27.0] — 2026-08-14
|
|
52
|
+
Improvement note #44, found while building a settings screen whose only numeric field looked like it
|
|
53
|
+
belonged to a different application.
|
|
54
|
+
### Fixed
|
|
55
|
+
- **`.fdy-input[type="number"]` no longer shows the user agent's spin buttons.** They are OS widgets
|
|
56
|
+
in OS colours that no theme reaches, so on a dark surface they read as a light-grey artefact glued
|
|
57
|
+
to an otherwise themed field — the one unthemed control on a page where everything else is themed
|
|
58
|
+
to the last pixel. Both halves ship, because they cover different engines: `appearance: textfield`
|
|
59
|
+
(Firefox) and `::-webkit-outer/inner-spin-button { appearance: none }` (Blink/WebKit).
|
|
60
|
+
### Added
|
|
61
|
+
- **`[data-fdy-number]` + `freeday-number.js`** — the increment affordance back on the kit's terms,
|
|
62
|
+
since hiding the native buttons removes it. **Not a new block:** it is an `.fdy-input-group` with
|
|
63
|
+
two `__btn`s, so it inherits the shared border, `:focus-within` ring and `:has()` error promotion
|
|
64
|
+
that already existed. The reported `.fdy-number` block would have duplicated all of it.
|
|
65
|
+
- **No custom event.** Stepping dispatches native bubbling `input` + `change` on the input, so
|
|
66
|
+
`v-model` / `onChange` / `@bind` work with no adapter and no new API — the input stays the source
|
|
67
|
+
of truth. That is also why this needs no typed wrapper in any of the four stacks.
|
|
68
|
+
- `min`/`max`/`step` live on the input and the buttons never redo the arithmetic (`stepUp()`/
|
|
69
|
+
`stepDown()` clamp for free). They go `disabled` at a bound, on a `disabled`/`readonly` field,
|
|
70
|
+
and when `step="any"` — which has no defined increment and makes `stepUp()` throw, so a stepper
|
|
71
|
+
cannot honestly express it.
|
|
72
|
+
- A `MutationObserver` watches `disabled`/`readonly`/`min`/`max`/`step`: a framework changes those
|
|
73
|
+
without firing an event, and a button that still looks enabled while doing nothing is the exact
|
|
74
|
+
lie this state machine exists to prevent. (First observer in the kit — the alternative was a
|
|
75
|
+
button whose appearance silently drifts from its behaviour.)
|
|
76
|
+
- The buttons are **not tab stops** (`tabindex="-1"`): the input is already focusable and ↑/↓
|
|
77
|
+
already step it, so two extra stops per field cost every keyboard user and buy nothing. They keep
|
|
78
|
+
an `aria-label`, and `type="button"` so they cannot submit their form.
|
|
79
|
+
- `.fdy-input-group__btn` gains a **leading-position** rule (divider on the correct side when the
|
|
80
|
+
button comes first) and a **`:disabled`** state — dim + `not-allowed`, with `:hover` withdrawn,
|
|
81
|
+
because a disabled button still matches `:hover`.
|
|
82
|
+
- `COMPONENTS.md` now answers **which `type` `.fdy-input` covers**, which is the question that would
|
|
83
|
+
have prevented this note: every text-like type themes identically, and the two that keep a native
|
|
84
|
+
widget are named with what to do about each.
|
|
85
|
+
### Notes on the shape of the fix
|
|
86
|
+
- **`type="search"` deliberately left alone.** Its WebKit clear (×) button is unthemed too — and it
|
|
87
|
+
is on the kit's own docs pages, 10 of them — but it is the only way to empty the field. Stripping
|
|
88
|
+
it removes function, not chrome; the number arrows only removed a mouse-only increment that ↑/↓
|
|
89
|
+
still provides. `type="date"`/`time` likewise keep their picker indicator; the kit ships its own
|
|
90
|
+
datepicker/timepicker and the docs now say to use those instead.
|
|
91
|
+
- **Why this was never caught:** `type="number"` appears **zero** times across `docs/index.html`,
|
|
92
|
+
`docs/reference-screen.html` and `examples/` (against `search` ×10, `text` ×12, `date` ×4). The kit
|
|
93
|
+
never used the input it shipped. The docs page now carries a real number field, in the input-group
|
|
94
|
+
section where it belongs.
|
|
95
|
+
### Added — guards
|
|
96
|
+
- `test/css.test.mjs` asserts both engine halves survive (each is invisible in review and each brings
|
|
97
|
+
the artefact back on one engine only), and states in-file why `search`/`date` are excluded.
|
|
98
|
+
- `browser/number.mjs` drives real clicks and a real Tab: stepping fires native `input` + `change`,
|
|
99
|
+
bounds disable the right button, a disabled button changes nothing, `readonly` and `step="any"`
|
|
100
|
+
are inert without throwing, and Tab skips the buttons. Mutation-checked against five defects.
|
|
101
|
+
- `browser/harness.mjs` gains `pressKey` — tab order only moves for a trusted key, so the claim is
|
|
102
|
+
measured rather than read off an attribute.
|
|
103
|
+
|
|
6
104
|
## [1.26.0] — 2026-08-14
|
|
7
105
|
Improvement note #43, found while chasing a "the upload is stuck" report on a 626 KB PDF: the
|
|
8
106
|
transfer took about a second, the server then spent nearly a minute reading the document.
|
package/COMPONENTS.md
CHANGED
|
@@ -91,6 +91,7 @@ element is fine. Events are bubbling
|
|
|
91
91
|
| `data-fdy-tree` | `freeday-tree.js` | `FreedayTree` | — |
|
|
92
92
|
| `data-fdy-rating` | `freeday-rating.js` | `FreedayRating` | — (native `change`) |
|
|
93
93
|
| `data-fdy-slider` | `freeday-slider.js` | `FreedaySlider` | — (native `input`) |
|
|
94
|
+
| `data-fdy-number` | `freeday-number.js` | `FreedayNumber` | — (native `input`, `change`) |
|
|
94
95
|
| `data-fdy-drawer="id"` | `freeday-drawer.js` | `FreedayDrawer` | — |
|
|
95
96
|
| `data-fdy-chart="type"` | `freeday-chart.js` | `FreedayChart` | — (`.update(el)` to repaint) |
|
|
96
97
|
| — (imperative) | `freeday-toast.js` | `Freeday.toast()` / `Freeday.dismiss()` | — |
|
|
@@ -307,6 +308,17 @@ native control, otherwise a `<div>` + explicitly associated label.
|
|
|
307
308
|
</label>
|
|
308
309
|
```
|
|
309
310
|
|
|
311
|
+
**Which `type` does `.fdy-input` cover?** Every text-like type — `text` `email` `password` `tel`
|
|
312
|
+
`url` `search` `number` — themes identically. Two carry a native widget the kit does *not* override:
|
|
313
|
+
|
|
314
|
+
| Type | What the UA still draws | What to do |
|
|
315
|
+
|---|---|---|
|
|
316
|
+
| `search` | WebKit's clear (×) button, in OS colours | **Left alone on purpose** — it is the only way to empty the field. Want it themed? Drop the type back to `text` and add a `.fdy-input-group__btn` that clears. |
|
|
317
|
+
| `date` `time` `datetime-local` | the picker indicator glyph, and the whole native picker | Use `.fdy-datepicker` / `.fdy-timepicker` / `.fdy-datetimepicker` instead — the kit ships its own. |
|
|
318
|
+
|
|
319
|
+
`number` used to be a third: the UA spin buttons are unthemeable, so `.fdy-input[type="number"]`
|
|
320
|
+
now hides them and `[data-fdy-number]` below gives the affordance back.
|
|
321
|
+
|
|
310
322
|
## Input group — `.fdy-input-group`
|
|
311
323
|
Prefix/suffix addons around an input: text (`Rp`, `%`), a decorative icon
|
|
312
324
|
(`__addon--icon`), or an action button (`__btn`).
|
|
@@ -320,6 +332,31 @@ Prefix/suffix addons around an input: text (`Rp`, `%`), a decorative icon
|
|
|
320
332
|
</div>
|
|
321
333
|
```
|
|
322
334
|
|
|
335
|
+
## Number field — `[data-fdy-number]`
|
|
336
|
+
A number input with its increment/decrement affordance back, after `.fdy-input` removed the
|
|
337
|
+
browser's own. **Not a new block** — it is an `.fdy-input-group` with two `__btn`s, so it inherits
|
|
338
|
+
the shared border, focus ring and error promotion. Needs `freeday-number.js`.
|
|
339
|
+
|
|
340
|
+
```html
|
|
341
|
+
<div class="fdy-input-group" data-fdy-number>
|
|
342
|
+
<button type="button" class="fdy-input-group__btn" data-fdy-number-step="-1"
|
|
343
|
+
tabindex="-1" aria-label="Kurangi">−</button>
|
|
344
|
+
<input class="fdy-input" type="number" min="0" max="10" step="1" value="1" aria-label="Konfirmasi">
|
|
345
|
+
<button type="button" class="fdy-input-group__btn" data-fdy-number-step="1"
|
|
346
|
+
tabindex="-1" aria-label="Tambah">+</button>
|
|
347
|
+
</div>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
- **No custom event.** Stepping fires native bubbling `input` + `change` on the input, so `v-model`,
|
|
351
|
+
`onChange` and `@bind` work with no adapter — the input stays the source of truth.
|
|
352
|
+
- `min` / `max` / `step` live on the **input**; the buttons never do the arithmetic themselves
|
|
353
|
+
(`stepUp()`/`stepDown()` clamp for free) and go `disabled` at a bound, on a `disabled`/`readonly`
|
|
354
|
+
field, and when `step="any"` (a stepper cannot express "no defined increment").
|
|
355
|
+
- The buttons are **not tab stops** (`tabindex="-1"`): the input is already focusable and ↑/↓
|
|
356
|
+
already step it, so extra stops would cost every keyboard user and buy nothing. Keep the
|
|
357
|
+
`aria-label` — pointer and browse-mode users still get a named control.
|
|
358
|
+
- `type="button"` is required. Inside a `<form>`, a bare `<button>` submits it.
|
|
359
|
+
|
|
323
360
|
## Checkbox · radio · switch
|
|
324
361
|
Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrapping `<label>`;
|
|
325
362
|
`.fdy-checkbox` on a bare `<input type="checkbox">` used inside tables/trees.
|
|
@@ -848,7 +885,11 @@ is `role="group" aria-roledescription="slide"`.
|
|
|
848
885
|
Parts `__body` `__title` `__desc` `__footer`. Modifiers:
|
|
849
886
|
|
|
850
887
|
- `--elevated` — raise it (use sparingly; most surfaces are flat)
|
|
851
|
-
- `--interactive` — cursor + hover-lift for a card that *has* a click handler
|
|
888
|
+
- `--interactive` — cursor + hover-lift for a card that *has* a click handler. **Presentational
|
|
889
|
+
only:** it is the one affordance in the kit whose correctness lives outside it — the CSS cannot
|
|
890
|
+
know whether a handler exists, so the modifier without a control (a card that looks clickable and
|
|
891
|
+
swallows every click) and a control without the modifier both fail silently. Pair it with a real
|
|
892
|
+
control — normally the stretched target below.
|
|
852
893
|
- `--button` — for a card that **is** the control: render it as `<button>` and add this to reset
|
|
853
894
|
the UA button box **without** losing the card surface/border
|
|
854
895
|
- `.fdy-card--button` never replaces keyboard semantics — a clickable card must be a real
|
|
@@ -858,6 +899,44 @@ Parts `__body` `__title` `__desc` `__footer`. Modifiers:
|
|
|
858
899
|
- The card is `position:relative`, so a badge or ribbon you absolutely position inside it anchors
|
|
859
900
|
to the card. That is also what keeps hidden labels inside it from escaping — see *Containment*.
|
|
860
901
|
|
|
902
|
+
### One card, one primary action, one escape hatch
|
|
903
|
+
|
|
904
|
+
A card with **two** actions cannot be `--button`: interactive content nested in a `<button>` is
|
|
905
|
+
invalid HTML. Give the primary control `.fdy-btn--stretch` — its hit area covers the whole card,
|
|
906
|
+
while it stays a real `<button>`/`<a>` with real keyboard semantics.
|
|
907
|
+
|
|
908
|
+
```html
|
|
909
|
+
<article class="fdy-card fdy-card--interactive">
|
|
910
|
+
<div class="fdy-card__body">
|
|
911
|
+
<h3 class="fdy-card__title">Workspace Alpha</h3>
|
|
912
|
+
<p class="fdy-card__desc">12 members · 4 boards</p>
|
|
913
|
+
</div>
|
|
914
|
+
<div class="fdy-card__footer">
|
|
915
|
+
<button class="fdy-btn fdy-btn--text" type="button">Details</button> <!-- escape hatch -->
|
|
916
|
+
<button class="fdy-btn fdy-btn--stretch" type="button">Open workspace Alpha</button>
|
|
917
|
+
</div>
|
|
918
|
+
</article>
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
- **The escape hatch needs nothing.** Every focusable element in a card that holds a stretched
|
|
922
|
+
target is raised above the overlay automatically — forgetting a `z-index` here fails silently, and
|
|
923
|
+
a silent failure is what this pattern exists to remove. The `position` that raising needs is only a
|
|
924
|
+
**default** (zero-specificity), so a control you pin yourself — a corner dismiss, a favourite star —
|
|
925
|
+
keeps its own `position:absolute` and is raised anyway.
|
|
926
|
+
- **Name the stretched button for what the whole card does** ("Open workspace Alpha", not "Open").
|
|
927
|
+
Its label is the accessible name of the entire hit area.
|
|
928
|
+
- **Do not add `.fdy-btn--stretch` to more than one control per card** — the last one wins the
|
|
929
|
+
overlap, and which one that is depends on source order.
|
|
930
|
+
- Text under the overlay is no longer selectable; that is the cost of the pattern, not a bug.
|
|
931
|
+
- **Cards only.** The overlay anchors to the nearest *positioned* ancestor, and in the kit that is
|
|
932
|
+
`.fdy-card`. A `.fdy-list__row` is **not** positioned (`.fdy-list` is), so a stretched target in a
|
|
933
|
+
list row would cover the **whole list** — for clickable rows use `.fdy-list__row--button`.
|
|
934
|
+
- `.fdy-btn` nudges itself on `:hover`/`:active`, and a transformed element becomes the containing
|
|
935
|
+
block for its own absolutely positioned descendants — so a hand-rolled `::after{inset:0}` overlay
|
|
936
|
+
**re-anchors to the button mid-gesture** and the click never lands. `--stretch` neutralises those
|
|
937
|
+
transforms and moves the press feedback to the card. This is why the pattern is shipped rather
|
|
938
|
+
than documented.
|
|
939
|
+
|
|
861
940
|
## Badge — `.fdy-badge`
|
|
862
941
|
Inline status pill: `--success` `--warning` `--danger` `--info` `--outline`. Never colour-only —
|
|
863
942
|
the text carries the meaning.
|
package/README.id.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> **Lebih banyak _free day_ buat dev — UI kit-nya sudah siap pakai.**
|
|
6
6
|
|
|
7
7
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.28.0)
|
|
9
9
|
|
|
10
10
|
UI KIT yang token-driven & framework-agnostic — satu sumber kebenaran untuk warna, tipografi,
|
|
11
11
|
spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
|
|
@@ -130,6 +130,7 @@ Semua auto-init `[data-fdy-*]` saat `DOMContentLoaded` — idempotent, progressi
|
|
|
130
130
|
| `freeday-datetime` | `[data-fdy-datetimepicker]` | `fdy-datetime-change` `{date,time,value}` · `window.FreedayDatetime` |
|
|
131
131
|
| `freeday-cascade` | `[data-fdy-cascade]` (model `<ul>` bersarang) | `fdy-cascade-change` `{value,path,labels}` · `window.FreedayCascade` |
|
|
132
132
|
| `freeday-mask` | `[data-fdy-mask]`, `[data-fdy-password]` | `fdy-mask` `{value,raw}` · `window.FreedayMask` |
|
|
133
|
+
| `freeday-number` | `[data-fdy-number]` | — (native `input`/`change`) · `window.FreedayNumber` |
|
|
133
134
|
| `freeday-form` | `[data-fdy-validate]` (form) | `fdy-form-invalid`/`-valid` · `window.FreedayForm` |
|
|
134
135
|
| `freeday-chip` | `[data-fdy-chips]`, `.fdy-chip__remove` | `fdy-chip-change`/`fdy-chip-remove` · `window.FreedayChip` |
|
|
135
136
|
| `freeday-upload` | `[data-fdy-dropzone]` | `fdy-upload-add`/`-remove` · `window.FreedayUpload` |
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> **More free days for devs — the UI kit is ready to use.**
|
|
6
6
|
|
|
7
7
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.28.0)
|
|
9
9
|
|
|
10
10
|
A token-driven, framework-agnostic UI kit — one source of truth for color, typography,
|
|
11
11
|
spacing, and components. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
|
|
@@ -129,6 +129,7 @@ Each one auto-inits `[data-fdy-*]` on `DOMContentLoaded` — idempotent, progres
|
|
|
129
129
|
| `freeday-datetime` | `[data-fdy-datetimepicker]` | `fdy-datetime-change` `{date,time,value}` · `window.FreedayDatetime` |
|
|
130
130
|
| `freeday-cascade` | `[data-fdy-cascade]` (nested `<ul>` model) | `fdy-cascade-change` `{value,path,labels}` · `window.FreedayCascade` |
|
|
131
131
|
| `freeday-mask` | `[data-fdy-mask]`, `[data-fdy-password]` | `fdy-mask` `{value,raw}` · `window.FreedayMask` |
|
|
132
|
+
| `freeday-number` | `[data-fdy-number]` | — (native `input`/`change`) · `window.FreedayNumber` |
|
|
132
133
|
| `freeday-form` | `[data-fdy-validate]` (form) | `fdy-form-invalid`/`-valid` · `window.FreedayForm` |
|
|
133
134
|
| `freeday-chip` | `[data-fdy-chips]`, `.fdy-chip__remove` | `fdy-chip-change`/`fdy-chip-remove` · `window.FreedayChip` |
|
|
134
135
|
| `freeday-upload` | `[data-fdy-dropzone]` | `fdy-upload-add`/`-remove` · `window.FreedayUpload` |
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/* Freeday — number field enhancer (optional, zero-dependency).
|
|
2
|
+
* Gives a native <input type="number"> its increment/decrement affordance back, on the kit's terms:
|
|
3
|
+
* .fdy-input hides the user agent's own spin buttons (unthemeable OS widgets), so this puts two real
|
|
4
|
+
* buttons in an .fdy-input-group instead. No new block — the group already owns the shared border,
|
|
5
|
+
* :focus-within ring and error promotion.
|
|
6
|
+
*
|
|
7
|
+
* Markup contract:
|
|
8
|
+
* <div class="fdy-input-group" data-fdy-number>
|
|
9
|
+
* <button type="button" class="fdy-input-group__btn" data-fdy-number-step="-1"
|
|
10
|
+
* tabindex="-1" aria-label="Kurangi">−</button>
|
|
11
|
+
* <input class="fdy-input" type="number" min="0" max="10" step="1" value="1" aria-label="…">
|
|
12
|
+
* <button type="button" class="fdy-input-group__btn" data-fdy-number-step="1"
|
|
13
|
+
* tabindex="-1" aria-label="Tambah">+</button>
|
|
14
|
+
* </div>
|
|
15
|
+
*
|
|
16
|
+
* No custom event: the input is the source of truth, so stepping dispatches native bubbling `input`
|
|
17
|
+
* and `change`. v-model / onChange / @bind bind to the input directly, exactly as they would without
|
|
18
|
+
* this enhancer.
|
|
19
|
+
*
|
|
20
|
+
* The buttons are deliberately OUT of the tab order (tabindex="-1"): the input is already focusable
|
|
21
|
+
* and ArrowUp/ArrowDown already step it, so two extra tab stops per field would cost every keyboard
|
|
22
|
+
* user something and buy nothing. They keep an aria-label, so pointer and browse-mode users still
|
|
23
|
+
* get a named control.
|
|
24
|
+
*/
|
|
25
|
+
(function () {
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
function initNumber(root) {
|
|
29
|
+
if (root.dataset.fdyNumberReady === '1') return;
|
|
30
|
+
|
|
31
|
+
var input = root.querySelector('input[type="number"]');
|
|
32
|
+
var buttons = Array.prototype.slice.call(root.querySelectorAll('[data-fdy-number-step]'));
|
|
33
|
+
if (!input || !buttons.length) return;
|
|
34
|
+
root.dataset.fdyNumberReady = '1';
|
|
35
|
+
|
|
36
|
+
/* step="any" means there is no defined increment, and stepUp()/stepDown() throw InvalidStateError
|
|
37
|
+
on such a field. A stepper cannot express it, so the buttons stay visibly disabled rather than
|
|
38
|
+
failing on click. Read live, not captured: `step` is one of the attributes watched below. */
|
|
39
|
+
function stepless() {
|
|
40
|
+
return (input.getAttribute('step') || '').toLowerCase() === 'any';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function frozen() {
|
|
44
|
+
return stepless() || input.disabled || input.readOnly;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Bounds are read from the input, never recomputed: stepUp()/stepDown() already clamp to
|
|
48
|
+
min/max/step, so these two only decide whether the button tells the truth about what it does. */
|
|
49
|
+
function atLimit(direction) {
|
|
50
|
+
var bound = direction > 0 ? input.max : input.min;
|
|
51
|
+
if (bound === '' || input.value === '') return false;
|
|
52
|
+
var value = Number(input.value);
|
|
53
|
+
var limit = Number(bound);
|
|
54
|
+
if (isNaN(value) || isNaN(limit)) return false;
|
|
55
|
+
return direction > 0 ? value >= limit : value <= limit;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sync() {
|
|
59
|
+
buttons.forEach(function (btn) {
|
|
60
|
+
var direction = Number(btn.getAttribute('data-fdy-number-step')) < 0 ? -1 : 1;
|
|
61
|
+
btn.disabled = frozen() || atLimit(direction);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
buttons.forEach(function (btn) {
|
|
66
|
+
btn.addEventListener('click', function () {
|
|
67
|
+
if (frozen()) return;
|
|
68
|
+
if (Number(btn.getAttribute('data-fdy-number-step')) < 0) input.stepDown();
|
|
69
|
+
else input.stepUp();
|
|
70
|
+
/* The value changed without the user typing, so say so the way the platform does — frameworks
|
|
71
|
+
listen to these, not to a kit-specific event. */
|
|
72
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
73
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
74
|
+
sync();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
input.addEventListener('input', sync);
|
|
79
|
+
input.addEventListener('change', sync);
|
|
80
|
+
/* Typing is not the only thing that moves a bound: a framework flips `disabled` or recomputes
|
|
81
|
+
`max` without any event firing, and a button that still looks enabled but no longer does
|
|
82
|
+
anything is the lie this state machine exists to avoid. */
|
|
83
|
+
new MutationObserver(sync).observe(input, {
|
|
84
|
+
attributes: true,
|
|
85
|
+
attributeFilter: ['disabled', 'readonly', 'min', 'max', 'step'],
|
|
86
|
+
});
|
|
87
|
+
sync();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function initAll(context) {
|
|
91
|
+
var root = context || document;
|
|
92
|
+
/* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
|
|
93
|
+
if (root.matches && root.matches('[data-fdy-number]')) initNumber(root);
|
|
94
|
+
Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-number]'), initNumber);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (document.readyState === 'loading') {
|
|
98
|
+
document.addEventListener('DOMContentLoaded', function () { initAll(); });
|
|
99
|
+
} else {
|
|
100
|
+
initAll();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
window.FreedayNumber = { init: initNumber, initAll: initAll };
|
|
104
|
+
})();
|
package/dist/freeday.bundle.css
CHANGED
|
@@ -711,6 +711,22 @@ a { color: var(--color-primary); }
|
|
|
711
711
|
.fdy-btn--ghost[aria-pressed="true"],.fdy-btn--text[aria-pressed="true"]{background:var(--color-primary-soft);color:var(--color-primary-strong);border-color:var(--color-primary-border);box-shadow:none;}
|
|
712
712
|
.fdy-btn--ghost[aria-pressed="true"]:hover,.fdy-btn--text[aria-pressed="true"]:hover{background:var(--color-primary-soft);transform:none;box-shadow:none;}
|
|
713
713
|
|
|
714
|
+
/* Stretched hit area (#007) — spread this button's click target over its nearest positioned
|
|
715
|
+
* ancestor, so "one card, one primary action, one escape hatch" needs no click handler on the
|
|
716
|
+
* <article>: the real control stays a real <button>/<a> and keeps its keyboard semantics.
|
|
717
|
+
* `.fdy-card` is already position:relative, so it is the anchor.
|
|
718
|
+
*
|
|
719
|
+
* The transforms MUST go, and this is the whole reason the modifier exists rather than a docs line:
|
|
720
|
+
* a transformed element becomes the containing block for its own absolutely-positioned descendants,
|
|
721
|
+
* so `inset:0` re-anchors from the card to the button's own box the moment the button is hovered or
|
|
722
|
+
* pressed. Measured: mousedown lands on the button, the overlay collapses under the pointer, mouseup
|
|
723
|
+
* lands on some other element, and the browser fires `click` on their common ancestor — the button
|
|
724
|
+
* never gets one. It demos fine on hover for --text/--ghost (their hover is already transform:none)
|
|
725
|
+
* and only a real press reveals it; the default and --danger fills break one step earlier, on hover.
|
|
726
|
+
* Press feedback moves to the card, which is the object actually being pressed (see card.css). */
|
|
727
|
+
.fdy-btn--stretch::after{content:'';position:absolute;inset:0;}
|
|
728
|
+
.fdy-btn--stretch:hover,.fdy-btn--stretch:active{transform:none;}
|
|
729
|
+
|
|
714
730
|
/* Icon-only — square; combine with --ghost/--text/--sm/--lg. Requires aria-label. */
|
|
715
731
|
.fdy-btn--icon{width:var(--control-h);padding:0;}
|
|
716
732
|
.fdy-btn--icon.fdy-btn--sm{width:calc(var(--control-h) - var(--space-2));}
|
|
@@ -744,9 +760,13 @@ a { color: var(--color-primary); }
|
|
|
744
760
|
.fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
|
|
745
761
|
.fdy-card--interactive{cursor:pointer;}
|
|
746
762
|
.fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
|
|
763
|
+
/* Pressing a stretched target (see .fdy-btn--stretch) is pressing the CARD — the button's own
|
|
764
|
+
nudge had to go, and the object under the pointer is the card. Ordered before the reduced-motion
|
|
765
|
+
block and the disabled rules so both still win at equal specificity. */
|
|
766
|
+
.fdy-card--interactive:has(.fdy-btn--stretch:active){transform:translateY(-1px);}
|
|
747
767
|
/* The global reduced-motion reset kills the ANIMATION; drop the -3px shift too so the card
|
|
748
768
|
* doesn't jump. The shadow still communicates the affordance. */
|
|
749
|
-
@media (prefers-reduced-motion:reduce){.fdy-card--interactive:hover{transform:none;}}
|
|
769
|
+
@media (prefers-reduced-motion:reduce){.fdy-card--interactive:hover,.fdy-card--interactive:has(.fdy-btn--stretch:active){transform:none;}}
|
|
750
770
|
/* When the whole card IS the control — a real <button> so the row is keyboard-reachable and
|
|
751
771
|
* announced — reset the UA button box. Note: it does NOT reset background/border, because
|
|
752
772
|
* .fdy-card already sets its own surface + border (resetting them here would strip the card). */
|
|
@@ -755,6 +775,17 @@ a { color: var(--color-primary); }
|
|
|
755
775
|
--interactive lift is withdrawn so a refusing control stops answering the pointer. */
|
|
756
776
|
.fdy-card--button:disabled,.fdy-card--button[aria-disabled="true"],.fdy-card--interactive[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
757
777
|
.fdy-card--interactive:disabled:hover,.fdy-card--interactive[aria-disabled="true"]:hover{box-shadow:var(--shadow-lift);transform:none;}
|
|
778
|
+
/* Escape hatches stay clickable above a stretched target — automatic, not a documented convention,
|
|
779
|
+
because forgetting it fails SILENTLY: the secondary control looks and hovers exactly as before and
|
|
780
|
+
simply never receives the click, which is the very symptom the pattern exists to cure. Matched by
|
|
781
|
+
shape (anything focusable) so a control the kit does not ship yet is covered too. */
|
|
782
|
+
.fdy-card:has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch){z-index:1;}
|
|
783
|
+
/* The position is a DEFAULT, not an override: z-index needs a positioned box (or a flex/grid item),
|
|
784
|
+
but an app that deliberately pins a control inside the card — a corner dismiss, a favourite star —
|
|
785
|
+
must keep its own `position:absolute`. Wrapped in :where() so this whole selector weighs nothing
|
|
786
|
+
and ANY app rule wins it; measured, because at normal specificity the kit silently dragged an
|
|
787
|
+
absolutely positioned dismiss button back into the flow. Do not merge these two rules. */
|
|
788
|
+
:where(.fdy-card:has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch)){position:relative;}
|
|
758
789
|
.fdy-card__body{padding:var(--space-5);}
|
|
759
790
|
.fdy-card__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);color:var(--color-text);margin:0 0 var(--space-1);}
|
|
760
791
|
.fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
|
|
@@ -1230,6 +1261,12 @@ a { color: var(--color-primary); }
|
|
|
1230
1261
|
.fdy-input-group__btn:hover{background:var(--color-surface-3);color:var(--color-text);}
|
|
1231
1262
|
.fdy-input-group__btn:focus-visible{outline:none;color:var(--color-text);box-shadow:inset 0 0 0 2px var(--color-primary);}
|
|
1232
1263
|
.fdy-input-group__btn svg{display:block;width:1.1rem;height:1.1rem;}
|
|
1264
|
+
/* Leading action button (the "−" of a number stepper): the divider belongs on the other side. */
|
|
1265
|
+
.fdy-input-group__btn:first-child{border-left:0;border-right:var(--bw) solid var(--color-border);}
|
|
1266
|
+
/* At a bound, or on a disabled/read-only field, the button must look as inert as it acts — same dim
|
|
1267
|
+
+ not-allowed as .fdy-input:disabled. :hover still matches a disabled button, so it is withdrawn. */
|
|
1268
|
+
.fdy-input-group__btn:disabled{opacity:.5;cursor:not-allowed;}
|
|
1269
|
+
.fdy-input-group__btn:disabled:hover{background:var(--color-surface-2);color:var(--color-text-muted);}
|
|
1233
1270
|
/* Error state must win over the neutralizing rules above — promote to the wrapper. */
|
|
1234
1271
|
.fdy-input-group:has(.fdy-input--error),.fdy-input-group:has(.fdy-combo--error),.fdy-input-group:has([aria-invalid="true"]){border-color:var(--color-danger);}
|
|
1235
1272
|
.fdy-input-group:has(.fdy-input--error):focus-within,.fdy-input-group:has(.fdy-combo--error):focus-within,.fdy-input-group:has([aria-invalid="true"]):focus-within{border-color:var(--color-danger);box-shadow:inset 0 1px 2px rgba(16,14,30,.04),0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
@@ -1254,6 +1291,14 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1254
1291
|
.fdy-input[readonly],.fdy-textarea[readonly]{background:var(--color-surface-2);border-color:var(--color-border);color:var(--color-text);cursor:default;}
|
|
1255
1292
|
.fdy-input[readonly]:hover,.fdy-textarea[readonly]:hover{border-color:var(--color-border);}
|
|
1256
1293
|
.fdy-input[readonly]:focus-visible,.fdy-textarea[readonly]:focus-visible{border-color:var(--color-border-strong);box-shadow:none;}
|
|
1294
|
+
/* Numbers (#44): the user agent draws its own spin buttons inside the field, in OS widget colours no
|
|
1295
|
+
theme reaches — on a dark surface they read as a light-grey artefact glued to a themed field, and
|
|
1296
|
+
it is the only unthemed control on the page. Removed so .fdy-input looks the same whatever its
|
|
1297
|
+
type. `appearance:textfield` is the Firefox half, the pseudo-element the Blink/WebKit half; both
|
|
1298
|
+
are needed. This trades away the mouse-only increment — arrow keys on a focused number input still
|
|
1299
|
+
work, and [data-fdy-number] gives the affordance back on the kit's terms. */
|
|
1300
|
+
.fdy-input[type="number"]{appearance:textfield;-webkit-appearance:textfield;}
|
|
1301
|
+
.fdy-input[type="number"]::-webkit-outer-spin-button,.fdy-input[type="number"]::-webkit-inner-spin-button{appearance:none;-webkit-appearance:none;margin:0;}
|
|
1257
1302
|
.fdy-input--error,.fdy-input[aria-invalid="true"],.fdy-textarea[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
1258
1303
|
.fdy-input--error:focus,.fdy-input[aria-invalid="true"]:focus,.fdy-textarea[aria-invalid="true"]:focus{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
1259
1304
|
.fdy-help{font-size:var(--text-xs);color:var(--color-text-muted);}
|
package/dist/freeday.css
CHANGED
|
@@ -341,6 +341,22 @@ a { color: var(--color-primary); }
|
|
|
341
341
|
.fdy-btn--ghost[aria-pressed="true"],.fdy-btn--text[aria-pressed="true"]{background:var(--color-primary-soft);color:var(--color-primary-strong);border-color:var(--color-primary-border);box-shadow:none;}
|
|
342
342
|
.fdy-btn--ghost[aria-pressed="true"]:hover,.fdy-btn--text[aria-pressed="true"]:hover{background:var(--color-primary-soft);transform:none;box-shadow:none;}
|
|
343
343
|
|
|
344
|
+
/* Stretched hit area (#007) — spread this button's click target over its nearest positioned
|
|
345
|
+
* ancestor, so "one card, one primary action, one escape hatch" needs no click handler on the
|
|
346
|
+
* <article>: the real control stays a real <button>/<a> and keeps its keyboard semantics.
|
|
347
|
+
* `.fdy-card` is already position:relative, so it is the anchor.
|
|
348
|
+
*
|
|
349
|
+
* The transforms MUST go, and this is the whole reason the modifier exists rather than a docs line:
|
|
350
|
+
* a transformed element becomes the containing block for its own absolutely-positioned descendants,
|
|
351
|
+
* so `inset:0` re-anchors from the card to the button's own box the moment the button is hovered or
|
|
352
|
+
* pressed. Measured: mousedown lands on the button, the overlay collapses under the pointer, mouseup
|
|
353
|
+
* lands on some other element, and the browser fires `click` on their common ancestor — the button
|
|
354
|
+
* never gets one. It demos fine on hover for --text/--ghost (their hover is already transform:none)
|
|
355
|
+
* and only a real press reveals it; the default and --danger fills break one step earlier, on hover.
|
|
356
|
+
* Press feedback moves to the card, which is the object actually being pressed (see card.css). */
|
|
357
|
+
.fdy-btn--stretch::after{content:'';position:absolute;inset:0;}
|
|
358
|
+
.fdy-btn--stretch:hover,.fdy-btn--stretch:active{transform:none;}
|
|
359
|
+
|
|
344
360
|
/* Icon-only — square; combine with --ghost/--text/--sm/--lg. Requires aria-label. */
|
|
345
361
|
.fdy-btn--icon{width:var(--control-h);padding:0;}
|
|
346
362
|
.fdy-btn--icon.fdy-btn--sm{width:calc(var(--control-h) - var(--space-2));}
|
|
@@ -374,9 +390,13 @@ a { color: var(--color-primary); }
|
|
|
374
390
|
.fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
|
|
375
391
|
.fdy-card--interactive{cursor:pointer;}
|
|
376
392
|
.fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
|
|
393
|
+
/* Pressing a stretched target (see .fdy-btn--stretch) is pressing the CARD — the button's own
|
|
394
|
+
nudge had to go, and the object under the pointer is the card. Ordered before the reduced-motion
|
|
395
|
+
block and the disabled rules so both still win at equal specificity. */
|
|
396
|
+
.fdy-card--interactive:has(.fdy-btn--stretch:active){transform:translateY(-1px);}
|
|
377
397
|
/* The global reduced-motion reset kills the ANIMATION; drop the -3px shift too so the card
|
|
378
398
|
* doesn't jump. The shadow still communicates the affordance. */
|
|
379
|
-
@media (prefers-reduced-motion:reduce){.fdy-card--interactive:hover{transform:none;}}
|
|
399
|
+
@media (prefers-reduced-motion:reduce){.fdy-card--interactive:hover,.fdy-card--interactive:has(.fdy-btn--stretch:active){transform:none;}}
|
|
380
400
|
/* When the whole card IS the control — a real <button> so the row is keyboard-reachable and
|
|
381
401
|
* announced — reset the UA button box. Note: it does NOT reset background/border, because
|
|
382
402
|
* .fdy-card already sets its own surface + border (resetting them here would strip the card). */
|
|
@@ -385,6 +405,17 @@ a { color: var(--color-primary); }
|
|
|
385
405
|
--interactive lift is withdrawn so a refusing control stops answering the pointer. */
|
|
386
406
|
.fdy-card--button:disabled,.fdy-card--button[aria-disabled="true"],.fdy-card--interactive[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
387
407
|
.fdy-card--interactive:disabled:hover,.fdy-card--interactive[aria-disabled="true"]:hover{box-shadow:var(--shadow-lift);transform:none;}
|
|
408
|
+
/* Escape hatches stay clickable above a stretched target — automatic, not a documented convention,
|
|
409
|
+
because forgetting it fails SILENTLY: the secondary control looks and hovers exactly as before and
|
|
410
|
+
simply never receives the click, which is the very symptom the pattern exists to cure. Matched by
|
|
411
|
+
shape (anything focusable) so a control the kit does not ship yet is covered too. */
|
|
412
|
+
.fdy-card:has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch){z-index:1;}
|
|
413
|
+
/* The position is a DEFAULT, not an override: z-index needs a positioned box (or a flex/grid item),
|
|
414
|
+
but an app that deliberately pins a control inside the card — a corner dismiss, a favourite star —
|
|
415
|
+
must keep its own `position:absolute`. Wrapped in :where() so this whole selector weighs nothing
|
|
416
|
+
and ANY app rule wins it; measured, because at normal specificity the kit silently dragged an
|
|
417
|
+
absolutely positioned dismiss button back into the flow. Do not merge these two rules. */
|
|
418
|
+
:where(.fdy-card:has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch)){position:relative;}
|
|
388
419
|
.fdy-card__body{padding:var(--space-5);}
|
|
389
420
|
.fdy-card__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);color:var(--color-text);margin:0 0 var(--space-1);}
|
|
390
421
|
.fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
|
|
@@ -860,6 +891,12 @@ a { color: var(--color-primary); }
|
|
|
860
891
|
.fdy-input-group__btn:hover{background:var(--color-surface-3);color:var(--color-text);}
|
|
861
892
|
.fdy-input-group__btn:focus-visible{outline:none;color:var(--color-text);box-shadow:inset 0 0 0 2px var(--color-primary);}
|
|
862
893
|
.fdy-input-group__btn svg{display:block;width:1.1rem;height:1.1rem;}
|
|
894
|
+
/* Leading action button (the "−" of a number stepper): the divider belongs on the other side. */
|
|
895
|
+
.fdy-input-group__btn:first-child{border-left:0;border-right:var(--bw) solid var(--color-border);}
|
|
896
|
+
/* At a bound, or on a disabled/read-only field, the button must look as inert as it acts — same dim
|
|
897
|
+
+ not-allowed as .fdy-input:disabled. :hover still matches a disabled button, so it is withdrawn. */
|
|
898
|
+
.fdy-input-group__btn:disabled{opacity:.5;cursor:not-allowed;}
|
|
899
|
+
.fdy-input-group__btn:disabled:hover{background:var(--color-surface-2);color:var(--color-text-muted);}
|
|
863
900
|
/* Error state must win over the neutralizing rules above — promote to the wrapper. */
|
|
864
901
|
.fdy-input-group:has(.fdy-input--error),.fdy-input-group:has(.fdy-combo--error),.fdy-input-group:has([aria-invalid="true"]){border-color:var(--color-danger);}
|
|
865
902
|
.fdy-input-group:has(.fdy-input--error):focus-within,.fdy-input-group:has(.fdy-combo--error):focus-within,.fdy-input-group:has([aria-invalid="true"]):focus-within{border-color:var(--color-danger);box-shadow:inset 0 1px 2px rgba(16,14,30,.04),0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
@@ -884,6 +921,14 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
884
921
|
.fdy-input[readonly],.fdy-textarea[readonly]{background:var(--color-surface-2);border-color:var(--color-border);color:var(--color-text);cursor:default;}
|
|
885
922
|
.fdy-input[readonly]:hover,.fdy-textarea[readonly]:hover{border-color:var(--color-border);}
|
|
886
923
|
.fdy-input[readonly]:focus-visible,.fdy-textarea[readonly]:focus-visible{border-color:var(--color-border-strong);box-shadow:none;}
|
|
924
|
+
/* Numbers (#44): the user agent draws its own spin buttons inside the field, in OS widget colours no
|
|
925
|
+
theme reaches — on a dark surface they read as a light-grey artefact glued to a themed field, and
|
|
926
|
+
it is the only unthemed control on the page. Removed so .fdy-input looks the same whatever its
|
|
927
|
+
type. `appearance:textfield` is the Firefox half, the pseudo-element the Blink/WebKit half; both
|
|
928
|
+
are needed. This trades away the mouse-only increment — arrow keys on a focused number input still
|
|
929
|
+
work, and [data-fdy-number] gives the affordance back on the kit's terms. */
|
|
930
|
+
.fdy-input[type="number"]{appearance:textfield;-webkit-appearance:textfield;}
|
|
931
|
+
.fdy-input[type="number"]::-webkit-outer-spin-button,.fdy-input[type="number"]::-webkit-inner-spin-button{appearance:none;-webkit-appearance:none;margin:0;}
|
|
887
932
|
.fdy-input--error,.fdy-input[aria-invalid="true"],.fdy-textarea[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
888
933
|
.fdy-input--error:focus,.fdy-input[aria-invalid="true"]:focus,.fdy-textarea[aria-invalid="true"]:focus{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
889
934
|
.fdy-help{font-size:var(--text-xs);color:var(--color-text-muted);}
|
package/dist/freeday.js
CHANGED
|
@@ -2166,6 +2166,111 @@
|
|
|
2166
2166
|
window.FreedayMenu = { init: initMenu, initAll: initAll };
|
|
2167
2167
|
})();
|
|
2168
2168
|
|
|
2169
|
+
/* Freeday — number field enhancer (optional, zero-dependency).
|
|
2170
|
+
* Gives a native <input type="number"> its increment/decrement affordance back, on the kit's terms:
|
|
2171
|
+
* .fdy-input hides the user agent's own spin buttons (unthemeable OS widgets), so this puts two real
|
|
2172
|
+
* buttons in an .fdy-input-group instead. No new block — the group already owns the shared border,
|
|
2173
|
+
* :focus-within ring and error promotion.
|
|
2174
|
+
*
|
|
2175
|
+
* Markup contract:
|
|
2176
|
+
* <div class="fdy-input-group" data-fdy-number>
|
|
2177
|
+
* <button type="button" class="fdy-input-group__btn" data-fdy-number-step="-1"
|
|
2178
|
+
* tabindex="-1" aria-label="Kurangi">−</button>
|
|
2179
|
+
* <input class="fdy-input" type="number" min="0" max="10" step="1" value="1" aria-label="…">
|
|
2180
|
+
* <button type="button" class="fdy-input-group__btn" data-fdy-number-step="1"
|
|
2181
|
+
* tabindex="-1" aria-label="Tambah">+</button>
|
|
2182
|
+
* </div>
|
|
2183
|
+
*
|
|
2184
|
+
* No custom event: the input is the source of truth, so stepping dispatches native bubbling `input`
|
|
2185
|
+
* and `change`. v-model / onChange / @bind bind to the input directly, exactly as they would without
|
|
2186
|
+
* this enhancer.
|
|
2187
|
+
*
|
|
2188
|
+
* The buttons are deliberately OUT of the tab order (tabindex="-1"): the input is already focusable
|
|
2189
|
+
* and ArrowUp/ArrowDown already step it, so two extra tab stops per field would cost every keyboard
|
|
2190
|
+
* user something and buy nothing. They keep an aria-label, so pointer and browse-mode users still
|
|
2191
|
+
* get a named control.
|
|
2192
|
+
*/
|
|
2193
|
+
(function () {
|
|
2194
|
+
'use strict';
|
|
2195
|
+
|
|
2196
|
+
function initNumber(root) {
|
|
2197
|
+
if (root.dataset.fdyNumberReady === '1') return;
|
|
2198
|
+
|
|
2199
|
+
var input = root.querySelector('input[type="number"]');
|
|
2200
|
+
var buttons = Array.prototype.slice.call(root.querySelectorAll('[data-fdy-number-step]'));
|
|
2201
|
+
if (!input || !buttons.length) return;
|
|
2202
|
+
root.dataset.fdyNumberReady = '1';
|
|
2203
|
+
|
|
2204
|
+
/* step="any" means there is no defined increment, and stepUp()/stepDown() throw InvalidStateError
|
|
2205
|
+
on such a field. A stepper cannot express it, so the buttons stay visibly disabled rather than
|
|
2206
|
+
failing on click. Read live, not captured: `step` is one of the attributes watched below. */
|
|
2207
|
+
function stepless() {
|
|
2208
|
+
return (input.getAttribute('step') || '').toLowerCase() === 'any';
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
function frozen() {
|
|
2212
|
+
return stepless() || input.disabled || input.readOnly;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
/* Bounds are read from the input, never recomputed: stepUp()/stepDown() already clamp to
|
|
2216
|
+
min/max/step, so these two only decide whether the button tells the truth about what it does. */
|
|
2217
|
+
function atLimit(direction) {
|
|
2218
|
+
var bound = direction > 0 ? input.max : input.min;
|
|
2219
|
+
if (bound === '' || input.value === '') return false;
|
|
2220
|
+
var value = Number(input.value);
|
|
2221
|
+
var limit = Number(bound);
|
|
2222
|
+
if (isNaN(value) || isNaN(limit)) return false;
|
|
2223
|
+
return direction > 0 ? value >= limit : value <= limit;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
function sync() {
|
|
2227
|
+
buttons.forEach(function (btn) {
|
|
2228
|
+
var direction = Number(btn.getAttribute('data-fdy-number-step')) < 0 ? -1 : 1;
|
|
2229
|
+
btn.disabled = frozen() || atLimit(direction);
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
buttons.forEach(function (btn) {
|
|
2234
|
+
btn.addEventListener('click', function () {
|
|
2235
|
+
if (frozen()) return;
|
|
2236
|
+
if (Number(btn.getAttribute('data-fdy-number-step')) < 0) input.stepDown();
|
|
2237
|
+
else input.stepUp();
|
|
2238
|
+
/* The value changed without the user typing, so say so the way the platform does — frameworks
|
|
2239
|
+
listen to these, not to a kit-specific event. */
|
|
2240
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2241
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2242
|
+
sync();
|
|
2243
|
+
});
|
|
2244
|
+
});
|
|
2245
|
+
|
|
2246
|
+
input.addEventListener('input', sync);
|
|
2247
|
+
input.addEventListener('change', sync);
|
|
2248
|
+
/* Typing is not the only thing that moves a bound: a framework flips `disabled` or recomputes
|
|
2249
|
+
`max` without any event firing, and a button that still looks enabled but no longer does
|
|
2250
|
+
anything is the lie this state machine exists to avoid. */
|
|
2251
|
+
new MutationObserver(sync).observe(input, {
|
|
2252
|
+
attributes: true,
|
|
2253
|
+
attributeFilter: ['disabled', 'readonly', 'min', 'max', 'step'],
|
|
2254
|
+
});
|
|
2255
|
+
sync();
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
function initAll(context) {
|
|
2259
|
+
var root = context || document;
|
|
2260
|
+
/* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
|
|
2261
|
+
if (root.matches && root.matches('[data-fdy-number]')) initNumber(root);
|
|
2262
|
+
Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-number]'), initNumber);
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
if (document.readyState === 'loading') {
|
|
2266
|
+
document.addEventListener('DOMContentLoaded', function () { initAll(); });
|
|
2267
|
+
} else {
|
|
2268
|
+
initAll();
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
window.FreedayNumber = { init: initNumber, initAll: initAll };
|
|
2272
|
+
})();
|
|
2273
|
+
|
|
2169
2274
|
/* Freeday — popover positioning helper (optional, zero-dependency).
|
|
2170
2275
|
* Lifts a dropdown panel into the top layer via the native Popover API so it escapes ANY
|
|
2171
2276
|
* ancestor overflow clip (a .fdy-card with overflow:hidden, an app-shell main with
|
package/docs/getting-started.md
CHANGED
|
@@ -184,7 +184,7 @@ live docs also have a copy button per component.
|
|
|
184
184
|
```bash
|
|
185
185
|
npm i @cahyo-dimas/freeday
|
|
186
186
|
```
|
|
187
|
-
Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.
|
|
187
|
+
Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.28.0"` (public npm package). `dist/` is
|
|
188
188
|
committed and published → no build step; `npm ci` runs without auth.
|
|
189
189
|
|
|
190
190
|
### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cahyo-dimas/freeday",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.0",
|
|
4
4
|
"description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"scripts": {
|
|
72
72
|
"build": "node tokens/build.mjs",
|
|
73
73
|
"test": "node --test",
|
|
74
|
-
"test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs",
|
|
74
|
+
"test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs",
|
|
75
75
|
"prepack": "node tokens/build.mjs",
|
|
76
76
|
"version": "node tokens/build.mjs && git add dist",
|
|
77
77
|
"typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit"
|
|
@@ -45,6 +45,22 @@
|
|
|
45
45
|
.fdy-btn--ghost[aria-pressed="true"],.fdy-btn--text[aria-pressed="true"]{background:var(--color-primary-soft);color:var(--color-primary-strong);border-color:var(--color-primary-border);box-shadow:none;}
|
|
46
46
|
.fdy-btn--ghost[aria-pressed="true"]:hover,.fdy-btn--text[aria-pressed="true"]:hover{background:var(--color-primary-soft);transform:none;box-shadow:none;}
|
|
47
47
|
|
|
48
|
+
/* Stretched hit area (#007) — spread this button's click target over its nearest positioned
|
|
49
|
+
* ancestor, so "one card, one primary action, one escape hatch" needs no click handler on the
|
|
50
|
+
* <article>: the real control stays a real <button>/<a> and keeps its keyboard semantics.
|
|
51
|
+
* `.fdy-card` is already position:relative, so it is the anchor.
|
|
52
|
+
*
|
|
53
|
+
* The transforms MUST go, and this is the whole reason the modifier exists rather than a docs line:
|
|
54
|
+
* a transformed element becomes the containing block for its own absolutely-positioned descendants,
|
|
55
|
+
* so `inset:0` re-anchors from the card to the button's own box the moment the button is hovered or
|
|
56
|
+
* pressed. Measured: mousedown lands on the button, the overlay collapses under the pointer, mouseup
|
|
57
|
+
* lands on some other element, and the browser fires `click` on their common ancestor — the button
|
|
58
|
+
* never gets one. It demos fine on hover for --text/--ghost (their hover is already transform:none)
|
|
59
|
+
* and only a real press reveals it; the default and --danger fills break one step earlier, on hover.
|
|
60
|
+
* Press feedback moves to the card, which is the object actually being pressed (see card.css). */
|
|
61
|
+
.fdy-btn--stretch::after{content:'';position:absolute;inset:0;}
|
|
62
|
+
.fdy-btn--stretch:hover,.fdy-btn--stretch:active{transform:none;}
|
|
63
|
+
|
|
48
64
|
/* Icon-only — square; combine with --ghost/--text/--sm/--lg. Requires aria-label. */
|
|
49
65
|
.fdy-btn--icon{width:var(--control-h);padding:0;}
|
|
50
66
|
.fdy-btn--icon.fdy-btn--sm{width:calc(var(--control-h) - var(--space-2));}
|
package/src/components/card.css
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
.fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
|
|
6
6
|
.fdy-card--interactive{cursor:pointer;}
|
|
7
7
|
.fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
|
|
8
|
+
/* Pressing a stretched target (see .fdy-btn--stretch) is pressing the CARD — the button's own
|
|
9
|
+
nudge had to go, and the object under the pointer is the card. Ordered before the reduced-motion
|
|
10
|
+
block and the disabled rules so both still win at equal specificity. */
|
|
11
|
+
.fdy-card--interactive:has(.fdy-btn--stretch:active){transform:translateY(-1px);}
|
|
8
12
|
/* The global reduced-motion reset kills the ANIMATION; drop the -3px shift too so the card
|
|
9
13
|
* doesn't jump. The shadow still communicates the affordance. */
|
|
10
|
-
@media (prefers-reduced-motion:reduce){.fdy-card--interactive:hover{transform:none;}}
|
|
14
|
+
@media (prefers-reduced-motion:reduce){.fdy-card--interactive:hover,.fdy-card--interactive:has(.fdy-btn--stretch:active){transform:none;}}
|
|
11
15
|
/* When the whole card IS the control — a real <button> so the row is keyboard-reachable and
|
|
12
16
|
* announced — reset the UA button box. Note: it does NOT reset background/border, because
|
|
13
17
|
* .fdy-card already sets its own surface + border (resetting them here would strip the card). */
|
|
@@ -16,6 +20,17 @@
|
|
|
16
20
|
--interactive lift is withdrawn so a refusing control stops answering the pointer. */
|
|
17
21
|
.fdy-card--button:disabled,.fdy-card--button[aria-disabled="true"],.fdy-card--interactive[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
18
22
|
.fdy-card--interactive:disabled:hover,.fdy-card--interactive[aria-disabled="true"]:hover{box-shadow:var(--shadow-lift);transform:none;}
|
|
23
|
+
/* Escape hatches stay clickable above a stretched target — automatic, not a documented convention,
|
|
24
|
+
because forgetting it fails SILENTLY: the secondary control looks and hovers exactly as before and
|
|
25
|
+
simply never receives the click, which is the very symptom the pattern exists to cure. Matched by
|
|
26
|
+
shape (anything focusable) so a control the kit does not ship yet is covered too. */
|
|
27
|
+
.fdy-card:has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch){z-index:1;}
|
|
28
|
+
/* The position is a DEFAULT, not an override: z-index needs a positioned box (or a flex/grid item),
|
|
29
|
+
but an app that deliberately pins a control inside the card — a corner dismiss, a favourite star —
|
|
30
|
+
must keep its own `position:absolute`. Wrapped in :where() so this whole selector weighs nothing
|
|
31
|
+
and ANY app rule wins it; measured, because at normal specificity the kit silently dragged an
|
|
32
|
+
absolutely positioned dismiss button back into the flow. Do not merge these two rules. */
|
|
33
|
+
:where(.fdy-card:has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch)){position:relative;}
|
|
19
34
|
.fdy-card__body{padding:var(--space-5);}
|
|
20
35
|
.fdy-card__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);color:var(--color-text);margin:0 0 var(--space-1);}
|
|
21
36
|
.fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
|
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
.fdy-input-group__btn:hover{background:var(--color-surface-3);color:var(--color-text);}
|
|
18
18
|
.fdy-input-group__btn:focus-visible{outline:none;color:var(--color-text);box-shadow:inset 0 0 0 2px var(--color-primary);}
|
|
19
19
|
.fdy-input-group__btn svg{display:block;width:1.1rem;height:1.1rem;}
|
|
20
|
+
/* Leading action button (the "−" of a number stepper): the divider belongs on the other side. */
|
|
21
|
+
.fdy-input-group__btn:first-child{border-left:0;border-right:var(--bw) solid var(--color-border);}
|
|
22
|
+
/* At a bound, or on a disabled/read-only field, the button must look as inert as it acts — same dim
|
|
23
|
+
+ not-allowed as .fdy-input:disabled. :hover still matches a disabled button, so it is withdrawn. */
|
|
24
|
+
.fdy-input-group__btn:disabled{opacity:.5;cursor:not-allowed;}
|
|
25
|
+
.fdy-input-group__btn:disabled:hover{background:var(--color-surface-2);color:var(--color-text-muted);}
|
|
20
26
|
/* Error state must win over the neutralizing rules above — promote to the wrapper. */
|
|
21
27
|
.fdy-input-group:has(.fdy-input--error),.fdy-input-group:has(.fdy-combo--error),.fdy-input-group:has([aria-invalid="true"]){border-color:var(--color-danger);}
|
|
22
28
|
.fdy-input-group:has(.fdy-input--error):focus-within,.fdy-input-group:has(.fdy-combo--error):focus-within,.fdy-input-group:has([aria-invalid="true"]):focus-within{border-color:var(--color-danger);box-shadow:inset 0 1px 2px rgba(16,14,30,.04),0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
package/src/components/input.css
CHANGED
|
@@ -18,6 +18,14 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
18
18
|
.fdy-input[readonly],.fdy-textarea[readonly]{background:var(--color-surface-2);border-color:var(--color-border);color:var(--color-text);cursor:default;}
|
|
19
19
|
.fdy-input[readonly]:hover,.fdy-textarea[readonly]:hover{border-color:var(--color-border);}
|
|
20
20
|
.fdy-input[readonly]:focus-visible,.fdy-textarea[readonly]:focus-visible{border-color:var(--color-border-strong);box-shadow:none;}
|
|
21
|
+
/* Numbers (#44): the user agent draws its own spin buttons inside the field, in OS widget colours no
|
|
22
|
+
theme reaches — on a dark surface they read as a light-grey artefact glued to a themed field, and
|
|
23
|
+
it is the only unthemed control on the page. Removed so .fdy-input looks the same whatever its
|
|
24
|
+
type. `appearance:textfield` is the Firefox half, the pseudo-element the Blink/WebKit half; both
|
|
25
|
+
are needed. This trades away the mouse-only increment — arrow keys on a focused number input still
|
|
26
|
+
work, and [data-fdy-number] gives the affordance back on the kit's terms. */
|
|
27
|
+
.fdy-input[type="number"]{appearance:textfield;-webkit-appearance:textfield;}
|
|
28
|
+
.fdy-input[type="number"]::-webkit-outer-spin-button,.fdy-input[type="number"]::-webkit-inner-spin-button{appearance:none;-webkit-appearance:none;margin:0;}
|
|
21
29
|
.fdy-input--error,.fdy-input[aria-invalid="true"],.fdy-textarea[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
22
30
|
.fdy-input--error:focus,.fdy-input[aria-invalid="true"]:focus,.fdy-textarea[aria-invalid="true"]:focus{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
23
31
|
.fdy-help{font-size:var(--text-xs);color:var(--color-text-muted);}
|