diff --git a/ordinis-admin-ui/package.json b/ordinis-admin-ui/package.json index f4c2bba..83d9f90 100644 --- a/ordinis-admin-ui/package.json +++ b/ordinis-admin-ui/package.json @@ -31,6 +31,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "d3-force": "^3.0.0", + "d3-geo": "^3.1.1", "i18next": "^26.0.3", "i18next-browser-languagedetector": "^8.0.2", "leaflet": "^1.9.4", @@ -44,7 +45,9 @@ "react-oidc-context": "^3.3.1", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", - "tw-animate-css": "^1.4.0" + "topojson-client": "^3.1.0", + "tw-animate-css": "^1.4.0", + "world-atlas": "^2.0.2" }, "devDependencies": { "@semantic-release/changelog": "^6.0.3", @@ -59,9 +62,12 @@ "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/d3-force": "^3.0.10", + "@types/d3-geo": "^3.1.0", + "@types/geojson": "^7946.0.16", "@types/leaflet": "^1.9.21", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", + "@types/topojson-client": "^3.1.5", "@vitejs/plugin-react": "^4.3.4", "conventional-changelog-conventionalcommits": "^9.3.1", "jsdom": "^29.1.1", diff --git a/ordinis-admin-ui/pnpm-lock.yaml b/ordinis-admin-ui/pnpm-lock.yaml index a77534c..71c8276 100644 --- a/ordinis-admin-ui/pnpm-lock.yaml +++ b/ordinis-admin-ui/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: d3-force: specifier: ^3.0.0 version: 3.0.0 + d3-geo: + specifier: ^3.1.1 + version: 3.1.1 i18next: specifier: ^26.0.3 version: 26.0.8(typescript@5.9.3) @@ -107,9 +110,15 @@ importers: tailwind-merge: specifier: ^3.6.0 version: 3.6.0 + topojson-client: + specifier: ^3.1.0 + version: 3.1.0 tw-animate-css: specifier: ^1.4.0 version: 1.4.0 + world-atlas: + specifier: ^2.0.2 + version: 2.0.2 devDependencies: '@semantic-release/changelog': specifier: ^6.0.3 @@ -147,6 +156,12 @@ importers: '@types/d3-force': specifier: ^3.0.10 version: 3.0.10 + '@types/d3-geo': + specifier: ^3.1.0 + version: 3.1.0 + '@types/geojson': + specifier: ^7946.0.16 + version: 7946.0.16 '@types/leaflet': specifier: ^1.9.21 version: 1.9.21 @@ -156,6 +171,9 @@ importers: '@types/react-dom': specifier: ^19.0.0 version: 19.2.3(@types/react@19.2.14) + '@types/topojson-client': + specifier: ^3.1.5 + version: 3.1.5 '@vitejs/plugin-react': specifier: ^4.3.4 version: 4.7.0(vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0)) @@ -1619,6 +1637,9 @@ packages: '@types/d3-force@3.0.10': resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1642,6 +1663,12 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/topojson-client@3.1.5': + resolution: {integrity: sha512-C79rySTyPxnQNNguTZNI1Ct4D7IXgvyAs3p9HPecnl6mNrJ5+UhvGNYcZfpROYV2lMHI48kJPxwR+F9C6c7nmw==} + + '@types/topojson-specification@1.0.5': + resolution: {integrity: sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ==} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1898,6 +1925,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} @@ -1966,6 +1996,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + d3-dispatch@3.0.1: resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} engines: {node: '>=12'} @@ -1974,6 +2008,10 @@ packages: resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} engines: {node: '>=12'} + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + d3-quadtree@3.0.1: resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} engines: {node: '>=12'} @@ -2407,6 +2445,10 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -3422,6 +3464,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + topojson-client@3.1.0: + resolution: {integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==} + hasBin: true + tough-cookie@6.0.1: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} @@ -3677,6 +3723,9 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + world-atlas@2.0.2: + resolution: {integrity: sha512-IXfV0qwlKXpckz1FhwXVwKRjiIhOnWttOskm5CtxMsjgE/MXAYRHWJqgXOpM8IkcPBoXnyTU5lFHcYa5ChG0LQ==} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -5017,6 +5066,10 @@ snapshots: '@types/d3-force@3.0.10': {} + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + '@types/estree@1.0.8': {} '@types/geojson@7946.0.16': {} @@ -5037,6 +5090,15 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/topojson-client@3.1.5': + dependencies: + '@types/geojson': 7946.0.16 + '@types/topojson-specification': 1.0.5 + + '@types/topojson-specification@1.0.5': + dependencies: + '@types/geojson': 7946.0.16 + '@vitejs/plugin-react@4.7.0(vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0))': dependencies: '@babel/core': 7.29.0 @@ -5317,6 +5379,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + commander@2.20.3: {} + compare-func@2.0.0: dependencies: array-ify: 1.0.0 @@ -5386,6 +5450,10 @@ snapshots: csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + d3-dispatch@3.0.1: {} d3-force@3.0.0: @@ -5394,6 +5462,10 @@ snapshots: d3-quadtree: 3.0.1 d3-timer: 3.0.1 + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + d3-quadtree@3.0.1: {} d3-timer@3.0.1: {} @@ -5856,6 +5928,8 @@ snapshots: ini@1.3.8: {} + internmap@2.0.3: {} + is-arrayish@0.2.1: {} is-binary-path@2.1.0: @@ -6697,6 +6771,10 @@ snapshots: dependencies: is-number: 7.0.0 + topojson-client@3.1.0: + dependencies: + commander: 2.20.3 + tough-cookie@6.0.1: dependencies: tldts: 7.0.30 @@ -6888,6 +6966,8 @@ snapshots: wordwrap@1.0.0: {} + world-atlas@2.0.2: {} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 diff --git a/ordinis-admin-ui/src/styles.css b/ordinis-admin-ui/src/styles.css index 6f5d7b4..a99ad65 100644 --- a/ordinis-admin-ui/src/styles.css +++ b/ordinis-admin-ui/src/styles.css @@ -399,16 +399,13 @@ body { transform-box: fill-box; animation: ord-globe-pulse 2.4s ease-in-out infinite; } -.globe-loader .globe-sphere { - transform-origin: 100px 100px; - transform-box: fill-box; - animation: ord-globe-spin 14s linear infinite; -} +/* Sphere/land внутри globe-loader rotates через d3 projection (rAF + * пересчитывает path d-attr), не через CSS transform — иначе land + * distort'илась бы при rotation (no projection math). */ @media (prefers-reduced-motion: reduce) { .globe-loader .globe-whirl, .globe-loader .globe-whirl-rev, - .globe-loader .globe-pulse, - .globe-loader .globe-sphere { + .globe-loader .globe-pulse { animation: none; } } diff --git a/ordinis-admin-ui/src/ui/components/globe-loader.tsx b/ordinis-admin-ui/src/ui/components/globe-loader.tsx index 62ee49f..7b40eee 100644 --- a/ordinis-admin-ui/src/ui/components/globe-loader.tsx +++ b/ordinis-admin-ui/src/ui/components/globe-loader.tsx @@ -1,39 +1,123 @@ -import { useId as useReactId } from 'react' +import { useEffect, useId as useReactId, useRef, useState } from 'react' +import { geoOrthographic, geoPath, geoGraticule10 } from 'd3-geo' +import type { Feature, FeatureCollection, Geometry } from 'geojson' import { cn } from '@/lib/utils' /** - * GlobeLoader — orbital globe spinner per Downloads/Globe loader prototype. + * GlobeLoader — orbital globe spinner per /Users/zimin/Downloads/Globe loader + * prototype. Real Natural Earth land outline через d3-geo orthographic + * projection, rotating longitude continuously. Outer whirl arcs + pulsing + * ring CSS-only. * - * Чистый CSS/SVG (без d3 / topojson) — упрощённая sphere с graticule - * meridians + pulsing outer ring + два контр-вращающихся whirl arcs с tail - * gradient. Stylized в дизайн-системе: stroke=currentColor (берёт цвет ink - * из родителя — works на любом фоне, в т.ч. dark mode). + *
Lazy-loads world-atlas (~55kb) — пока геометрия не доехала, видны + * только sphere + graticule + orbital arcs (still looks like a loader). * - * Использование: + *
Использование:
*