@typed/ui 1.0.0-beta.4 → 1.0.0-beta.5

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 (157) hide show
  1. package/README.md +53 -9
  2. package/dist/Alert.d.ts +78 -0
  3. package/dist/Alert.d.ts.map +1 -0
  4. package/dist/Alert.js +37 -0
  5. package/dist/Button.d.ts +144 -0
  6. package/dist/Button.d.ts.map +1 -0
  7. package/dist/Button.js +38 -0
  8. package/dist/Carousel.d.ts +547 -0
  9. package/dist/Carousel.d.ts.map +1 -0
  10. package/dist/Carousel.js +387 -0
  11. package/dist/Checkbox.d.ts +203 -0
  12. package/dist/Checkbox.d.ts.map +1 -0
  13. package/dist/Checkbox.js +140 -0
  14. package/dist/Collection.d.ts +254 -0
  15. package/dist/Collection.d.ts.map +1 -0
  16. package/dist/Collection.js +218 -0
  17. package/dist/Combobox.d.ts +516 -0
  18. package/dist/Combobox.d.ts.map +1 -0
  19. package/dist/Combobox.js +371 -0
  20. package/dist/Component.d.ts +127 -0
  21. package/dist/Component.d.ts.map +1 -0
  22. package/dist/Component.js +60 -0
  23. package/dist/Composite.d.ts +823 -0
  24. package/dist/Composite.d.ts.map +1 -0
  25. package/dist/Composite.js +615 -0
  26. package/dist/Dialog.d.ts +544 -0
  27. package/dist/Dialog.d.ts.map +1 -0
  28. package/dist/Dialog.js +355 -0
  29. package/dist/Disclosure.d.ts +219 -0
  30. package/dist/Disclosure.d.ts.map +1 -0
  31. package/dist/Disclosure.js +128 -0
  32. package/dist/Dom/Events.d.ts +122 -0
  33. package/dist/Dom/Events.d.ts.map +1 -0
  34. package/dist/Dom/Events.js +192 -0
  35. package/dist/Dom/Props.d.ts +161 -0
  36. package/dist/Dom/Props.d.ts.map +1 -0
  37. package/dist/Dom/Props.js +110 -0
  38. package/dist/Dom/Refs.d.ts +58 -0
  39. package/dist/Dom/Refs.d.ts.map +1 -0
  40. package/dist/Dom/Refs.js +61 -0
  41. package/dist/Dom/Render.d.ts +59 -0
  42. package/dist/Dom/Render.d.ts.map +1 -0
  43. package/dist/Dom/Render.js +71 -0
  44. package/dist/Dom/Types.d.ts +570 -0
  45. package/dist/Dom/Types.d.ts.map +1 -0
  46. package/dist/Dom/Types.js +1 -0
  47. package/dist/Dom/index.d.ts +20 -0
  48. package/dist/Dom/index.d.ts.map +1 -0
  49. package/dist/Dom/index.js +8 -0
  50. package/dist/Dom.d.ts +14 -0
  51. package/dist/Dom.d.ts.map +1 -0
  52. package/dist/Dom.js +13 -0
  53. package/dist/Focusable.d.ts +85 -0
  54. package/dist/Focusable.d.ts.map +1 -0
  55. package/dist/Focusable.js +35 -0
  56. package/dist/Form.d.ts +1695 -0
  57. package/dist/Form.d.ts.map +1 -0
  58. package/dist/Form.js +987 -0
  59. package/dist/Grid.d.ts +569 -0
  60. package/dist/Grid.d.ts.map +1 -0
  61. package/dist/Grid.js +379 -0
  62. package/dist/Group.d.ts +147 -0
  63. package/dist/Group.d.ts.map +1 -0
  64. package/dist/Group.js +63 -0
  65. package/dist/Heading.d.ts +86 -0
  66. package/dist/Heading.d.ts.map +1 -0
  67. package/dist/Heading.js +48 -0
  68. package/dist/Hovercard.d.ts +297 -0
  69. package/dist/Hovercard.d.ts.map +1 -0
  70. package/dist/Hovercard.js +188 -0
  71. package/dist/HttpRouter.d.ts +129 -6
  72. package/dist/HttpRouter.d.ts.map +1 -1
  73. package/dist/HttpRouter.js +196 -53
  74. package/dist/Link.d.ts +67 -28
  75. package/dist/Link.d.ts.map +1 -1
  76. package/dist/Link.js +91 -37
  77. package/dist/Listbox.d.ts +437 -0
  78. package/dist/Listbox.d.ts.map +1 -0
  79. package/dist/Listbox.js +316 -0
  80. package/dist/Menu.d.ts +972 -0
  81. package/dist/Menu.d.ts.map +1 -0
  82. package/dist/Menu.js +731 -0
  83. package/dist/Menubar.d.ts +367 -0
  84. package/dist/Menubar.d.ts.map +1 -0
  85. package/dist/Menubar.js +265 -0
  86. package/dist/Meter.d.ts +217 -0
  87. package/dist/Meter.d.ts.map +1 -0
  88. package/dist/Meter.js +94 -0
  89. package/dist/NativeDetails.d.ts +41 -0
  90. package/dist/NativeDetails.d.ts.map +1 -0
  91. package/dist/NativeDetails.js +40 -0
  92. package/dist/NativeDialog.d.ts +64 -0
  93. package/dist/NativeDialog.d.ts.map +1 -0
  94. package/dist/NativeDialog.js +50 -0
  95. package/dist/NativePopover.d.ts +41 -0
  96. package/dist/NativePopover.d.ts.map +1 -0
  97. package/dist/NativePopover.js +46 -0
  98. package/dist/Popover.d.ts +241 -0
  99. package/dist/Popover.d.ts.map +1 -0
  100. package/dist/Popover.js +141 -0
  101. package/dist/RadioGroup.d.ts +432 -0
  102. package/dist/RadioGroup.d.ts.map +1 -0
  103. package/dist/RadioGroup.js +291 -0
  104. package/dist/Role.d.ts +64 -0
  105. package/dist/Role.d.ts.map +1 -0
  106. package/dist/Role.js +27 -0
  107. package/dist/Select.d.ts +529 -0
  108. package/dist/Select.d.ts.map +1 -0
  109. package/dist/Select.js +407 -0
  110. package/dist/Separator.d.ts +54 -0
  111. package/dist/Separator.d.ts.map +1 -0
  112. package/dist/Separator.js +26 -0
  113. package/dist/Slider.d.ts +188 -0
  114. package/dist/Slider.d.ts.map +1 -0
  115. package/dist/Slider.js +96 -0
  116. package/dist/SpinButton.d.ts +188 -0
  117. package/dist/SpinButton.d.ts.map +1 -0
  118. package/dist/SpinButton.js +96 -0
  119. package/dist/Storybook.d.ts +76 -0
  120. package/dist/Storybook.d.ts.map +1 -0
  121. package/dist/Storybook.js +102 -0
  122. package/dist/Switch.d.ts +187 -0
  123. package/dist/Switch.d.ts.map +1 -0
  124. package/dist/Switch.js +116 -0
  125. package/dist/Tab.d.ts +26 -0
  126. package/dist/Tab.d.ts.map +1 -0
  127. package/dist/Tab.js +25 -0
  128. package/dist/Tabs.d.ts +591 -0
  129. package/dist/Tabs.d.ts.map +1 -0
  130. package/dist/Tabs.js +346 -0
  131. package/dist/Toolbar.d.ts +366 -0
  132. package/dist/Toolbar.d.ts.map +1 -0
  133. package/dist/Toolbar.js +245 -0
  134. package/dist/Tooltip.d.ts +296 -0
  135. package/dist/Tooltip.d.ts.map +1 -0
  136. package/dist/Tooltip.js +172 -0
  137. package/dist/Tree.d.ts +591 -0
  138. package/dist/Tree.d.ts.map +1 -0
  139. package/dist/Tree.js +434 -0
  140. package/dist/TreeGrid.d.ts +645 -0
  141. package/dist/TreeGrid.d.ts.map +1 -0
  142. package/dist/TreeGrid.js +420 -0
  143. package/dist/VisuallyHidden.d.ts +54 -0
  144. package/dist/VisuallyHidden.d.ts.map +1 -0
  145. package/dist/VisuallyHidden.js +27 -0
  146. package/dist/WindowSplitter.d.ts +376 -0
  147. package/dist/WindowSplitter.d.ts.map +1 -0
  148. package/dist/WindowSplitter.js +222 -0
  149. package/dist/index.d.ts +49 -0
  150. package/dist/index.d.ts.map +1 -1
  151. package/dist/index.js +49 -0
  152. package/package.json +45 -18
  153. package/src/HttpRouter.test.ts +0 -256
  154. package/src/HttpRouter.ts +0 -165
  155. package/src/Link.test.ts +0 -84
  156. package/src/Link.ts +0 -107
  157. package/src/index.ts +0 -2
