@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.
Files changed (66) hide show
  1. package/NOTICE +3 -0
  2. package/README.md +145 -2
  3. package/THIRD_PARTY_NOTICES.md +8 -0
  4. package/css/ui-primitives.css +344 -0
  5. package/jsr.json +36 -4
  6. package/package.json +86 -4
  7. package/src/actions/api.ts +268 -0
  8. package/src/actions/async-feedback.ts +217 -0
  9. package/src/actions/cleanup.ts +66 -0
  10. package/src/actions/debounce.ts +150 -0
  11. package/src/actions/define-helpers.ts +87 -0
  12. package/src/actions/define.ts +851 -0
  13. package/src/actions/error.ts +161 -0
  14. package/src/actions/index.ts +60 -0
  15. package/src/actions/loading.ts +131 -0
  16. package/src/actions/notifier.ts +61 -0
  17. package/src/actions/poll-until.ts +133 -0
  18. package/src/actions/poll.ts +156 -0
  19. package/src/actions/registry.ts +259 -0
  20. package/src/actions/retry.ts +79 -0
  21. package/src/actions/testing.ts +29 -0
  22. package/src/actions/transport.ts +104 -0
  23. package/src/actions/types.ts +249 -0
  24. package/src/dom/bind-list.ts +103 -0
  25. package/src/dom/el.ts +67 -0
  26. package/src/dom/index.ts +9 -0
  27. package/src/dom/reconcile-tree.ts +268 -0
  28. package/src/dom/reconcile.ts +76 -0
  29. package/src/fetch/index.ts +18 -0
  30. package/src/fetch/instance.ts +37 -0
  31. package/src/fetch/request.ts +437 -0
  32. package/src/fetch/timeout.ts +67 -0
  33. package/src/fetch/types.ts +206 -0
  34. package/src/fetch/verbs.ts +103 -0
  35. package/src/index.ts +4 -8
  36. package/src/primitives/announce.ts +134 -0
  37. package/src/primitives/ask.ts +406 -0
  38. package/src/primitives/dialog-engine.ts +500 -0
  39. package/src/primitives/dialog.ts +263 -0
  40. package/src/primitives/disclosure.ts +287 -0
  41. package/src/primitives/focus-trap.ts +157 -0
  42. package/src/primitives/fx.ts +204 -0
  43. package/src/primitives/live-regions.ts +83 -0
  44. package/src/primitives/modal-host.ts +214 -0
  45. package/src/primitives/modal.ts +247 -0
  46. package/src/primitives/owner.ts +322 -0
  47. package/src/primitives/popover.ts +500 -0
  48. package/src/primitives/popup-core.ts +603 -0
  49. package/src/primitives/popup.ts +23 -0
  50. package/src/primitives/roving-focus.ts +198 -0
  51. package/src/primitives/scroll-lock.ts +119 -0
  52. package/src/primitives/skeleton.ts +142 -0
  53. package/src/primitives/theme.ts +202 -0
  54. package/src/primitives/toast/engine.ts +327 -0
  55. package/src/primitives/toast/index.ts +139 -0
  56. package/src/primitives/toast/view.ts +244 -0
  57. package/src/primitives/tooltip.ts +377 -0
  58. package/src/primitives/transition.ts +119 -0
  59. package/src/primitives/view-transition.ts +57 -0
  60. package/src/signal/bus.ts +126 -0
  61. package/src/signal/collection.ts +182 -0
  62. package/src/signal/index.ts +34 -0
  63. package/src/signal/resource.ts +102 -0
  64. package/src/signal/signal-map.ts +46 -0
  65. package/src/signal/signal.ts +1002 -0
  66. package/src/signal/store.ts +84 -0
package/NOTICE ADDED
@@ -0,0 +1,3 @@
1
+ ui
2
+ Copyright 2026 cplieger
3
+ https://github.com/cplieger/ui
package/README.md CHANGED
@@ -1,8 +1,149 @@
1
1
  # ui
2
2
 
3
- `@cplieger/ui` is a TypeScript package for browser UIs built without a framework: signals and keyed list rendering, accessible widgets such as dialogs, popovers, toasts and tooltips, typed JSON requests, and server actions with pending state and retries. It combines four earlier packages, `@cplieger/reactive`, `@cplieger/ui-primitives`, `@cplieger/fetch` and `@cplieger/actions`, into one package with one version.
3
+ [![npm](https://img.shields.io/npm/v/@cplieger/ui)](https://www.npmjs.com/package/@cplieger/ui) [![JSR](https://jsr.io/badges/@cplieger/ui)](https://jsr.io/@cplieger/ui) [![Mutation (TS)](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/cplieger/ui/badges/mutation-ts.json)](https://github.com/cplieger/ui/issues?q=label%3Astryker-tracker)
4
4
 
5
- This repository is being set up. The package's code arrives with its first development release.
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
+ ![A demo settings page built with the widgets and an example skin, showing an open modal with a popover menu, a tooltip, two toasts, an expanded disclosure, a theme switch and skeleton rows](docs/images/header.png)
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.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": ["LICENSE", "README.md", "src/**/*.ts", "jsr.json"],
10
- "exclude": ["src/**/*.test.ts", "src/**/*-setup.ts", "src/**/test-helpers/**"]
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.0",
3
+ "version": "1.0.0-dev.1",
4
4
  "type": "module",
5
- "sideEffects": false,
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
- "files": ["src/", "jsr.json", "!src/**/*.test.ts", "!src/**/*-setup.ts", "!src/**/test-helpers/**"],
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",