@cplieger/ui 0.0.0 → 1.0.0-dev.1
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/NOTICE +3 -0
- package/README.md +145 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/css/ui-primitives.css +344 -0
- package/jsr.json +36 -4
- package/package.json +86 -4
- package/src/actions/api.ts +268 -0
- package/src/actions/async-feedback.ts +217 -0
- package/src/actions/cleanup.ts +66 -0
- package/src/actions/debounce.ts +150 -0
- package/src/actions/define-helpers.ts +87 -0
- package/src/actions/define.ts +851 -0
- package/src/actions/error.ts +161 -0
- package/src/actions/index.ts +60 -0
- package/src/actions/loading.ts +131 -0
- package/src/actions/notifier.ts +61 -0
- package/src/actions/poll-until.ts +133 -0
- package/src/actions/poll.ts +156 -0
- package/src/actions/registry.ts +259 -0
- package/src/actions/retry.ts +79 -0
- package/src/actions/testing.ts +29 -0
- package/src/actions/transport.ts +104 -0
- package/src/actions/types.ts +249 -0
- package/src/dom/bind-list.ts +103 -0
- package/src/dom/el.ts +67 -0
- package/src/dom/index.ts +9 -0
- package/src/dom/reconcile-tree.ts +268 -0
- package/src/dom/reconcile.ts +76 -0
- package/src/fetch/index.ts +18 -0
- package/src/fetch/instance.ts +37 -0
- package/src/fetch/request.ts +437 -0
- package/src/fetch/timeout.ts +67 -0
- package/src/fetch/types.ts +206 -0
- package/src/fetch/verbs.ts +103 -0
- package/src/index.ts +4 -8
- package/src/primitives/announce.ts +134 -0
- package/src/primitives/ask.ts +406 -0
- package/src/primitives/dialog-engine.ts +500 -0
- package/src/primitives/dialog.ts +263 -0
- package/src/primitives/disclosure.ts +287 -0
- package/src/primitives/focus-trap.ts +157 -0
- package/src/primitives/fx.ts +204 -0
- package/src/primitives/live-regions.ts +83 -0
- package/src/primitives/modal-host.ts +214 -0
- package/src/primitives/modal.ts +247 -0
- package/src/primitives/owner.ts +322 -0
- package/src/primitives/popover.ts +500 -0
- package/src/primitives/popup-core.ts +603 -0
- package/src/primitives/popup.ts +23 -0
- package/src/primitives/roving-focus.ts +198 -0
- package/src/primitives/scroll-lock.ts +119 -0
- package/src/primitives/skeleton.ts +142 -0
- package/src/primitives/theme.ts +202 -0
- package/src/primitives/toast/engine.ts +327 -0
- package/src/primitives/toast/index.ts +139 -0
- package/src/primitives/toast/view.ts +244 -0
- package/src/primitives/tooltip.ts +377 -0
- package/src/primitives/transition.ts +119 -0
- package/src/primitives/view-transition.ts +57 -0
- package/src/signal/bus.ts +126 -0
- package/src/signal/collection.ts +182 -0
- package/src/signal/index.ts +34 -0
- package/src/signal/resource.ts +102 -0
- package/src/signal/signal-map.ts +46 -0
- package/src/signal/signal.ts +1002 -0
- package/src/signal/store.ts +84 -0
package/NOTICE
ADDED
package/README.md
CHANGED
|
@@ -1,8 +1,149 @@
|
|
|
1
1
|
# ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@cplieger/ui) [](https://jsr.io/@cplieger/ui) [](https://github.com/cplieger/ui/issues?q=label%3Astryker-tracker)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@cplieger/ui` lets you build a TypeScript browser UI without a framework, from signals, accessible widgets, typed JSON requests and server actions.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
It replaces the change listeners, focus handling, `try`/`catch` blocks and loading flags you would otherwise write by hand. It has no runtime dependencies and ships as ESM-only TypeScript source, so it needs a bundler that compiles TypeScript 6 or later. The widgets need Chrome or Edge 132, Firefox 133 or Safari 26 or later. It is licensed under Apache-2.0.
|
|
10
|
+
|
|
11
|
+
## Why use it
|
|
12
|
+
|
|
13
|
+
`@cplieger/ui` is built for apps that draw their interface with plain DOM code and keep their own look.
|
|
14
|
+
|
|
15
|
+
- A signal write runs every effect that depends on it before it returns, and every subpath shares that one signal graph.
|
|
16
|
+
- `bindList` repaints only the row of a changed item, and `el` builds elements without HTML strings.
|
|
17
|
+
- The 15 widgets ship behavior and ARIA wiring with no colors or fonts, styled through `--uip-*` properties and `.uip-*` classes.
|
|
18
|
+
- Requests never throw, and actions add optimistic updates, rollback and retry for failures you mark as transient.
|
|
19
|
+
- A bundler drops every subpath you do not import.
|
|
20
|
+
|
|
21
|
+
Consider [Solid](https://github.com/solidjs/solid) if you want components, JSX and server-side rendering. Consider [TanStack Query](https://tanstack.com/query/latest) if you also need a server-state cache.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npx jsr add @cplieger/ui
|
|
27
|
+
# or
|
|
28
|
+
npm i @cplieger/ui
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
A signal holds a value, and an effect re-runs when what it read changes. `el` builds the elements:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { effect, el, signal } from "@cplieger/ui";
|
|
37
|
+
|
|
38
|
+
const count = signal(0);
|
|
39
|
+
const label = el("output");
|
|
40
|
+
const button = el("button", { onclick: () => count.value++ }, "Add one");
|
|
41
|
+
document.body.append(button, label);
|
|
42
|
+
|
|
43
|
+
effect(() => {
|
|
44
|
+
label.textContent = `Clicked ${count.value} times`;
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Connect the toasts to the actions once at startup, then define each action at module scope and dispatch it from your event handlers:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { apiAction, configure, retryNetwork } from "@cplieger/ui/actions";
|
|
52
|
+
import { error, success } from "@cplieger/ui/toast";
|
|
53
|
+
import "@cplieger/ui/css";
|
|
54
|
+
|
|
55
|
+
configure({ success, error });
|
|
56
|
+
|
|
57
|
+
const deleteItem = apiAction<string>({
|
|
58
|
+
name: "items.delete",
|
|
59
|
+
request: (id) => ({ method: "DELETE", path: `/api/items/${id}` }),
|
|
60
|
+
error: "Could not delete the item",
|
|
61
|
+
retryable: retryNetwork,
|
|
62
|
+
retry: { count: 2, delay: 300 },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await deleteItem.dispatch("item-1"); // the result, or null after the error toast
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
To call a JSON API directly, create one instance per backend and branch on the result:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { createFetch } from "@cplieger/ui/fetch";
|
|
72
|
+
|
|
73
|
+
const api = createFetch({ baseUrl: "https://api.example.com/v1" });
|
|
74
|
+
|
|
75
|
+
const res = await api.apiGetRaw<{ name: string }>("/users/me");
|
|
76
|
+
if (res.ok) console.log(res.data.name);
|
|
77
|
+
else console.error(res.status, res.code, res.error);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Subpaths
|
|
81
|
+
|
|
82
|
+
Import each part from its own subpath. The last column names the package each one came from, with the same exports.
|
|
83
|
+
|
|
84
|
+
| Import from | What it holds | Earlier package |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| `@cplieger/ui` | everything in `/signal` and `/dom` | `@cplieger/reactive` |
|
|
87
|
+
| `@cplieger/ui/signal` | signals, computed values, effects, owners, resources, stores, collections and the event bus | `@cplieger/reactive` |
|
|
88
|
+
| `@cplieger/ui/dom` | `el`, `reconcile`, `bindList`, `patch` and `trackHandler` | `@cplieger/reactive` |
|
|
89
|
+
| `@cplieger/ui/fetch` | typed JSON requests | `@cplieger/fetch` |
|
|
90
|
+
| `@cplieger/ui/actions` | server actions | `@cplieger/actions` |
|
|
91
|
+
| `@cplieger/ui/actions/testing` | `resetActionFramework` for tests | `@cplieger/actions/testing` |
|
|
92
|
+
| `@cplieger/ui/<widget>` | one widget, such as `@cplieger/ui/dialog` | `@cplieger/ui-primitives/<widget>` |
|
|
93
|
+
| `@cplieger/ui/css` | the base stylesheet, on npm only | `@cplieger/ui-primitives/css` |
|
|
94
|
+
|
|
95
|
+
The widget subpaths are `toast`, `tooltip`, `popover`, `popup`, `dialog`, `modal`, `ask`, `disclosure`, `focus-trap`, `roving-focus`, `skeleton`, `theme`, `view-transition`, `transition` and `announce`. Every name the `@cplieger/ui-primitives` root exported is in the widget subpath of the same topic.
|
|
96
|
+
|
|
97
|
+
Eight widget subpaths start watching the `open` attribute of dialogs when they load, so they need a DOM at import: `dialog`, `modal`, `ask`, `announce`, `toast`, `popup`, `popover` and `tooltip`. The package root and every other subpath touch no DOM at import, so they also run under Node, Deno, Bun or Workers. `package.json` marks the watching modules as side effects, so `import "@cplieger/ui/dialog"` keeps its watcher in a bundle.
|
|
98
|
+
|
|
99
|
+
## API
|
|
100
|
+
|
|
101
|
+
- Signals, from `/signal` or the root: `signal`, `computed`, `effect`, `batch`, `untracked`, `touch`, `on`, `subscribe`, `isSignal`, `isComputed`, `setEffectErrorHandler`, `createRoot`, `runWithOwner` and `resource`.
|
|
102
|
+
- State, from `/signal` or the root: `createStore`, `SignalMap`, `createCollection` and `createBus`.
|
|
103
|
+
- DOM, from `/dom` or the root: `el`, `reconcile`, `KEY_ATTR`, `bindList`, `patch` and `trackHandler`.
|
|
104
|
+
- Requests, from `/fetch`: `createFetch` with its `apiGet`, `apiPost`, `apiPut`, `apiPatch` and `apiDelete` helpers, `withTimeout` and `API_TIMEOUT_MS`.
|
|
105
|
+
- Actions, from `/actions`: `configure`, `configureApi`, `configureTransport`, `defineAction`, `apiAction`, `transportAction`, the helpers `debouncedDispatch`, `pollAction`, `pollUntil`, `bindLoadingState`, `withAsyncFeedback` and `registerCleanup`, and the state reads `isPending`, `pendingCount`, `subscribeToActions`, `subscribeByName` and `getActionLog`.
|
|
106
|
+
- Widgets, one subpath each: [toast](docs/toast.md), [announce](docs/announce.md), [tooltip](docs/tooltip.md), [popover](docs/popover.md), [popup](docs/popup.md), [dialog](docs/dialog.md), [modal](docs/modal.md), [ask](docs/ask.md), [focus-trap](docs/focus-trap.md), [roving-focus](docs/roving-focus.md), [disclosure](docs/disclosure.md), [theme](docs/theme.md), [view-transition](docs/view-transition.md), [transition](docs/transition.md) and [skeleton](docs/skeleton.md).
|
|
107
|
+
|
|
108
|
+
The full reference, generated from the doc comments, is on [JSR](https://jsr.io/@cplieger/ui/doc).
|
|
109
|
+
|
|
110
|
+
## A write runs its effects before it returns
|
|
111
|
+
|
|
112
|
+
When `count.value = 1` returns, every effect that depends on `count` has already run, so the next line reads the DOM those effects wrote. There is no queue and no pending state to wait for.
|
|
113
|
+
|
|
114
|
+
`batch(fn)` is the only way to defer. Writes inside it coalesce, and the held effects run once, synchronously, when the outermost batch returns, even when the body throws. To run effects on a later task, make the write on a later task. [Signals, stores and collections](docs/signals.md) has the full rules.
|
|
115
|
+
|
|
116
|
+
## Server text is untrusted
|
|
117
|
+
|
|
118
|
+
- With an absolute `baseUrl`, a request path cannot reach another origin. Without one, the path goes to `fetch()` unchanged, so never build a whole path from untrusted text.
|
|
119
|
+
- An error message can carry text from the server's response. Your `configure` adapter renders it as text, never as HTML. The toasts do that.
|
|
120
|
+
- A failed response's `body` and `code` come from the server. Check their shape and the `status` before you act on them.
|
|
121
|
+
- Set `sensitive: true` on an action whose arguments or result carry a secret, a token or personal data, so the action log keeps none of them.
|
|
122
|
+
|
|
123
|
+
[Base URLs and untrusted responses](docs/security-model.md) and [Sensitive actions](docs/lifecycle.md#sensitive-actions) cover each rule.
|
|
124
|
+
|
|
125
|
+
## Unsupported by design
|
|
126
|
+
|
|
127
|
+
- The signal and DOM modules leave out ownership outside a root, an `onMount` lifecycle, server-side rendering, Suspense, a custom scheduler, a flush barrier such as `flushSync`, and controlled form inputs. [What the signal and DOM modules leave out](docs/signal-non-goals.md) gives the reasons.
|
|
128
|
+
- The fetch module leaves out retries, interceptor chains, response caching, global settings and streamed responses. [What the fetch module leaves out](docs/fetch-non-goals.md) gives the reasons.
|
|
129
|
+
- The actions module leaves out a query cache, framework adapters, a throttle helper and batch dispatch. [What the actions module leaves out](docs/actions-non-goals.md) gives the reasons.
|
|
130
|
+
|
|
131
|
+
## Documentation
|
|
132
|
+
|
|
133
|
+
- [Signals, stores and collections](docs/signals.md) and [Building and updating the DOM](docs/dom.md) give the exact rules of `/signal` and `/dom`.
|
|
134
|
+
- [Results and errors](docs/results.md), [Requests, timeouts and runtimes](docs/requests.md) and [Base URLs and untrusted responses](docs/security-model.md) cover `/fetch`.
|
|
135
|
+
- [Dispatch lifecycle](docs/lifecycle.md), [HTTP and streaming actions](docs/http.md) and [UI and polling helpers](docs/helpers.md) cover `/actions`.
|
|
136
|
+
- [CSS contract](docs/css.md) covers the base stylesheet and the names you style. Each widget's page is linked from the [API](#api) list.
|
|
137
|
+
|
|
138
|
+
## Credits
|
|
139
|
+
|
|
140
|
+
- The signal engine follows the design of [@preact/signals-core](https://github.com/preactjs/signals) in its dependency graph, its glitch-free computed refresh, its run-on-write effects and its untracked `subscribe` callback.
|
|
141
|
+
- The `on()` helper follows [Solid](https://github.com/solidjs/solid)'s `on()`, and `untracked()` is Solid's `untrack()` under the name Preact gives it. `createRoot`, `runWithOwner` and owned nested effects follow Solid's ownership model.
|
|
142
|
+
- `onSuccess`, `onError` and `onSettled` follow TanStack Query's mutation callbacks, and `configureApi` follows RTK Query's `fetchBaseQuery`.
|
|
143
|
+
|
|
144
|
+
## Contributing
|
|
145
|
+
|
|
146
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
6
147
|
|
|
7
148
|
## Disclaimer
|
|
8
149
|
|
|
@@ -13,3 +154,5 @@ This project was built with AI-assisted tooling using [Claude](https://claude.co
|
|
|
13
154
|
## License
|
|
14
155
|
|
|
15
156
|
Apache-2.0. See [LICENSE](LICENSE).
|
|
157
|
+
|
|
158
|
+
Third-party attributions are in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
No third-party code is included in this repository. The signal engine is a re-derivation of an upstream design, not a copy of its source, so no license text is reproduced here.
|
|
4
|
+
|
|
5
|
+
Two designs are followed:
|
|
6
|
+
|
|
7
|
+
- The dependency graph and the scheduler in `src/signal/signal.ts` follow [@preact/signals-core](https://github.com/preactjs/signals) (MIT): the doubly-linked source and target edge nodes (`DepNode`, `SourceNode` and `TargetNode`), the pull-based glitch-free `refreshComputed`, the eager drain in `endBatch`, and the untracked `subscribe` callback (upstream `preactjs/signals#188`). The baseline is `@preact/signals-core@1.14.4`. `CONTRIBUTING.md` lists every deliberate delta from it and the upstream machinery this engine does not carry.
|
|
8
|
+
- The explicit dependency helper `on()` in `src/signal/signal.ts` follows [solid-js](https://github.com/solidjs/solid) (MIT), whose `on()` has the same deps-plus-body shape. `untracked()` is Solid's `untrack()` under the name Preact gives it.
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/* ui-primitives.css — structural + behavioral base for @cplieger/ui.
|
|
2
|
+
*
|
|
3
|
+
* Ships motion + layout only. Colors, radii, fonts, borders, shadows are the
|
|
4
|
+
* consuming app's SKIN: target the .uip-* classes directly in your own CSS, or
|
|
5
|
+
* override the --uip-* custom properties below. Every class and property here
|
|
6
|
+
* is documented in the README contract tables.
|
|
7
|
+
*
|
|
8
|
+
* State classes (is-entering / is-shown / is-leaving, is-destructive) are
|
|
9
|
+
* toggled by the library at runtime; style them to define your skin's motion
|
|
10
|
+
* and emphasis. */
|
|
11
|
+
|
|
12
|
+
/* Structural + behavioral defaults. Override any of these in your own :root.
|
|
13
|
+
* Motion is fully parameterized: every transition/animation exposes BOTH a
|
|
14
|
+
* -duration and a paired -easing (timing function) custom property, each used
|
|
15
|
+
* with an inline fallback. Defaults reproduce the built-in motion exactly. */
|
|
16
|
+
:root {
|
|
17
|
+
--uip-z-toast: 9999;
|
|
18
|
+
--uip-z-tooltip: 10000;
|
|
19
|
+
--uip-toast-offset: 1rem;
|
|
20
|
+
--uip-toast-gap: 0.5rem;
|
|
21
|
+
--uip-toast-row-gap: 0.5rem;
|
|
22
|
+
--uip-toast-max-width: 24rem;
|
|
23
|
+
--uip-toast-enter-duration: 250ms;
|
|
24
|
+
--uip-toast-enter-easing: ease;
|
|
25
|
+
--uip-toast-leave-duration: 150ms;
|
|
26
|
+
--uip-toast-leave-easing: ease;
|
|
27
|
+
--uip-toast-duration: 4000ms;
|
|
28
|
+
--uip-toast-easing: linear;
|
|
29
|
+
--uip-toast-progress-size: 2px;
|
|
30
|
+
--uip-toast-progress-color: currentcolor;
|
|
31
|
+
--uip-tooltip-fade-duration: 100ms;
|
|
32
|
+
--uip-tooltip-fade-easing: ease;
|
|
33
|
+
--uip-dialog-leave-duration: 150ms;
|
|
34
|
+
--uip-dialog-leave-easing: ease;
|
|
35
|
+
--uip-backdrop: oklch(0% 0 0deg / 50%);
|
|
36
|
+
|
|
37
|
+
/* Modal (native <dialog>). The platform top layer owns stacking (no z-index)
|
|
38
|
+
* and ::backdrop is the dim overlay (reuses --uip-backdrop by default). Fades
|
|
39
|
+
* out on leave like dialog/ask; enter is instant (matching the family). */
|
|
40
|
+
--uip-modal-backdrop: var(--uip-backdrop);
|
|
41
|
+
--uip-modal-leave-duration: 150ms;
|
|
42
|
+
--uip-modal-leave-easing: ease;
|
|
43
|
+
|
|
44
|
+
/* Disclosure (animated collapsible). */
|
|
45
|
+
--uip-disclosure-duration: 200ms;
|
|
46
|
+
--uip-disclosure-easing: ease;
|
|
47
|
+
|
|
48
|
+
/* Popover (anchored floating panel). z-index orders it below toast (9999) /
|
|
49
|
+
* tooltip (10000) in the base layer. A modal is a native <dialog> in the top
|
|
50
|
+
* layer (above every base-layer z-index), so a DISCONNECTED panel opened from
|
|
51
|
+
* within a modal is hosted INTO that <dialog> (the trigger's nearest open
|
|
52
|
+
* dialog, else the topmost open one) to sit over it; a caller-connected panel
|
|
53
|
+
* stays where the caller put it — connect it inside the dialog when opening
|
|
54
|
+
* from a modal. */
|
|
55
|
+
--uip-z-popover: 1100;
|
|
56
|
+
--uip-popover-enter-duration: 100ms;
|
|
57
|
+
--uip-popover-enter-easing: ease;
|
|
58
|
+
--uip-popover-leave-duration: 100ms;
|
|
59
|
+
--uip-popover-leave-easing: ease;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ===== Toast ============================================================= */
|
|
63
|
+
|
|
64
|
+
.uip-toast-stack {
|
|
65
|
+
position: fixed;
|
|
66
|
+
z-index: var(--uip-z-toast, 9999);
|
|
67
|
+
inset-block-start: var(--uip-toast-offset, 1rem);
|
|
68
|
+
inset-inline-end: var(--uip-toast-offset, 1rem);
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
gap: var(--uip-toast-gap, 0.5rem);
|
|
72
|
+
max-inline-size: var(--uip-toast-max-width, 24rem);
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Each toast owns pointer events; the stack itself lets clicks fall through the
|
|
77
|
+
* gaps to the app beneath.
|
|
78
|
+
*
|
|
79
|
+
* A COLUMN, because the retry button is a sibling of the message span: with no
|
|
80
|
+
* layout here it flows inline and lands wherever the wrapped message's last line
|
|
81
|
+
* ends, which moves with every message length. The visually-hidden dismiss hint
|
|
82
|
+
* and the progress bar are both out of flow, so the column holds exactly the
|
|
83
|
+
* message and the action row. */
|
|
84
|
+
.uip-toast {
|
|
85
|
+
position: relative;
|
|
86
|
+
display: flex;
|
|
87
|
+
flex-direction: column;
|
|
88
|
+
gap: var(--uip-toast-row-gap, 0.5rem);
|
|
89
|
+
overflow: hidden;
|
|
90
|
+
pointer-events: auto;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
opacity: 0;
|
|
93
|
+
transform: translateY(-8px);
|
|
94
|
+
transition:
|
|
95
|
+
opacity var(--uip-toast-enter-duration, 250ms) var(--uip-toast-enter-easing, ease),
|
|
96
|
+
transform var(--uip-toast-enter-duration, 250ms) var(--uip-toast-enter-easing, ease);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.uip-toast.is-shown {
|
|
100
|
+
opacity: 1;
|
|
101
|
+
transform: translateY(0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.uip-toast.is-leaving {
|
|
105
|
+
opacity: 0;
|
|
106
|
+
transform: translateY(-8px);
|
|
107
|
+
transition:
|
|
108
|
+
opacity var(--uip-toast-leave-duration, 150ms) var(--uip-toast-leave-easing, ease),
|
|
109
|
+
transform var(--uip-toast-leave-duration, 150ms) var(--uip-toast-leave-easing, ease);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* The action shrinks to its label and sits at the inline END of its own row,
|
|
113
|
+
* rather than stretching to the column's width like the message above it. */
|
|
114
|
+
.uip-toast-retry {
|
|
115
|
+
align-self: flex-end;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* The progress bar is absolutely positioned and comes LAST in the DOM, so it
|
|
119
|
+
* paints over both of these. A skin with little block padding would have the bar
|
|
120
|
+
* crossing the message's descenders or the action's lower border, so the content
|
|
121
|
+
* takes the higher layer here rather than in every consumer's skin. */
|
|
122
|
+
.uip-toast-msg,
|
|
123
|
+
.uip-toast-retry {
|
|
124
|
+
position: relative;
|
|
125
|
+
z-index: 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Countdown bar. Its duration comes from the --uip-toast-duration custom
|
|
129
|
+
* property the library sets inline per toast (do not set it here per-toast).
|
|
130
|
+
* Pause/resume is driven by animation-play-state, set inline by the library. */
|
|
131
|
+
.uip-toast-progress {
|
|
132
|
+
position: absolute;
|
|
133
|
+
inset-inline: 0;
|
|
134
|
+
inset-block-end: 0;
|
|
135
|
+
block-size: var(--uip-toast-progress-size, 2px);
|
|
136
|
+
background: var(--uip-toast-progress-color, currentcolor);
|
|
137
|
+
transform-origin: left center;
|
|
138
|
+
animation: uip-toast-progress var(--uip-toast-duration, 4000ms) var(--uip-toast-easing, linear)
|
|
139
|
+
both;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@keyframes uip-toast-progress {
|
|
143
|
+
from {
|
|
144
|
+
transform: scaleX(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
to {
|
|
148
|
+
transform: scaleX(0);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ===== Tooltip =========================================================== */
|
|
153
|
+
|
|
154
|
+
/* position:fixed; the library sets inset-block-start / inset-inline-start
|
|
155
|
+
* inline from a measured, viewport-clamped position (flips below when there is
|
|
156
|
+
* no room above). */
|
|
157
|
+
.uip-tooltip {
|
|
158
|
+
position: fixed;
|
|
159
|
+
z-index: var(--uip-z-tooltip, 10000);
|
|
160
|
+
inset-block-start: 0;
|
|
161
|
+
inset-inline-start: 0;
|
|
162
|
+
pointer-events: none;
|
|
163
|
+
opacity: 1;
|
|
164
|
+
transition: opacity var(--uip-tooltip-fade-duration, 100ms) var(--uip-tooltip-fade-easing, ease);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.uip-tooltip.is-leaving {
|
|
168
|
+
opacity: 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* ===== Dialog + Ask ====================================================== */
|
|
172
|
+
|
|
173
|
+
/* ask is the Promise-shaped question dialog (boolean, or input-collecting
|
|
174
|
+
* with the .uip-ask--input modifier); it shares the exact same dialog-family
|
|
175
|
+
* motion (the --uip-dialog-* properties). */
|
|
176
|
+
.uip-ask,
|
|
177
|
+
.uip-dialog {
|
|
178
|
+
opacity: 1;
|
|
179
|
+
transition: opacity var(--uip-dialog-leave-duration, 150ms) var(--uip-dialog-leave-easing, ease);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.uip-ask.is-leaving,
|
|
183
|
+
.uip-dialog.is-leaving {
|
|
184
|
+
opacity: 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.uip-ask::backdrop,
|
|
188
|
+
.uip-dialog::backdrop {
|
|
189
|
+
background: var(--uip-backdrop, oklch(0% 0 0deg / 50%));
|
|
190
|
+
opacity: 1;
|
|
191
|
+
transition: opacity var(--uip-dialog-leave-duration, 150ms) var(--uip-dialog-leave-easing, ease);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.uip-ask.is-leaving::backdrop,
|
|
195
|
+
.uip-dialog.is-leaving::backdrop {
|
|
196
|
+
opacity: 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* ===== Modal (native <dialog>) ========================================== */
|
|
200
|
+
|
|
201
|
+
/* Built on the platform <dialog>: the top layer, ::backdrop, focus containment,
|
|
202
|
+
* and background inerting all come from the browser (no z-index, no overlay
|
|
203
|
+
* div). Auto-margined to center in the viewport, which also sidesteps the Safari
|
|
204
|
+
* <dialog> height bug the old overlay grid-centering worked around. Like
|
|
205
|
+
* dialog/ask it fades out on leave — the library adds `is-leaving` and waits
|
|
206
|
+
* for its transitionend before close() — so exit works without display/overlay
|
|
207
|
+
* allow-discrete; enter is instant, matching the rest of the <dialog> family.
|
|
208
|
+
* The inner content keeps the `.uip-modal-dialog` class as a skinning hook. */
|
|
209
|
+
.uip-modal {
|
|
210
|
+
margin: auto;
|
|
211
|
+
opacity: 1;
|
|
212
|
+
transition: opacity var(--uip-modal-leave-duration, 150ms) var(--uip-modal-leave-easing, ease);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.uip-modal.is-leaving {
|
|
216
|
+
opacity: 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.uip-modal::backdrop {
|
|
220
|
+
background: var(--uip-modal-backdrop, oklch(0% 0 0deg / 50%));
|
|
221
|
+
opacity: 1;
|
|
222
|
+
transition: opacity var(--uip-modal-leave-duration, 150ms) var(--uip-modal-leave-easing, ease);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.uip-modal.is-leaving::backdrop {
|
|
226
|
+
opacity: 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* ===== Disclosure (animated collapsible) ================================= */
|
|
230
|
+
|
|
231
|
+
/* interpolate-size lets height transition to/from the `auto` keyword on modern
|
|
232
|
+
* engines; the library falls back to a measured scrollHeight px target where it
|
|
233
|
+
* is unsupported. The library drives the height inline; this owns the clip and
|
|
234
|
+
* the transition. */
|
|
235
|
+
.uip-disclosure-region {
|
|
236
|
+
overflow: hidden;
|
|
237
|
+
interpolate-size: allow-keywords;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
241
|
+
.uip-disclosure-region {
|
|
242
|
+
transition: height var(--uip-disclosure-duration, 200ms) var(--uip-disclosure-easing, ease);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* ===== Popover (anchored floating panel) ================================= */
|
|
247
|
+
|
|
248
|
+
/* position:fixed; the library sets left / top (and, in full-bleed `stretch:
|
|
249
|
+
* "viewport"` mode, left + right) inline from placeAnchored's measured, flipped,
|
|
250
|
+
* clamped position. Structural only — the app skins background / border /
|
|
251
|
+
* radius / shadow by targeting .uip-popover. */
|
|
252
|
+
.uip-popover {
|
|
253
|
+
position: fixed;
|
|
254
|
+
z-index: var(--uip-z-popover, 1100);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.uip-popover[hidden] {
|
|
258
|
+
display: none;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* Optional skinnable enter fade — plays once when the panel opens. */
|
|
262
|
+
.uip-popover.is-open {
|
|
263
|
+
animation: uip-popover-enter var(--uip-popover-enter-duration, 100ms)
|
|
264
|
+
var(--uip-popover-enter-easing, ease);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* Leave fade — the controller adds is-leaving and waits for this transition (or
|
|
268
|
+
* a fallback timeout) before it sets [hidden], mirroring dialog/modal/toast so
|
|
269
|
+
* the panel animates out instead of vanishing on dismiss. */
|
|
270
|
+
.uip-popover.is-leaving {
|
|
271
|
+
opacity: 0;
|
|
272
|
+
transition: opacity var(--uip-popover-leave-duration, 100ms) var(--uip-popover-leave-easing, ease);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* is-stretched marks the full-bleed (stretch: "viewport") variant. The inset is
|
|
276
|
+
* applied inline, so this is purely a skin hook (e.g. square the top corners /
|
|
277
|
+
* drop side borders on the full-width panel); it needs no structural rule. */
|
|
278
|
+
|
|
279
|
+
@keyframes uip-popover-enter {
|
|
280
|
+
from {
|
|
281
|
+
opacity: 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
to {
|
|
285
|
+
opacity: 1;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* ===== Popup (reveal + light-dismiss, no placement) ====================== */
|
|
290
|
+
|
|
291
|
+
/* The popup primitive is fully headless about motion: the library only toggles
|
|
292
|
+
* the state classes (is-open on reveal, is-leaving before [hidden]) and owns
|
|
293
|
+
* the [hidden] display rule. The app supplies any enter/exit transition on its
|
|
294
|
+
* own classes or on .uip-popup.is-open / .is-leaving; the leave finalizes on
|
|
295
|
+
* the panel's first transitionend (or a 400ms fallback when there is none). */
|
|
296
|
+
.uip-popup[hidden] {
|
|
297
|
+
display: none;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* ===== Visually hidden (live regions, collapsed labels) ================= */
|
|
301
|
+
|
|
302
|
+
/* Standard visually-hidden ("sr-only"): removed from the visual layout but
|
|
303
|
+
* kept in the accessibility tree, which `display: none` would remove it from. */
|
|
304
|
+
.uip-visually-hidden {
|
|
305
|
+
position: absolute;
|
|
306
|
+
overflow: hidden;
|
|
307
|
+
clip-path: inset(50%);
|
|
308
|
+
width: 1px;
|
|
309
|
+
height: 1px;
|
|
310
|
+
margin: -1px;
|
|
311
|
+
padding: 0;
|
|
312
|
+
white-space: nowrap;
|
|
313
|
+
border: 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* ===== Reduced motion ==================================================== */
|
|
317
|
+
|
|
318
|
+
/* Neutralize motion to near-zero rather than 0 so transitionend / animationend
|
|
319
|
+
* still fire and the library's leave-lifecycles complete promptly. The 0.01ms
|
|
320
|
+
* values are below the time-min-milliseconds threshold by design, hence the
|
|
321
|
+
* scoped disables. */
|
|
322
|
+
@media (prefers-reduced-motion: reduce) {
|
|
323
|
+
.uip-toast,
|
|
324
|
+
.uip-toast.is-leaving,
|
|
325
|
+
.uip-tooltip,
|
|
326
|
+
.uip-ask,
|
|
327
|
+
.uip-dialog,
|
|
328
|
+
.uip-modal,
|
|
329
|
+
.uip-ask::backdrop,
|
|
330
|
+
.uip-dialog::backdrop,
|
|
331
|
+
.uip-modal::backdrop,
|
|
332
|
+
.uip-modal.is-leaving,
|
|
333
|
+
.uip-modal.is-leaving::backdrop,
|
|
334
|
+
.uip-popover.is-leaving {
|
|
335
|
+
/* stylelint-disable-next-line time-min-milliseconds -- sub-10ms keeps transitionend firing so JS leave-lifecycles still complete under reduced motion */
|
|
336
|
+
transition-duration: 0.01ms;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.uip-toast-progress,
|
|
340
|
+
.uip-popover.is-open {
|
|
341
|
+
/* stylelint-disable-next-line time-min-milliseconds -- sub-10ms keeps animationend firing under reduced motion */
|
|
342
|
+
animation-duration: 0.01ms;
|
|
343
|
+
}
|
|
344
|
+
}
|
package/jsr.json
CHANGED
|
@@ -1,12 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "1.0.0-dev.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"exports": {
|
|
6
|
-
".": "./src/index.ts"
|
|
6
|
+
".": "./src/index.ts",
|
|
7
|
+
"./signal": "./src/signal/index.ts",
|
|
8
|
+
"./dom": "./src/dom/index.ts",
|
|
9
|
+
"./fetch": "./src/fetch/index.ts",
|
|
10
|
+
"./actions": "./src/actions/index.ts",
|
|
11
|
+
"./actions/testing": "./src/actions/testing.ts",
|
|
12
|
+
"./toast": "./src/primitives/toast/index.ts",
|
|
13
|
+
"./tooltip": "./src/primitives/tooltip.ts",
|
|
14
|
+
"./popover": "./src/primitives/popover.ts",
|
|
15
|
+
"./popup": "./src/primitives/popup.ts",
|
|
16
|
+
"./dialog": "./src/primitives/dialog.ts",
|
|
17
|
+
"./modal": "./src/primitives/modal.ts",
|
|
18
|
+
"./ask": "./src/primitives/ask.ts",
|
|
19
|
+
"./disclosure": "./src/primitives/disclosure.ts",
|
|
20
|
+
"./focus-trap": "./src/primitives/focus-trap.ts",
|
|
21
|
+
"./roving-focus": "./src/primitives/roving-focus.ts",
|
|
22
|
+
"./skeleton": "./src/primitives/skeleton.ts",
|
|
23
|
+
"./theme": "./src/primitives/theme.ts",
|
|
24
|
+
"./view-transition": "./src/primitives/view-transition.ts",
|
|
25
|
+
"./transition": "./src/primitives/transition.ts",
|
|
26
|
+
"./announce": "./src/primitives/announce.ts"
|
|
7
27
|
},
|
|
8
28
|
"publish": {
|
|
9
|
-
"include": [
|
|
10
|
-
|
|
29
|
+
"include": [
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"NOTICE",
|
|
32
|
+
"README.md",
|
|
33
|
+
"THIRD_PARTY_NOTICES.md",
|
|
34
|
+
"src/**/*.ts",
|
|
35
|
+
"css/**/*.css",
|
|
36
|
+
"jsr.json"
|
|
37
|
+
],
|
|
38
|
+
"exclude": [
|
|
39
|
+
"src/**/*.test.ts",
|
|
40
|
+
"src/**/*-setup.ts",
|
|
41
|
+
"src/**/test-helpers/**"
|
|
42
|
+
]
|
|
11
43
|
}
|
|
12
44
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,94 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "1.0.0-dev.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"sideEffects":
|
|
5
|
+
"sideEffects": [
|
|
6
|
+
"**/*.css",
|
|
7
|
+
"./src/primitives/announce.ts",
|
|
8
|
+
"./src/primitives/ask.ts",
|
|
9
|
+
"./src/primitives/dialog-engine.ts",
|
|
10
|
+
"./src/primitives/dialog.ts",
|
|
11
|
+
"./src/primitives/modal-host.ts",
|
|
12
|
+
"./src/primitives/modal.ts",
|
|
13
|
+
"./src/primitives/popover.ts",
|
|
14
|
+
"./src/primitives/popup-core.ts",
|
|
15
|
+
"./src/primitives/popup.ts",
|
|
16
|
+
"./src/primitives/toast/index.ts",
|
|
17
|
+
"./src/primitives/toast/view.ts",
|
|
18
|
+
"./src/primitives/tooltip.ts"
|
|
19
|
+
],
|
|
6
20
|
"exports": {
|
|
7
|
-
".": "./src/index.ts"
|
|
21
|
+
".": "./src/index.ts",
|
|
22
|
+
"./signal": "./src/signal/index.ts",
|
|
23
|
+
"./dom": "./src/dom/index.ts",
|
|
24
|
+
"./fetch": "./src/fetch/index.ts",
|
|
25
|
+
"./actions": "./src/actions/index.ts",
|
|
26
|
+
"./actions/testing": "./src/actions/testing.ts",
|
|
27
|
+
"./toast": "./src/primitives/toast/index.ts",
|
|
28
|
+
"./tooltip": "./src/primitives/tooltip.ts",
|
|
29
|
+
"./popover": "./src/primitives/popover.ts",
|
|
30
|
+
"./popup": "./src/primitives/popup.ts",
|
|
31
|
+
"./dialog": "./src/primitives/dialog.ts",
|
|
32
|
+
"./modal": "./src/primitives/modal.ts",
|
|
33
|
+
"./ask": "./src/primitives/ask.ts",
|
|
34
|
+
"./disclosure": "./src/primitives/disclosure.ts",
|
|
35
|
+
"./focus-trap": "./src/primitives/focus-trap.ts",
|
|
36
|
+
"./roving-focus": "./src/primitives/roving-focus.ts",
|
|
37
|
+
"./skeleton": "./src/primitives/skeleton.ts",
|
|
38
|
+
"./theme": "./src/primitives/theme.ts",
|
|
39
|
+
"./view-transition": "./src/primitives/view-transition.ts",
|
|
40
|
+
"./transition": "./src/primitives/transition.ts",
|
|
41
|
+
"./announce": "./src/primitives/announce.ts",
|
|
42
|
+
"./css": "./css/ui-primitives.css"
|
|
8
43
|
},
|
|
9
|
-
"
|
|
44
|
+
"scripts": {
|
|
45
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
46
|
+
"typecheck:tests": "tsc -p tsconfig.test.json",
|
|
47
|
+
"test": "vitest --run",
|
|
48
|
+
"test:coverage": "vitest --run --coverage",
|
|
49
|
+
"lint:eslint": "eslint .",
|
|
50
|
+
"lint:prettier": "prettier --check .",
|
|
51
|
+
"lint:stylelint": "stylelint \"css/**/*.css\"",
|
|
52
|
+
"lint:knip": "knip",
|
|
53
|
+
"lint:fix": "eslint . --fix && prettier --write ."
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@eslint/js": "10.0.1",
|
|
57
|
+
"@stryker-mutator/core": "10.1.1",
|
|
58
|
+
"@stryker-mutator/typescript-checker": "10.1.1",
|
|
59
|
+
"@stryker-mutator/vitest-runner": "10.1.1",
|
|
60
|
+
"@types/node": "26.6.6",
|
|
61
|
+
"@typescript/native": "npm:typescript@7.0.2",
|
|
62
|
+
"@vitest/browser-playwright": "5.0.5",
|
|
63
|
+
"@vitest/coverage-v8": "5.0.5",
|
|
64
|
+
"esbuild": "0.28.2",
|
|
65
|
+
"eslint": "10.14.0",
|
|
66
|
+
"fast-check": "4.10.2",
|
|
67
|
+
"knip": "6.44.1",
|
|
68
|
+
"playwright": "1.64.1",
|
|
69
|
+
"prettier": "3.9.10",
|
|
70
|
+
"rolldown": "1.2.16",
|
|
71
|
+
"stylelint": "17.16.0",
|
|
72
|
+
"stylelint-config-standard": "40.0.0",
|
|
73
|
+
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
74
|
+
"typescript-eslint": "8.72.1",
|
|
75
|
+
"vitest": "5.0.5"
|
|
76
|
+
},
|
|
77
|
+
"overrides": {
|
|
78
|
+
"qs": "^6.15.2",
|
|
79
|
+
"minimatch": "^11.0.1"
|
|
80
|
+
},
|
|
81
|
+
"files": [
|
|
82
|
+
"src/",
|
|
83
|
+
"css/",
|
|
84
|
+
"jsr.json",
|
|
85
|
+
"LICENSE",
|
|
86
|
+
"NOTICE",
|
|
87
|
+
"THIRD_PARTY_NOTICES.md",
|
|
88
|
+
"!src/**/*.test.ts",
|
|
89
|
+
"!src/**/*-setup.ts",
|
|
90
|
+
"!src/**/test-helpers/**"
|
|
91
|
+
],
|
|
10
92
|
"description": "Build framework-free TypeScript browser UIs with signals, accessible widgets, typed requests and server actions",
|
|
11
93
|
"license": "Apache-2.0",
|
|
12
94
|
"types": "./src/index.ts",
|