ui:next widgets1/2
This commit is contained in:
parent
1407e6620e
commit
0626be2cf6
0
packages/ui-next/docs/legacy/layout.json
Normal file
0
packages/ui-next/docs/legacy/layout.json
Normal file
389
packages/ui-next/package-lock.json
generated
389
packages/ui-next/package-lock.json
generated
@ -15,7 +15,8 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"zod": "^4.3.6"
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
@ -733,356 +734,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
|
||||
"integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
|
||||
"integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
|
||||
"integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
|
||||
"integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
|
||||
"integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
|
||||
"integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
|
||||
"integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
|
||||
"integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
|
||||
"integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
|
||||
"integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
|
||||
"integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
|
||||
"integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
|
||||
"integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
|
||||
"integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
|
||||
"integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
|
||||
"integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
|
||||
"integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
|
||||
"integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
|
||||
@ -1457,14 +1108,14 @@
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.28",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
|
||||
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@ -1664,7 +1315,7 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
@ -2467,7 +2118,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@ -2965,6 +2615,35 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
|
||||
"integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,8 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"zod": "^4.3.6"
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
|
||||
@ -21,6 +21,7 @@ import {
|
||||
} from "@/examples/migration/searchSchema";
|
||||
import { ScrollRestorationDemo } from "@/examples/migration/ScrollRestorationDemo";
|
||||
import { WidgetSystemDemoPage } from "@/examples/widgets/WidgetSystemDemoPage";
|
||||
import { FlexNodeDemoPage } from "@/examples/widgets/FlexNodeDemoPage";
|
||||
|
||||
export function buildMigrationExamplesBranch(rootRoute: AnyRoute) {
|
||||
function ExamplesIndex() {
|
||||
@ -94,6 +95,9 @@ export function buildMigrationExamplesBranch(rootRoute: AnyRoute) {
|
||||
<Link to="/examples/widgets-system" className="link">
|
||||
widgets + plugins
|
||||
</Link>
|
||||
<Link to="/examples/layout" className="link">
|
||||
layout nodes
|
||||
</Link>
|
||||
<Link to="/examples/loader-demo" className="link">
|
||||
loader
|
||||
</Link>
|
||||
@ -294,6 +298,12 @@ export function buildMigrationExamplesBranch(rootRoute: AnyRoute) {
|
||||
component: WidgetSystemDemoPage,
|
||||
});
|
||||
|
||||
const flexNodeDemoRoute = createRoute({
|
||||
getParentRoute: () => examplesRoute,
|
||||
path: "layout",
|
||||
component: FlexNodeDemoPage,
|
||||
});
|
||||
|
||||
const loaderDemoRoute = createRoute({
|
||||
getParentRoute: () => examplesRoute,
|
||||
path: "loader-demo",
|
||||
@ -375,6 +385,7 @@ export function buildMigrationExamplesBranch(rootRoute: AnyRoute) {
|
||||
scrollRestorationDemoRoute,
|
||||
lazyScrollRestorationRoute,
|
||||
widgetSystemDemoRoute,
|
||||
flexNodeDemoRoute,
|
||||
loaderDemoRoute,
|
||||
navigateDemoRoute,
|
||||
redirectRoute,
|
||||
|
||||
103
packages/ui-next/src/examples/widgets/FlexNodeDemoPage.tsx
Normal file
103
packages/ui-next/src/examples/widgets/FlexNodeDemoPage.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { bootWidgetSystem } from "@/widgets/bootstrap/boot";
|
||||
import { NodeRenderer } from "@/widgets/renderer/NodeRenderer";
|
||||
import { useLayoutStore } from "@/store/useLayoutStore";
|
||||
import type { LayoutNode, PageLayout } from "@/widgets/types";
|
||||
|
||||
// ─── Seed data ────────────────────────────────────────────────────────────────
|
||||
// Mirrors the "FlexibleContainer + PhotoCards" example from widgets-api §14,
|
||||
// adapted to use text-block widgets (available in the core plugin).
|
||||
|
||||
const PAGE_ID = "demo-flex-page";
|
||||
|
||||
function makeSeedLayout(): PageLayout {
|
||||
const textBlock = (id: string, title: string, body: string): LayoutNode => ({
|
||||
id,
|
||||
type: "text-block",
|
||||
props: { title, body },
|
||||
children: [],
|
||||
parentId: null,
|
||||
});
|
||||
|
||||
const row1: LayoutNode = {
|
||||
id: "r1",
|
||||
type: "flex-row",
|
||||
props: {},
|
||||
children: [textBlock("w1", "Hero section", "Full-width content spanning the single column.")],
|
||||
parentId: "root",
|
||||
layout: { display: "grid", columns: 1, gap: 16 },
|
||||
};
|
||||
|
||||
const row2: LayoutNode = {
|
||||
id: "r2",
|
||||
type: "flex-row",
|
||||
props: {},
|
||||
children: [
|
||||
textBlock("w2", "Card A", "Column 0 — flex-row with 3 equal columns."),
|
||||
textBlock("w3", "Card B", "Column 1 — position = order in children[]."),
|
||||
textBlock("w4", "Card C", "Column 2 — no rowId/column indices needed."),
|
||||
],
|
||||
parentId: "root",
|
||||
layout: { display: "grid", columns: 3, gap: 12 },
|
||||
};
|
||||
|
||||
const root: LayoutNode = {
|
||||
id: "root",
|
||||
type: "flex",
|
||||
props: { title: "Demo flex container", showTitle: true, gap: 24 },
|
||||
children: [row1, row2],
|
||||
parentId: null,
|
||||
layout: { display: "flex", direction: "column", gap: 24 },
|
||||
};
|
||||
|
||||
return {
|
||||
id: PAGE_ID,
|
||||
name: "Flex node demo",
|
||||
root,
|
||||
version: "1.0.0",
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Page component ───────────────────────────────────────────────────────────
|
||||
|
||||
export function FlexNodeDemoPage() {
|
||||
const { pages, initPage } = useLayoutStore();
|
||||
const page = pages[PAGE_ID];
|
||||
|
||||
useEffect(() => {
|
||||
void bootWidgetSystem().then(() => {
|
||||
if (!useLayoutStore.getState().pages[PAGE_ID]) {
|
||||
initPage(makeSeedLayout());
|
||||
}
|
||||
});
|
||||
}, [initPage]);
|
||||
|
||||
return (
|
||||
<section className="nested">
|
||||
<h1>Flex node scaffold</h1>
|
||||
<p className="muted">
|
||||
Unified Node model (§14) — <code>flex</code> + <code>flex-row</code> nodes rendered by{" "}
|
||||
<code>NodeRenderer</code>. No <code>rowId</code>/column indices; position = order in{" "}
|
||||
<code>children[]</code>.
|
||||
</p>
|
||||
|
||||
{page ? (
|
||||
<NodeRenderer node={page.root} pageId={PAGE_ID} />
|
||||
) : (
|
||||
<p className="muted">Bootstrapping…</p>
|
||||
)}
|
||||
|
||||
<details className="panel" style={{ marginTop: "2rem" }}>
|
||||
<summary style={{ cursor: "pointer" }}>
|
||||
<strong>Node tree (raw)</strong>
|
||||
</summary>
|
||||
<pre style={{ fontSize: "0.75rem", overflow: "auto", maxHeight: "400px" }}>
|
||||
{JSON.stringify(page?.root ?? null, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -24,7 +24,7 @@ const rootRoute = createRootRoute({
|
||||
App → Settings
|
||||
</Link>
|
||||
<Link to="/examples" className="link">
|
||||
Migration examples
|
||||
Examples
|
||||
</Link>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
import type { PageLayout } from "@/widgets/types";
|
||||
import type { PagePersistence } from "./PagePersistence";
|
||||
|
||||
const KEY_PREFIX = "layout:";
|
||||
const INDEX_KEY = "layout:__index__";
|
||||
|
||||
type IndexEntry = { id: string; name: string };
|
||||
|
||||
function indexKey(): string {
|
||||
return INDEX_KEY;
|
||||
}
|
||||
|
||||
function pageKey(pageId: string): string {
|
||||
return `${KEY_PREFIX}${pageId}`;
|
||||
}
|
||||
|
||||
function readIndex(): IndexEntry[] {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(indexKey()) ?? "[]") as IndexEntry[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writeIndex(entries: IndexEntry[]): void {
|
||||
localStorage.setItem(indexKey(), JSON.stringify(entries));
|
||||
}
|
||||
|
||||
export class LocalStoragePagePersistence implements PagePersistence {
|
||||
async load(pageId: string): Promise<PageLayout | null> {
|
||||
try {
|
||||
const raw = localStorage.getItem(pageKey(pageId));
|
||||
if (!raw) return null;
|
||||
return JSON.parse(raw) as PageLayout;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async save(pageId: string, layout: PageLayout): Promise<void> {
|
||||
localStorage.setItem(pageKey(pageId), JSON.stringify(layout));
|
||||
|
||||
const index = readIndex();
|
||||
const existing = index.findIndex((e) => e.id === pageId);
|
||||
const entry: IndexEntry = { id: pageId, name: layout.name };
|
||||
if (existing >= 0) {
|
||||
index[existing] = entry;
|
||||
} else {
|
||||
index.push(entry);
|
||||
}
|
||||
writeIndex(index);
|
||||
}
|
||||
|
||||
async remove(pageId: string): Promise<void> {
|
||||
localStorage.removeItem(pageKey(pageId));
|
||||
writeIndex(readIndex().filter((e) => e.id !== pageId));
|
||||
}
|
||||
|
||||
async list(): Promise<IndexEntry[]> {
|
||||
return readIndex();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import type { PageLayout } from "@/widgets/types";
|
||||
|
||||
/** Swap implementations to change the storage backend. */
|
||||
export interface PagePersistence {
|
||||
load(pageId: string): Promise<PageLayout | null>;
|
||||
save(pageId: string, layout: PageLayout): Promise<void>;
|
||||
remove(pageId: string): Promise<void>;
|
||||
list(): Promise<Array<{ id: string; name: string }>>;
|
||||
}
|
||||
182
packages/ui-next/src/store/useLayoutStore.ts
Normal file
182
packages/ui-next/src/store/useLayoutStore.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
import type { LayoutNode, LayoutStoreAccessor, PageLayout } from "@/widgets/types";
|
||||
import type { PagePersistence } from "./persistence/PagePersistence";
|
||||
import { LocalStoragePagePersistence } from "./persistence/LocalStoragePagePersistence";
|
||||
|
||||
// ─── Tree helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
function findNode(root: LayoutNode, id: string): LayoutNode | null {
|
||||
if (root.id === id) return root;
|
||||
for (const child of root.children) {
|
||||
const found = findNode(child, id);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new root with the given node replaced by `updater(node)`.
|
||||
* Immutable — does not mutate the original tree.
|
||||
*/
|
||||
function mapNode(
|
||||
root: LayoutNode,
|
||||
id: string,
|
||||
updater: (node: LayoutNode) => LayoutNode,
|
||||
): LayoutNode {
|
||||
if (root.id === id) return updater(root);
|
||||
return {
|
||||
...root,
|
||||
children: root.children.map((c) => mapNode(c, id, updater)),
|
||||
};
|
||||
}
|
||||
|
||||
/** Remove a node from the tree (by id). Returns new root. */
|
||||
function removeNodeFromTree(root: LayoutNode, id: string): LayoutNode {
|
||||
return {
|
||||
...root,
|
||||
children: root.children
|
||||
.filter((c) => c.id !== id)
|
||||
.map((c) => removeNodeFromTree(c, id)),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Store types ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface LayoutState {
|
||||
pages: Record<string, PageLayout>;
|
||||
|
||||
// ── Page lifecycle ──────────────────────────────────────────────────────
|
||||
initPage(layout: PageLayout): void;
|
||||
loadPage(pageId: string): Promise<PageLayout | null>;
|
||||
savePage(pageId: string): Promise<void>;
|
||||
removePage(pageId: string): Promise<void>;
|
||||
|
||||
// ── Tree operations ─────────────────────────────────────────────────────
|
||||
/** Append (or insert at index) a node under parentId. */
|
||||
addChild(pageId: string, parentId: string, node: LayoutNode, index?: number): void;
|
||||
/** Remove a node from the tree (by nodeId). */
|
||||
removeNode(pageId: string, nodeId: string): void;
|
||||
/** Move nodeId to a new parent, optionally at a specific index. */
|
||||
moveNode(pageId: string, nodeId: string, newParentId: string, index?: number): void;
|
||||
/** Shallow-merge props onto a node. */
|
||||
updateNodeProps(pageId: string, nodeId: string, props: Partial<Record<string, unknown>>): void;
|
||||
}
|
||||
|
||||
// ─── Factory ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function createStore(persistence: PagePersistence) {
|
||||
return create<LayoutState>()((set, get) => ({
|
||||
pages: {},
|
||||
|
||||
// ── Page lifecycle ──────────────────────────────────────────────────
|
||||
|
||||
initPage(layout) {
|
||||
set((s) => ({ pages: { ...s.pages, [layout.id]: layout } }));
|
||||
},
|
||||
|
||||
async loadPage(pageId) {
|
||||
const layout = await persistence.load(pageId);
|
||||
if (layout) {
|
||||
set((s) => ({ pages: { ...s.pages, [pageId]: layout } }));
|
||||
}
|
||||
return layout;
|
||||
},
|
||||
|
||||
async savePage(pageId) {
|
||||
const layout = get().pages[pageId];
|
||||
if (layout) await persistence.save(pageId, { ...layout, updatedAt: Date.now() });
|
||||
},
|
||||
|
||||
async removePage(pageId) {
|
||||
await persistence.remove(pageId);
|
||||
set((s) => {
|
||||
const pages = { ...s.pages };
|
||||
delete pages[pageId];
|
||||
return { pages };
|
||||
});
|
||||
},
|
||||
|
||||
// ── Tree operations ─────────────────────────────────────────────────
|
||||
|
||||
addChild(pageId, parentId, node, index) {
|
||||
set((s) => {
|
||||
const page = s.pages[pageId];
|
||||
if (!page) return s;
|
||||
const newRoot = mapNode(page.root, parentId, (parent) => {
|
||||
const kids = [...parent.children];
|
||||
const at = index ?? kids.length;
|
||||
kids.splice(at, 0, { ...node, parentId });
|
||||
return { ...parent, children: kids };
|
||||
});
|
||||
return { pages: { ...s.pages, [pageId]: { ...page, root: newRoot, updatedAt: Date.now() } } };
|
||||
});
|
||||
},
|
||||
|
||||
removeNode(pageId, nodeId) {
|
||||
set((s) => {
|
||||
const page = s.pages[pageId];
|
||||
if (!page) return s;
|
||||
const newRoot = removeNodeFromTree(page.root, nodeId);
|
||||
return { pages: { ...s.pages, [pageId]: { ...page, root: newRoot, updatedAt: Date.now() } } };
|
||||
});
|
||||
},
|
||||
|
||||
moveNode(pageId, nodeId, newParentId, index) {
|
||||
set((s) => {
|
||||
const page = s.pages[pageId];
|
||||
if (!page) return s;
|
||||
|
||||
const moving = findNode(page.root, nodeId);
|
||||
if (!moving) return s;
|
||||
|
||||
// Remove from old position, then insert under new parent
|
||||
const withoutNode = removeNodeFromTree(page.root, nodeId);
|
||||
const newRoot = mapNode(withoutNode, newParentId, (parent) => {
|
||||
const kids = [...parent.children];
|
||||
const at = index ?? kids.length;
|
||||
kids.splice(at, 0, { ...moving, parentId: newParentId });
|
||||
return { ...parent, children: kids };
|
||||
});
|
||||
|
||||
return { pages: { ...s.pages, [pageId]: { ...page, root: newRoot, updatedAt: Date.now() } } };
|
||||
});
|
||||
},
|
||||
|
||||
updateNodeProps(pageId, nodeId, props) {
|
||||
set((s) => {
|
||||
const page = s.pages[pageId];
|
||||
if (!page) return s;
|
||||
const newRoot = mapNode(page.root, nodeId, (n) => ({
|
||||
...n,
|
||||
props: { ...n.props, ...props },
|
||||
}));
|
||||
return { pages: { ...s.pages, [pageId]: { ...page, root: newRoot, updatedAt: Date.now() } } };
|
||||
});
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
// ─── Singleton + accessor ────────────────────────────────────────────────────
|
||||
|
||||
export const useLayoutStore = createStore(new LocalStoragePagePersistence());
|
||||
|
||||
/**
|
||||
* Adapter implementing `LayoutStoreAccessor` so the plugin system can read
|
||||
* the store without depending directly on Zustand.
|
||||
*/
|
||||
export const layoutStoreAccessor: LayoutStoreAccessor = {
|
||||
getState: () => ({
|
||||
pages: useLayoutStore.getState().pages,
|
||||
}),
|
||||
subscribe: (selector, callback) => {
|
||||
let prev = selector({ pages: useLayoutStore.getState().pages });
|
||||
return useLayoutStore.subscribe((s) => {
|
||||
const next = selector({ pages: s.pages });
|
||||
if (!Object.is(next, prev)) {
|
||||
prev = next;
|
||||
callback(next);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -3,8 +3,14 @@ import type { WidgetPlugin } from "@/widgets/types";
|
||||
import { TextBlockWidget } from "@/widgets/widgets/sample/TextBlockWidget";
|
||||
import type { TextBlockWidgetProps } from "@/widgets/widgets/sample/TextBlockWidget";
|
||||
|
||||
import { FlexNode } from "@/widgets/nodes/flex/FlexNode";
|
||||
import type { FlexNodeProps } from "@/widgets/nodes/flex/FlexNode";
|
||||
import { FlexRowNode } from "@/widgets/nodes/flex/FlexRowNode";
|
||||
import type { FlexRowNodeProps } from "@/widgets/nodes/flex/FlexRowNode";
|
||||
|
||||
/**
|
||||
* Registers baseline widgets — mirrors wrapping `registerWidgets()` as a plugin (widgets-api §13).
|
||||
* Registers baseline widgets and layout nodes.
|
||||
* Mirrors `registerWidgets()` as a plugin (widgets-api §13).
|
||||
*/
|
||||
export const coreWidgetsPlugin: WidgetPlugin = {
|
||||
id: "polymech:core-widgets",
|
||||
@ -12,6 +18,44 @@ export const coreWidgetsPlugin: WidgetPlugin = {
|
||||
version: "0.0.0",
|
||||
priority: 100,
|
||||
setup(api) {
|
||||
// ── Layout nodes (§14) ──────────────────────────────────────────────────
|
||||
|
||||
api.registerWidget<FlexNodeProps>({
|
||||
component: FlexNode,
|
||||
metadata: {
|
||||
id: "flex",
|
||||
name: "Flex container",
|
||||
category: "layout",
|
||||
description: "Vertical flex column; children are flex-row nodes.",
|
||||
tags: ["layout", "core"],
|
||||
defaultProps: { gap: 16 },
|
||||
},
|
||||
constraints: {
|
||||
canHaveChildren: true,
|
||||
allowedChildTypes: ["flex-row"],
|
||||
draggable: true,
|
||||
deletable: true,
|
||||
},
|
||||
});
|
||||
|
||||
api.registerWidget<FlexRowNodeProps>({
|
||||
component: FlexRowNode,
|
||||
metadata: {
|
||||
id: "flex-row",
|
||||
name: "Flex row",
|
||||
category: "layout",
|
||||
description: "CSS grid row; columns controlled by node.layout.columns.",
|
||||
tags: ["layout", "core"],
|
||||
},
|
||||
constraints: {
|
||||
canHaveChildren: true,
|
||||
draggable: false,
|
||||
deletable: true,
|
||||
},
|
||||
});
|
||||
|
||||
// ── Content widgets ──────────────────────────────────────────────────────
|
||||
|
||||
api.registerWidget<TextBlockWidgetProps>({
|
||||
component: TextBlockWidget,
|
||||
metadata: {
|
||||
@ -29,6 +73,7 @@ export const coreWidgetsPlugin: WidgetPlugin = {
|
||||
body: { type: "markdown", label: "Body", group: "Content" },
|
||||
},
|
||||
},
|
||||
constraints: { canHaveChildren: false, draggable: true, deletable: true },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
/**
|
||||
* Widget / plugin / extension-slot scaffolding (see `packages/ui/docs/widgets-api.md`).
|
||||
*/
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
export type {
|
||||
BaseWidgetProps,
|
||||
ConfigField,
|
||||
HookContext,
|
||||
HookHandler,
|
||||
HookName,
|
||||
LayoutNode,
|
||||
LayoutStore,
|
||||
LayoutStoreAccessor,
|
||||
NodeConstraints,
|
||||
NodeLayout,
|
||||
PageLayout,
|
||||
PluginAPI,
|
||||
SlotId,
|
||||
SlotProps,
|
||||
@ -18,6 +25,7 @@ export type {
|
||||
WidgetWrapper,
|
||||
} from "@/widgets/types";
|
||||
|
||||
// ─── Registries & plugin system ──────────────────────────────────────────────
|
||||
export { WidgetRegistry } from "@/widgets/registry/WidgetRegistry";
|
||||
export { HookRegistry } from "@/widgets/plugins/HookRegistry";
|
||||
export { SlotRegistry } from "@/widgets/plugins/SlotRegistry";
|
||||
@ -25,17 +33,34 @@ export { PluginManager } from "@/widgets/plugins/PluginManager";
|
||||
export { createPluginAPI } from "@/widgets/plugins/createPluginAPI";
|
||||
export { ExtensionSlot } from "@/widgets/slots/ExtensionSlot";
|
||||
|
||||
// ─── Singletons ───────────────────────────────────────────────────────────────
|
||||
export {
|
||||
layoutStore,
|
||||
useLayoutStore,
|
||||
widgetRegistry,
|
||||
hookRegistry,
|
||||
slotRegistry,
|
||||
pluginManager,
|
||||
} from "@/widgets/system";
|
||||
|
||||
// ─── Bootstrap ───────────────────────────────────────────────────────────────
|
||||
export { bootWidgetSystem } from "@/widgets/bootstrap/boot";
|
||||
export { coreWidgetsPlugin } from "@/widgets/bootstrap/corePlugin";
|
||||
export { demoToolbarPlugin } from "@/widgets/bootstrap/demoToolbarPlugin";
|
||||
|
||||
// ─── Node renderer ───────────────────────────────────────────────────────────
|
||||
export { NodeRenderer } from "@/widgets/renderer/NodeRenderer";
|
||||
export type { NodeRendererProps } from "@/widgets/renderer/NodeRenderer";
|
||||
|
||||
// ─── Layout nodes ────────────────────────────────────────────────────────────
|
||||
export { FlexNode } from "@/widgets/nodes/flex/FlexNode";
|
||||
export type { FlexNodeProps } from "@/widgets/nodes/flex/FlexNode";
|
||||
export { FlexRowNode } from "@/widgets/nodes/flex/FlexRowNode";
|
||||
export type { FlexRowNodeProps } from "@/widgets/nodes/flex/FlexRowNode";
|
||||
|
||||
// ─── Persistence ─────────────────────────────────────────────────────────────
|
||||
export type { PagePersistence } from "@/store/persistence/PagePersistence";
|
||||
export { LocalStoragePagePersistence } from "@/store/persistence/LocalStoragePagePersistence";
|
||||
|
||||
// ─── Sample widgets ──────────────────────────────────────────────────────────
|
||||
export { TextBlockWidget } from "@/widgets/widgets/sample/TextBlockWidget";
|
||||
export type { TextBlockWidgetProps } from "@/widgets/widgets/sample/TextBlockWidget";
|
||||
|
||||
50
packages/ui-next/src/widgets/nodes/flex/FlexNode.tsx
Normal file
50
packages/ui-next/src/widgets/nodes/flex/FlexNode.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import type { BaseWidgetProps } from "@/widgets/types";
|
||||
|
||||
export interface FlexNodeProps extends BaseWidgetProps {
|
||||
gap?: number;
|
||||
title?: string;
|
||||
showTitle?: boolean;
|
||||
collapsible?: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flex container node — renders children in a vertical flex column.
|
||||
*
|
||||
* Structural: `children` is populated by NodeRenderer when
|
||||
* `constraints.canHaveChildren` is true.
|
||||
*
|
||||
* node.layout.gap is the preferred source; `gap` prop is the fallback
|
||||
* (stored in node.props for persistence).
|
||||
*/
|
||||
export function FlexNode({
|
||||
layout,
|
||||
gap,
|
||||
title,
|
||||
showTitle,
|
||||
enabled = true,
|
||||
children,
|
||||
}: FlexNodeProps) {
|
||||
if (!enabled) return null;
|
||||
|
||||
const resolvedGap = layout?.gap ?? gap ?? 16;
|
||||
|
||||
const inner = (
|
||||
<div className="relative min-w-0 flex flex-col" style={{ gap: resolvedGap }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (showTitle && title) {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-slate-600 dark:text-slate-300 mb-2">
|
||||
{title}
|
||||
</h3>
|
||||
{inner}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return inner;
|
||||
}
|
||||
31
packages/ui-next/src/widgets/nodes/flex/FlexRowNode.tsx
Normal file
31
packages/ui-next/src/widgets/nodes/flex/FlexRowNode.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import type { BaseWidgetProps } from "@/widgets/types";
|
||||
|
||||
export interface FlexRowNodeProps extends BaseWidgetProps {
|
||||
// All layout config lives in node.layout — no extra props needed.
|
||||
}
|
||||
|
||||
/**
|
||||
* Flex-row node — renders children as a CSS grid row.
|
||||
*
|
||||
* `layout.columns` → gridTemplateColumns (equal fr units)
|
||||
* `layout.gap` → column + row gap
|
||||
*
|
||||
* On narrow viewports (`max-md`) the grid collapses to a single column,
|
||||
* matching the behaviour of the legacy FlexContainerView.
|
||||
*/
|
||||
export function FlexRowNode({ layout, children }: FlexRowNodeProps) {
|
||||
const columns = layout?.columns ?? 1;
|
||||
const gap = layout?.gap ?? 16;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid min-w-0 max-md:!grid-cols-1"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${columns}, 1fr)`,
|
||||
gap,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -71,7 +71,3 @@ export class HookRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/** Stub store until real Zustand layout store is wired */
|
||||
export function createStubLayoutStore(): LayoutStore {
|
||||
return { __stub: true };
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { createPluginAPI } from "@/widgets/plugins/createPluginAPI";
|
||||
import { HookRegistry } from "@/widgets/plugins/HookRegistry";
|
||||
import { SlotRegistry } from "@/widgets/plugins/SlotRegistry";
|
||||
import { WidgetRegistry } from "@/widgets/registry/WidgetRegistry";
|
||||
import type { WidgetPlugin } from "@/widgets/types";
|
||||
import type { LayoutStoreAccessor, WidgetPlugin } from "@/widgets/types";
|
||||
|
||||
export class PluginManager {
|
||||
private readonly plugins = new Map<string, WidgetPlugin>();
|
||||
@ -11,7 +11,7 @@ export class PluginManager {
|
||||
private readonly widgetRegistry: WidgetRegistry,
|
||||
private readonly hookRegistry: HookRegistry,
|
||||
private readonly slotRegistry: SlotRegistry,
|
||||
private readonly layoutStore: Record<string, unknown>,
|
||||
private readonly storeAccessor: LayoutStoreAccessor,
|
||||
) {}
|
||||
|
||||
async register(plugin: WidgetPlugin): Promise<void> {
|
||||
@ -24,7 +24,7 @@ export class PluginManager {
|
||||
widgetRegistry: this.widgetRegistry,
|
||||
hookRegistry: this.hookRegistry,
|
||||
slotRegistry: this.slotRegistry,
|
||||
layoutStore: this.layoutStore,
|
||||
storeAccessor: this.storeAccessor,
|
||||
pluginId: plugin.id,
|
||||
priority: plugin.priority ?? 0,
|
||||
});
|
||||
|
||||
@ -6,6 +6,7 @@ import type {
|
||||
HookHandler,
|
||||
HookName,
|
||||
LayoutStore,
|
||||
LayoutStoreAccessor,
|
||||
PluginAPI,
|
||||
SlotId,
|
||||
SlotProps,
|
||||
@ -19,7 +20,7 @@ type CreatePluginAPIParams = {
|
||||
widgetRegistry: WidgetRegistry;
|
||||
hookRegistry: HookRegistry;
|
||||
slotRegistry: SlotRegistry;
|
||||
layoutStore: Record<string, unknown>;
|
||||
storeAccessor: LayoutStoreAccessor;
|
||||
pluginId: string;
|
||||
priority: number;
|
||||
};
|
||||
@ -28,7 +29,7 @@ export function createPluginAPI({
|
||||
widgetRegistry,
|
||||
hookRegistry,
|
||||
slotRegistry,
|
||||
layoutStore,
|
||||
storeAccessor,
|
||||
pluginId,
|
||||
priority,
|
||||
}: CreatePluginAPIParams): PluginAPI {
|
||||
@ -78,11 +79,9 @@ export function createPluginAPI({
|
||||
slotRegistry.inject(slotId, pluginId, component);
|
||||
},
|
||||
|
||||
getStore: () => layoutStore as Readonly<LayoutStore>,
|
||||
getStore: () => storeAccessor.getState(),
|
||||
|
||||
subscribe: <T>(selector: (state: LayoutStore) => T, callback: (value: T) => void) => {
|
||||
callback(selector(layoutStore as LayoutStore));
|
||||
return () => {};
|
||||
},
|
||||
subscribe: <T>(selector: (state: LayoutStore) => T, callback: (value: T) => void) =>
|
||||
storeAccessor.subscribe(selector, callback),
|
||||
};
|
||||
}
|
||||
|
||||
64
packages/ui-next/src/widgets/renderer/NodeRenderer.tsx
Normal file
64
packages/ui-next/src/widgets/renderer/NodeRenderer.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { widgetRegistry } from "@/widgets/system";
|
||||
import type { LayoutNode } from "@/widgets/types";
|
||||
|
||||
export interface NodeRendererProps {
|
||||
node: LayoutNode;
|
||||
depth?: number;
|
||||
isEditMode?: boolean;
|
||||
pageId?: string;
|
||||
onPropsChange?: (nodeId: string, partial: Record<string, unknown>) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive renderer for the unified Node model (widgets-api §14).
|
||||
*
|
||||
* Resolves `node.type` from the WidgetRegistry, injects base node props, and
|
||||
* recurses into `node.children` for nodes with `constraints.canHaveChildren`.
|
||||
*/
|
||||
export function NodeRenderer({
|
||||
node,
|
||||
depth = 0,
|
||||
isEditMode = false,
|
||||
pageId = "",
|
||||
onPropsChange,
|
||||
}: NodeRendererProps) {
|
||||
const def = widgetRegistry.get(node.type);
|
||||
if (!def) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn(`[NodeRenderer] Unknown node type: "${node.type}"`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const Component = def.component;
|
||||
const canNest = def.constraints?.canHaveChildren ?? false;
|
||||
|
||||
const handlePropsChange = async (partial: Record<string, unknown>) => {
|
||||
await onPropsChange?.(node.id, partial);
|
||||
};
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...(node.props as Record<string, unknown>)}
|
||||
nodeId={node.id}
|
||||
widgetInstanceId={node.id}
|
||||
widgetDefId={node.type}
|
||||
layout={node.layout}
|
||||
isEditMode={isEditMode}
|
||||
onPropsChange={handlePropsChange}
|
||||
>
|
||||
{canNest
|
||||
? node.children.map((child) => (
|
||||
<NodeRenderer
|
||||
key={child.id}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
isEditMode={isEditMode}
|
||||
pageId={pageId}
|
||||
onPropsChange={onPropsChange}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
@ -1,12 +1,14 @@
|
||||
/**
|
||||
* Singleton widget system for the POC. For tests, instantiate `PluginManager` + registries manually.
|
||||
* Process-wide widget system singletons.
|
||||
* For tests, instantiate PluginManager + registries manually.
|
||||
*/
|
||||
import { PluginManager } from "@/widgets/plugins/PluginManager";
|
||||
import { HookRegistry } from "@/widgets/plugins/HookRegistry";
|
||||
import { SlotRegistry } from "@/widgets/plugins/SlotRegistry";
|
||||
import { WidgetRegistry } from "@/widgets/registry/WidgetRegistry";
|
||||
import { layoutStoreAccessor } from "@/store/useLayoutStore";
|
||||
|
||||
export const layoutStore: Record<string, unknown> = {};
|
||||
export { useLayoutStore } from "@/store/useLayoutStore";
|
||||
|
||||
export const widgetRegistry = new WidgetRegistry();
|
||||
export const hookRegistry = new HookRegistry();
|
||||
@ -16,5 +18,5 @@ export const pluginManager = new PluginManager(
|
||||
widgetRegistry,
|
||||
hookRegistry,
|
||||
slotRegistry,
|
||||
layoutStore,
|
||||
layoutStoreAccessor,
|
||||
);
|
||||
|
||||
@ -1,8 +1,46 @@
|
||||
/**
|
||||
* Types aligned with `packages/ui/docs/widgets-api.md` (§1, §2, §13).
|
||||
* Types aligned with `packages/ui/docs/widgets-api.md` (§1, §2, §13, §14).
|
||||
* Intentionally minimal — extend as the host app gains real layout stores.
|
||||
*/
|
||||
import type { ComponentType, ReactElement } from "react";
|
||||
import type { ComponentType, ReactElement, ReactNode } from "react";
|
||||
|
||||
// ─── Node model (§14) ──────────────────────────────────────────────────────
|
||||
|
||||
export interface NodeLayout {
|
||||
display: "flex" | "grid" | "block" | "none";
|
||||
columns?: number;
|
||||
gap?: number;
|
||||
direction?: "row" | "column";
|
||||
align?: "stretch" | "start" | "center" | "end";
|
||||
}
|
||||
|
||||
export interface NodeConstraints {
|
||||
canHaveChildren: boolean;
|
||||
allowedChildTypes?: string[];
|
||||
maxChildren?: number;
|
||||
draggable?: boolean;
|
||||
deletable?: boolean;
|
||||
}
|
||||
|
||||
/** A single element in the layout tree — replaces LayoutContainer + WidgetInstance. */
|
||||
export interface LayoutNode {
|
||||
id: string;
|
||||
/** Registry key: 'flex' | 'flex-row' | 'text-block' | 'image' | … */
|
||||
type: string;
|
||||
props: Record<string, unknown>;
|
||||
children: LayoutNode[];
|
||||
parentId: string | null;
|
||||
layout?: NodeLayout;
|
||||
}
|
||||
|
||||
export interface PageLayout {
|
||||
id: string;
|
||||
name: string;
|
||||
root: LayoutNode;
|
||||
version: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
// ─── Widget props (§2) ─────────────────────────────────────────────────────
|
||||
|
||||
@ -21,6 +59,12 @@ export interface BaseWidgetProps {
|
||||
contextVariables?: Record<string, unknown>;
|
||||
pageContext?: Record<string, unknown>;
|
||||
customClassName?: string;
|
||||
/** Injected by NodeRenderer — the node's id (equals widgetInstanceId in node mode). */
|
||||
nodeId?: string;
|
||||
/** Injected by NodeRenderer — layout hints for structural nodes. */
|
||||
layout?: NodeLayout;
|
||||
/** Populated by NodeRenderer for nodes with canHaveChildren: true. */
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export type WidgetCategory =
|
||||
@ -75,6 +119,8 @@ export interface WidgetDefinition<P extends BaseWidgetProps = BaseWidgetProps> {
|
||||
editComponent?: React.LazyExoticComponent<ComponentType<P>>;
|
||||
previewComponent?: ComponentType<P>;
|
||||
validate?: (props: P) => Record<string, string> | null;
|
||||
/** Node model: structural constraints evaluated at runtime (not persisted). */
|
||||
constraints?: NodeConstraints;
|
||||
}
|
||||
|
||||
export interface WidgetInstance {
|
||||
@ -86,7 +132,10 @@ export interface WidgetInstance {
|
||||
|
||||
// ─── Plugins & hooks (§13) ───────────────────────────────────────────────────
|
||||
|
||||
export type LayoutStore = Record<string, unknown>;
|
||||
/** Read-only snapshot exposed to plugins via PluginAPI.getStore(). */
|
||||
export type LayoutStore = {
|
||||
pages: Record<string, PageLayout>;
|
||||
};
|
||||
|
||||
export interface HookContext {
|
||||
pluginId: string;
|
||||
@ -140,6 +189,15 @@ export type WidgetWrapper = <P extends BaseWidgetProps>(
|
||||
Component: ComponentType<P>,
|
||||
) => ComponentType<P>;
|
||||
|
||||
/** Minimal accessor interface passed into PluginManager / createPluginAPI. */
|
||||
export interface LayoutStoreAccessor {
|
||||
getState: () => Readonly<LayoutStore>;
|
||||
subscribe: <T>(
|
||||
selector: (state: LayoutStore) => T,
|
||||
callback: (value: T) => void,
|
||||
) => () => void;
|
||||
}
|
||||
|
||||
export interface PluginAPI {
|
||||
registerWidget: <P extends BaseWidgetProps>(definition: WidgetDefinition<P>) => void;
|
||||
unregisterWidget: (widgetId: string) => void;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user