package/dist/index.js CHANGED
@@ -1,2 +1,51 @@
1
+ /**
2
+ * Typed UI: DOM-cooperative components built on Effect, Fx, Router, and Template.
3
+ *
4
+ * @remarks
5
+ * Namespace exports group related component families while direct exports expose
6
+ * the component constructor, HTTP SSR adapters, and navigation link. The DOM is
7
+ * the integration boundary: hosts receive real nodes, events, attributes, refs,
8
+ * and web-standard behavior. Running Effect Scopes own subscriptions and cleanup.
9
+ *
10
+ * @since 1.0.0
11
+ * @category ui
12
+ */
13
+ export * as Button from "./Button.js";
14
+ export * as Alert from "./Alert.js";
15
+ export * as Checkbox from "./Checkbox.js";
16
+ export * as Carousel from "./Carousel.js";
17
+ export * as Collection from "./Collection.js";
18
+ export * as Combobox from "./Combobox.js";
19
+ export * from "./Component.js";
20
+ export * as Composite from "./Composite.js";
21
+ export * as Dialog from "./Dialog.js";
22
+ export * as Disclosure from "./Disclosure.js";
23
+ export * as Dom from "./Dom.js";
24
+ export * as Focusable from "./Focusable.js";
25
+ export * as Form from "./Form.js";
26
+ export * as Group from "./Group.js";
27
+ export * as Grid from "./Grid.js";
28
+ export * as Heading from "./Heading.js";
29
+ export * as Hovercard from "./Hovercard.js";
1
30
  export * from "./HttpRouter.js";
