@omerdlw/base-framework 1.0.0 → 1.0.2
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/LICENSE +1 -1
- package/README.md +121 -52
- package/dist/atoms/index.d.ts +0 -1
- package/dist/atoms/index.d.ts.map +1 -1
- package/dist/atoms/index.js +1 -64
- package/dist/atoms/index.js.map +1 -1
- package/dist/error/index.js +22 -25
- package/dist/error/index.js.map +1 -1
- package/dist/hooks/index.d.ts +2 -9
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +97 -540
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/types.d.ts +3 -45
- package/dist/hooks/types.d.ts.map +1 -1
- package/dist/hooks/use-controllable-state.d.ts +1 -1
- package/dist/hooks/use-controllable-state.d.ts.map +1 -1
- package/dist/hooks/use-global-event.d.ts +0 -2
- package/dist/hooks/use-global-event.d.ts.map +1 -1
- package/dist/hooks/use-hotkey.d.ts +1 -1
- package/dist/hooks/use-hotkey.d.ts.map +1 -1
- package/dist/index.js +11283 -11772
- package/dist/index.js.map +1 -1
- package/dist/kernel/adapters.d.ts.map +1 -1
- package/dist/kernel/index.d.ts +1 -0
- package/dist/kernel/index.d.ts.map +1 -1
- package/dist/kernel/index.js +10 -81
- package/dist/kernel/index.js.map +1 -1
- package/dist/kernel/installed.d.ts.map +1 -1
- package/dist/kernel/page-controller.d.ts.map +1 -1
- package/dist/kernel/provider.d.ts.map +1 -1
- package/dist/modules/ambient/constants.d.ts +1 -1
- package/dist/modules/ambient/constants.d.ts.map +1 -1
- package/dist/modules/ambient/index.js +180 -326
- package/dist/modules/ambient/index.js.map +1 -1
- package/dist/modules/ambient/module.d.ts.map +1 -1
- package/dist/modules/background/constants.d.ts +4 -2
- package/dist/modules/background/constants.d.ts.map +1 -1
- package/dist/modules/background/context.d.ts +3 -0
- package/dist/modules/background/context.d.ts.map +1 -1
- package/dist/modules/background/hooks.d.ts +1 -1
- package/dist/modules/background/index.d.ts +2 -2
- package/dist/modules/background/index.d.ts.map +1 -1
- package/dist/modules/background/index.js +1172 -1270
- package/dist/modules/background/index.js.map +1 -1
- package/dist/modules/background/overlay.d.ts +1 -1
- package/dist/modules/background/overlay.d.ts.map +1 -1
- package/dist/modules/background/types.d.ts +1 -1
- package/dist/modules/background/types.d.ts.map +1 -1
- package/dist/modules/context-menu/constants.d.ts +1 -1
- package/dist/modules/context-menu/constants.d.ts.map +1 -1
- package/dist/modules/context-menu/context.d.ts.map +1 -1
- package/dist/modules/context-menu/hooks.d.ts.map +1 -1
- package/dist/modules/context-menu/index.js +133 -238
- package/dist/modules/context-menu/index.js.map +1 -1
- package/dist/modules/controls/constants.d.ts +1 -1
- package/dist/modules/controls/constants.d.ts.map +1 -1
- package/dist/modules/controls/hooks.d.ts +1 -1
- package/dist/modules/controls/index.js +28 -162
- package/dist/modules/controls/index.js.map +1 -1
- package/dist/modules/controls/types.d.ts +1 -1
- package/dist/modules/controls/types.d.ts.map +1 -1
- package/dist/modules/dock/cards.d.ts +2 -2
- package/dist/modules/dock/constants.d.ts +1 -1
- package/dist/modules/dock/constants.d.ts.map +1 -1
- package/dist/modules/dock/context.d.ts +10 -6
- package/dist/modules/dock/context.d.ts.map +1 -1
- package/dist/modules/dock/hooks.d.ts +5 -5
- package/dist/modules/dock/hooks.d.ts.map +1 -1
- package/dist/modules/dock/index.d.ts +1 -1
- package/dist/modules/dock/index.d.ts.map +1 -1
- package/dist/modules/dock/index.js +2060 -2080
- package/dist/modules/dock/index.js.map +1 -1
- package/dist/modules/dock/runtime/dock.d.ts.map +1 -1
- package/dist/modules/dock/types.d.ts +1 -1
- package/dist/modules/dock/types.d.ts.map +1 -1
- package/dist/modules/index.js +10789 -11027
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/loading/constants.d.ts +4 -2
- package/dist/modules/loading/constants.d.ts.map +1 -1
- package/dist/modules/loading/context.d.ts +3 -0
- package/dist/modules/loading/context.d.ts.map +1 -1
- package/dist/modules/loading/hooks.d.ts +1 -1
- package/dist/modules/loading/hooks.d.ts.map +1 -1
- package/dist/modules/loading/index.d.ts +2 -2
- package/dist/modules/loading/index.d.ts.map +1 -1
- package/dist/modules/loading/index.js +118 -222
- package/dist/modules/loading/index.js.map +1 -1
- package/dist/modules/loading/types.d.ts +1 -1
- package/dist/modules/loading/types.d.ts.map +1 -1
- package/dist/modules/media/constants.d.ts +2 -1
- package/dist/modules/media/constants.d.ts.map +1 -1
- package/dist/modules/media/context.d.ts +3 -0
- package/dist/modules/media/context.d.ts.map +1 -1
- package/dist/modules/media/index.d.ts +2 -2
- package/dist/modules/media/index.d.ts.map +1 -1
- package/dist/modules/media/index.js +61 -218
- package/dist/modules/media/index.js.map +1 -1
- package/dist/modules/modal/constants.d.ts +1 -1
- package/dist/modules/modal/constants.d.ts.map +1 -1
- package/dist/modules/modal/context.d.ts.map +1 -1
- package/dist/modules/modal/hooks.d.ts +2 -2
- package/dist/modules/modal/index.js +20 -366
- package/dist/modules/modal/index.js.map +1 -1
- package/dist/modules/notification/constants.d.ts +4 -2
- package/dist/modules/notification/constants.d.ts.map +1 -1
- package/dist/modules/notification/context.d.ts +4 -0
- package/dist/modules/notification/context.d.ts.map +1 -1
- package/dist/modules/notification/hooks.d.ts +1 -1
- package/dist/modules/notification/index.d.ts +2 -3
- package/dist/modules/notification/index.d.ts.map +1 -1
- package/dist/modules/notification/index.js +70 -154
- package/dist/modules/notification/index.js.map +1 -1
- package/dist/modules/notification/module.d.ts.map +1 -1
- package/dist/provider.js +255 -257
- package/dist/provider.js.map +1 -1
- package/dist/theme.d.ts +1 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +2 -2
- package/dist/theme.js.map +1 -1
- package/dist/utils/index.d.ts +5 -6
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -57
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/number.d.ts +0 -1
- package/dist/utils/number.d.ts.map +1 -1
- package/dist/utils/object.d.ts +0 -2
- package/dist/utils/object.d.ts.map +1 -1
- package/dist/utils/string.d.ts +0 -3
- package/dist/utils/string.d.ts.map +1 -1
- package/dist/utils/timing.d.ts +0 -1
- package/dist/utils/timing.d.ts.map +1 -1
- package/docs/README.md +124 -0
- package/docs/architecture-and-rules.md +129 -0
- package/docs/core-engine.md +196 -0
- package/docs/custom-modules.md +267 -0
- package/docs/getting-started.md +199 -0
- package/docs/modules/README.md +176 -0
- package/docs/modules/ambient.md +150 -0
- package/docs/modules/background.md +195 -0
- package/docs/modules/context-menu.md +248 -0
- package/docs/modules/controls.md +159 -0
- package/docs/modules/dock.md +734 -0
- package/docs/modules/error-boundary.md +160 -0
- package/docs/modules/kernel.md +266 -0
- package/docs/modules/loading.md +157 -0
- package/docs/modules/media.md +196 -0
- package/docs/modules/modal.md +229 -0
- package/docs/modules/notification.md +161 -0
- package/docs/modules/result.md +135 -0
- package/docs/testing.md +160 -0
- package/docs/theming.md +185 -0
- package/package.json +50 -2
- package/templates/AGENTS.md +165 -0
- package/dist/hooks/use-async-action.d.ts +0 -3
- package/dist/hooks/use-async-action.d.ts.map +0 -1
- package/dist/hooks/use-debounce.d.ts +0 -2
- package/dist/hooks/use-debounce.d.ts.map +0 -1
- package/dist/hooks/use-intersection-observer.d.ts +0 -4
- package/dist/hooks/use-intersection-observer.d.ts.map +0 -1
- package/dist/hooks/use-media-query.d.ts +0 -2
- package/dist/hooks/use-media-query.d.ts.map +0 -1
- package/dist/hooks/use-mounted.d.ts +0 -2
- package/dist/hooks/use-mounted.d.ts.map +0 -1
- package/dist/hooks/use-server-action.d.ts +0 -4
- package/dist/hooks/use-server-action.d.ts.map +0 -1
- package/dist/hooks/use-storage-state.d.ts +0 -5
- package/dist/hooks/use-storage-state.d.ts.map +0 -1
- package/dist/modules/theme.d.ts +0 -3
- package/dist/modules/theme.d.ts.map +0 -1
- package/dist/utils/json.d.ts +0 -3
- package/dist/utils/json.d.ts.map +0 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Result — `@omerdlw/base-framework/result`
|
|
2
|
+
|
|
3
|
+
> **Read this first if you need to:** write a Server Action (or any function that crosses the network), handle success/failure without `try/catch`, validate input in one place, or understand how failures reach the user.
|
|
4
|
+
|
|
5
|
+
## 1. At a glance
|
|
6
|
+
|
|
7
|
+
| | |
|
|
8
|
+
| :-------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| **Import** | `import { ok, err, type Result, createSafeAction } from "@omerdlw/base-framework/result"` |
|
|
10
|
+
| **Layer** | `src/core` foundation. **Server-safe**: no React, no `next/*`; imports only `@omerdlw/base-framework/utils` (`UserError`, `toUserMessage`, `report`). |
|
|
11
|
+
| **Enforced by** | [Rule 6](../architecture-and-rules.md#rule-6-result-pattern-returns): Server Actions (`"use server"`) must return `Result<T, E>`. Never throw across the network. |
|
|
12
|
+
| **Tests** | `tests/core/result.test.ts` |
|
|
13
|
+
|
|
14
|
+
## 2. Why
|
|
15
|
+
|
|
16
|
+
Next.js serializes thrown errors poorly and strips their details in production. A returned `Result` arrives intact and forces the caller to handle both branches:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
type Result<T, E = string> =
|
|
20
|
+
| { success: true; data: T; code?: string }
|
|
21
|
+
| { success: false; error: E; code?: string };
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`data` exists only on success and `error` only on failure (`never` on the other branch), so TypeScript narrows on `result.success`.
|
|
25
|
+
|
|
26
|
+
## 3. Quick start
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
"use server";
|
|
30
|
+
import { ok, err, type Result } from "@omerdlw/base-framework/result";
|
|
31
|
+
import { UserError, toUserMessage } from "@omerdlw/base-framework/utils";
|
|
32
|
+
import { createServerSupabaseClient } from "@/infrastructure/supabase/server";
|
|
33
|
+
|
|
34
|
+
export async function renameProjectAction(
|
|
35
|
+
id: string,
|
|
36
|
+
name: string,
|
|
37
|
+
): Promise<Result<{ id: string }>> {
|
|
38
|
+
if (!name.trim()) return err("Name is required", "VALIDATION");
|
|
39
|
+
try {
|
|
40
|
+
const supabase = await createServerSupabaseClient();
|
|
41
|
+
const { error } = await supabase
|
|
42
|
+
.from("projects")
|
|
43
|
+
.update({ name })
|
|
44
|
+
.eq("id", id);
|
|
45
|
+
if (error) throw new UserError("Couldn't rename this project"); // shown as written
|
|
46
|
+
return ok({ id });
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return err(toUserMessage(e, { fallback: "Couldn't rename this project" })); // never e.message
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// Same guarantees, less boilerplate
|
|
55
|
+
export const renameProject = createSafeAction(
|
|
56
|
+
async (input: { id: string; name: string }) => updateProject(input), // may throw; may return a Result or a plain value
|
|
57
|
+
{ schema: RenameSchema, errorCode: "RENAME_FAILED" },
|
|
58
|
+
);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
// Client: branch explicitly, or let toast.fromResult handle feedback
|
|
63
|
+
const result = await renameProjectAction(id, name);
|
|
64
|
+
if (isErr(result)) return; // result.error is user-safe text
|
|
65
|
+
toast.fromResult(result, { success: "Project renamed successfully!" }); // see notification.md
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 4. API
|
|
69
|
+
|
|
70
|
+
| Export | Signature | Description |
|
|
71
|
+
| :------------------------------------------------------------ | :------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
72
|
+
| `Result<T, E = string>`, `SuccessResult<T>`, `ErrorResult<E>` | types | `readonly` fields; both carry an optional `code`. |
|
|
73
|
+
| `ok(data, code?)` | `→ SuccessResult<T>` | `code` is included only when given. |
|
|
74
|
+
| `err(error, code?)` | `→ ErrorResult<E>` | |
|
|
75
|
+
| `isResult(value)` | `(unknown) → value is Result` | True for objects with a boolean `success` **and** a matching `data` (success) / `error` (failure) key. Used by `toast.promise` and `toast.fromResult`. |
|
|
76
|
+
| `isOk(r)`, `isErr(r)` | type guards | |
|
|
77
|
+
| `unwrap(r)` | `→ T` | Returns `data`, or **throws** (the error itself if it is an `Error`, otherwise `new Error(String(error))`). Trusted internal code only. |
|
|
78
|
+
| `unwrapOr(r, fallback)` | `→ T` | `data` or the fallback. |
|
|
79
|
+
| `map(r, fn)`, `mapErr(r, fn)` | `→ Result` | Transform one branch; `code` is preserved. |
|
|
80
|
+
| `match(r, { ok, err })` | `→ R` | Branch handling returning a value. |
|
|
81
|
+
| `tryCatch(promiseOrFn, mapError?)` | `→ Promise<Result<T, E>>` | Awaits a promise (or an async function); thrown/rejected → `err(...)`. |
|
|
82
|
+
| `createSafeAction(handler, options?)` | `→ (...args) => Promise<Result<TOutput, E>>` | See below. |
|
|
83
|
+
|
|
84
|
+
### 4.1 How thrown errors become messages
|
|
85
|
+
|
|
86
|
+
`tryCatch` and `createSafeAction` convert a caught error with `mapError(error)` if you pass one, otherwise `toUserMessage(error)`:
|
|
87
|
+
|
|
88
|
+
- a `UserError`, a string, or a 4xx payload from our own API is used **as written**;
|
|
89
|
+
- anything else maps to a fixed sentence from `USER_MESSAGES` (`generic`, `network`, `timeout`, `unauthorized`, `forbidden`, `notFound`, `rateLimited`, `server`) so database/SDK wording never reaches the UI;
|
|
90
|
+
- unless it is a `UserError`, the original error is **reported** via `report("Result", error)` (see [error-boundary.md](./error-boundary.md#7-user-facing-messages-and-report)).
|
|
91
|
+
|
|
92
|
+
### 4.2 `createSafeAction(handler, options)`
|
|
93
|
+
|
|
94
|
+
| Option | Meaning |
|
|
95
|
+
| :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
96
|
+
| `schema` | Anything with `safeParse(input)` (e.g. Zod). Applied to the **first argument**; on success the parsed value replaces it. Failure → `err(firstIssueMessage \| message \| "Invalid input", errorCode ?? "VALIDATION_ERROR")`. |
|
|
97
|
+
| `validate(...args)` | Extra synchronous check on the (parsed) args. Return an `ErrorResult` to fail with it, `false` → `"Validation failed"`, a non-empty string → that message, anything else passes. |
|
|
98
|
+
| `mapError(error)` | Maps both validation messages and thrown errors to your error type `E`. |
|
|
99
|
+
| `errorCode` | `code` set on failures (validation default: `"VALIDATION_ERROR"`). |
|
|
100
|
+
|
|
101
|
+
The handler may return a `Result` (returned as is), or a plain value (wrapped in `ok(value)`), or throw (→ `err`). The wrapper itself never throws.
|
|
102
|
+
|
|
103
|
+
## 5. Where Results flow
|
|
104
|
+
|
|
105
|
+
| Consumer | Behaviour |
|
|
106
|
+
| :----------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
107
|
+
| `toast.fromResult` / `toast.promise` | Toasts `success(data)` or `error` (falling back to `toUserMessage`). |
|
|
108
|
+
| Notification module | Shows `APP_ERROR` / `STATE_CHANGE` events that have `notify: true`. |
|
|
109
|
+
| Dock | Shows **every** `APP_ERROR` as its error status card (regardless of `notify`). |
|
|
110
|
+
|
|
111
|
+
So a failed `useServerAction` produces a dock error card, plus a toast if you passed `toast: true`.
|
|
112
|
+
|
|
113
|
+
## 6. Recipes and gotchas
|
|
114
|
+
|
|
115
|
+
| I want to… | Do this |
|
|
116
|
+
| :--------------------------- | :---------------------------------------------------------------------------------------------------- |
|
|
117
|
+
| Return a validation error | `return err("Name is required", "VALIDATION")`. |
|
|
118
|
+
| Show server text to the user | Throw `new UserError("…")` (or return `err("…")`) with text you wrote; never forward `error.message`. |
|
|
119
|
+
| Custom error shape | `Result<T, MyError>` and pass `mapError` to `createSafeAction` / `tryCatch`. |
|
|
120
|
+
| Narrow in TypeScript | `if (r.success) r.data else r.error`, or `isOk` / `isErr`. |
|
|
121
|
+
| Chain transformations | `map(r, fn)` / `mapErr(r, fn)`. |
|
|
122
|
+
|
|
123
|
+
- A Server Action must return plain serializable data: do not put `Error` instances, class instances or functions in a `Result` that crosses the network.
|
|
124
|
+
- `unwrap` throws: never use it in a `"use server"` function's return path.
|
|
125
|
+
- `createSafeAction`'s `schema` validates only the **first** argument.
|
|
126
|
+
- `isResult` requires the matching data/error key: `{ success: true }` without `data` is **not** a Result (use `ok(undefined)`).
|
|
127
|
+
|
|
128
|
+
## 7. File map
|
|
129
|
+
|
|
130
|
+
A single file, `src/core/result.ts`. It depends only on `./utils/user-message` and `./utils/report`, so it is safe on both server and client (and is one of the server-safe core entries that must not import React or `next/*`).
|
|
131
|
+
|
|
132
|
+
## 8. Dependencies
|
|
133
|
+
|
|
134
|
+
- **Uses:** `@omerdlw/base-framework/utils` (`UserError`, `toUserMessage`, `report`) only.
|
|
135
|
+
- **Used by:** every `server/actions.ts` in `src/features/**`, `@omerdlw/base-framework/hooks` (`useServerAction`, `useAsyncAction`), `@omerdlw/base-framework/modules/notification` (`isResult` for `toast.fromResult` / `promise`), and route-level helpers that return `Result`.
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Testing Guide
|
|
2
|
+
|
|
3
|
+
> **Read this first if you need to:** run package tests, understand the test harness (`happy-dom`, `renderHook`, `Themed`), write tests for new modules, or test downstream components that consume `@omerdlw/base-framework`.
|
|
4
|
+
>
|
|
5
|
+
> **Stack:** Node's built-in test runner (`node:test`) + `node:assert/strict`, TypeScript executed directly through a resolver hook (`register.mjs`), `happy-dom` for React components and hooks, zero external framework overhead.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Running Tests
|
|
10
|
+
|
|
11
|
+
| Command | Purpose |
|
|
12
|
+
| :--- | :--- |
|
|
13
|
+
| `npm test` | Runs the full 520+ test suite (`tests/**/*.test.ts`) in ~1 second. |
|
|
14
|
+
| `npm run test:coverage` | Runs the suite with complete line, branch, and function coverage reporting for `src/**`. |
|
|
15
|
+
| `npm run type-check` | Full TypeScript compiler validation (`tsc --noEmit`). |
|
|
16
|
+
| `npm run check:architecture` | Static AST validation checking for 0 circular dependencies and verified `uses` peer declarations. |
|
|
17
|
+
| `node --import ./tests/support/register.mjs --test tests/core/kernel.test.ts` | Run a single test file. |
|
|
18
|
+
| `node --import ./tests/support/register.mjs --test --test-name-pattern="surface" tests/modules/dock.test.ts` | Run tests matching a specific pattern. |
|
|
19
|
+
|
|
20
|
+
**Pre-flight Verification:**
|
|
21
|
+
`npm run type-check && npm run lint && npm run check:architecture && npm test && npm run build`
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 2. Test Suite Layout
|
|
26
|
+
|
|
27
|
+
Tests mirror the package's architecture, **one TypeScript test file per unit**:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
tests/
|
|
31
|
+
architecture.test.ts Layer boundaries, module contracts, barrel exports, cyclical imports
|
|
32
|
+
core/ Microkernel, error handling, hooks, atoms, tokens, utils, events, result, theme
|
|
33
|
+
modules/ ambient, background, context-menu, controls, dock, loading, media, modal, notification
|
|
34
|
+
support/ Test harness: happy-dom, render, themed wrapper, stubs (§3)
|
|
35
|
+
tsconfig.json TypeScript compilation rules for tests
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 3. Test Harness (`tests/support`)
|
|
41
|
+
|
|
42
|
+
| File | Provides |
|
|
43
|
+
| :--- | :--- |
|
|
44
|
+
| `register.mjs` | Node module loader hook: resolves `@omerdlw/base-framework` imports on the fly and transpiles TypeScript. |
|
|
45
|
+
| `dom.ts` | Configures a **`happy-dom`** browser environment (`window`, `document`, `matchMedia`, observers) for React component and hook testing. |
|
|
46
|
+
| `render.ts` | React 19 testing primitives: `render(element)`, `renderHook(hook, { wrapper })`, `flush()`. Automatically unmounts trees after every test to prevent leaks. |
|
|
47
|
+
| `themes.ts` | `Themed` (`ThemeProvider` loaded with test themes) and `withThemes()`. Required when testing any module view or atom that consumes theme slots. |
|
|
48
|
+
| `registry.ts` | Test registry with `builtInModules` for microkernel and module integration testing. |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 4. Testing Patterns
|
|
53
|
+
|
|
54
|
+
### 4.1 Testing Pure Utils & Math
|
|
55
|
+
|
|
56
|
+
Import the utility directly and assert deterministic behavior:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { describe, test } from "node:test";
|
|
60
|
+
import assert from "node:assert/strict";
|
|
61
|
+
import { clamp, toFiniteNumber } from "@omerdlw/base-framework/utils";
|
|
62
|
+
|
|
63
|
+
describe("math utils", () => {
|
|
64
|
+
test("clamp restricts numbers within min and max boundaries", () => {
|
|
65
|
+
assert.equal(clamp(5, 0, 10), 5);
|
|
66
|
+
assert.equal(clamp(-5, 0, 10), 0);
|
|
67
|
+
assert.equal(clamp(15, 0, 10), 10);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("toFiniteNumber handles non-numeric fallbacks", () => {
|
|
71
|
+
assert.equal(toFiniteNumber("123"), 123);
|
|
72
|
+
assert.equal(toFiniteNumber(NaN, 0), 0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 4.2 Testing Hooks & Subscriptions
|
|
78
|
+
|
|
79
|
+
Render hooks inside `Themed` or `CoreProvider` using `renderHook`:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { describe, test } from "node:test";
|
|
83
|
+
import assert from "node:assert/strict";
|
|
84
|
+
import { renderHook } from "../support/render.ts";
|
|
85
|
+
import { useGlobalEvent } from "@omerdlw/base-framework/hooks";
|
|
86
|
+
import { globalEvents, EVENT_TYPES } from "@omerdlw/base-framework/events";
|
|
87
|
+
|
|
88
|
+
describe("useGlobalEvent", () => {
|
|
89
|
+
test("receives published event payloads", () => {
|
|
90
|
+
let received = null;
|
|
91
|
+
const { unmount } = renderHook(() => {
|
|
92
|
+
useGlobalEvent(EVENT_TYPES.STATE_CHANGE, (payload) => {
|
|
93
|
+
received = payload;
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
globalEvents.publish(EVENT_TYPES.STATE_CHANGE, { key: "auth", value: true });
|
|
98
|
+
assert.deepEqual(received, { key: "auth", value: true });
|
|
99
|
+
|
|
100
|
+
unmount();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 4.3 Testing Diagnostics & Error Reports
|
|
106
|
+
|
|
107
|
+
Install an ephemeral report sink with `setReportSink` and release it in cleanup:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { describe, test } from "node:test";
|
|
111
|
+
import assert from "node:assert/strict";
|
|
112
|
+
import { report, setReportSink } from "@omerdlw/base-framework/utils";
|
|
113
|
+
|
|
114
|
+
describe("error reporter", () => {
|
|
115
|
+
test("routes diagnostic messages to the configured sink", () => {
|
|
116
|
+
const logs: any[] = [];
|
|
117
|
+
const release = setReportSink((scope, err, level) => {
|
|
118
|
+
logs.push({ scope, err, level });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
report("dock", new Error("Invalid surface state"), "warning");
|
|
122
|
+
release();
|
|
123
|
+
|
|
124
|
+
assert.equal(logs.length, 1);
|
|
125
|
+
assert.equal(logs[0].scope, "dock");
|
|
126
|
+
assert.equal(logs[0].level, "warning");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 5. Testing Downstream Applications
|
|
134
|
+
|
|
135
|
+
When testing Next.js components or pages that consume `@omerdlw/base-framework`:
|
|
136
|
+
|
|
137
|
+
1. **Wrapping with `CoreProvider`:** Provide a test wrapper with the required modules:
|
|
138
|
+
```tsx
|
|
139
|
+
import { CoreProvider } from "@omerdlw/base-framework/provider";
|
|
140
|
+
import { dockModule } from "@omerdlw/base-framework/modules/dock";
|
|
141
|
+
import { notificationModule } from "@omerdlw/base-framework/modules/notification";
|
|
142
|
+
|
|
143
|
+
export function TestProviders({ children }: { children: React.ReactNode }) {
|
|
144
|
+
return <CoreProvider modules={[dockModule, notificationModule]}>{children}</CoreProvider>;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
2. **Testing Declarative Routes (`usePage`):** Since `usePage()` accepts a static or reactive configuration object, verify that components invoke `usePage()` with the expected metadata, loading flags, or dock card configs.
|
|
148
|
+
3. **Asserting Server Action Results:** Verify that Server Actions return `Result<T, E>` (`ok()` or `err()`) rather than throwing raw unhandled exceptions.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 6. What the Suite Protects
|
|
153
|
+
|
|
154
|
+
| Layer | Enforced Invariants |
|
|
155
|
+
| :--- | :--- |
|
|
156
|
+
| **Architecture** | 0 circular dependencies, downward-only imports, clean barrel exports |
|
|
157
|
+
| **Microkernel** | Registry ordering, topological provider nesting, `usePage` atomic reconciliation |
|
|
158
|
+
| **Modules** | Dock flows/surfaces/guards/status, modal stack order, context-menu scoring, controls rails, notification queues, media sessions |
|
|
159
|
+
| **Atoms** | Accessibility contracts (ARIA), keyboard navigation, deterministic `cn()` class merging |
|
|
160
|
+
| **Tokens & Theme** | GPU-accelerated motion presets, OKLCH token boundaries, Z-index hierarchy |
|
package/docs/theming.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Theming System Guide — `@omerdlw/base-framework/theme`
|
|
2
|
+
|
|
3
|
+
> **Read this first if you need to:** customize the visual design of any first-party module (`dock`, `modal`, `notification`, etc.), supply custom Tailwind styles, or understand the theme slot contract.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Philosophy: Logic Decoupled from Aesthetics
|
|
8
|
+
|
|
9
|
+
`@omerdlw/base-framework` enforces a strict rule: **Modules own interaction mechanics, accessibility, and state machines; consumer applications own visual design.**
|
|
10
|
+
|
|
11
|
+
Modules ship with **zero hardcoded visual Tailwind colors or aesthetic choices**. Instead, each module declares a typed **Theme Spec** defining required "slots" (e.g. `card`, `backdrop`, `header`, `item`).
|
|
12
|
+
|
|
13
|
+
If a required theme is not provided to `<ThemeProvider>`, `useTheme()` throws an informative error at mount time:
|
|
14
|
+
```
|
|
15
|
+
Theme "dock" is missing. Add its theme configuration to your ThemeProvider.
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 2. The Theme API
|
|
21
|
+
|
|
22
|
+
Import the theming primitives from `@omerdlw/base-framework/theme`:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import {
|
|
26
|
+
defineThemeSpec,
|
|
27
|
+
defineTheme,
|
|
28
|
+
ThemeProvider,
|
|
29
|
+
useTheme,
|
|
30
|
+
type ThemeSpec,
|
|
31
|
+
type ThemeConfig,
|
|
32
|
+
type ThemeEntry,
|
|
33
|
+
} from "@omerdlw/base-framework/theme";
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2.1 `defineThemeSpec<Slots>(id)`
|
|
37
|
+
Used by module authors to define the contract:
|
|
38
|
+
```ts
|
|
39
|
+
export type DockThemeSlot = "card" | "header" | "title" | "description" | "icon" | "surface";
|
|
40
|
+
export const dockTheme = defineThemeSpec<DockThemeSlot>("dock");
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2.2 `defineTheme(spec, { slots, styles? })`
|
|
44
|
+
Used by application developers to implement the styling contract. TypeScript enforces that **every required slot** is provided:
|
|
45
|
+
```ts
|
|
46
|
+
export const dockThemeConfig = defineTheme(dockTheme, {
|
|
47
|
+
slots: {
|
|
48
|
+
card: "rounded-[28px] bg-neutral-900/80 backdrop-blur-xl border border-white/10 shadow-2xl p-4",
|
|
49
|
+
header: "flex items-center gap-3",
|
|
50
|
+
title: "text-sm font-medium text-white",
|
|
51
|
+
description: "text-xs text-white/50",
|
|
52
|
+
icon: "text-white/80",
|
|
53
|
+
surface: "rounded-3xl bg-neutral-900 border border-white/10 p-6",
|
|
54
|
+
},
|
|
55
|
+
styles: {
|
|
56
|
+
// Optional inline CSS styles (e.g. tokenized Z-Index)
|
|
57
|
+
card: { zIndex: 100 },
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 2.3 `ThemeProvider({ themes, children })`
|
|
63
|
+
Wraps your application or `CoreProvider` to supply the theme map:
|
|
64
|
+
```tsx
|
|
65
|
+
<ThemeProvider themes={[dockThemeConfig, modalThemeConfig, notificationThemeConfig]}>
|
|
66
|
+
<CoreProvider modules={modules}>
|
|
67
|
+
{children}
|
|
68
|
+
</CoreProvider>
|
|
69
|
+
</ThemeProvider>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2.4 `useTheme(spec)`
|
|
73
|
+
Consumed internally by module overlays to read the active slots:
|
|
74
|
+
```tsx
|
|
75
|
+
const { slots, styles } = useTheme(dockTheme);
|
|
76
|
+
return <div className={slots.card} style={styles.card} />;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 3. Module Theme Slot Reference
|
|
82
|
+
|
|
83
|
+
Below is the complete reference of theme specs exported by the 9 first-party modules:
|
|
84
|
+
|
|
85
|
+
| Module | Spec Export | Import Path | Primary Slots |
|
|
86
|
+
| :--- | :--- | :--- | :--- |
|
|
87
|
+
| **Dock** | `dockTheme` | `@omerdlw/base-framework/modules/dock` | `card`, `header`, `title`, `description`, `icon`, `surface`, `status` |
|
|
88
|
+
| **Modal** | `modalTheme` | `@omerdlw/base-framework/modules/modal` | `backdrop`, `dialog`, `header`, `title`, `body`, `footer`, `close` |
|
|
89
|
+
| **Notification** | `notificationTheme` | `@omerdlw/base-framework/modules/notification` | `viewport`, `toast`, `title`, `description`, `icon`, `action`, `close` |
|
|
90
|
+
| **Ambient** | `ambientTheme` | `@omerdlw/base-framework/modules/ambient` | `container`, `canvas` |
|
|
91
|
+
| **Background** | `backgroundTheme` | `@omerdlw/base-framework/modules/background` | `layer`, `overlay`, `fades` |
|
|
92
|
+
| **Controls** | `controlsTheme` | `@omerdlw/base-framework/modules/controls` | `rail`, `button`, `icon` |
|
|
93
|
+
| **Loading** | `loadingTheme` | `@omerdlw/base-framework/modules/loading` | `overlay`, `spinner`, `message` |
|
|
94
|
+
| **Context Menu** | `contextMenuTheme` | `@omerdlw/base-framework/modules/context-menu` | `menu`, `item`, `separator`, `label`, `shortcut` |
|
|
95
|
+
| **Error Boundary** | `errorTheme` | `@omerdlw/base-framework/error` | `boundary`, `card`, `title`, `message`, `retryButton` |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 4. Complete Starter Theme Configuration
|
|
100
|
+
|
|
101
|
+
Create a theme definitions file in your project (e.g. `src/config/themes.ts`):
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { defineTheme } from "@omerdlw/base-framework/theme";
|
|
105
|
+
import { dockTheme } from "@omerdlw/base-framework/modules/dock";
|
|
106
|
+
import { modalTheme } from "@omerdlw/base-framework/modules/modal";
|
|
107
|
+
import { notificationTheme } from "@omerdlw/base-framework/modules/notification";
|
|
108
|
+
import { loadingTheme } from "@omerdlw/base-framework/modules/loading";
|
|
109
|
+
import { errorTheme } from "@omerdlw/base-framework/error";
|
|
110
|
+
|
|
111
|
+
export const appThemes = [
|
|
112
|
+
defineTheme(dockTheme, {
|
|
113
|
+
slots: {
|
|
114
|
+
card: "rounded-[24px] bg-neutral-900/90 backdrop-blur-2xl border border-white/10 shadow-2xl p-4 text-white",
|
|
115
|
+
header: "flex items-center gap-3",
|
|
116
|
+
title: "text-sm font-semibold tracking-tight",
|
|
117
|
+
description: "text-xs text-white/50",
|
|
118
|
+
icon: "text-white/80",
|
|
119
|
+
surface: "rounded-3xl bg-neutral-900/95 border border-white/10 p-6 backdrop-blur-3xl",
|
|
120
|
+
status: "rounded-xl bg-white/5 px-3 py-1.5 text-xs text-white/70",
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
|
|
124
|
+
defineTheme(modalTheme, {
|
|
125
|
+
slots: {
|
|
126
|
+
backdrop: "fixed inset-0 bg-black/60 backdrop-blur-sm",
|
|
127
|
+
dialog: "rounded-2xl bg-neutral-900 border border-white/10 shadow-2xl p-6 text-white max-w-lg w-full",
|
|
128
|
+
header: "mb-4",
|
|
129
|
+
title: "text-lg font-bold",
|
|
130
|
+
body: "text-sm text-white/80",
|
|
131
|
+
footer: "mt-6 flex justify-end gap-3",
|
|
132
|
+
close: "size-8 center rounded-full hover:bg-white/10 text-white/60 hover:text-white",
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
|
|
136
|
+
defineTheme(notificationTheme, {
|
|
137
|
+
slots: {
|
|
138
|
+
viewport: "fixed bottom-6 right-6 flex flex-col gap-2 z-50 pointer-events-none",
|
|
139
|
+
toast: "pointer-events-auto flex items-center gap-3 rounded-xl bg-neutral-900 border border-white/10 shadow-xl px-4 py-3 text-sm text-white",
|
|
140
|
+
title: "font-medium text-white",
|
|
141
|
+
description: "text-xs text-white/60",
|
|
142
|
+
icon: "shrink-0",
|
|
143
|
+
action: "text-xs font-semibold text-sky-400 hover:text-sky-300",
|
|
144
|
+
close: "text-white/40 hover:text-white",
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
|
|
148
|
+
defineTheme(loadingTheme, {
|
|
149
|
+
slots: {
|
|
150
|
+
overlay: "center fixed inset-0 h-screen w-screen bg-black/40 backdrop-blur-xs",
|
|
151
|
+
spinner: "animate-spin text-white/80",
|
|
152
|
+
message: "text-xs font-medium text-white/60 mt-3",
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
|
|
156
|
+
defineTheme(errorTheme, {
|
|
157
|
+
slots: {
|
|
158
|
+
boundary: "center min-h-[300px] w-full p-6",
|
|
159
|
+
card: "rounded-2xl bg-red-950/20 border border-red-500/20 p-6 max-w-md text-center",
|
|
160
|
+
title: "text-base font-semibold text-red-400",
|
|
161
|
+
message: "text-xs text-red-200/70 mt-2",
|
|
162
|
+
retryButton: "mt-4 inline-flex h-8 items-center px-3 rounded-lg bg-red-500/20 text-red-300 text-xs font-medium hover:bg-red-500/30",
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
];
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Then in `src/app/providers.tsx`:
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
"use client";
|
|
172
|
+
|
|
173
|
+
import { ThemeProvider } from "@omerdlw/base-framework/theme";
|
|
174
|
+
import { CoreProvider } from "@omerdlw/base-framework/provider";
|
|
175
|
+
import { appThemes } from "@/config/themes";
|
|
176
|
+
import { modules } from "@/config/modules";
|
|
177
|
+
|
|
178
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
179
|
+
return (
|
|
180
|
+
<ThemeProvider themes={appThemes}>
|
|
181
|
+
<CoreProvider modules={modules}>{children}</CoreProvider>
|
|
182
|
+
</ThemeProvider>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
```
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omerdlw/base-framework",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Microkernel engine and first-party declarative UI modules for app-like Next.js products.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"main": "./dist/index.js",
|
|
7
8
|
"types": "./dist/index.d.ts",
|
|
8
9
|
"exports": {
|
|
@@ -61,6 +62,51 @@
|
|
|
61
62
|
"import": "./dist/atoms/index.js",
|
|
62
63
|
"default": "./dist/atoms/index.js"
|
|
63
64
|
},
|
|
65
|
+
"./dock": {
|
|
66
|
+
"types": "./dist/modules/dock/index.d.ts",
|
|
67
|
+
"import": "./dist/modules/dock/index.js",
|
|
68
|
+
"default": "./dist/modules/dock/index.js"
|
|
69
|
+
},
|
|
70
|
+
"./modal": {
|
|
71
|
+
"types": "./dist/modules/modal/index.d.ts",
|
|
72
|
+
"import": "./dist/modules/modal/index.js",
|
|
73
|
+
"default": "./dist/modules/modal/index.js"
|
|
74
|
+
},
|
|
75
|
+
"./notification": {
|
|
76
|
+
"types": "./dist/modules/notification/index.d.ts",
|
|
77
|
+
"import": "./dist/modules/notification/index.js",
|
|
78
|
+
"default": "./dist/modules/notification/index.js"
|
|
79
|
+
},
|
|
80
|
+
"./ambient": {
|
|
81
|
+
"types": "./dist/modules/ambient/index.d.ts",
|
|
82
|
+
"import": "./dist/modules/ambient/index.js",
|
|
83
|
+
"default": "./dist/modules/ambient/index.js"
|
|
84
|
+
},
|
|
85
|
+
"./background": {
|
|
86
|
+
"types": "./dist/modules/background/index.d.ts",
|
|
87
|
+
"import": "./dist/modules/background/index.js",
|
|
88
|
+
"default": "./dist/modules/background/index.js"
|
|
89
|
+
},
|
|
90
|
+
"./context-menu": {
|
|
91
|
+
"types": "./dist/modules/context-menu/index.d.ts",
|
|
92
|
+
"import": "./dist/modules/context-menu/index.js",
|
|
93
|
+
"default": "./dist/modules/context-menu/index.js"
|
|
94
|
+
},
|
|
95
|
+
"./controls": {
|
|
96
|
+
"types": "./dist/modules/controls/index.d.ts",
|
|
97
|
+
"import": "./dist/modules/controls/index.js",
|
|
98
|
+
"default": "./dist/modules/controls/index.js"
|
|
99
|
+
},
|
|
100
|
+
"./loading": {
|
|
101
|
+
"types": "./dist/modules/loading/index.d.ts",
|
|
102
|
+
"import": "./dist/modules/loading/index.js",
|
|
103
|
+
"default": "./dist/modules/loading/index.js"
|
|
104
|
+
},
|
|
105
|
+
"./media": {
|
|
106
|
+
"types": "./dist/modules/media/index.d.ts",
|
|
107
|
+
"import": "./dist/modules/media/index.js",
|
|
108
|
+
"default": "./dist/modules/media/index.js"
|
|
109
|
+
},
|
|
64
110
|
"./modules": {
|
|
65
111
|
"types": "./dist/modules/index.d.ts",
|
|
66
112
|
"import": "./dist/modules/index.js",
|
|
@@ -74,11 +120,13 @@
|
|
|
74
120
|
},
|
|
75
121
|
"files": [
|
|
76
122
|
"dist",
|
|
123
|
+
"docs",
|
|
124
|
+
"templates",
|
|
77
125
|
"README.md",
|
|
78
126
|
"LICENSE"
|
|
79
127
|
],
|
|
80
128
|
"scripts": {
|
|
81
|
-
"build": "tsup && tsc --emitDeclarationOnly --outDir dist",
|
|
129
|
+
"build": "rm -rf dist && tsup && tsc --emitDeclarationOnly --outDir dist",
|
|
82
130
|
"prepublishOnly": "npm run build",
|
|
83
131
|
"dev": "tsup --watch",
|
|
84
132
|
"type-check": "tsc --noEmit",
|