@xyd-js/source-react-runtime 0.0.0-build-23166ce-20260423151359
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/__fixtures__/-1.vite-lib.custom-property/input/package.json +8 -0
- package/__fixtures__/-1.vite-lib.custom-property/input/src/UserCard.tsx +27 -0
- package/__fixtures__/-1.vite-lib.custom-property/input/src/index.ts +1 -0
- package/__fixtures__/-1.vite-lib.custom-property/input/tsconfig.json +12 -0
- package/__fixtures__/-1.vite-lib.custom-property/input/vite.config.ts +22 -0
- package/__fixtures__/-1.vite-lib.custom-property/output.js +10 -0
- package/__fixtures__/1.vite-lib.user-card/input/package.json +8 -0
- package/__fixtures__/1.vite-lib.user-card/input/src/UserCard.tsx +27 -0
- package/__fixtures__/1.vite-lib.user-card/input/src/index.ts +1 -0
- package/__fixtures__/1.vite-lib.user-card/input/tsconfig.json +12 -0
- package/__fixtures__/1.vite-lib.user-card/input/vite.config.ts +22 -0
- package/__fixtures__/1.vite-lib.user-card/output.js +10 -0
- package/__fixtures__/2.vite-lib.sample-app/input/package.json +8 -0
- package/__fixtures__/2.vite-lib.sample-app/input/src/components/UserProfile.tsx +42 -0
- package/__fixtures__/2.vite-lib.sample-app/input/src/contexts/UserContext.tsx +27 -0
- package/__fixtures__/2.vite-lib.sample-app/input/src/index.ts +3 -0
- package/__fixtures__/2.vite-lib.sample-app/input/src/types/user.ts +18 -0
- package/__fixtures__/2.vite-lib.sample-app/input/tsconfig.json +12 -0
- package/__fixtures__/2.vite-lib.sample-app/input/vite.config.ts +22 -0
- package/__fixtures__/2.vite-lib.sample-app/output.js +27 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/package.json +8 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/src/components/AddTodoForm.tsx +51 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/src/components/FilterBar.tsx +56 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/src/components/StatsPanel.tsx +44 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/src/components/TodoItem.tsx +54 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/src/index.ts +6 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/src/types/todo.ts +23 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/tsconfig.json +12 -0
- package/__fixtures__/3.vite-lib.sample-real-app/input/vite.config.ts +22 -0
- package/__fixtures__/3.vite-lib.sample-real-app/output.js +63 -0
- package/__fixtures__/4.vite-app.user-card/input/package.json +8 -0
- package/__fixtures__/4.vite-app.user-card/input/src/UserCard.tsx +27 -0
- package/__fixtures__/4.vite-app.user-card/input/src/index.ts +1 -0
- package/__fixtures__/4.vite-app.user-card/input/tsconfig.json +12 -0
- package/__fixtures__/4.vite-app.user-card/input/vite.config.ts +23 -0
- package/__fixtures__/4.vite-app.user-card/output.js +10 -0
- package/__fixtures__/5.rollup.user-card/input/package.json +8 -0
- package/__fixtures__/5.rollup.user-card/input/rollup.config.mjs +20 -0
- package/__fixtures__/5.rollup.user-card/input/src/UserCard.tsx +27 -0
- package/__fixtures__/5.rollup.user-card/input/src/index.ts +1 -0
- package/__fixtures__/5.rollup.user-card/input/tsconfig.json +12 -0
- package/__fixtures__/5.rollup.user-card/output.js +11 -0
- package/__fixtures__/6.esbuild.user-card/input/esbuild.config.mjs +19 -0
- package/__fixtures__/6.esbuild.user-card/input/package.json +8 -0
- package/__fixtures__/6.esbuild.user-card/input/src/UserCard.tsx +27 -0
- package/__fixtures__/6.esbuild.user-card/input/src/index.ts +1 -0
- package/__fixtures__/6.esbuild.user-card/input/tsconfig.json +12 -0
- package/__fixtures__/6.esbuild.user-card/output.js +11 -0
- package/__fixtures__/7.react-router.app/input/app/components/ProductCard.tsx +26 -0
- package/__fixtures__/7.react-router.app/input/app/entry.server.tsx +36 -0
- package/__fixtures__/7.react-router.app/input/app/root.tsx +23 -0
- package/__fixtures__/7.react-router.app/input/app/routes/cart.tsx +16 -0
- package/__fixtures__/7.react-router.app/input/app/routes/home.tsx +29 -0
- package/__fixtures__/7.react-router.app/input/app/routes/product.tsx +6 -0
- package/__fixtures__/7.react-router.app/input/app/routes.ts +7 -0
- package/__fixtures__/7.react-router.app/input/app/types/product.ts +12 -0
- package/__fixtures__/7.react-router.app/input/package.json +8 -0
- package/__fixtures__/7.react-router.app/input/react-router.config.ts +2 -0
- package/__fixtures__/7.react-router.app/input/tsconfig.json +13 -0
- package/__fixtures__/7.react-router.app/input/vite.config.ts +14 -0
- package/__fixtures__/7.react-router.app/output.js +44 -0
- package/__fixtures__/8.tanstack-router.app/input/index.html +5 -0
- package/__fixtures__/8.tanstack-router.app/input/package.json +8 -0
- package/__fixtures__/8.tanstack-router.app/input/src/components/EmployeeTable.tsx +45 -0
- package/__fixtures__/8.tanstack-router.app/input/src/main.tsx +19 -0
- package/__fixtures__/8.tanstack-router.app/input/src/routeTree.gen.ts +77 -0
- package/__fixtures__/8.tanstack-router.app/input/src/routes/__root.tsx +13 -0
- package/__fixtures__/8.tanstack-router.app/input/src/routes/employees.tsx +23 -0
- package/__fixtures__/8.tanstack-router.app/input/src/routes/index.tsx +5 -0
- package/__fixtures__/8.tanstack-router.app/input/src/types/employee.ts +13 -0
- package/__fixtures__/8.tanstack-router.app/input/tsconfig.json +12 -0
- package/__fixtures__/8.tanstack-router.app/input/vite.config.ts +21 -0
- package/__fixtures__/8.tanstack-router.app/output.js +63 -0
- package/__tests__/source-react-runtime.test.ts +61 -0
- package/__tests__/utils.ts +100 -0
- package/dist/esbuild.d.ts +20 -0
- package/dist/esbuild.js +378 -0
- package/dist/esbuild.js.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +348 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
- package/src/esbuild.ts +45 -0
- package/src/index.ts +437 -0
- package/src/json-schema-to-uniform.ts +108 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsup.json +6 -0
- package/tsup.config.ts +23 -0
- package/vitest.config.ts +7 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) LiveSession Sp.z.o.o
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @xyd-js/source-react-runtime
|
|
2
|
+
|
|
3
|
+
Build plugin that auto-detects React components and injects runtime type metadata (`__xydUniform` by default) using [typia](https://typia.io/) for TypeScript type resolution and [@xyd-js/openapi](../xyd-openapi) for JSON Schema → xyd uniform conversion.
|
|
4
|
+
|
|
5
|
+
No manual annotations needed — just add the plugin to your build config.
|
|
6
|
+
|
|
7
|
+
## Supported bundlers
|
|
8
|
+
|
|
9
|
+
| Bundler | Import | Build command |
|
|
10
|
+
|---------|--------|---------------|
|
|
11
|
+
| **Vite** | `@xyd-js/source-react-runtime` | `vite build` |
|
|
12
|
+
| **Rollup** | `@xyd-js/source-react-runtime` | `rollup -c` |
|
|
13
|
+
| **esbuild** | `@xyd-js/source-react-runtime/esbuild` | `node esbuild.config.mjs` |
|
|
14
|
+
| **React Router v7** | `@xyd-js/source-react-runtime` | `react-router build` |
|
|
15
|
+
| **TanStack Router** | `@xyd-js/source-react-runtime` | `vite build` |
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
### Vite / React Router / TanStack Router
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// vite.config.ts
|
|
23
|
+
import { defineConfig } from "vite";
|
|
24
|
+
import react from "@vitejs/plugin-react";
|
|
25
|
+
import { xydSourceReactRuntime } from "@xyd-js/source-react-runtime";
|
|
26
|
+
|
|
27
|
+
export default defineConfig({
|
|
28
|
+
plugins: [
|
|
29
|
+
xydSourceReactRuntime(),
|
|
30
|
+
react(),
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Rollup
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
// rollup.config.mjs
|
|
39
|
+
import typescript from "@rollup/plugin-typescript";
|
|
40
|
+
import { xydSourceReactRuntime } from "@xyd-js/source-react-runtime";
|
|
41
|
+
|
|
42
|
+
export default {
|
|
43
|
+
input: "src/index.ts",
|
|
44
|
+
plugins: [
|
|
45
|
+
xydSourceReactRuntime(),
|
|
46
|
+
typescript(),
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### esbuild
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
// esbuild.config.mjs
|
|
55
|
+
import * as esbuild from "esbuild";
|
|
56
|
+
import { xydSourceReactRuntimeEsbuild } from "@xyd-js/source-react-runtime/esbuild";
|
|
57
|
+
|
|
58
|
+
await esbuild.build({
|
|
59
|
+
entryPoints: ["src/index.ts"],
|
|
60
|
+
outfile: "dist/index.js",
|
|
61
|
+
bundle: true,
|
|
62
|
+
format: "esm",
|
|
63
|
+
plugins: [
|
|
64
|
+
xydSourceReactRuntimeEsbuild(),
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Options
|
|
70
|
+
|
|
71
|
+
All options are optional.
|
|
72
|
+
|
|
73
|
+
| Option | Type | Default | Description |
|
|
74
|
+
|--------|------|---------|-------------|
|
|
75
|
+
| `tsconfig` | `string` | `./tsconfig.json` | Path to `tsconfig.json`. Configurable for monorepos or custom locations. |
|
|
76
|
+
| `propertyName` | `string` | `__xydUniform` | Property name for the injected metadata |
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
xydSourceReactRuntime({
|
|
80
|
+
tsconfig: resolve(__dirname, "tsconfig.json"),
|
|
81
|
+
propertyName: "__docs", // Component.__docs = JSON.parse('...')
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## What it detects
|
|
86
|
+
|
|
87
|
+
The plugin auto-detects:
|
|
88
|
+
|
|
89
|
+
- **Exported function components** — `export function MyComponent(props: Props) { ... }`
|
|
90
|
+
- **Exported arrow components** — `export const MyComponent = (props: Props) => { ... }`
|
|
91
|
+
- **Re-exported components** — `function MyComponent(props: Props) { ... } export { MyComponent }`
|
|
92
|
+
- **Contexts** — `export const MyContext = createContext<ValueType>(null)`
|
|
93
|
+
|
|
94
|
+
Props with `React.*` types (e.g. `React.ReactNode`) are skipped because typia cannot resolve React's internal types.
|
|
95
|
+
|
|
96
|
+
## How it works
|
|
97
|
+
|
|
98
|
+
1. **Detect** — scans all source files from `tsconfig.json` using the TypeScript compiler AST to find exported components and their props type names
|
|
99
|
+
2. **Inject** — creates in-memory modified source files with `typia.json.schemas<[PropsType]>()` calls appended
|
|
100
|
+
3. **Transform** — runs typia's TypeScript transform on the full program (all files) for cross-file type resolution
|
|
101
|
+
4. **Convert** — converts the resulting JSON Schema to xyd uniform format using `@xyd-js/openapi`'s `schemaObjectToUniformDefinitionProperties`
|
|
102
|
+
5. **Output** — replaces the typia call with `Component.{propertyName} = JSON.parse('...')`
|
|
103
|
+
|
|
104
|
+
## Output format
|
|
105
|
+
|
|
106
|
+
Each component gets a static property with the [xyd uniform Reference](../xyd-uniform) format:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
// Build output
|
|
110
|
+
function UserCard(props) { /* ... */ }
|
|
111
|
+
UserCard.__xydUniform = JSON.parse('{"title":"UserCard","definitions":[{"title":"Props","properties":[{"name":"name","type":"string","meta":[{"name":"required","value":"true"}]},{"name":"email","type":"string","meta":[{"name":"required","value":"true"}]}]}]}');
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
At runtime, access the metadata:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { UserCard } from "./components";
|
|
118
|
+
|
|
119
|
+
console.log(UserCard.__xydUniform);
|
|
120
|
+
// { title: "UserCard", definitions: [{ title: "Props", properties: [...] }] }
|
|
121
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
interface UserCardProps {
|
|
2
|
+
/** Full name of the user */
|
|
3
|
+
name: string;
|
|
4
|
+
|
|
5
|
+
/** Email address of the user */
|
|
6
|
+
email: string;
|
|
7
|
+
|
|
8
|
+
/** URL to the user's avatar image */
|
|
9
|
+
avatarUrl?: string;
|
|
10
|
+
|
|
11
|
+
/** Role or title of the user */
|
|
12
|
+
role?: "admin" | "editor" | "viewer";
|
|
13
|
+
|
|
14
|
+
/** Whether the card is in a loading state */
|
|
15
|
+
loading?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function UserCard(props: UserCardProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div>
|
|
21
|
+
{props.avatarUrl && <img src={props.avatarUrl} alt={props.name} />}
|
|
22
|
+
<h3>{props.name}</h3>
|
|
23
|
+
<p>{props.email}</p>
|
|
24
|
+
{props.role && <span>{props.role}</span>}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {UserCard} from './UserCard';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {defineConfig} from 'vite';
|
|
2
|
+
import {resolve} from 'node:path';
|
|
3
|
+
import react from '@vitejs/plugin-react';
|
|
4
|
+
import {xydSourceReactRuntime} from '@xyd-js/source-react-runtime';
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [
|
|
8
|
+
xydSourceReactRuntime({tsconfig: resolve(__dirname, 'tsconfig.json'), propertyName: '__docs'}),
|
|
9
|
+
react(),
|
|
10
|
+
],
|
|
11
|
+
build: {
|
|
12
|
+
lib: {
|
|
13
|
+
entry: 'src/index.ts',
|
|
14
|
+
formats: ['es'],
|
|
15
|
+
fileName: 'index',
|
|
16
|
+
},
|
|
17
|
+
rollupOptions: {
|
|
18
|
+
external: ['react', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom'],
|
|
19
|
+
},
|
|
20
|
+
minify: false,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// === index.js ===
|
|
2
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
+
function UserCard(props) {
|
|
4
|
+
return jsxs("div", { children: [props.avatarUrl && jsx("img", { src: props.avatarUrl, alt: props.name }), jsx("h3", { children: props.name }), jsx("p", { children: props.email }), props.role && jsx("span", { children: props.role })] });
|
|
5
|
+
}
|
|
6
|
+
UserCard.__docs = JSON.parse(`{"title":"UserCard","canonical":"","description":"","definitions":[{"title":"Props","properties":[{"name":"name","type":"string","description":"Full name of the user","meta":[{"name":"required","value":"true"}]},{"name":"email","type":"string","description":"Email address of the user","meta":[{"name":"required","value":"true"}]},{"name":"avatarUrl","type":"string","description":"URL to the user's avatar image","meta":[]},{"name":"role","type":"$xor","description":"Role or title of the user","properties":[{"name":"role","type":"object","description":"","meta":[]},{"name":"role","type":"object","description":"","meta":[]},{"name":"role","type":"object","description":"","meta":[]}],"meta":[]},{"name":"loading","type":"boolean","description":"Whether the card is in a loading state","meta":[]}],"meta":[{"name":"type","value":"parameters"}]}],"examples":{"groups":[]}}`);
|
|
7
|
+
export {
|
|
8
|
+
UserCard
|
|
9
|
+
};
|
|
10
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
interface UserCardProps {
|
|
2
|
+
/** Full name of the user */
|
|
3
|
+
name: string;
|
|
4
|
+
|
|
5
|
+
/** Email address of the user */
|
|
6
|
+
email: string;
|
|
7
|
+
|
|
8
|
+
/** URL to the user's avatar image */
|
|
9
|
+
avatarUrl?: string;
|
|
10
|
+
|
|
11
|
+
/** Role or title of the user */
|
|
12
|
+
role?: "admin" | "editor" | "viewer";
|
|
13
|
+
|
|
14
|
+
/** Whether the card is in a loading state */
|
|
15
|
+
loading?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function UserCard(props: UserCardProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div>
|
|
21
|
+
{props.avatarUrl && <img src={props.avatarUrl} alt={props.name} />}
|
|
22
|
+
<h3>{props.name}</h3>
|
|
23
|
+
<p>{props.email}</p>
|
|
24
|
+
{props.role && <span>{props.role}</span>}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {UserCard} from './UserCard';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {defineConfig} from 'vite';
|
|
2
|
+
import {resolve} from 'node:path';
|
|
3
|
+
import react from '@vitejs/plugin-react';
|
|
4
|
+
import {xydSourceReactRuntime} from '@xyd-js/source-react-runtime';
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [
|
|
8
|
+
xydSourceReactRuntime(),
|
|
9
|
+
react(),
|
|
10
|
+
],
|
|
11
|
+
build: {
|
|
12
|
+
lib: {
|
|
13
|
+
entry: 'src/index.ts',
|
|
14
|
+
formats: ['es'],
|
|
15
|
+
fileName: 'index',
|
|
16
|
+
},
|
|
17
|
+
rollupOptions: {
|
|
18
|
+
external: ['react', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom'],
|
|
19
|
+
},
|
|
20
|
+
minify: false,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// === index.js ===
|
|
2
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
+
function UserCard(props) {
|
|
4
|
+
return jsxs("div", { children: [props.avatarUrl && jsx("img", { src: props.avatarUrl, alt: props.name }), jsx("h3", { children: props.name }), jsx("p", { children: props.email }), props.role && jsx("span", { children: props.role })] });
|
|
5
|
+
}
|
|
6
|
+
UserCard.__xydUniform = JSON.parse(`{"title":"UserCard","canonical":"","description":"","definitions":[{"title":"Props","properties":[{"name":"name","type":"string","description":"Full name of the user","meta":[{"name":"required","value":"true"}]},{"name":"email","type":"string","description":"Email address of the user","meta":[{"name":"required","value":"true"}]},{"name":"avatarUrl","type":"string","description":"URL to the user's avatar image","meta":[]},{"name":"role","type":"$xor","description":"Role or title of the user","properties":[{"name":"role","type":"object","description":"","meta":[]},{"name":"role","type":"object","description":"","meta":[]},{"name":"role","type":"object","description":"","meta":[]}],"meta":[]},{"name":"loading","type":"boolean","description":"Whether the card is in a loading state","meta":[]}],"meta":[{"name":"type","value":"parameters"}]}],"examples":{"groups":[]}}`);
|
|
7
|
+
export {
|
|
8
|
+
UserCard
|
|
9
|
+
};
|
|
10
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { User } from "../types/user";
|
|
2
|
+
|
|
3
|
+
interface UserProfileProps {
|
|
4
|
+
/** The user object to display */
|
|
5
|
+
user: User;
|
|
6
|
+
|
|
7
|
+
/** Show the full address block */
|
|
8
|
+
showAddress?: boolean;
|
|
9
|
+
|
|
10
|
+
/** Called when the edit button is clicked */
|
|
11
|
+
onEdit: (userId: number) => void;
|
|
12
|
+
|
|
13
|
+
/** Custom CSS class name */
|
|
14
|
+
className?: string;
|
|
15
|
+
|
|
16
|
+
/** Maximum number of tags to display */
|
|
17
|
+
maxTags?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function UserProfile({ user, showAddress, onEdit, className, maxTags }: UserProfileProps) {
|
|
21
|
+
const visibleTags = maxTags ? user.tags.slice(0, maxTags) : user.tags;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className={className}>
|
|
25
|
+
<h2>{user.name}</h2>
|
|
26
|
+
<p>{user.email}</p>
|
|
27
|
+
<span>{user.role}</span>
|
|
28
|
+
{showAddress && user.address && (
|
|
29
|
+
<address>
|
|
30
|
+
{user.address.street}, {user.address.city}, {user.address.country}
|
|
31
|
+
{user.address.zip && ` ${user.address.zip}`}
|
|
32
|
+
</address>
|
|
33
|
+
)}
|
|
34
|
+
<div>
|
|
35
|
+
{visibleTags.map((tag) => (
|
|
36
|
+
<span key={tag}>{tag}</span>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
<button type="button" onClick={() => onEdit(user.id)}>Edit</button>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createContext, useMemo, useState } from "react";
|
|
2
|
+
import type { User } from "../types/user";
|
|
3
|
+
|
|
4
|
+
interface UserContextValue {
|
|
5
|
+
currentUser: User | null;
|
|
6
|
+
isLoggedIn: boolean;
|
|
7
|
+
login: (user: User) => void;
|
|
8
|
+
logout: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const UserContext = createContext<UserContextValue | null>(null);
|
|
12
|
+
UserContext.displayName = "UserContext";
|
|
13
|
+
|
|
14
|
+
function UserProvider({ children }: { children: React.ReactNode }) {
|
|
15
|
+
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
|
16
|
+
|
|
17
|
+
const value = useMemo((): UserContextValue => ({
|
|
18
|
+
currentUser,
|
|
19
|
+
isLoggedIn: currentUser !== null,
|
|
20
|
+
login: setCurrentUser,
|
|
21
|
+
logout: () => setCurrentUser(null),
|
|
22
|
+
}), [currentUser]);
|
|
23
|
+
|
|
24
|
+
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { UserContext, UserProvider };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Role = "admin" | "editor" | "viewer";
|
|
2
|
+
|
|
3
|
+
export interface Address {
|
|
4
|
+
street: string;
|
|
5
|
+
city: string;
|
|
6
|
+
country: string;
|
|
7
|
+
zip?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface User {
|
|
11
|
+
id: number;
|
|
12
|
+
name: string;
|
|
13
|
+
email: string;
|
|
14
|
+
role: Role;
|
|
15
|
+
address: Address;
|
|
16
|
+
tags: string[];
|
|
17
|
+
joinedAt: string;
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {defineConfig} from 'vite';
|
|
2
|
+
import {resolve} from 'node:path';
|
|
3
|
+
import react from '@vitejs/plugin-react';
|
|
4
|
+
import {xydSourceReactRuntime} from '@xyd-js/source-react-runtime';
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [
|
|
8
|
+
xydSourceReactRuntime(),
|
|
9
|
+
react(),
|
|
10
|
+
],
|
|
11
|
+
build: {
|
|
12
|
+
lib: {
|
|
13
|
+
entry: 'src/index.ts',
|
|
14
|
+
formats: ['es'],
|
|
15
|
+
fileName: 'index',
|
|
16
|
+
},
|
|
17
|
+
rollupOptions: {
|
|
18
|
+
external: ['react', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom'],
|
|
19
|
+
},
|
|
20
|
+
minify: false,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// === index.js ===
|
|
2
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useState, useMemo } from "react";
|
|
4
|
+
function UserProfile({ user, showAddress, onEdit, className, maxTags }) {
|
|
5
|
+
const visibleTags = maxTags ? user.tags.slice(0, maxTags) : user.tags;
|
|
6
|
+
return jsxs("div", { className, children: [jsx("h2", { children: user.name }), jsx("p", { children: user.email }), jsx("span", { children: user.role }), showAddress && user.address && jsxs("address", { children: [user.address.street, ", ", user.address.city, ", ", user.address.country, user.address.zip && ` ${user.address.zip}`] }), jsx("div", { children: visibleTags.map((tag) => jsx("span", { children: tag }, tag)) }), jsx("button", { type: "button", onClick: () => onEdit(user.id), children: "Edit" })] });
|
|
7
|
+
}
|
|
8
|
+
UserProfile.__xydUniform = JSON.parse('{"title":"UserProfile","canonical":"","description":"","definitions":[{"title":"Props","properties":[{"name":"user","type":"object","description":"","meta":[{"name":"required","value":"true"}],"properties":[{"name":"id","type":"number","description":"","meta":[{"name":"required","value":"true"}]},{"name":"name","type":"string","description":"","meta":[{"name":"required","value":"true"}]},{"name":"email","type":"string","description":"","meta":[{"name":"required","value":"true"}]},{"name":"role","type":"$xor","description":"","properties":[{"name":"role","type":"object","description":"","meta":[{"name":"required","value":"true"}]},{"name":"role","type":"object","description":"","meta":[{"name":"required","value":"true"}]},{"name":"role","type":"object","description":"","meta":[{"name":"required","value":"true"}]}],"meta":[{"name":"required","value":"true"}]},{"name":"address","type":"object","description":"","meta":[{"name":"required","value":"true"}],"properties":[{"name":"street","type":"string","description":"","meta":[{"name":"required","value":"true"}]},{"name":"city","type":"string","description":"","meta":[{"name":"required","value":"true"}]},{"name":"country","type":"string","description":"","meta":[{"name":"required","value":"true"}]},{"name":"zip","type":"string","description":"","meta":[]}]},{"name":"tags","type":"$array","description":"","meta":[{"name":"required","value":"true"}],"properties":[],"ofProperty":{"name":"","type":"string","properties":[],"description":"","meta":[{"name":"required","value":"true"}]}},{"name":"joinedAt","type":"string","description":"","meta":[{"name":"required","value":"true"}]}]},{"name":"showAddress","type":"boolean","description":"Show the full address block","meta":[]},{"name":"className","type":"string","description":"Custom CSS class name","meta":[]},{"name":"maxTags","type":"number","description":"Maximum number of tags to display","meta":[]}],"meta":[{"name":"type","value":"parameters"}]}],"examples":{"groups":[]}}');
|
|
9
|
+
const UserContext = createContext(null);
|
|
10
|
+
UserContext.displayName = "UserContext";
|
|
11
|
+
function UserProvider({ children }) {
|
|
12
|
+
const [currentUser, setCurrentUser] = useState(null);
|
|
13
|
+
const value = useMemo(() => ({
|
|
14
|
+
currentUser,
|
|
15
|
+
isLoggedIn: currentUser !== null,
|
|
16
|
+
login: setCurrentUser,
|
|
17
|
+
logout: () => setCurrentUser(null)
|
|
18
|
+
}), [currentUser]);
|
|
19
|
+
return jsx(UserContext.Provider, { value, children });
|
|
20
|
+
}
|
|
21
|
+
UserContext.__xydUniform = JSON.parse('{"title":"UserContext","canonical":"","description":"","definitions":[{"title":"Props","properties":[{"name":"currentUser","type":"$xor","description":"","properties":[{"name":"currentUser","type":"null","description":"","meta":[{"name":"required","value":"true"}]},{"name":"currentUser","type":"object","description":"","meta":[{"name":"required","value":"true"}],"properties":[{"name":"id","type":"number","description":"","meta":[{"name":"required","value":"true"}]},{"name":"name","type":"string","description":"","meta":[{"name":"required","value":"true"}]},{"name":"email","type":"string","description":"","meta":[{"name":"required","value":"true"}]},{"name":"role","type":"$xor","description":"","properties":[{"name":"role","type":"object","description":"","meta":[{"name":"required","value":"true"}]},{"name":"role","type":"object","description":"","meta":[{"name":"required","value":"true"}]},{"name":"role","type":"object","description":"","meta":[{"name":"required","value":"true"}]}],"meta":[{"name":"required","value":"true"}]},{"name":"address","type":"object","description":"","meta":[{"name":"required","value":"true"}],"properties":[{"name":"street","type":"string","description":"","meta":[{"name":"required","value":"true"}]},{"name":"city","type":"string","description":"","meta":[{"name":"required","value":"true"}]},{"name":"country","type":"string","description":"","meta":[{"name":"required","value":"true"}]},{"name":"zip","type":"string","description":"","meta":[]}]},{"name":"tags","type":"$array","description":"","meta":[{"name":"required","value":"true"}],"properties":[],"ofProperty":{"name":"","type":"string","properties":[],"description":"","meta":[{"name":"required","value":"true"}]}},{"name":"joinedAt","type":"string","description":"","meta":[{"name":"required","value":"true"}]}]}],"meta":[{"name":"required","value":"true"}]},{"name":"isLoggedIn","type":"boolean","description":"","meta":[{"name":"required","value":"true"}]}],"meta":[{"name":"type","value":"parameters"}]}],"examples":{"groups":[]}}');
|
|
22
|
+
export {
|
|
23
|
+
UserContext,
|
|
24
|
+
UserProfile,
|
|
25
|
+
UserProvider
|
|
26
|
+
};
|
|
27
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Priority } from "../types/todo";
|
|
3
|
+
|
|
4
|
+
interface AddTodoFormProps {
|
|
5
|
+
/** Callback fired after a todo is successfully added */
|
|
6
|
+
onAdd: (title: string, priority: Priority, description?: string, tags?: string[]) => void;
|
|
7
|
+
|
|
8
|
+
/** Default priority for new todos */
|
|
9
|
+
defaultPriority?: Priority;
|
|
10
|
+
|
|
11
|
+
/** Placeholder text for the title input */
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function AddTodoForm({ onAdd, defaultPriority = "medium", placeholder = "What needs to be done?" }: AddTodoFormProps) {
|
|
16
|
+
const [title, setTitle] = useState("");
|
|
17
|
+
const [priority, setPriority] = useState<Priority>(defaultPriority);
|
|
18
|
+
const [description, setDescription] = useState("");
|
|
19
|
+
const [tags, setTags] = useState("");
|
|
20
|
+
|
|
21
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
if (!title.trim()) return;
|
|
24
|
+
onAdd(
|
|
25
|
+
title.trim(),
|
|
26
|
+
priority,
|
|
27
|
+
description.trim() || undefined,
|
|
28
|
+
tags.split(",").map((t) => t.trim()).filter(Boolean),
|
|
29
|
+
);
|
|
30
|
+
setTitle("");
|
|
31
|
+
setDescription("");
|
|
32
|
+
setTags("");
|
|
33
|
+
setPriority(defaultPriority);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<form onSubmit={handleSubmit} className="add-todo-form">
|
|
38
|
+
<input type="text" value={title} onChange={(e) => setTitle(e.target.value)} placeholder={placeholder} />
|
|
39
|
+
<select value={priority} onChange={(e) => setPriority(e.target.value as Priority)}>
|
|
40
|
+
<option value="low">Low</option>
|
|
41
|
+
<option value="medium">Medium</option>
|
|
42
|
+
<option value="high">High</option>
|
|
43
|
+
<option value="urgent">Urgent</option>
|
|
44
|
+
</select>
|
|
45
|
+
<textarea value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Description (optional)" />
|
|
46
|
+
<input type="text" value={tags} onChange={(e) => setTags(e.target.value)} placeholder="Tags (comma separated)" />
|
|
47
|
+
<button type="submit" disabled={!title.trim()}>Add</button>
|
|
48
|
+
</form>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { FilterStatus, TodoStats } from "../types/todo";
|
|
2
|
+
|
|
3
|
+
interface FilterBarProps {
|
|
4
|
+
/** Current active filter */
|
|
5
|
+
filter: FilterStatus;
|
|
6
|
+
|
|
7
|
+
/** Called when the filter changes */
|
|
8
|
+
onFilterChange: (filter: FilterStatus) => void;
|
|
9
|
+
|
|
10
|
+
/** Current search query */
|
|
11
|
+
searchQuery: string;
|
|
12
|
+
|
|
13
|
+
/** Called when the search query changes */
|
|
14
|
+
onSearchChange: (query: string) => void;
|
|
15
|
+
|
|
16
|
+
/** Todo statistics for displaying counts */
|
|
17
|
+
stats: TodoStats;
|
|
18
|
+
|
|
19
|
+
/** Called when "Clear completed" is clicked */
|
|
20
|
+
onClearCompleted: () => void;
|
|
21
|
+
|
|
22
|
+
/** Additional CSS classes */
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function FilterBar({ filter, onFilterChange, searchQuery, onSearchChange, stats, onClearCompleted, className }: FilterBarProps) {
|
|
27
|
+
const tabs: { value: FilterStatus; label: string }[] = [
|
|
28
|
+
{ value: "all", label: `All (${stats.total})` },
|
|
29
|
+
{ value: "active", label: `Active (${stats.active})` },
|
|
30
|
+
{ value: "completed", label: `Done (${stats.completed})` },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className={`filter-bar ${className || ""}`}>
|
|
35
|
+
<div className="tabs">
|
|
36
|
+
{tabs.map((tab) => (
|
|
37
|
+
<button
|
|
38
|
+
key={tab.value}
|
|
39
|
+
type="button"
|
|
40
|
+
className={filter === tab.value ? "active" : ""}
|
|
41
|
+
onClick={() => onFilterChange(tab.value)}
|
|
42
|
+
>
|
|
43
|
+
{tab.label}
|
|
44
|
+
</button>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
<input type="search" value={searchQuery} onChange={(e) => onSearchChange(e.target.value)} placeholder="Search..." />
|
|
48
|
+
{stats.completed > 0 && (
|
|
49
|
+
<button type="button" onClick={onClearCompleted} className="clear-btn">
|
|
50
|
+
Clear completed
|
|
51
|
+
</button>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|