2
31
  export * from "./Link.js";
32
+ export * as Listbox from "./Listbox.js";
33
+ export * as Menu from "./Menu.js";
34
+ export * as Menubar from "./Menubar.js";
35
+ export * as Meter from "./Meter.js";
36
+ export * as Role from "./Role.js";
37
+ export * as RadioGroup from "./RadioGroup.js";
38
+ export * as Popover from "./Popover.js";
39
+ export * as Separator from "./Separator.js";
40
+ export * as Select from "./Select.js";
41
+ export * as Slider from "./Slider.js";
42
+ export * as SpinButton from "./SpinButton.js";
43
+ export * as Switch from "./Switch.js";
44
+ export * as Tooltip from "./Tooltip.js";
45
+ export * as Toolbar from "./Toolbar.js";
46
+ export * as Tree from "./Tree.js";
47
+ export * as TreeGrid from "./TreeGrid.js";
48
+ export * as Tab from "./Tab.js";
49
+ export * as Tabs from "./Tabs.js";
50
+ export * as VisuallyHidden from "./VisuallyHidden.js";
51
+ export * as WindowSplitter from "./WindowSplitter.js";
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "@typed/ui",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.5",
4
+ "description": "Web integration for Typed routing, templates, and Effect HTTP.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/TylorS/typed-smol/tree/main/packages/ui#readme",
7
+ "bugs": "https://github.com/TylorS/typed-smol/issues",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/TylorS/typed-smol.git",
11
+ "directory": "packages/ui"
12
+ },
4
13
  "files": [
5
- "dist",
6
- "src"
14
+ "dist"
7
15
  ],
8
16
  "type": "module",
