From 7c7cfa07aa0a31a62dcf6f31a62d4275e72d6305 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:20:43 +0000 Subject: [PATCH 1/3] Initial plan From e60e064c5b2e33c1017409a1c214fafe9aab094b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:28:59 +0000 Subject: [PATCH 2/3] Add RestTable component and shadcn UI components Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com> --- package.json | 8 + pnpm-lock.yaml | 684 ++++++++++++++++++ public/r/badge.json | 14 + public/r/checkbox.json | 18 + public/r/registry.json | 88 +++ public/r/rest-table.json | 53 ++ public/r/spinner.json | 17 + public/r/table.json | 14 + registry.json | 88 +++ .../rest-table/components/badge-bar.tsx | 26 + .../rest-table/components/file-preview.tsx | 33 + .../blocks/rest-table/components/pager.tsx | 56 ++ .../rest-table/components/rest-form-modal.tsx | 122 ++++ .../new-york/blocks/rest-table/rest-table.tsx | 429 +++++++++++ registry/new-york/ui/badge.tsx | 36 + registry/new-york/ui/checkbox.tsx | 28 + registry/new-york/ui/spinner.tsx | 17 + registry/new-york/ui/table.tsx | 117 +++ 18 files changed, 1848 insertions(+) create mode 100644 public/r/badge.json create mode 100644 public/r/checkbox.json create mode 100644 public/r/rest-table.json create mode 100644 public/r/spinner.json create mode 100644 public/r/table.json create mode 100644 registry/new-york/blocks/rest-table/components/badge-bar.tsx create mode 100644 registry/new-york/blocks/rest-table/components/file-preview.tsx create mode 100644 registry/new-york/blocks/rest-table/components/pager.tsx create mode 100644 registry/new-york/blocks/rest-table/components/rest-form-modal.tsx create mode 100644 registry/new-york/blocks/rest-table/rest-table.tsx create mode 100644 registry/new-york/ui/badge.tsx create mode 100644 registry/new-york/ui/checkbox.tsx create mode 100644 registry/new-york/ui/spinner.tsx create mode 100644 registry/new-york/ui/table.tsx diff --git a/package.json b/package.json index bb53024..b13cbe5 100644 --- a/package.json +++ b/package.json @@ -10,17 +10,25 @@ "registry:build": "shadcn build" }, "dependencies": { + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-slot": "^1.2.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "lodash": "^4.17.21", "lucide-react": "^0.487.0", + "mobx": "^6.15.0", + "mobx-i18n": "^0.7.2", + "mobx-react": "^9.2.1", + "mobx-react-helper": "^0.5.1", + "mobx-restful": "^2.1.4", "next": "15.5.2", "react": "19.1.0", "react-dom": "19.1.0", "shadcn": "^3.0.0", "tailwind-merge": "^3.3.1", "tw-animate-css": "^1.3.6", + "web-utility": "^4.6.4", "zod": "^3.25.76" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0eb4699..a44d5cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,9 @@ importers: .: dependencies: + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-label': specifier: ^2.1.7 version: 2.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -24,9 +27,27 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + lodash: + specifier: ^4.17.21 + version: 4.17.21 lucide-react: specifier: ^0.487.0 version: 0.487.0(react@19.1.0) + mobx: + specifier: ^6.15.0 + version: 6.15.0 + mobx-i18n: + specifier: ^0.7.2 + version: 0.7.2(element-internals-polyfill@3.0.2)(mobx@6.15.0)(typescript@5.9.2) + mobx-react: + specifier: ^9.2.1 + version: 9.2.1(mobx@6.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + mobx-react-helper: + specifier: ^0.5.1 + version: 0.5.1(element-internals-polyfill@3.0.2)(mobx@6.15.0)(react@19.1.0)(typescript@5.9.2) + mobx-restful: + specifier: ^2.1.4 + version: 2.1.4(core-js@3.46.0)(element-internals-polyfill@3.0.2)(jsdom@27.1.0)(mobx@6.15.0)(typescript@5.9.2) next: specifier: 15.5.2 version: 15.5.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -45,6 +66,9 @@ importers: tw-animate-css: specifier: ^1.3.6 version: 1.3.6 + web-utility: + specifier: ^4.6.4 + version: 4.6.4(element-internals-polyfill@3.0.2)(typescript@5.9.2) zod: specifier: ^3.25.76 version: 3.25.76 @@ -79,6 +103,9 @@ importers: packages: + '@acemir/cssom@0.9.23': + resolution: {integrity: sha512-2kJ1HxBKzPLbmhZpxBiTZggjtgCwKg1ma5RHShxvd6zgqhDEdEkzpiwe7jLkI2p2BrZvFCXIihdoMkl1H39VnA==} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -91,6 +118,15 @@ packages: resolution: {integrity: sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA==} hasBin: true + '@asamuzakjp/css-color@4.0.5': + resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==} + + '@asamuzakjp/dom-selector@6.7.4': + resolution: {integrity: sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -211,6 +247,38 @@ packages: '@bundled-es-modules/tough-cookie@0.1.6': resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.15': + resolution: {integrity: sha512-q0p6zkVq2lJnmzZVPR33doA51G7YOja+FBvRdp5ISIthL0MtFCgYHHhR563z9WFGxcOn0WfjSkPDJ5Qig3H3Sw==} + engines: {node: '>=18'} + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@dotenvx/dotenvx@1.49.0': resolution: {integrity: sha512-M1cyP6YstFQCjih54SAxCqHLMMi8QqV8tenpgGE48RTXWD7vfMYJiw/6xcCDpS2h28AcLpTsFCZA863Ge9yxzA==} hasBin: true @@ -568,6 +636,22 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -577,6 +661,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-label@2.1.7': resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} peerDependencies: @@ -590,6 +683,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.1.3': resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: @@ -612,6 +718,51 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': 19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -628,6 +779,9 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + '@tailwindcss/node@4.1.11': resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} @@ -737,6 +891,9 @@ packages: '@types/node@20.19.9': resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==} + '@types/node@22.19.0': + resolution: {integrity: sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==} + '@types/react-dom@19.1.2': resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==} peerDependencies: @@ -1011,6 +1168,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + body-parser@2.2.0: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} @@ -1138,6 +1298,9 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + core-js@3.46.0: + resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==} + cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -1155,6 +1318,14 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssstyle@5.3.3: + resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==} + engines: {node: '>=20'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -1165,6 +1336,10 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + data-urls@6.0.0: + resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} + engines: {node: '>=20'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -1194,6 +1369,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + dedent@1.6.0: resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} peerDependencies: @@ -1251,6 +1429,9 @@ packages: electron-to-chromium@1.5.198: resolution: {integrity: sha512-G5COfnp3w+ydVu80yprgWSfmfQaYRh9DOxfhAxstLyetKaLyl55QrNjx8C38Pc/C+RaDmb1M0Lk8wPEMQ+bGgQ==} + element-internals-polyfill@3.0.2: + resolution: {integrity: sha512-uB0/Qube3lkwh8SmkTnGIyUgJ9YdqVSzIoHMRCEQjAbD4Y5UzsVbch1tIxjTgUe5k3gy1U0ZMKMJ90A81lqwig==} + emoji-regex@10.5.0: resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} @@ -1268,6 +1449,10 @@ packages: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -1663,10 +1848,18 @@ packages: headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -1683,6 +1876,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + idb-keyval@6.2.2: + resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1802,6 +1998,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -1886,6 +2085,15 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsdom@27.1.0: + resolution: {integrity: sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -1930,6 +2138,12 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + koajax@3.1.2: + resolution: {integrity: sha512-4dFDSc5/931hCtSoycuN/5Ke2GGhfRBRTvvDvwva/ruEayQuGH1VsqcT69y1nLtGit5Scbjxh5Y7rCnN3M5Zdw==} + peerDependencies: + core-js: '>=3' + jsdom: '>=21' + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -2012,9 +2226,15 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.isequalwith@4.4.0: + resolution: {integrity: sha512-dcZON0IalGBpRmJBmMkaoV7d3I80R2O+FrzsZyHdNSFrANq/cgDqKQNmAHE8UEj4+QYWwwhkQOVdLHiAopzlsQ==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-symbols@6.0.0: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} @@ -2023,6 +2243,10 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2038,6 +2262,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -2100,6 +2327,51 @@ packages: engines: {node: '>=10'} hasBin: true + mobx-i18n@0.7.2: + resolution: {integrity: sha512-W1W2/nd/0ah6hqBJqpIyAILyvqaNAVwpACH3sm0PeQ5/+0wYEY06+g4WPFQX4y5QNTCr46zgoiQnJ+l8dPXDMQ==} + peerDependencies: + mobx: '>=6.11' + + mobx-react-helper@0.5.1: + resolution: {integrity: sha512-8jwR6LbPmC5s0tcmPz6CjXs1uarAcKjeTD+Oqbd7Vk4Ce49yDxeUOxG07VAcWZVnjnJXE0n79oG3z9c2XEEWTw==} + peerDependencies: + mobx: '>=6.11' + react: '>=16' + + mobx-react-lite@4.1.1: + resolution: {integrity: sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg==} + peerDependencies: + mobx: ^6.9.0 + react: ^16.8.0 || ^17 || ^18 || ^19 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + mobx-react@9.2.1: + resolution: {integrity: sha512-WJNNm0FB2n0Z0u+jS1QHmmWyV8l2WiAj8V8I/96kbUEN2YbYCoKW+hbbqKKRUBqElu0llxM7nWKehvRIkhBVJw==} + peerDependencies: + mobx: ^6.9.0 + react: ^16.8.0 || ^17 || ^18 || ^19 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + mobx-restful@2.1.4: + resolution: {integrity: sha512-yIxWQTeTQCy47VuS5EEwri2bXuGlw8KiUI7yNYcQ8PqrUVSCVUGjoiaUHPPtz4wSjretAzVmIu07cP1fyHpVDg==} + peerDependencies: + mobx: '>=6.11' + + mobx@6.15.0: + resolution: {integrity: sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2264,6 +2536,9 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -2384,6 +2659,9 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -2392,6 +2670,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -2444,6 +2726,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} @@ -2634,6 +2920,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tailwind-merge@3.3.1: resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} @@ -2658,6 +2947,13 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tldts-core@7.0.17: + resolution: {integrity: sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==} + + tldts@7.0.17: + resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2670,6 +2966,14 @@ packages: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -2767,14 +3071,49 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + web-streams-polyfill@4.2.0: + resolution: {integrity: sha512-0rYDzGOh9EZpig92umN5g5D/9A1Kff7k0/mzPSSCY8jEQeYkgRMoY7LhbXtUCWzLCMX0TUE9aoHkjFNB7D9pfA==} + engines: {node: '>= 8'} + + web-utility@4.6.4: + resolution: {integrity: sha512-nPZfFROLtQ6VJaoT0DKEtJ4YfBffg8altB7rl99XgscJPXZ5e7Mxj+ovqdLmjOTdAtEuCexKiyuxzg/m6qHcCg==} + peerDependencies: + element-internals-polyfill: '>=1' + typescript: '>=4.1' + + webidl-conversions@8.0.0: + resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} + engines: {node: '>=20'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -2816,6 +3155,25 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -2857,6 +3215,8 @@ packages: snapshots: + '@acemir/cssom@0.9.23': {} + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -2871,6 +3231,24 @@ snapshots: package-manager-detector: 1.3.0 tinyexec: 1.0.1 + '@asamuzakjp/css-color@4.0.5': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.2 + + '@asamuzakjp/dom-selector@6.7.4': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.2 + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -3046,6 +3424,28 @@ snapshots: '@types/tough-cookie': 4.0.5 tough-cookie: 4.1.4 + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.15': {} + + '@csstools/css-tokenizer@3.0.4': {} + '@dotenvx/dotenvx@1.49.0': dependencies: commander: 11.1.0 @@ -3365,12 +3765,36 @@ snapshots: '@open-draft/until@2.1.0': {} + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.2)(react@19.1.0)': dependencies: react: 19.1.0 optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-context@1.1.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-label@2.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -3380,6 +3804,16 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@19.1.0) @@ -3396,6 +3830,40 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.12.0': {} @@ -3408,6 +3876,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@swc/helpers@0.5.17': + dependencies: + tslib: 2.8.1 + '@tailwindcss/node@4.1.11': dependencies: '@ampproject/remapping': 2.3.0 @@ -3503,6 +3975,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@22.19.0': + dependencies: + undici-types: 6.21.0 + '@types/react-dom@19.1.2(@types/react@19.1.2)': dependencies: '@types/react': 19.1.2 @@ -3790,6 +4266,10 @@ snapshots: balanced-match@1.0.2: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + body-parser@2.2.0: dependencies: bytes: 3.1.2 @@ -3916,6 +4396,8 @@ snapshots: cookie@0.7.2: {} + core-js@3.46.0: {} + cors@2.8.5: dependencies: object-assign: 4.1.1 @@ -3936,12 +4418,28 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + cssstyle@5.3.3: + dependencies: + '@asamuzakjp/css-color': 4.0.5 + '@csstools/css-syntax-patches-for-csstree': 1.0.15 + css-tree: 3.1.0 + csstype@3.1.3: {} damerau-levenshtein@1.0.8: {} data-uri-to-buffer@4.0.1: {} + data-urls@6.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -3968,6 +4466,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + dedent@1.6.0: {} deep-is@0.1.4: {} @@ -4015,6 +4515,8 @@ snapshots: electron-to-chromium@1.5.198: {} + element-internals-polyfill@3.0.2: {} + emoji-regex@10.5.0: {} emoji-regex@8.0.0: {} @@ -4028,6 +4530,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.2 + entities@6.0.1: {} + env-paths@2.2.1: {} error-ex@1.3.2: @@ -4606,6 +5110,10 @@ snapshots: headers-polyfill@4.0.3: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -4614,6 +5122,13 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -4629,6 +5144,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + idb-keyval@6.2.2: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -4737,6 +5254,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} is-regex@1.2.1: @@ -4811,6 +5330,33 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@27.1.0: + dependencies: + '@acemir/cssom': 0.9.23 + '@asamuzakjp/dom-selector': 6.7.4 + cssstyle: 5.3.3 + data-urls: 6.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -4848,6 +5394,18 @@ snapshots: kleur@4.1.5: {} + koajax@3.1.2(core-js@3.46.0)(element-internals-polyfill@3.0.2)(jsdom@27.1.0)(typescript@5.9.2): + dependencies: + '@swc/helpers': 0.5.17 + core-js: 3.46.0 + jsdom: 27.1.0 + regenerator-runtime: 0.14.1 + web-streams-polyfill: 4.2.0 + web-utility: 4.6.4(element-internals-polyfill@3.0.2)(typescript@5.9.2) + transitivePeerDependencies: + - element-internals-polyfill + - typescript + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -4910,8 +5468,12 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.isequalwith@4.4.0: {} + lodash.merge@4.6.2: {} + lodash@4.17.21: {} + log-symbols@6.0.0: dependencies: chalk: 5.5.0 @@ -4921,6 +5483,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + lru-cache@11.2.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -4935,6 +5499,8 @@ snapshots: math-intrinsics@1.1.0: {} + mdn-data@2.12.2: {} + media-typer@1.1.0: {} merge-descriptors@2.0.0: {} @@ -4980,6 +5546,60 @@ snapshots: mkdirp@3.0.1: {} + mobx-i18n@0.7.2(element-internals-polyfill@3.0.2)(mobx@6.15.0)(typescript@5.9.2): + dependencies: + '@swc/helpers': 0.5.17 + '@types/node': 22.19.0 + mobx: 6.15.0 + regenerator-runtime: 0.14.1 + web-utility: 4.6.4(element-internals-polyfill@3.0.2)(typescript@5.9.2) + transitivePeerDependencies: + - element-internals-polyfill + - typescript + + mobx-react-helper@0.5.1(element-internals-polyfill@3.0.2)(mobx@6.15.0)(react@19.1.0)(typescript@5.9.2): + dependencies: + '@swc/helpers': 0.5.17 + lodash.isequalwith: 4.4.0 + mobx: 6.15.0 + react: 19.1.0 + web-utility: 4.6.4(element-internals-polyfill@3.0.2)(typescript@5.9.2) + transitivePeerDependencies: + - element-internals-polyfill + - typescript + + mobx-react-lite@4.1.1(mobx@6.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + mobx: 6.15.0 + react: 19.1.0 + use-sync-external-store: 1.6.0(react@19.1.0) + optionalDependencies: + react-dom: 19.1.0(react@19.1.0) + + mobx-react@9.2.1(mobx@6.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + mobx: 6.15.0 + mobx-react-lite: 4.1.1(mobx@6.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + optionalDependencies: + react-dom: 19.1.0(react@19.1.0) + + mobx-restful@2.1.4(core-js@3.46.0)(element-internals-polyfill@3.0.2)(jsdom@27.1.0)(mobx@6.15.0)(typescript@5.9.2): + dependencies: + '@swc/helpers': 0.5.17 + idb-keyval: 6.2.2 + koajax: 3.1.2(core-js@3.46.0)(element-internals-polyfill@3.0.2)(jsdom@27.1.0)(typescript@5.9.2) + mobx: 6.15.0 + regenerator-runtime: 0.14.1 + web-utility: 4.6.4(element-internals-polyfill@3.0.2)(typescript@5.9.2) + transitivePeerDependencies: + - core-js + - element-internals-polyfill + - jsdom + - typescript + + mobx@6.15.0: {} + ms@2.1.3: {} msw@2.10.4(@types/node@20.19.9)(typescript@5.9.2): @@ -5171,6 +5791,10 @@ snapshots: parse-ms@4.0.0: {} + parse5@8.0.0: + dependencies: + entities: 6.0.1 + parseurl@1.3.3: {} path-browserify@1.0.1: {} @@ -5282,6 +5906,8 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regenerator-runtime@0.14.1: {} + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -5293,6 +5919,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + requires-port@1.0.0: {} resolve-from@4.0.0: {} @@ -5355,6 +5983,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.26.0: {} semver@6.3.1: {} @@ -5637,6 +6269,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + symbol-tree@3.2.4: {} + tailwind-merge@3.3.1: {} tailwindcss@4.1.11: {} @@ -5661,6 +6295,12 @@ snapshots: fdir: 6.4.6(picomatch@4.0.3) picomatch: 4.0.3 + tldts-core@7.0.17: {} + + tldts@7.0.17: + dependencies: + tldts-core: 7.0.17 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -5674,6 +6314,14 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.17 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + ts-api-utils@2.1.0(typescript@5.9.2): dependencies: typescript: 5.9.2 @@ -5805,10 +6453,40 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 + use-sync-external-store@1.6.0(react@19.1.0): + dependencies: + react: 19.1.0 + vary@1.1.2: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + web-streams-polyfill@3.3.3: {} + web-streams-polyfill@4.2.0: {} + + web-utility@4.6.4(element-internals-polyfill@3.0.2)(typescript@5.9.2): + dependencies: + '@swc/helpers': 0.5.17 + element-internals-polyfill: 3.0.2 + regenerator-runtime: 0.14.1 + typescript: 5.9.2 + + webidl-conversions@8.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.0 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -5874,6 +6552,12 @@ snapshots: wrappy@1.0.2: {} + ws@8.18.3: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + y18n@5.0.8: {} yallist@3.1.1: {} diff --git a/public/r/badge.json b/public/r/badge.json new file mode 100644 index 0000000..be81e99 --- /dev/null +++ b/public/r/badge.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "badge", + "type": "registry:ui", + "title": "Badge", + "description": "A badge component for displaying tags and labels.", + "files": [ + { + "path": "registry/new-york/ui/badge.tsx", + "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n \"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n {\n variants: {\n variant: {\n default:\n \"border-transparent bg-primary text-primary-foreground hover:bg-primary/80\",\n secondary:\n \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n destructive:\n \"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80\",\n outline: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nexport interface BadgeProps\n extends React.HTMLAttributes,\n VariantProps {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n return (\n
\n )\n}\n\nexport { Badge, badgeVariants }\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/public/r/checkbox.json b/public/r/checkbox.json new file mode 100644 index 0000000..be336f0 --- /dev/null +++ b/public/r/checkbox.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "checkbox", + "type": "registry:ui", + "title": "Checkbox", + "description": "A checkbox component with Radix UI.", + "dependencies": [ + "@radix-ui/react-checkbox", + "lucide-react" + ], + "files": [ + { + "path": "registry/new-york/ui/checkbox.tsx", + "content": "import * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { Check } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Checkbox = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n \n \n \n \n))\nCheckbox.displayName = CheckboxPrimitive.Root.displayName\n\nexport { Checkbox }\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/public/r/registry.json b/public/r/registry.json index 326c51c..11075d1 100644 --- a/public/r/registry.json +++ b/public/r/registry.json @@ -75,6 +75,94 @@ "type": "registry:component" } ] + }, + { + "name": "table", + "type": "registry:ui", + "title": "Table", + "description": "A responsive table component.", + "files": [ + { + "path": "registry/new-york/ui/table.tsx", + "type": "registry:ui" + } + ] + }, + { + "name": "checkbox", + "type": "registry:ui", + "title": "Checkbox", + "description": "A checkbox component with Radix UI.", + "dependencies": ["@radix-ui/react-checkbox", "lucide-react"], + "files": [ + { + "path": "registry/new-york/ui/checkbox.tsx", + "type": "registry:ui" + } + ] + }, + { + "name": "spinner", + "type": "registry:ui", + "title": "Spinner", + "description": "A loading spinner component.", + "dependencies": ["lucide-react"], + "files": [ + { + "path": "registry/new-york/ui/spinner.tsx", + "type": "registry:ui" + } + ] + }, + { + "name": "badge", + "type": "registry:ui", + "title": "Badge", + "description": "A badge component for displaying tags and labels.", + "files": [ + { + "path": "registry/new-york/ui/badge.tsx", + "type": "registry:ui" + } + ] + }, + { + "name": "rest-table", + "type": "registry:component", + "title": "REST Table", + "description": "A MobX-powered table component with CRUD operations.", + "dependencies": [ + "lodash", + "mobx", + "mobx-i18n", + "mobx-react", + "mobx-react-helper", + "mobx-restful", + "web-utility" + ], + "registryDependencies": ["button", "checkbox", "spinner", "table", "badge", "card", "input", "label"], + "files": [ + { + "path": "registry/new-york/blocks/rest-table/rest-table.tsx", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/rest-table/components/badge-bar.tsx", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/rest-table/components/file-preview.tsx", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/rest-table/components/pager.tsx", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/rest-table/components/rest-form-modal.tsx", + "type": "registry:component" + } + ] } ] } diff --git a/public/r/rest-table.json b/public/r/rest-table.json new file mode 100644 index 0000000..3eedc0e --- /dev/null +++ b/public/r/rest-table.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "rest-table", + "type": "registry:component", + "title": "REST Table", + "description": "A MobX-powered table component with CRUD operations.", + "dependencies": [ + "lodash", + "mobx", + "mobx-i18n", + "mobx-react", + "mobx-react-helper", + "mobx-restful", + "web-utility" + ], + "registryDependencies": [ + "button", + "checkbox", + "spinner", + "table", + "badge", + "card", + "input", + "label" + ], + "files": [ + { + "path": "registry/new-york/blocks/rest-table/rest-table.tsx", + "content": "import * as React from \"react\"\nimport { debounce } from \"lodash\"\nimport { computed, observable } from \"mobx\"\nimport { TranslationModel } from \"mobx-i18n\"\nimport { observer } from \"mobx-react\"\nimport { ObservedComponent } from \"mobx-react-helper\"\nimport { DataObject, Filter, IDType } from \"mobx-restful\"\nimport { isEmpty } from \"web-utility\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/registry/new-york/ui/button\"\nimport { Checkbox } from \"@/registry/new-york/ui/checkbox\"\nimport { Spinner } from \"@/registry/new-york/ui/spinner\"\nimport {\n Table,\n TableBody,\n TableCell,\n TableFooter,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/registry/new-york/ui/table\"\n\nimport { BadgeBar } from \"./components/badge-bar\"\nimport { FilePreview } from \"./components/file-preview\"\nimport { Pager } from \"./components/pager\"\nimport { Field, RestFormModal } from \"./components/rest-form-modal\"\n\nexport interface Column\n extends Omit, \"renderLabel\"> {\n renderHead?: Field[\"renderLabel\"]\n renderBody?: (data: T) => React.ReactNode\n renderFoot?: React.ReactNode | ((data: keyof T) => React.ReactNode)\n}\n\nexport interface RestFormProps {\n store: any\n translator: any\n onSubmit?: (data: T) => Promise\n}\n\ntype Translator = RestFormProps[\"translator\"] &\n TranslationModel<\n string,\n | \"create\"\n | \"view\"\n | \"edit\"\n | \"delete\"\n | \"total_x_rows\"\n | \"sure_to_delete_x\"\n >\n\nexport interface RestTableProps<\n D extends DataObject,\n F extends Filter = Filter\n> extends Omit, \"onSubmit\">,\n Omit, \"id\" | \"fields\" | \"translator\"> {\n filter?: F\n editable?: boolean\n deletable?: boolean\n columns: Column[]\n translator: Translator\n onCheck?: (keys: IDType[]) => any\n responsive?: boolean\n}\n\n@observer\nexport class RestTable<\n D extends DataObject,\n F extends Filter = Filter\n> extends ObservedComponent> {\n static readonly displayName = \"RestTable\"\n\n componentDidMount() {\n const { store, filter } = this.props\n\n store.clear()\n store.getList(filter)\n }\n\n @observable\n accessor checkedKeys: IDType[] = []\n\n toggleCheck(key: IDType) {\n const { checkedKeys } = this\n const index = checkedKeys.indexOf(key)\n\n this.checkedKeys =\n index < 0\n ? [...checkedKeys, key]\n : [...checkedKeys.slice(0, index), ...checkedKeys.slice(index + 1)]\n\n this.props.onCheck?.(this.checkedKeys)\n }\n\n toggleCheckAll = () => {\n const { store, onCheck } = this.props\n const { indexKey, currentPage } = store\n\n this.checkedKeys = this.checkedKeys.length\n ? []\n : currentPage.map(({ [indexKey]: ID }: D) => ID)\n\n onCheck?.(this.checkedKeys)\n }\n\n @computed\n get checkColumn(): Column {\n const { checkedKeys, toggleCheckAll } = this\n const { indexKey, currentPage } = this.observedProps.store\n\n return {\n renderHead: () => {\n const allChecked =\n !!currentPage[0] &&\n currentPage.every(({ [indexKey]: ID }: D) =>\n checkedKeys.includes(ID)\n )\n const someChecked =\n !!checkedKeys.length && checkedKeys.length < currentPage.length\n\n return (\n \n )\n },\n renderBody: ({ [indexKey]: ID }: D) => (\n this.toggleCheck(ID)}\n />\n ),\n } as Column\n }\n\n @computed\n get operateColumn(): Column {\n const { editable, deletable, columns, store, translator } =\n this.observedProps\n const { t } = translator\n const readOnly = columns.every(({ readOnly }) => readOnly)\n const disabled = columns.every(({ disabled }) => disabled)\n\n return {\n renderHead: () => <>,\n renderBody: (data: D) => (\n
\n {!disabled &&\n editable && (\n (store.currentOne = data)}\n >\n {readOnly ? t(\"view\") : t(\"edit\")}\n \n )}\n {deletable && (\n this.deleteList([data.id])}\n >\n {t(\"delete\")}\n \n )}\n
\n ),\n } as Column\n }\n\n @computed\n get columns(): Column[] {\n const { editable, deletable, columns, onCheck } = this.observedProps\n\n return [\n onCheck && this.checkColumn,\n ...columns.map(\n ({ renderBody, ...column }) =>\n ({\n ...column,\n renderBody: renderBody ?? this.renderCustomBody(column),\n }) as Column\n ),\n (editable || deletable) && this.operateColumn,\n ].filter(Boolean) as Column[]\n }\n\n @computed\n get hasHeader() {\n return this.columns.some(({ renderHead }) => renderHead)\n }\n\n @computed\n get hasFooter() {\n return this.columns.some(({ renderFoot }) => renderFoot)\n }\n\n @computed\n get editing() {\n return !isEmpty(this.observedProps.store.currentOne)\n }\n\n renderCustomBody = ({\n key,\n type,\n multiple,\n options,\n accept,\n rows,\n }: Column): Column[\"renderBody\"] =>\n type === \"url\"\n ? ({ [key]: value }: D) =>\n value && (\n \n {value as string}\n \n )\n : type === \"email\"\n ? ({ [key]: value }: D) =>\n value && (\n \n {value as string}\n \n )\n : type === \"tel\"\n ? ({ [key]: value }: D) =>\n value && (\n \n {value as string}\n \n )\n : type === \"file\"\n ? ({ [key]: value }: D) =>\n (\n (Array.isArray(value) ? value : [value]) as string[]\n ).map(\n (path) =>\n path && (\n \n )\n )\n : options || multiple\n ? ({ [key]: value }: D) =>\n value && (\n ({ text }))}\n />\n )\n : !options && rows\n ? ({ [key]: value }: D) => (\n \n {value as string}\n

\n )\n : undefined\n\n renderTable() {\n const {\n className,\n columns: _,\n store,\n translator,\n editable,\n deletable,\n onCheck,\n onSubmit,\n responsive = true,\n ...tableProps\n } = this.props\n const { hasHeader, hasFooter, columns, editing } = this\n const { indexKey, downloading, currentPage } = store\n\n return (\n \n {hasHeader && (\n \n \n {columns.map(({ key, renderHead }, index) =>\n key || renderHead ? (\n \n {typeof renderHead === \"function\"\n ? renderHead(key)\n : renderHead || (key as string)}\n \n ) : null\n )}\n \n \n )}\n \n {!editing && downloading > 0 ? (\n \n \n \n \n \n ) : (\n currentPage.map((data: D) => (\n \n {columns.map(({ key, renderBody }, index) =>\n key || renderBody ? (\n \n {renderBody?.(data) || (key && data[key])}\n \n ) : null\n )}\n \n ))\n )}\n \n\n {hasFooter && (\n \n \n {columns.map(({ key, renderFoot }, index) =>\n key || renderFoot ? (\n \n {typeof renderFoot === \"function\"\n ? renderFoot(key)\n : renderFoot || (key as string)}\n \n ) : null\n )}\n \n \n )}\n
\n )\n }\n\n getList = debounce(\n ({ pageIndex, pageSize }: { pageIndex: number; pageSize: number }) => {\n const { store, filter } = this.props\n\n if (store.downloading < 1) store.getList(filter, pageIndex, pageSize)\n }\n )\n\n async deleteList(keys: IDType[]) {\n const { translator, store } = this.props\n\n if (confirm(translator.t(\"sure_to_delete_x\", { keys })))\n for (const key of keys) await store.deleteOne(key)\n }\n\n render() {\n const {\n className,\n editable,\n deletable,\n store,\n translator,\n onSubmit,\n ...divProps\n } = this.props\n const { t } = translator\n const { indexKey, pageSize, pageIndex, pageCount, totalCount } = store\n\n return (\n
\n
\n \n
\n {deletable && (\n this.deleteList(this.checkedKeys)}\n >\n {t(\"delete\")}\n \n )}\n {editable && (\n \n (store.currentOne[indexKey] = \"\" as D[keyof D])\n }\n >\n {t(\"create\")}\n \n )}\n
\n
\n\n {this.renderTable()}\n\n {editable && (\n ({\n ...field,\n renderLabel: renderHead,\n }))}\n {...{ store, translator, onSubmit }}\n />\n )}\n
\n )\n }\n}\n", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/rest-table/components/badge-bar.tsx", + "content": "import * as React from \"react\"\nimport { Badge } from \"@/registry/new-york/ui/badge\"\n\nexport interface BadgeItem {\n text: string\n}\n\nexport interface BadgeBarProps {\n list: BadgeItem[]\n}\n\nexport class BadgeBar extends React.Component {\n render() {\n const { list } = this.props\n\n return (\n
\n {list.map((item, index) => (\n \n {item.text}\n \n ))}\n
\n )\n }\n}\n", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/rest-table/components/file-preview.tsx", + "content": "import * as React from \"react\"\n\nexport interface FilePreviewProps {\n type?: string\n path: string\n}\n\nexport class FilePreview extends React.Component {\n render() {\n const { type, path } = this.props\n\n if (!path) return null\n\n const isImage = type?.startsWith('image/')\n\n return (\n
\n {isImage ? (\n \"preview\"\n ) : (\n \n {path.split('/').pop()}\n \n )}\n
\n )\n }\n}\n", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/rest-table/components/pager.tsx", + "content": "import * as React from \"react\"\nimport { Button } from \"@/registry/new-york/ui/button\"\nimport { ChevronLeft, ChevronRight } from \"lucide-react\"\n\nexport interface PagerProps {\n pageSize: number\n pageIndex: number\n pageCount: number\n onChange: (params: { pageIndex: number; pageSize: number }) => void\n}\n\nexport class Pager extends React.Component {\n handlePrevious = () => {\n const { pageIndex, pageSize, onChange } = this.props\n if (pageIndex > 1) {\n onChange({ pageIndex: pageIndex - 1, pageSize })\n }\n }\n\n handleNext = () => {\n const { pageIndex, pageSize, pageCount, onChange } = this.props\n if (pageIndex < pageCount) {\n onChange({ pageIndex: pageIndex + 1, pageSize })\n }\n }\n\n render() {\n const { pageIndex, pageCount } = this.props\n\n return (\n
\n \n \n Previous\n \n \n Page {pageIndex} of {pageCount}\n \n = pageCount}\n >\n Next\n \n \n
\n )\n }\n}\n", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/rest-table/components/rest-form-modal.tsx", + "content": "import * as React from \"react\"\nimport { observer } from \"mobx-react\"\nimport { DataObject } from \"mobx-restful\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york/ui/card\"\nimport { Button } from \"@/registry/new-york/ui/button\"\nimport { Input } from \"@/registry/new-york/ui/input\"\nimport { Label } from \"@/registry/new-york/ui/label\"\n\nexport interface Field {\n key: keyof T\n type?: string\n renderLabel?: ((key: keyof T) => React.ReactNode) | React.ReactNode\n readOnly?: boolean\n disabled?: boolean\n multiple?: boolean\n options?: any[]\n accept?: string\n rows?: number\n}\n\nexport interface RestFormModalProps {\n fields: Field[]\n store: any\n translator: any\n onSubmit?: (data: T) => Promise\n}\n\n@observer\nexport class RestFormModal extends React.Component<\n RestFormModalProps\n> {\n handleClose = () => {\n this.props.store.currentOne = {}\n }\n\n handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n const { store, onSubmit } = this.props\n const data = store.currentOne\n\n if (onSubmit) {\n await onSubmit(data)\n } else {\n if (data[store.indexKey]) {\n await store.updateOne(data, data[store.indexKey])\n } else {\n await store.createOne(data)\n }\n }\n\n this.handleClose()\n }\n\n render() {\n const { fields, store, translator } = this.props\n const { currentOne } = store\n const { t } = translator\n\n if (!currentOne || Object.keys(currentOne).length === 0) {\n return null\n }\n\n const isEditing = !!currentOne[store.indexKey]\n\n return (\n
\n \n \n {isEditing ? t(\"edit\") : t(\"create\")}\n \n {isEditing\n ? \"Edit the existing item\"\n : \"Create a new item\"}\n \n \n
\n \n {fields.map((field) => {\n if (!field.key || field.readOnly) return null\n\n const label =\n typeof field.renderLabel === \"function\"\n ? field.renderLabel(field.key)\n : field.renderLabel || String(field.key)\n\n return (\n
\n \n {\n currentOne[field.key] = e.target.value as T[keyof T]\n }}\n disabled={field.disabled}\n />\n
\n )\n })}\n
\n \n \n \n \n
\n
\n
\n )\n }\n}\n", + "type": "registry:component" + } + ] +} \ No newline at end of file diff --git a/public/r/spinner.json b/public/r/spinner.json new file mode 100644 index 0000000..48b5d43 --- /dev/null +++ b/public/r/spinner.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "spinner", + "type": "registry:ui", + "title": "Spinner", + "description": "A loading spinner component.", + "dependencies": [ + "lucide-react" + ], + "files": [ + { + "path": "registry/new-york/ui/spinner.tsx", + "content": "import * as React from \"react\"\nimport { Loader2 } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Spinner({\n className,\n ...props\n}: React.HTMLAttributes) {\n return (\n
\n \n
\n )\n}\n\nexport { Spinner }\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/public/r/table.json b/public/r/table.json new file mode 100644 index 0000000..f30aa2f --- /dev/null +++ b/public/r/table.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "table", + "type": "registry:ui", + "title": "Table", + "description": "A responsive table component.", + "files": [ + { + "path": "registry/new-york/ui/table.tsx", + "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Table = React.forwardRef<\n HTMLTableElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n
\n \n
\n))\nTable.displayName = \"Table\"\n\nconst TableHeader = React.forwardRef<\n HTMLTableSectionElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n))\nTableHeader.displayName = \"TableHeader\"\n\nconst TableBody = React.forwardRef<\n HTMLTableSectionElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n))\nTableBody.displayName = \"TableBody\"\n\nconst TableFooter = React.forwardRef<\n HTMLTableSectionElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n tr]:last:border-b-0\",\n className\n )}\n {...props}\n />\n))\nTableFooter.displayName = \"TableFooter\"\n\nconst TableRow = React.forwardRef<\n HTMLTableRowElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n))\nTableRow.displayName = \"TableRow\"\n\nconst TableHead = React.forwardRef<\n HTMLTableCellElement,\n React.ThHTMLAttributes\n>(({ className, ...props }, ref) => (\n \n))\nTableHead.displayName = \"TableHead\"\n\nconst TableCell = React.forwardRef<\n HTMLTableCellElement,\n React.TdHTMLAttributes\n>(({ className, ...props }, ref) => (\n \n))\nTableCell.displayName = \"TableCell\"\n\nconst TableCaption = React.forwardRef<\n HTMLTableCaptionElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n))\nTableCaption.displayName = \"TableCaption\"\n\nexport {\n Table,\n TableHeader,\n TableBody,\n TableFooter,\n TableHead,\n TableRow,\n TableCell,\n TableCaption,\n}\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/registry.json b/registry.json index 326c51c..11075d1 100644 --- a/registry.json +++ b/registry.json @@ -75,6 +75,94 @@ "type": "registry:component" } ] + }, + { + "name": "table", + "type": "registry:ui", + "title": "Table", + "description": "A responsive table component.", + "files": [ + { + "path": "registry/new-york/ui/table.tsx", + "type": "registry:ui" + } + ] + }, + { + "name": "checkbox", + "type": "registry:ui", + "title": "Checkbox", + "description": "A checkbox component with Radix UI.", + "dependencies": ["@radix-ui/react-checkbox", "lucide-react"], + "files": [ + { + "path": "registry/new-york/ui/checkbox.tsx", + "type": "registry:ui" + } + ] + }, + { + "name": "spinner", + "type": "registry:ui", + "title": "Spinner", + "description": "A loading spinner component.", + "dependencies": ["lucide-react"], + "files": [ + { + "path": "registry/new-york/ui/spinner.tsx", + "type": "registry:ui" + } + ] + }, + { + "name": "badge", + "type": "registry:ui", + "title": "Badge", + "description": "A badge component for displaying tags and labels.", + "files": [ + { + "path": "registry/new-york/ui/badge.tsx", + "type": "registry:ui" + } + ] + }, + { + "name": "rest-table", + "type": "registry:component", + "title": "REST Table", + "description": "A MobX-powered table component with CRUD operations.", + "dependencies": [ + "lodash", + "mobx", + "mobx-i18n", + "mobx-react", + "mobx-react-helper", + "mobx-restful", + "web-utility" + ], + "registryDependencies": ["button", "checkbox", "spinner", "table", "badge", "card", "input", "label"], + "files": [ + { + "path": "registry/new-york/blocks/rest-table/rest-table.tsx", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/rest-table/components/badge-bar.tsx", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/rest-table/components/file-preview.tsx", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/rest-table/components/pager.tsx", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/rest-table/components/rest-form-modal.tsx", + "type": "registry:component" + } + ] } ] } diff --git a/registry/new-york/blocks/rest-table/components/badge-bar.tsx b/registry/new-york/blocks/rest-table/components/badge-bar.tsx new file mode 100644 index 0000000..835729e --- /dev/null +++ b/registry/new-york/blocks/rest-table/components/badge-bar.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import { Badge } from "@/registry/new-york/ui/badge" + +export interface BadgeItem { + text: string +} + +export interface BadgeBarProps { + list: BadgeItem[] +} + +export class BadgeBar extends React.Component { + render() { + const { list } = this.props + + return ( +
+ {list.map((item, index) => ( + + {item.text} + + ))} +
+ ) + } +} diff --git a/registry/new-york/blocks/rest-table/components/file-preview.tsx b/registry/new-york/blocks/rest-table/components/file-preview.tsx new file mode 100644 index 0000000..1a6f726 --- /dev/null +++ b/registry/new-york/blocks/rest-table/components/file-preview.tsx @@ -0,0 +1,33 @@ +import * as React from "react" + +export interface FilePreviewProps { + type?: string + path: string +} + +export class FilePreview extends React.Component { + render() { + const { type, path } = this.props + + if (!path) return null + + const isImage = type?.startsWith('image/') + + return ( +
+ {isImage ? ( + preview + ) : ( + + {path.split('/').pop()} + + )} +
+ ) + } +} diff --git a/registry/new-york/blocks/rest-table/components/pager.tsx b/registry/new-york/blocks/rest-table/components/pager.tsx new file mode 100644 index 0000000..33bd89b --- /dev/null +++ b/registry/new-york/blocks/rest-table/components/pager.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Button } from "@/registry/new-york/ui/button" +import { ChevronLeft, ChevronRight } from "lucide-react" + +export interface PagerProps { + pageSize: number + pageIndex: number + pageCount: number + onChange: (params: { pageIndex: number; pageSize: number }) => void +} + +export class Pager extends React.Component { + handlePrevious = () => { + const { pageIndex, pageSize, onChange } = this.props + if (pageIndex > 1) { + onChange({ pageIndex: pageIndex - 1, pageSize }) + } + } + + handleNext = () => { + const { pageIndex, pageSize, pageCount, onChange } = this.props + if (pageIndex < pageCount) { + onChange({ pageIndex: pageIndex + 1, pageSize }) + } + } + + render() { + const { pageIndex, pageCount } = this.props + + return ( +
+ + + Page {pageIndex} of {pageCount} + + +
+ ) + } +} diff --git a/registry/new-york/blocks/rest-table/components/rest-form-modal.tsx b/registry/new-york/blocks/rest-table/components/rest-form-modal.tsx new file mode 100644 index 0000000..e8c29be --- /dev/null +++ b/registry/new-york/blocks/rest-table/components/rest-form-modal.tsx @@ -0,0 +1,122 @@ +import * as React from "react" +import { observer } from "mobx-react" +import { DataObject } from "mobx-restful" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/registry/new-york/ui/card" +import { Button } from "@/registry/new-york/ui/button" +import { Input } from "@/registry/new-york/ui/input" +import { Label } from "@/registry/new-york/ui/label" + +export interface Field { + key: keyof T + type?: string + renderLabel?: ((key: keyof T) => React.ReactNode) | React.ReactNode + readOnly?: boolean + disabled?: boolean + multiple?: boolean + options?: any[] + accept?: string + rows?: number +} + +export interface RestFormModalProps { + fields: Field[] + store: any + translator: any + onSubmit?: (data: T) => Promise +} + +@observer +export class RestFormModal extends React.Component< + RestFormModalProps +> { + handleClose = () => { + this.props.store.currentOne = {} + } + + handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + const { store, onSubmit } = this.props + const data = store.currentOne + + if (onSubmit) { + await onSubmit(data) + } else { + if (data[store.indexKey]) { + await store.updateOne(data, data[store.indexKey]) + } else { + await store.createOne(data) + } + } + + this.handleClose() + } + + render() { + const { fields, store, translator } = this.props + const { currentOne } = store + const { t } = translator + + if (!currentOne || Object.keys(currentOne).length === 0) { + return null + } + + const isEditing = !!currentOne[store.indexKey] + + return ( +
+ + + {isEditing ? t("edit") : t("create")} + + {isEditing + ? "Edit the existing item" + : "Create a new item"} + + +
+ + {fields.map((field) => { + if (!field.key || field.readOnly) return null + + const label = + typeof field.renderLabel === "function" + ? field.renderLabel(field.key) + : field.renderLabel || String(field.key) + + return ( +
+ + { + currentOne[field.key] = e.target.value as T[keyof T] + }} + disabled={field.disabled} + /> +
+ ) + })} +
+ + + + +
+
+
+ ) + } +} diff --git a/registry/new-york/blocks/rest-table/rest-table.tsx b/registry/new-york/blocks/rest-table/rest-table.tsx new file mode 100644 index 0000000..c4afff1 --- /dev/null +++ b/registry/new-york/blocks/rest-table/rest-table.tsx @@ -0,0 +1,429 @@ +import * as React from "react" +import { debounce } from "lodash" +import { computed, observable } from "mobx" +import { TranslationModel } from "mobx-i18n" +import { observer } from "mobx-react" +import { ObservedComponent } from "mobx-react-helper" +import { DataObject, Filter, IDType } from "mobx-restful" +import { isEmpty } from "web-utility" + +import { cn } from "@/lib/utils" +import { Button } from "@/registry/new-york/ui/button" +import { Checkbox } from "@/registry/new-york/ui/checkbox" +import { Spinner } from "@/registry/new-york/ui/spinner" +import { + Table, + TableBody, + TableCell, + TableFooter, + TableHead, + TableHeader, + TableRow, +} from "@/registry/new-york/ui/table" + +import { BadgeBar } from "./components/badge-bar" +import { FilePreview } from "./components/file-preview" +import { Pager } from "./components/pager" +import { Field, RestFormModal } from "./components/rest-form-modal" + +export interface Column + extends Omit, "renderLabel"> { + renderHead?: Field["renderLabel"] + renderBody?: (data: T) => React.ReactNode + renderFoot?: React.ReactNode | ((data: keyof T) => React.ReactNode) +} + +export interface RestFormProps { + store: any + translator: any + onSubmit?: (data: T) => Promise +} + +type Translator = RestFormProps["translator"] & + TranslationModel< + string, + | "create" + | "view" + | "edit" + | "delete" + | "total_x_rows" + | "sure_to_delete_x" + > + +export interface RestTableProps< + D extends DataObject, + F extends Filter = Filter +> extends Omit, "onSubmit">, + Omit, "id" | "fields" | "translator"> { + filter?: F + editable?: boolean + deletable?: boolean + columns: Column[] + translator: Translator + onCheck?: (keys: IDType[]) => any + responsive?: boolean +} + +@observer +export class RestTable< + D extends DataObject, + F extends Filter = Filter +> extends ObservedComponent> { + static readonly displayName = "RestTable" + + componentDidMount() { + const { store, filter } = this.props + + store.clear() + store.getList(filter) + } + + @observable + accessor checkedKeys: IDType[] = [] + + toggleCheck(key: IDType) { + const { checkedKeys } = this + const index = checkedKeys.indexOf(key) + + this.checkedKeys = + index < 0 + ? [...checkedKeys, key] + : [...checkedKeys.slice(0, index), ...checkedKeys.slice(index + 1)] + + this.props.onCheck?.(this.checkedKeys) + } + + toggleCheckAll = () => { + const { store, onCheck } = this.props + const { indexKey, currentPage } = store + + this.checkedKeys = this.checkedKeys.length + ? [] + : currentPage.map(({ [indexKey]: ID }: D) => ID) + + onCheck?.(this.checkedKeys) + } + + @computed + get checkColumn(): Column { + const { checkedKeys, toggleCheckAll } = this + const { indexKey, currentPage } = this.observedProps.store + + return { + renderHead: () => { + const allChecked = + !!currentPage[0] && + currentPage.every(({ [indexKey]: ID }: D) => + checkedKeys.includes(ID) + ) + const someChecked = + !!checkedKeys.length && checkedKeys.length < currentPage.length + + return ( + + ) + }, + renderBody: ({ [indexKey]: ID }: D) => ( + this.toggleCheck(ID)} + /> + ), + } as Column + } + + @computed + get operateColumn(): Column { + const { editable, deletable, columns, store, translator } = + this.observedProps + const { t } = translator + const readOnly = columns.every(({ readOnly }) => readOnly) + const disabled = columns.every(({ disabled }) => disabled) + + return { + renderHead: () => <>, + renderBody: (data: D) => ( +
+ {!disabled && + editable && ( + + )} + {deletable && ( + + )} +
+ ), + } as Column + } + + @computed + get columns(): Column[] { + const { editable, deletable, columns, onCheck } = this.observedProps + + return [ + onCheck && this.checkColumn, + ...columns.map( + ({ renderBody, ...column }) => + ({ + ...column, + renderBody: renderBody ?? this.renderCustomBody(column), + }) as Column + ), + (editable || deletable) && this.operateColumn, + ].filter(Boolean) as Column[] + } + + @computed + get hasHeader() { + return this.columns.some(({ renderHead }) => renderHead) + } + + @computed + get hasFooter() { + return this.columns.some(({ renderFoot }) => renderFoot) + } + + @computed + get editing() { + return !isEmpty(this.observedProps.store.currentOne) + } + + renderCustomBody = ({ + key, + type, + multiple, + options, + accept, + rows, + }: Column): Column["renderBody"] => + type === "url" + ? ({ [key]: value }: D) => + value && ( + + {value as string} + + ) + : type === "email" + ? ({ [key]: value }: D) => + value && ( + + {value as string} + + ) + : type === "tel" + ? ({ [key]: value }: D) => + value && ( + + {value as string} + + ) + : type === "file" + ? ({ [key]: value }: D) => + ( + (Array.isArray(value) ? value : [value]) as string[] + ).map( + (path) => + path && ( + + ) + ) + : options || multiple + ? ({ [key]: value }: D) => + value && ( + ({ text }))} + /> + ) + : !options && rows + ? ({ [key]: value }: D) => ( +

+ {value as string} +

+ ) + : undefined + + renderTable() { + const { + className, + columns: _, + store, + translator, + editable, + deletable, + onCheck, + onSubmit, + responsive = true, + ...tableProps + } = this.props + const { hasHeader, hasFooter, columns, editing } = this + const { indexKey, downloading, currentPage } = store + + return ( + + {hasHeader && ( + + + {columns.map(({ key, renderHead }, index) => + key || renderHead ? ( + + {typeof renderHead === "function" + ? renderHead(key) + : renderHead || (key as string)} + + ) : null + )} + + + )} + + {!editing && downloading > 0 ? ( + + + + + + ) : ( + currentPage.map((data: D) => ( + + {columns.map(({ key, renderBody }, index) => + key || renderBody ? ( + + {renderBody?.(data) || (key && data[key])} + + ) : null + )} + + )) + )} + + + {hasFooter && ( + + + {columns.map(({ key, renderFoot }, index) => + key || renderFoot ? ( + + {typeof renderFoot === "function" + ? renderFoot(key) + : renderFoot || (key as string)} + + ) : null + )} + + + )} +
+ ) + } + + getList = debounce( + ({ pageIndex, pageSize }: { pageIndex: number; pageSize: number }) => { + const { store, filter } = this.props + + if (store.downloading < 1) store.getList(filter, pageIndex, pageSize) + } + ) + + async deleteList(keys: IDType[]) { + const { translator, store } = this.props + + if (confirm(translator.t("sure_to_delete_x", { keys }))) + for (const key of keys) await store.deleteOne(key) + } + + render() { + const { + className, + editable, + deletable, + store, + translator, + onSubmit, + ...divProps + } = this.props + const { t } = translator + const { indexKey, pageSize, pageIndex, pageCount, totalCount } = store + + return ( +
+
+ +
+ {deletable && ( + + )} + {editable && ( + + )} +
+
+ + {this.renderTable()} + + {editable && ( + ({ + ...field, + renderLabel: renderHead, + }))} + {...{ store, translator, onSubmit }} + /> + )} +
+ ) + } +} diff --git a/registry/new-york/ui/badge.tsx b/registry/new-york/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/registry/new-york/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/registry/new-york/ui/checkbox.tsx b/registry/new-york/ui/checkbox.tsx new file mode 100644 index 0000000..ddbdd01 --- /dev/null +++ b/registry/new-york/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/registry/new-york/ui/spinner.tsx b/registry/new-york/ui/spinner.tsx new file mode 100644 index 0000000..66b21b6 --- /dev/null +++ b/registry/new-york/ui/spinner.tsx @@ -0,0 +1,17 @@ +import * as React from "react" +import { Loader2 } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Spinner({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ +
+ ) +} + +export { Spinner } diff --git a/registry/new-york/ui/table.tsx b/registry/new-york/ui/table.tsx new file mode 100644 index 0000000..7f3502f --- /dev/null +++ b/registry/new-york/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} From a630c985b1fa9934323cead41a478538ac868420 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:32:54 +0000 Subject: [PATCH 3/3] Add documentation and fix TypeScript issues Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com> --- package.json | 1 + pnpm-lock.yaml | 8 + public/r/rest-table.json | 4 +- registry/new-york/blocks/rest-table/README.md | 221 ++++++++++++++++++ .../rest-table/components/rest-form-modal.tsx | 2 +- .../new-york/blocks/rest-table/example.tsx | 108 +++++++++ .../new-york/blocks/rest-table/rest-table.tsx | 99 +++++--- 7 files changed, 403 insertions(+), 40 deletions(-) create mode 100644 registry/new-york/blocks/rest-table/README.md create mode 100644 registry/new-york/blocks/rest-table/example.tsx diff --git a/package.json b/package.json index b13cbe5..5a0211f 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@tailwindcss/postcss": "^4.1.11", + "@types/lodash": "^4.17.20", "@types/node": "^20.19.9", "@types/react": "19.1.2", "@types/react-dom": "19.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a44d5cb..f7608d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,6 +79,9 @@ importers: '@tailwindcss/postcss': specifier: ^4.1.11 version: 4.1.11 + '@types/lodash': + specifier: ^4.17.20 + version: 4.17.20 '@types/node': specifier: ^20.19.9 version: 20.19.9 @@ -888,6 +891,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + '@types/node@20.19.9': resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==} @@ -3971,6 +3977,8 @@ snapshots: '@types/json5@0.0.29': {} + '@types/lodash@4.17.20': {} + '@types/node@20.19.9': dependencies: undici-types: 6.21.0 diff --git a/public/r/rest-table.json b/public/r/rest-table.json index 3eedc0e..d4c6f53 100644 --- a/public/r/rest-table.json +++ b/public/r/rest-table.json @@ -26,7 +26,7 @@ "files": [ { "path": "registry/new-york/blocks/rest-table/rest-table.tsx", - "content": "import * as React from \"react\"\nimport { debounce } from \"lodash\"\nimport { computed, observable } from \"mobx\"\nimport { TranslationModel } from \"mobx-i18n\"\nimport { observer } from \"mobx-react\"\nimport { ObservedComponent } from \"mobx-react-helper\"\nimport { DataObject, Filter, IDType } from \"mobx-restful\"\nimport { isEmpty } from \"web-utility\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/registry/new-york/ui/button\"\nimport { Checkbox } from \"@/registry/new-york/ui/checkbox\"\nimport { Spinner } from \"@/registry/new-york/ui/spinner\"\nimport {\n Table,\n TableBody,\n TableCell,\n TableFooter,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/registry/new-york/ui/table\"\n\nimport { BadgeBar } from \"./components/badge-bar\"\nimport { FilePreview } from \"./components/file-preview\"\nimport { Pager } from \"./components/pager\"\nimport { Field, RestFormModal } from \"./components/rest-form-modal\"\n\nexport interface Column\n extends Omit, \"renderLabel\"> {\n renderHead?: Field[\"renderLabel\"]\n renderBody?: (data: T) => React.ReactNode\n renderFoot?: React.ReactNode | ((data: keyof T) => React.ReactNode)\n}\n\nexport interface RestFormProps {\n store: any\n translator: any\n onSubmit?: (data: T) => Promise\n}\n\ntype Translator = RestFormProps[\"translator\"] &\n TranslationModel<\n string,\n | \"create\"\n | \"view\"\n | \"edit\"\n | \"delete\"\n | \"total_x_rows\"\n | \"sure_to_delete_x\"\n >\n\nexport interface RestTableProps<\n D extends DataObject,\n F extends Filter = Filter\n> extends Omit, \"onSubmit\">,\n Omit, \"id\" | \"fields\" | \"translator\"> {\n filter?: F\n editable?: boolean\n deletable?: boolean\n columns: Column[]\n translator: Translator\n onCheck?: (keys: IDType[]) => any\n responsive?: boolean\n}\n\n@observer\nexport class RestTable<\n D extends DataObject,\n F extends Filter = Filter\n> extends ObservedComponent> {\n static readonly displayName = \"RestTable\"\n\n componentDidMount() {\n const { store, filter } = this.props\n\n store.clear()\n store.getList(filter)\n }\n\n @observable\n accessor checkedKeys: IDType[] = []\n\n toggleCheck(key: IDType) {\n const { checkedKeys } = this\n const index = checkedKeys.indexOf(key)\n\n this.checkedKeys =\n index < 0\n ? [...checkedKeys, key]\n : [...checkedKeys.slice(0, index), ...checkedKeys.slice(index + 1)]\n\n this.props.onCheck?.(this.checkedKeys)\n }\n\n toggleCheckAll = () => {\n const { store, onCheck } = this.props\n const { indexKey, currentPage } = store\n\n this.checkedKeys = this.checkedKeys.length\n ? []\n : currentPage.map(({ [indexKey]: ID }: D) => ID)\n\n onCheck?.(this.checkedKeys)\n }\n\n @computed\n get checkColumn(): Column {\n const { checkedKeys, toggleCheckAll } = this\n const { indexKey, currentPage } = this.observedProps.store\n\n return {\n renderHead: () => {\n const allChecked =\n !!currentPage[0] &&\n currentPage.every(({ [indexKey]: ID }: D) =>\n checkedKeys.includes(ID)\n )\n const someChecked =\n !!checkedKeys.length && checkedKeys.length < currentPage.length\n\n return (\n \n )\n },\n renderBody: ({ [indexKey]: ID }: D) => (\n this.toggleCheck(ID)}\n />\n ),\n } as Column\n }\n\n @computed\n get operateColumn(): Column {\n const { editable, deletable, columns, store, translator } =\n this.observedProps\n const { t } = translator\n const readOnly = columns.every(({ readOnly }) => readOnly)\n const disabled = columns.every(({ disabled }) => disabled)\n\n return {\n renderHead: () => <>,\n renderBody: (data: D) => (\n
\n {!disabled &&\n editable && (\n (store.currentOne = data)}\n >\n {readOnly ? t(\"view\") : t(\"edit\")}\n \n )}\n {deletable && (\n this.deleteList([data.id])}\n >\n {t(\"delete\")}\n \n )}\n
\n ),\n } as Column\n }\n\n @computed\n get columns(): Column[] {\n const { editable, deletable, columns, onCheck } = this.observedProps\n\n return [\n onCheck && this.checkColumn,\n ...columns.map(\n ({ renderBody, ...column }) =>\n ({\n ...column,\n renderBody: renderBody ?? this.renderCustomBody(column),\n }) as Column\n ),\n (editable || deletable) && this.operateColumn,\n ].filter(Boolean) as Column[]\n }\n\n @computed\n get hasHeader() {\n return this.columns.some(({ renderHead }) => renderHead)\n }\n\n @computed\n get hasFooter() {\n return this.columns.some(({ renderFoot }) => renderFoot)\n }\n\n @computed\n get editing() {\n return !isEmpty(this.observedProps.store.currentOne)\n }\n\n renderCustomBody = ({\n key,\n type,\n multiple,\n options,\n accept,\n rows,\n }: Column): Column[\"renderBody\"] =>\n type === \"url\"\n ? ({ [key]: value }: D) =>\n value && (\n \n {value as string}\n \n )\n : type === \"email\"\n ? ({ [key]: value }: D) =>\n value && (\n \n {value as string}\n \n )\n : type === \"tel\"\n ? ({ [key]: value }: D) =>\n value && (\n \n {value as string}\n \n )\n : type === \"file\"\n ? ({ [key]: value }: D) =>\n (\n (Array.isArray(value) ? value : [value]) as string[]\n ).map(\n (path) =>\n path && (\n \n )\n )\n : options || multiple\n ? ({ [key]: value }: D) =>\n value && (\n ({ text }))}\n />\n )\n : !options && rows\n ? ({ [key]: value }: D) => (\n \n {value as string}\n

\n )\n : undefined\n\n renderTable() {\n const {\n className,\n columns: _,\n store,\n translator,\n editable,\n deletable,\n onCheck,\n onSubmit,\n responsive = true,\n ...tableProps\n } = this.props\n const { hasHeader, hasFooter, columns, editing } = this\n const { indexKey, downloading, currentPage } = store\n\n return (\n \n {hasHeader && (\n \n \n {columns.map(({ key, renderHead }, index) =>\n key || renderHead ? (\n \n {typeof renderHead === \"function\"\n ? renderHead(key)\n : renderHead || (key as string)}\n \n ) : null\n )}\n \n \n )}\n \n {!editing && downloading > 0 ? (\n \n \n \n \n \n ) : (\n currentPage.map((data: D) => (\n \n {columns.map(({ key, renderBody }, index) =>\n key || renderBody ? (\n \n {renderBody?.(data) || (key && data[key])}\n \n ) : null\n )}\n \n ))\n )}\n \n\n {hasFooter && (\n \n \n {columns.map(({ key, renderFoot }, index) =>\n key || renderFoot ? (\n \n {typeof renderFoot === \"function\"\n ? renderFoot(key)\n : renderFoot || (key as string)}\n \n ) : null\n )}\n \n \n )}\n
\n )\n }\n\n getList = debounce(\n ({ pageIndex, pageSize }: { pageIndex: number; pageSize: number }) => {\n const { store, filter } = this.props\n\n if (store.downloading < 1) store.getList(filter, pageIndex, pageSize)\n }\n )\n\n async deleteList(keys: IDType[]) {\n const { translator, store } = this.props\n\n if (confirm(translator.t(\"sure_to_delete_x\", { keys })))\n for (const key of keys) await store.deleteOne(key)\n }\n\n render() {\n const {\n className,\n editable,\n deletable,\n store,\n translator,\n onSubmit,\n ...divProps\n } = this.props\n const { t } = translator\n const { indexKey, pageSize, pageIndex, pageCount, totalCount } = store\n\n return (\n
\n
\n \n
\n {deletable && (\n this.deleteList(this.checkedKeys)}\n >\n {t(\"delete\")}\n \n )}\n {editable && (\n \n (store.currentOne[indexKey] = \"\" as D[keyof D])\n }\n >\n {t(\"create\")}\n \n )}\n
\n
\n\n {this.renderTable()}\n\n {editable && (\n ({\n ...field,\n renderLabel: renderHead,\n }))}\n {...{ store, translator, onSubmit }}\n />\n )}\n
\n )\n }\n}\n", + "content": "import * as React from \"react\"\nimport { debounce } from \"lodash\"\nimport { computed, observable } from \"mobx\"\nimport { TranslationModel } from \"mobx-i18n\"\nimport { observer } from \"mobx-react\"\nimport { ObservedComponent } from \"mobx-react-helper\"\nimport { DataObject, Filter, IDType } from \"mobx-restful\"\nimport { isEmpty } from \"web-utility\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/registry/new-york/ui/button\"\nimport { Checkbox } from \"@/registry/new-york/ui/checkbox\"\nimport { Spinner } from \"@/registry/new-york/ui/spinner\"\nimport {\n Table,\n TableBody,\n TableCell,\n TableFooter,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/registry/new-york/ui/table\"\n\nimport { BadgeBar } from \"./components/badge-bar\"\nimport { FilePreview } from \"./components/file-preview\"\nimport { Pager } from \"./components/pager\"\nimport { Field, RestFormModal } from \"./components/rest-form-modal\"\n\nexport interface Column\n extends Partial, \"renderLabel\">> {\n key?: keyof T\n renderHead?: Field[\"renderLabel\"]\n renderBody?: (data: T) => React.ReactNode\n renderFoot?: React.ReactNode | ((data: keyof T) => React.ReactNode)\n}\n\nexport interface RestFormProps {\n store: any\n translator: any\n onSubmit?: (data: T) => Promise\n}\n\ntype Translator = RestFormProps[\"translator\"] &\n TranslationModel<\n string,\n | \"create\"\n | \"view\"\n | \"edit\"\n | \"delete\"\n | \"total_x_rows\"\n | \"sure_to_delete_x\"\n >\n\nexport interface RestTableProps<\n D extends DataObject,\n F extends Filter = Filter\n> extends Omit, \"onSubmit\">,\n Omit, \"id\" | \"fields\" | \"translator\"> {\n filter?: F\n editable?: boolean\n deletable?: boolean\n columns: Column[]\n translator: Translator\n onCheck?: (keys: IDType[]) => any\n responsive?: boolean\n}\n\n@observer\nexport class RestTable<\n D extends DataObject,\n F extends Filter = Filter\n> extends ObservedComponent> {\n static readonly displayName = \"RestTable\"\n\n componentDidMount() {\n const { store, filter } = this.props\n\n store.clear()\n store.getList(filter)\n }\n\n @observable\n accessor checkedKeys: IDType[] = []\n\n toggleCheck(key: IDType) {\n const { checkedKeys } = this\n const index = checkedKeys.indexOf(key)\n\n this.checkedKeys =\n index < 0\n ? [...checkedKeys, key]\n : [...checkedKeys.slice(0, index), ...checkedKeys.slice(index + 1)]\n\n this.props.onCheck?.(this.checkedKeys)\n }\n\n toggleCheckAll = () => {\n const { store, onCheck } = this.props\n const { indexKey, currentPage } = store\n\n this.checkedKeys = this.checkedKeys.length\n ? []\n : currentPage.map(({ [indexKey]: ID }: D) => ID)\n\n onCheck?.(this.checkedKeys)\n }\n\n @computed\n get checkColumn(): Column {\n const { checkedKeys, toggleCheckAll } = this\n const { indexKey, currentPage } = this.observedProps.store\n\n return {\n renderHead: () => {\n const allChecked =\n !!currentPage[0] &&\n currentPage.every(({ [indexKey]: ID }: D) =>\n checkedKeys.includes(ID)\n )\n const someChecked =\n !!checkedKeys.length && checkedKeys.length < currentPage.length\n\n return (\n \n )\n },\n renderBody: ({ [indexKey]: ID }: D) => (\n this.toggleCheck(ID)}\n />\n ),\n } as Column\n }\n\n @computed\n get operateColumn(): Column {\n const { editable, deletable, columns, store, translator } =\n this.observedProps\n const { t } = translator\n const readOnly = columns.every(({ readOnly }) => readOnly)\n const disabled = columns.every(({ disabled }) => disabled)\n\n return {\n renderHead: () => <>,\n renderBody: (data: D) => (\n
\n {!disabled &&\n editable && (\n (store.currentOne = data)}\n >\n {readOnly ? t(\"view\") : t(\"edit\")}\n \n )}\n {deletable && (\n this.deleteList([data.id])}\n >\n {t(\"delete\")}\n \n )}\n
\n ),\n } as Column\n }\n\n @computed\n get columns(): Column[] {\n const { editable, deletable, columns, onCheck } = this.observedProps\n\n return [\n onCheck && this.checkColumn,\n ...columns.map(\n ({ renderBody, ...column }) =>\n ({\n ...column,\n renderBody: renderBody ?? this.renderCustomBody(column),\n }) as Column\n ),\n (editable || deletable) && this.operateColumn,\n ].filter(Boolean) as Column[]\n }\n\n @computed\n get hasHeader() {\n return this.columns.some(({ renderHead }) => renderHead)\n }\n\n @computed\n get hasFooter() {\n return this.columns.some(({ renderFoot }) => renderFoot)\n }\n\n @computed\n get editing() {\n return !isEmpty(this.observedProps.store.currentOne)\n }\n\n renderCustomBody = ({\n key,\n type,\n multiple,\n options,\n accept,\n rows,\n }: Column): Column[\"renderBody\"] => {\n if (!key) return undefined\n\n return type === \"url\"\n ? (data: D) => {\n const value = data[key]\n return (\n value && (\n \n {value as string}\n \n )\n )\n }\n : type === \"email\"\n ? (data: D) => {\n const value = data[key]\n return (\n value && (\n \n {value as string}\n \n )\n )\n }\n : type === \"tel\"\n ? (data: D) => {\n const value = data[key]\n return (\n value && (\n \n {value as string}\n \n )\n )\n }\n : type === \"file\"\n ? (data: D) => {\n const value = data[key]\n return (\n (Array.isArray(value) ? value : [value]) as string[]\n ).map(\n (path) =>\n path && (\n \n )\n )\n }\n : options || multiple\n ? (data: D) => {\n const value = data[key]\n return (\n value && (\n ({ text }))}\n />\n )\n )\n }\n : !options && rows\n ? (data: D) => {\n const value = data[key]\n return (\n \n {value as string}\n

\n )\n }\n : undefined\n }\n\n renderTable() {\n const {\n className,\n columns: _,\n store,\n translator,\n editable,\n deletable,\n onCheck,\n onSubmit,\n responsive = true,\n ...tableProps\n } = this.props\n const { hasHeader, hasFooter, columns, editing } = this\n const { indexKey, downloading, currentPage } = store\n\n return (\n \n {hasHeader && (\n \n \n {columns.map(({ key, renderHead }, index) =>\n key || renderHead ? (\n \n {typeof renderHead === \"function\"\n ? key && renderHead(key)\n : renderHead || (key as string)}\n \n ) : null\n )}\n \n \n )}\n \n {!editing && downloading > 0 ? (\n \n \n \n \n \n ) : (\n currentPage.map((data: D) => (\n \n {columns.map(({ key, renderBody }, index) =>\n key || renderBody ? (\n \n {renderBody?.(data) || (key && data[key])}\n \n ) : null\n )}\n \n ))\n )}\n \n\n {hasFooter && (\n \n \n {columns.map(({ key, renderFoot }, index) =>\n key || renderFoot ? (\n \n {typeof renderFoot === \"function\"\n ? key && renderFoot(key)\n : renderFoot || (key as string)}\n \n ) : null\n )}\n \n \n )}\n
\n )\n }\n\n getList = debounce(\n ({ pageIndex, pageSize }: { pageIndex: number; pageSize: number }) => {\n const { store, filter } = this.props\n\n if (store.downloading < 1) store.getList(filter, pageIndex, pageSize)\n }\n )\n\n async deleteList(keys: IDType[]) {\n const { translator, store } = this.props\n\n if (confirm(translator.t(\"sure_to_delete_x\", { keys })))\n for (const key of keys) await store.deleteOne(key)\n }\n\n render() {\n const {\n className,\n editable,\n deletable,\n store,\n translator,\n onSubmit,\n ...divProps\n } = this.props\n const { t } = translator\n const { indexKey, pageSize, pageIndex, pageCount, totalCount } = store\n\n return (\n
\n
\n \n
\n {deletable && (\n this.deleteList(this.checkedKeys)}\n >\n {t(\"delete\")}\n \n )}\n {editable && (\n \n (store.currentOne[indexKey] = \"\" as D[keyof D])\n }\n >\n {t(\"create\")}\n \n )}\n
\n
\n\n {this.renderTable()}\n\n {editable && (\n ({\n ...field,\n renderLabel: renderHead,\n }))}\n {...{ store, translator, onSubmit }}\n />\n )}\n
\n )\n }\n}\n", "type": "registry:component" }, { @@ -46,7 +46,7 @@ }, { "path": "registry/new-york/blocks/rest-table/components/rest-form-modal.tsx", - "content": "import * as React from \"react\"\nimport { observer } from \"mobx-react\"\nimport { DataObject } from \"mobx-restful\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york/ui/card\"\nimport { Button } from \"@/registry/new-york/ui/button\"\nimport { Input } from \"@/registry/new-york/ui/input\"\nimport { Label } from \"@/registry/new-york/ui/label\"\n\nexport interface Field {\n key: keyof T\n type?: string\n renderLabel?: ((key: keyof T) => React.ReactNode) | React.ReactNode\n readOnly?: boolean\n disabled?: boolean\n multiple?: boolean\n options?: any[]\n accept?: string\n rows?: number\n}\n\nexport interface RestFormModalProps {\n fields: Field[]\n store: any\n translator: any\n onSubmit?: (data: T) => Promise\n}\n\n@observer\nexport class RestFormModal extends React.Component<\n RestFormModalProps\n> {\n handleClose = () => {\n this.props.store.currentOne = {}\n }\n\n handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n const { store, onSubmit } = this.props\n const data = store.currentOne\n\n if (onSubmit) {\n await onSubmit(data)\n } else {\n if (data[store.indexKey]) {\n await store.updateOne(data, data[store.indexKey])\n } else {\n await store.createOne(data)\n }\n }\n\n this.handleClose()\n }\n\n render() {\n const { fields, store, translator } = this.props\n const { currentOne } = store\n const { t } = translator\n\n if (!currentOne || Object.keys(currentOne).length === 0) {\n return null\n }\n\n const isEditing = !!currentOne[store.indexKey]\n\n return (\n
\n \n \n {isEditing ? t(\"edit\") : t(\"create\")}\n \n {isEditing\n ? \"Edit the existing item\"\n : \"Create a new item\"}\n \n \n
\n \n {fields.map((field) => {\n if (!field.key || field.readOnly) return null\n\n const label =\n typeof field.renderLabel === \"function\"\n ? field.renderLabel(field.key)\n : field.renderLabel || String(field.key)\n\n return (\n
\n \n {\n currentOne[field.key] = e.target.value as T[keyof T]\n }}\n disabled={field.disabled}\n />\n
\n )\n })}\n
\n \n \n \n \n
\n
\n
\n )\n }\n}\n", + "content": "import * as React from \"react\"\nimport { observer } from \"mobx-react\"\nimport { DataObject } from \"mobx-restful\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york/ui/card\"\nimport { Button } from \"@/registry/new-york/ui/button\"\nimport { Input } from \"@/registry/new-york/ui/input\"\nimport { Label } from \"@/registry/new-york/ui/label\"\n\nexport interface Field {\n key?: keyof T\n type?: string\n renderLabel?: ((key: keyof T) => React.ReactNode) | React.ReactNode\n readOnly?: boolean\n disabled?: boolean\n multiple?: boolean\n options?: any[]\n accept?: string\n rows?: number\n}\n\nexport interface RestFormModalProps {\n fields: Field[]\n store: any\n translator: any\n onSubmit?: (data: T) => Promise\n}\n\n@observer\nexport class RestFormModal extends React.Component<\n RestFormModalProps\n> {\n handleClose = () => {\n this.props.store.currentOne = {}\n }\n\n handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n const { store, onSubmit } = this.props\n const data = store.currentOne\n\n if (onSubmit) {\n await onSubmit(data)\n } else {\n if (data[store.indexKey]) {\n await store.updateOne(data, data[store.indexKey])\n } else {\n await store.createOne(data)\n }\n }\n\n this.handleClose()\n }\n\n render() {\n const { fields, store, translator } = this.props\n const { currentOne } = store\n const { t } = translator\n\n if (!currentOne || Object.keys(currentOne).length === 0) {\n return null\n }\n\n const isEditing = !!currentOne[store.indexKey]\n\n return (\n
\n \n \n {isEditing ? t(\"edit\") : t(\"create\")}\n \n {isEditing\n ? \"Edit the existing item\"\n : \"Create a new item\"}\n \n \n
\n \n {fields.map((field) => {\n if (!field.key || field.readOnly) return null\n\n const label =\n typeof field.renderLabel === \"function\"\n ? field.renderLabel(field.key)\n : field.renderLabel || String(field.key)\n\n return (\n
\n \n {\n currentOne[field.key] = e.target.value as T[keyof T]\n }}\n disabled={field.disabled}\n />\n
\n )\n })}\n
\n \n \n \n \n
\n
\n
\n )\n }\n}\n", "type": "registry:component" } ] diff --git a/registry/new-york/blocks/rest-table/README.md b/registry/new-york/blocks/rest-table/README.md new file mode 100644 index 0000000..fec1115 --- /dev/null +++ b/registry/new-york/blocks/rest-table/README.md @@ -0,0 +1,221 @@ +# RestTable Component + +A MobX-powered table component with full CRUD operations support, migrated to use shadcn UI components. + +## Features + +- **React Class Component**: Maintains the original React class component architecture +- **MobX Integration**: Full reactive data management with MobX +- **CRUD Operations**: Built-in support for Create, Read, Update, Delete operations +- **Pagination**: Integrated pagination controls +- **Row Selection**: Checkbox-based row selection +- **Custom Rendering**: Flexible column rendering with custom components +- **Type Safety**: Full TypeScript support +- **shadcn UI**: Built on top of shadcn UI components + +## Installation + +```bash +npx shadcn@latest add rest-table +``` + +This will install: +- The RestTable component and all its supporting components +- Required UI components (table, checkbox, spinner, badge, button, card, input, label) +- Required dependencies (mobx, mobx-react, mobx-restful, etc.) + +## Usage + +### Basic Example + +```tsx +import { RestTable } from "@/components/rest-table" +import { RestStore } from "mobx-restful" +import { TranslationModel } from "mobx-i18n" + +// Define your data type +interface User { + id: string + name: string + email: string + role: string +} + +// Create a MobX store +const userStore = new RestStore({ + baseURI: "https://api.example.com/users", + indexKey: "id" +}) + +// Create translator +const translator = new TranslationModel({ + en: { + create: "Create", + edit: "Edit", + delete: "Delete", + view: "View", + total_x_rows: "Total {totalCount} rows", + sure_to_delete_x: "Are you sure to delete {keys}?" + } +}) + +// Use the RestTable component +function UsersTable() { + return ( + "Name", + }, + { + key: "email", + type: "email", + renderHead: () => "Email", + }, + { + key: "role", + renderHead: () => "Role", + } + ]} + /> + ) +} +``` + +### Advanced Column Rendering + +```tsx + "Avatar", + }, + { + key: "name", + renderHead: () => "Name", + renderBody: (user) => ( +
{user.name}
+ ) + }, + { + key: "tags", + multiple: true, + renderHead: () => "Tags", + }, + { + key: "website", + type: "url", + renderHead: () => "Website", + } + ]} +/> +``` + +### With Row Selection + +```tsx +function UsersTable() { + const handleCheck = (keys: string[]) => { + console.log("Selected keys:", keys) + } + + return ( + + ) +} +``` + +### Custom Submit Handler + +```tsx + { + // Custom submit logic + await customAPI.save(data) + }} + columns={[...]} +/> +``` + +## API Reference + +### RestTable Props + +| Prop | Type | Description | +|------|------|-------------| +| `store` | `RestStore` | MobX RESTful store instance | +| `translator` | `Translator` | Translation model with required keys | +| `columns` | `Column[]` | Array of column definitions | +| `filter` | `Filter` | Optional filter for data fetching | +| `editable` | `boolean` | Enable edit/create operations | +| `deletable` | `boolean` | Enable delete operations | +| `onCheck` | `(keys: IDType[]) => void` | Callback for row selection | +| `onSubmit` | `(data: D) => Promise` | Custom submit handler | +| `responsive` | `boolean` | Enable responsive table layout (default: true) | + +### Column Definition + +| Property | Type | Description | +|----------|------|-------------| +| `key` | `keyof T` | Data field key | +| `type` | `string` | Field type: 'text', 'email', 'url', 'tel', 'file' | +| `renderHead` | `React.ReactNode \| Function` | Custom header renderer | +| `renderBody` | `(data: T) => React.ReactNode` | Custom cell renderer | +| `renderFoot` | `React.ReactNode \| Function` | Custom footer renderer | +| `readOnly` | `boolean` | Make field read-only | +| `disabled` | `boolean` | Disable field | +| `multiple` | `boolean` | Enable multiple values (renders as badges) | +| `options` | `any[]` | Field options | +| `accept` | `string` | File accept types | +| `rows` | `number` | Textarea rows | + +## Translation Keys + +The translator must provide the following translation keys: + +- `create`: Create button text +- `edit`: Edit button text +- `view`: View button text +- `delete`: Delete button text +- `total_x_rows`: Total rows text (with `{totalCount}` placeholder) +- `sure_to_delete_x`: Delete confirmation text (with `{keys}` placeholder) + +## Architecture + +The RestTable maintains the **React class component** architecture from the original MobX-RESTful-table implementation: + +- Uses `@observer` decorator for MobX reactivity +- Extends `ObservedComponent` for enhanced observable props handling +- Uses class properties with `@observable` and `@computed` decorators +- All methods are class methods, not hooks + +This ensures compatibility with existing MobX-based applications and maintains the proven architecture pattern. + +## Components Structure + +The RestTable includes several supporting components: + +- **BadgeBar**: Displays multiple values as badges +- **FilePreview**: Previews file URLs (images or links) +- **Pager**: Pagination controls +- **RestFormModal**: Modal form for create/edit operations + +All components follow the same class component pattern for consistency. diff --git a/registry/new-york/blocks/rest-table/components/rest-form-modal.tsx b/registry/new-york/blocks/rest-table/components/rest-form-modal.tsx index e8c29be..2609267 100644 --- a/registry/new-york/blocks/rest-table/components/rest-form-modal.tsx +++ b/registry/new-york/blocks/rest-table/components/rest-form-modal.tsx @@ -14,7 +14,7 @@ import { Input } from "@/registry/new-york/ui/input" import { Label } from "@/registry/new-york/ui/label" export interface Field { - key: keyof T + key?: keyof T type?: string renderLabel?: ((key: keyof T) => React.ReactNode) | React.ReactNode readOnly?: boolean diff --git a/registry/new-york/blocks/rest-table/example.tsx b/registry/new-york/blocks/rest-table/example.tsx new file mode 100644 index 0000000..01d10b6 --- /dev/null +++ b/registry/new-york/blocks/rest-table/example.tsx @@ -0,0 +1,108 @@ +"use client" + +import * as React from "react" +import { RestTable, Column } from "./rest-table" +import { makeObservable, observable } from "mobx" +import { DataObject } from "mobx-restful" + +// Mock store for demonstration +class MockStore { + @observable accessor currentPage: T[] = [] + @observable accessor currentOne: Partial = {} + @observable accessor downloading = 0 + @observable accessor pageSize = 10 + @observable accessor pageIndex = 1 + @observable accessor pageCount = 1 + @observable accessor totalCount = 0 + + indexKey: keyof T = "id" as keyof T + + constructor() { + makeObservable(this) + } + + clear() { + this.currentPage = [] + } + + async getList() { + // Mock data + this.downloading = 1 + setTimeout(() => { + this.currentPage = [] as T[] + this.totalCount = 0 + this.pageCount = 1 + this.downloading = 0 + }, 500) + } + + async deleteOne(id: any) { + this.currentPage = this.currentPage.filter( + (item) => item[this.indexKey] !== id + ) + } + + async createOne(data: Partial) { + // Mock create + console.log("Creating:", data) + } + + async updateOne(data: Partial, id: any) { + // Mock update + console.log("Updating:", data, id) + } +} + +// Mock translator +const mockTranslator = { + t: (key: string, params?: any) => { + const translations: Record = { + create: "Create", + edit: "Edit", + view: "View", + delete: "Delete", + total_x_rows: `Total ${params?.totalCount || 0} rows`, + sure_to_delete_x: `Are you sure to delete ${params?.keys || ""}?`, + } + return translations[key] || key + }, +} + +interface ExampleData extends DataObject { + id: string + name: string + email: string + status: string +} + +export function RestTableExample() { + const [store] = React.useState(() => new MockStore()) + + const columns: Column[] = [ + { + key: "name", + renderHead: () => "Name", + }, + { + key: "email", + type: "email", + renderHead: () => "Email", + }, + { + key: "status", + renderHead: () => "Status", + }, + ] + + return ( +
+ +
+ ) +} diff --git a/registry/new-york/blocks/rest-table/rest-table.tsx b/registry/new-york/blocks/rest-table/rest-table.tsx index c4afff1..ea8cd3a 100644 --- a/registry/new-york/blocks/rest-table/rest-table.tsx +++ b/registry/new-york/blocks/rest-table/rest-table.tsx @@ -27,7 +27,8 @@ import { Pager } from "./components/pager" import { Field, RestFormModal } from "./components/rest-form-modal" export interface Column - extends Omit, "renderLabel"> { + extends Partial, "renderLabel">> { + key?: keyof T renderHead?: Field["renderLabel"] renderBody?: (data: T) => React.ReactNode renderFoot?: React.ReactNode | ((data: keyof T) => React.ReactNode) @@ -213,42 +214,57 @@ export class RestTable< options, accept, rows, - }: Column): Column["renderBody"] => - type === "url" - ? ({ [key]: value }: D) => - value && ( - - {value as string} - - ) - : type === "email" - ? ({ [key]: value }: D) => + }: Column): Column["renderBody"] => { + if (!key) return undefined + + return type === "url" + ? (data: D) => { + const value = data[key] + return ( value && ( {value as string} ) - : type === "tel" - ? ({ [key]: value }: D) => + ) + } + : type === "email" + ? (data: D) => { + const value = data[key] + return ( value && ( {value as string} ) + ) + } + : type === "tel" + ? (data: D) => { + const value = data[key] + return ( + value && ( + + {value as string} + + ) + ) + } : type === "file" - ? ({ [key]: value }: D) => - ( + ? (data: D) => { + const value = data[key] + return ( (Array.isArray(value) ? value : [value]) as string[] ).map( (path) => @@ -256,23 +272,32 @@ export class RestTable< ) ) + } : options || multiple - ? ({ [key]: value }: D) => - value && ( - ({ text }))} - /> + ? (data: D) => { + const value = data[key] + return ( + value && ( + ({ text }))} + /> + ) ) + } : !options && rows - ? ({ [key]: value }: D) => ( -

- {value as string} -

- ) + ? (data: D) => { + const value = data[key] + return ( +

+ {value as string} +

+ ) + } : undefined + } renderTable() { const { @@ -299,7 +324,7 @@ export class RestTable< key || renderHead ? ( {typeof renderHead === "function" - ? renderHead(key) + ? key && renderHead(key) : renderHead || (key as string)} ) : null @@ -336,7 +361,7 @@ export class RestTable< key || renderFoot ? ( {typeof renderFoot === "function" - ? renderFoot(key) + ? key && renderFoot(key) : renderFoot || (key as string)} ) : null