9
17
  "exports": {
@@ -19,22 +27,41 @@
19
27
  "publishConfig": {
20
28
  "access": "public"
21
29
  },
22
- "scripts": {
23
- "build": "[ -d dist ] || rm -f tsconfig.tsbuildinfo; tsc",
24
- "test": "vitest run --passWithNoTests"
25
- },
26
30
  "dependencies": {
27
- "@effect/platform-node": "catalog:",
28
- "@typed/fx": "workspace:*",
29
- "@typed/id": "workspace:*",
30
- "@typed/navigation": "workspace:*",
31
- "@typed/router": "workspace:*",
32
- "@typed/template": "workspace:*",
33
- "effect": "catalog:",
34
- "happy-dom": "catalog:"
31
+ "@typed/fx": "2.0.0-beta.5",
32
+ "@typed/id": "1.0.0-beta.5",
33
+ "@typed/navigation": "1.0.0-beta.5",
34
+ "@typed/router": "1.0.0-beta.5",
35
+ "@typed/template": "1.0.0-beta.5",
36
+ "effect": "4.0.0-rc.112"
35
37
  },
36
38
  "devDependencies": {
37
- "typescript": "catalog:",
38
- "vitest": "catalog:"
39
+ "@effect/platform-node": "4.0.0-rc.112",
40
+ "@storybook/addon-a11y": "10.6.0",
41
+ "@storybook/addon-vitest": "10.6.0",
42
+ "@storybook/html-vite": "10.6.0",
43
+ "@tailwindcss/vite": "^4.3.3",
44
+ "@vitest/browser-playwright": "5.0.0",
45
+ "@vitest/coverage-v8": "5.0.0",
46
+ "daisyui": "^5.7.28",
47
+ "happy-dom": "20.13.2",
48
+ "playwright": "^1.62.1",
49
+ "storybook": "10.6.0",
50
+ "tailwindcss": "^4.3.3",
51
+ "typescript": "7.0.2",
52
+ "vitest": "5.0.0"
53
+ },
54
+ "scripts": {
55
+ "build": "node -e \"const fs=require('node:fs'); fs.rmSync('dist',{recursive:true,force:true}); fs.rmSync('tsconfig.tsbuildinfo',{force:true})\" && tsc",
56
+ "test": "pnpm run test:types && pnpm run test:node && pnpm run test:browser && pnpm run test:storybook",
57
+ "test:node": "vitest run --config vitest.config.ts --project=node",
58
+ "test:browser": "vitest run --config vitest.browser.config.ts --project firefox && vitest run --config vitest.browser.config.ts --project chromium && vitest run --config vitest.browser.config.ts --project webkit",
59
+ "test:types": "tsc -p tsconfig.type-tests.json && tsc -p tsconfig.tests.json && tsc -p tsconfig.stories.json",
60
+ "storybook": "storybook dev --ci --port 6006",
61
+ "storybook:build": "storybook build",
62
+ "storybook:serve": "vite preview --outDir storybook-static --port 6007 --strictPort",
63
+ "test:storybook": "vitest run --config vitest.config.ts --project=storybook",
64
+ "test:storybook:coverage": "vitest run --config vitest.config.ts --project=storybook --coverage",
65
+ "test:a11y": "pnpm run test:storybook"
39
66
  }
40
- }
67
+ }
@@ -1,256 +0,0 @@
1
- import { NodeHttpServer } from "@effect/platform-node";
2
- import { assert, describe, it } from "vitest";
3
- import { Effect, Layer } from "effect";
4
- import { Fx } from "@typed/fx";
5
- import { Ids } from "@typed/id";
6
- import { Navigation } from "@typed/navigation";
7
- import { CurrentRoute } from "@typed/router/CurrentRoute";
8
- import * as Matcher from "@typed/router/Matcher";
9
- import * as Route from "@typed/router/Route";
10
- import { html, StaticHtmlRenderTemplate } from "@typed/template";
11
- import { handleHttpServerError, ssrForHttp } from "./HttpRouter.js";
12
- import { HttpClient, HttpRouter } from "effect/unstable/http";
13
-
14
- describe("typed/ui/HttpRouter", () => {
15
- it("renders simple html template", () => {
16
- const matcher = Matcher.empty.match(Route.Parse("home"), html` <div>Hello, world!</div> `);
17
- const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
18
- Layer.provide(StaticHtmlRenderTemplate),
19
- HttpRouter.serve,
20
- Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
21
- );
22
- return Effect.gen(function* () {
23
- const response = yield* HttpClient.get("/home").pipe(Effect.flatMap((r) => r.text));
24
- assert.strictEqual(response, "<div>Hello, world!</div>");
25
- }).pipe(Effect.provide(Live), Effect.scoped, Effect.runPromise);
26
- });
27
-
28
- it("renders html template with route params", () => {
29
- const users = Route.Join(Route.Parse("users"), Route.Param("id"));
30
- const matcher = Matcher.empty.match(
31
- users,
32
- (params) => html`<div>User ${params.pipe(Fx.map((p) => p.id))}</div>`,
33
- );
34
- const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
35
- Layer.provide(StaticHtmlRenderTemplate),
36
- HttpRouter.serve,
37
- Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
38
- );
39
- return Effect.gen(function* () {
40
- const response = yield* HttpClient.get("/users/123").pipe(Effect.flatMap((r) => r.text));
41
- assert.strictEqual(response, "<div>User 123</div>");
42
- }).pipe(Effect.provide(Live), Effect.scoped, Effect.runPromise);
43
- });
44
-
45
- it("renders html template with search params", () => {
46
- const route = Route.Parse("search");
47
- const matcher = Matcher.empty.match(route, html` <div>Search results</div> `);
48
- const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
49
- Layer.provide(StaticHtmlRenderTemplate),
50
- HttpRouter.serve,
51
- Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
52
- );
53
- return Effect.gen(function* () {
54
- const response = yield* HttpClient.get("/search?q=test").pipe(Effect.flatMap((r) => r.text));
55
- assert.strictEqual(response, "<div>Search results</div>");
56
- }).pipe(Effect.provide(Live), Effect.scoped, Effect.runPromise);
57
- });
58
-
59
- it("handles multiple routes", () => {
60
- const home = Route.Parse("home");
61
- const about = Route.Parse("about");
62
- const matcher = Matcher.empty
63
- .match(home, html` <div>Home</div> `)
64
- .match(about, html` <div>About</div> `);
65
- const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
66
- Layer.provide(StaticHtmlRenderTemplate),
67
- HttpRouter.serve,
68
- Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
69
- );
70
- return Effect.gen(function* () {
71
- const homeResponse = yield* HttpClient.get("/home").pipe(Effect.flatMap((r) => r.text));
72
- assert.strictEqual(homeResponse, "<div>Home</div>");
73
- const aboutResponse = yield* HttpClient.get("/about").pipe(Effect.flatMap((r) => r.text));
74
- assert.strictEqual(aboutResponse, "<div>About</div>");
75
- }).pipe(Effect.provide(Live), Effect.scoped, Effect.runPromise);
76
- });
77
-
78
- it("returns 404 for unmatched routes", () => {
79
- const matcher = Matcher.empty.match(Route.Parse("home"), html` <div>Home</div> `);
80
- const Live = HttpRouter.use(
81
- Effect.fn(function* (router) {
82
- yield* ssrForHttp(router, matcher);
83
- yield* handleHttpServerError(router);
84
- }),
85
- ).pipe(
86
- Layer.provide(StaticHtmlRenderTemplate),
87
- HttpRouter.serve,
88
- Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
89
- );
90
- return Effect.gen(function* () {
91
- const response = yield* HttpClient.get("/notfound");
92
- assert.strictEqual(response.status, 404);
93
- }).pipe(Effect.provide(Live), Effect.scoped, Effect.runPromise);
94
- });
95
-
96
- it("renders dynamic content from Effect", () => {
97
- const matcher = Matcher.empty.match(
98
- Route.Parse("dynamic"),
99
- html`<div>Value: ${Effect.succeed("42")}</div>`,
100
- );
101
- const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
102
- HttpRouter.serve,
103
- Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
104
- Layer.provide(StaticHtmlRenderTemplate),
105
- );
106
- return Effect.gen(function* () {
107
- const response = yield* HttpClient.get("/dynamic").pipe(Effect.flatMap((r) => r.text));
108
- assert.strictEqual(response, "<div>Value: 42</div>");
109
- }).pipe(Effect.provide(Live), Effect.scoped, Effect.runPromise);
110
- });
111
-
112
- it("sets correct content-type header", () => {
113
- const matcher = Matcher.empty.match(Route.Parse("home"), html` <div>Hello</div> `);
114
- const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
115
- Layer.provide(StaticHtmlRenderTemplate),
116
- HttpRouter.serve,
117
- Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
118
- );
119
- return Effect.gen(function* () {
120
- const response = yield* HttpClient.get("/home");
121
- const contentType = response.headers["content-type"];
122
- assert.strictEqual(contentType, "text/html; charset=utf-8");
123
- }).pipe(Effect.provide(Live), Effect.scoped, Effect.runPromise);
124
- });
125
-
126
- it("handles nested routes", () => {
127
- const users = Route.Join(Route.Parse("api"), Route.Parse("users"));
128
- const user = Route.Join(users, Route.Param("id"));
129
- const matcher = Matcher.empty
130
- .match(users, html` <div>Users list</div> `)
131
- .match(user, (params) => html`<div>User ${params.pipe(Fx.map((p) => p.id))}</div>`);
132
- const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
133
- Layer.provide(StaticHtmlRenderTemplate),
134
- HttpRouter.serve,
135
- Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
136
- );
137
- return Effect.gen(function* () {
138
- const listResponse = yield* HttpClient.get("/api/users").pipe(Effect.flatMap((r) => r.text));
139
- assert.strictEqual(listResponse, "<div>Users list</div>");
140
- const userResponse = yield* HttpClient.get("/api/users/456").pipe(
141
- Effect.flatMap((r) => r.text),
142
- );
143
- assert.strictEqual(userResponse, "<div>User 456</div>");
144
- }).pipe(Effect.provide(Live), Effect.scoped, Effect.runPromise);
145
- });
146
-
147
- it("provides Navigation with correct base and origin", () => {
148
- const matcher = Matcher.empty.match(
149
- Route.Parse("test"),
150
- Fx.gen(function* () {
151
- const origin = yield* Navigation.origin;
152
- const base = yield* Navigation.base;
153
- const currentEntry = yield* Navigation.currentEntry;
154
- return html`<div
155
- data-origin="${origin}"
156
- data-base="${base}"
157
- data-url="${currentEntry.url.href}"
158
- ></div>`;
159
- }),
160
- );
161
- const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
162
- Layer.provide(StaticHtmlRenderTemplate),
163
- HttpRouter.serve,
164
- Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
165
- );
166
- return Effect.gen(function* () {
167
- const response = yield* HttpClient.get("/test").pipe(Effect.flatMap((r) => r.text));
168
- assert.ok(response.includes('data-origin="http://localhost"'));
169
- assert.ok(response.includes('data-base="/"'));
170
- assert.ok(response.includes('data-url="http://localhost/test"'));
171
- }).pipe(Effect.provide(Live), Effect.scoped, Effect.runPromise);
172
- });
173
-
174
- it("provides CurrentRoute with correct route path", () => {
175
- const matcher = Matcher.empty.match(
176
- Route.Parse("users"),
177
- Fx.gen(function* () {
178
- const currentRoute = yield* CurrentRoute;
179
- return html`<div data-path="${currentRoute.route.path}"></div>`;
180
- }),
181
- );
182
- const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
183
- Layer.provide(StaticHtmlRenderTemplate),
184
- HttpRouter.serve,
185
- Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
186
- );
187
- return Effect.gen(function* () {
188
- const response = yield* HttpClient.get("/users").pipe(Effect.flatMap((r) => r.text));
189
- assert.ok(response.includes('data-path="/users"'));
190
- }).pipe(Effect.provide(Live), Effect.scoped, Effect.runPromise);
191
- });
192
-
193
- it("provides CurrentRoute with no parent for root routes", () => {
194
- const matcher = Matcher.empty.match(
195
- Route.Parse("home"),
196
- Fx.gen(function* () {
197
- const currentRoute = yield* CurrentRoute;
198
- const hasParent = currentRoute.parent !== undefined;
199
- return html`<div data-has-parent="${hasParent}"></div>`;
200
- }),
201
- );
202
- const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
203
- Layer.provide(StaticHtmlRenderTemplate),
204
- HttpRouter.serve,
205
- Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
206
- );
207
- return Effect.gen(function* () {
208
- const response = yield* HttpClient.get("/home").pipe(Effect.flatMap((r) => r.text));
209
- assert.ok(response.includes('data-has-parent="false"'));
210
- }).pipe(Effect.provide(Live), Effect.scoped, Effect.runPromise);
211
- });
212
-
213
- it("provides CurrentRoute with parent for nested routes", () => {
214
- const api = Route.Parse("api");
215
- const users = Route.Parse("users");
216
- const matcher = Matcher.empty.match(Route.Slash, html` <div>API</div> `).match(
217
- users,
218
- Fx.gen(function* () {
219
- const currentRoute = yield* CurrentRoute;
220
- const hasParent = currentRoute.parent !== undefined;
221
- const parentPath = currentRoute.parent?.route.path ?? "none";
222
- return html`<div data-has-parent="${hasParent}" data-parent-path="${parentPath}"></div>`;
223
- }),
224
- );
225
- const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
226
- Layer.provide(StaticHtmlRenderTemplate),
227
- HttpRouter.serve,
228
- Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
229
- Layer.provide(CurrentRoute.extend(api)),
230
- );
231
- return Effect.gen(function* () {
232
- const response = yield* HttpClient.get("/api/users").pipe(Effect.flatMap((r) => r.text));
233
- assert.ok(response.includes('data-has-parent="true"'));
234
- assert.ok(response.includes('data-parent-path="/api"'));
235
- }).pipe(Effect.provide(Live), Effect.scoped, Effect.runPromise);
236
- });
237
-
238
- it("provides Navigation with correct currentEntry for different paths", () => {
239
- const matcher = Matcher.empty.match(
240
- Route.Parse("about"),
241
- Fx.gen(function* () {
242
- const currentEntry = yield* Navigation.currentEntry;
243
- return html`<div data-pathname="${currentEntry.url.pathname}"></div>`;
244
- }),
245
- );
246
- const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
247
- Layer.provide(StaticHtmlRenderTemplate),
248
- HttpRouter.serve,
249
- Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
250
- );
251
- return Effect.gen(function* () {
252
- const response = yield* HttpClient.get("/about").pipe(Effect.flatMap((r) => r.text));
253
- assert.ok(response.includes('data-pathname="/about"'));
254
- }).pipe(Effect.provide(Live), Effect.scoped, Effect.runPromise);
255
- });
256
- });
package/src/HttpRouter.ts DELETED
@@ -1,165 +0,0 @@
1
- import * as Effect from "effect/Effect";
2
- import * as Exit from "effect/Exit";
3
- import { dual } from "effect/Function";
4
- import * as Layer from "effect/Layer";
5
- import * as Option from "effect/Option";
6
- import * as Scope from "effect/Scope";
7
- import * as Context from "effect/Context";
8
- import { type HttpRouter, type Route, RouteContext } from "effect/unstable/http/HttpRouter";
9
- import * as HttpServerError from "effect/unstable/http/HttpServerError";
10
- import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
11
- import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
12
- import { RefSubject } from "@typed/fx";
13
- import {
14
- type CurrentRouteTree,
15
- type CompiledEntry,
16
- compile,
17
- CurrentRoute,
18
- makeCatchManager,
19
- makeLayerManager,
20
- makeLayoutManager,
21
- type Matcher,
22
- Join,
23
- Parse,
24
- type Router,
25
- } from "@typed/router";
26
- import { initialMemory } from "@typed/navigation";
27
- import { renderToHtmlString, type RenderEvent } from "@typed/template";
28
-
29
- type ProvidedForSsr = Scope.Scope | Router;
30
-
31
- export const ssrForHttp: {
32
- <E, R>(
33
- input: Matcher<RenderEvent, E, R>,
34
- ): (router: HttpRouter) => Effect.Effect<void, never, Exclude<R, ProvidedForSsr>>;
35
- <E, R>(
36
- router: HttpRouter,
37
- input: Matcher<RenderEvent, E, R>,
38
- ): Effect.Effect<void, never, Exclude<R, ProvidedForSsr>>;
39
- } = dual(2, <E, R>(router: HttpRouter, input: Matcher<RenderEvent, E, R>) => {
40
- return Effect.gen(function* () {
41
- const matcher = Option.match(yield* Effect.serviceOption(CurrentRoute), {
42
- onNone: () => input,
43
- onSome: (parent: CurrentRouteTree) => input.prefix(parent.route),
44
- });
45
- const entries = compile(matcher.cases);
46
- const currentServices = yield* Effect.context<R>();
47
-
48
- yield* router.addAll(entries.map((e: CompiledEntry) => toRoute(e, currentServices)));
49
- });
50
- });
51
-
52
- export function handleHttpServerError(router: HttpRouter) {
53
- return router.addGlobalMiddleware(
54
- Effect.catch((error: unknown) =>
55
- HttpServerError.isHttpServerError(error)
56
- ? Effect.succeed(HttpServerResponse.text(error.message, { status: getStatus(error) }))
57
- : Effect.fail(error),
58
- ),
59
- );
60
- }
61
-
62
- function getStatus(error: HttpServerError.HttpServerError): number {
63
- switch (error.reason._tag) {
64
- case "RouteNotFound":
65
- return 404;
66
- case "RequestParseError":
67
- return 400;
68
- case "InternalError":
69
- case "ResponseError":
70
- return 500;
71
- }
72
- }
73
-
74
- function toRoute(entry: CompiledEntry, currentServices: Context.Context<never>): Route<any, any> {
75
- return {
76
- ["~effect/http/HttpRouter/Route"]: "~effect/http/HttpRouter/Route",
77
- method: "GET",
78
- path: entry.route.path,
79
- handler: Effect.gen(function* () {
80
- const fiberId = yield* Effect.fiberId;
81
- const rootScope = yield* Effect.scope;
82
- const routeContext = yield* RouteContext;
83
- const request = yield* HttpServerRequest.HttpServerRequest;
84
- const searchParams = yield* HttpServerRequest.ParsedSearchParams;
85
- const provided = Layer.mergeAll(
86
- initialMemory({ url: request.url }),
87
- Layer.succeed(
88
- CurrentRoute,
89
- yield* Effect.serviceOption(CurrentRoute).pipe(
90
- Effect.map(
91
- Option.match({
92
- onNone: (): CurrentRouteTree => ({
93
- route: Parse(request.url),
94
- parent: undefined,
95
- }),
96
- onSome: (parent: CurrentRouteTree): CurrentRouteTree => ({
97
- route: Join(parent.route, Parse(request.url)),
98
- parent,
99
- }),
100
- }),
101
- ),
102
- ),
103
- ),
104
- );
105
- const input = { ...routeContext.params, ...searchParams };
106
-
107
- const params = yield* Effect.mapError(
108
- entry.decode(input),
109
- (cause) =>
110
- new HttpServerError.HttpServerError({
111
- reason: new HttpServerError.RequestParseError({ request, cause }),
112
- }),
113
- );
114
-
115
- const memoMap = yield* Layer.makeMemoMap;
116
- const layerManager = makeLayerManager(memoMap, rootScope, fiberId);
117
- const layoutManager = makeLayoutManager(rootScope, fiberId);
118
- const catchManager = makeCatchManager(rootScope, fiberId);
119
- const prepared = yield* layerManager.prepare(entry.layers.concat(provided));
120
-
121
- const guardExit = yield* entry
122
- .guard(params)
123
- .pipe(Effect.provideContext(prepared.services), Effect.exit);
124
-
125
- if (Exit.isFailure(guardExit) || Option.isNone(guardExit.value)) {
126
- yield* prepared.rollback;
127
- return yield* new HttpServerError.HttpServerError({
128
- reason: new HttpServerError.RouteNotFound({ request }),
129
- });
130
- }
131
-
132
- const matchedParams = guardExit.value.value;
133
- yield* prepared.commit;
134
-
135
- const scope = yield* Scope.fork(rootScope);
136
- const paramsRef = yield* RefSubject.make(matchedParams).pipe(Scope.provide(scope));
137
-
138
- const preparedServices = prepared.services as Context.Context<any>;
139
- const handlerServices = Context.merge(
140
- Context.merge(currentServices, preparedServices),
141
- Context.make(Scope.Scope, scope),
142
- );
143
-
144
- const handlerFx = entry.handler(paramsRef);
145
-
146
- const withLayouts = yield* layoutManager.apply(
147
- entry.layouts,
148
- matchedParams,
149
- handlerFx,
150
- preparedServices,
151
- );
152
-
153
- const withCatches = yield* catchManager.apply(entry.catches, withLayouts, preparedServices);
154
-
155
- const html = yield* renderToHtmlString(withCatches).pipe(
156
- Effect.provideContext(handlerServices),
157
- );
158
- return HttpServerResponse.text(html, {
159
- headers: { "content-type": "text/html; charset=utf-8" },
160
- });
161
- }),
162
- uninterruptible: false,
163
- prefix: Option.none(),
164
- };
165
- }
package/src/Link.test.ts DELETED
@@ -1,84 +0,0 @@
1
- import { assert, describe, it } from "vitest";
2
- import { Effect } from "effect";
3
- import * as Layer from "effect/Layer";
4
- import { Fx } from "@typed/fx";
5
- import { CurrentPath } from "@typed/navigation";
6
- import { ServerRouter } from "@typed/router/Router";
7
- import { DomRenderTemplate, render } from "@typed/template";
8
- import { Link } from "./Link.js";
9
- import { Window } from "happy-dom";
10
-
11
- describe("typed/ui/Link", () => {
12
- it("renders <a> with href and content", () =>
13
- Effect.gen(function* () {
14
- const [window, layer] = createHappyDomLayer();
15
-
16
- const [root] = yield* render(
17
- Link({ href: "/about", content: "Go to about" }),
18
- window.document.body,
19
- ).pipe(Fx.provide(layer), Fx.take(1), Fx.collectAll);
20
-
21
- assert(root instanceof window.HTMLElement);
22
- const anchor = root as HTMLAnchorElement;
23
- assert(anchor.tagName === "A");
24
- assert(anchor.getAttribute("href") === "/about");
25
- assert(anchor.textContent === "Go to about");
26
- }).pipe(Effect.scoped, Effect.runPromise));
27
-
28
- it("intercepts same-origin click and navigates", () => {
29
- const [window, layer] = createHappyDomLayer({ url: "http://localhost/" });
30
- return Effect.gen(function* () {
31
- const [root] = yield* render(
32
- Link({ href: "/about", content: "Go" }),
33
- window.document.body,
34
- ).pipe(Fx.take(1), Fx.collectAll);
35
-
36
- assert(root instanceof window.HTMLElement);
37
- const anchor = root as HTMLAnchorElement;
38
- assert(anchor.tagName === "A");
39
-
40
- const pathBefore = yield* CurrentPath;
41
- anchor.click();
42
- yield* Effect.sleep(50);
43
- const pathAfter = yield* CurrentPath;
44
-
45
- assert(pathBefore === "/" || pathBefore === "");
46
- assert(pathAfter === "/about");
47
- }).pipe(Effect.provide(layer), Effect.scoped, Effect.runPromise);
48
- });
49
-
50
- it("does not intercept when modifier key is pressed", () => {
51
- const [window, layer] = createHappyDomLayer();
52
- return Effect.gen(function* () {
53
- const [root] = yield* render(
54
- Link({ href: "/other", content: "Go" }),
55
- window.document.body,
56
- ).pipe(Fx.take(1), Fx.collectAll);
57
-
58
- assert(root instanceof window.HTMLElement);
59
- const anchor = root as HTMLAnchorElement;
60
- assert(anchor.tagName === "A");
61
-
62
- const ev = new window.MouseEvent("click", {
63
- bubbles: true,
64
- cancelable: true,
65
- ctrlKey: true,
66
- });
67
- anchor.dispatchEvent(ev);
68
- yield* Effect.sleep(50);
69
- const pathAfter = yield* CurrentPath;
70
-
71
- assert(pathAfter === "/" || pathAfter === "");
72
- }).pipe(Effect.provide(layer), Effect.scoped, Effect.runPromise);
73
- });
74
- });
75
-
76
- function createHappyDomLayer(...params: ConstructorParameters<typeof Window>) {
77
- const window = new Window(...params) as unknown as globalThis.Window & typeof globalThis;
78
- const domLayer = DomRenderTemplate.using(window.document);
79
- const opts = params[0] as { url?: string } | undefined;
80
- const url = opts?.url ?? "http://localhost/";
81
- const routerLayer = ServerRouter({ url });
82
- const layer = domLayer.pipe(Layer.merge(routerLayer));
83
- return [window, layer] as const;
84
- }